Linux 性能工程实战:从监控、定位、动态追踪到容器与网络调优

Linux 性能工程实战:从监控、定位、动态追踪到容器与网络调优

Linux 性能问题真正困难的地方,通常不是“不知道某个命令怎么用”,而是不知道应该从哪里开始、下一步看什么、看到异常后如何把指标与系统机制对应起来

CPU、内存、磁盘 I/O、网络只是性能问题的表象分类。进入真实系统后,一个看起来像“网络慢”的问题,可能来自连接跟踪表耗尽;一个“容器启动慢”的问题,可能并不是 Docker 本身慢,而是 CPU cgroup 把 JVM 启动阶段压到了极低的调度配额;一个“HTTP 超时”的问题,甚至可能在 TCP 三次握手完全正常的情况下,由 MTU、监听队列、临时端口或应用工作进程配置触发。

因此,一套可复用的性能工程方法应该把以下几件事串起来:

  1. 用监控保存问题现场,而不是等故障发生后再“登录服务器碰运气”。
  2. 从系统原理出发,把问题映射到 CPU、内存、I/O、网络或应用调用链中的具体层次。
  3. 先用低成本工具确定方向,再用 perf、火焰图、ftrace、eBPF/BCC 等动态追踪手段深入热点。
  4. 优化必须围绕已经证实的瓶颈进行,并通过同一套基准测试验证收益和副作用。
  5. 系统与应用不是两套割裂的方法论。系统瓶颈会拖慢应用,应用的不合理行为同样会制造系统瓶颈。

本文围绕这条主线,建立一套从监控、诊断、动态追踪、案例定位到优化验证的 Linux 性能工程体系。

版本背景:文中的主要实验环境来自 Ubuntu 18.04、Docker、Java 8、Nginx、PHP-FPM 等环境。很多原理至今仍然成立,但部分具体命令、内核接口和默认行为已经演进。例如现代 JDK 默认具备更完善的容器资源感知能力,现代发行版常使用 cgroup v2、nftables 后端以及更新的块 I/O 调度器。遇到版本差异时,应以当前内核、JDK 和发行版文档为准,不要机械复制旧环境参数。

1. 性能工程的核心:指标只是现象,机制才是因果

性能排障最容易陷入两个误区。

第一个误区是“看到什么高就优化什么”。CPU 高就扩 CPU,内存高就加内存,TIME_WAIT 多就调内核参数。这种方式偶尔有效,但很容易把真正的问题藏得更深。

第二个误区是“工具驱动”。手里有 top 就看 top,有 perf 就上 perf,会 eBPF 就什么都用 eBPF。性能工具应该由待验证的假设来选择,而不是反过来让工具决定分析路径。

更稳定的方式是:

flowchart LR
    A[业务症状/监控告警] --> B[建立可量化指标]
    B --> C[按系统/应用原理定位层次]
    C --> D[用低成本工具验证假设]
    D --> E{证据是否足够}
    E -- 否 --> F[perf/ftrace/eBPF/抓包等深入追踪]
    F --> D
    E -- 是 --> G[确认瓶颈机制]
    G --> H[最小范围优化]
    H --> I[同基准回归验证]
    I --> J[纳入持续监控]

整个流程有两个关键动作:

  • 把当前状态与系统原理进行比较:哪里不符合正常机制,哪里就是优先排查对象。
  • 每一步尽量缩小搜索空间:先确认“哪类资源”,再确认“哪个进程”,再确认“哪个函数/协议层/队列”,不要一上来就抓全量数据。

2. 监控先行:没有历史数据,偶发问题几乎无法复盘

很多性能故障具有瞬时性:高负载持续几十秒、连接跟踪表偶尔打满、某个依赖服务在特定时间抖动。等工程师登录服务器后,现场可能已经恢复。

因此,监控系统的价值并不只是“画图”,而是持续保留证据,使性能分析从“现场侦查”升级为“有时间线的取证”。

2.1 系统资源:USE 方法

USE(Utilization, Saturation, Errors)方法把一类资源的性能状态压缩为三个维度:

  • Utilization(使用率):资源被实际使用的时间或容量比例。
  • Saturation(饱和度):资源是否出现排队、等待或无法继续接收更多工作。
  • Errors(错误数):资源处理失败、丢弃、异常等事件。

只看使用率很容易误判。例如 CPU 只使用了 30%,但任务可能长期受 cgroup 配额限制并处在调度等待状态;磁盘利用率未必 100%,但高尾延迟和长队列同样可以让业务卡住。

资源 使用率 饱和度 错误/异常
CPU user/system/总使用率 run queue、load average、调度等待 硬件错误、异常中断
内存 已用/可用内存、Swap 占比 换页、回收压力、缺页异常 分配失败、OOM
磁盘 I/O 设备利用率、吞吐、IOPS I/O 队列长度、延迟 I/O 错误
文件系统 空间、inode 使用率 缓存回收压力 读写错误、只读挂载等
网络 BPS/PPS、接口利用率 重传、队列、连接状态压力 丢包、网卡错误、协议错误
文件描述符 已使用数量 接近上限 EMFILE/ENFILE
连接跟踪 nf_conntrack_count / max 接近表容量 table full, dropping packet
TCP 连接 建连/活跃连接 SYN 队列、accept 队列、TIME_WAIT reset、retransmit、timeout

USE 的作用不是代替所有指标,而是帮你快速确定“哪个资源最值得继续深挖”。系统日志、进程明细、缓存统计、协议扩展统计等依然应该保留,只是它们通常属于第二层证据。

2.2 应用与微服务:RED 方法

应用程序的核心问题不是“CPU 有没有吃满”,而是用户看到的服务质量。因此应用层更适合 RED:

  • Rate:请求速率,例如 QPS/RPS。
  • Errors:失败请求数或错误率。
  • Duration:请求处理时延,尤其要关注 P95/P99 等尾延迟。

CPU 很低并不能证明应用正常。锁竞争、RPC 卡顿、数据库慢查询、线程池排队、下游超时,都可能让 CPU 很空闲但接口很慢。

在 RED 之外,应用至少还应暴露三类补充指标:

  • 进程本身的 CPU、内存、磁盘 I/O、网络使用;
  • 对依赖服务的调用次数、失败率、时延;
  • 应用内部关键阶段的耗时、队列长度、异常数和业务状态。

2.3 指标、日志、链路追踪是互补关系

只靠 Metrics 很容易知道“哪里红了”,但不一定知道“为什么红”。日志提供上下文,分布式追踪提供跨组件的因果路径。

flowchart TD
    M[Metrics 指标] --> A[发现异常与趋势]
    T[Tracing 链路追踪] --> B[定位跨服务瓶颈节点]
    L[Logs 日志] --> C[还原参数/异常/业务上下文]
    A --> D[性能根因]
    B --> D
    C --> D

一个微服务请求延迟异常,可以先从 RED 指标确认哪条接口和哪个时间段出现问题,再用 Jaeger、Zipkin、Pinpoint 等追踪系统定位到某个下游调用,最后利用对应组件日志解释具体错误。

日志侧的经典架构是 ELK:

  • Logstash:采集与预处理;
  • Elasticsearch:索引和检索;
  • Kibana:查询与可视化。

当日志采集端资源敏感时,可以使用更轻量的采集器形成 EFK 等组合。核心思想不变:指标负责度量,日志负责上下文,追踪负责跨组件因果关系

2.4 Prometheus 监控架构

一个完整监控系统至少包含采集、存储、查询处理、告警和可视化。

flowchart LR
    T[Targets / Exporters] -->|Pull| P[Prometheus Retrieval]
    SD[Service Discovery] --> P
    J[Short-lived Jobs] --> PG[Pushgateway]
    PG --> P
    P --> TSDB[TSDB]
    TSDB --> Q[PromQL]
    Q --> AM[Alertmanager]
    Q --> G[Grafana / Web UI]

Prometheus 的关键点包括:

  • 默认以 Pull 模式从 HTTP 指标端点抓取数据;
  • 服务发现可以自动适配动态实例,尤其适合容器和 Kubernetes;
  • TSDB 保存时间序列;
  • PromQL 负责查询、过滤、聚合;
  • Alertmanager 负责分组、抑制、静默和通知;
  • Grafana 负责构建面向排障的仪表板。

Pushgateway 不应被理解为通用 Push 替代方案。工程上它更适合无法被稳定 scrape 的短生命周期批任务,长期服务仍应优先暴露可拉取的指标端点。

3. 一套通用的性能问题分析步骤

当监控告警已经告诉你“服务变慢”或“某资源异常”,可以把问题分成两个视角。

3.1 系统资源视角

最常见的是 CPU、内存、磁盘/文件系统 I/O、网络,以及连接跟踪、文件描述符、套接字队列等内核软件资源。

分析顺序不是固定命令表,而是从指标逐层缩小:

flowchart LR
    A[系统告警] --> B{资源类别}
    B --> C[CPU]
    B --> D[内存]
    B --> E[磁盘/文件系统]
    B --> F[网络/内核资源]
    C --> C1[系统还是进程?]
    D --> D1[不足/泄漏/Swap/缓存?]
    E --> E1[设备还是进程/文件系统?]
    F --> F1[接口/协议/队列/连接跟踪?]
    C1 --> G[定位进程/函数]
    D1 --> G
    E1 --> G
    F1 --> G

3.2 应用视角

应用性能问题通常最终落在三类原因:

  1. 资源瓶颈:应用把 CPU、内存、I/O、网络或内核资源打满。
  2. 依赖服务瓶颈:数据库、缓存、中间件、RPC 下游慢或失败。
  3. 应用自身瓶颈:算法复杂度、锁竞争、线程模型、队列、错误重试、系统调用模式等。

这三类并非互斥。例如应用创建海量短连接,本质上是应用行为,但最终可能表现为 TIME_WAIT、临时端口耗尽、conntrack 表满和系统 CPU 升高。

4. CPU:从 top 到热点函数

CPU 性能分析要先理解几个不同概念:

  • user CPU:用户态执行时间;
  • system CPU:内核态执行时间;
  • iowait:CPU 等待 I/O 的时间表现;
  • softirq/hardirq:软中断和硬中断;
  • steal:虚拟化环境中被宿主机抢占的时间;
  • load average:可运行和不可中断任务的综合压力,不等于 CPU 使用率。

4.1 常见分析链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 全局快速观察
top
vmstat 1
mpstat -P ALL 1

# 定位高 CPU 进程和线程
pidstat -u -t 1
ps -eo pid,ppid,comm,%cpu,%mem --sort=-%cpu | head

# 查看上下文切换
pidstat -w -p <PID> 1

# 进一步找热点函数
perf top
perf record -g -p <PID> -- sleep 30
perf report

如果 system CPU 或 softirq 异常高,不要只盯用户进程。大量网络收包、Netfilter、文件系统、块设备、驱动等工作都可能消耗内核 CPU。

5. 内核线程 CPU 高:为什么普通进程工具不够用

Linux 启动后有几个特殊进程:

  • PID 0:idle/空闲任务;
  • PID 1:init/systemd,管理用户态进程;
  • PID 2:kthreadd,管理内核线程。

常见内核线程包括:

内核线程 作用
ksoftirqd/N 处理软中断,每个 CPU 通常都有
kswapd 内存回收
kworker/* 内核工作队列
migration/N 调度迁移和负载均衡
jbd2/* ext4 等文件系统日志相关处理
pdflush 老内核中的脏页回写线程,后续已整合进工作队列机制

可以从 PID 2 的子孙快速找到它们:

1
2
ps -f --ppid 2 -p 2
ps -ef | grep '\[.*\]'

5.1 ksoftirqd 高 CPU 案例

用 hping3 高频发送 TCP SYN:

1
hping3 -S -p 80 -i u10 192.168.0.30

服务端 top 可能看到:

  • 两个 CPU 的 si 超过 30%;
  • ksoftirqd/0ksoftirqd/1 出现在高 CPU 进程顶部。

普通的 pstack 对内核线程并不好用,/proc/<pid>/stack 也可能只展示到 kthread 框架层。此时 perf 更合适:

1
2
perf record -a -g -p <KSOFTIRQD_PID> -- sleep 30
perf report

典型调用链可以出现:

1
2
3
4
5
6
7
net_rx_action
-> process_backlog
-> netif_receive_skb
-> br_handle_frame
-> br_nf_pre_routing
-> ...
-> ip_rcv

这些符号提供了强烈的机制线索:

  • net_rx_action / netif_receive_skb:网络收包路径;
  • br_*:Linux bridge;
  • nf_*:Netfilter;
  • PRE_ROUTING:数据包在路由前经过的 Netfilter 钩子,容器端口映射/NAT 可能参与其中。

当 Docker 创建 veth、接入 docker0、配置 NAT 后,网络包比裸机进程多经过桥接和 Netfilter 路径,因此在高 PPS 场景下会增加软中断和内核协议处理开销。

6. 火焰图:把调用栈变成可读的热点地图

perf report 可以展开调用栈,但调用层级较深时不够直观。FlameGraph 把采样调用栈转换为 SVG。

生成流程通常是:

1
2
3
4
5
6
git clone https://github.com/brendangregg/FlameGraph
cd FlameGraph

perf script -i /root/perf.data \
| ./stackcollapse-perf.pl --all \
| ./flamegraph.pl > flame.svg

理解火焰图要记住四点:

  • 横轴宽度代表采样比例,越宽说明在采样数据中占比越大;
  • 纵轴表示调用栈深度,下层调用上层;
  • 同一层左右位置通常不是执行时间顺序,不要把横轴当时间轴
  • 普通火焰图中的颜色通常只是区分函数,不代表“越红越慢”。

常见火焰图类型:

类型 数据关注点 适用问题
On-CPU 正在消耗 CPU 的调用栈 CPU 高、热点函数
Off-CPU 阻塞/等待时的调用栈 锁、I/O、调度等待
内存火焰图 分配、释放、换页等事件 内存分配热点、缺页
Hot/Cold On-CPU + Off-CPU 同时观察运行与等待
差分火焰图 两组 profile 的差异 优化前后、版本对比

CPU 火焰图与内存火焰图的格式没有本质区别,真正不同的是采样事件和输入调用栈。例如:

1
2
3
4
5
# 默认常用于 CPU 周期采样
perf record -g -p <PID> -- sleep 30

# 以缺页事件作为数据源
perf record -e page-faults -g -p <PID> -- sleep 30

7. 动态追踪:当采样还不能解释“为什么”

性能采样擅长回答“时间主要花在哪”,动态追踪进一步回答“某个事件发生时到底带了什么参数、经过什么调用、发生了多少次”。

动态追踪的关键思想是:在不修改业务代码、不停止服务的情况下,把处理逻辑挂到探针上

7.1 动态追踪的事件源

事件源大体可以分成三类。

静态探针

编译时已经存在:

  • tracepoints:Linux 内核预定义跟踪点;
  • USDT(Userland Statically Defined Tracing):应用程序预埋用户态探针。

动态探针

运行时按需挂载:

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

硬件事件

由 PMC(Performance Monitoring Counter)提供,例如:

  • CPU cycles;
  • instructions;
  • cache miss;
  • branch miss。

7.2 常见动态追踪机制

flowchart TD
    E[事件源] --> TP[Tracepoints / USDT]
    E --> KP[kprobes / uprobes]
    E --> PMC[PMCs]
    TP --> F[ftrace]
    TP --> P[perf]
    KP --> F
    KP --> P
    KP --> B[eBPF/BCC]
    PMC --> P
    TP --> B
    KP --> S[SystemTap]

历史上 DTrace 建立了非常完整的动态追踪模型;Linux 生态后来形成了 ftrace、perf、SystemTap、eBPF/BCC 等方案。

8. ftrace:已知内核函数时,查看精确调用过程

ftrace 很适合这样的场景:你已经通过 perf 知道某个内核函数可疑,希望进一步查看它具体调用了谁、每层耗时如何。

ftrace 通常通过 tracefs/debugfs 暴露接口:

1
2
3
4
cd /sys/kernel/debug/tracing
cat available_tracers
cat available_filter_functions
cat available_events

以跟踪某个 open 路径相关内核函数为例,传统方式大致包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 指定函数
printf '%s\n' do_sys_open > set_graph_function

# 选择 function_graph tracer
echo function_graph > current_tracer
echo funcgraph-proc > trace_options

# 开始
printf '1\n' > tracing_on
ls
printf '0\n' > tracing_on

# 查看
cat trace

function_graph 输出会包含 CPU、任务/PID、函数耗时和嵌套调用关系。

直接操作 tracefs 步骤较多,可以用 trace-cmd 封装:

1
2
trace-cmd record -p function_graph -g do_sys_open -O funcgraph-proc ls
trace-cmd report

内核函数名称会随版本变化。文中示例的 do_sys_open 在新内核中可能已经被拆分或替换。实际操作前应先在 available_filter_functions、BTF、符号表或当前内核源码中确认。

9. perf:从采样器升级为事件追踪器

perf 不只会 record CPU。它还可以处理硬件事件、tracepoint,以及通过 perf probe 动态添加 kprobe/uprobe。

9.1 动态添加内核函数探针

1
2
3
4
perf list
perf probe --add do_sys_open
perf record -e probe:do_sys_open -aR sleep 10
perf script

如果需要函数参数,可以先查询调试符号:

1
perf probe -V do_sys_open

假设要读取 filename

1
2
3
4
perf probe --del probe:do_sys_open
perf probe --add 'do_sys_open filename:string'
perf record -e probe:do_sys_open -aR ls
perf script

9.2 straceperf trace

strace 基于 ptrace,非常方便,但对性能敏感进程可能产生明显扰动。大量系统调用会带来频繁跟踪开销,跟踪机制还可能影响目标进程的调度行为。

当目标是低侵入地观察系统调用时,可以考虑:

1
perf trace ls

这并不意味着 strace 不能用,而是要意识到观察者本身会改变系统。越是高 QPS、低延迟场景,越需要关注工具开销。

9.3 用户态 uprobe

对带符号信息的用户态二进制,也可以追踪函数进入/返回。例如对 Bash 的 readline

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

如果 perf probe 提示没有 debug information,通常有两种解决方式:

  1. 安装应用对应的 debuginfo/dbgsym 包;
  2. 使用源码重新编译,并启用 -g 等调试信息。

10. eBPF/BCC:把“观察”变成可编程的数据处理

ftrace 和 perf 很强,但在“按条件过滤、聚合直方图、关联事件、维护状态”方面不够自由。eBPF 允许受验证的 BPF 程序在内核中执行,再通过 maps、ring/perf buffer 等机制与用户态交换数据。

从概念上可以简化为三步:

flowchart LR
    A[用户态跟踪程序] --> B[编译为 BPF 字节码]
    B --> C[Verifier 校验并加载到内核]
    C --> D[kprobe/uprobe/tracepoint 等事件]
    D --> E[Maps / Buffer]
    E --> F[用户态聚合与展示]

BCC 把编译、加载、事件绑定、数据读取等通用部分封装起来,使脚本开发更容易。一个典型结构是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from bcc import BPF

program = r"""
#include <uapi/linux/ptrace.h>

int trace_open(struct pt_regs *ctx) {
u64 pid_tgid = bpf_get_current_pid_tgid();
// 真实脚本中可以读取参数、写 map 或向用户态提交事件
return 0;
}
"""

b = BPF(text=program)
b.attach_kprobe(event="do_sys_open", fn_name="trace_open")
b.trace_print()

这里最重要的不是背代码,而是理解 eBPF 的能力边界:它适合做事件级、可编程、低侵入的观测,但开发者需要理解内核函数参数、数据结构、事件模型以及 eBPF 本身的限制。

BCC 自带大量现成工具,通常应该先复用,再考虑自己写。常见方向包括:

  • execsnoop:进程创建;
  • opensnoop:文件打开;
  • biolatencybiosnoop:块 I/O;
  • tcpconnecttcpaccepttcpretrans:TCP;
  • runqlat:调度延迟;
  • offcputime:Off-CPU 分析;
  • oomkill:OOM 事件。

11. 动态追踪工具怎么选

不要追求“最先进”,要选择能以最低风险验证假设的工具。

场景 优先工具 原因
CPU 热点、调用栈概览 perf + FlameGraph 通用、成本低、适合先找方向
已知内核函数,想看调用过程 ftrace / trace-cmd 函数图直观
系统调用快速观察 perf trace / strace 使用简单;性能敏感场景优先低侵入方案
需要参数过滤、统计分布、事件关联 eBPF/BCC 可编程性最强
旧 RHEL/旧内核环境 SystemTap 历史上对旧内核支持更成熟
容器行为综合观察 sysdig 等容器观测工具 将系统调用、网络、进程、容器元数据关联

一个非常实用的组合是:

1
2
3
4
5
6
7
监控 / top / ss / iostat

perf + 火焰图找热点

ftrace 或 eBPF 对热点做定点追踪

结合源码和协议机制确认根因

12. 容器化为什么会让应用“突然变慢”

容器不是虚拟机,它共享宿主机内核,但通过 namespaces 隔离视图,通过 cgroups 限制资源。性能问题因此多了一层:应用看到的世界与调度器真正允许它使用的资源,可能并不完全一致

12.1 案例:Java 容器 OOM

案例容器限制为 512 MiB,并给 Java 应用分配一块约 256 MiB 的对象:

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

现象是:Tomcat 最终启动并短暂响应一次请求,随后容器退出。

先确认容器状态:

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

关键状态:

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

再看内核日志:

1
dmesg | tail -n 100

可以发现 memory cgroup 已达到 512 MiB 限制,Java 进程因 cgroup OOM 被杀。

12.2 为什么限制 512 MiB,JVM 却可能规划出更大的堆

在旧 JDK/旧容器环境中,JVM 可能依据宿主机物理内存计算默认堆大小,而不是依据容器的 cgroup 限制。

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

docker exec tomcat free -m

案例中 JVM 看到的最大堆接近 2 GiB,而容器真实内存限制只有 512 MiB。

资料中的演示通过显式 -Xms/-Xmx 解决:

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

但生产环境不能简单理解成“容器 512 MiB,Xmx 就设置 512 MiB”。JVM 还需要 Metaspace、线程栈、JIT Code Cache、Direct Memory、JNI/native 内存等空间,进程和 cgroup 本身也存在其他开销。堆上限必须给非堆和 native memory 留出余量

现代 JDK 已经具备更成熟的容器资源感知能力,但仍建议通过监控确认 heap、native memory 和 cgroup memory 的实际关系,而不是只相信默认值。

12.3 OOM 修复后为什么启动仍然要 22 秒

问题容器还设置了:

1
--cpus 0.1

即只允许大约 0.1 CPU 的配额。

全局 top 看起来 CPU 很空闲,但针对容器 Java 线程运行:

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

可以看到线程 %CPU 不高,却出现非常高的 %wait。这说明不是“系统没 CPU”,而是线程受 cgroup CPU 配额节流,长期等待被调度

把配额从 0.1 CPU 提升到 1 CPU 后,案例启动时间从约 22 秒下降到约 2 秒。

这说明性能分析必须区分:

  • 宿主机是否有空闲 CPU;
  • 容器是否被允许使用这些 CPU;
  • 应用是否因 cgroup throttling 被迫等待。

12.4 为什么不能干脆取消所有资源限制

取消限制会让单个异常容器消耗整机资源,最终把其他服务一起拖垮。正确做法不是“不要限制”,而是:

  1. 用压测和生产监控估算资源需求;
  2. 给 CPU/内存设置合理 request/limit 或 cgroup 配额;
  3. 让运行时(JVM、线程池、缓存、worker 数)理解这份资源预算;
  4. 同时监控 throttling、OOM、工作队列和尾延迟。

13. 容器冷启动:不要只测一个总耗时

Serverless/FaaS 的冷启动可以拆成多个阶段:

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

总耗时只是结果。真正有用的是每个阶段的 duration。

可以通过链路追踪或阶段埋点记录:

  • 调度器排队时间;
  • 镜像是否命中本地缓存、拉取耗时;
  • CNI/网络 namespace 配置耗时;
  • runtime 创建容器耗时;
  • JVM/语言运行时初始化耗时;
  • 应用加载配置、建立连接池、JIT/类加载耗时。

不同阶段对应完全不同的优化方式:热点镜像缓存、网络资源预分配、预热容器池、应用延迟初始化等。冷启动性能分析的核心仍然是:先拆阶段,再找最大耗时,再深入其子流程

14. 网络丢包:必须沿协议栈逐层排查

“丢包”不是一个单独位置的故障。包可能在物理链路、NIC ring buffer、qdisc、链路层、IP 层、TCP 层、Netfilter、socket buffer,甚至应用层被丢弃或超时。

flowchart TB
    A[应用] --> B[Socket]
    B --> C[TCP/UDP]
    C --> D[IP]
    D --> E[Netfilter / 路由]
    E --> F[qdisc / Traffic Control]
    F --> G[NIC / Ring Buffer]
    G --> H[物理网络]

14.1 先确认是不是与真实协议一致的“丢包”

Nginx 使用 TCP,因此测试 TCP 80 端口时,hping3 比 ICMP ping 更贴近目标路径:

1
hping3 -c 10 -S -p 80 192.168.0.30

案例出现 50% packet loss,RTT 从数毫秒波动到秒级,已经具有典型重传/超时特征。

14.2 链路层与 qdisc

进入容器后先看接口:

1
netstat -i

关注:

  • RX-ERR / TX-ERR;
  • RX-DRP / TX-DRP;
  • RX-OVR / TX-OVR;
  • MTU。

随后检查 Traffic Control:

1
tc -s qdisc show dev eth0

案例发现 netem loss 30%,并已有 dropped 计数。这说明响应包被 qdisc 主动丢弃。

删除规则:

1
tc qdisc del dev eth0 root

但再次测试仍有丢包,这恰好说明一个重要原则:找到一个问题,不代表已经找到全部问题

14.3 网络层与传输层累计统计

1
netstat -s

重点寻找:

  • failed connection attempts;
  • segments retransmitted;
  • TCPTimeouts;
  • TCPSynRetrans;
  • resets received for embryonic SYN_RECV sockets。

案例中大量失败集中在三次握手和 SYN 重传,说明还需要继续检查 Netfilter、连接跟踪等路径。

netstat -s 是自系统启动以来的累计值。线上排障应比较故障窗口前后的增量,或直接在监控系统中记录 counter rate,而不是只看一个绝对累计值。

14.4 连接跟踪表

Docker/NAT 环境尤其要检查 conntrack:

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

如果 count 接近 max,并且 dmesg 出现:

1
nf_conntrack: table full, dropping packet

就已经有非常强的根因证据。

14.5 iptables/Netfilter 规则

对丢包问题,DROPREJECT 规则的包计数比规则文本本身更重要:

1
iptables -t filter -nvL

案例中 INPUT 和 OUTPUT 都存在随机 30% DROP 规则,而且命中计数持续增长。删除这些规则后,TCP SYN 测试变为 0% 丢包。

现代发行版可能通过 nftables 兼容层实现 iptables 命令;排障时要确认实际规则后端。

14.6 SYN 正常,不等于 HTTP 正常

删除 DROP 后,hping3 访问 80 端口完全正常,但:

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

仍然超时。

这时抓包:

1
tcpdump -i eth0 -nn port 80

可以看到 TCP 三次握手完成,但没有看到预期的 HTTP GET 数据,3 秒后客户端发送 FIN。

再次检查接口统计,发现 RX-DRP 持续增加,同时接口 MTU 被设置成了 100。SYN 包很小可以通过,而携带 HTTP 数据的包更大,因此行为出现差异。

恢复 MTU:

1
2
3
4
5
# 旧环境示例
ifconfig eth0 mtu 1500

# 新环境更常见的写法
ip link set dev eth0 mtu 1500

此后 HTTP 正常。

这个案例说明:

  • TCP 端口可达 != 应用请求可用;
  • 小包正常 != 大包正常;
  • 同一故障可以叠加多个独立根因;
  • 抓包的价值不是“看有没有包”,而是把协议应有的交互实际捕获到的交互逐帧比较。

15. 吞吐量断崖式下降:一个完整的多瓶颈案例

另一个案例使用 Nginx + PHP-FPM,通过 wrk 并发 1000 连接压测。起始吞吐量只有约 189 req/s,且几乎全部是非 2xx/3xx 响应。

这个案例很有价值,因为它不是一个根因,而是连续暴露了多个瓶颈。

15.1 第一步:先看连接状态,不要先猜 CPU

1
2
wrk --latency -c 1000 -d 1800 http://192.168.0.30
ss -s

1000 并发下,Established 只有个位数,而 TIME_WAIT/closed 却超过千级。

再看内核日志:

1
dmesg | tail

出现:

1
nf_conntrack: table full, dropping packet

查询发现:

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

测试环境故意把最大连接跟踪数设成了 200,并已全部用完。提升上限后,吞吐量从约 189 提升到 5382 req/s。

这里的 1048576 是案例调试值,不是“生产通用最佳值”。conntrack 上限增加会消耗更多内存,应该根据连接规模、内存预算和哈希表行为评估。

15.2 第二步:吞吐量高了,但 499 仍然很多

查看 Nginx 日志:

1
docker logs nginx --tail 20

大量状态码 499。Nginx 的 499 表示客户端在服务器响应完成前主动断开,通常意味着客户端超时而服务端处理太慢。

继续看 PHP-FPM 日志:

1
docker logs phpfpm --tail 20

出现:

1
server reached max_children setting (5)

说明 PHP-FPM 最大 worker 数只有 5,在高并发下形成应用级排队。增加 worker 后成功请求数量明显提升。

这里体现了系统性能与应用性能的联动:conntrack 是内核资源,max_children 是应用工作模型,两者都会把同一个 RPS 指标压低。

15.3 第三步:监听队列溢出

再次压测并比较协议统计增量:

1
netstat -s | grep -E 'listen|socket|SYN'

可以看到:

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

查看监听队列:

1
ss -ltnp

案例中 Nginx 和 PHP-FPM 的 backlog 只有 10,系统 somaxconn 也只有 10。

1
sysctl net.core.somaxconn

监听队列的实际能力受应用传给 listen() 的 backlog 与内核上限共同影响。只调应用、不调内核,或者只调内核、不调应用,都可能没有效果。

15.4 第四步:临时端口范围只有 50 个

Nginx 作为 FastCGI 客户端连接本机 PHP-FPM,需要临时端口。错误日志出现:

1
connect() ... failed (99: Cannot assign requested address)

查询:

1
sysctl net.ipv4.ip_local_port_range

案例故意配置为:

1
20000 20050

只有 50 个临时端口,显然无法支撑大量短连接。扩大范围后,地址不可用问题明显缓解。

15.5 第五步:参数调大后,CPU 反而成了新瓶颈

前面消除了 conntrack、worker、backlog 和端口范围限制后,系统终于能处理更多请求,结果 system CPU 接近 50%,CPU idle 只剩约 2%。

这很正常:优化一个瓶颈,常常会把系统推向下一个瓶颈

此时用 perf + 火焰图看到热点:

1
2
3
4
do_syscall_64
-> tcp_v4_connect
-> inet_hash_connect
-> __init_check_established

inet_hash_connect() 与临时端口分配相关,__init_check_established() 需要检查候选端口是否可用。

再看:

1
ss -s

发现约 32768 个 TIME_WAIT。大量 TIME_WAIT 占用端口,内核为新连接寻找可用端口的成本上升。

案例继续检查:

1
sysctl net.ipv4.tcp_tw_reuse

并在测试环境中开启可复用后,吞吐量恢复到约 5180 req/s,且不再有大规模 Non-2xx/3xx。

tcp_tw_reuse 之类 TCP 参数具有明确的协议语义和版本差异,不能因为“案例有效”就无脑在生产打开。优化前应确认连接方向、内核版本、NAT 拓扑和实际 TIME_WAIT 来源。很多时候更根本的优化是减少不必要的短连接、启用连接池或长连接。

15.6 这个案例真正值得记住的不是参数,而是分析顺序

整个根因链可以画成:

flowchart TD
    A[吞吐量 189 req/s + 大量失败] --> B[ss: estab 少 TIME_WAIT 多]
    B --> C[dmesg: conntrack table full]
    C --> D[扩大 conntrack 后 RPS 大增]
    D --> E[Nginx 499]
    E --> F[PHP-FPM max_children=5]
    F --> G[成功请求上升]
    G --> H[netstat: listen queue overflow]
    H --> I[backlog/somaxconn 过小]
    I --> J[connect EADDRNOTAVAIL]
    J --> K[临时端口范围过小]
    K --> L[CPU sys 高]
    L --> M[perf/火焰图: inet_hash_connect]
    M --> N[TIME_WAIT 大量占用]
    N --> O[连接复用/连接模型优化]

性能调优不是“一次命中真凶”,而是不断消除当前最主要的限制,让下一个限制浮现出来。

16. 网络性能分析的分层方法

从 Linux 网络收发路径来看,可以建立如下检查清单。

16.1 链路层

关注:

  • BPS/PPS;
  • 网卡 RX/TX error/drop;
  • Ring Buffer;
  • qdisc;
  • MTU;
  • NIC offload;
  • 硬/软中断分布。

常用工具:

1
2
3
4
5
ip -s link
ethtool -S eth0
ethtool -k eth0
tc -s qdisc show dev eth0
sar -n DEV 1

16.2 网络层

关注:

  • 路由是否正确;
  • MTU/分片;
  • overlay 封装;
  • Netfilter/NAT;
  • conntrack。
1
2
3
4
5
6
ip route
ip rule
mtr <host>
traceroute <host>
iptables -nvL
conntrack -S

16.3 传输层

关注:

  • Established/SYN_RECV/TIME_WAIT;
  • 重传;
  • listen/accept 队列;
  • socket buffer;
  • 临时端口;
  • reset/timeout。
1
2
3
4
ss -s
ss -ltnp
ss -tan state time-wait
netstat -s

16.4 应用层

关注:

  • QPS/RPS;
  • 错误率;
  • 响应时间;
  • DNS;
  • HTTP/TLS;
  • 连接池与超时;
  • 下游依赖。

这也是为什么“网络问题”最后经常需要日志、链路追踪和应用指标一起分析。

17. 常用性能工具速查:从指标选工具

工具的正确使用方式是“我需要观察某个指标,所以选择工具”,不是“我会某个工具,所以到处用它”。

17.1 CPU

指标/目标 常用工具 备注
平均负载 uptimetop/proc/loadavg load 不等于 CPU 使用率
系统 CPU topvmstatmpstatsar/proc/stat sar 适合历史数据
进程 CPU toppspidstathtopatop pidstat 适合按时间采样
系统上下文切换 vmstat 同时观察 runnable/uninterruptible
进程上下文切换 pidstat -w 区分 voluntary/nonvoluntary
软中断 topmpstat/proc/softirqs 网络高 PPS 常见
硬中断 vmstat/proc/interrupts 可结合 IRQ affinity
CPU Cache/硬件事件 perf stat cycles、instructions、cache-misses 等
热点/调用栈 perf、FlameGraph 先找大方向
动态追踪 ftrace、BCC/eBPF、SystemTap 已知热点后深入

17.2 内存

指标/目标 常用工具
系统已用/可用/剩余 freevmstatsar/proc/meminfo
进程 RSS/VSS 等 pstoppidstat -r/proc/<pid>/status
进程内存映射 pmap/proc/<pid>/mapssmaps
Swap 使用与换入换出 freevmstatsar
缺页异常 pidstat -rperf
Page Cache freevmstatsarcachestat
Cache 命中 cachetop
slab slabtop/proc/slabinfo
内存泄漏 memleak、Valgrind 等

17.3 文件系统与磁盘 I/O

指标/目标 常用工具
文件系统容量 df
inode df -i
page/slab cache /proc/meminfovmstatsarslabtop
设备利用率/IOPS/吞吐/延迟/队列 iostat -xsar -ddstat/proc/diskstats
进程 I/O pidstat -diotop
块 I/O 事件 blktrace + blkparse
系统调用 I/O straceperf trace
块设备 I/O 动态追踪 biosnoopbiotop
内核 I/O 路径 ftrace、BCC、SystemTap

17.4 网络

指标/目标 常用工具
BPS sarnethogsiftop/proc/net/dev
PPS sar -n DEV/proc/net/dev
连接 ssnetstat
错误/重传/协议统计 ss -snetstat -ssar -n EDEV/EIP
延迟 pinghping3
连接跟踪 conntracknf_conntrack_count/max
路由路径 mtrtracerouteip route
DNS dignslookup
防火墙/NAT iptables/nft
网卡特性 ethtool
抓包 tcpdump、Wireshark
动态追踪 ftrace、BCC/eBPF、SystemTap

17.5 基准测试工具

性能分析必须配合可重复基准,否则很难证明“优化真的有效”。

  • 磁盘 I/O:fio
  • 网络带宽:iperf
  • 包处理:pktgen 等;
  • HTTP:abwrk

基准测试时必须固定:请求模型、并发度、数据集、预热时间、测试时长、CPU/内存限制、服务版本和网络拓扑。

18. 性能优化的一般方法

找到根因之后,优化才真正开始。这里的原则是:先消除无效工作,再提高资源利用效率,最后才考虑堆硬件或复杂架构

18.1 CPU 优化

典型方向:

  • 简化算法和代码路径,减少不必要计算;
  • 合并系统调用、批处理工作;
  • 通过 CPU affinity 改善缓存局部性;
  • 平衡 IRQ/软中断到多核;
  • 对不同任务设置优先级和 cgroup 配额;
  • 避免过度线程化造成上下文切换和锁竞争。

“异步”并不天然降低 CPU 消耗。对于纯 CPU 密集型任务,错误的异步拆分反而可能增加调度与同步开销。异步更适合隐藏 I/O 等待、改善吞吐和解耦慢路径。

18.2 内存优化

典型方向:

  • 修复泄漏与不受控缓存;
  • 为服务设定合理 cgroup 内存预算;
  • 控制 JVM/运行时堆、native memory 和线程数;
  • 使用内存池、大页等减少高频分配和页表/缺页成本;
  • 评估 Swap 策略,避免在低延迟服务中因频繁换页产生不可接受的尾延迟。

资料中的“无必要时关闭 Swap”来自低延迟性能场景。现代生产系统是否禁用 Swap,要结合内核、工作负载和容器编排策略判断,不能一刀切。

18.3 磁盘与文件系统 I/O 优化

典型方向:

  • HDD 升级 SSD/NVMe,或通过 RAID 在可靠性/吞吐间权衡;
  • 根据设备和工作负载选择合适 I/O scheduler;
  • 调整预读、队列深度;
  • 减少小而随机的 I/O,尽可能批量化;
  • 合理利用 Page Cache;
  • 调整 dirty page 回写策略;
  • 将高干扰 I/O 工作负载隔离到独立设备。

旧环境常讨论 noopdeadline。现代多队列块层更常见的是 nonemq-deadline 等,具体以 /sys/block/<dev>/queue/scheduler 为准。

18.4 网络优化

内核资源层面可能涉及:

  • socket buffer;
  • SYN/accept backlog;
  • conntrack 容量;
  • 文件描述符;
  • 本地临时端口范围;
  • Keepalive、重传和超时参数;
  • MTU;
  • TIME_WAIT 与连接复用策略。

网卡层面可能涉及:

  • GRO/GSO/TSO 等 offload;
  • RSS/RPS/XPS 和多队列;
  • IRQ affinity;
  • Ring Buffer;
  • 网卡队列长度。

极限网络场景下,还会考虑:

  • DPDK:绕过传统内核网络栈,用户态轮询处理;
  • XDP:在数据包进入完整协议栈前尽早处理。

但越靠近“极限优化”,维护成本越高。普通 Web 服务首先应该检查连接模型、队列、超时、反向代理和应用逻辑,别一上来就把 DPDK 当万能药。

18.5 应用程序通常才是最值得优化的位置

很多 system CPU 高,并不是“内核太慢”,而是应用做了过多系统调用;很多数据库 I/O 高,并不是“磁盘太慢”,而是 SQL、索引或数据模型不合理。

应用侧常见优化包括:

  • 优化算法与数据结构;
  • 缓存热点数据;
  • 减少网络往返和磁盘访问;
  • 使用连接池、长连接;
  • I/O 多路复用;
  • 合理的异步/多线程/多进程模型;
  • 批量处理;
  • 消息队列削峰;
  • CDN、负载均衡和水平扩展。

当单机优化使系统越来越复杂时,应重新评估是否应该通过水平扩展解决问题,而不是执着于把一台机器压到极限。

19. 性能优化必须建立验证闭环

一次合格的优化至少要回答五个问题:

  1. 优化前的业务指标是多少?
  2. 当前证据能否明确证明瓶颈机制?
  3. 修改项是否只针对这个机制?
  4. 优化后相同基准下提升多少?
  5. 是否引入新的 CPU、内存、错误率或尾延迟副作用?

推荐保留这样的记录:

项目 优化前 优化后 说明
RPS 同一 wrk 参数
P50/P95/P99 不只看平均值
Error Rate 业务错误 + Socket error
CPU user/sys/softirq 区分 CPU 类型
内存/Swap 包括 cgroup memory
TCP retrans/drop 看增量
conntrack 使用率 count/max
listen queue overflow counter rate

没有这一步,所谓“优化”很容易只是把问题从 A 移到了 B。

20. 一份可落地的排障 Checklist

当线上出现“慢、抖、超时、吞吐下降”时,可以按下面顺序推进。

第一阶段:保留现场

1
2
3
4
5
6
7
8
9
# 只示意方向,真实生产请由监控系统持续采集
uptime
free -m
vmstat 1
mpstat -P ALL 1
iostat -x 1
ss -s
ip -s link
dmesg | tail -n 100

同时保存:应用 RED 指标、依赖调用、错误日志、Trace。

第二阶段:确定主导资源

  • CPU:user/sys/softirq/iowait 哪个高?
  • 内存:是真的不足,还是 cache、Swap、泄漏?
  • I/O:设备忙还是某进程制造 I/O?
  • 网络:带宽、PPS、丢包、重传、队列、conntrack 哪个异常?
  • 应用:错误率、P99、依赖调用、工作队列是否异常?

第三阶段:定位主体

  • 哪个进程/线程?
  • 哪个容器/cgroup?
  • 哪个 socket/端口?
  • 哪个磁盘/文件?
  • 哪个依赖服务?

第四阶段:进入机制层

  • CPU 热点:perf + FlameGraph;
  • 精确内核函数:ftrace;
  • 参数/事件关联:eBPF/BCC;
  • 网络协议:tcpdump + Wireshark;
  • 系统调用:perf trace/strace
  • 内核符号不理解:查当前版本内核源码。

第五阶段:最小改动 + 回归

一次只改最重要的瓶颈因素,重新执行同样压测,并观察系统是否暴露新的限制。

21. 深入内核:看到陌生函数名怎么办

perf、ftrace、eBPF 最终经常会把你带到一个陌生内核函数,例如:

1
2
3
4
inet_hash_connect
br_nf_pre_routing
netif_receive_skb
__init_check_established

不要凭函数名“脑补”。正确做法是查当前运行内核版本的源码:

  1. 先确认 uname -r
  2. 找到该版本源码;
  3. 搜索函数定义;
  4. 看调用者与被调用者;
  5. 结合数据结构、锁、分支条件和错误路径理解。

源码阅读能力是动态追踪的最终放大器。只会看到热点函数,但不知道函数在系统中的职责,就很难从“相关性”走到“因果关系”。

22. 最终方法论:把性能工具放回它应该在的位置

Linux 性能工程可以收敛成四层。

flowchart TB
    A[业务层: Rate / Error / Duration] --> B[资源层: USE]
    B --> C[系统机制层: 调度/内存/I/O/协议栈/cgroup]
    C --> D[实现层: 系统调用/内核函数/应用函数]
    D --> E[优化与验证]
    E --> A

业务层告诉你用户是否真的受影响;资源层告诉你系统哪里出现压力;机制层解释为什么会出现这些指标;实现层让你落到具体函数、队列、规则和参数;最后通过基准与监控形成闭环。

真正值得长期积累的不是一组 sysctl 参数,而是下面几种能力:

  • 能把业务症状翻译成可测量指标;
  • 能从 Linux 原理判断指标之间的因果关系;
  • 能根据指标选择成本最低的工具;
  • 能从调用栈、抓包和内核统计中建立证据链;
  • 能区分案例参数与生产最佳实践;
  • 能在优化后继续寻找下一个瓶颈,而不是见到一次提升就结束;
  • 能把排障过程中真正有价值的指标沉淀进监控系统。

性能优化从来不是“把所有参数调大”。它更像一个持续的工程循环:观察 → 假设 → 验证 → 优化 → 回归 → 再观察。只要这个循环建立起来,无论问题最终落在 JVM、容器、Linux 内核、网络协议还是应用代码,都能沿着同一套方法逐步逼近根因。


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