Linux 基础体系 · 第 21/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux 性能分析方法:CPU、内存、磁盘、网络与 eBPF 证据链

一、性能分析不是“找一个最高的指标”

性能问题通常不是某个指标单独异常,而是一条跨组件的因果链:

flowchart LR
    A[请求或任务变慢] --> B[应用线程状态]
    B --> C{正在运行还是等待}
    C -->|运行| D[CPU 调度与执行]
    C -->|等待内存| E[缺页 回收 Swap OOM]
    C -->|等待存储| F[块层队列 设备延迟]
    C -->|等待网络| G[Socket TCP 路由 DNS 网卡]
    D --> H[eBPF/perf 采样或跟踪]
    E --> H
    F --> H
    G --> H
    H --> I[时间相关的证据链]

例如,“CPU 使用率 100%”可能表示:

  • 用户程序确实在执行大量计算;
  • 内核在处理网络软中断;
  • 虚拟机被宿主机抢占,客体看到的是 steal
  • 一个线程占满单个 CPU,而其他 CPU 空闲;
  • CPU 使用率并不高,但大量线程在等待磁盘,导致请求仍然很慢。

因此,性能分析的第一步不是调参,而是回答三个问题:

  1. 谁在等待,等待什么?
  2. 等待发生在哪个时间区间、哪个 CPU、哪个设备或哪个连接上?
  3. 观测到的现象能否由下一层证据解释?

下面的分析方法反复使用四类证据:

  • **资源利用率:**CPU、内存、磁盘、网络使用了多少;
  • **饱和度:**资源是否已经无法及时服务新的工作;
  • **错误与异常:**丢包、重传、OOM、I/O 错误、软中断积压;
  • **时延与状态:**请求到底在哪个状态停留了多久。

“利用率高”并不自动等于“瓶颈”;“利用率低”也不自动等于“没有问题”。饱和度和时延通常比单一利用率更接近用户感知。


二、先建立观测边界:时间、对象和单位

2.1 采样值与累计值不同

Linux 中很多接口提供的是累计计数器,例如 /proc/stat 中的 CPU 时间、/proc/diskstats 中的 I/O 次数和扇区数。两次读取的差值才代表时间区间内发生的活动:

Δx=x(t2)x(t1)\Delta x = x(t_2)-x(t_1)

如果直接把累计值当成当前速率,就会得到错误结论。

速率为:

rate=Δxt2t1rate = \frac{\Delta x}{t_2-t_1}

其中时间单位必须与计数器单位一致。/proc/stat 的 CPU 时间通常以 jiffy 表示,jiffy 与 USER_HZ 相关,不能无条件假定为固定的毫秒数;可以用:

getconf CLK_TCK

读取当前系统的时钟刻度。

2.2 先确认对象是否一致

性能命令可能观察的是不同层次:

  • 主机整体;
  • 某个进程或线程;
  • 某个 cgroup;
  • 某个容器的 PID namespace;
  • 某个网卡、磁盘或网络命名空间;
  • 某个 CPU。

容器内看到的 /proc、CPU 数量和网络设备不一定代表整个宿主机。分析前应记录:

uname -a
nproc
cat /proc/cmdline
cat /proc/pressure/cpu /proc/pressure/memory /proc/pressure/io

/proc/pressure/* 是 PSI(Pressure Stall Information)接口,用于描述任务因资源不足而停顿的时间。它不是资源使用率,而是“有多少任务、在多长时间内无法继续推进”的证据。


三、CPU:从运行队列到上下文切换

3.1 CPU 时间的构成

/proc/stat 的第一行以 cpu 开头,常见字段顺序是:

cpu user nice system idle iowait irq softirq steal guest guest_nice

不同内核版本可能增加字段,解析时不应依赖固定总列数。主要含义是:

  • user:用户态执行时间;
  • nice:运行在调整过 nice 值的任务上的用户态时间;
  • system:内核态执行时间;
  • idle:没有任务运行的时间;
  • iowait:等待 I/O 的一种统计状态;
  • irq:硬中断处理时间;
  • softirq:软中断处理时间;
  • steal:虚拟机被虚拟化平台占用、客体无法运行的时间;
  • guest:运行虚拟 CPU 客体所计入的时间,通常已经包含在用户态相关统计中,不能简单重复相加。

在一个时间窗口内,常见的忙碌比例近似为:

busy=1Δidle+ΔiowaitΔtotalbusy = 1-\frac{\Delta idle+\Delta iowait}{\Delta total}

但这个公式是观察口径,不是调度器的严格定义。是否将 iowait 算作 idle,取决于分析目的;分析“CPU 是否执行任务”时可视为未执行用户任务,分析“CPU 是否被 I/O 相关工作拖住”时则应单独展示。

可以用 mpstat 观察每个 CPU:

mpstat -P ALL 1 5

典型字段包括 %usr%sys%iowait%irq%soft%steal%idle。其中:

  • %sys 高,可能是系统调用、网络协议栈、文件系统或内存管理开销;
  • %soft 高,可能是网络收包、定时器、块设备完成处理等软中断;
  • %steal 高,客体内部优化无法直接消除宿主机争用;
  • 总 CPU 不高但单个 CPU 100%,仍可能发生单线程瓶颈。

3.2 Load 不是 CPU 使用率

Linux 的 load average 表示一段时间内处于可运行状态或不可中断睡眠状态的任务数量的平滑平均。常见实现中:

  • R 状态的任务,即等待或正在获得 CPU 的可运行任务;
  • D 状态的任务,即通常在等待内核资源、常见于块 I/O 的不可中断睡眠任务。

它不是“CPU 百分比”,也不是简单的进程数。

假设一台 4 个逻辑 CPU 的机器:

  • 4 个线程都在运行:load 约为 4,CPU 可能接近满载;
  • 8 个线程都在争用 CPU:load 约为 8,运行队列明显排队;
  • 1 个线程等待磁盘而处于 D 状态、CPU 很空闲:load 仍可能增加;
  • 4 个线程分别等待不同资源,load 为 4,并不表示 4 个 CPU 都在忙。

因此,应把 load 与运行队列和任务状态结合:

uptime
vmstat 1 5
ps -eo state,pid,tid,psr,pcpu,stat,wchan:32,comm --sort=-pcpu | head -30

vmstat 中:

  • r 是可运行队列中的任务数;
  • b 是阻塞、通常处于不可中断睡眠的任务数;
  • cs 是上下文切换速率;
  • in 是中断速率;
  • siso 是 swap in/out;
  • wa 是 I/O wait 的统计口径。

r 大于逻辑 CPU 数只说明存在排队,不能单独说明排队时延。调度延迟还受任务优先级、CPU 亲和性、cgroup 配额、实时任务和锁竞争影响。

3.3 上下文切换的因果关系

上下文切换是调度器把 CPU 从一个可执行上下文切换到另一个上下文。其成本不只是保存和恢复寄存器,还可能影响:

  • CPU cache 局部性;
  • TLB 局部性;
  • 分支预测;
  • 内核锁竞争;
  • 调度器自身开销。

但“上下文切换多”不等于“系统一定慢”。高并发网络服务器可能有大量正常切换;只有当切换增加并同时伴随运行队列、系统态时间或请求时延增加时,才更像调度开销或线程模型问题。

查看进程级切换:

pidstat -w -p <PID> 1 5

常见字段:

  • cswch/s:主动上下文切换,例如线程阻塞等待 I/O、锁或条件变量;
  • nvcswch/s:非自愿上下文切换,例如时间片耗尽或被更高优先级任务抢占。

可以通过 /proc/<PID>/status 查看累计值:

grep -E 'voluntary_ctxt_switches|nonvoluntary_ctxt_switches' /proc/<PID>/status

3.4 软中断是 CPU 与网络的连接点

硬中断通常只完成快速确认和调度后续处理,较重的工作常在软中断上下文中完成。网络接收路径可能经历:

  1. 网卡收到数据并触发硬件中断;
  2. 驱动通过 NAPI 调度接收处理;
  3. 内核在软中断中批量取包;
  4. 经过协议栈、socket 队列和应用读取;
  5. 应用线程被唤醒并处理数据。

查看软中断累计计数:

cat /proc/softirqs

若某个 CPU 的 NET_RX 增长很快,同时 %soft 高、应用 CPU 下降或网络延迟升高,应继续检查:

cat /proc/interrupts
ethtool -S eth0

/proc/interrupts 显示中断分布;ethtool -S 显示网卡驱动相关计数器,但字段名称依赖驱动。需要避免把“软中断高”直接解释为“网络带宽满”:小包洪泛、包处理成本、连接建立、丢包重传都可能造成高包率而非高字节率。

3.5 用 perf 区分“在哪里花时间”

perf 依赖内核 perf_event 能力,可能受 kernel.perf_event_paranoid、容器权限和安全策略限制。对一个进程进行 CPU 采样:

perf stat -p <PID> -e \
  task-clock,context-switches,cpu-migrations,cycles,instructions,cache-misses \
  sleep 10

perf record -g -p <PID> -- sleep 10
perf report

第一条命令回答“发生了多少”:

  • task-clock:任务实际获得的 CPU 时间;
  • context-switches:上下文切换次数;
  • cpu-migrations:跨 CPU 迁移次数;
  • cyclesinstructions:硬件计数器,受虚拟化和硬件支持影响;
  • cache-misses:硬件定义的 cache miss 事件,不同 CPU 的精确定义不同。

第二组命令回答“在哪里发生”:调用栈中若集中在业务计算函数,可能是算法瓶颈;若集中在锁、内存分配、系统调用或网络协议栈,则应沿对应路径继续调查。

采样是统计方法。短时间采样可能遗漏低频路径;调用栈还可能受编译优化、帧指针和符号表影响。生产环境应控制采样频率和持续时间,避免不必要的开销。


四、内存:区分容量、回收、缺页与分配失败

4.1 “已用内存”不是一个足够准确的概念

Linux 会主动使用空闲内存作为 page cache,因此:

free -h

中的 used 不能直接表示应用已经耗尽内存。更有意义的是:

  • available:内核估计在不触发严重 swap 的情况下仍可供新分配使用的内存;
  • buff/cache:缓冲区和文件页缓存等;
  • swap used:已使用 swap 的容量,但不等于当前正在发生 swap I/O。

available 是内核估计值,不是严格保证;不同内核版本和内存回收状态会影响估算。

进一步查看:

cat /proc/meminfo
cat /proc/vmstat
cat /proc/pressure/memory

重要字段包括:

  • MemFree:真正未使用的页,往往比直觉中小;
  • CachedSReclaimable:部分可回收内存;
  • AnonPages:匿名页,如堆、栈;
  • Unevictable:不能正常回收的页;
  • Dirty:已修改、尚未写回的页;
  • SlabSReclaimable:内核对象缓存;
  • pgfault:缺页异常总数;
  • pgmajfault:主缺页异常,通常需要磁盘等慢路径;
  • pswpinpswpout:swap 读写页数。

4.2 缺页异常的两条路径

缺页异常表示进程访问的虚拟地址当前没有满足访问要求的页表映射。它不必然是错误,也不必然意味着磁盘 I/O。

次缺页

如果目标页已经在内存中,只是:

  • 页表项尚未建立;
  • 采用写时复制;
  • 访问了已缓存但尚未映射的文件页;

内核可以在内存中完成处理,称为 minor fault 或次缺页。它通常比主缺页便宜,但频繁发生仍会消耗 CPU。

主缺页

如果目标页不在内存中,需要从文件或 swap 读入,可能发生 major fault。路径大致为:

  1. CPU 触发页错误;
  2. 内核查找 VMA 和访问权限;
  3. 判断是匿名页、文件页、写时复制还是非法访问;
  4. 若页不在内存,提交或等待存储 I/O;
  5. 页面读入后建立页表映射;
  6. 返回用户态重新执行被中断的指令。

进程级观察:

pidstat -r -p <PID> 1 5

如果 majflt/s 增加,同时磁盘读延迟、I/O 队列和请求时延也增加,才有理由把缺页与磁盘瓶颈关联起来。仅有 pgfault 很高,不能证明发生了 swap。

4.3 虚拟内存地址并不等于物理内存占用

进程的虚拟地址空间可以远大于实际驻留内存。查看进程内存概览:

grep -E 'VmPeak|VmSize|VmRSS|RssAnon|RssFile|VmSwap|Threads' \
  /proc/<PID>/status
  • VmSize 是虚拟地址空间;
  • VmRSS 是当前驻留集的近似值;
  • RssAnon 主要是匿名内存;
  • RssFile 主要是文件映射驻留页;
  • VmSwap 表示该进程匿名页中有多少被换出,具体统计口径受内核版本影响。

RSS 还可能重复计算共享页。例如多个进程映射同一个共享库,每个进程的 RSS 都可能包含该页。要分析进程真实内存归属,可使用 PSS:

grep -E '^(Pss|Private_|Shared_)' /proc/<PID>/smaps_rollup

smapssmaps_rollup 的读取可能有成本,批量扫描大量进程时应谨慎。

4.4 内存回收与 Swap 的关系

当可回收内存不足时,内核可能:

  1. 回收干净的文件页;
  2. 写回 dirty 文件页;
  3. 回收 slab;
  4. 将匿名页写入 swap;
  5. 仍无法满足分配时触发 OOM。

因此 swap 不只是“多一层内存”,它改变了故障表现:程序可能不立即 OOM,而是进入极高时延的换入换出状态。

观察回收活动:

vmstat 1 10

siso 持续非零,且 wa、磁盘延迟、内存 PSI 同时升高,通常说明内存压力已经传导至存储。若只是 swap 曾经使用过,但当前 si/so 为零,不能据此判断当前存在 swap 瓶颈。

vm.swappiness 是回收策略倾向,不是“使用到多少百分比才开始 swap”的阈值。修改它会改变内核在匿名页和文件页之间的回收选择,不能替代对内存泄漏、工作集过大或 cgroup 限制的调查。

4.5 OOM 的故障路径

OOM(Out Of Memory)表示某次内存分配在当前约束下无法满足,不一定等于整台机器的物理内存绝对为零。约束可能来自:

  • 全局内存;
  • cgroup memory limit;
  • memcg 回收失败;
  • 高阶页分配失败;
  • 特定 GFP 分配上下文;
  • 不可回收或受限内存。

检查内核日志:

journalctl -k -b | grep -i -E 'out of memory|oom|killed process'
dmesg -T | grep -i -E 'out of memory|oom|killed process'

OOM killer 选择进程时会考虑内存使用、oom_score_adj 等因素,因此“占内存最多的进程一定被杀”并不是规范保证。

在 cgroup v2 中,重点文件通常包括:

cat /sys/fs/cgroup/memory.current
cat /sys/fs/cgroup/memory.max
cat /sys/fs/cgroup/memory.events
cat /sys/fs/cgroup/memory.pressure

memory.events 中的 oomoom_kill 有助于区分“发生过 OOM 条件”和“确实杀死了进程”。路径会因 systemd、容器运行时和 cgroup 挂载方式不同而变化,不能硬编码为所有发行版相同。


五、磁盘与块 I/O:从请求排队到文件系统完成

5.1 I/O 发生在多个层次

一次应用写文件,可能经过:

  1. 应用调用 write()
  2. 数据复制到 page cache;
  3. 文件系统更新页和元数据;
  4. 脏页达到阈值或后台线程触发回写;
  5. 文件系统提交块请求;
  6. 块层排队、合并和调度;
  7. 设备完成请求;
  8. 若应用调用 fsync(),还要等待持久化语义满足。

因此,write() 返回并不必然表示数据已经落盘。是否需要真正持久化取决于应用调用、文件打开方式、文件系统和设备语义。fsync() 通常比普通写更能暴露设备持久化延迟,但也会改变负载。

5.2 iostat 中的数字如何解释

iostat -xz 1 5

常见字段:

  • r/sw/s:每秒读写请求数;
  • rkB/swkB/s:每秒读写吞吐;
  • await:请求从进入设备队列到完成的平均等待时间,通常包括排队与服务时间;
  • aqu-sz:平均队列长度;
  • %util:设备忙碌时间比例的统计值。

%util 接近 100% 在单队列、单请求设备上常常意味着饱和,但在现代多队列 NVMe 上,它不等于设备性能已经耗尽;应结合 await、队列深度、吞吐、设备规格和请求大小判断。

利用 Little 定律可以建立一个近似关系:

L=λWL = \lambda W

其中:

  • LL:系统中平均存在的请求数,即平均队列长度;
  • λ\lambda:到达率,即 IOPS;
  • WW:请求平均停留时间,即平均延迟。

例如某设备平均处理 2000 IOPS,平均 await 为 10 ms:

L2000×0.010=20L \approx 2000 \times 0.010 = 20

如果 IOPS 不变而 await 升至 100 ms,平均队列长度约为 200。此时应用变慢的证据不是“磁盘百分比高”,而是请求在设备前排队和完成变慢。

5.3 /proc/diskstats 适合确认累计变化

cat /proc/diskstats

该接口提供设备级累计请求数、扇区数、进行中的 I/O、等待时间等字段。字段位置和解释应以当前内核文档为准,不建议手写脚本无条件假定所有内核字段完全一致。

iostatsar 等工具已经完成了差分和单位换算。诊断时应记录采样间隔,否则无法正确解释累计计数。

5.4 文件系统层与设备层可能出现不同答案

应用 write() 变慢,可能是:

  • page cache 分配内存受阻;
  • 文件系统锁竞争;
  • dirty page 回写导致 throttling;
  • 块层队列拥塞;
  • 设备本身延迟升高;
  • NFS 等远程文件系统等待网络;
  • fsync() 等待持久化。

因此应同时观察:

pidstat -d -p <PID> 1 5
iostat -xz 1 5
cat /proc/pressure/io

如果进程 I/O 时间升高,但块设备没有对应读写,可能是在文件系统、网络文件系统、锁或内存回收路径中等待。反过来,设备延迟升高但目标进程没有 I/O,也可能是其他进程造成的共享设备争用。

生产环境不要把以下命令当作无风险修复:

sync
echo 3 | sudo tee /proc/sys/vm/drop_caches

sync 会推动脏数据写回;drop_caches 会丢弃可回收 page cache 和 slab,造成后续冷缓存读延迟,通常只适合受控实验。它不能修复磁盘慢、内存泄漏或应用缓存设计问题。

5.5 fio 只能在隔离目标上使用

fio 可以构造可重复的 I/O 负载,例如:

fio --name=read-test \
    --filename=/var/tmp/fio-test \
    --size=1G \
    --rw=randread \
    --bs=4k \
    --iodepth=16 \
    --numjobs=1 \
    --direct=1 \
    --runtime=30 \
    --time_based \
    --group_reporting

这里:

  • randread 产生随机读;
  • bs=4k 指定请求块大小;
  • iodepth=16 指定异步队列深度;
  • direct=1 尽量绕过 page cache;
  • runtime=30 限制持续时间。

该命令只写入指定文件,但仍会消耗设备带宽和 IOPS。绝不能把测试文件改成生产块设备、数据库数据文件或未挂载设备;即使是文件测试,也应确认不会影响在线服务。


六、网络:从名称解析到网卡队列的完整路径

6.1 网络问题先区分 DNS、连接、传输和应用等待

一次访问通常经过:

  1. 解析名称;
  2. 查找路由;
  3. 建立 TCP 或 QUIC 等传输连接;
  4. 进行 TLS 握手;
  5. 发送请求;
  6. 等待服务端处理和响应;
  7. 接收数据并交给应用。

任一步骤都可能表现为“网络慢”。

6.2 DNS 证据

使用 dig 分离 DNS 解析:

dig example.com A
dig @1.1.1.1 example.com A +stats
dig +trace example.com

输出中应重点看:

  • status:如 NOERRORSERVFAILNXDOMAIN
  • ANSWER SECTION:是否返回预期记录;
  • Query time:本次查询的往返时间;
  • SERVER:实际使用的 DNS 服务器;
  • TTL:缓存有效期。

+trace 会从根开始迭代查询,适合确认委派、权威服务器和 DNSSEC 等路径,但它绕过本机递归解析器,不能直接代表应用实际使用的解析路径。

应用实际解析还受 /etc/nsswitch.conf/etc/resolv.conf、systemd-resolved、搜索域、IPv6 优先级以及 NSS 模块影响。因此 dig 成功不等于 getaddrinfo() 一定成功。

6.3 路由与邻居状态

ip addr
ip route
ip route get 203.0.113.10
ip neigh

ip route get 查询的是内核针对特定目标、源地址、接口和下一跳作出的路由选择,通常比只看 ip route 更接近一次实际发送。

邻居表中的 INCOMPLETEFAILED 可能表示 ARP 或 IPv6 邻居发现未完成。路由正确但邻居解析失败,数据包仍然不能正常发出。

修改路由、地址或邻居表会影响在线流量,不能把排障命令和修复命令混为一谈。只读查询通常风险较低,ip route add/del 则需要明确回滚方案。

6.4 ss 观察 Socket 状态

ss -s
ss -lntp
ss -antp
ss -ti
  • -l:监听 socket;
  • -n:不做名称解析;
  • -t:TCP;
  • -p:显示关联进程,可能需要权限;
  • -i:显示 TCP 内部信息。

需要区分:

  • SYN-SENT 多:客户端连接建立请求发出但未完成;
  • SYN-RECV 多:服务端收到 SYN,但握手尚未完成,可能是半连接队列或回包路径问题;
  • ESTAB 多:不代表一定健康,还要看发送/接收队列和 TCP 指标;
  • TIME-WAIT 多:主动关闭方保留连接状态,短连接高并发时可能正常;
  • Recv-Q 长时间增长:应用读取不及时或接收处理受阻;
  • Send-Q 长时间增长:对端读取慢、网络拥塞、发送受阻或应用持续产生数据。

ss -ti 中可能出现 rtt、重传、拥塞窗口等信息。TCP 指标是连接级状态,必须结合连接生命周期和采样时间判断,不能只看一个瞬时值。

6.5 ping 和 tcpdump 的边界

ping -c 5 203.0.113.10
sudo tcpdump -ni eth0 host 203.0.113.10 and port 443

ping 使用 ICMP;目标或中间设备可能限速、过滤 ICMP,所以 ping 失败不必然表示 TCP 业务不可用。反之 ping 正常也不证明 443 端口、TLS 或应用正常。

tcpdump 可以确认数据包是否经过某个观测点:

  • 客户端发出 SYN 后是否收到 SYN/ACK;
  • 是否存在重复 ACK、重传、RST;
  • DNS 请求是否发出、响应是否返回;
  • TLS 握手停在哪个方向。

抓包是高价值证据,但可能包含敏感数据、消耗磁盘和 CPU。生产环境应限制接口、过滤条件、持续时间和文件大小:

sudo timeout 30 tcpdump -ni eth0 \
  'host 203.0.113.10 and tcp port 443' \
  -c 2000 -w /tmp/trace.pcap

抓包只说明“数据包在此处看到的状态”,不能直接证明远端已经收到;需要在多个观测点或结合 TCP 确认、交换机和对端日志进行因果闭合。

6.6 网卡统计与软中断结合

ip -s link show dev eth0
ethtool -S eth0
ethtool -k eth0

ip -s link 可观察接口层收发包和错误;ethtool -S 可观察驱动和硬件统计,如丢包、ring 溢出、校验相关计数等,但字段依赖网卡驱动。

ethtool -k 显示 checksum offload、TSO、GSO、GRO 等卸载能力。抓包中看到的包大小和线上真实物理帧大小可能不同,因为抓包点可能位于卸载处理之前或之后。不能仅凭 tcpdump 中的“大包”判断网络线上存在同样大小的帧。


七、eBPF:把“现象”连接到“代码路径”

7.1 eBPF 解决什么问题

eBPF 是一种在内核中运行受验证程序的机制。程序通常附着到:

  • tracepoint;
  • kprobe/kretprobe;
  • raw tracepoint;
  • perf event;
  • cgroup;
  • socket、TC、XDP 等网络挂载点。

用户态程序负责加载、配置、读取 map 或 ring buffer,并在结束时卸载。内核 verifier 会检查程序的内存访问、控制流和调用能力;这不是“任意内核代码执行”。

一个典型生命周期是:

sequenceDiagram
    participant U as 用户态工具
    participant K as 内核 verifier
    participant H as 挂载点
    participant M as BPF map/ring buffer

    U->>K: 加载 BPF 程序与 map
    K->>K: 验证内存访问和控制流
    K->>H: attach
    H->>M: 事件或计数
    U->>M: 读取并聚合
    U->>H: detach
    U->>K: 释放程序和 map

常见工具分工:

  • bpftrace:快速编写短脚本;
  • bpftool:检查程序、map、链接和系统 BPF 状态;
  • BCC:提供较多现成工具,但依赖发行版和 Python/内核环境;
  • libbpf:适合构建长期运行、版本兼容要求更高的工具。

7.2 第一条可运行的 eBPF 证据

统计进程进入调度器的次数:

sudo bpftrace -e '
tracepoint:sched:sched_switch
{
  @[comm] = count();
}'

它统计的是调度切换事件中观察到的任务名,不是“每个任务实际运行了多少时间”。如果要观察调度延迟,应记录任务变为 runnable 的时间,再与真正切入 CPU 的时间相减;简单的 count() 无法回答这个问题。

统计系统调用入口:

sudo bpftrace -e '
tracepoint:syscalls:sys_enter_openat
{
  @[comm] = count();
}'

这适合确认某类调用是否异常频繁,但不能仅凭调用次数证明调用耗时。要分析耗时,需要在入口记录时间,在返回点计算差值,并正确处理并发和线程标识。

概念上的入口/返回脚本如下:

sudo bpftrace -e '
tracepoint:syscalls:sys_enter_read
{
  @start[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_read
/@start[tid]/
{
  @lat_us = hist((nsecs - @start[tid]) / 1000);
  delete(@start[tid]);
}'

这里:

  • tid 用于把同一线程的入口和返回配对;
  • nsecs 是 bpftrace 提供的单调时间;
  • hist() 生成对数直方图;
  • 入口记录后,即使系统调用返回错误,也会经过 exit tracepoint,能够清理状态。

但该示例仍有边界:

  • 过滤器为空,会观察全机所有线程;
  • 长时间阻塞的调用会使 @start map 增长;
  • 信号、异常路径和工具版本可能影响具体事件语义;
  • read() 的耗时不等于磁盘耗时,它也可能等待 socket、管道或 page cache。

生产环境应加入 PID、cgroup、进程名或设备过滤,并限制 map 大小和运行时间。

7.3 用 bpftrace 分析块 I/O 延迟

不同内核和发行版的 tracepoint 字段可能略有差异,先检查格式:

sudo cat /sys/kernel/tracing/events/block/block_rq_issue/format
sudo cat /sys/kernel/tracing/events/block/block_rq_complete/format

在确认字段后,可以使用类似脚本:

sudo bpftrace -e '
tracepoint:block:block_rq_issue
{
  @issue[args->dev, args->sector] = nsecs;
}
tracepoint:block:block_rq_complete
/@issue[args->dev, args->sector]/
{
  @lat_us = hist((nsecs - @issue[args->dev, args->sector]) / 1000);
  delete(@issue[args->dev, args->sector]);
}'

其推导过程是:

  1. block_rq_issue 表示请求进入块设备处理路径,记录时间;
  2. block_rq_complete 表示请求完成,查找同一请求;
  3. 两个时间点相减,得到从 issue 到 complete 的停留时间;
  4. 以直方图聚合,避免只看平均值掩盖长尾。

示例使用 dev + sector 作为配对键只是演示思路,实际请求合并、重用和字段语义可能使键设计需要调整;更稳妥的程序应使用 tracepoint 提供的请求标识或内核版本适配逻辑。不能未经验证地把示例直接当成所有设备和内核的生产工具。

7.4 eBPF 与传统工具的关系

eBPF 不是替代 vmstatiostatsstcpdumpperf,而是补充它们的关联能力:

  • iostat 告诉你设备延迟升高;
  • eBPF 可以进一步按进程、文件系统路径或调用栈归因;
  • ss 告诉你 socket 队列增长;
  • eBPF 可以跟踪应用何时调用 sendmsg()、内核何时处理网络事件;
  • perf 适合 CPU 采样;
  • eBPF 可以在特定 tracepoint 上做低侵入的事件过滤和聚合。

eBPF 也有限制:

  • 事件点不一定覆盖完整语义;
  • kprobe 依赖内核符号和实现,稳定性通常不如 tracepoint;
  • 内核配置、BTF、权限、锁定内核和容器安全策略可能阻止加载;
  • 采集调用栈、按 PID/文件/连接保存维度时,map 可能膨胀;
  • 过滤不当会对高频路径产生明显开销。

检查环境:

sudo bpftool feature probe kernel
sudo bpftool prog show
sudo bpftool map show

是否可执行还取决于当前用户权限、CAP_BPFCAP_PERFMONCAP_SYS_ADMIN 的发行版和内核配置组合,以及 LSM、安全策略和容器边界。命令失败时应先读取错误信息,而不是盲目降低安全策略。


八、把五类资源串成一条可验证证据链

8.1 从用户感知时延开始

假设 API 请求突然从 20 ms 变为 2 s。不要先执行“清缓存”或重启服务,而应建立时间窗口:

date -Is
uptime
vmstat 1 10
iostat -xz 1 10
mpstat -P ALL 1 10
ss -s
cat /proc/pressure/cpu /proc/pressure/memory /proc/pressure/io

若问题是间歇性的,持续采样的历史指标比事后一次性快照更重要。快照只能说明“现在是什么样”,不能说明“慢请求发生时是什么样”。

8.2 一个完整推导示例:内存压力导致 I/O 和请求时延

观察到:

  1. freeavailable 下降;
  2. vmstatsi/so 持续增加;
  3. iostatawaitaqu-sz 增加;
  4. /proc/pressure/memory/proc/pressure/iosome 增加;
  5. 应用线程栈停在读文件或内存分配相关路径。

因果推导为:

工作集超出可用内存回收匿名页或文件页swap/page-in 或回写块 I/O 排队线程睡眠请求时延升高\text{工作集超出可用内存} \rightarrow \text{回收匿名页或文件页} \rightarrow \text{swap/page-in 或回写} \rightarrow \text{块 I/O 排队} \rightarrow \text{线程睡眠} \rightarrow \text{请求时延升高}

为了排除“只是其他进程在做 I/O”,再执行:

pidstat -r -d -p <PID> 1 10

若目标进程的 majflt/s、I/O 速率和等待时间与慢请求同时出现,证据链更完整。若只有设备 await 高而目标进程没有 major fault,可能是数据库、日志进程或其他 cgroup 造成共享设备争用。

8.3 另一个示例:CPU 不满但服务仍慢

观察到:

  • 整机 CPU 使用率 40%;
  • 某个 CPU 接近 100%;
  • ss 显示监听 socket 的 Recv-Q 持续增长;
  • /proc/softirqs 中某个 CPU 的 NET_RX 增长很快;
  • 应用线程没有足够时间读取 socket。

推导为:

网卡收包集中到单个 CPUNET_RX 软中断占用该 CPU应用线程与软中断争用socket 接收队列增长请求处理延迟增加\text{网卡收包集中到单个 CPU} \rightarrow \text{NET\_RX 软中断占用该 CPU} \rightarrow \text{应用线程与软中断争用} \rightarrow \text{socket 接收队列增长} \rightarrow \text{请求处理延迟增加}

此时增加线程数未必有效,因为问题可能是 IRQ/RPS/XPS、CPU 亲和性、单队列网卡或处理路径集中。应结合:

cat /proc/interrupts
cat /proc/softirqs
ethtool -l eth0
ethtool -n eth0
ss -lnt

修改 IRQ 亲和性、RPS 或网卡队列属于生产变更,可能改善一个 NUMA 节点,却损害另一个节点;变更前需要记录原值并准备恢复。

8.4 反例:Load 高但 CPU 并不忙

假设 8 CPU 主机上:

  • load average 为 20;
  • mpstat 显示 %idle 很高;
  • vmstatb 很大;
  • iostat 显示设备 await 达到数百毫秒。

不能得出“CPU 不够,需要扩容 CPU”。更合理的解释是大量任务处于不可中断 I/O 等待,load 把它们计入了系统压力,但 CPU 没有工作可执行。此时应定位慢 I/O、设备争用、文件系统或远程存储,而不是修改调度参数。

8.5 反例:内存使用高但没有内存瓶颈

假设:

  • used 很高;
  • buff/cache 很高;
  • available 仍然充足;
  • si/so 为零;
  • memory PSI 接近零;
  • 应用请求时延正常。

这通常是 Linux 利用空闲内存缓存文件的正常表现。直接执行 drop_caches 可能让后续请求变慢,却没有解决任何问题。

8.6 反例:网络重传不一定是本机网卡丢包

TCP 重传可能来自:

  • 本机发包后链路丢失;
  • 对端接收缓慢导致拥塞控制;
  • 中间设备丢包;
  • 接收端校验、队列或处理不足;
  • 抓包点位置和 offload 导致观察偏差。

应同时比较:

ss -ti
ip -s link show dev eth0
ethtool -S eth0
sudo tcpdump -ni eth0 'host <peer> and tcp port <port>'

如果接口错误计数没有增长,但 TCP 重传增长,不能立即断定网卡健康后就排除链路问题;丢包可能发生在本机之后。需要对端或中间设备证据才能闭合因果。


九、诊断命令的风险、验证与恢复

9.1 低风险只读观察也有成本

以下操作一般是观察性质:

cat /proc/stat
cat /proc/meminfo
vmstat 1
iostat -xz 1
ss -s
ip route get <address>

但高频读取 /proc/<pid>/smaps、全量抓包、全机高频 eBPF 事件跟踪,仍然可能消耗 CPU、内存、锁和磁盘。观察本身也可能改变被观察系统,尤其是高频短系统调用和网络包路径。

9.2 变更操作必须保存原状态

下列操作会改变系统行为:

  • 修改 sysctl
  • 修改 CPU affinity、IRQ affinity、RPS/XPS;
  • 调整 cgroup 限制;
  • 清理 page cache;
  • 杀进程或重启服务;
  • 启动 fio
  • 加载高频 eBPF 程序;
  • 删除路由、邻居和连接。

变更前至少保存:

sysctl -a > /tmp/sysctl.before
ip route show table all > /tmp/routes.before
cat /proc/irq/*/smp_affinity_list > /tmp/irq-affinity.before 2>/dev/null

变更后必须用相同窗口重新采样,比较:

  • 用户请求时延;
  • 错误率;
  • CPU、PSI、I/O、网络队列;
  • 是否出现新的丢包、OOM 或软中断异常。

如果只看到资源指标改善,却没有用户指标改善,说明原假设可能不成立,或者瓶颈位于另一层。


十、推荐的分析顺序

一个可重复的性能分析流程可以压缩为以下因果顺序:

第一步:定义症状

明确是吞吐下降、平均延迟升高、P99 长尾、错误增加,还是偶发卡顿。记录时间范围、受影响对象和部署变更。

第二步:确认任务状态

ps -eo state,pid,tid,psr,pcpu,stat,wchan:32,comm
pidstat -w -r -d -p <PID> 1 10

判断线程是在运行、被抢占、等待锁、等待内存还是等待 I/O。

第三步:观察资源饱和度

vmstat 1
iostat -xz 1
mpstat -P ALL 1
cat /proc/pressure/{cpu,memory,io}

把 CPU 运行队列、内存 PSI、I/O PSI 和设备延迟放在同一时间轴上。

第四步:沿故障路径深入

  • CPU:perf、调度事件、软中断;
  • 内存:/proc/vmstat、major fault、cgroup memory events;
  • 磁盘:块设备延迟、进程 I/O、文件系统和回写;
  • 网络:DNS、路由、socket 队列、TCP 状态、抓包、网卡统计。

第五步:用 eBPF 做归因

只有当宏观指标已经指出方向,才使用 eBPF 缩小范围,例如:

  • 哪个进程产生了最多块 I/O;
  • 哪类系统调用出现长尾;
  • 哪个调用栈占据 CPU;
  • 哪个 cgroup 产生了大量网络或调度事件。

先过滤对象和事件,再开始采集;不要一开始就对全机所有高频事件做无限维度聚合。

第六步:验证假设而不是验证命令

一个有效假设应能预测多个可观测结果。例如“swap 导致请求变慢”应同时预测 major fault、swap I/O、磁盘等待和内存 PSI 上升;如果只出现其中一个,应保留其他解释。

性能分析的目标不是找到一个“最高”的数字,而是形成一条从请求、线程状态、内核路径到硬件或网络设备的可复现证据链,并能在变更后用同一组证据确认问题确实改善。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。