Linux 性能工程实战:从容器、网络丢包、动态追踪到监控与系统化优化

Linux 性能工程实战:从容器、网络丢包、动态追踪到监控与系统化优化

Linux 性能问题真正困难的地方,通常不是“不知道某个命令怎么用”,而是面对一个模糊现象时,不知道应该观察什么、怎样把现象映射到系统原理、如何逐层缩小范围,以及优化之后如何证明问题真的解决了。

容器启动慢、服务间歇性丢包、ksoftirqd CPU 占用高、Nginx 吞吐量骤降,看起来是完全不同的故障。把这些问题放到一起,会发现它们背后的方法高度一致:

先从用户可感知的性能现象出发,建立指标;再按照应用和操作系统的工作原理逐层缩小范围;当常规指标无法解释现象时,用 perf、ftrace、eBPF 等动态追踪深入调用路径;最后只针对已经确认的瓶颈优化,并用同一套基准重新验证。

这套方法比记住几十个性能命令更重要。工具只是观测系统的“传感器”,真正决定排障效率的是你对 CPU、内存、I/O、网络、容器隔离和应用执行模型的理解。

本文案例的技术背景主要处于 Ubuntu 18.04、Linux 4.x、Docker、Java 8/10 前后的环境。文末单独整理了现代 Linux、JDK 和容器环境下需要重新确认的版本差异,避免直接照抄历史配置。

一、把性能问题看成一个闭环,而不是一堆命令

一个完整的性能工程过程,可以抽象为下面这条链路:

flowchart LR
    A[用户现象<br/>慢、错误、吞吐下降、启动慢] --> B[建立可重复基线]
    B --> C[应用指标 RED<br/>Rate / Errors / Duration]
    B --> D[系统资源 USE<br/>Utilization / Saturation / Errors]
    C --> E[定位瓶颈层]
    D --> E
    E --> F[进程、协议栈、内核资源分析]
    F --> G[perf / ftrace / eBPF<br/>动态追踪与调用栈]
    G --> H[确认根因]
    H --> I[针对性优化]
    I --> J[重新压测与对比]
    J --> K[监控、日志、Trace 固化]
    K --> A

这里有两个重要原则。

第一,现象不是根因。CPU 100% 只是现象,可能是业务算法、频繁系统调用、软中断、锁竞争或内核网络处理引起的。网络超时也不意味着一定是“网络不好”,监听队列、连接跟踪、临时端口、应用处理过慢都可能表现为超时。

第二,优化之后瓶颈会迁移。一个高并发服务可能先被 nf_conntrack_max 卡住,调大后暴露 PHP 工作进程不足,再继续暴露监听队列太短、临时端口不足、TIME_WAIT 过多。性能优化不是找到一个参数改掉就结束,而是不断把当前最窄的瓶颈移走,直到系统达到目标。

二、性能瓶颈的两层模型:系统资源与应用程序

从故障定位角度,可以先把问题拆成两个观察面。

2.1 系统资源瓶颈

典型系统资源包括:

  • CPU;
  • 内存;
  • 文件系统与磁盘 I/O;
  • 网络;
  • 文件描述符;
  • Socket 队列;
  • 本地临时端口;
  • 连接跟踪表;
  • 内核队列与各种协议栈资源。

对这些资源最实用的抽象是 USE Method

  • Utilization(使用率):资源有多少比例正在被使用;
  • Saturation(饱和度):资源排队、等待的程度;
  • Errors(错误数):资源处理过程中出现了多少错误。

例如 CPU 使用率不高,不代表 CPU 没有问题。如果运行队列很长,线程长期等待调度,CPU 已经存在饱和;网络带宽没有跑满,也不代表网络没有问题,SYN 重传、丢包、监听队列溢出同样能让服务不可用。

2.2 应用程序瓶颈

应用问题又可以归纳成三类:

  1. 资源瓶颈:应用被 CPU、内存、I/O、网络或内核资源限制;
  2. 依赖服务瓶颈:数据库、Redis、RPC、消息队列、远程服务等变慢;
  3. 应用自身瓶颈:算法复杂度、锁、死锁、线程模型、工作进程数量、业务逻辑等出现问题。

因此,真正高效的排障不是“先看 CPU,再看内存,再看磁盘”这种固定流程,而是:

先确认用户现象发生在哪一层,再选择能够验证这个假设的指标。


三、容器化之后,性能边界发生了什么变化

容器没有改变 Linux 的基本性能分析原理,但它增加了新的边界:

  • namespace 改变进程看到的 PID、网络、挂载点等视图;
  • cgroups 改变 CPU、内存等资源的可用上限;
  • Overlay/联合文件系统增加 I/O 路径;
  • veth、bridge、NAT、Netfilter 增加网络路径;
  • CNI、CSI、GPU Device Plugin 等组件继续叠加新的依赖。

所以分析容器性能时,必须同时问两个问题:

进程实际能使用多少资源?

以及:

进程自己认为它能使用多少资源?

二者不一致时,非常容易产生诡异问题。

3.1 Java 容器 OOM:容器有 512 MB,JVM 却按主机内存算堆

一个典型案例是 Tomcat 容器:

1
2
3
4
5
6
docker run \
--name tomcat \
--cpus 0.1 \
-m 512M \
-p 8080:8080 \
-itd feisky/tomcat:8

应用逻辑会主动分配约 256 MB:

1
2
3
4
<%
byte data[] = new byte[256*1024*1024];
out.println("Hello, wolrd!");
%>

表面看,256 MB 小于容器 512 MB,应该不会 OOM。但实际访问一次后,容器退出。

第一步不是猜 JVM,而是先确认容器状态:

1
2
docker ps -a
docker inspect tomcat -f '{{json .State}}' | jq

关键状态类似:

1
2
3
4
5
{
"Status": "exited",
"OOMKilled": true,
"ExitCode": 137
}

OOMKilled=true 已经明确说明容器中的进程因为内存不足被杀死。137 通常对应进程收到 SIGKILL 后退出。

继续看内核日志:

1
dmesg

案例中可以看到类似信息:

1
2
3
4
memory: usage 524288kB, limit 524288kB
Memory cgroup out of memory: Kill process ... (java)
Killed process ... (java)
anon-rss:517316kB

这里已经把根因范围缩得很小:

  • OOM 来自 memory cgroup;
  • cgroup 上限确实是 512 MB;
  • Java 匿名内存接近 512 MB;
  • 主要不是页缓存,而是进程主动申请的内存。

再检查 JVM 自己认为最大堆能有多大:

1
2
3
docker exec tomcat \
java -XX:+PrintFlagsFinal -version |
grep HeapSize

案例输出:

1
2
InitialHeapSize := 132120576
MaxHeapSize := 2092957696

也就是说,最大堆接近 1.95 GB,远远超过容器的 512 MB。

再看容器内部:

1
docker exec tomcat free -m

当时的运行环境中,容器里的 free 仍然看到宿主机约 8 GB 内存,而不是 512 MB 的 cgroup 限额。旧版本 JVM 根据自己看到的主机物理内存做 Ergonomics,最终得到的最大堆也远大于容器上限。

这就是典型的:

cgroup 限制是 512 MB,但应用的资源自感知仍然是宿主机级别。

3.1.1 历史案例中的处理方式

案例通过显式 JVM 参数约束堆:

1
2
3
4
5
6
7
docker run \
--name tomcat \
--cpus 0.1 \
-m 512M \
-e JAVA_OPTS='-Xmx512m -Xms512m' \
-p 8080:8080 \
-itd feisky/tomcat:8

这样确实能避免 JVM 按宿主机内存把堆放大到接近 2 GB。

但工程上还要注意一个容易被忽略的问题:

容器内存限制不能简单等于 -Xmx

JVM 除了 Java Heap,还有 Metaspace、线程栈、Code Cache、Direct Buffer、JIT、GC 数据结构以及其他 Native Memory。生产环境应给这些非堆内存保留余量,而不是把 512 MB 容器完整交给 -Xmx512m

这个问题在案例留言中也被直接提出,说明仅仅“把 JVM 堆设成容器上限”并不是完整的容量规划方案。

3.2 容器启动慢:CPU 使用率只有 10%,为什么线程却等了 97%?

修复 OOM 后,Tomcat 能稳定运行,但启动仍然需要约 22 秒。

先看系统整体:

1
top

宿主机两个 CPU 都大量空闲,内存也非常充足。表面上看,机器没有压力。

这时如果只看“系统 CPU 使用率”,很容易误判。

继续针对容器中的 Java 线程看:

1
2
PID=$(docker inspect tomcat -f '{{.State.Pid}}')
pidstat -t -p "$PID" 1

关键不是 %CPU,而是 %wait。案例中部分 Java 线程:

1
2
%CPU 约 2% ~ 6%
%wait 最高达到 97%

这意味着线程大部分时间在等待 CPU 调度,而不是执行。

回看启动参数:

1
--cpus 0.1

容器只有 0.1 个 CPU,也就是大约 10% CPU 配额。Java 启动本身会进行类加载、验证、初始化、JIT 等大量工作,所以 CPU quota 很快成为启动阶段的饱和点。

把 CPU 上限从 0.1 调整到 1 后,案例启动时间从约 22 秒下降到约 2 秒

这个案例特别值得记住,因为它说明:

低 CPU 使用率不等于 CPU 不饱和。

如果系统有 Cgroup CPU quota,线程可能在配额耗尽后被 throttle。观察等待时间、调度和 cgroup 限流,比只盯着宿主机总体 CPU 更有意义。

3.3 为什么不能为了省事取消容器资源限制

最简单的“优化”当然是删掉 CPU、内存限制,让容器随便用。

问题是,这会把单个应用的问题放大成整台宿主机的问题。一个内存泄漏或 CPU 死循环,就可能把其他容器一起拖慢。

因此正确思路不是“不要限制”,而是:

  1. 为容器设置合理的 CPU、内存限制;
  2. 确保应用能够感知这些限制;
  3. 让 JVM、线程池、连接池等应用内部资源与容器配额匹配;
  4. 通过监控持续验证是否出现 throttle、OOM、排队等饱和现象。

四、从容器启动扩展到 Serverless 冷启动

冷启动不是单纯的“应用启动慢”,它是一条跨系统链路。

一个 Serverless/FaaS 实例从请求到可服务,大致经历:

flowchart LR
    A[事件触发] --> B[资源调度]
    B --> C[镜像拉取]
    C --> D[网络配置]
    D --> E[容器/沙箱启动]
    E --> F[应用初始化]
    F --> G[Ready]

因此冷启动分析最重要的问题不是先跑 top,而是:

总耗时分别花在了哪一个阶段?

应先用链路跟踪或分阶段埋点把启动过程拆开,再对最慢的阶段继续下钻。

典型优化方向包括:

  • 镜像拉取慢:热点镜像缓存、减小镜像体积;
  • 网络配置慢:网络资源预分配;
  • 调度慢:降低调度路径成本;
  • 容器启动慢:预创建或复用暖实例;
  • 应用初始化慢:继续用 CPU、I/O、动态追踪分析应用内部。

这也是性能分析中很重要的一个思想:

面对复杂系统,先拆端到端耗时,再优化最慢阶段;不要一上来就在单机里找热点。


五、网络丢包:沿着 Linux 网络收发路径逐层排查

网络丢包最容易出现“工具说没问题,但业务还是超时”的情况。原因是一个数据包从网卡到应用,要经过很多层,每一层都可能丢。

flowchart BT
    A[物理网络 / 虚拟交换] --> B[网卡与 Ring Buffer]
    B --> C[链路层 / qdisc / QoS]
    C --> D[IP 层 / 路由 / MTU]
    D --> E[Netfilter / iptables / conntrack]
    E --> F[TCP / UDP]
    F --> G[Socket 队列与缓冲区]
    G --> H[应用程序]

    B -. Ring 溢出 .-> X1[丢包]
    C -. tc/netem/QoS .-> X2[丢包]
    D -. 路由/MTU/分片 .-> X3[丢包]
    E -. DROP/表满 .-> X4[丢包]
    F -. 重传/端口/协议资源 .-> X5[异常]
    G -. backlog/缓冲区溢出 .-> X6[丢包]
    H -. 处理异常/超时 .-> X7[失败]

从链路底层到应用层,常见原因包括:

  • 物理链路拥塞或错误;
  • 网卡 Ring Buffer 溢出;
  • 帧校验失败;
  • tc/QoS 主动丢包;
  • 路由失败;
  • MTU 配置错误;
  • conntrack 表满;
  • iptables DROP/REJECT;
  • SYN 队列、Accept 队列、Socket 缓冲区满;
  • 应用处理不过来;
  • 客户端超时主动断开。

5.1 先选对探针:ping、hping3 和 curl 验证的不是同一件事

案例中 Nginx 使用 TCP 80 端口,所以不用 ICMP ping 作为主要验证工具,而是使用:

1
hping3 -c 10 -S -p 80 192.168.0.30

结果:

1
2
3
4
10 packets transmitted
5 packets received
50% packet loss
RTT: 3ms ~ 3s

这已经说明 TCP SYN 层面存在严重异常。

但要注意:

  • ping 验证 ICMP;
  • hping3 -S 主要验证 TCP SYN/SYN-ACK;
  • curl 才真正发送 HTTP 请求和 Payload。

因此“80 端口能握手”不能推出“HTTP 服务正常”。

这个差异后来正是定位 MTU 问题的关键。

5.2 链路层:先看网卡统计,再看 qdisc

进入容器:

1
docker exec -it nginx bash

查看接口统计:

1
netstat -i

经典字段:

  • RX-OK:成功接收包数;
  • RX-ERR:接收错误;
  • RX-DRP:接收丢包;
  • RX-OVR:Ring Buffer 溢出;
  • TX-*:发送方向对应指标。

但案例中网卡统计没有明显错误。这不能说明链路层彻底没问题,因为 tc qdisc 造成的丢包不会一定体现在这些接口统计中。

继续看 qdisc:

1
tc -s qdisc show dev eth0

案例输出:

1
2
qdisc netem ... loss 30%
Sent 432 bytes 8 pkt (dropped 4, ...)

说明 netem 人为配置了 30% 丢包。

删除规则:

1
tc qdisc del dev eth0 root

这里出现了第一个很典型的性能排障陷阱:

发现一个真实问题,不代表它就是唯一问题。

删掉 netem 后,业务仍然约 50% 丢包,必须继续向上层分析。

5.3 网络层与传输层:netstat -s 看重传、超时和握手失败

查看协议栈统计:

1
netstat -s

案例重点指标包括:

1
2
3
4
5
11 failed connection attempts
4 segments retransmitted
11 resets received for embryonic SYN_RECV sockets
TCPTimeouts: 7
TCPSynRetrans: 4

这些数据说明主要异常集中在 TCP 建连阶段:

  • 连接尝试失败;
  • SYN 重传;
  • TCP 超时;
  • 半连接状态被重置。

netstat -s 的价值在于告诉你“哪一层在异常”,但它通常不能直接告诉你“哪条规则、哪个函数、哪个包造成了异常”。

因此还需要继续检查 Netfilter、conntrack 和抓包。

5.4 conntrack:先比较当前值和上限

检查:

1
2
sysctl net.netfilter.nf_conntrack_max
sysctl net.netfilter.nf_conntrack_count

案例中:

1
2
nf_conntrack_max   = 262144
nf_conntrack_count = 182

当前远未达到上限,因此可以排除“连接跟踪表满”这个假设。

注意这里的方法比“看见 Docker 就怀疑 conntrack”更重要:

任何优化前都应该先验证资源是否真的耗尽。

5.5 iptables:不要只看规则,要看规则计数器

查看 filter 表:

1
iptables -t filter -nvL

案例发现 INPUT 和 OUTPUT 链都有 DROP 规则,且计数器持续增加。这两条规则使用 statistic 模块随机丢弃约 30% 的包。

对应删除形式可以写成:

1
2
3
4
5
iptables -t filter -D INPUT \
-m statistic --mode random --probability 0.3 -j DROP

iptables -t filter -D OUTPUT \
-m statistic --mode random --probability 0.3 -j DROP

删除之后:

1
hping3 -c 10 -S -p 80 192.168.0.30

TCP SYN 测试恢复为:

1
2
3
10 packets transmitted
10 packets received
0% packet loss

如果此时停止排查,会得到一个错误结论,因为真正的 HTTP 请求仍然超时。

5.6 hping3 正常、curl 超时:抓包把问题缩到 Payload

继续测试:

1
curl --max-time 3 http://192.168.0.30/

结果:

1
Operation timed out after 3000 milliseconds with 0 bytes received

抓包:

1
tcpdump -i eth0 -nn port 80

观察到:

  1. SYN;
  2. SYN+ACK;
  3. ACK;

三次握手正常。

接下来直到 3 秒超时,都没有看到 HTTP GET;最后只看到客户端发 FIN 关闭连接。

这时应该问:

为什么只带 TCP Header 的握手包能到,而携带 HTTP Payload 的包到不了?

再次查看接口:

1
netstat -i

案例中出现:

1
2
eth0 MTU = 100
RX-DRP = 344

默认以太网 MTU 通常是 1500,而容器接口被故意设成了 100。SYN 包很小,可以通过;HTTP GET 携带数据后,整个包超过 MTU,因此产生异常。

恢复 MTU:

1
ifconfig eth0 mtu 1500

随后 curl 恢复正常。

现代环境更推荐用:

1
ip link set dev eth0 mtu 1500

这个案例真正值得保留的不是“把 MTU 改成 1500”,而是下面这个推理链:

1
2
3
4
5
6
7
8
9
10
11
12
13
hping3 正常

curl 失败

说明握手与应用数据行为不同

tcpdump 看到三次握手,但没有 GET

接口 RX-DRP 增长

只有较大的数据包失败

检查 MTU

这就是“用协议原理解释工具差异”。


六、内核线程 CPU 高:从 ksoftirqd 进入内核调用栈

普通进程 CPU 高,可以用 stracepstacklsofperf 等工具。但内核线程不是普通用户态进程,很多进程级工具无法直接观察其内部行为。

6.1 先认识 Linux 的几个特殊进程

Linux 启动后,最重要的几个低 PID 进程是:

  • PID 0:idle/swapper,CPU 没有其他任务时执行;
  • PID 1:init/systemd,管理用户态进程;
  • PID 2:kthreadd,管理内核线程。

查看内核线程:

1
ps -f --ppid 2 -p 2

或者:

1
ps -ef | grep '\[.*\]'

常见内核线程包括:

内核线程 作用
ksoftirqd/N 处理软中断,每个 CPU 通常都有
kswapd 内存回收
kworker 执行内核工作队列
migration/N 调度负载均衡和 CPU 迁移
jbd2/* 文件系统 Journaling
pdflush 历史内核中的脏页回写线程,后续已并入其他机制

看到内核线程名字,本身就能给排障方向。

例如 ksoftirqd 高,第一反应就应该是:

软中断为什么这么多?是不是网络收发?

6.2 用 hping3 制造大量 SYN 后,软中断超过 30%

案例使用:

1
hping3 -S -p 80 -i u10 192.168.0.30

也就是每 10 微秒发一个 SYN。

服务器 top 中:

1
2
3
4
5
%Cpu0 ... 32.4 si
%Cpu1 ... 34.5 si

ksoftirqd/0
ksoftirqd/1

两个 CPU 的 si 软中断都超过 30%,ksoftirqd 也成为最显眼的内核线程。

这时知道“软中断高”还不够,还需要知道它到底执行了什么。

6.3 为什么 pstack 不适合,perf 可以

ksoftirqd/0

1
pstack 9

会出现无法 attach 的问题。

即使:

1
cat /proc/9/stack

也只能看到非常粗的内核线程框架:

1
2
3
smpboot_thread_fn
kthread
ret_from_fork

要看热点调用路径,可以用:

1
2
perf record -a -g -p 9 -- sleep 30
perf report

案例中能看到类似调用链:

1
2
3
4
5
6
7
8
run_ksoftirqd
└─ __do_softirq
└─ net_rx_action
└─ process_backlog
└─ netif_receive_skb
└─ br_handle_frame
└─ br_nf_pre_routing
└─ ...

这些函数已经非常有信息量:

  • net_rx_action:网络接收软中断;
  • netif_receive_skb:接收 skb;
  • br_handle_frame:经过 Linux bridge;
  • br_nf_pre_routing:bridge 上进入 Netfilter PREROUTING;
  • 后续还能看到 IP 转发、Netfilter、发送等路径。

这与 Docker 的网络拓扑完全对应:

1
2
3
4
5
6
7
8
9
10
11
12
13
物理网卡

宿主机协议栈

Netfilter / DNAT

docker0 bridge

veth pair

容器 eth0

Nginx

也就是说,ksoftirqd CPU 高不是一个“神秘内核问题”,而是大量网络包经过 bridge、Netfilter、容器网络路径后产生的软中断计算成本。


七、火焰图:把“热点函数列表”变成可阅读的调用关系

perf report 能看调用树,但层级一深就很难读。火焰图的价值在于把大量采样栈压成一个可交互的 SVG。

生成流程:

1
2
git clone https://github.com/brendangregg/FlameGraph
cd FlameGraph

假设 perf.data 已生成:

1
2
3
perf script -i /root/perf.data |
./stackcollapse-perf.pl --all |
./flamegraph.pl > ksoftirqd.svg

7.1 火焰图怎么看

两个轴一定不要理解反:

  • 横轴宽度:采样次数或采样占比,越宽越“热”;
  • 纵轴高度:调用栈深度;
  • 上层函数由下层函数调用;
  • 同一层左右位置通常不是时间顺序。

颜色通常只是为了区分函数,并没有天然的性能含义。

最重要的阅读动作是:

从底部向上,找最宽的调用链。

7.2 火焰图的常见类型

类型 关注点
on-CPU CPU 真正在执行什么
off-CPU 在等待 I/O、锁或其他资源时阻塞在哪
内存火焰图 内存分配、释放、换页等调用栈
热/冷火焰图 综合 on-CPU 与 off-CPU
差分火焰图 对比优化前后或两个时期的热点差异

CPU 火焰图和内存火焰图的绘制格式本身没有本质区别,区别主要在采样事件和函数栈来源

例如普通 perf record 常用于 CPU 采样;如果换成内存相关事件:

1
perf record -e page-fault -a -g -- sleep 60

得到的调用栈就主要围绕缺页行为。

也可以直接跟踪内存相关系统调用事件,例如:

1
2
3
perf record \
-e syscalls:sys_enter_mmap \
-a -g -- sleep 60

7.3 火焰图最容易被误解的两个点

第一,从某个顶部函数向下看到的是这个函数的调用栈,不是整个内核网络流程的完整流程图。TCP、conntrack 等路径可能分布在其他较窄的“火焰”里。

第二,普通火焰图不表达横向时间顺序。不要根据两个函数左右位置推断“先调用左边,再调用右边”。


八、动态追踪:当普通指标已经无法解释问题时

性能问题最麻烦的一类是:

  • 线上偶发;
  • 无法稳定复现;
  • 重启后消失;
  • 只有高并发或特定输入才出现;
  • 不能为了调试停服务;
  • 不能临时修改代码重新发版。

传统方法有两个明显限制:

  • GDB 断点会暂停程序;
  • 加日志需要修改代码、重新编译和部署。

动态追踪的核心价值是:

在内核或应用保持运行的同时,把探针挂到特定事件或函数上,观察正在发生什么。

相比基于 ptrace 的进程跟踪,动态追踪通常对目标程序行为干扰更小。资料给出的经验值是额外开销通常可以控制在约 5% 或更低,但实际开销仍然取决于探针数量、触发频率和采集内容。

8.1 动态追踪的三类事件源

硬件事件:PMC

Performance Monitoring Counter 可以记录:

  • CPU cycles;
  • instructions;
  • branch prediction;
  • Cache miss;
  • LLC 等硬件性能事件。

这类事件是 perf statperf record 很重要的数据源。

静态探针

静态探针在源码中预先定义,典型包括:

  • Tracepoint;
  • USDT(Userland Statically Defined Tracing)。

Tracepoint 在内核中非常常见;USDT 则用于用户态程序,MySQL、PostgreSQL 等软件都曾提供这类探针。

动态探针

运行时再附加,不需要源码预埋:

  • kprobe / kretprobe:内核函数进入与返回;
  • uprobe / uretprobe:用户态函数进入与返回。

8.2 从 DTrace 到 SystemTap,再到 eBPF

这套动态追踪体系的思想脉络可以理解为:

1
2
3
4
5
6
7
DTrace

SystemTap

Linux 原生追踪机制不断增强

ftrace / perf / eBPF

SystemTap 会把脚本编译为内核模块再加载执行,功能很强,但启动和调试信息依赖较重。在旧版本 RHEL 环境中,它长期是非常重要的选择。

eBPF 则把可验证的 BPF 程序加载到内核中运行,可以与 kprobe、uprobe、tracepoint、Maps 等机制组合,后来成为 Linux 可观测性和网络技术的重要基础。


九、ftrace:已知内核函数后,看它内部到底调用了什么

ftrace 非常适合这样的场景:

perf 已经告诉你某个内核函数可疑,现在想继续看它具体调用了哪些函数。

ftrace 通常通过 debugfs/tracefs 提供接口:

1
cd /sys/kernel/debug/tracing

如果 debugfs 没挂载:

1
mount -t debugfs nodev /sys/kernel/debug

查看支持的 tracer:

1
cat available_tracers

常用的包括:

  • function:函数跟踪;
  • function_graph:函数调用图。

查看可追踪函数和事件:

1
2
cat available_filter_functions
cat available_events

以历史内核中的 do_sys_open 为例:

1
2
3
4
5
6
7
8
9
echo do_sys_open > set_graph_function
echo function_graph > current_tracer
echo funcgraph-proc > trace_options

echo 1 > tracing_on
ls
echo 0 > tracing_on

cat trace

输出会显示:

  • CPU;
  • 任务名与 PID;
  • 函数执行耗时;
  • 缩进后的函数调用关系。

原生接口很灵活,但操作步骤多。trace-cmd 可以把流程简化:

1
2
3
4
5
6
7
trace-cmd record \
-p function_graph \
-g do_sys_open \
-O funcgraph-proc \
ls

trace-cmd report

一个非常实用的工具选择原则是:

perf / 火焰图适合“不知道热点在哪”;ftrace 适合“已经知道函数名,想继续深挖调用过程”。


十、perf 不只是采样:perf probe 与 perf trace

perf 最常见的用途是 CPU 采样,但它也能动态添加 probe。

查看事件:

1
perf list

添加内核函数探针:

1
perf probe --add do_sys_open

记录:

1
2
3
perf record \
-e probe:do_sys_open \
-aR sleep 10

查看:

1
perf script

如果只知道函数被调用还不够,还可以查询参数:

1
perf probe -V do_sys_open

在案例内核中可以看到:

1
2
3
4
5
char* filename
int dfd
int flags
struct open_flags op
umode_t mode

于是可以把 filename 加进 probe:

1
2
3
4
perf probe --del probe:do_sys_open

perf probe \
--add 'do_sys_open filename:string'

然后:

1
2
3
4
5
perf record \
-e probe:do_sys_open \
-aR ls

perf script

这样能直接看到进程正在打开哪些文件。

10.1 为什么性能敏感服务里不要随便 strace

strace 基于 ptrace。高频系统调用场景下,它可能产生两个明显影响:

  1. 频繁用户态/内核态切换;
  2. 通过信号和进程控制影响目标进程执行。

因此数据库、低延迟服务等对时序敏感的程序,不应该把长时间 strace 当成无成本操作。

perf trace 提供了类似的系统调用跟踪能力:

1
perf trace ls

并且可以做系统级跟踪,而不只局限于一个目标进程。

10.2 跟踪用户态函数

perf probe 也可以通过 uprobes 跟踪用户态二进制文件。

例如跟踪 Bash 的 readline 返回:

1
2
3
4
5
6
7
8
9
10
11
12
perf probe \
-x /bin/bash \
'readline%return +0($retval):string'

perf record \
-e probe_bash:readline__return \
-aR sleep 5

perf script

perf probe \
--del probe_bash:readline__return

查看二进制中的函数和参数:

1
2
perf probe -x /bin/bash --funcs
perf probe -x /bin/bash -V readline

如果函数名、参数无法解析,通常需要安装调试符号或使用带 -g 的版本重新编译。


十一、eBPF 与 BCC:把动态追踪变成可编程数据处理

ftrace 和 perf 很强,但灵活性仍然受限。eBPF 的不同之处在于,它允许把受验证的程序加载到内核中,让事件过滤、聚合、统计直接在内核侧完成。

可以把 eBPF tracing 简化成三步:

1
2
3
4
5
6
7
用户态生成 BPF 字节码

内核 verifier 校验并加载

挂到 kprobe / uprobe / tracepoint 等事件

通过 Maps / perf buffer 等向用户态输出

BCC(BPF Compiler Collection)进一步把很多重复工作封装成 Python/Lua API。

下面是资料中用于追踪 do_sys_open 的经典 BCC 思路。代码属于早期 BCC API 风格,理解结构比直接复制到当前内核更重要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
from bcc import BPF

prog = r"""
#include <uapi/linux/ptrace.h>
#include <uapi/linux/limits.h>
#include <linux/sched.h>

struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
char fname[NAME_MAX];
};

BPF_PERF_OUTPUT(events);

int hello(
struct pt_regs *ctx,
int dfd,
const char __user *filename,
int flags
) {
struct data_t data = {};

data.pid = bpf_get_current_pid_tgid();
data.ts = bpf_ktime_get_ns();

if (bpf_get_current_comm(
&data.comm,
sizeof(data.comm)
) == 0) {
bpf_probe_read(
&data.fname,
sizeof(data.fname),
(void *)filename
);
}

events.perf_submit(
ctx,
&data,
sizeof(data)
);

return 0;
}
"""

b = BPF(text=prog)

b.attach_kprobe(
event="do_sys_open",
fn_name="hello"
)

start = 0

def print_event(cpu, data, size):
global start

event = b["events"].event(data)

if start == 0:
start = event.ts

time_s = (
float(event.ts - start) /
1000000000
)

print(
"%-18.9f %-16s %-6d %-16s"
% (
time_s,
event.comm,
event.pid,
event.fname
)
)

b["events"].open_perf_buffer(
print_event
)

print(
"%-18s %-16s %-6s %-16s"
% (
"TIME(s)",
"COMM",
"PID",
"FILE"
)
)

while True:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
break

输出可以类似:

1
2
3
4
TIME(s)            COMM             PID    FILE
0.000000000 irqbalance 1073 /proc/interrupts
0.000175401 irqbalance 1073 /proc/stat
0.000258802 irqbalance 1073 /proc/irq/9/smp_affinity

BCC 的门槛比 perf 高,因为要理解:

  • C;
  • Python;
  • 内核数据结构;
  • 被跟踪函数的参数;
  • BPF Maps;
  • 内核版本特性。

但一旦需要“按参数分布聚合”“统计 I/O 大小直方图”“跟踪高频内核事件”这类任务,eBPF 的能力就远高于简单采样。


十二、动态追踪工具怎么选

可以把工具选择压缩成一张表:

场景 更适合的工具
不知道热点在哪,先找 CPU 热点 perf + 火焰图
已知内核函数,想看调用图 ftrace / trace-cmd
需要动态添加内核/用户态 probe perf probe
需要系统调用追踪,又不想长时间依赖 ptrace perf trace
新一些的 Linux,需要灵活统计与聚合 eBPF / BCC
旧版本 RHEL 内核 SystemTap
历史容器运行时综合分析 sysdig

资料中有一个非常形象的描述:

1
sysdig ≈ strace + tcpdump + htop + iftop + lsof + docker inspect

这更适合理解其定位:它试图从容器场景提供一个统一的系统事件观察面。

动态追踪通常还依赖 Debug Symbol。遇到:

1
The /bin/bash file has no debug information.

可以:

  • 安装对应 debuginfo/dbgsym;
  • 或重新编译应用并增加 -g

perf、eBPF 或 SystemTap 显示一个完全陌生的内核函数时,最可靠的解释来源仍然是对应内核版本源码。使用 Bootlin Elixir 这类内核源码浏览器,可以从函数定义继续追变量、宏和调用关系。


十三、吞吐量下降:一个性能瓶颈不断迁移的完整案例

吞吐量案例非常适合建立“系统性思维”,因为它不是一个单点问题,而是一连串资源限制。

架构是:

1
2
3
4
5
wrk / curl

Nginx
↓ FastCGI
PHP-FPM

13.1 初始基线:1000 并发,只有 189 QPS

压测:

1
2
3
4
wrk \
--latency \
-c 1000 \
http://192.168.0.30

初始关键结果:

1
2
3
1910 requests in 10.10s
Non-2xx or 3xx responses: 1910
Requests/sec: 189.10

所有响应都异常,并且吞吐只有约 189 QPS。

传输数据总量只有约 573 KB,因此带宽不是第一嫌疑。应从 TCP 连接和应用处理能力继续看。

13.2 第一层:连接跟踪表只有 200

为了观察现场,把压测时间拉长:

1
2
3
4
5
wrk \
--latency \
-c 1000 \
-d 1800 \
http://192.168.0.30

查看连接:

1
ss -s

案例中:

1
2
estab 5
timewait 1178

高并发 1000,但真正建立的连接只有个位数,显然不正常。

看内核日志:

1
dmesg | tail

发现:

1
nf_conntrack: table full, dropping packet

检查:

1
2
sysctl net.netfilter.nf_conntrack_max
sysctl net.netfilter.nf_conntrack_count

结果:

1
2
nf_conntrack_max   = 200
nf_conntrack_count = 200

确认表已满。

案例调大:

1
2
sysctl -w \
net.netfilter.nf_conntrack_max=1048576

重新压测:

1
Requests/sec: 5382.21

吞吐从 189 提升到 5382,接近 30 倍。

但还有:

1
Non-2xx or 3xx responses: 45577

也就是说,QPS 好看了,业务成功率仍然很差。

这是性能优化中非常重要的提醒:

不要只盯吞吐量。错误率和响应时间必须一起看。

13.3 第二层:Nginx 499,PHP-FPM max_children=5

Nginx 日志:

1
docker logs nginx --tail 3

出现大量:

1
499

Nginx 的 499 表示服务端还没来得及响应,客户端已经主动关闭连接。压测工具有超时,所以真正的问题仍然是服务器处理过慢。

再看 PHP-FPM:

1
docker logs phpfpm --tail 5

发现:

1
server reached max_children setting (5)

PHP-FPM 最多只有 5 个子进程,同时处理能力明显不足。

案例把 max_children 从 5 提高到 20。资料给出的经验是每个 PHP-FPM 子进程可能占用约 20 MB,但这个值必须以实际应用 RSS/PSS 为准,不能直接套用。

重新测试后,成功请求数量明显提升,但仍有大量异常。

13.4 第三层:Socket Listen Queue 溢出

检查协议统计:

1
netstat -s | grep socket

两次采样之间:

1
2
times the listen queue of a socket overflowed
SYNs to LISTEN sockets dropped

都在快速增长。

看监听队列:

1
ss -ltnp

案例里 Nginx 和 PHP-FPM 的队列都只有约 10,Nginx 当前队列已经顶到上限。

进一步检查:

1
2
3
4
5
6
7
8
docker exec nginx \
grep backlog /etc/nginx/nginx.conf

docker exec phpfpm \
grep backlog \
/opt/bitnami/php/etc/php-fpm.d/www.conf

sysctl net.core.somaxconn

案例中:

1
2
Nginx backlog = 10
somaxconn = 10

因此监听队列本身就是瓶颈。

资料的案例把应用 backlog 增大到 8192,把 somaxconn 增大到 65536。

这里还有一个非常重要的后续更正:原案例对半连接和全连接队列的区分不够严谨。高并发建连问题还应该检查:

1
sysctl net.ipv4.tcp_max_syn_backlog

因此生产排查时要同时区分:

  • SYN 半连接队列;
  • 已完成握手、等待 accept() 的全连接队列;
  • 应用 listen(backlog)
  • somaxconn 上限。

只改一个 somaxconn 并不能覆盖所有连接队列问题。

13.5 第四层:Nginx 作为客户端,临时端口范围只有 50 个

监听队列问题消失后,Nginx 又出现:

1
2
connect() to 127.0.0.1:9000 failed
Cannot assign requested address

也就是 EADDRNOTAVAIL

Nginx 连接 PHP-FPM 时自己是客户端,需要本地临时端口。

查看:

1
sysctl net.ipv4.ip_local_port_range

案例故意设成:

1
20000 20050

总共只有约 50 个临时端口。

扩展:

1
2
sysctl -w \
net.ipv4.ip_local_port_range="10000 65535"

错误减少,但整体吞吐反而下降,并出现较多 Socket read errors。

这不是“优化失败”,而是说明瓶颈继续迁移。

13.6 第五层:系统 CPU 高,火焰图指向临时端口分配

此时 top

1
2
3
CPU sy 接近 50%
CPU idle 约 2%
Nginx、dockerd 等进程 CPU 很高

于是用 perf + 火焰图:

1
perf record -g

再生成 Flame Graph。

热点调用链中出现:

1
2
3
4
do_syscall_64
tcp_v4_connect
inet_hash_connect
__inet_check_established

inet_hash_connect() 负责临时端口分配,__inet_check_established() 需要检查端口是否已经被占用。

查看:

1
ss -s

发现:

1
timewait 32768

大量 TIME_WAIT 占据端口,使寻找可用临时端口本身变成 CPU 热点。

再看:

1
sysctl net.ipv4.tcp_tw_reuse

案例为:

1
0

开启端口重用后,最终测试:

1
2
3
4
5
52119 requests in 10.06s
Requests/sec: 5180.48
Non-2xx or 3xx: 0
Socket read errors: 850
timeout: 0

虽然仍有少量 read error,但业务错误已经基本消失,吞吐稳定在 5000 QPS 以上。

13.7 这个案例真正要记住的不是 sysctl 数值

整个过程可以压缩为:

flowchart TD
    A[189 QPS + 全部错误] --> B[ss -s]
    B --> C[conntrack 表满]
    C --> D[调大 conntrack]
    D --> E[QPS 5382 但大量 499]
    E --> F[PHP-FPM max_children 太小]
    F --> G[成功数增加]
    G --> H[Listen Queue 溢出]
    H --> I[调整 backlog / somaxconn / SYN backlog]
    I --> J[EADDRNOTAVAIL]
    J --> K[临时端口范围过小]
    K --> L[扩大 local port range]
    L --> M[系统 CPU 高]
    M --> N[perf 火焰图]
    N --> O[TIME_WAIT 导致端口分配热点]
    O --> P[针对性处理]

这就是多瓶颈系统的典型特征:

每解决一层限制,下一层瓶颈才有机会暴露。

因此生产环境里最危险的做法,就是看到一个 sysctl 参数就直接“调大到很大”。


十四、系统监控:用 USE 把“资源是否有瓶颈”变成长期数据

现场排障工具有一个天然缺陷:

你登录服务器时,问题可能已经消失。

因此性能工程最终一定会走向监控。

14.1 USE 方法

一个实用的资源监控表可以这样建立:

资源 Utilization Saturation Errors
CPU CPU 使用率 运行队列、Load Hardware Error
内存 已用内存、Swap 使用率 换页、直接回收、内存压力 分配失败、OOM
存储设备 I/O busy 时间 I/O 队列、延迟 I/O Error
文件系统 容量、inode 使用率 空间/inode 接近上限 读写错误
网络 BPS/PPS、带宽利用率 重传、队列拥塞 网卡错误、丢包
文件描述符 已用 FD 比例 接近系统/进程上限 打开失败
conntrack count/max 接近表上限 table full/drop
TCP 连接 当前连接数 TIME_WAIT、SYN backlog reset、timeout

USE 不是说“只监控三个指标”,而是给每一种资源建立三个最重要的问题:

1
2
3
用了多少?
排队多不多?
有没有出错?

这样告警出现后,能很快知道是哪个资源进入了瓶颈状态。

14.2 大量性能工具最终都在读 /proc/sys

很多常见工具其实是对内核数据的不同展示:

CPU:

1
2
3
4
/proc/loadavg
/proc/stat
/proc/softirqs
/proc/interrupts

内存:

1
2
3
4
/proc/meminfo
/proc/slabinfo
/proc/<pid>/status
/proc/<pid>/maps

I/O:

1
2
/proc/diskstats
/sys/block/<device>/stat

网络:

1
2
3
/proc/net
/proc/net/dev
/proc/sys/net/netfilter/*

这说明监控系统和现场命令不是两套体系。监控系统只是把这些数据长期保存下来,让你在故障发生后仍然能回看现场。


十五、Prometheus:把采集、存储、查询、告警和可视化连起来

一个完整监控系统至少需要:

  • 数据采集;
  • 服务发现;
  • 时序存储;
  • 查询处理;
  • 告警;
  • 可视化。

Prometheus 的基本结构可以抽象为:

flowchart LR
    A[Targets / Exporters] -->|Pull| P[Prometheus Server]
    B[Short-lived Jobs] --> PG[Pushgateway]
    PG -->|Pull| P
    SD[Service Discovery<br/>Kubernetes / File SD] --> P

    P --> T[TSDB]
    P --> Q[PromQL]
    Q --> G[Grafana / Web UI]
    Q --> AM[Alertmanager]
    AM --> N[Email / IM / Pager]

Retrieval / Targets

Prometheus 最常见的是 Pull:目标暴露 HTTP Metrics Endpoint,Prometheus 定期采集。

对生命周期很短的任务,可以通过 Pushgateway 保存需要被拉取的指标。

Service Discovery

在 Kubernetes 等动态环境中,Pod、Service 会不断变化,静态写死监控地址不可维护,因此服务发现非常关键。

TSDB

监控数据天然是:

1
timestamp + metric + labels + value

所以适合时间序列数据库。

PromQL

PromQL 负责:

  • 查询;
  • 过滤;
  • 聚合;
  • 速率计算;
  • 告警表达式;
  • Grafana 数据源。

Alertmanager

告警不是越多越好。Alertmanager 通过:

  • 分组;
  • 抑制;
  • 静默;

减少告警风暴,把真正需要人处理的问题送出去。


十六、应用监控:RED 比 USE 更接近用户体验

系统资源监控关心“机器是否健康”,应用监控更关心:

用户请求是否健康。

对于服务和微服务,RED 是很实用的抽象:

  • Rate:单位时间请求量;
  • Errors:错误请求数或错误率;
  • Duration:请求处理耗时。

这三个指标也可以理解为:

1
2
3
吞吐量
成功率
延迟

一个接口 QPS 下降、P99 上升、错误率升高,即使 CPU 只有 20%,应用也已经有性能问题。

16.1 只有 RED 还不够

发生性能问题后,还需要继续定位,所以应用至少还应监控三类内部指标。

进程资源

例如:

  • CPU;
  • RSS/PSS;
  • GC;
  • 文件描述符;
  • I/O;
  • Socket;
  • 线程池;
  • 连接池。

依赖调用

每个 RPC/数据库/缓存依赖至少有:

  • 调用次数;
  • 错误数;
  • Duration。

关键业务阶段

例如一个订单接口内部可以拆成:

1
2
3
4
5
6
7
8
9
参数校验

查询库存

计算价格

写数据库

发消息

如果每一步都有耗时和错误指标,性能故障就不必从整个进程开始盲猜。


十七、Metrics、Trace、Logs 是互补关系

可观测性不应该只做一个 Grafana Dashboard。

17.1 Metrics:告诉你哪里异常

Metrics 是某个时间窗口上的数值:

1
2
3
4
5
QPS
P99
CPU%
memory bytes
retransmits/s

适合长期保存、聚合、告警。

17.2 Trace:告诉你请求在哪一跳慢

分布式调用链可以使用 Zipkin、Jaeger、Pinpoint 等系统。

入口生成 Trace ID,后续 RPC、数据库调用等都携带同一个 Trace Context,就可以重建完整请求路径:

1
2
3
4
5
6
7
Gateway
↓ 20ms
Order Service
↓ 15ms
Inventory Service
↓ 430ms
Redis

这样能迅速判断问题到底在:

  • 当前服务;
  • 某个下游;
  • 数据库;
  • Redis;
  • RPC 网络;
  • 还是某个具体 Span。

17.3 Logs:告诉你当时发生了什么

日志擅长保存上下文:

  • 请求参数;
  • 错误堆栈;
  • 状态码;
  • 业务对象;
  • SQL;
  • 异常分支。

经典日志架构是 ELK:

1
2
3
4
5
Logstash

Elasticsearch

Kibana

Logstash 负责采集和处理,Elasticsearch 负责索引与搜索,Kibana 负责检索和展示。

由于 Logstash 资源开销较大,资源紧张场景也常用 Fluentd 等更轻量采集器,形成 EFK 类架构。

三者的关系可以概括为:

1
2
3
Metrics:发现问题
Trace:缩小到调用链和组件
Logs:恢复具体上下文

十八、收到性能告警后,应该怎么分析

现在可以把分析流程收敛成一套工程化套路。

18.1 第一步:先明确用户现象

不要从 top 开始,而是先确定:

  • QPS 下降了吗;
  • 错误率上升了吗;
  • 平均延迟还是 P99 上升;
  • 是所有请求还是特定接口;
  • 是持续还是间歇;
  • 从什么时间开始;
  • 是否与发布、扩容、流量变化相关。

这一步决定后面要验证的假设。

18.2 第二步:从监控恢复故障现场

优先看问题窗口,而不是“现在”:

  • RED;
  • USE;
  • Pod/容器 throttle;
  • OOM;
  • GC;
  • 网络错误;
  • TCP 重传;
  • I/O latency;
  • dependency duration。

如果监控已经能把范围缩到某个进程,就不必从整台机器重新扫一遍。

18.3 第三步:确认系统资源瓶颈

CPU

第一层:

1
2
3
4
5
uptime
top
vmstat 1
mpstat -P ALL 1
pidstat 1

关注:

  • user/system/iowait/softirq;
  • load;
  • runnable;
  • context switch;
  • per-process CPU;
  • %wait

继续:

1
2
3
perf top
perf record -g
perf report

如果是软中断:

1
2
3
4
cat /proc/softirqs
cat /proc/interrupts
sar -n DEV 1
tcpdump

内存

第一层:

1
2
3
free -m
vmstat 1
sar -B 1

进程:

1
2
3
4
5
6
ps
top
pidstat -r 1
pmap <pid>
cat /proc/<pid>/status
cat /proc/<pid>/smaps

缓存/泄漏:

1
2
3
4
5
cachestat
cachetop
slabtop
memleak
valgrind

磁盘与文件系统

系统:

1
2
3
iostat -xz 1
sar -d 1
vmstat 1

进程:

1
2
pidstat -d 1
iotop

深入:

1
2
3
4
5
blktrace
biosnoop
biotop
strace
perf trace

网络

接口与吞吐:

1
2
sar -n DEV 1
ip -s link

连接:

1
2
ss -s
ss -ltnp

协议统计:

1
netstat -s

路由:

1
2
3
ip route
traceroute
mtr

DNS:

1
2
dig
nslookup

防火墙/NAT:

1
iptables -nvL

连接跟踪:

1
2
sysctl net.netfilter.nf_conntrack_count
sysctl net.netfilter.nf_conntrack_max

抓包:

1
tcpdump -i any -nn

18.4 第四步:如果系统资源正常,回到应用和依赖

如果 CPU、内存、I/O、网络都没有明显瓶颈,应用仍然慢,重点就转向:

  • 锁;
  • 线程池;
  • 连接池;
  • RPC;
  • 数据库;
  • 缓存;
  • 算法;
  • 热点函数;
  • 系统调用。

优先使用:

1
2
3
4
5
Trace
应用 Metrics
应用日志
perf
动态追踪

而不是继续随机调整 sysctl。


十九、性能优化的一般方法

找到瓶颈以后,优化通常比定位更依赖具体业务。但可以保留几个稳定的方向。

19.1 CPU 优化

核心目标:

减少无效工作、提高 Cache Locality、降低调度开销。

典型方法:

  • 优化算法;
  • 简化热点路径;
  • 减少不必要系统调用;
  • 批处理;
  • CPU Affinity;
  • 中断负载均衡;
  • 避免频繁线程切换;
  • 对非关键进程使用 cgroup 约束;
  • 给关键任务更合理的优先级。

19.2 内存优化

常见方向:

  • 修复泄漏;
  • 减少频繁动态分配;
  • 内存池;
  • Huge Page;
  • 控制缓存;
  • 为容器/进程设置内存上限;
  • 避免不受控 Swap;
  • 为关键服务做合理 OOM 策略。

注意,“禁用 Swap”不是任何场景的机械规则。它的核心目的,是避免延迟敏感服务在内存压力下突然进入高延迟换页路径。是否完全禁用,需要结合工作负载和现代容器编排策略判断。

19.3 磁盘和文件系统优化

典型方向:

  • SSD 替代慢磁盘;
  • 合理 RAID;
  • 调整 I/O Pattern;
  • 批量读写;
  • 增大单次 I/O;
  • 降低随机 I/O;
  • 利用 Page Cache;
  • 调整 Dirty Page 回写;
  • 按业务隔离磁盘;
  • 使用合适的 I/O Scheduler。

历史资料中常见 noopdeadline 等调度器名称;现代 blk-mq 环境下具体可用调度器和推荐方式要以当前内核、设备类型为准。

19.4 网络优化

内核资源

可能涉及:

  • Socket Buffer;
  • somaxconn
  • SYN backlog;
  • conntrack;
  • 文件描述符;
  • 临时端口范围;
  • Keepalive;
  • 重传;
  • TIME_WAIT;
  • MTU。

原则不是“全调大”,而是:

1
2
3
先证明限制发生
再计算容量
最后改参数

网卡能力

高吞吐场景可以利用:

  • RSS;
  • 多队列;
  • GRO;
  • GSO;
  • 硬件 offload;
  • IRQ Affinity。

极限网络性能

当 Linux 通用协议栈本身成为瓶颈,才考虑:

  • DPDK;
  • XDP。

这类技术换来的是更高复杂度,不应该用于普通 Web 服务的常规优化。

19.5 应用程序优化通常收益最大

系统参数经常只是把“上限”调高,真正能从源头减少工作量的地方仍然是应用。

典型方向:

  • 改算法;
  • 减少数据库访问;
  • SQL 优化;
  • 缓存;
  • 写时复制;
  • 合并 I/O;
  • 长连接;
  • DNS Cache;
  • I/O 多路复用;
  • 异步处理;
  • 合理线程/进程模型;
  • 消息队列;
  • CDN;
  • 负载均衡;
  • 水平扩展。

如果单机优化已经复杂到难以维护,通常应该考虑:

是否应该水平扩展,而不是继续追求单机极限。


二十、Linux 性能工具速查:从指标出发选工具

性能工具很多,但不要按“工具名”背,应该按“我要看什么指标”来选。

20.1 CPU

指标 常用工具 重点
平均负载 uptimetop/proc/loadavg 是否有长期排队
系统 CPU topvmstatmpstatsar user/sys/iowait/softirq
进程 CPU toppspidstathtop 谁在消耗 CPU
上下文切换 vmstatpidstat -w 调度开销
软中断 topmpstat/proc/softirqs 网络高并发常见
硬中断 vmstat/proc/interrupts IRQ 分布
CPU Cache perf stat cache miss 等硬件事件
热点函数 perf + 火焰图 调用栈
已知函数深挖 ftrace 精确函数调用
高级动态追踪 BCC/eBPF、SystemTap 自定义事件和聚合

20.2 内存

指标 常用工具
系统已用/可用内存 freevmstatsar/proc/meminfo
进程 RSS/VSS pstoppidstat -r/proc/<pid>/status
地址空间分布 pmap/proc/<pid>/maps
Swap freevmstatsar
缺页 pidstat -rsar -B
Page Cache cachestatcachetop
Slab slabtop/proc/slabinfo
泄漏 memleakvalgrind

20.3 文件系统与磁盘 I/O

指标 常用工具
文件系统容量 df
inode df -i
目录项/Slab slabtop/proc/slabinfo
IOPS/吞吐/延迟/队列 iostatsardstat/proc/diskstats
进程 I/O pidstat -diotop
Block I/O 跟踪 blktrace
Block 延迟 biosnoopbiotop
系统调用 straceperf trace
基准测试 fio

20.4 网络

指标 常用工具
BPS sarnethogsiftop/proc/net/dev
PPS sar -n DEV/proc/net/dev
连接数 ssnetstat
错误/重传 netstat -ssar -n EDEV
延迟 pinghping3
conntrack nf_conntrack_count/maxconntrack
路由 ip routemtrtraceroute
DNS dignslookup
防火墙/NAT iptables
网卡能力 ethtool
抓包 tcpdump、Wireshark
动态追踪 ftrace、BCC/eBPF、SystemTap
带宽基准 iperf
发包基准 pktgen
HTTP 基准 abwrk

选择工具时还要考虑现实约束:

  • 能不能安装软件;
  • 是否有 root;
  • 内核版本;
  • 是否允许产生额外开销;
  • 是否在线上;
  • 是否有调试符号;
  • 容器里还是宿主机里;
  • 是否需要进入 namespace。

二十一、把现场命令变成长期监控

真正成熟的性能体系,不应该等出问题后才临时执行:

1
2
3
4
top
iostat
ss
netstat

更好的做法是把这些命令背后的指标长期采集。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
CPU:
user/system/softirq
run queue
context switch

Memory:
available
swap in/out
page fault
OOM

Disk:
util
await
queue
errors

Network:
BPS/PPS
drops
retransmits
SYN backlog
conntrack

Application:
Rate
Errors
P50/P95/P99
dependency duration

这样一次性能故障可以形成完整闭环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
告警

历史指标恢复现场

确定问题层级

现场动态追踪

找到根因

优化

基准对比

新增监控项,避免下次重复排查

每解决一次事故,都应该让下一次事故更容易定位。


二十二、现代环境下需要重新确认的历史差异

这些案例非常适合学习方法,但其中很多命令和结论来自 2019 年前后的运行环境。迁移到现代 Linux、Kubernetes 和 JDK 时,需要区分“方法”与“具体实现”。

22.1 Java 已增强容器资源感知

历史案例中的 Java 8 环境看不到 cgroup 限制,导致 JVM 按宿主机内存计算 Heap。

Java 10 开始明显增强容器资源感知,后续 JDK 继续完善 cgroup 支持。现代 JDK 通常能够根据容器限制计算可用内存和 CPU。

但仍然应该实际验证:

1
java -XX:+PrintFlagsFinal -version

以及容器中的:

  • Max Heap;
  • GC;
  • Direct Memory;
  • Native Memory;
  • CPU Processor Count。

不要因为“JDK 支持容器”就放弃容量验证。

22.2 cgroups v1 与 v2 文件不同

历史 Docker 环境大量使用 cgroups v1。现代发行版越来越多采用 cgroups v2。

概念仍然一样:

1
2
3
CPU quota
Memory limit
IO limit

但控制文件、指标和观测方式会变化。

22.3 netstat / ifconfig 已偏历史

文章中大量使用:

1
2
netstat
ifconfig

它们来自 net-tools

现代 Linux 更常见:

1
2
ss
ip

例如:

1
2
3
4
5
6
ip -s link
ip addr
ip route
ip link set dev eth0 mtu 1500
ss -s
ss -ltnp

22.4 iptables 可能只是 nftables 的兼容前端

现代发行版常使用 nftables,iptables 命令可能由 iptables-nft 后端实现。

因此遇到防火墙/NAT 问题时,要先确认当前系统到底使用哪套规则体系,而不是假设所有机器都仍然是旧式 xtables。

22.5 内核函数名不是稳定 API

示例中的:

1
do_sys_open

在不同内核版本中可能已经发生变化或被其他路径替代。

因此动态追踪示例最重要的是方法:

1
2
先确认当前内核符号
再挂 probe

而不是记死某个函数名。

22.6 早期 BCC 示例不代表现代 eBPF 的唯一写法

现代 eBPF 生态还有:

  • bpftrace;
  • libbpf;
  • CO-RE;
  • BTF;
  • 更丰富的现成 tracing 工具。

但是 BCC 示例仍然很适合理解:

1
2
3
4
5
6
7
8
9
Probe

BPF Program

Kernel Event

Map / Buffer

Userspace

22.7 不要复制案例里的 sysctl 数值

下面这些值都是为了演示瓶颈而设计的案例参数:

1
2
3
4
5
nf_conntrack_max = 200
somaxconn = 10
ip_local_port_range = 20000 20050
CPU = 0.1
MTU = 100

即使优化后的:

1
2
3
4
nf_conntrack_max = 1048576
somaxconn = 65536
backlog = 8192
ip_local_port_range = 10000 65535

也不能直接作为生产推荐值。

正确做法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
业务并发模型

连接建立速率

连接生命周期

内存消耗

队列长度

当前系统限制

容量计算

再调整

tcp_tw_reuse 等参数在不同内核版本上的实现和适用条件也可能变化,更不应该只因为“TIME_WAIT 多”就机械开启。


二十三、一套可以真正落地的性能排障 Checklist

遇到性能问题时,可以按下面顺序执行。

1. 明确用户现象

1
2
3
4
5
6
吞吐?
错误?
延迟?
启动?
只有某接口?
只有某时间段?

2. 建立或复现基线

记录:

1
2
3
4
5
QPS
P50/P95/P99
错误率
资源使用
测试参数

3. 看应用 RED

1
2
3
Rate
Errors
Duration

4. 看系统 USE

1
2
3
4
5
6
7
8
CPU
Memory
Disk
Network
FD
Socket
Conntrack
Port

5. 找到最可疑进程/组件

1
2
3
4
5
top
pidstat
ss
iostat
sar

6. 按系统原理继续缩小

例如网络:

1
2
3
4
5
6
7
8
9
10
11
12
13
网卡

qdisc

IP

Netfilter

TCP

Socket

应用

7. 常规指标解释不了时再动态追踪

1
2
3
4
perf
火焰图
ftrace
eBPF/BCC

8. 修改最小变量

一次只修改能够验证当前根因的关键配置或代码,避免多个优化同时上线导致无法归因。

9. 使用相同压测重新验证

必须比较:

1
2
3
4
5
6
7
吞吐量
延迟
错误率
CPU
内存
I/O
网络

不能只证明“某一个数字变好了”。

10. 把新发现补进监控

如果这次事故是:

1
conntrack table full

那么以后就应该长期监控:

1
nf_conntrack_count / nf_conntrack_max

而不是等下一次再跑 dmesg


二十四、总结:性能分析的核心是“用原理解释指标”

把容器 OOM、慢启动、网络丢包、ksoftirqd、动态追踪、Nginx 吞吐优化和监控体系放到一起,会得到一套非常稳定的方法论。

容器性能问题,要把 cgroups、namespace、容器网络和应用自身资源模型一起看;网络故障,要按 网卡、qdisc、IP、Netfilter、TCP、Socket、应用逐层排查;内核线程异常,要借助 perf 和火焰图从热点函数理解内核正在做什么;常规指标不足时,再通过 ftrace、perf probe、eBPF/BCC动态追踪真实执行路径。

系统资源用 USE 建立长期指标,应用服务用 RED 关注吞吐、错误和延迟,再通过 Metrics、Trace、Logs 把“发现问题、定位组件、恢复上下文”串成一个闭环。

真正值得形成肌肉记忆的不是:

1
2
CPU 高就用哪个命令
网络慢就改哪个 sysctl

而是:

1
2
3
4
5
6
7
8
9
10
11
现象是什么

哪个指标能证明它

这个指标在系统原理中对应哪一层

当前数据和正常工作机制哪里不一致

还能用什么工具把范围继续缩小

如何验证根因而不是继续猜

性能工具只是手段,系统原理才是索引。

当你能够把一个异常指标放回 Linux 的执行路径中解释清楚,性能排障就不再是“碰运气调参数”,而会变成一个可以重复、可以验证、可以监控、可以持续改进的工程过程。


Linux 性能工程实战:从容器、网络丢包、动态追踪到监控与系统化优化
https://allendericdalexander.github.io/2026/08/11/devops/linux/performanceOptimization/06linux-performance-engineering-practical-methodology/
作者
AtLuoFu
发布于
2026年8月11日
许可协议