Apollo2 + ros1的中间件总结
Ros1
核心通信框架
(ps:rpc基于开源的 https://xmlrpc-c.sourceforge.net/ 改的)
- Pub进程向Master注册Pub的RpcServer
- Sub进程向Master注册Sub的RpcServer
- 查看是否存在符合Topic的Sub,符合后立即进行Rpc连接(异步调用Sub进程的publisherUpdate),这个步骤告诉Sub发送这个Topic的Pub进程的RpcServer Uri是多少; 从这步开始也叫服务发现,只不过依赖Master
- Sub端通过上面得到的Pub的uri主动发起requestTopic异步函数调用
- Pub端告诉Sub,Tcp传输Server的ip(那个listen的)
- 常规的socket通信了,Sub主动connect,从而建立通信
- Pub通过socket write开始传输数据
缺点分析
- 任何新的Sub和Pub都依赖Master来注册Rpc的Uri,一旦Master没了就什么都没了 ---> 备份Master/不依赖Master
- 多次拷贝的通信
上面可以知道本质传递消息走的就是socket通信,那么当一个节点被多个节点订阅,这个传输时间是线性增长的!有几个sub就要拷贝几次
优化方式:组播,Sub端只需要拷贝一次 - 多次拷贝
Pub --》Serialize --》Copy TcpBuffer
Sub --》 Copy TcpBuffer --》Deserialize
Apollo1的改进
- 加入FastRtps库,替换调Master,我这边直接给出最大差别:
内部核心还是差不多,但是这个前期Rpc的ip地址发现,改用rtps库来拓扑发现,用百度的图来看下组合:
再次强调,这个步骤不是具体的数据传输,只是封装了Topic、DataType让其他节点知道这个的存在,然后继续和Ros1一样走RPC的远程调用连接 - 使用自定义共享内存来IPC传输(你问为什么不直接用fastrtps的?这个时候fastrtps还不支持Shm,百度就自己搞了一个)
核心技术为:Boost的Shared Memory(https://theboostcpplibraries.com/boost.interprocess-shared-memory)
百度基于这个自定义整套的结构,大体共享内存组成为:
对象的创建就是在内存上,平时我们可能在堆/栈,对于Shm也是一片内存,同样可以直接在这个内存上创建对象!
这里可以看见Block是是实际传输的最小节点,每一个Block都有一把锁,这也同时减少了锁的颗粒度,不需要一把锁控制整个Segment
缺点:Apollo的Shm实现可能存在跳帧,因为每次读端的Read取的index都是最新的Block_index
看一下整体给出的对比(借用百度的图):
说明:依然存在序列化和反序列化,除了这个外没有拷贝。也就是说这种方式不是零拷贝,但是我认为优于普通的Shm,因为一般使用Shm都是先把数据拷贝到共享内存,再进行拷出,只是为了不走内核
- Protobuffer替换msg
这个就是纯纯的技术替换了,考虑数据的兼容性直接引入了这个
额外想说的是这套框架上很快能加入Protobuffer,原因是对于代码中对于Write的抽象:
这样对于Protobuffer只需要额外写一个Wrapper就可以了
数据的接收
习惯了Ros的接收方式的同学,再用其他中间件可能会有习惯,主要就是因为Ros目前是支持异步轮训多次和一次的原因,AutoSarap Cm文档也说过好的中间件应该是支持异步会调(只在数据更新的时候触发)和同步轮询(类似于自旋,不让Cpu进行上下文切换)
这个时候仅仅是把数据放到全局queue中
如果客户端主动进行一次spin就会将已经存在的数据都进行触发回调
提醒:
- 默认都是一个GlobaQueue,如果接收多种消息,A消息的回调慢可能对B消息丢帧和及时性有影响 --》可以考虑自定义Queue
- Pub接收数据和回调用的锁是一把锁,回调做了超过队列 * 帧率的事情就会导致丢帧 --> 比如GAC目前PC的解码程序
调测工具 : hz echo
- 这个工具的最大难点就是怎么通过Topic + DataType来反射到具体类型
- ros通过本地环境 + python语法糖 :
msg通过生成工具会生成python的代码
然后使用python的__import__('%s.%s' % (package, type_str))导入对应py模块
然后getattr(getattr(pypkg, type_str), base_type)就可以获得字符串对应的类了 - Apollo怎么做的?
Protobuffer直接支持反射,可以通过proto进行反射出类型 - senseloto怎么做的?
使用Idl包裹 + 动态库加载方式,非常巧妙和适合 - mdc怎么做的?
以上的方式几乎都依赖于对应消息的so,对于华为,自定义了Msg到类的动态加载方式,可以仅仅替换Idl就可以直接序列化对应的消息
唯一的小缺点:需要作为底软来考虑动态Idl->类的统一方式,否则会有数据不对齐风险
一个圆,圆内是你会的,圆外是你不知道的。而当圆越大,你知道的越多,不知道的也越多了