ROS 2 排障笔记:GDB、日志与 Tracing

ROS 2 的软件问题可以先分成四类:节点退出了、节点卡住了、节点收不到数据、节点运行得太慢。 它们需要不同的证据。GDB 看进程停下来的调用栈和变量,日志记录业务经过,ros2 tracing 记录运行过程中的事件时间线;strace 看系统调用,perf 找 CPU 热点。先问清楚想证明什么,再选工具,会比同时打开所有工具更容易定位问题。

本文以 Ubuntu 22.04 + ROS 2 Humble + C++ / rclcpp 为操作基线,资料核对日期为 2026-09-27。命令在 Linux 的 Bash 中执行;macOS 用户应进入 Linux 容器或虚拟机。Jazzy、Rolling 的 tracing 子命令和默认配置可能不同,不能把新版本文档的命令直接当作 Humble 命令。

随文提供一个包含“正常、异常、慢回调”三种模式的最小示例包。示例编译与 GDB 验证结果见验证记录;tracing 部分给出官方配置流程及教学时间线,教学数字不是本站机器的 tracing 实测结果。

一、按现象选择工具

现象 先做什么 工具能回答的问题 不能据此直接判断
节点突然退出、segfault、abort 保存日志、GDB / core 停在哪一行?调用链和变量是什么? 崩溃位置一定是最早写坏内存的位置
进程还在,但没有输出 CPU / 线程状态、所有线程栈 在计算、睡眠、等锁,还是等响应? 栈里出现 futex 就是死锁
topic 有名字,但收不到消息 图、类型、QoS、域、发现 发布者与订阅者是否真的匹配? topic list 有名字就说明数据链路正常
频率下降、偶尔卡顿 tracing,再按证据用 perf 哪个回调耗时?何时开始和结束? 回调耗时长一定是 CPU 计算多
随机内存错误 ASan / UBSan,再回到 GDB 越界、释放后使用等发生在哪里? 一次无报错就证明没有内存问题
相同输入偶发不同结果 rosbag 固定输入、保留参数和版本 改动前后是否能稳定比较? bag 重放能复刻原来的线程调度

backtrace 与 tracing 不是同一件事。 前者是暂停时的函数调用栈;后者是不断记录事件,结束后再按时间分析。ROS 2 提供了日志、CLI 和 tracing 的集成入口;GDB、LTTng、perf 则是更通用的工具。

从 ROS 图与日志到 GDB 调用栈、Tracing 时间线的排障地图

对真实机器人,GDB 断点会暂停控制进程;练习时先在仿真或脱离执行机构的环境进行。测性能时撤掉断点,并记录日志和跟踪配置,避免把调试开销误当成系统原有的延迟。

二、先把现场保存下来

2.1 环境与版本

在启动目标程序的终端中执行。这里假设自己的工作区叫 ~/diagnostics_ws;实际工程替换成自己的路径。

source /opt/ros/humble/setup.bash
# 已构建过自己的工作区时,再 source 它:
source ~/diagnostics_ws/install/setup.bash

printenv ROS_DISTRO RMW_IMPLEMENTATION ROS_DOMAIN_ID ROS_LOCALHOST_ONLY
ros2 doctor --report > /tmp/ros2-doctor.txt
ros2 pkg prefix diagnostic_demo
ros2 pkg executables diagnostic_demo

环境变量没有输出,通常表示未显式设置,不等于相关功能没有默认值。pkg prefix 则能检查是否加载了错误的 overlay:你刚改了源码,却一直在运行另一份安装结果,是常见的“修复无效”。ros2 doctor 是环境检查入口,不是应用逻辑正确性的证明。ROS 2 doctor 教程

每次复现至少记下:代码提交、ROS 包版本、完整启动命令、参数 YAML、输入数据、故障时间、实际进程 PID。不要只保存最后一行 process has died。

2.2 图正常,不代表通信正常

以下命令针对后面的小实验,换成自己的节点名和 topic 名即可:

ros2 node list
ros2 node info /diagnostic_demo
ros2 topic list -t
ros2 topic info /heartbeat --verbose
ros2 topic echo /heartbeat
ros2 topic hz /heartbeat
ros2 param dump /diagnostic_demo

echo 和 hz 会持续运行,检查后按 Ctrl+C,再执行下一条。按这个顺序看:

  1. 节点名、命名空间、重映射是否与你预期一致。
  2. topic 类型和发布/订阅端点是否正确。
  3. Reliability、Durability 等 QoS 是否兼容。Best Effort 发布者不能满足要求 Reliable 的订阅者。
  4. 两端 ROS_DOMAIN_ID、网络可达性和发现配置是否一致。
  5. 回调有没有真正得到执行;进程存在不代表 Executor 在正常推进。

订阅传感器的 Best Effort 数据时,可显式试验:

ros2 topic echo /scan --qos-reliability best_effort

ros2 topic hz 测到的是这个 CLI 订阅者的接收节奏,会受它的 QoS、系统负载和网络影响,不能直接当成发布者内部定时器频率。需要核对目标进程自身时间线时用 tracing。官方 topic 教程、QoS 兼容性

只有怀疑 CLI 图缓存异常,或修改了域/RMW 后,才考虑 ros2 daemon stop、ros2 daemon start;它们重启的是 CLI daemon,不会重启你的业务节点,也不会修复 QoS。

三、日志:先知道程序走到了哪里

3.1 配置级别和输出位置

export ROS_LOG_DIR="$HOME/ros2-debug-logs"
export RCUTILS_COLORIZED_OUTPUT=0
export RCUTILS_CONSOLE_OUTPUT_FORMAT='[{severity}] [{time}] [{name}] [{function_name}:{line_number}] {message}'

ros2 run diagnostic_demo diagnostic_node \
--ros-args -p mode:=normal --log-level diagnostic_demo:=debug

diagnostic_demo:=debug 指定 logger 名称;不确定名称时先用 --log-level debug。logger 名称通常与节点名相关,代码也可以创建子 logger。没有在编译产物中保留的日志,运行时调高等级也无法恢复。

默认日志目录通常是 ~/.ros/log,也会受 ROS_HOME 影响;这里通过 ROS_LOG_DIR 显式指定。控制台、磁盘日志和 /rosout 是不同输出路径;ros2 launch 还会管理自己的进程日志,不应假定所有信息都在同一个文件。图形环境可用 ros2 run rqt_console rqt_console 按节点和级别筛选 /rosout。日志配置文档

3.2 读日志时找“第一处偏离预期”

例如“开始处理请求 42”之后再无“请求 42 处理完毕”,这个区间就值得打断点或加测量。推荐带上输入编号、状态、关键尺寸和错误码,不要只打印“到了这里”。

RCLCPP_DEBUG(get_logger(), "request=%u size=%zu", request_id, values.size());
RCLCPP_WARN_THROTTLE(
get_logger(), *get_clock(), 2000, "waiting for a valid input");

这段是插入业务代码的片段,变量由业务提供;2000 是毫秒。节流能减少刷屏,但使用的 clock 若为仿真时钟且停止推进,也会影响节流表现。计量代码执行时长优先用 std::chrono::steady_clock;消息业务时间仍按你的 ROS 时间语义处理。

四、准备一个能亲手排查的小程序

包中只有一个 100 ms 定时器,每次往 /heartbeat 发布字符串。参数 mode 在启动时读取:

模式 行为 学习目标
normal 直接发布 建立基线
crash 第五次回调用 values.at(5) 访问长度为 3 的 vector 捕获 C++ 异常、读调用栈
slow 每次回调睡眠 80 ms 后发布 区分墙钟耗时与 CPU 时间

下载 package.xml、CMakeLists.txt、diagnostic_node.cpp,按下面的目录存放。网站仓库中已有同样的目录,可直接复制。

~/diagnostics_ws/src/diagnostic_demo/
├── package.xml
├── CMakeLists.txt
└── src/
└── diagnostic_node.cpp

Ubuntu 中先确保已安装 ROS 2 Humble,并配置其 apt 源,再安装调试和构建工具:

sudo apt update
sudo apt install gdb build-essential python3-colcon-common-extensions \
ros-humble-rclcpp ros-humble-std-msgs

source /opt/ros/humble/setup.bash
cd ~/diagnostics_ws
colcon build --packages-select diagnostic_demo --symlink-install \
--cmake-args -DCMAKE_BUILD_TYPE=Debug
source install/setup.bash
ros2 run diagnostic_demo diagnostic_node --ros-args -p mode:=normal

看到递增的 count 后按 Ctrl+C。每个新终端都需要 source;--symlink-install 也不意味着改 C++ 后无需重新编译。

实际工程里,Debug 适合单步和看变量;RelWithDebInfo 更接近日常优化构建,并保留调试信息,但可能出现 <optimized out>、内联、单步跳行。定位问题与性能对比最好使用独立构建目录。只重编自己的包,不会自动给 /opt/ros 中已经安装的第三方库补上符号。ROS 2 GDB 接入教程

五、GDB:崩溃后怎么看

5.1 从 ros2 run 启动

ros2 run --prefix 'gdb -q --args' diagnostic_demo diagnostic_node \
--ros-args -p mode:=crash

出现 (gdb) 后,下面输入的是 GDB 命令:

set pagination off
catch throw
run
bt

catch throw 让程序在 C++ 异常抛出时就停下来,而不是等最终 abort。本例会抛出 std::out_of_range;它不是故意制造 SIGSEGV。若想直接运行到崩溃,可省略 catch throw,或启动时使用 gdb -q -ex run --args。

catch throw 也会捕获第三方库随后自行处理的异常。本次容器验证中,Fast DDS 初始化就曾先触发 catchpoint,所以第一次停住不一定是目标故障。先看 bt;若在初始化阶段,可 continue。为了直接聚焦本例,也可以在全新 GDB 会话中先执行 break DiagnosticNode::tick if count_ == 4、run,到达业务回调后 delete 1、catch throw、continue。

不要写 gdb --args ros2 run ... 来调试 C++ 节点:ros2 是 Python CLI 入口,真正需要调试的是它启动的 ELF 程序。--prefix 把 GDB 放在节点可执行文件前面。

5.2 栈帧的正确方向

下面是结构示意,实际帧号和库函数层数随环境变化:

#0  __cxa_throw (...)
#1 std::__throw_out_of_range_fmt (...)
#2 std::vector<int>::_M_range_check (...)
#3 std::vector<int>::at (...)
#4 DiagnosticNode::tick (this=...) at diagnostic_node.cpp:...
#5 ... rclcpp timer / executor ...
#... main (...)

#0 是当前最内层执行位置,#1 是它的调用者,编号更大则更靠近外层调用者。 先从 #0 往外找自己的源码帧,再看是谁把什么参数传进来。可以从 main 往回理解调用路径,但不能把最底部当成当前故障指令。GDB backtrace 手册

假设你实际输出中 DiagnosticNode::tick 是第 4 帧:

frame 4
list
info args
info locals
print count_
print values
bt full

本例应该得到 count_ = 5,values 只有三个元素。由此能形成可验证结论:第五次定时器回调访问了非法下标 5,抛出异常,且没有被捕获。 “GDB 停在 libstdc++ 里面,所以 libstdc++ 有 bug”则不成立。

SIGSEGV 往往提示非法内存访问;SIGABRT 可能来自断言失败、未捕获异常、显式 abort 或分配器检测到内存破坏。信号是线索,需要结合栈和此前日志。

5.3 条件断点、单步和记录

另起一次调试,用下面这些命令在第五次回调进入前观察状态:

break DiagnosticNode::tick if count_ == 4
run
next
print count_
step
finish
continue

条件是 4,因为 tick() 进入后才执行 ++count_。next 通常跨过函数调用,step 进入函数,finish 跑到当前函数返回;遇到标准库模板不要机械地一直 step。

保存一次完整现场:

set logging file /tmp/ros2-gdb.txt
set logging overwrite on
set logging enabled on
info threads
thread apply all bt full
info sharedlibrary
set logging enabled off

不知道变量时优先缩小到业务栈帧再 print。No symbol 先查编译符号及程序版本;No source file 查源码路径;<optimized out> 查优化选项。容器内构建、宿主机看源码时,可以用 set substitute-path /build/ws /local/ws 映射路径,前提是两边确实是同一份源码。

5.4 Launch、组件与插件

对于无 GUI 的 launch,最省事的自动取栈方式是给目标 Node加批处理前缀。以下是完整 launch 结构,保存成 debug.launch.py 后可用 ros2 launch ./debug.launch.py 执行:

from launch import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description():
return LaunchDescription([
Node(
package='diagnostic_demo',
executable='diagnostic_node',
parameters=[{'mode': 'crash'}],
prefix=[
'gdb -q -batch -ex "set pagination off" '
'-ex run -ex "thread apply all bt full" --args'
],
output='screen',
),
])

批处理会在崩溃停止后输出栈并退出,不提供交互提示符。要交互调试,图形 Linux 可改为 prefix=['xterm -e gdb -q -ex run --args'](需安装 xterm 和可用 DISPLAY),无 GUI 则在独立终端 attach。

Composable Node 是共享库,被装入 component container;ros2_control 的控制器和硬件插件也通常在承载进程中运行。给容器/manager 进程加 GDB,或 attach 到它的 PID,不能拿插件 .so 当可执行文件。动态加载前可设 set breakpoint pending on,然后对插件类的方法下断点;调试符号必须包含该插件。ros2_control 调试说明

六、卡死与 core:没有正常退出提示怎么办

6.1 进程还活着:观察线程,再 attach

Linux 中先用 ps -eo pid,ppid,pcpu,stat,args 找到真正的节点或承载进程。不要把 ros2 launch 的 Python 父进程当成目标。

TARGET_PID=1234  # 改成核对过的实际 PID
ps -p "$TARGET_PID" -o pid,ppid,lstart,stat,args
top -H -p "$TARGET_PID"
# 退出 top 后再附加:
gdb -q -p "$TARGET_PID"

在 GDB 中:

set pagination off
info threads
thread apply all bt
detach
quit

attach 通常会暂停目标。detach 放开进程,使其继续运行;采第二次快照前必须先让程序运行一段时间,不能对暂停的进程连续看两次栈就宣称“栈一直不变”。

栈/CPU 现象 下一步核查
高 CPU,栈常在自己的循环或算法 查循环边界、输入规模;用 perf 找热点
sleep、poll、epoll_wait 可能正常等待;核对预期输入和超时
futex、mutex、condition variable 找等待对象、持有者和唤醒条件;一次采样不能证明死锁
回调里 future.wait(),响应一直不处理 检查 Executor 线程和 callback group

ROS 2 常见自锁链路是:定时器回调持有互斥 callback group → 同步等待 service 结果 → 处理结果又需要该 group → 两边无法推进。只换 MultiThreadedExecutor,但所有回调仍在同一个默认互斥组,通常解决不了。优先改成异步状态机;需要并行时再设计不同回调组和共享数据保护。Callback Groups 官方说明

若提示 ptrace: Operation not permitted,检查用户、Yama、容器 capability 和 seccomp。不要默认关掉整机安全限制。Linux Docker 中可在专用调试容器增加 --cap-add=SYS_PTRACE;仅在仍受 seccomp 阻止且已理解范围时增加 --security-opt seccomp=unconfined。不要给日常服务无条件加 --privileged。

6.2 已经退出:读 core

在启动目标的同一 shell 中设置,并先检查系统收集方式:

ulimit -c unlimited
cat /proc/sys/kernel/core_pattern

若系统使用 systemd-coredump,可通过 coredumpctl list 找崩溃记录,用 coredumpctl info PID 核对时间和可执行文件,然后 coredumpctl debug PID。这里的 PID 来自崩溃记录,不是随手选一个当前 PID。coredumpctl 手册

若拿到普通 core 文件:

gdb /exact/build/diagnostic_node /path/to/core

进入后仍然用 thread apply all bt full。必须保留对应版本的可执行文件、共享库和符号;重新编译后的“同名文件”不一定匹配。ulimit 只控制限制,不保证当前目录出现 core;Ubuntu 的 Apport、systemd 配置、容器环境都可能改变去向。

七、ros2 tracing:把运行过程录成时间线

7.1 先分清四层

rclcpp / rcl / rmw 等代码中的 tracepoint
↓
tracetools:ROS 2 的跟踪接口
↓
LTTng-UST:采集用户态事件 → CTF 文件
↓
Babeltrace 看原始事件 / tracetools_analysis 做关联和统计

ros2 trace 用来配置并控制采集,不会自动给任意 C++ 业务函数插桩。回调的开始/结束可以由 ROS 2 埋点提供;想知道回调内部某个矩阵求解阶段的耗时,需进一步采样或增加自己的测量点。

先学用户态 tracing,不需要一上来就安装内核模块。后续若要证明线程何时被抢占、何时进入 runnable 状态,再考虑 sched_switch、sched_wakeup 等内核事件以及相应的权限和分析工具。ros2_tracing Humble 说明

7.2 安装,并验证是否真的启用

在配置好 ROS apt 源的 Ubuntu 22.04 中:

sudo apt update
sudo apt install lttng-tools liblttng-ust-dev \
python3-lttng python3-babeltrace babeltrace \
ros-humble-ros2trace ros-humble-tracetools-launch

source /opt/ros/humble/setup.bash
ros2 trace --help
ros2 run tracetools status

有命令、库已启用、采到事件,是三个不同的检查。 status 应输出 Tracing enabled。若输出 Tracing disabled,仅安装 LTTng 命令还不够,需要重新构建启用了 LTTng 的 tracetools。

Humble 的二进制安装可使用独立 overlay,避免与自己的项目源码混在一起:

sudo apt install git python3-rosdep python3-colcon-common-extensions
mkdir -p ~/tracing_ws/src
cd ~/tracing_ws/src
git clone --branch humble https://github.com/ros2/ros2_tracing.git
cd ~/tracing_ws
source /opt/ros/humble/setup.bash
rosdep update
rosdep install --from-paths src --ignore-src --rosdistro humble -y
colcon build --packages-up-to ros2trace tracetools_launch \
--cmake-force-configure
source install/setup.bash
ros2 run tracetools status

若 rosdep 尚未初始化,按报错先执行一次 sudo rosdep init;已初始化的系统不要重复。若你自己编译的 ROS 核心曾使用 TRACETOOLS_DISABLED=ON,只替换接口库不能恢复被删掉的调用点,需要重新配置、重编相关核心包;不要在这种情况下仅凭 status 就认为完整可用。

采集终端和应用终端都使用同一套 overlay。已经构建好的业务工作区可以在干净终端按以下顺序加载:

source /opt/ros/humble/setup.bash
source ~/tracing_ws/install/local_setup.bash
source ~/diagnostics_ws/install/local_setup.bash
ros2 run tracetools status

这里用 local_setup.bash 追加当前层,避免业务工作区的 setup.bash 自动重放旧 underlay 链。环境改变后重启业务节点;进程已经加载的共享库不会因为另一个终端 source 了脚本而改变。

7.3 第一次采集:一定先启动跟踪

终端 A 加载上述环境后:

ros2 trace --session-name demo-slow --list

读提示并按 Enter 开始录制。--list 在这里展示启用事件清单,并非单纯查询后立即退出。Humble 的参数定义可查对应分支源码。入门命令不启用 kernel 事件;使用时仍以本机 --help 和启动清单为准。

终端 B 加载同样环境,再启动节点:

ros2 run diagnostic_demo diagnostic_node --ros-args -p mode:=slow

录约 10 秒,在 B 按 Ctrl+C 停节点,再到 A 按 Enter 结束采集。记录 A 打印的实际路径;默认通常是 ~/.ros/tracing/demo-slow,也可能受 ROS_TRACE_DIR 或 ROS_HOME 影响。下一次换一个新 session 名。

babeltrace "$HOME/.ros/tracing/demo-slow" | less

先确认存在 ros2:callback_start 和 ros2:callback_end,再确认初始化、节点、定时器与回调关联事件。只在节点已经运行之后才开始采集,可能错过初始化映射,出现“有时间戳却不知道对应哪个节点”的问题。官方采集与分析教程

不要照搬新版本的 ros2 trace start/stop/pause 工作流;先看 Humble 安装版本是否支持相应子命令。上面的交互方式足够完成第一次实验。

7.4 怎么读一条事件

真实格式会随版本变化,以下是手工简化的教学示意:

timestamp       event                 vpid  vtid   callback
10.000000000 ros2:callback_start 4200 4200 0xABC
10.080400000 ros2:callback_end 4200 4200 0xABC
10.100100000 ros2:callback_start 4200 4200 0xABC

时间戳是事件发生时间;vpid/vtid 区分进程和线程,callback 是进程内回调对象标识,不是 topic 名。字段是否出现取决于事件和采集 context。地址不能跨进程或跨次运行当成同一个对象使用。

这组数字可以计算:

  • 第一次回调的墙钟持续时间:10.0804 - 10.0000 = 80.4 ms。
  • 两次开始之间的间隔:10.1001 - 10.0000 = 100.1 ms。
  • 回调结束后到下一次开始:19.7 ms,但这不是“调度延迟”。

若没有记录预期就绪/截止时间,就不能凭两个回调边界算出 Executor 的排队延迟。若没有关联某条消息的发送与处理,也不能把这 80.4 ms 称为端到端延迟。

分析时必须按进程、线程、回调标识及事件顺序正确配对,不能把整个文件里相邻两行机械相减;嵌套或可重入回调尤其需要处理。检查开始/结束是否成对,有无 discarded/lost events 提示,采集是否在回调中间结束,再计算统计量。

7.5 打开分析 notebook,怎么看图

优先采用与 Humble 对应的 tracetools_analysis 分支及其示例 notebook,避免混用主分支 API:

cd ~/tracing_ws/src
git clone --branch humble https://github.com/ros-tracing/tracetools_analysis.git
cd ~/tracing_ws
rosdep install --from-paths src --ignore-src --rosdistro humble -y
colcon build --packages-up-to tracetools_analysis
source install/setup.bash
sudo apt install jupyter-notebook python3-bokeh
jupyter notebook ~/tracing_ws/src/tracetools_analysis/tracetools_analysis/analysis/callback_duration.ipynb

用安装了 ROS Python 包的解释器/kernel 运行 notebook;随手切到另一个虚拟环境,可能找不到 tracetools_analysis、babeltrace 或版本匹配的依赖。浏览器打开后,将 notebook 的 path 改成实际 trace 的绝对路径,按顺序执行单元格。Humble 分析示例目录

官方教程用订阅回调演示分析,Humble 的 callback_duration.ipynb 会遍历多种回调。为了聚焦本例的 timer,运行到 notebook 创建 data_util 的单元格后,新增一个单元格,先列出回调归属,再筛选本例的定时器:

matches = []
for callback, symbol in data_util.get_callback_symbols().items():
owner = data_util.get_callback_owner_info(callback) or ''
print(hex(callback), symbol, owner)
if owner.startswith('Timer') and 'diagnostic_demo' in owner:
matches.append(callback)

assert len(matches) == 1, '请检查回调清单、初始化事件和本次采集的节点'
durations = data_util.get_callback_durations(matches[0]).copy()
durations = durations.sort_values('timestamp')
assert not durations.empty, '没有完整回调样本'
duration_ms = durations['duration'].dt.total_seconds() * 1000.0
print(duration_ms.describe(percentiles=[0.50, 0.95, 0.99]))

from bokeh.io import output_notebook
from bokeh.plotting import figure, show

output_notebook()
plot = figure(title='diagnostic_demo timer',
x_axis_label='Callback index', y_axis_label='Duration (ms)')
plot.circle(list(range(len(duration_ms))), duration_ms.tolist(), size=4)
show(plot)

这段按 Humble 的 Ros2DataModelUtil 接口 编写;它适用于本例单节点、单定时器的采集,不应直接拿来合并多次运行或地址复用的复杂记录。想先按官方订阅回调示例学习,可用 demo_nodes_cpp 的 talker 和 listener 替代本例,同样先采集后启动两个节点,再按输出清单选择订阅回调。

阅读图表时先核对轴和单位,再看分布:

看什么 解释与下一步
每次调用耗时的时间曲线 是稳定偏慢,还是某段时间突然变慢?
p50、p95、p99、最大值及样本数 平均值可能掩盖尾部;少量样本的 p99 不稳定
timer 的开始间隔 100 ms 的目标节奏是否稳定?是否出现长空档?
与 CPU 采样的对应关系 墙钟 80 ms 且 CPU 很低,优先查等待、睡眠或抢占

先采 normal,再以同样窗口、环境、日志级别采 slow。本例 slow 模式的睡眠决定了回调墙钟时间应至少接近 80 ms,调度和其他操作还会增加时间;它并不需要消耗 80 ms CPU。100 ms 定时器仍可能表现为约 10 Hz,所以“频率看着正常”也不等于回调有充足余量。

跨进程消息时延还需要关联消息身份;跨机器则进一步需要时钟同步和误差分析。ROS 仿真时间、系统墙钟、LTTng 时钟域不能随意直接相减。

7.6 没有数据时按层排查

现象 检查顺序
ros2 trace 不存在 是否装了 ros2trace,当前 shell 是否加载正确环境
Tracing disabled 是否检测到 LTTng,是否重编并加载启用后的 tracetools
enabled 但没有 ROS 事件 两端环境、节点是否重新启动、session 是否开始、事件是否启用
有事件但关联不出节点 是否漏了初始化事件;是否混了进程或不同运行的地址
Python 节点信息不完整 Humble 的核心分析主要围绕 rclcpp 埋点,不要假定 rclpy 完全相同
容器中 session / shared memory 异常 先在同一容器、同一用户运行 tracer 与节点,检查 session daemon 和 /dev/shm
提示丢事件 缩小范围/缩短窗口,评估缓冲区和磁盘开销,重采后再做结论

Docker Desktop 的 Linux 内核属于虚拟机。容器用户态跟踪可用于理解 ROS 回调,但不能把它当成 macOS 宿主的完整调度轨迹;内核 tracing 是否可用要另行确认。无需为了入门用户态 tracing 直接安装 DKMS 或改宿主内核。

八、其余工具:什么时候接着用

8.1 strace 看系统调用

Ubuntu 可先安装 strace,然后对核对过的 PID 操作:

sudo apt install strace
TARGET_PID=1234
strace -f -tt -T -p "$TARGET_PID" -o /tmp/ros2-strace.txt

采样一小段后 Ctrl+C 结束跟踪并脱离目标。-f 跟踪线程/后续子进程,-tt 打印时间,-T 打印每次系统调用耗时。重点找反复失败的文件访问、长时间读写、连接失败和等待。futex 不是死锁证明;它也用于正常线程同步。strace 手册

8.2 perf 找 CPU 花在哪里

在有匹配内核 perf 工具和采样权限的 Linux 中:

TARGET_PID=1234
perf record -F 99 -g --call-graph dwarf -p "$TARGET_PID" -- sleep 15
perf report

先看热点函数,再看调用路径。示例中睡眠 80 ms 不会自然成为 on-CPU 热点;perf 的 CPU 采样和 tracing 的墙钟持续时间回答的是不同问题。没有符号先补符号;权限报错先检查 perf_event_paranoid 与容器能力,不要把禁用所有限制当成固定步骤。Ubuntu 内核工具包、Docker VM 内核和发行版内核未必一一匹配。perf record 手册

8.3 Sanitizer 把内存错误提前暴露

对于自己的 CMake target,可在专用调试构建中同时加编译和链接选项,位置放在 add_executable() 之后。下面以示例 target 为名:

target_compile_options(diagnostic_node PRIVATE
-fsanitize=address,undefined -fno-omit-frame-pointer)
target_link_options(diagnostic_node PRIVATE
-fsanitize=address,undefined)

若采用 target_link_options,将项目最低 CMake 版本设为至少 3.13(Ubuntu 22.04 的系统 CMake 满足)。用独立目录构建并加载:

colcon build --build-base build-asan --install-base install-asan \
--packages-select diagnostic_demo --cmake-args -DCMAKE_BUILD_TYPE=Debug
source install-asan/setup.bash

ASan 报告里先看错误类型、非法访问栈,再看分配/释放栈。它适合排查越界、use-after-free 等,运行开销使其不适合作为控制频率基准;本例 vector::at() 是主动检查并抛异常,不是一个验证 ASan 抓越界能力的实验。数据竞争另用 TSan,TSan 与 ASan 分开构建运行。AddressSanitizer 官方文档

8.4 rosbag 固定输入

ros2 bag record -o input_case /scan /tf /tf_static
ros2 bag info input_case
ros2 bag play input_case --clock

按你的故障更换 topic,录完后 Ctrl+C。离线重放时隔离真实输入发布者,并让目标节点使用 use_sim_time:=true;核查 /tf_static 等 transient-local 数据与录制/播放 QoS。bag 固定的是消息输入及其记录时间,无法重现原来的线程竞争、网络丢包和外设状态;没有录到的参数、配置及服务交互也不会自动补齐。rosbag2 的 QoS 配置

九、按这三次练习学下来

第一次:找准异常位置。 构建示例,跑 normal 建立基线,再用 catch throw 跑 crash。独立写下“哪个回调、第几次、哪个容器、下标是多少”,而不是只抄整个 backtrace。改正访问逻辑后重新构建、重跑相同输入。

第二次:识别慢回调。 采 normal 与 slow 两份 trace,分别检查事件完整性、回调标识和耗时。用时间差解释 80 ms,用 CPU 观察解释为什么“慢”不一定“忙”。再把 sleep 改成 150 ms,预测它超过 100 ms 周期后开始间隔会怎样变化,然后实测;不要假定错过的 timer 会无限补发。

第三次:迁移到自己的节点。 选择一个已知故障,先保存命令、参数、版本和输入,只启用能回答当前问题的工具。修改后比较同一条件下的错误次数、回调耗时分布或吞吐,补一个能防止复发的测试。

每次最终留下一张“证据卡”:现象 → 复现条件 → 最小证据 → 原因 → 修改 → 相同条件复测。例如“日志停了”只是现象;“线程 A 在互斥组内等 future,完成回调需要同组,重复复现且改为异步后恢复”才形成可审核的原因链。

继续阅读:GDB 实战、Linux 进程与系统调用排障、ROS 2 QoS、ROS 2 Control、Executor 与硬件接口。

坐标树断开、变换方向不对或出现 extrapolation 报错时,接着看 TF2 实战:坐标树、变换方向与时间排查。

知识共享许可协议