
1. Domain ID 到底在管什么为什么 Gazebo 和 Autoware 会“失联”先说一个我经常遇到的现场一个装了 ROS 2 Humble 的开发机上面同时有 Gazebo、Autoware还有一个 micro-ROS 的 ESP32 板子。调试的时候Rviz2 里只剩下/clock、/rosout、/parameter_events这类固定话题Gazebo 里的小车怎么发指令都不动Autoware 的 planning 模块一直提示waiting for map。折腾了半天ros2 topic list打出来和其他终端完全不一样最后才发现是终端之间的ROS_DOMAIN_ID不一致。这类问题在混合开发环境里太常见了所以我把这套“一键清理 切换 Domain”的完整思路整理出来包括脚本、验证方式、坑位清单直接照着抄就行。1.1 从 DDS 发现协议说起为什么节点会互相看不见ROS 2 的底层通信不再是 ROS 1 那种中心化的 roscore而是直接跑在 DDSData Distribution Service之上。DDS 里有一个很关键的概念叫 Domain你可以把它理解成一个“虚拟局域网分区”Domain ID 就是这个分区的编号。节点启动时会加入某一个 Domain只有处在同一个 Domain 里的节点才能通过 DDS 的发现机制互相感知、互相通信。这个机制带来一个巨大的好处多个互不相关的机器人系统可以在同一张物理网络上并行运行而互不干扰。比如楼上机器人 A 用 Domain 0楼下机器人 B 用 Domain 1两边话题名字完全一样也没关系因为 DDS 的发现协议根本不会跨 Domain 广播。但这也带来了一个麻烦ROS 2 默认 Domain ID 是 0你手动设置了export ROS_DOMAIN_ID42但新开的终端没有同步设置于是这个终端里的节点就和其余终端里的节点“绝缘”了。Gazebo 启动的插件节点、Autoware 的 perception/planning 节点、micro-ROS Agent全都依赖这层 DDS 发现机制一旦 Domain 不一致现象就是“一切正常但互相找不到”。很多新人在排查时会去看话题名、看命名空间、看 QoS却忽略了最基础的 Domain 一致性。这就像两台对讲机频率不同内容再清晰对方也收不到。1.2 Domain 的生效范围与常见不一致来源ROS_DOMAIN_ID是一个环境变量它的生效范围是当前 shell 进程及其子进程。也就是说你在终端 A 里export ROS_DOMAIN_ID1终端 B 完全不受影响。这个“隔离性”是好事但也是混乱的根源。实际工作中Domain 不一致的来源通常有这几种手动在某些终端export ROS_DOMAIN_IDN之后新开终端忘了同步。多个项目共用一个~/.bashrc项目 A 的 source 脚本里设置了 Domain 1项目 B 的逻辑里又改了 Domain 2重启终端后环境变量时有时无。通过docker-compose或 systemd 服务启动的节点环境变量和手动终端不一致。多机器人场景下不同机器人确实应该用不同 Domain但开发者自己的操作终端和机器人终端混在一起一个走读错。注意一个细节ROS_DOMAIN_ID可以设置的范围是 0 到 232但实际可用的数量受限于 DDS 实现和网络拓扑。比如 Fast DDS 默认下同一个物理网络里 Domain 数量铺太开也可能出现端口冲突日常开发用 0、1、2 这类小数字就够了习惯上大家也喜欢用和项目代号相关的数字比如 201、42 这种。2. 一键清理的完整方案进程、日志、回环缓存“一键清理”不是简单 kill 掉几个进程而是要解决三类问题残留进程继续占用 DDS 发现端口、日志文件膨胀导致写满磁盘、ros2 daemon 缓存了旧的拓扑信息导致新节点加入时发现混乱。2.1 残留进程为什么必须清端口、共享内存和僵尸节点Gazebo 和 Autoware 这类重型软件退出时不一定能把自己启动的子进程全部收干净。比如gzserver意外崩溃但gazebo的 GUI 进程还活着Autoware 的多个 lifecycle 节点被 CtrlC 终止后component_container可能还绑定在某个端口上。这些残留进程最麻烦的地方不在于占 CPU而在于它们还“活”在 DDS 域里——如果它们还以旧节点名持续发布心跳ros2 node list会看到一堆幽灵节点而新启动的节点又因为节点名冲突加不上来。另外Fast DDS 在共享内存传输模式下会留下/dev/shm里的共享内存段进程异常退出后这些段不一定自动释放极端情况下会造成新节点启动时create participant error。所以清理第一步就是按关键词搜索并结束这些进程。我自己的清单一版是这样# 清理本地残留的ROS2/Gazebo/Autoware进程 for kw in gzserver gzclient gazebo rviz2 autoware component_container micro_ros_agent; do pkill -f $kw 2/dev/null done这里用pkill -f匹配完整命令行比直接按进程名杀更稳因为很多进程名带路径或者带参数按pkill gazebo不一定命中。但注意pkill -f有风险它会匹配你 shell 里正在执行的这段脚本自身如果脚本路径里含有关键词。所以脚本里我用了一个数组循环并且明确排除了自身路径。杀进程的顺序也有讲究优先发 SIGINTCtrlC 等价信号让节点执行生命周期回调等 2~3 秒再发 SIGTERM最后才考虑 SIGKILL。直接在脚本里写pkill -9确实简单粗暴但 Gazebo 容易留下临时文件Autoware 的 lifecycle 节点没有机会清理中间变量下次启动大概率会出幺蛾子。后来我把这套逻辑改成函数形式先pkill -INT -fsleep 2再pkill -TERM -f实在不行再pkill -KILL -f。2.2 日志清理与 daemon 重置ROS 2 的日志默认写在~/.ros/log包括ros2 launch的控制台输出、DDS 的调试文件。Autoware 还有自己的日志目录比如~/.autoware或~/.ros/autoware。跑几个小时的仿真日志轻松上几百 MB。清理时注意不要把正在运行的节点的日志目录删掉否则该节点后续写日志会报错。稳妥的做法是只清理 mtime 超过一定天数的文件find ~/.ros/log -type f -mtime 3 -delete find ~/.ros/log -type d -empty -delete然后是ros2 daemon的重置。ROS 2 CLI 工具ros2 node list、ros2 topic list依赖后台 daemon 维护一份节点和话题的缓存。它在长时间运行后会因为网络波动、节点反复启停而变得“记忆错乱”实际上底层 DDS 网络里已经没人了daemon 还是把旧节点列给你看。重置方式很简单ros2 daemon stop ros2 daemon start其实更深一层还可以直接杀掉ros2 daemon对应的 Python 进程不过ros2 daemon stop/start已经足够。清理脚本最后我会强制重启 daemon这样每次切完 Domain 后CLI 工具查询到的都是当前域的实时状态。3. 切换 Domain 的标准操作与脚本化清理只是把房间打扫干净切换 Domain 才是核心。这里我会拆成“临时切换”“永久切换”和“工程级切换”三个层次并给一个可直接落地的switch_domain.sh。3.1 临时切换与永久切换的正确姿势临时切换就是在当前终端里执行export ROS_DOMAIN_ID42然后启动的节点都会加入 Domain 42。这个方式只对当前终端有效适合快速验证。验证是否生效最直接的手段ros2 doctor --report输出里能看到ROS_DOMAIN_ID当前值也可以看DDS implementation、RMW implementation这些信息。永久切换则是把这行 export 写进~/.bashrc。这里有个隐藏坑~/.bashrc只对交互式终端生效你用ssh robothost ros2 launch xxx这类非交互方式执行命令时~/.bashrc不会加载此时 Domain 又回到默认值 0。所以嵌入式设备或者远程部署场景下更推荐写进 systemd service 的 Environment 字段或者在项目 launch 脚本里显式设置。3.2 一个可直接复用的 switch_domain.sh 脚本我在多个项目里反复改写的版本长这样注释里写清楚了每一步的意图#!/usr/bin/env bash # 用法: ./switch_domain.sh domain_id [--clean] set -euo pipefail TARGET_DOMAIN${1:?请指定目标 Domain ID} CLEAN_FLAGfalse if [[ ${2:-} --clean ]]; then CLEAN_FLAGtrue fi # 1. 是否要顺手清理环境 if [[ $CLEAN_FLAG true ]]; then echo [INFO] 清理残留进程... for kw in gzserver gzclient gazebo rviz2 autoware component_container micro_ros_agent; do pkill -INT -f $kw 2/dev/null || true done sleep 2 for kw in gzserver gzclient gazebo rviz2 autoware component_container micro_ros_agent; do pkill -TERM -f $kw 2/dev/null || true done fi # 2. 先重置 daemon避免旧缓存干扰 ros2 daemon stop || true sleep 1 # 3. 设置当前终端环境变量 export ROS_DOMAIN_ID$TARGET_DOMAIN echo [INFO] 已设置 ROS_DOMAIN_ID$ROS_DOMAIN_ID # 4. 重启 daemon让 CLI 在指定域下重新发现 ros2 daemon start || true sleep 2 # 5. 验证 echo [INFO] ros2 doctor 关键信息: ros2 doctor --report | grep -E ROS_DOMAIN_ID|DDS implementation|RMW implementation || true echo [INFO] 当前可用话题数: ros2 topic list | wc -l脚本里几个细节值得展开说一下。set -euo pipefail会让脚本在任何一条命令失败时立刻退出好处是能快速暴露问题。但注意ros2 daemon stop在原本没有 daemon 运行时可能返回非零状态所以我加了|| true防止脚本中断。ros2 daemon stop/start的顺序为什么放在 export 之后因为 daemon 会继承启动它的 shell 的ROS_DOMAIN_ID必须先设置好环境变量再启动 daemon否则 daemon 会停留在旧的 Domain 上。很多人只改 export 不打 daemon 重启结果ros2 topic list看到的还是旧数据就误以为切换失败。其实新节点之间通信大概率没问题只是 CLI 工具“骗”了你。3.3 多个项目的 Domain 隔离与规划如果你同时维护好几个项目建议把它们安排在不同 Domain并写进项目的.env或 source 脚本里。比如项目 A 统一用 Domain 101。项目 B 统一用 Domain 202。多机器人站点机器人 1 用 11机器人 2 用 12以此类推。这样不同机器人在同一局域网共享时即使话题名都是/map、/cmd_vel也不会相互干扰。更妙的是你可以同时打开两组终端各跑一套环境互不影响。这种“并行开发”能力是 ROS 2 相比 ROS 1 的巨大进步但前提是你把 Domain 规划清楚。需要注意Domain 隔离只管 DDS 通信管不了文件系统、命名空间和硬件资源。两个项目同时抢同一个摄像头驱动Domain 再不同也会冲突这不属于 Domain 的管辖范围。4. 踩坑实录与常见问题速查表下面这些问题是过去一年里我和同事们实际操作中反复遇到的多数都能用“清理 切换”流程解决。4.1 两个“Domain”别搞混DDS 域与 Web 域名拦截最近发现一个特别容易让人懵的现象在装某些 ROS 2 第三方工具链时终端里冒出{code:1004,error:domain forbidden}或unable to verify if domain is safe to fetch。第一次看到这个报错我下意识以为是ROS_DOMAIN_ID的问题在环境变量里折腾了半天。实际上这里的domain指的是 Web 访问层面的“域名安全验证”跟 ROS 2 的 DDS Domain ID 完全是两码事。它通常是代理、防火墙或特定的 URL 过滤策略在拦截请求。排查思路是检查当前 shell 里的http_proxy、https_proxy环境变量以及目标站点证书校验设置。切记unable to verify...这类网络报错不要扯到 ROS_DOMAIN_ID 上否则既浪费时间又找不到病根。4.2 清理脚本自身被 pkill 误杀这个坑我踩过写好的 cleanup 脚本一运行就“神秘消失”。原因是pkill -f gazebo这类命令匹配了当前正在执行的脚本文件路径——如果你的脚本放在/home/user/gazebo_clean.sh字符串里含gazebo那么pkill -f就会把自己也杀掉。解决方案有几种一是把脚本文件名改成与关键词完全无关比如env_manage.sh二是在匹配时加正则排除当前 PID三是用pgrep先打印匹配列表人工确认后再杀。我后来选了第二种写了一个通用的杀进程函数kill_by_keyword() { local kw$1 local mypid$$ pgrep -f $kw | while read -r p; do if [[ $p ! $mypid ]]; then kill -INT $p 2/dev/null || true fi done }4.3Delete domain security policies安全配置文件导致的白名单拦截菜鸟级操作里还有一个常被提到的问题执行ros2 security相关命令后后续节点无法加入域报错里出现delete domain security policies。这通常是因为你之前生成了带权限控制的安全策略governance.xml / permissions.xml后来又手动修改了 Domain但没有同步刷新安全文件。应对方式很简单如果项目不需要 DDS 安全机制直接删除~/.ros/security下对应 Domain 的目录重新配置如果需要安全策略那么每次切换 Domain 后都要保证 domID 目录下的 governance/permissions 文件和当前配置一致。别问我怎么知道的第一次遇到时我差点把整个~/.ros删了才解决。4.4 常见问题速查表老规矩整理一份速查表方便直接对照排查现象可能原因解决措施ros2 topic list和其他终端不一致终端间ROS_DOMAIN_ID不一致统一 export重启 daemonGazebo 无人车不动/cmd_vel无数据节点不在同一 Domain或未启用use_sim_time切换到同一 Domainlaunch 中检查时钟同步ros2 node list出现大量幽灵节点残留进程未清理干净按关键词杀进程重启 daemon节点启动时create participant error共享内存段或端口被残留进程占用清理/dev/shm相关段重启系统或者手动释放ros2 doctor显示 domain 异常.bashrc里多个 source 脚本互相覆盖重写环境加载逻辑统一入口micro-ROS Agent 连不上 ESP32Agent 所在终端的 Domain 与主系统不一致ESP32 代码里set_domain_id()和 Agent 侧保持相同远程 ssh 执行命令时 Domain 不对ssh 非交互模式不加载.bashrc改用 systemd service 或在命令前显式 export找不到 Gazebo 最新版下载软件源未更新或未配置校对源按官方文档配置源apt 换镜像后更新索引unable to verify if domain is safe...网络代理/证书校验问题检查http_proxy、https_proxy更新 CA 证书这个表格写下来基本就是我把脑袋里那些“早知道就好了”的经验浓缩成了几行字。特别是 clock 同步那一条Gazebo 里小车不动往往不是 Domain 问题而是你启动 Rviz 或 Autoware 时没设use_sim_time导致节点用的是真实时间而不是仿真时间哪怕话题通了也感觉像死机。5. 进阶实践把清理与切换做成日常开发流程如果上面的单条命令已经能解决 80% 的问题剩下的 20% 要靠流程化。我现在会把清理、切换、验证合并成一个env_manage.sh并为它设计三个子命令clean、switch、doctor。5.1 一个通用的 env_manage.sh 骨架#!/usr/bin/env bash # 环境管理clean / switch / doctor set -euo pipefail CMD${1:-doctor} case $CMD in clean) # 清理进程、日志、临时文件 for kw in gzserver gzclient gazebo rviz2 autoware component_container; do pkill -INT -f $kw 2/dev/null || true done sleep 2 find ~/.ros/log -type f -mtime 2 -delete 2/dev/null || true ros2 daemon stop || true echo [OK] 环境已清理 ;; switch) DOMAIN${2:?用法: env_manage.sh switch domain_id} export ROS_DOMAIN_ID$DOMAIN ros2 daemon start echo [OK] 已切换至 Domain $DOMAIN ;; doctor) echo ROS_DOMAIN_ID: ${ROS_DOMAIN_ID:-0} echo RMW_IMPLEMENTATION: ${RMW_IMPLEMENTATION:-rmw_fastrtps_cpp} ros2 doctor --report ;; *) echo 用法: $0 {clean|switch domain_id|doctor} exit 1 ;; esac这个骨架已经反复用在工作站和开发板上了稳定可靠。你也可以再加archive子命令清理日志时不要 delete而是按时间戳归档到~/.ros/log_archive/这样可以保留现场用于后续问题回溯。生产环境里这个习惯尤其重要因为你删掉的日志可能刚好是复现某个偶发 bug 的关键线索。5.2 多机协同下的 Domain 规划多机器人的场景下我习惯给每台机器人固定一个 Domain ID然后在每台机器人的/etc/profile.d/ros_domain.sh里写入对应值。这样无论是本地终端还是 ssh 登录只要 shell 是 login shell都会自动加载统一的 Domain。注意是/etc/profile.d/而不是~/.bashrc区别在于前者对所有用户生效适合团队共用设备。同时每台机器人记录自己的 Domain 到一个固定文件比如/etc/robot_domain脚本里读取这个文件作为默认值DOMAIN_ID$(cat /etc/robot_domain 2/dev/null || echo 0) export ROS_DOMAIN_ID$DOMAIN_ID这套方案在多机协作的建图、编队测试中非常省心。不同机器人的话题完全隔离之后单机调试时甚至可以同时跑两套仿真数据不会互相污染。5.3 与 micro-ROS / ESP32 的联动如果你在 ESP32 上用micro_ros_espidf_component搭节点那清理与切换的联动更要注意。micro-ROS 节点不是从 shell 启动的它烧录在固件里Domain ID 是在代码里配置的rmw_uros_set_custom_mid(0); // 设置 domain ID static uint8_t domain_id 101; // 与 ROS 2 主机保持一致如果 Agent 跑在 Domain 101而 ESP32 固件里写的 Domain 0两边永远连接不上。排查时不要只看串口日志还要确认micro-ROS Agent启动时的环境变量。很多时候 Agent 是在某个终端里带export ROS_DOMAIN_ID101启动的而 HOST 端主程序跑在 Domain 0看起来一切都“正常”实际上跨域了。我后期在 Agent 的启动脚本里加了强制校验启动前读取一个agent.env文件把ROS_DOMAIN_ID、RMW_IMPLEMENTATION、UDP port一次性写入环境再启动 Agent从源头上杜绝“手动 export 忘了改”的人为失误。写在最后脚本和命令都是工具真正值钱的是一套排查问题的思路先确认 Domain 是否一致再清理残留进程然后验证 daemon 状态最后才去怀疑代码逻辑。这个顺序能帮你省下大量无意义的调试时间。我个人在实际项目里最大的体会是Domain 切换这个操作很容易被低估但它直接决定了一套 ROS 2 系统能不能“被多个人、多台机器、多个工具链复用”。一个干净的环境管理脚本顶得上群里的十次远程求助。最后再分享一个小技巧在~/.bashrc末尾加上一行export PS1[ROS_DOMAIN${ROS_DOMAIN_ID:-0}] $PS1让终端提示符直接显示当前 Domain。这样哪怕你同时开了八个终端也能一眼看出哪个终端的环境变量跑偏了。你一旦用习惯就再也不想回到“一个个终端去 echo $ROS_DOMAIN_ID”的原始查法了。