1 系统性能工程要回答什么
终端里执行同样的命令,昨天 2 秒完成,今天却要 8 秒;文件处理服务没有报错,等待队列却越积越长;主机 CPU 看起来还有余量,用户仍然觉得慢。这些现象都属于性能工程的入口。它要回答的是:什么工作在什么条件下变慢,时间消耗在哪里,怎样用证据选择改动,以及改动会付出什么代价。只看到“机器忙”或“机器闲”,都不足以给出答案。
先把“快”写成可以检查的目标。设想 1 个 Linux 文件索引服务:工作进程读取目录中的文件,更新内存索引,把处理结果追加到日志,并通过 TCP 连接接收查询。目标可以是:在约定的数据集和并发 16 下,持续完成至少 800 次查询/秒,p95 延迟不超过 40 毫秒,错误率低于 0.1%。这里的数值是教学示例,真正的门槛应来自使用需求和已知容量。没有负载与环境条件,“每秒 800 次”无法迁移到别的机器。
吞吐表示单位时间完成多少工作,延迟表示完成工作花多久,错误率表示多少工作没有按约定完成。p95 为延迟分布的 95 分位,表示约 95% 的样本不高于该值;它不等于平均值,也不表示最长等待。还应区分成功请求延迟和全部请求的结果:大量请求快速失败,可能使平均延迟下降,却让系统更不可用。报告里应同时写完成量、错误和超时,不能只挑好看的曲线。
资源指标帮助解释现象,但目标通常由工作完成情况决定。CPU 利用率、进程内存、磁盘队列、连接队列都是线索。它们需要附带对象和范围:是进程用了 100% 的 1 个逻辑 CPU,还是整机全部 CPU 用满?是某个目录所在文件系统空间不足,还是内存不足?性能工程要求先说清楚测量口径,再讨论瓶颈。否则同样的数字可能指向不同问题。

调查开始时,可以写下简短的问题陈述:“版本 B 在相同输入和并发下,完成量比版本 A 下降约 30%,p95 上升,错误率基本不变,回归出现在本次发布后。”这比“系统需要优化”更有用。它给出了比较对象和可被推翻的判断,也提醒我们版本变化只是相关线索,仍需排除机器、数据和外部依赖的差异。
2 方法论、基线与火焰图
基线是后续比较所依据的测量记录。它至少包括软件版本、启动参数、主机与资源限制、输入数据、并发或到达速率、预热方式、采样时段和测量工具。文件扫描尤其受缓存状态影响:刚读取过的数据可能从内存返回,首次访问则需要真正访问存储。应分别说明首次访问与预热后的表现,不能把旧版的冷启动结果与新版的热缓存结果放在同个结论里。不要为了制造“冷环境”随意在共享主机清理缓存。
负载模型也要明确。固定并发意味着前面的工作完成后,客户端再补充后续工作;服务变慢时,客户端可能自然减少发送速率。固定到达速率则持续按计划产生工作,服务能力不足时可能积累排队。两者测到的延迟与吞吐不能直接互换。入门实验可以采用固定并发,但应记录实际发送量、完成量、超时与在途数量,并确认发生器自身没有先耗尽 CPU、连接或带宽。
通常先重复运行旧版与新版,观察差异是否大于自然波动。可按 A、B、B、A 的顺序交替测试,减弱时间变化的影响;每轮使用相同预热和持续时间。需要记录异常轮次及原因,而不是删去不利结果。持续 120 秒的测试只支持这个窗口内的判断,未必覆盖缓存过期、日志轮转或长时间内存增长。基线的价值来自条件清楚和可以重做,不来自数字的小数位很多。

火焰图把采样得到的调用栈聚合展示。横向宽度通常表示某类调用栈在样本中占的比例,纵向表示调用层次。横轴位置通常不是时间顺序,图形高也不等于耗时长。CPU 火焰图中较宽的栈提示较多 CPU 样本落在那里,适合寻找反复执行的计算路径;但只采集正在 CPU 上运行的代码,可能看不到等锁、等文件或等网络的主要时间。因此,图里没有某个等待函数,不代表不存在等待。
采样必须说明类型、对象、时长和权限。例如在授权的测试主机上,perf record -F 49 -g -p "$pid" -- sleep 30 可对指定进程采集 30 秒样本,再用 perf report 查看聚合结果;具体调用栈完整性取决于构建信息和环境配置,火焰图可由兼容工具进一步转换生成。权限不足时记录限制,采用应用指标和线程状态继续调查,不为拿到图而放宽生产主机限制。先确认问题在采样窗口内发生,再解释样本。
3 CPU、内存、IO、网络联动
1 次任务可能先等待连接输入,再执行解析,读取文件,进入共享数据结构,最后发送结果。端到端时间包含计算、排队和等待,资源之间会互相改变需求。增加工作线程可能让 CPU 利用率提高,也可能让更多线程争用同个锁,让在途数据占用更多内存。缓存减少文件读取,却可能挤压其他进程的内存。看到某项利用率下降,不代表总体性能变好了,必须回到完成量、延迟和错误检查。
CPU 方面先分清运行与等待。top 可以观察整体趋势,top -H -p "$pid" 可以查看线程,pidstat -u -p "$pid" 1 可以按间隔观察进程 CPU。不同工具的百分比可能采用不同归一化方式,比较前要核对。整机平均利用率不高,也可能有 1 个执行串行工作的线程占满 1 个逻辑 CPU。另外,很多线程处于等待时,CPU 可以很闲;直接增加 CPU 配额未必改变那个等待条件。
内存方面,进程 RSS 表示驻留内存的某种统计口径,不能简单等同于应用独占的全部内存成本;共享页等因素会影响解释。free -m 的 available 比单看 free 更有参考价值,因为部分缓存能够回收。vmstat 1 可以帮助观察运行队列、换入换出和 CPU 状态,其首行通常是启动以来的平均,应与后续间隔行区分。容器或服务资源限制也要一起记录,主机有空闲不保证进程还能继续分配。
文件 IO 的慢可能来自读取量增加、随机访问、同步写入、设备排队,或文件系统空间与元数据问题。iostat -xz 1 可观察设备级活动,pidstat -d -p "$pid" 1 可辅助关联进程读写;这些命令在部分系统需要安装相应工具。设备平均等待是线索,不能代替某次应用操作的耗时;单个利用率百分比也不适合作为所有存储设备的通用饱和判据。df -h 与 df -i 分别帮助检查空间和 inode 用量。
网络方面,可用 ss -tinp 观察 TCP 连接状态与队列,结合应用连接建立耗时、读写耗时和对端日志理解等待。重传上升可能与丢包有关,但接收变慢、对端处理能力不足也会拉长响应时间。ping 成功只能说明相应探测在当时有回应,不能证明业务端口、传输量和应用处理均正常。网络栈在本课作为连接与传输的系统接口理解,暂不展开其内部实现。

这些证据必须落在相同时间窗口。假如延迟高峰发生在 14:05,而磁盘统计取自 14:20,二者很难互相解释。记录主机时钟、时区、进程标识和采样间隔,必要时用版本与实例标签连接各组数据。初步归因应表述为“哪些证据支持什么假设,还缺什么反证”,避免把资源曲线上的相似形状当成因果关系。
4 动手:定位一次回归的主因
准备隔离的 Linux 测试环境,使用已有的文件索引服务与负载脚本。约定脚本 ./load.sh 支持并发和时长参数,并输出完成量、错误、超时和延迟分布;这是本例接口,需要按真实工具调整。固定数据目录和请求集合,用旧版 A 与新版 B 比较。若目前没有可运行服务,也可以先按步骤审阅现有压测记录;不要把这里的示意输出冒充实测结果。
先核对进程,避免把终端、父进程或旧实例当成目标。用 ps -eo pid,ppid,comm,args 查找服务,再人工确认 PID,例如设置 pid=12345,随后用 ps -p "$pid" -o pid,lstart,etime,args 核对启动时间和命令行。命令行可能包含敏感参数,保存报告前应脱敏。记录服务提交版本、数据版本及资源限制;应用自报版本与实际启动文件也应对应,避免测到替换文件前的旧进程。
确认预热完成后,用 ./load.sh --concurrency 16 --duration 120 发起本例负载,在另外的终端同时采集线程、主机与连接状态。可以使用 pidstat -u -r -d -p "$pid" 1 和 vmstat 1,需要结束时只停止自己的采样命令。让每份输出带上实验编号和时间范围。通过 shell 重定向保存时,给文件取清楚的版本名,避免 > 覆盖旧结果;管道处理还要留意各环节退出码,不能把空报告当作没有异常。
假想结果是:A 每秒完成 800 次,p95 为 30 毫秒;B 每秒完成 560 次,p95 为 85 毫秒,两者错误率均为 0。先检查发送量与在途任务,确认发生器有余量,再查看同期 CPU、线程与文件活动。若 B 的 CPU 只有约 35%,大量工作线程等待,而磁盘没有明显整体拥塞,合理的下一步是区分等锁、等文件和等对端,尚不能直接宣布锁竞争已经证实。

应用已有的任务阶段计时、锁等待计时以及线程栈可以继续缩小范围。若需要额外跟踪,可在测试环境短时使用 strace -f -tt -T -p "$pid",观察系统调用及其耗时;它可能增加开销,输出也可能涉及路径和数据。看到较长的 futex 等待是同步等待的线索,仍需结合应用栈确认具体对象。看见长时间 read 或 recvfrom 也要核对文件描述符用途,不能仅凭函数名判断依赖类型。
现在把假设写成预测:“新版把文件追加放进共享索引的锁内,持锁者等待文件写入时,其余线程排队;缩短临界区应降低锁等待并恢复吞吐。”在测试版本中只改变这个因素,保持工作线程数、数据与负载不变。重复比较后,若等待下降、完成量恢复,证据便更强;若结果没有变化,就保存反证并调查其他等待来源。结论应包含测量范围,不应只留“已优化”几个字。
5 优化收益与复杂度取舍
优化的收益应与可维护性和资源代价同时讨论。把读取结果缓存起来,可能减少文件访问,但新增过期规则、内存预算和一致性要求。把任务拆成更多线程,可能提高并行度,也增加同步与调试负担。把日志异步写入,可能缩短请求路径,却改变失败时能保留多少记录。每项改动都需要说明它改变了哪段成本,是否改变结果语义,故障时由谁处理积压和丢失。
固定成本限制了总收益。假设某个串行任务原来耗时 100 毫秒,其中可优化部分占 40 毫秒;即使这部分耗时降为 0,剩余仍有 60 毫秒,总加速上限约为 1.67 倍。若只把这部分提高到原来的 2 倍速度,总耗时为 80 毫秒,加速为 1.25 倍。这种预算计算能帮助确定努力方向,但不直接预测并发系统的尾延迟,因为排队与竞争还可能随负载变化。
滑块用简化缓存模型展示收益递减。缓存从 0 增至 2000 条时,模型平均耗时从 10 毫秒降至 6 毫秒;继续增加仍有收益,但每增加相同条目数,节省的时间逐渐减少。每条约占 4 KiB,条目数达到 10000 时约需 39.06 MiB,已经超过示例预算 24 MiB。这里的曲线用于理解选择关系,真实命中率由访问分布决定,不能把模型里的平均值写成真实系统的 p95 承诺。
评估时可以列出收益、资源、复杂度和风险。例如修复重复文件扫描,可能用较小改动直接减少 IO;引入全新缓存层则需要更多运维约束。优先级应由证据和目标缺口决定。若当前延迟已经满足目标,而改动会使数据一致性难以验证,就没有必要仅为追求漂亮数字扩大范围。收益还要与业务负载匹配:在并发 1 下有效,不保证在并发 64 下仍有效。
优化后的容量余量也要保留。测试恰好达到每秒 800 次,不表示可以长期稳定承受该负载;输入大小、后台任务和短时突发会改变资源需求。可以逐步提高负载,观察何时排队持续增长、延迟恶化或错误上升,从而确定当前环境的适用范围。这里先建立“容量有边界”的意识,复杂容量建模与可靠性权衡留给后续课程。
6 常见坑:未复现就先改生产
最危险的习惯是把猜测直接变成生产变更。“CPU 不够”于是扩配,“网络慢”于是改连接参数,“内存紧张”于是清缓存。多个动作叠加后,即使性能恢复,也无法知道哪个动作有效,更无法判断是否只是流量下降。生产故障确实可能要求先恢复服务,此时应遵循已有止损预案,记录动作与时间;恢复动作和主因证明是不同的任务,恢复后仍需补齐复现与分析。
基线漂移也很常见。新版用了更小的数据集、旧版同时运行备份、两轮测试跨过日志轮转,都会制造假回归或假收益。检查表不需要很长,但必须包含影响最大的条件:版本、数据、负载、资源限制、缓存状态与后台任务。若条件无法完全一致,应明确差异,并缩小结论,而不是用“环境基本相同”掩盖未知因素。
平均值会掩盖等待,粗采样会漏过尖峰。每分钟平均 CPU 只有 30%,仍可能发生持续几秒的队列拥塞;平均延迟下降,也可能伴随少量任务等待数秒。采样间隔应与问题持续时间相称,但采得越细也不总是越好:过量日志、调用跟踪和高频采样可能改变系统行为。先用成本较低的指标确定时段,再在受控环境集中采集所需证据。
还有把“命令运行成功”误认为“观测有效”的错误。权限不足可能只看到部分进程,过滤条件可能选错 PID,服务重启后旧 PID 已失效,容器内看到的范围可能与宿主机不同。运行命令后应确认对象和时间,并保存工具版本与限制。检查文件时也要考虑 tail 是否仍跟着轮转前的旧文件,不能把日志突然停止直接解释成进程停止工作。
最后要警惕只改结果、不问语义。删除同步写入可能让延迟明显降低,却改变断电后的记录保障;减少校验可能提高吞吐,却让错误结果悄悄通过。性能测试应伴随必要的正确性检查。改动若影响文件权限、追加顺序、失败重试或连接超时,必须把行为变化写清楚。工程上可接受的优化,应当在约定语义下提升表现,而不是省略原本承诺的工作。
7 案例:锁竞争被误判为 CPU 不足
继续看文件索引服务。旧版只在更新共享索引时持锁,日志追加发生在释放锁之后。新版为了让代码路径看起来集中,把日志格式化与文件追加也移进临界区。平时写入很快,小数据测试没有暴露问题;遇到文件写入波动,持锁线程等待变长,其他工作线程排队。用户看到的是查询变慢,运维看到的是队列增长,这两种现象都没有直接告诉我们“锁”在哪里。
最初的猜测是计算资源不足,于是测试人员把工作线程从 4 增加到 8。结果吞吐没有成比例提高,p95 反而恶化。原因是共享锁保护的区域同个时刻仍只有 1 个线程执行,新增线程大多等待进入。若等待会让线程休眠,整机 CPU 利用率仍可能不高;换成额外机器资源也未必改变这个进程内部的串行区。低 CPU 在这里是线索,不能单独作为锁竞争结论。
证据需要互相补足:回归对应临界区变更;同个窗口内线程等待增加;应用阶段计时显示持锁跨度变长;增加工作线程没有恢复完成量;缩短临界区的测试版本降低了等待并改善吞吐。这条链比“火焰图里有锁函数”更可靠。CPU 火焰图可能主要显示仍在运行的日志格式化,等待中的线程未必占据很宽区域,因此要结合等待指标和线程栈解释。

修复也不能机械地把文件写入移出去。若日志顺序必须对应索引更新顺序,直接解锁后写入可能打乱顺序;若追加失败需要回滚索引,错误处理也会变化。测试实现可以先在锁内生成不可变的记录快照,再由明确的写入路径处理,但是否采用这种设计,要由顺序、持久性与失败语义决定。入门阶段先掌握检查这些约束的习惯,不展开复杂同步算法。
最终报告应同时保留成功与失败实验:增加线程未改善,缩短临界区在既定数据和并发下改善;其他负载和长时间运行仍需覆盖。若缩短临界区后磁盘成为新的限制,也不推翻原结论,只说明原先瓶颈被移除后暴露了后续约束。性能工程通常是逐步减少主导成本,不能承诺消除所有等待。
8 把性能门禁写进发布方案
性能门禁把“看起来没问题”变成发布前可检查的规则。门禁应同时包含绝对目标和相对基线。例如固定并发 16、固定数据与环境,要求 p95 不超过 40 毫秒,吞吐不低于约定下限,错误率低于 0.1%;还可以规定相对基线的吞吐下降不得超过 5%。这些是示例门槛,实际阈值要考虑测量波动和容量余量,不能随意套用。
每条规则都要说明样本范围与处理方式。若样本太少、发生器异常或环境被其他任务干扰,本轮应标记无效并调查,不能判定通过。若 1 轮失败后自动重跑直到通过,就等于丢弃失败证据。可约定先完成 3 轮固定测试,统一查看各轮结果、波动和失败原因;遇到接近阈值的结果,由负责人判断是否需要进一步实验,并记录决定。

测试通过后仍需灰度观察。实验室数据集无法覆盖所有真实访问方式,因此先在较小范围运行,观察相同口径的完成量、分位延迟、错误、资源和队列变化。扩大条件与停止条件都应提前确定,例如连续 3 个 1 分钟窗口出现持续排队增长便停止扩大。短窗口中的分位数可能不稳定,应结合样本数和较长窗口检查,不让自动规则追逐随机噪声。
回滚条件必须可执行。写明由谁操作、恢复哪个版本、怎样核对启动成功,以及文件格式或状态变更是否允许旧版继续读取。只保留旧二进制并不保证可以回滚。如果候选版本改变了持久化数据,发布前就要验证兼容路径。性能门禁因此不只是压测脚本,还包括观测口径、结果归档、停止规则和恢复能力;它让优化成果能够在真实运维中被检验。
9 从系统性能工程走向可靠性理论(S61)
性能问题与可靠性问题经常连在一起。服务变慢会让调用者更久地占用连接,排队会增加内存占用,超时后重试又可能增加输入负载。当系统接近处理上限,原本短暂的波动可能持续放大。此时单看平均吞吐是否达标已经不够,还要关心系统能否恢复,失败能否被限制,以及在压力下还能兑现哪些承诺。
不过,“延迟超过目标”与“服务不可用”不能不加定义地混为同件事。有的交互要求很短响应,有的批处理允许等待较长时间;同样的 2 秒,对不同任务的含义不同。应从用户任务定义成功,再决定延迟、正确性和完成期限如何进入指标。S60 学到的测量边界、负载条件和证据链,会成为下一课讨论可靠性目标的基础。
性能调查训练了可被证伪的推理方式:先预测变化,再收集数据,允许结果推翻解释。可靠性工程同样需要这种方法,但要面对更长时间、更多故障来源和更明确的损失约束。我们将在 S61 继续讨论如何表达可靠性、怎样理解失败与恢复,以及为何需要在成本、余量和目标之间做选择。本课先保留问题,不提前展开概率模型与复杂内部机制。
迁移到新的系统时,可以先问:完成工作的接口是什么,哪些阶段可能运行或等待,当前基线覆盖什么负载,哪项证据能区分竞争中的解释?如果只能得到有限观测,就明确未知范围,选择影响较小的验证步骤。能清楚说明“已知什么、凭什么、还不知道什么”,就是从工具使用走向系统工程判断的关键。
放进方案里
在发布方案中增加“性能验证与停止条件”段落,先写用户目标:文件索引服务在约定数据集、并发 16 和持续 120 秒的测试窗口内,完成量、p95、错误率分别满足团队确认的阈值。标明这是测试环境目标,并说明生产灰度采用的窗口和样本要求。记录基线版本、候选版本、启动参数、主机规格、资源限制、数据版本与预热策略,让接手者能够重新执行比较。
安排负责人使用同份负载脚本完成 3 轮测试,保存每轮发送量、完成量、超时、延迟分布与环境异常。同步保存进程 CPU、线程等待、内存、文件 IO 和连接队列的必要观测,统一实验编号与时间。无效轮次必须说明原因;任何失败都保留记录。若结果越过门槛或无法复现,暂停扩大范围,先确认环境,再提出包含预测结果的主因假设。
对拟采用的修复写清改动范围、预期收益和语义约束。例如缩短共享索引临界区时,必须验证日志顺序、失败处理和并发正确性,同时比较相同负载下的吞吐与等待。把增加线程无效等反证纳入报告,避免后续人员重复尝试。缓存或线程数等参数应附资源预算与适用负载,不能只留下推荐数值。
灰度阶段指定值守人与回滚执行人,写明观察窗口、扩大条件和停止条件。若延迟持续超标、队列持续增长或错误率越界,停止扩大,并按已演练的步骤恢复兼容版本。回滚后核对进程版本、服务结果及队列恢复情况,归档操作时间。发布结束后更新基线,保留未覆盖的长时间运行、突发流量和依赖异常,作为后续验证任务,而不是默认通过。
10 小结:下一步可靠性理论(S61)
系统性能工程从明确工作目标开始,靠可比较的基线、对齐的观测和单变量实验推进。CPU、内存、文件与网络指标帮助解释工作为什么变慢,优化则需要同时证明收益、正确性和可运维性。火焰图是证据工具,性能门禁是交付约束,两者都需要清楚的测量范围。
离开本课前,可以口头解释:为什么低 CPU 仍可能高延迟,为什么增加线程会加剧锁等待,以及为什么平均耗时下降不能证明发布安全。下一课 S61 将把这些判断放到失败、恢复与服务承诺中,继续建立可靠性理论的基础。