请求很慢,可能慢在 CPU、运行队列、锁或设备。追踪应先确定测量对象和起止边界,再收集足以验证假设的事件。
| 查阅目标 | 阅读位置 |
| 选工具、事件与采样 | 第 1~3 节 |
| 延迟关联、丢失与开销 | 第 4~5 节 |
| 转储、调试与诊断流程 | 第 6~7 节 |
1. 先确定问题和测量对象
| 问题 | 需要的证据 | 常见工具 |
| CPU 时间花在哪里? | 有代表性的栈采样 | perf |
| 哪类操作慢? | 配对的开始/结束时间及结果 | tracepoint、BPF 聚合 |
| 就绪任务为何迟迟没运行? | wakeup 到实际运行的关联 | 调度事件、runqlat 类工具 |
| 函数内部调用了什么? | 限定范围的函数进入/退出 | function_graph、fentry/fexit |
| 内核崩溃时状态如何? | 转储、日志、符号与配置 | kdump、crash |
| 某个内核状态转换是否正确? | 可控环境中的断点和单步 | KGDB/KDB |
同一个“请求耗时”可能包含应用排队、系统调用、块层排队和设备处理。不同层的延迟不能直接相加,除非明确事件嵌套与并行关系。
2. ftrace 与 tracefs
现代 tracefs 常挂载于 /sys/kernel/tracing,历史路径 /sys/kernel/debug/tracing 仍可能存在。available_tracers 列出当前可用 tracer,available_filter_functions 列出可跟踪函数;配置不同会改变可见内容。
cat /sys/kernel/tracing/available_tracers
cat /sys/kernel/tracing/events/sched/sched_switch/format
这些只读命令先确认能力与字段,读取失败不应直接判断函数不存在。开启 function/function_graph 会改变追踪状态;实际采集宜使用独立 trace instance,先设置过滤范围与缓冲区,再开启采集,结束后停用并清理。
function 记录所选函数进入;无过滤全量启用可能产生大量事件。
function_graph 关联函数进入和返回,展示调用层次及持续时间。是否计入被调度出去的时间等受到 tracer 选项影响。
trace 提供格式化缓冲区视图;trace_pipe 是消费式读取,多个读者会竞争事件,不能当作互不影响的广播日志。
CPU 缓冲区可能覆盖或丢失记录,跨 CPU 比较也需要理解 trace_clock。trace instance 可隔离多种采集配置,但共享动态探针定义等资源仍可能相互影响。接口和选项见 ftrace 文档。
动态 ftrace 不等于 kprobe event
动态 ftrace 通过可跟踪函数的插桩位置启停函数追踪;向 kprobe_events 或 dynamic_events 注册的是动态探针事件,属于另一种机制。读取 state+0x12(%di) 之类固定偏移表达式必须同时匹配架构、函数签名和结构布局,不能作为通用 TCP 状态读取示例。
优先检查是否有合适 tracepoint;确需动态函数探测时,再结合目标符号、BTF/DWARF 和实际源码解释参数。见 kprobe events。
3. perf:计数和采样回答不同问题
perf stat 汇总计数;perf record 按事件产生样本;perf report 汇总样本。记录一个 sleep 进程的 cycles,并不能代表整机十秒的 CPU 热点。
perf stat -e cycles,instructions,cache-misses -- ./workload
perf record -e cycles -g --call-graph dwarf -- ./workload
perf report
perf record -a -e cycles -g -- sleep 10
./workload 代表待测程序,需替换为实际命令。权限、PMU、虚拟化和平台能力决定事件是否可用;cache-misses 等通用名字在不同 CPU 上不一定对应完全相同的硬件含义。
事件过多导致 multiplexing 时,应检查运行时间比例和缩放结果。调用栈质量还依赖 frame pointer、DWARF、符号及展开方法;缺少符号、丢样本和错误展开都可能改变热点图。样本百分比是当前事件与记录范围下的占比,不是任意函数的精确墙钟耗时。工具语义见 perf stat与 perf record。
4. 正确构造延迟直方图
hist(nsecs) 展示的是时间戳分布,不是操作延迟。下面以 bpftrace 0.24 为例,记录指定进程在采集期间完整发生的 write 系统调用,使用 TID 关联入口与出口。
先检查目标 tracepoint 与字段存在:
bpftrace -lv 'tracepoint:syscalls:sys_enter_write'
bpftrace -lv 'tracepoint:syscalls:sys_exit_write'
保存为 write-latency.bt:
tracepoint:syscalls:sys_enter_write
/pid == $1/
{
@start[tid] = nsecs(monotonic);
}
tracepoint:syscalls:sys_exit_write
/pid == $1 && @start[tid]/
{
@lat_us = hist((nsecs(monotonic) - @start[tid]) / 1000);
@returns[args->ret < 0] = count();
delete(@start, tid);
}
interval:s:10
{
exit();
}
END
{
clear(@start);
}
bpftrace write-latency.bt 12345
将 12345 替换为待测进程 PID。@returns[0] 是非负返回,@returns[1] 是负返回;正值还可能是短写,不能直接叫“完整业务成功”。时间单位为微秒,10000 微秒是 10 毫秒。
此测量包括 syscall 期间阻塞和调度等待,不等于设备完成延迟;例如文件 write 返回后,数据可能还在页缓存。它也不覆盖 writev、io_uring 等其他写路径。采集开始前已进入或结束时尚未返回的调用不会进入完整样本,长尾分析应考虑这种截断。
关联身份:同步 syscall 与异步请求分开设计
TID 足以关联普通同步 syscall 的入口和出口,但异步 I/O 请求会跨线程/中断完成,应改用真正的请求身份,并处理复用、合并、重试和清理。追踪通用可递归函数时还需区分嵌套深度,不能照搬单个 start[tid]。语言与时间接口参阅 bpftrace 0.24 标准库。
让同一线程发起两项重叠请求,再选择完成线程。两种关联方法同时处理相同事件;即使错误关联算出一个正数,也不代表它是这项请求的真实延迟。
正在呈现知识画面
同一 TID 发出两个异步请求,开始时间属于谁?TID 可以关联普通同步 syscall,却不能通用标识异步请求。同一发起线程的重叠请求会覆盖 start[tid],跨线程完成又会找不到对应项。
{"id":"trace-request-correlation","title":"同一 TID 发出两个异步请求,开始时间属于谁?","summary":"TID 可以关联普通同步 syscall,却不能通用标识异步请求。同一发起线程的重叠请求会覆盖 start[tid],跨线程完成又会找不到对应项。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"two\"><div class=\"card\"><header>#101</header><label for=\"tid-101\" data-i18n=\"tid\"></label><select id=\"tid-101\"><option value=\"7\">7</option><option value=\"9\" selected>9</option></select><button id=\"start-101\" data-i18n=\"start\"></button><button id=\"end-101\" data-i18n=\"end\"></button></div><div class=\"card\"><header>#202</header><label for=\"tid-202\" data-i18n=\"tid\"></label><select id=\"tid-202\"><option value=\"7\">7</option><option value=\"9\">9</option></select><button id=\"start-202\" data-i18n=\"start\"></button><button id=\"end-202\" data-i18n=\"end\"></button></div></div><div class=\"actions\"><button id=\"wait\" data-i18n=\"wait\"></button></div><div class=\"metrics\"><div><span data-i18n=\"clock\"></span><strong id=\"clock\"></strong></div><div><span data-i18n=\"overwrites\"></span><strong id=\"overwrites\"></strong></div></div><div class=\"track-row\"><span data-i18n=\"events\"></span><div id=\"events\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"results\"></span><div id=\"results\" class=\"track\"></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.card label{font-size:12px;display:block;min-height:3.2em}.card button{margin-top:8px;min-height:4.8em}#events,#results{display:grid;grid-template-columns:1fr}#events .cell{min-height:52px;font-size:11px}#results .cell{min-height:6.5em;font-size:12px;white-space:pre-line}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let time=0,overwrites=0,req={},tidMap={},idMap={},events=[];function draw(){set('clock',time);set('overwrites',overwrites);for(const id of [101,202]){$('#start-'+id).disabled=!!req[id];$('#end-'+id).disabled=!req[id]||req[id].done;$('#tid-'+id).disabled=!!req[id]?.done;}cells('events',Array.from({length:4},(_,i)=>({text:events[i]??'—',cls:events[i]?'on':'empty'})));cells('results',[101,202].map(id=>{const r=req[id];return{text:'#'+id+'\\n'+(!r?.done?t('pending'):r.truth+' ms / '+(r.tid===null?t('missing'):r.tid+' ms'+(r.wrong?' '+t('wrong'):''))+' / '+r.byID+' ms'),cls:r?.done&&(r.wrong||r.tid===null)?'gold':r?.done?'on':'empty'};}));set('explanation',t('note'));}for(const id of [101,202]){$('#start-'+id).onclick=()=>{time+=10;req[id]={start:time,done:false};if(tidMap[7])overwrites++;tidMap[7]={id,time};idMap[id]=time;events.push(time+' ms · START #'+id+' · TID 7');draw();};$('#end-'+id).onclick=()=>{time+=10;const tid=+$('#tid-'+id).value,r=req[id],item=tidMap[tid];r.done=true;r.truth=time-r.start;r.tid=item?time-item.time:null;r.wrong=!!item&&item.id!==id;r.byID=time-idMap[id];delete tidMap[tid];delete idMap[id];events.push(time+' ms · END #'+id+' · TID '+tid);draw();};}$('#wait').onclick=()=>{time+=20;draw();};$('#reset').onclick=()=>{time=overwrites=0;req={};tidMap={};idMap={};events=[];draw();};draw();","audio":false,"strings":{"clock":"模型时间 / ms","end":"完成请求","events":"实际事件流","heading":"同一 TID 发出两个异步请求,开始时间属于谁?","intro":"分别启动和完成请求 101、202,为完成事件选择 TID。两种关联方法同时处理同一批事件,保留缺失或错配结果。","missing":"缺失","note":"先发起两个请求,再让 101 在 TID 7 完成:它可能取走属于 202 的起点。即使算出一个正数,也不是正确的 101 延迟。","overwrites":"TID 起点被覆盖次数","pending":"未完成","reset":"重置","results":"结果:真实时长 / TID 关联 / 请求 ID 关联","scope":"两个唯一请求 ID,一次生命周期,无事件丢失或 ID 复用。每次开始/完成使示意时钟增加 10 ms,额外等待可增加 20 ms。TID=7 是发起者;选择 9 表示另一完成线程,不是 CPU 迁移改变了 TID。真实追踪还要处理复用、重试和丢失。","scopeLabel":"模型范围与约定","start":"发起请求","tid":"完成事件的 TID","wait":"再经过 20 ms","wrong":"错配"}}
5. 聚合、丢失与开销
逐事件 printf 可能使用户态输出成为瓶颈。只需分布时优先在内核侧聚合 count/hist/sum;确需明细时明确采样、过滤、缓冲容量与丢失计数。
| 缺失位置 | 可能产生的偏差 |
| 入口事件丢失 | 出口找不到对应开始时间 |
| 出口事件丢失 | 开始状态残留,可能污染下一次关联 |
| map 满或输出缓冲不足 | 在途状态或明细缺失,样本不再完整 |
必须记录丢失并清理残留条目,不能让下一次请求继承旧时间戳。
BCC 提供大量现成工具,但传统 BCC 工作流通常在运行时编译 BPF,并非一整套固定预编译 CO-RE 二进制。biolatency、runqlat、tcpretrans 等名字说明大致用途,准确测量起止点和过滤条件仍要看所安装版本。见 BCC 项目。
6. 日志、动态调试与故障转储
printk 日志等级影响记录的严重性分类,控制台阈值决定哪些记录打印到控制台,不等于其他等级从日志环中消失。实际 KERN_* 宏编码应使用内核头文件,不要把可读的 <0> 展示形式伪写成当前宏定义。
dynamic debug 只控制已编译进相应机制的 pr_debug() / dev_dbg() 等调用点。按函数名查询没有条目,并不等于函数没有执行;该函数可能没有相应调试调用点。接口可能位于 /proc/dynamic_debug/control 或 debugfs,依配置而定。
kdump 需要预留 crashkernel 内存并准备捕获内核、initramfs 和保存路径。崩溃后切入捕获内核,原内存经 /proc/vmcore 等接口导出,再由用户态工具保存或过滤。不能只执行 kexec -p 就假设下一次一定获得完整可读转储;设备状态与捕获环境也必须支持。
转储分析:符号必须匹配崩溃时的内核
分析时使用崩溃内核匹配的 vmlinux、调试符号和必要模块,而不是机械套用当前 uname -r:机器可能已经重启到另一版本。crash 中 bt 通常查看当前/指定任务回溯,foreach bt 才是逐任务回溯。参见 kdump与 crash 帮助。
KGDB 可设断点、读寄存器及单步,暂停执行会改变时间行为,不适合用断点结果直接推断真实时延;连接传输、内核配置和符号也要匹配。配置与示例另见构建与开发目录。
7. 一次可解释的诊断流程
- 固定场景。 记录请求、时间窗口、负载与延迟边界。
- 缩小方向。 用计数或采样区分 on-CPU 与等待。
- 验证假设。 选择少量事件,必要时深入相关函数路径。
- 检查观测影响。 比较启用前后的吞吐、延迟与丢失情况。
- 保留解释条件。 记录工具版本、配置、过滤和测量限制。
每一步都应缩小不确定性,而不是单纯扩大日志量。
关联阅读:eBPF 基础、程序类型、用户态 ftrace 与 bpftrace 实践。