## 节点到参与者的映射 *本文分析了在 ROS 节点与 DDS 参与者之间强制实现一对一映射的性能影响,并提出了替代的实现方法。* 作者: [Ivan Paunovic](https://github.com/ivanpauno) 翻译:[Akana-kunama ](https://github.com/Akana-kunama) 写日期:2020-06 上次修改时间:2020 年 7 月 ### 背景 #### 节点 在 ROS 中,Node 是用于对其他实体进行分组的实体。 例如:发布者(Publishers)、订阅(Subscriptions)、服务器(Servers)、客户端(Clients)。 节点简化了组织和代码重用,因为它们可以以不同的方式组合。 #### 域参与者 参与者是一种 DDS 实体。 参与者还对其他实体进行分组,如发布者、订阅者、数据写入者、数据读取者等。 创建更多 Participants 会增加应用程序的开销: - 每个参与者都参与发现。 创建多个 Participant 通常会增加 CPU 使用率和网络 IO 负载。 - 每个参与者都会跟踪其他 DDS 实体。 在单个进程中使用多个可能会导致数据重复。 - 每个 Participant 都可以创建多个线程来处理事件、发现等。 每个参与者创建的线程数取决于 DDS 供应商(例如:[RTI Connext](https://community.rti.com/best-practices/create-few-domainParticipants-possible))。 因此,参与者是一个较为“重量级”的实体。 注意:这实际上可能取决于 DDS 供应商,其中一些供应商在参与者之间共享这些资源。 但是,许多 DDS 供应商不执行此类优化,并且实际上建议每个进程只创建一个参与者。`OpenSplice``RTI Connext``Fast-RTPS` #### 上下文 在 ROS 中,Context 表示初始化-关闭周期的非全局状态。它还封装了节点和其他实体之间共享的状态。大多数应用程序中,一个进程通常只有一个 ROS Context。 ### Foxy 之前的行为 节点和 DDS 参与者之间存在一对一的映射。 这简化了最初的实现,因为 DDS 参与者提供了许多与 ROS 节点相当的功能。 这种方法的缺点是创建许多 Participant 会带来开销。 此外,DDS 域参与者的最大数量非常有限。 例如,在 [RTI Connext](https://community.rti.com/kb/what-maximum-number-Participants-domain) 中,每个域限制为 120 个参与者。 ### 建议的方案 此提案的目标是通过避免每个节点创建一个 Participant 来提高整体性能。 如果可能,将避免 API 更改。 #### DDS 参与者到 ROS 实体的映射 除了 Foxy 之前使用的一对一节点到参与者映射外,还有两种选择: - 每个进程使用一个参与者。 - 每个 Context 使用一个参与者。 第二种方法更灵活,允许在单个应用程序中使用多个参与者,例如用于域桥应用程序。因此,选择了一对一的参与者与 Context 映射。 当多个节点在同一进程中运行时,可以通过多种方式对它们进行分组,例如为每个节点创建一个独立的 Context,将几个节点分组到同一个 Context 中,或对所有节点使用单一 Context。对于大多数应用程序,通常只创建一个 Context。 #### 发现信息 如果未使用一对一的节点到参与者映射,则需要额外的发现信息才能将其他实体与节点匹配,例如发布者、订阅等。 可以使用多种方法来共享此信息。 建议的方法使用了一个主题。 每个 Participant 都会发布一条消息,其中包含将实体与 Node 匹配所需的所有信息。 消息结构如下: - 参与者信息 - GID: 全局唯一标识符 - 节点信息 - Node 命名空间 - 节点名称 - Reader gid - 写入 GID 当一个实体更新时(例如:创建或销毁 Publisher),将发送一条新消息。 客户端和服务器根据其主题名称的 ROS 约定进行标识(请参阅[主题和服务名称到 DDS 的映射](https://design.ros2.org/articles/140_topic_and_service_name_mapping.md))。 本主题被视为实现详细信息,并非所有实现都必须使用它。 因此,所有必要的 logic 都必须在 rmw implementation 本身或 upstream package 中。 实现此 logic 会使其成为 API 的一部分,而不是实现细节。`rmw``rcl` 为避免代码重复,[rmw_dds_common](https://github.com/ros2/rmw_dds_common/) 包提供了此逻辑的常见实现。 ##### ROS 发现主题的详细信息 - 主题名称:`ros_discovery_info` - Writer QoS: - 耐久性:瞬态局部 - 历史记录:保留最后一条 - 历史深度: 1 - 可靠性: 可靠 - Reader QoS: - 耐久性:瞬态局部 - 历史记录:保留所有 - 历史深度: 1 - 可靠性: 可靠 ### 其他影响 #### 安全 以前,每个 Node 可以具有不同的安全构件。 这是可能的,因为每个 Node 都映射到一个 Participant。 新方法允许为每个进程指定不同的安全工件。 有关更多详细信息,请参阅 [ROS 2 安全区域](https://design.ros2.org/articles/ros2_security_enclaves.html)。 #### Ignore local publications 选项 [在创建 Subscription](https://github.com/ros2/rmw/blob/2250b3eee645d90f9e9d6c96d71ce3aada9944f3/rmw/include/rmw/rmw.h#L517) 时,可以设置一个选项。 该选项可避免从同一 Node 内的发布者接收消息。 这并未在所有 rmw 实现中实现(例如:[FastRTPS](https://github.com/ros2/rmw_fastrtps/blob/099f9eed9a0f581447405fbd877c6d3b15f1f26e/rmw_fastrtps_cpp/src/rmw_Subscription.cpp#L118))。`ignore_local_publications` 在此更改之后,实现此功能将不那么直接。 需要添加一些额外的逻辑,以便识别从哪个节点创建 Publisher。 ### 替代实现 #### 使用键控主题 可以使用键控主题,将参与者的 `gid` 作为键值。这将允许主题的读取端使用“保留最后一条,深度为 1”的历史记录策略。 #### 使用参与者/写入者/读取者的 `userData` QoS 与其使用自定义主题共享 ROS 特定的发现信息,还可以使用 `Participant`、`Reader` 和 `Writer` 的 `userData` QoS 组合: - **Participant `userData`**: 由参与者创建的节点列表(当节点被创建或销毁时更新)。 - **Reader `userData`**: 节点名称/命名空间。 - **Writer `userData`**: 节点名称/命名空间。 这些信息足以满足 ROS 的图形 API 要求。这种机制还可以避免所有节点必须读写同一主题,从安全角度看这是更好的选择。 未采用该替代方案的原因是目前某些支持的 DDS 供应商尚未完全支持这一机制。 ### 后续工作 该优化已应用于 `rmw_cyclonedds`、`rmw_fastrtps_cpp` 和 `rmw_fastrtps_dynamic_cpp`。其他基于 DDS 的实现(例如 `rmw_connext_cpp`)也可以采用相同的方法。