一 初识
ROS无法调度协调,且通信开销大,耗资源。百度自动驾驶团队开发了Cyber RT。
CyberRT从下到上依次为:
基础库:高性能,无锁队列;
通信层:Publish/Subscribe机制,Service/Client机制,服务自发现,自适应的通信机制(共享内存、Socket、进程内);
数据层:数据缓存与融合。多路传感器之间数据需要融合,而且算法可能需要缓存一定的数据。比如典型的仿真应用,不同算法模块之间需要有一个数据桥梁,数据层起到了这个模块间通信的桥梁的作用;
计算层:计算模型,任务以及任务调度;
此外,相比Ros,CyberRT增加了Component组件,组件之间通过 Cyber channel 通信。Cyber RT 中用Message实现模块间通信,其实现基于 protobuf。同时,CyberRT也支持异步计算任务,优化线程使用与系统资源分配,同时支持定义模块拓扑结构的配置文件。
但是CyberRT也存在用户经验少的短板,同时资源也没有Ros全面。
二 组件
早期,没有组件,使用一个公共基类。所有模块都继承此基类。
Component 是 Cyber RT提供的用来构建功能模块的基础类,可以理解为Cyber RT对算法功能模块的封装,配合Component对应的DAG文件,Cyber RT可实现对该功能模块的动态加载。以Apollo为例, Apollo 中所有的模块都由 Component 构建的。
被 Cyber RT加载的 Component 构成了一张去中心化的网络。每一个Component是一个算法功能模块,其依赖关系由对应的配置文件定义,而一个系统是由多个component组成,各个component由其相互间的依赖关系连接在一起,构成一张计算图。
下图是具有3个component,2个channel的简单网络:
2.1 Component 的优点
相较于在 main() 函数中写通信逻辑并编译为单独的可执行文件的方法,Component 有以下优点:
- 可以通过配置 launch 文件加载到不同进程中,可以弹性部署。
- 可以通过配置 DAG 文件来修改其中的参数配置,调度策略,Channel 名称。
- 可以接收多个种类的消息,并有多种消息融合策略。
- 接口简单,并且可以被 Cyber 框架动态地加载,更加灵活易用。
2.2 组件启动
在 Cyber RT中,所有的 Comopnent 都会被编译成独立的.so文件,Cyber RT 会根据开发者提供的配置文件,按需加载对应的 Component。所以,开发者需要为.so文件编写好配置文.dag文件和.launch文件,以供 Cyber RT正确的加载执行Component。
Cyber RT提供两种加载启动Component的方式,分别是使用cyber_launch工具启动component对应的launch文件,和使用mainboard启动component对应的dag文件。
cyber_launch工具可以启动dag文件和二进制文件,而mainboard执行启动dag文件。
一个dag文件对应一个进程,接着根据dag文件中有几个components,生成几个协程,每个components对应一个协程。每个components对应一个dag文件,而一个dag文件则对应一个或者多个components。
对于配置文件中已经编排好的任务,其拓扑结构就依据优先级确定了。我们根据上面的 conf 文档,可以简单画出任务的优先级拓扑情况,如下图,A、B、C、D 任务在第一个 group 中执行,E在第二个 group 中执行,对于没有出现在配置中的任务,比如F默认会放到第一个 group 中执行。 而且配置中我们对于任务进行了优先级设置,A、B、C、D 的任务优先级依次增大,正好对应下图的拓扑依赖关系,在链路中越靠后的任务优先级越高。其实,数据也是这样在任务拓扑图中传递,数据走到最后,执行的任务优先级越高,这是为保证整个流程可以快速走完,不被其他流程的任务打断。
分组的原理及意义?
2.3 创建及如何工作
- 组件类:通用的组件处理(消息触发)
- 定时组件类:带了定时触发的属性
- Init():初始化函数可以用来加载配置文件,实例化对象等。
- Proc():处理数据的逻辑
1、包含头文件;
2、定义一个类,并继承Component或者time Component;根据Component功能需要,选择继承Component或者继承TimeComponent。
3、重写Init()和Proc()函数;Init()函数在 Component 被加载的时候执行,用来对Component进行初始化,如Node创建,Node Reader创建,Node Writer创建等等;Proc()函数是实现该Component功能的核心函数,其中实现了该Component的核心逻辑功能。
4、在Cyber RT中注册该Component,只有在Cyber RT中注册了该Component,Cyber RT才能对其进行动态的加载,否则,cyber RT动态加载时报错。
Component 的通信是基于 Channel 通信实现的,使用 reader 和 writer 对 channel 读写实现数据读取与写出。
组件使用的详细教程,可参考如下博客。
Apollo Cyber RT学习日记 (一)_// 这里只是例子,实际上用的是`artracker`的`autosync`属性。 // 但也是一个-CSDN博客
三 调度
3.1 实现原理
- M,Machine,表示系统级线程,goroutine 是跑在 M 上的。线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。
- P,processor,是 goroutine 执行所必须的上下文环境,可以理解为协程处理器,是用来执行 goroutine 的。processor 维护着可运行的 goroutine 队列,里面存储着所有需要它来执行的 goroutine。
- G,goroutine,协程。
支持优先级抢占。
M是进程。
3.2 调度策略
Apollo 提供了两种调度策略,一种是 classic 策略,在代码中,用 SchedulerClassic 类实现;另一种是 Choreography 策略,代码中用 SchedulerChoreography 类实现。
- classic 策略
- 较为通用的调度策略
- 如果对当前自动驾驶车辆上的 DAG 结构不清楚,建议使用此策略
- 相关协程任务以组为单位与线程作绑定
- SchedulerClassic 采用了协程池的概念,协程不会绑定到具体的 Processor,而是放在全局的优先级队列中。Processor 运行时,每次从最高优先级的任务开始调度执行。
- choreography 策略
- 需要对车上的任务、结构足够熟悉
- 根据任务的执行依赖关系、任务的执行时长、任务 CPU 消耗情况、消息频率等,对某些任务进行编排
- SchedulerChoreography 类采用了本地队列和全局队列相结合的方式。他将主链路(choreography开头的配置)进行编排;而对非主链路的任务放到线程池中使用 classic 策略执行。
当使用 choreography 策略时,具体该怎么办?Well,根据任务优先级、执行时长、频率与调度之间的关系,任务编排有如下几个依据(经验):
- 在同一个路径上的任务尽量编排在同一个 Processor 中,如果 Processor 负载过高,可考虑将部分任务拆分到其他 Processor
- 为防止优先级倒挂,同一个路径上的任务从开始到结束,优先级应逐级升高
- 不同路径上的任务尽量不混排
- 高频且短耗时任务尽量编排在同一个 Processor 上
调度系统的详细分析,请参考如下博客。
Apollo Cyber RT 调度系统 - 峰子的乐园 (dingfen.github.io)
四 通信
4.1 Node
节点,CyberRT的另一个基础构件;每个模块都包含节点,它能够基于信道,服务等功能,与其他节点进行通信。各个节点之间进行通信即可形成拓扑关系。并完成指定任务。
4.2 Channel
信道,在 Cyber RT 中,若需要完成节点之间的通信,则需要建立一条信息传输通道,这被称为信道。节点可以将信息发送进入某一指定的信道之中,若有其他节点定义接口接收此信道消息,则可完成消息收发过程。若没有,则消息也依然存在于信道之中。
4.3 Reader/Writer
若需要完成基于信道的通信,首先需要定义消息的发送方(Writer)和接收方(Reader),以保证消息可以通过 Writer 和 Reader 共同指定的 Channel ,从一个节点传输到另一个节点。这类通信方式称之为基于信道的通信(也成发布—订阅通信),有如下特点: - 同一个节点可以同时发送多条消息,也可以同时接收多条消息,即可以同时定义多个 Writer 和 Reader - 基于信道的通信是一种单向通信,消息只能由 Writer 传输到 Reader,而不能够反向传输 - 信道中的消息不需要实时应答,也就是说,当某一条消息通过 Writer 送入 Channel 后,可以没有 Reader 来读取消息。当某一个 Reader 想要读取 Channel 中的信息时,Channel 中也许并没有消息输入。
4.4 Service/client
节点间双向通信,对服务发出请求时,客户端节点将收到响应。
4.5 传输原理
- INTRA:如果是同进程的,因为在同一地址空间,直接传指针就完了。
- SHM(Shared memory):如果是同一机器上,但跨进程的,为了高效可以使用共享内存。
- RTPS:如果是跨设备的,那就老老实实通过网络传吧。 - HYBRID: 框架需要根据节点间关系选择合适的传输后端。
五 插件
六 总结
模块化的component,分布式部署(动态化加载),标准化的通信,灵活的调度,且component还支持插件。
易于部署,灵活,好维护。