这一章在做什么
这一章是全书的预告:性能工作管哪些事、为什么难,以及后面各章反复要用的几样基本功。最该带走三件事:延迟能直接换算成「最多能快多少」,别的指标大多做不到;观测(计数器、剖析、跟踪)和实验(基准测试)是两只手,生产环境先用观测;遇到问题先把每种资源扫一遍,再往深处钻。读完这一页,你应该知道遇到哪类问题该翻哪一章。
什么时候翻这一章
- 刚拿到这本书,想知道哪一章管什么这一章几乎每一节都在给后面的章指路;上面的图和下面的表是本站按问题整理的对照。§1.1§1.7
- 接到一句「某某很慢」,不知道从哪下手先把问题问清楚,再把每种资源扫一遍,看哪里可疑。§1.5.1§1.10.1§1.11.1场景手册 →
- 想判断一项优化值不值得做用延迟算出这一项最多能带来多少加速,再决定排在第几。§1.5.4§1.6
- 在生产机上排查,拿不准该先看还是先压观测和实验各管什么,各有什么代价。§1.7§1.8场景手册 →
- 新版本上线前,想确认没有变慢第二个案例演示了非回归测试,以及压测时瓶颈跑到了压测客户端上。§1.11.2场景手册 →
这一章只开头,不展开。按问题找章,可以用这张表(本站整理,不是书里的):
| 遇到的事 | 翻哪里 | 先看什么 |
|---|---|---|
| 不知道按什么顺序查、怎么判断瓶颈 | 第 2 章 | USE 方法、延迟分析、统计里的平均值和分布 §2.5.9§2.8 |
| 工具输出里的数是从哪来的、开销多大 | 第 4 章 | 计数器、剖析、跟踪三类工具的来源 §4.2§4.3 |
| 自己的程序慢:在等锁、等 IO,还是在算 | 第 5 章 | CPU 剖析和 off-CPU 分析 §5.4.1§5.4.2 |
| CPU 忙、负载高、线程排队 | 第 6 章 | USE 方法、剖析、火焰图 §6.5§6.7.3 |
| 内存一直涨、被 OOM 杀、开始换页 | 第 7 章 | vmstat 和 PSI §7.5.1§7.5.2 |
| 读写文件慢,怀疑缓存 | 第 8 章 | 缓存命中率、文件系统延迟 §8.6.12§8.6.13 |
| 磁盘忙、IO 延迟高 | 第 9 章 | iostat 和延迟分布 §9.6.1§9.6.6 |
| 网络延迟、连不上、重传多 | 第 10 章 | ss、nstat、sar 的网络选项 §10.6.1§10.6.6 |
| 容器或虚拟机里数字不对 | 第 11 章 | 多租户和资源限制 §11.1.5 |
| 要做压测、对比两个版本 | 第 12 章 | 基准测试常见的错 §12.1.3 |
| 想用 perf、Ftrace、BPF 问具体问题 | 第 13–15 章 | 当说明书查 |
先用中文过一遍
延迟:能直接换算成「最多快多少」
一个成熟的系统里,已知没修的性能问题往往有一堆(§1.5.4)。难的不是找到一个问题,而是挑出影响最大的那个,这就要把每个问题量化。延迟(latency)是等待花掉的时间:一个请求、一次查询、一次文件读,都有自己的延迟。它好用在于能拆、能相减:知道一个请求的时间里有多少花在某件事上,就能算出把这件事完全去掉,最多能快几倍(§1.6)。
拿本站下面「动手」里的小程序说:每个请求约 20 毫秒,其中 6 毫秒在 CPU 上算,14 毫秒在等后端。把计算优化到快一倍,只省 3 毫秒,整体快 1.18 倍;等待减半,快 1.5 倍;等待全部消掉,上限是 20 ÷ 6 ≈ 3.3 倍。别的指标给不出这种结论:IOPS 降了一半,可能是好事,也可能只是每次 IO 变大了。还有两点要记住:延迟要带限定词,「网络延迟」可以指建连接的时间,也可以指整个请求的时间,书里会写成连接延迟、请求延迟;很多地方只给平均延迟或者根本不给,BPF 工具可以在任意位置量,并给出完整分布。
延迟好不好,本身没有标准答案:平均 1 毫秒的磁盘 IO,对一个服务可能很快,对另一个可能太慢(§1.5.1)。把「慢」变成能检查的事,要先定目标,比如平均响应时间不超过多少,或者百分之多少的请求要落在某个延迟以内。
观测和实验:两只手
观测(observability)是靠「看」来理解系统,不改变它的负载。看的手段有三种。计数器(counter)是内核和应用里一直在累加的整数:操作次数、字节数、错误数。工具隔一段时间读两次相减,算出速率、平均值、百分位,这叫统计(statistic);挑出来长期记录、画曲线、设告警的统计叫指标(metric)。计数器便宜、一直开着,但常常只能告诉你「CPU 或磁盘有问题」,说不出为什么(§1.7.1)。剖析(profiling)是定时采样,比如每秒打断几十次、记下正在跑的代码,拼出一张粗略的画像;最常见的是 CPU 剖析,画成火焰图。作者认为在指标之外,CPU 火焰图帮人找到的改进比任何工具都多,而且锁的自旋、过多的 malloc 这类问题也会在火焰图里留下 CPU 的痕迹(§1.7.2)。跟踪(tracing)是把每一个事件都记下来,或者当场汇总,三种里最细(§1.7.3)。
实验(experimentation)是自己施加一个人造的负载,量系统的反应,也就是基准测试。宏基准(macro-benchmark)模拟真实的客户端请求;微基准(micro-benchmark)只测一个部件,比如磁盘或网络,通常更容易重复、也更容易弄懂。实验的长处是负载固定:线上的流量每秒都在变,固定负载把这部分波动去掉,剩下的波动(比如吞吐忽高忽低分成两档)就显出来了。它的代价是会和真实负载抢资源,所以书里的建议是:生产环境先用观测工具,空闲的测试机可以先压测摸清硬件的底(§1.7、§1.8)。只用观测也不对:工具太多,挨个看可能花几个小时,换成一个实验也许更快就有答案。
跟踪:静态插桩、动态插桩和 BPF
跟踪要先有「插桩点」,事件发生时才有东西可记。静态插桩(static instrumentation)是写在源码里的固定点:内核里叫跟踪点(tracepoint),磁盘 IO、调度、系统调用都有;用户态程序和库里的叫 USDT。像代码里预先埋好的日志语句,位置是写代码的人定的。动态插桩(dynamic instrumentation)是在程序跑起来以后,改内存里的指令插进一段例程,像调试器下断点,只是执行完例程就接着跑,不停下来。对 C/C++ 程序员来说,相当于不重新编译、在线上进程任意一个函数里临时加一行 printf。Linux 内核里的这项能力叫 kprobes,2004 年开始合入(§1.7.3)。
BPF 原本是给 tcpdump 加速过滤表达式的内核小虚拟机,2013 年起扩展成通用的内核执行环境,保证安全,访问数据又快。它让 BCC 和 bpftrace 这两套前端可以「编程」:在任意插桩点上写一小段逻辑,在内核里直接汇总成直方图、计数。书里很多工具(比如抓短命进程的 execsnoop)都是 BCC 工具。这些工具都要 root;BPF 本身在第 3 章讲(§3.4.4),插桩点的原理在第 4 章(§4.3.5–§4.3.8),用法在第 15 章。
头一分钟:先问清楚,再把资源扫一遍
没有方法的排查像钓鱼:随便试,指望碰上(§1.10)。第一步是把「慢」落到实处。工单上写「磁盘慢」,可能只是有人在监控上看见磁盘很忙;应用到底有没有变慢、用什么量的、变慢前动过什么,都要先弄清。书里第一个案例就是先问了几个问题,才发现最初的怀疑很可能只是猜测(§1.11.1;怎样把问题问清楚,见 §2.5.5)。
第二步是在几分钟里把每种资源各看一眼,先排除掉一大片。书里 §1.10.1 给了一张十条命令的检查表,每条都指到详细讲它的那一节,值得在自己的机器上照着跑一遍。本站按要回答的问题换一种排法,顺带补上书外的 PSI:
- 有没有哪种资源让任务在等?内核 4.20 以后(还要编译时开了 PSI)有压力停顿信息:
/proc/pressure/下的cpu、memory、io这几个文件,各给出最近 10、60、300 秒里有任务因为这种资源而停顿的时间比例,一眼就能看出往哪边查(PSI,§7.5.2)。 - 是谁在用?pidstat 或 top 按进程列出 CPU,看有没有不该出现的进程(§6.6.7)。
- CPU 是整体不够,还是一个核满了?mpstat
-P ALL 1逐核看;平均值很低、某个核 100%,多半是单线程到顶了(§6.6.3)。 - 磁盘忙不忙、排不排队?iostat
-xz 1看利用率和平均等待(§9.6.1)。 - 内存紧不紧?free 看
available,别被buff/cache吓到;vmstat 的si、so不为零就是在换页(§7.5.1)。 - 网络有没有重传?nstat 或 sar 的网络选项看吞吐和 TCP 重传(§10.6.4、§10.6.6)。
- 内核有没有报错?dmesg 里找 OOM 和 IO 错误,错误比慢更该先处理。
两点提醒。监控面板上有同样的数,看面板也行,但监控常常一分钟一个点,会把几秒的突发抹平:书里的案例里,监控说磁盘 80%,按秒看却是经常顶到 100%、开始排队。扫完这一遍,你只知道哪种资源可疑,还不知道为什么;接下来按第 2 章的 USE 方法(§2.5.9)把每种资源查完整,再往下钻(§2.5.12),去对应资源那一章用剖析和跟踪找原因。
| 英文 | 中文 | 一句话 |
|---|---|---|
| latency | 延迟 | 等待花掉的时间;能拆、能相减,所以能换算成加速比的上限 §1.6 术语页 |
| observability | 可观测性、观测 | 靠看来理解系统,不改变负载:计数器、剖析、跟踪 §1.7 术语页 |
| counter / statistic / metric | 计数器 / 统计 / 指标 | 累加的整数;两次读数算出的速率和平均;挑出来长期监控的那些 §1.7.1 术语页 |
| profiling | 剖析 | 定时采样,拼出粗略的画像,最常见的是 CPU 剖析 §1.7.2 术语页 |
| flame graph | 火焰图 | 把采到的调用栈合并画出来,宽度是 CPU 时间的占比 §1.7.2 术语页 |
| tracing | 跟踪 | 按事件记录,保存下来或当场汇总 §1.7.3 术语页 |
| tracepoint / USDT | 跟踪点 / 用户态静态跟踪 | 静态插桩:内核和用户态程序源码里预先写好的点 §1.7.3 术语页 |
| dynamic instrumentation | 动态插桩 | 运行中改内存里的指令插入例程,内核里是 kprobes §1.7.3 术语页 |
| BPF | BPF | 内核里安全的执行环境,BCC 和 bpftrace 靠它编程 §1.7.3 术语页 |
| macro- / micro-benchmark | 宏基准 / 微基准 | 模拟真实负载 / 只测一个部件;后者更稳定、好重复 §1.8 术语页 |
| workload / resource analysis | 负载分析 / 资源分析 | 从请求往下看 / 从资源往上看,难题两头都试 §1.4 |
| canary testing | 金丝雀测试 | 新版本先在一台实例上接一小部分流量 §1.3 |
| non-regression testing | 非回归测试 | 确认改动没让性能退步;也常被叫作回归测试 §1.11.2 |
| USE method | USE 方法 | 对每种资源查利用率、饱和度、错误;第 2 章定义 §2.5.9 术语页 |
动手:在自己的机器上试
把一个请求的时间拆成「在算」和「在等」
下面这个程序假装处理 50 个请求,每个请求先算一会儿,再等一会儿。先猜:把计算优化快一倍,和把等待砍掉一半,哪个收益大、各快多少?然后量。
// req.c:假装处理 50 个请求。每个请求先在 CPU 上算一会儿,再等一会儿(假装在等数据库)
#include <stdio.h>
#include <time.h>
#ifndef WORK
#define WORK 8500000 // 计算量:本站的 WSL2 上大约 6 毫秒
#endif
#ifndef WAIT_MS
#define WAIT_MS 14 // 每个请求等多少毫秒
#endif
static volatile unsigned sink;
static void compute(long n) { // 在 CPU 上
unsigned s = 0;
for (long i = 0; i < n; i++) s = s * 31 + (unsigned)i;
sink = s;
}
static void wait_backend(long ms) { // 不在 CPU 上,只是等
struct timespec ts = { 0, ms * 1000000L };
nanosleep(&ts, NULL);
}
int main(void) {
for (int id = 0; id < 50; id++) {
compute(WORK);
wait_backend(WAIT_MS);
}
printf("done %u\n", sink);
return 0;
}
# 先观测:real 是墙上时间,user + sys 是在 CPU 上的时间 $ gcc -O2 req.c -o req $ time ./req # 再实验:计算减半、等待减半,各编一份 $ gcc -O2 -DWORK=4250000 req.c -o req_cpu $ gcc -O2 -DWAIT_MS=7 req.c -o req_wait $ time ./req_cpu $ time ./req_wait
本站在 WSL2 上看到的(三次各截了三行):
req: real 0.988s user 0.284s sys 0.000s req_cpu: real 0.851s user 0.146s sys 0.000s req_wait: real 0.655s user 0.297s sys 0.004s
- 原版跑了约 1 秒,在 CPU 上的只有 0.28 秒,七成多时间在等。这个程序只有一个线程,所以 real 减去 user 和 sys,就是它不在 CPU 上的时间(§6.6.8)。机器很忙时,排队等 CPU 的时间也会算进这一块。
- 按延迟估算:去掉等待,上限是 0.99 ÷ 0.28 ≈ 3.5 倍(上面的图按取整的 20 ÷ 6 算,是 3.3 倍);计算减半预计到 0.85 秒,等待减半预计到 0.64 秒左右。实测和估算对得上。先算账,再决定优化哪一段,这就是 §1.6 说的量化。
- 前两条命令是观测,后面重新编译再量是实验。真实的服务没法这样随手改参数,但思路一样:先观测出时间花在哪,再用实验确认改了以后真的变快。
- WSL2
- 本站实测过(Debian 13,gcc 14.2,bash 内建的
time),不需要 root。WORK的值是按本机校准的,换一台机器 6 毫秒会变,比例的道理不变。 - 服务器
- 别在生产机上跑。对正在跑的服务没法重编译,可以用
pidstat -u -p PID 1看它每秒用掉多少 CPU 时间,除以每秒的请求数,得到每个请求在 CPU 上花的时间,再和应用自己记录的请求延迟比。等待花在哪里,用第 5 章的 off-CPU 分析(§5.4.2,BCC 的 offcputime 要 root)。 - ARM 板
- 交叉编译(如
aarch64-linux-gnu-gcc -O2 req.c -o req)后拷上去。板子慢,同样的WORK会花更久,按比例调小。shell 内建的time或 busybox 的time都能给出 real 和 user(看你的 busybox 裁了哪些)。
成书之后
BPF 还是 eBPF:两个名字并用
- 书里
- 脚注说 eBPF 是早期对扩展后的 BPF 的叫法,现在这项技术就叫 BPF,全书都写 BPF(§1.7.3)。
- 今天
- 两个名字都在用。IETF 在 2024 年 10 月发布了 RFC 9669,定义 BPF 的指令集,摘要里写的是 eBPF,并注明它已经不是任何词的缩写、也常被叫作 BPF;原来的 BPF 有时叫 cBPF(classic BPF)。查资料时两个词都搜一下。
分节地图
这一章是全书的导览,每一节都短,结尾是两个虚构的案例。先读的是讲延迟、观测和实验、头 60 秒清单的几节,它们是后面各章天天要用的基本功;性能为什么难、云计算、两个案例,想了解时再读。
- Computer performance is an exciting, varied…约 100 词开篇四条学习目标,也就是这一章的目录。
- Systems Performance约 200 词系统性能研究整条数据路径,书里的全栈一直到硬件;手边要有一张数据路径图。
- Roles约 200 词角色谁在做性能工作、性能团队怎么分工;上手用不到。
- Activities约 500 词活动从立项到上线的十项性能活动;金丝雀测试、蓝绿部署、容量规划在这里出现。
- Perspectives约 100 词视角从负载往下看与从资源往上看,第 2 章展开。
- Performance Is Challenging约 100 词性能为什么难四个原因的引子。
- Subjectivity约 200 词主观性好不好取决于预期;怎样用目标把它变成能检查的事。
- Complexity约 300 词复杂性没有明显的起点、连锁故障、瓶颈转移、线上负载在实验室复现不了。
- Multiple Causes约 100 词多个原因几件各自正常的事凑在一起才出问题。
- Multiple Performance Issues约 200 词多个性能问题问题总有一堆,要量化,挑最要紧的。
- Latency约 400 词延迟为什么延迟能估出加速比上限而 IOPS 不能;延迟要带限定词。
- Observability约 100 词可观测性观测和实验的分界;生产环境先用哪一种。
- Counters, Statistics, and Metrics约 400 词计数器、统计和指标从计数器到告警的几层叫法;指标什么时候够用、什么时候只指方向。
- Profiling约 300 词剖析采样画粗略画像;火焰图能从 CPU 的痕迹里看出哪些别的问题。
- Tracing约 700 词跟踪静态插桩、动态插桩、BPF 的来历;细节在第 4、15 章。
- Experimentation约 400 词实验宏基准和微基准;固定负载能显出什么;为什么两种工具都要用。
- Cloud Computing约 200 词云计算按时计费让优化直接省钱;邻居干扰、看不见物理机。
- Methodologies约 100 词方法论没有方法的排查是什么样子。
- Linux Perf Analysis in 60 Seconds约 100 词Linux 性能分析的头 60 秒一张十条命令的检查表,每条指到详细讲它的节。照着跑一遍。
- Case Studies约 100 词案例两个虚构的案例;作者说这只是一种做法。
- Slow Disks约 900 词磁盘慢从问清问题、USE 扫资源到和同类机器对比,看方法怎么串起来。
- Software Change约 800 词软件变更非回归测试:压测到顶、定速比资源消耗、找出真正的瓶颈。
- More Reading约 100 词延伸阅读指向第 16 章和第 2 章,没有新内容。
词与短语
只收先读和用到再查两档的节。斜体那一行是原句里连续的几个词,在电子书里搜它就能找到那一句。
§1.1 Systems Performance
§1.3 Activities
§1.5.1 Subjectivity
§1.5.2 Complexity
§1.5.4 Multiple Performance Issues
§1.6 Latency
§1.7 Observability
§1.7.1 Counters, Statistics, and Metrics
§1.7.2 Profiling
§1.7.3 Tracing
§1.8 Experimentation
§1.9 Cloud Computing
§1.10 Methodologies
§1.11 Case Studies
§1.11.1 Slow Disks
§1.11.2 Software Change
诊断题:换成你来查
场景和输出都是本站编的。先自己判断,再点选项看解释。
场景:你的 C++ 服务平均每个请求 40 毫秒。用跟踪工具拆开一个请求,时间是这样分的:
parse_json 8 ms (on-CPU) mysql_query 30 ms (waiting) other 2 ms
同事打算花两周把 JSON 解析器重写,快一倍。
解析从 8 毫秒降到 4 毫秒,请求从 40 变成 36,40 ÷ 36 ≈ 1.11 倍。同样的账算一下数据库那一段:等待全部消掉,上限是 40 ÷ 10 = 4 倍,哪怕只砍掉一半(加缓存、补索引)也是 40 ÷ 25 = 1.6 倍。按延迟算,先动数据库那一段更划算。这类估算不需要压测,有延迟的拆分就能算。
场景:一块 ARM 板上的日志服务写 eMMC。你把写日志的方式从「每条立刻写」改成「攒一批再写」。改动前后各跑一分钟 iostat:
w/s before 1200 after 150
攒批以后每次写得更大,次数少了不等于干的活少了,书里正是拿这一点说明 IOPS 不能直接比较。该看的是 iostat 里每次写的平均大小(wareq-sz)和平均等待(w_await),更该看的是应用这边:记一条日志的延迟是不是降了。eMMC 故障要看 dmesg 里的 IO 错误,不能从次数推断。
场景:生产环境里一台网关的 p99 延迟这周涨了一倍。同事说:「在这台机器上跑十分钟 fio 和压测工具,看看盘和网卡到底能跑多快,就知道是不是硬件不行了。」
压测会和线上请求抢盘、抢网卡,把正在出问题的系统搅得更乱,所以生产环境先用观测工具。实验并不是没用:在一台空闲的同配置机器上压,能知道硬件的底,也能用固定负载复现问题,这是另一只手。重启可能让现象暂时消失,也把排查的线索一起抹掉了。
场景:数据库的慢查询变多了。监控系统每分钟一个点,磁盘利用率一直在 55% 左右。你登上机器跑 iostat -x 1,连着看了几秒:
%util aqu-sz r_await 100 9.8 41.2 12 0.1 0.6 100 11.3 47.9 10 0.1 0.5
满载的那几秒里队列有十来个请求,单次读的等待从不到 1 毫秒涨到四十多毫秒,查询变慢就落在这些时刻。一分钟的平均值把 100% 和 10% 平均成了中间值,两档都代表不了。下一步按负载特征看是谁在突发地读(第 9 章的 iostat 和 biolatency,§9.6.1、§9.6.6),再想办法让它摊平或者走缓存。
场景:你给自己的 C++ 服务做发版前的压测,压测工具是组里以前写的单线程程序。逐步加目标速率:
target achieved server_cpu 10000 10000 16% 15000 15000 24% 20000 18100 29% 30000 18200 29%
服务端 CPU、磁盘、网卡都不忙,线程也不排队。
服务端资源都闲着、吞吐却平了,说明限制在别处。数据路径上还有客户端和网络,单线程的压测工具最容易先到顶:在客户端机器上 pidstat -t -p PID 1 看它的线程是不是 100%。解决办法是多开几个客户端、放到不同机器上一起压。至于新旧版本有没有退步,先固定一个两边都压得住的速率,比较每个请求用掉多少 CPU。
值得停下来的句子
the real task isn’t finding an issue; it’s identifying which issue or issues matter the most.— §1.5.4。本站译:真正的活不是找到一个问题,而是认出哪一个或哪几个问题最要紧。
排查时最容易犯的错,是看到第一个「不正常」的数就认定是它。成熟的软件里本来就躺着一堆已知的性能问题,找到一个毫不稀奇。这句话把排查从「找问题」改成「排优先级」,而排优先级就要量化:这个问题让请求慢了多少毫秒、修好它最多快几倍。这也是为什么书里紧接着就讲延迟。
you have two hands, observability and experimentation. Only using one type of tool is like trying to solve a problem one-handed.— §1.8。本站译:你有两只手,观测和实验。只用一种工具,就像单手解决问题。
这是作者转述一位前辈工程师的比喻。只会看监控的人,可能在几十个工具之间转几个小时;只会压测的人,在生产机上会把现场搅乱。实际的节奏是交替的:观测给出假设,实验验证假设,再回到观测确认改动的效果。上面「动手」里的小程序就是这个节奏的缩小版。
带走的几条
一句话:先量化再动手:用延迟算出上限,生产上先观测,扫完资源再往深处钻。
量出一段占请求多少时间,去掉它最多快「总时间 ÷ 剩余时间」倍。只占两成的部分优化到零,也只快 1.25 倍。
§1.6次数少了,可能只是每次变大了。要配上每次的大小和延迟一起看。
§1.6压测会和线上负载抢资源。实验放到空闲的同配置机器上做,但别只用一只手。
§1.8问清慢是怎么量的、从什么时候开始、改过什么;再把 CPU、内存、磁盘、网络、错误各看一眼。书里 §1.10.1 有现成的检查表。
§1.10.1一分钟一个点的 55%,可能是一半时间满载。到现场用 1 秒的间隔看。
§1.11.1服务端都闲着、吞吐却平了,瓶颈可能在压测客户端、网络,或者负载生成器自己。
§1.11.2