Skip to content

诊断监控与故障定位 ​

在知识图谱中的位置:模块四 · 04_工程实践 · 第 3 节 难度:⭐⭐⭐ | 前置知识:模块一·基础概念(话题/QoS/TF)、01_ROS2应用架构设计模式.md(错误隔离)、02_ROS2测试体系.md(QoS 校验)


1. 概述 ​

机器人在真机上出问题,往往是"传感器有数据、节点在跑、但行为不对"这类软故障,而不是崩溃。ROS 2 崩溃会留下 core 和日志,而软故障只会留下"话题没数据 / 频率不对 / TF 过期"这些症状。本节讲三件事:① 如何把系统"变得可观测"(日志、诊断、可视化三件套);② 典型故障的定位 playbook(话题无数据、节点卡死、TF 过期);③ 性能剖析与生命周期看门狗。

核心心法:先看拓扑(有没有)→ 再看契约(对不对)→ 最后看数据流(通不通),这个顺序能解决 80% 的 ROS 2 故障。


2. 核心概念 ​

2.1 可观测三件套 ​

维度工具回答的问题
日志(Logs)RCLCPP_* 宏 + logger 层级 + rqt_console节点内部发生了什么
诊断(Diagnostics)diagnostic_updater / diagnostic_aggregator子系统"健不健康"
可视化(Introspection)rqt_graph / rqt_plot / ros2 topic hz数据在系统里怎么流动

三者互补:日志是"事后取证",诊断是"实时健康状态",可视化是"运行时拓扑与流量"。

2.2 日志宏族与 logger 层级 ​

ROS 2 日志基于 rcutils,按严重级别分级(DEBUG < INFO < WARN < ERROR < FATAL),每个 logger 有自己的名字和级别阈值,低于阈值的日志被丢弃。logger 名用点号层级组织,继承父级设置:

cpp
auto logger = this->get_logger();               // logger 名 = 节点名,如 "nav_controller"
auto child  = this->get_logger().get_child("odom"); // 子 logger = "nav_controller.odom"
RCLCPP_INFO(logger, "pose received: x=%.2f", msg.x);
RCLCPP_WARN(child, "odom covariance too large");
RCLCPP_ERROR(logger, "planning failed: %s", e.what());

2.3 diagnostics 包:主动健康上报 ​

diagnostic_updater(https://index.ros.org/p/diagnostic_updater/,源码 https://github.com/ros/diagnostics)让节点周期上报自己的健康状态,而不是等上层来猜。节点把自己的关键指标(CPU、内存、传感器帧率、电池、温度)打包成 diagnostic_msgs/DiagnosticStatus(OK / WARN / ERROR / STALE),发布到 /diagnostics。diagnostic_aggregator 再把多个节点的状态聚合成一张整机健康图,供状态机/看门狗/仪表盘消费。

2.4 故障定位的"三层顺序" ​

① 拓扑层(实体存在吗)→ ros2 node list / ros2 topic list / rqt_graph;② 契约层(QoS/类型/命名对不对)→ ros2 topic info --verbose;③ 数据流层(数据真的在流吗)→ ros2 topic echo/hz/bw。绝大多数故障死在①和②,根本到不了"算法逻辑"。


3. 技术原理/方法 ​

3.1 日志:级别控制与环境变量 ​

  • 运行时设级别:ros2 run my_pkg my_node --ros-args --log-level DEBUG(只影响该节点)。
  • per-logger 设级别:代码里 logger.set_level(rclcpp::Logger::Level::Debug) 只放开某个子 logger,避免全局 DEBUG 刷屏。
  • 控制台输出:环境变量 RCUTILS_CONSOLE_OUTPUT_FORMAT 自定义格式,RCUTILS_COLORIZED_OUTPUT=0 关彩色,RCUTILS_LOGGING_BUFFERED_STREAM=0 关缓冲(真机崩溃时日志不丢)。
  • 日志设计规范:官方设计文档 https://design.ros2.org/articles/logging.html。

说明:ROS 2 的日志级别控制主要通过 --log-level 参数与 logger API,官方文档未提供名为 ROS_LOG_LEVEL 的环境变量(那是 ROS 1 生态的概念);本节以 --log-level 为准。

3.2 诊断:diagnostic_updater + diagnostic_aggregator ​

cpp
#include <diagnostic_updater/diagnostic_updater.hpp>

// 节点内
diagnostic_updater::Updater updater{this};
updater.setHardwareID("lidar");
updater.add("scan_frequency", this, &MyNode::checkScanRate);
// 周期调用 updater.force_update(); 或 setPeriod + 定时调用 updater.update()

void MyNode::checkScanRate(diagnostic_updater::DiagnosticStatusWrapper& stat) {
  if (scan_hz_ < 5.0) {
    stat.summary(diagnostic_msgs::msg::DiagnosticStatus::ERROR, "scan rate too low");
  } else {
    stat.summary(diagnostic_msgs::msg::DiagnosticStatus::OK, "normal");
  }
  stat.add("scan_hz", scan_hz_);
}
bash
# 观测
ros2 topic echo /diagnostics         # 原始诊断
ros2 topic echo /diagnostics_agg     # 聚合后(需 aggregator)
ros2 run diagnostic_aggregator aggregator_node   # 示例启动

diagnostic_aggregator 支持分析器(analyzer):比如"任一 Error 级别状态即整体 Error",供上层状态机订阅 /diagnostics_agg 做降级决策。

3.3 可视化与运行时探测 ​

bash
rqt_graph        # 拓扑图:节点/话题连线,一眼看循环依赖、断链
rqt_console      # 日志查看器:按级别/logger 过滤,实时监控
rqt_plot         # 话题数值曲线:看频率/幅值抖动
rqt_tf_tree      # TF 树:找 frame 断链

ros2 topic list -t          # 列出话题+类型
ros2 topic info /scan --verbose   # 看发布者/订阅者 + 各自 QoS(契约核对)
ros2 topic hz /scan         # 实际发布频率
ros2 topic bw /scan         # 带宽
ros2 topic echo /scan --once # 单帧数据
ros2 node info /node        # 节点订阅/发布/服务/Action 清单

3.4 性能剖析:ros2 tracing 与 perf ​

bash
ros2 trace                       # 开始追踪(需 tracetools 环境)
# ... 复现问题 ...
ros2 trace stop                  # 停止并生成 trace 数据,用 tracecompass/babeltrace 分析

3.5 生命周期与看门狗:transition 作为运行态信号 ​

managed node(rclcpp_lifecycle)的状态机本身就是健康信号:active = 正常运行,inactive = 降级/停止,finalized = 终结。看门狗可以:

  1. 订阅 /diagnostics,某个关键状态 ERROR 超过 N 秒 → 触发降级。
  2. 调用 ros2 lifecycle set /node configure/inactivate 让故障节点退出 active。
  3. 用 on_exit 事件(见 01_ROS2应用架构设计模式.md)重启进程。

状态查询/操作:

bash
ros2 lifecycle list /node        # 列出可用状态
ros2 lifecycle get /node         # 当前状态
ros2 lifecycle set /node activate  # 激活

官方生命周期设计:https://design.ros2.org/articles/node_lifecycle.html。


4. 实践指南 ​

4.1 典型故障定位 playbook ​

Playbook ① 话题无数据

bash
ros2 topic list | grep scan        # ① 话题存在吗?没有→节点没起/话题名写错
ros2 node info /lidar_node         # ② 节点订阅/发布清单里有没有它
ros2 topic info /scan --verbose    # ③ 发布者/订阅者 QoS 是否匹配?
                                   #    BEST_EFFORT vs RELIABLE 静默丢数据
ros2 topic echo /scan -q           # ④ 发布者到底发没发(-q 只看匹配的订阅)
ros2 topic hz /scan                # ⑤ 频率是否正常(0 → 发布者内部卡死)

根因排序:话题名/命名空间不匹配 > QoS 不匹配 > 发布者内部异常(用 rqt_console 看 ERROR)> 发布者 executor 阻塞。

Playbook ② 节点卡死(executor 死锁/阻塞)

bash
ros2 node info /node               # 节点还在吗?(还在=卡,不在=崩了)
ros2 topic hz /node/out            # 输出停更但节点在 → executor 阻塞
# 症状:某回调里 sleep/阻塞 IO/加锁不释放,单线程 executor 全被卡住

排查:① 检查是否有长阻塞回调(网络、文件、std::this_thread::sleep_for)占住 SingleThreadedExecutor;② 是否有死锁(回调里拿锁,另一个线程持锁等回调);③ 改用 MultiThreadedExecutor + ReentrantCallbackGroup,把慢回调隔离到独立回调组/线程;④ ros2 tracing 看是哪个回调卡住。

Playbook ③ TF 过期(frame 链断)

bash
ros2 run tf2_ros tf2_echo map base_link   # 查 map→base_link 变换
ros2 run rqt_tf_tree rqt_tf_tree          # 看整棵 TF 树,找断点
ros2 topic hz /tf                         # TF 发布频率

排查:① tf2_echo 报"frame does not exist" → 某段 TF 没发布(里程计/定位没起);② 报"lookup would require extrapolation" → TF 还在但过期(频率低、丢帧、时间戳不连续),检查发布者频率与 /use_sim_time 一致性;③ 静态 TF 用 StaticTransformPublisher 一次性发布,别塞进高频 /tf。

4.2 最佳实践 ​

  1. 关键节点全部上报 diagnostics(传感器帧率、CPU、内存、电池),这是量产的"仪表盘"而不是锦上添花。
  2. 日志级别分层:默认 INFO,调试时 --log-level DEBUG,真机 ERROR 要能触发告警。
  3. 命名规范让你可 grep:logger 用 get_child 建层级(node.driver.lidar),排查时 rqt_console 按前缀过滤。
  4. bag 是复现利器:真机出问题先 ros2 bag record -a 抓现场,回放复现 + 定位,见 02_ROS2测试体系.md。

4.3 陷阱 ​

  • QoS 不匹配无显式报错:ROS 2 不会主动弹错误,只会静默收不到——这是"话题无数据"的头号根因。
  • ros2 topic echo 有输出 ≠ 你的节点收得到:echo 会自动适配 QoS,你的订阅者 QoS 可能仍不匹配。用 --verbose 逐项核对。
  • STALE 诊断状态:诊断消息默认有失效时间(stale),节点崩溃后 /diagnostics 上的状态会变 STALE,看门狗要处理 STALE 而非只看 ERROR。
  • 日志缓冲:真机崩溃时,缓冲的日志可能没刷盘,RCUTILS_LOGGING_BUFFERED_STREAM=0 关缓冲换实时性。

4.4 调优 ​

  • 日志开销:RCLCPP_DEBUG 在编译时可用 -DRCUTILS_LOGGING_MIN_SEVERITY 裁剪(生产关 DEBUG 减少开销);高频回调里避免打 INFO。
  • 诊断频率:诊断周期别和业务同频(如 100Hz),1Hz 足够,避免 /diagnostics 刷爆带宽。
  • 追踪开销:LTTng 追踪有 CPU 开销,只在复现时开启,别常驻生产。

5. 方案对比 ​

监控方案实时性实现成本适用场景局限
日志(RCLCPP_*)事后极低开发/调试/取证无主动健康判断
诊断(diagnostics)实时(1Hz 级)低量产健康监控、看门狗需各节点配合上报
可视化(rqt)实时人工极低开发联调需人看,不能自动告警
追踪(ros2 tracing/perf)离线分析中性能瓶颈、调度抖动开销大、数据量大
生命周期看门狗实时自动中故障自动降级/恢复需设计状态机与降级策略

绝对不适用场景:当问题是偶发、难复现、涉及跨进程时序(如某次 DDS 延迟尖峰导致控制抖动)时,不要只靠加日志——日志会掩盖时序问题且难以海量检索,应直接上 ros2 tracing 抓精确时序;反之,稳态健康监控用 tracing 则是杀鸡用牛刀,diagnostics 才是正确工具。


6. 工具链 ​

工具/包用途来源
rclcpp 日志宏 + --log-level日志分级官方
rqt_console / rqt_graph / rqt_plot / rqt_tf_tree可视化官方(desktop 装)
diagnostic_updater / diagnostic_aggregator健康上报与聚合ros/diagnostics
ros2 topic/node/lifecycle CLI运行时探测官方
ros2 tracing(tracetools)LTTng 追踪ros2/ros2_tracing
perfCPU/调度剖析Linux 通用
ros2 bag现场录制/回放复现官方

7. 参考资料 ​


8. 学习路径 ​

  1. 入门:跑两个节点,用 rqt_graph 看拓扑,rqt_console 看日志,ros2 topic info --verbose 看 QoS。
  2. 上手:给一个节点加 RCLCPP_* 分层日志 + --log-level DEBUG,体会 logger 层级。
  3. 进阶:给传感器节点加 diagnostic_updater,用 /diagnostics 看健康状态。
  4. 实战:按 4.1 三个 playbook 依次制造并定位"话题无数据 / 节点卡死 / TF 过期"三种故障。
  5. 串联:进入 04_持续构建发布与部署.md,把这些监控手段纳入发布与部署流水线。

9. 核心面试三问 ​

Q1:真机上"话题有名字但收不到数据",你按什么顺序排查?

答:严格按"拓扑→契约→数据流"三层:① ros2 topic list / ros2 node info 确认实体存在(节点是否在跑、话题名/命名空间是否重映射正确);② ros2 topic info /xx --verbose 核对发布者与订阅者 QoS(reliability/durability/depth)是否一致——这是 ROS 2 头号静默故障;③ ros2 topic echo/hz 确认发布者真在发、频率正常。特别注意 echo 有输出不代表你的节点收得到(echo 自动适配 QoS),必须逐项核对契约。

Q2:日志、诊断(diagnostics)、追踪(tracing)三者区别是什么?各在什么时机用?

答:日志是事后取证(开发调试、崩溃定位,成本最低);诊断是实时健康状态(节点主动上报 OK/WARN/ERROR,供看门狗/仪表盘做运行时判断,量产必备);追踪是精确时序剖析(LTTng 抓回调耗时、DDS 收发、executor 调度,用于偶发性能/时序问题,开销大不能常驻)。判断标准:稳态健康用 diagnostics,偶发时序用 tracing,其余用日志。

Q3:节点"卡死但没崩溃"(进程在,输出停更),你怎么定位?

答:先区分"崩溃"和"卡死"——ros2 node info 还在就是卡死。卡死最常见是 executor 被阻塞:① 单线程 executor 里某个回调做了长阻塞(sleep、网络、文件 IO)或死锁(回调内拿锁),把所有回调都卡住;② 用 ros2 tracing 定位是哪个回调没返回;③ 修复手段:长任务移出回调(放独立线程/线程池)、改 MultiThreadedExecutor + 独立回调组隔离慢回调、锁的持有时间最小化。核心是"executor 是单点,任何阻塞回调都会放大成整个节点卡死"。


下一步:04_持续构建发布与部署.md · 模块导读:README.md