
1. 从一次“翻车”的调优说起去年帮一个朋友看他的数据处理服务单节点跑批任务CPU 是当时挺主流的一颗 16 核 32 线程的桌面级处理器内存 64GB DDR4 3200硬盘是 PCIe 4.0 的 NVMe SSD。按纸面参数看这配置处理他那点数据量应该是绰绰有余。结果他跟我说跑一个大约 200GB 的日志聚合任务耗时将近 40 分钟而且 CPU 占用率死活上不去一直在 30% 上下晃悠。我上去先看了下top发现 CPU 的waiowait指标高得离谱接近 40%。再看iostatSSD 的利用率直接顶到 100%但吞吐量只有标称值的四分之一左右。这就很有意思了——硬件明明“很快”为什么实际跑起来“快不起来”后来把任务拆开看发现他的程序在读取日志文件时用的是单线程逐行读取每读一行就做一次字符串分割和正则匹配然后写数据库。问题出在两个地方第一单线程读取根本喂不饱 NVMe 的队列深度第二频繁的小块随机写把 SSD 的写放大搞得很难看。换句话说硬件的“快”是有前提的它需要软件用正确的方式去“喂”它。这件事让我觉得有必要把硬件体系结构和性能方法论这块的东西系统梳理一遍。因为太多人——包括不少工作了三五年的开发者——对硬件的理解还停留在“i7 比 i5 快”“SSD 比 HDD 快”这种粗粒度认知上。一旦遇到性能问题要么盲目升级硬件要么对着代码瞎改完全不知道瓶颈到底在哪一层。这篇文章就是想把“硬件凭什么快”和“为什么有时快不起来”这两个问题讲透。我会从体系结构的基本原理出发结合性能方法论把 CPU、内存、存储、总线这几个核心子系统的运作逻辑拆开来讲然后给出可落地的性能分析方法。适合所有需要跟性能打交道的开发者、运维和架构师不管你是刚入行还是已经踩过几年坑应该都能从中找到对自己有用的东西。2. 硬件凭什么快体系结构层面的速度来源2.1 流水线、超标量与乱序执行CPU 的“快”从何而来很多人对 CPU 性能的理解就是“主频高就快”这个认知大概停留在奔腾 4 时代。实际上现代 CPU 的性能来源是一个复杂的组合主频只是其中一个维度。先讲流水线。一条指令的执行可以拆成取指、译码、执行、访存、写回五个阶段。如果不做流水线每条指令串行执行那 CPU 的吞吐量就是“主频除以每条指令的周期数”。流水线的思路是让这五个阶段并行起来——当第一条指令在执行阶段时第二条指令已经在译码第三条在取指。理想情况下每个时钟周期都能完成一条指令吞吐量直接翻了好几倍。但流水线有个天敌叫冒险。比如数据冒险下一条指令要用到上一条指令的计算结果但上一条还没写回。这时候要么插入气泡stall要么做前递forwarding把执行阶段的结果直接送给下一条指令的输入。控制冒险则来自分支跳转——CPU 不知道下一条该取哪条指令只能靠分支预测来猜。猜对了继续跑猜错了就要清空流水线代价是十几个甚至几十个周期的浪费。超标量是另一个维度。现代 CPU 每个周期可以同时发射多条指令比如 4 发射、6 发射甚至 8 发射。这意味着前端需要同时取多条指令、同时译码后端要有多个执行单元ALU、FPU、Load/Store 单元等来并行处理。但指令之间往往有依赖关系超标量的效率取决于指令级并行度ILP。乱序执行就是为了解决依赖问题而生的。CPU 不按程序顺序执行指令而是看哪些指令的操作数已经就绪就优先执行哪些。这需要重排序缓冲区ROB来保证最终结果按程序顺序提交还需要寄存器重命名来消除假依赖。乱序执行让 CPU 在遇到长延迟操作比如访存时可以继续执行后面不相关的指令把等待时间利用起来。这三者结合起来才是 CPU“快”的真正原因。但这也解释了为什么有时候“快不起来”——如果你的代码指令级并行度很低全是串行依赖那再多的执行单元也帮不上忙。比如一个长链的累加操作每一步都依赖上一步的结果乱序执行也无能为力。实操心得判断代码是否受限于指令级并行度可以看 CPU 的 IPC每周期指令数。如果 IPC 远低于 CPU 的发射宽度比如 4 发射的 CPU 只跑到 1.0 IPC说明代码的并行度不够或者频繁遭遇分支预测失败和缓存未命中。2.2 缓存层次与局部性原理内存墙的应对之道CPU 主频早就突破了 5GHz但内存的访问延迟还停留在几十纳秒的量级。如果 CPU 每次都直接访问内存那大部分时间都在等。这就是所谓的内存墙。缓存的思路是利用局部性原理。时间局部性是指刚访问过的数据很可能马上又被访问空间局部性是指访问了某个地址附近的地址很可能也会被访问。缓存把最近访问过的数据留在离 CPU 更近的地方下次再访问就不用去内存了。现代 CPU 通常有三层缓存L1 最小最快几十 KB延迟 4-5 个周期L2 居中几百 KB 到几 MB延迟 10-20 个周期L3 最大最慢几十 MB延迟 30-50 个周期。L1 和 L2 是每个核心私有的L3 是所有核心共享的。缓存的工作单位是缓存行cache line通常是 64 字节。这意味着即使你只读一个字节CPU 也会把整个 64 字节的块加载进来。这个设计对顺序访问非常友好但对随机访问就不太妙了——每次随机访问都可能触发一次缓存行填充而填充的 64 字节里可能只有几个字节是有用的。缓存命中率是性能的关键指标。一次 L1 命中可能只要 4 个周期一次内存访问可能要 200-300 个周期。如果缓存命中率从 99% 降到 95%看起来只差了 4 个百分点但实际性能可能下降好几倍。这就是为什么数据结构的选择和内存访问模式对性能影响巨大。举个例子同样是遍历一个二维数组按行遍历和按列遍历的性能可能差好几倍。因为 C 语言里二维数组是行优先存储的按行遍历是顺序访问缓存行利用率高按列遍历是跳跃访问每次都可能触发缓存未命中。2.3 存储介质的物理极限SSD 为什么也会“慢”SSD 的“快”来自 NAND 闪存的并行性。一颗 SSD 内部有多颗闪存芯片每颗芯片内部又有多个 die、plane。主控可以把读写请求分散到不同的芯片和 die 上并行处理从而实现远高于单颗芯片的吞吐量。但 SSD 有几个物理限制。第一读写不对称。NAND 闪存的编程写操作比读取操作慢得多而且写入前必须先擦除。擦除的单位是块block通常几百 KB 到几 MB而读写的单位是页page通常 4KB 到 16KB。这就导致了写放大——当你写入一个 4KB 的页时如果目标块里已经有数据主控需要先把整个块读出来修改后再写回去。写放大会显著缩短 SSD 寿命也会降低写入性能。第二垃圾回收。SSD 不能像 HDD 那样原地覆盖写必须写到新的页然后把旧页标记为无效。当无效页积累到一定程度主控需要做垃圾回收把有效页搬移到一起腾出完整的块来擦除。垃圾回收期间SSD 的读写性能会明显下降。第三队列深度。SSD 的并行性需要足够的队列深度来喂。如果软件只发一个请求等它完成再发下一个那 SSD 内部的并行通道根本用不起来。这就是为什么 NVMe 协议支持高达 64K 的队列深度而 AHCI 只有 32 个命令槽。回到开头那个案例朋友的程序单线程逐行读取队列深度基本是 1NVMe 的并行性完全没发挥出来。后来改成多线程分块读取每个线程负责一段文件队列深度上去了读取吞吐量直接翻了四倍。2.4 总线与互连数据高速公路的通行规则CPU、内存、外设之间需要通过总线或互连来通信。传统的前端总线FSB早就被点对点互连取代了。Intel 的 QPI/UPI、AMD 的 Infinity Fabric都是把内存控制器集成到 CPU 内部通过高速串行链路连接各个组件。PCIe 是目前外设互连的主流标准。PCIe 3.0 x4 的理论带宽是 4GB/s 左右PCIe 4.0 x4 翻倍到 8GB/sPCIe 5.0 x4 再翻倍到 16GB/s。但这是理论值实际有效带宽还要考虑协议开销、编码方式等因素。总线的带宽是共享的。如果多个设备同时争抢总线带宽每个设备能分到的就少了。比如一块 NVMe SSD 和一块显卡同时跑如果它们共享 PCIe 通道就可能互相影响。这也是为什么高端主板会把 CPU 提供的 PCIe 通道和芯片组提供的通道分开标注。注意事项在排查性能问题时不要忽略总线层面的瓶颈。用lspci -vv可以看到每个 PCIe 设备的链路宽度和速率。如果一块 PCIe 4.0 的 SSD 跑在 PCIe 3.0 的插槽上带宽直接减半但很多人根本不会注意到这一点。3. 为什么有时快不起来性能瓶颈的常见来源3.1 阿姆达尔定律与并行化的天花板阿姆达尔定律是性能优化领域最基础的定律之一。它说的是如果一个任务中有一部分必须串行执行那么无论你怎么增加并行度整体加速比都有一个上限。公式很简单加速比 1 / (S P/N)其中 S 是串行部分的比例P 是并行部分的比例S P 1N 是并行度。当 N 趋向无穷大时加速比趋向于 1/S。也就是说如果 10% 的代码必须串行那最大加速比就是 10 倍哪怕你有 1000 个核心。这个定律解释了为什么很多程序在核心数增加到一定程度后性能就不再提升了。比如一个任务 95% 可以并行5% 必须串行那 16 核的加速比大约是 1/(0.05 0.95/16) ≈ 9.1 倍32 核是 1/(0.05 0.95/32) ≈ 11.9 倍。从 16 核到 32 核核心数翻倍但加速比只提升了 30%。更麻烦的是并行化本身也有开销。线程创建、同步、通信都需要时间。当并行粒度太细时开销可能超过收益。这就是为什么有时候单线程反而比多线程快——多线程的同步开销吃掉了并行带来的好处。3.2 内存带宽与延迟被忽视的隐形瓶颈很多人优化性能时只盯着 CPU 利用率看到 CPU 没跑满就觉得还有余量。但实际上内存带宽和内存延迟经常是真正的瓶颈。内存带宽是指单位时间内能从内存读取或写入的数据量。DDR4 3200 双通道的理论带宽是 51.2GB/sDDR5 4800 双通道是 76.8GB/s。看起来很大但如果你跑的是内存密集型任务比如大规模矩阵运算、数据聚合、视频编解码内存带宽很容易被打满。内存延迟则是另一个维度。一次内存访问的延迟大约是 60-100 纳秒而一次 L1 缓存访问只要 1 纳秒左右。如果代码的缓存命中率低CPU 大部分时间都在等内存那再高的主频也没用。判断是否受限于内存可以看 CPU 的stall周期数或者用性能计数器看LLC-miss最后一级缓存未命中的比例。如果 LLC 未命中率很高说明数据访问模式对缓存不友好需要优化数据布局或访问顺序。3.3 锁竞争与伪共享多核时代的暗坑多核编程里有两个经典的性能杀手锁竞争和伪共享。锁竞争是指多个线程同时争抢同一把锁。拿不到锁的线程只能等待CPU 时间被浪费在自旋或上下文切换上。锁的粒度越粗竞争越激烈。比如用一个全局锁保护整个哈希表那所有线程的读写都要串行化并行度直接归零。伪共享更隐蔽。前面说过缓存的工作单位是缓存行64 字节。如果两个线程分别修改同一个缓存行里的不同变量虽然它们逻辑上不冲突但硬件层面会认为它们在争抢同一个缓存行。每次一个线程修改另一个线程的缓存行就失效了必须重新从内存加载。这会导致大量的缓存失效和内存流量性能急剧下降。解决伪共享的办法是填充padding把每个线程独立使用的变量放到不同的缓存行里。比如在变量前后各加 64 字节的填充确保它独占一个缓存行。struct padded_counter { char pad1[64]; long counter; char pad2[64]; };实操心得伪共享问题在性能剖析工具里往往表现为“莫名其妙的缓存未命中”和“内存带宽异常高”。如果你看到多个核心在修改相邻的变量而性能远低于预期优先怀疑伪共享。用perf c2c可以检测缓存行竞争。3.4 I/O 路径上的层层损耗从应用程序发起一次 I/O 请求到数据真正落到存储介质上中间要经过很多层系统调用、文件系统、块层、设备驱动、控制器、最后到存储介质。每一层都有开销每一层都可能成为瓶颈。系统调用本身就有开销一次read或write系统调用大约需要几百纳秒到几微秒。如果 I/O 请求很小很频繁系统调用的开销占比就会很高。这就是为什么批量 I/O 比逐条 I/O 效率高——把 1000 次小请求合并成 10 次大请求系统调用次数减少两个数量级。文件系统的开销也不可忽视。日志文件系统如 ext4在写入时需要先写日志再写数据这增加了额外的写入量。如果对数据安全性要求不高可以用datawriteback模式减少日志开销。更极端的情况下直接用裸设备或 SPDK 绕过内核可以进一步降低开销。块层的 I/O 调度器也会影响性能。传统的cfq调度器适合机械硬盘会做请求合并和排序但对于 NVMe SSDnone或mq-deadline调度器通常更好因为 SSD 本身就有很强的并行处理能力不需要内核再做太多调度。4. 性能方法论从测量到优化的完整闭环4.1 USE 方法快速定位资源瓶颈USE 方法是 Brendan Gregg 提出的性能分析框架三个字母分别代表 Utilization利用率、Saturation饱和度、Errors错误。对每一种资源CPU、内存、磁盘、网络都检查这三个指标。利用率是指资源在单位时间内有多少时间处于忙碌状态。CPU 利用率 80% 意味着 80% 的时间 CPU 在执行指令。但利用率高不一定就是瓶颈——如果 CPU 利用率 100% 但任务吞吐量还在提升说明 CPU 还没到极限。利用率低也不一定就没问题——如果 CPU 利用率只有 30% 但任务很慢说明瓶颈在别处。饱和度是指资源排队等待的程度。CPU 的饱和度可以用运行队列长度来衡量如果运行队列长度持续大于核心数说明有任务在等 CPU。磁盘的饱和度可以用 I/O 队列深度来衡量。饱和度高说明资源不够用需要扩容或优化。错误是指资源层面的错误计数比如网卡的丢包、磁盘的坏道、内存的 ECC 错误。这些错误往往会导致性能下降但容易被忽略。USE 方法的好处是系统化不会漏掉某个资源。排查性能问题时先对每个资源过一遍 USE通常很快就能定位到瓶颈所在。4.2 火焰图与性能剖析找到代码层面的热点定位到瓶颈资源后下一步是找到具体是哪段代码导致的。火焰图是目前最直观的性能剖析可视化工具。火焰图的原理是对调用栈进行采样每个采样点记录当前的调用栈然后把所有采样点聚合起来。横轴是采样比例纵轴是调用栈深度。每个矩形代表一个函数宽度代表它占用的 CPU 时间比例。火焰图从上往下看是调用关系从下往上看是时间分布。看火焰图有几个技巧。第一找“平顶”——如果某个函数在火焰图顶部很宽说明它自己消耗了大量 CPU而不是在等子函数。第二找“宽底”——如果某个底层函数很宽说明它被很多上层函数调用可能是公共热点。第三注意颜色——通常红色代表 CPU 密集蓝色代表 I/O 等待但不同工具的颜色方案可能不同。生成火焰图通常用perf采样然后用 FlameGraph 工具链处理perf record -F 99 -g -- ./your_program perf script | stackcollapse-perf.pl | flamegraph.pl flamegraph.svg-F 99表示每秒采样 99 次-g表示记录调用栈。采样频率不要太高否则开销太大也不要太低否则统计意义不够。99Hz 是一个常用的折中值。4.3 基准测试与对照实验用数据说话性能优化最忌讳的就是“我觉得这样会更快”。人的直觉在性能问题上往往不准必须用数据说话。基准测试的关键是控制变量。每次只改一个因素其他条件保持一致。比如你想测试缓存行对齐对性能的影响那就只改对齐方式其他代码、数据规模、运行环境都不变。跑多次取平均值去掉异常值。对照实验是另一个重要方法。如果你怀疑某个因素是瓶颈可以做一个“移除实验”——把可疑因素去掉看性能是否提升。比如怀疑锁竞争是瓶颈可以把锁去掉哪怕结果不正确看性能能提升多少。如果去掉锁之后性能大幅提升说明锁竞争确实是瓶颈。基准测试还要注意预热。JIT 编译、缓存预热、连接池初始化等都需要时间。如果测试时间太短测到的可能只是预热阶段的性能。通常建议先跑几轮预热等性能稳定后再开始计时。注意事项基准测试的环境要尽量干净。关掉不必要的后台进程固定 CPU 频率关闭睿频绑定 CPU 核心避免 NUMA 跨节点访问。这些细节看起来琐碎但对测试结果的可靠性影响很大。4.4 从微基准到系统级不同粒度的性能视角性能分析可以在不同粒度上进行每个粒度看到的东西不一样。微基准是针对某个特定操作或函数的测试比如测量一次哈希查找的耗时、一次内存拷贝的带宽。微基准的好处是干净、可控能精确测量某个因素影响。但微基准的结果不一定能直接推广到真实场景因为真实场景中有太多微基准里没有的因素。模块级基准是针对某个模块或组件的测试比如数据库的写入吞吐量、消息队列的端到端延迟。模块级基准比微基准更接近真实场景但仍然简化了很多因素。系统级基准是端到端的测试模拟真实的工作负载。比如用 YCSB 测试数据库、用 wrk 测试 Web 服务。系统级基准最能反映真实性能但变量太多很难定位具体瓶颈。实践中通常是从系统级基准开始发现性能不达标后逐步下钻到模块级和微基准定位到具体问题后再回到系统级验证优化效果。这个“自顶向下”的分析路径比“自底向上”更高效因为系统级的问题往往不是单个微基准能暴露的。5. 典型场景下的性能问题拆解5.1 高并发网络服务从网卡到应用的完整链路一个高并发网络服务的性能瓶颈可能出现在链路的任何一环。从网卡收到数据包开始到应用层处理完请求返回响应中间要经过网卡驱动、内核协议栈、socket 层、应用层。网卡层面中断处理是第一个开销。每个数据包到达都会触发中断如果包速率很高中断处理本身就会消耗大量 CPU。NAPINew API机制通过轮询加中断的方式减少中断次数但轮询本身也有开销。更激进的做法是用DPDK或XDP绕过内核协议栈直接在用户态处理数据包。内核协议栈层面TCP 的处理涉及连接管理、拥塞控制、流量控制、重传等。这些逻辑很复杂开销也不小。如果服务只需要简单的请求-响应模式可以考虑用 UDP 或 QUIC 来减少协议栈开销。应用层面常见的瓶颈包括线程模型、内存分配、序列化/反序列化。比如每个请求创建一个线程的模型在高并发下线程创建和上下文切换的开销会很大。改用事件驱动或协程模型可以显著降低开销。5.2 大数据处理当 CPU 不是瓶颈时看什么大数据处理任务通常被认为是 CPU 密集型的但实际上很多任务受限于内存带宽或 I/O。比如一个简单的group by聚合如果数据量远大于缓存那每次访问都可能触发内存读取。这时候内存带宽就是瓶颈CPU 大部分时间在等数据。优化方向是减少内存访问量——用更紧凑的数据结构、做分块处理让数据留在缓存里、用 SIMD 指令一次处理多个数据。再比如排序操作如果数据量大于内存就需要外部排序涉及大量的磁盘 I/O。这时候 I/O 带宽和延迟就是瓶颈。优化方向是减少 I/O 次数——用更大的缓冲区、做多路归并减少读写轮次、用压缩减少数据量。判断大数据任务是否受限于 CPU可以看 CPU 的 IPC 和 stall 周期。如果 IPC 很低、stall 周期很高说明 CPU 在等内存或 I/O单纯增加 CPU 核心数没用。5.3 数据库场景随机读写与日志写入的博弈数据库的性能问题通常集中在两个地方随机读写和日志写入。随机读写主要发生在索引查找和数据页访问。B 树的查找需要多次随机访问每次都可能触发磁盘 I/O。如果索引太大放不进内存性能会急剧下降。优化方向包括增大缓冲池、用更紧凑的索引结构、做分区减少索引大小。日志写入是另一个关键路径。数据库需要保证事务的持久性通常采用 WALWrite-Ahead Logging机制先把日志写到磁盘再写数据页。日志写入是顺序的但每次事务提交都需要 fsync 确保日志落盘这个 fsync 的延迟直接决定了事务的响应时间。优化日志写入的方法包括批量提交把多个事务的日志合并成一次 fsync、用非易失性内存NVDIMM做日志设备、调整日志文件系统的挂载参数。但要注意这些优化可能牺牲一定的持久性保证需要根据业务场景权衡。5.4 容器与虚拟化抽象层的性能代价容器和虚拟化带来了部署的便利性但也引入了额外的性能开销。容器本身的开销很小因为容器共享宿主机的内核没有硬件虚拟化。但容器的网络和存储通常有额外开销。比如 Docker 默认的 bridge 网络需要做 NAT增加了数据包的处理路径。用 host 网络模式可以避免这个开销但牺牲了网络隔离。虚拟机的开销更大因为需要模拟硬件、运行独立的操作系统。CPU 虚拟化通过硬件扩展Intel VT-x、AMD-V已经接近原生性能但 I/O 虚拟化仍然是瓶颈。virtio 半虚拟化驱动比全模拟设备快很多但相比原生设备仍有差距。SR-IOV 可以让虚拟机直接访问物理设备性能接近原生但配置复杂且需要硬件支持。实操心得在容器或虚拟机中做性能测试时一定要先测裸机的基线性能。否则你无法区分性能问题是应用本身导致的还是虚拟化层引入的。我见过不少团队在容器里测出性能不达标花了很多时间优化应用最后发现是容器网络配置的问题。6. 性能优化的常见误区与避坑指南6.1 过早优化与过度优化平衡的艺术Donald Knuth 那句“过早优化是万恶之源”被引用了太多次以至于很多人把它当成不优化的借口。但这句话的本意是不要在没有测量数据的情况下凭直觉优化。如果你已经通过 profiling 定位到了瓶颈那优化就不是“过早”。反过来过度优化也是问题。为了追求极致的性能把代码写得极其复杂、难以维护但实际收益可能只有几个百分点。这种优化不值得。优化的收益应该和它带来的复杂度成正比。如果优化让代码复杂度翻倍但性能只提升 5%那不如不做。判断是否值得优化的一个简单标准这个优化是否解决了实际的性能问题如果当前性能已经满足需求那就不需要优化。如果性能不达标那就优化瓶颈最大的部分而不是所有部分。6.2 盲目升级硬件钱要花在刀刃上遇到性能问题就升级硬件这是最省事但也最费钱的做法。而且很多时候升级硬件并不能解决问题。比如一个受限于单线程性能的应用你给它加再多核心也没用。一个受限于内存延迟的应用你给它换更高频率的内存可能只有几个百分点的提升。一个受限于随机 I/O 的应用你给它换更大容量的 SSD 也不解决问题。正确的做法是先做性能分析定位到瓶颈资源然后针对性地升级。如果瓶颈是 CPU 单核性能那就选单核性能更强的 CPU如果瓶颈是内存带宽那就增加内存通道数或换更高频率的内存如果瓶颈是 I/O 延迟那就换更低延迟的存储设备。6.3 忽视可观测性没有数据就没有优化性能优化的前提是能观测到性能数据。如果系统没有埋点、没有监控、没有日志那优化就是盲人摸象。可观测性包括三个层面指标metrics、日志logs、追踪traces。指标是聚合的数值比如 QPS、延迟、错误率日志是离散的事件记录追踪是请求级别的调用链。对于性能优化来说指标是最基础的。你需要知道系统的吞吐量、延迟分布、资源利用率。延迟分布尤其重要——平均值会掩盖长尾问题P99、P999 才能反映真实用户体验。追踪则能帮你定位到具体是哪个环节慢。一个请求经过多个服务如果没有追踪你很难知道是哪个服务拖慢了整体。OpenTelemetry 是目前主流的追踪标准支持多种语言和后端。6.4 忽略工作负载特征不同场景不同策略性能优化没有银弹不同的工作负载需要不同的策略。CPU 密集型任务关注的是指令级并行度、分支预测、缓存命中率。优化方向包括算法优化、SIMD 向量化、减少分支。内存密集型任务关注的是内存带宽和延迟。优化方向包括数据布局优化、缓存分块、预取。I/O 密集型任务关注的是吞吐量和延迟。优化方向包括批量处理、异步 I/O、减少数据拷贝。延迟敏感型任务关注的是 P99 延迟。优化方向包括减少排队、避免锁竞争、用无锁数据结构。吞吐量敏感型任务关注的是单位时间处理量。优化方向包括提高并行度、流水线化、减少同步开销。在优化之前先搞清楚你的工作负载属于哪一类然后选择对应的策略。用错策略可能适得其反。7. 我在实际调优中积累的几个判断技巧调优这件事工具和方法论是基础但真正拉开差距的往往是经验直觉。分享几个我在实际工作中总结的判断技巧不一定严谨但实战中挺管用。第一看 CPU 利用率的同时看 IPC。如果 CPU 利用率高但 IPC 低说明 CPU 在空转等数据瓶颈在内存或 I/O。如果 CPU 利用率高且 IPC 也高说明 CPU 确实在干活可以考虑加核心或优化算法。如果 CPU 利用率低但任务慢那瓶颈肯定不在 CPU去查 I/O、锁、网络。第二延迟问题优先怀疑排队。如果一个请求的延迟很高但 CPU、内存、磁盘的利用率都不高那大概率是排队导致的。排队可能发生在锁、连接池、线程池、队列等任何地方。找到排队点要么增加资源要么减少排队时间。第三吞吐量问题优先怀疑并行度。如果吞吐量上不去先看并行度够不够。单线程程序加线程多线程程序看锁竞争分布式系统看分片是否均匀。并行度不够的原因可能是串行部分太多阿姆达尔定律也可能是同步开销太大。第四性能突然下降优先怀疑外部变化。如果系统之前跑得好好的突然变慢了先别急着改代码。查一下最近有没有部署新版本、有没有改配置、数据量有没有突增、有没有其他进程在抢资源。很多性能问题不是代码本身的问题而是环境变化导致的。第五优化效果要能量化。每次优化前后都要有明确的性能数据对比。如果优化后性能没有明显提升要么是优化方向错了要么是瓶颈不在那里。不要因为“理论上应该更快”就认为优化有效数据不会骗人。最后再分享一个小技巧在做性能优化时准备一个“性能日志”记录每次优化的假设、操作、结果和结论。过一段时间回头看你会发现很多当时觉得理所当然的判断其实是错的而这些错误正是经验积累的来源。