Gazebo ROS Control 插件偶发性加载失败:一个隐蔽的竞争条件
问题描述
在 ROS Noetic + Gazebo 仿真环境中,为四足机械臂(Go1 + Z1)的 URDF 模型添加深度相机(libgazebo_ros_openni_kinect.so)后,自定义的 gazebo_ros_control 插件偶发性无法加载。
具体表现:
controller_manager服务不存在controller_spawner报错:Controller Spawner couldn't find the expected controller_manager ROS interface.rostopic echo /joint_states无数据- 添加相机之前一切正常,添加之后偶发失败
排查过程
1. 确认插件卡在哪里
查看 ~/.ros/log/latest/rosout.log,发现 gazebo_ros_control 的日志停在了:
gazebo_ros_control plugin is waiting for model URDF in parameter [robot_description] on the ROS param server.
之后没有任何后续日志 ------initSim 从未被调用。
但手动检查参数服务器,参数确实存在:
bash
rosparam get /robot_description | head -5
# 正常返回 URDF 内容,长度约 58KB
2. 确认是 deferred thread 卡死
gazebo_ros_control 插件的 Load() 函数会创建一个 detached 的 deferred_load_thread_,在后台线程中执行 getURDF() → parseTransmissions() → initSim() → 创建 ControllerManager。这允许其他 Gazebo 插件(如 ft_sensor)在此期间继续加载。
通过检查 gzserver 进程的线程状态:
bash
cat /proc/<gzserver_pid>/task/<thread_id>/wchan
# 输出: hrtimer_nanosleep
有一个线程持续处于 hrtimer_nanosleep 状态------这正是 getURDF() 函数中每 100ms 轮询参数的 sleep 循环。说明这个线程永远找不到参数,但参数明明存在。
3. 在 ROS Master 日志中找到根本原因
关键突破来自 ~/.ros/log/latest/master.log:
[rosmaster.master][ERROR] 2026-04-13 22:17:13,397: Traceback (most recent call last):
File ".../rosmaster/master_api.py", line 435, in searchParam
search_key = self.param_server.search_param(caller_id, key)
File ".../rosmaster/paramserver.py", line 116, in search_param
raise ValueError("namespace must be global")
ValueError: namespace must be global
而这个错误的上下文是相机插件正在密集注册参数:
22:17:13,395: +PUB [/camera/rgb/image_raw/compressed] /gazebo
22:17:13,396: +SERVICE [/camera/rgb/image_raw/compressed/set_parameters] /gazebo
22:17:13,397: [ERROR] ValueError: namespace must be global <-- searchParam 失败!
22:17:13,397: +PUB [/camera/rgb/image_raw/compressed/parameter_descriptions] /gazebo
22:17:13,398: +PUB [/camera/rgb/image_raw/compressed/parameter_updates] /gazebo
根本原因
gazebo_ros_control 插件的 getURDF() 函数调用 model_nh_.searchParam("robot_description", ...) 来查找 URDF 参数。
同时,深度相机插件 libgazebo_ros_openni_kinect.so 的 deferred LoadThread 在启动时会向 ROS Master 密集注册大量参数和 topic(compressedDepth、compressed、theora 等 image_transport 相关的参数)。
当两个线程同时 向 ROS Master 发送 XML-RPC 请求时,触发了 ROS Master paramserver.py 中的一个 bug:search_param 函数收到了格式异常的 namespace 参数,抛出 ValueError: namespace must be global。
这导致:
searchParam返回失败getURDF认为参数不存在,继续循环等待- 由于代码中使用了
ROS_INFO_ONCE,后续循环不再打印任何日志 - 表面上看起来像是"静默卡死"
偶发性 的原因:只有当两个线程的 XML-RPC 请求恰好在 ROS Master 中交错时才会触发。如果 gazebo_ros_control 在相机插件的参数注册风暴开始之前就完成了 searchParam,则一切正常。
修复方案
将 gazebo.xacro 中 <robotParam> 的值从相对路径 改为全局路径 (加前导 /):
xml
<!-- 修复前 -->
<robotParam>robot_description</robotParam>
<!-- 修复后 -->
<robotParam>/robot_description</robotParam>
当参数名以 / 开头时,searchParam 不需要沿 namespace 层级向上搜索,直接匹配全局参数,避免了触发 ROS Master 中的并发 bug。
诊断方法速查
如果你遇到了类似的 gazebo_ros_control 加载失败问题,可以按以下步骤排查:
bash
# 1. 检查 rosout 日志中是否卡在 "waiting for model URDF"
grep "waiting for model URDF" ~/.ros/log/latest/rosout.log
# 2. 检查 controller_manager 服务是否存在
rosservice list | grep controller_manager/load_controller
# 3. 关键:检查 ROS Master 日志中是否有 searchParam 错误
grep "namespace must be global" ~/.ros/log/latest/master.log
# 4. 检查 gzserver 线程状态,确认是否有线程卡在 sleep 循环
for tid in $(ls /proc/$(pgrep -f "gzserver.*ode")/task/); do
wchan=$(cat /proc/$(pgrep -f "gzserver.*ode")/task/$tid/wchan 2>/dev/null)
[ "$wchan" = "hrtimer_nanosleep" ] && echo "Thread $tid stuck in nanosleep"
done
适用环境
- ROS Noetic (Ubuntu 20.04)
- Gazebo 11
- ros-noetic-gazebo-ros-control 2.9.3
- 任何同时使用
gazebo_ros_control和libgazebo_ros_openni_kinect.so(或其他会密集注册参数的 Gazebo 插件)的场景