客户架构师说系统会崩?那就一起压测
开跑之前先商定通过标准、环境和场景,质疑者就成了测试结果的共同作者。
要点速览
- 不要争论系统会不会崩,而是和客户一起写清楚怎么测出它在哪里崩。
- 先商定阈值,使用数据量充足、接近生产环境的环境,先测单个组件,再测整个系统。
- 把负载推到超出预期的水平,同时测量恢复情况,最后用 ADR 记录决策。
架构评审已经进行了 40 分钟。你刚介绍完一个要部署在客户侧的文档搜索服务的设计,对方的首席架构师双臂交叉:“每个请求都同步经过同一个数据库,到了高峰期肯定会崩。”
你认为设计没有问题,他认为一定会崩。你们争论的是一件还没发生的事,谁也没法靠嘴赢,而多吵一分钟,客户对你的信任就少一分。
吵得更好也没用。更好的做法是,邀请他一起设计那个可能证明他是对的测试。优秀的前线部署工程师(FDE)和只会写代码的人,差别就在这项技能:把分歧变成一场实验,而评判标准由双方事先商定。
为什么测试能让争论降温?
因为它改变了问题本身。“会不会崩?”是信念之争;“在多大负载下崩、哪个组件先出问题?”是工程问题,而每个工程师都想知道答案。
在 Google,要安全地运行一个系统,必须先了解它的极限。Google 的 SRE 书中明确写道,工程师用压力测试找出 Web 服务的极限,负载测试能揭示崩溃点,这是容量规划的基础知识。
这样看,架构师的警告并不是麻烦,而是一条还没人写下来的测试需求。
当你说“您说得对,我们确实不知道崩溃点在哪里,一起把它找出来吧”,他就从批评者变成了共同作者。无论结果如何,测试方法从一开始就得到了他的认可。
阈值必须在开跑之前签字确认
危害最大的错误,是先跑测试,再讨论结果意味着什么。到那时,双方都会按对自己有利的方式解读图表。AWS Well-Architected 正是把这一点列为反模式:没有定义清晰、可度量的性能要求,例如响应时间、吞吐量和可扩展性目标。
Grafana k6 为此内置了一套机制:thresholds,即为每个指标声明的通过/失败标准。阈值就写在测试文件里,双方商定的内容因此保存在版本控制中,而不只是会议纪要里的一行字。
第二件要商定的事是在哪里跑。AWS 提醒,缩小规模的测试环境可能误判系统在生产环境中的表现。如果你在一台很小的预发布机器上跑,系统扛住了,架构师完全有理由不认这个结果。所以,让他来选环境。
一个完整的示例
沿用上面的场景,并假设一些数字。经过 20 分钟的讨论,双方商定:正常负载是 50 个并发用户,架构师担心的峰值是它的四倍。正常负载下的通过阈值是 p95 低于 800 ms、错误率低于 1%。
架构师又补充了一个你没想到的问题:如果系统在高峰期崩溃,负载要降到多少它才能自行恢复?Google 的 SRE 书也提出过同样的问题:当部分服务器在高负载下崩溃时,负载要降到多低,系统才能重新稳定?于是,场景分为三个阶段。
import http from 'k6/http';
import { check } from 'k6';
import { SharedArray } from 'k6/data';
// 客户提供的真实(已脱敏)查询,不是编造的
const queries = new SharedArray('queries', () => JSON.parse(open('./queries.json')));
export const options = {
scenarios: {
baseline: { executor: 'constant-vus', vus: 50, duration: '15m' },
stress: {
executor: 'ramping-vus', startTime: '15m', startVUs: 50,
stages: [{ duration: '10m', target: 200 }, { duration: '5m', target: 200 }],
},
recovery: { executor: 'constant-vus', startTime: '30m', vus: 25, duration: '10m' },
},
thresholds: {
// 双方签字认可的阈值:只适用于正常负载和恢复阶段
'http_req_duration{scenario:baseline}': ['p(95)<800'],
'http_req_failed{scenario:baseline}': ['rate<0.01'],
'http_req_duration{scenario:recovery}': ['p(95)<800'],
},
};
export default function () {
const q = queries[Math.floor(Math.random() * queries.length)];
const res = http.post(`${__ENV.BASE_URL}/search`, JSON.stringify({ q }),
{ headers: { 'Content-Type': 'application/json' } });
check(res, { '状态码 200': (r) => r.status === 200 });
}
注意,stress 阶段没有设阈值。它不是用来通过的,而是用来找崩溃点的。k6 按目的给测试分类,压力测试用来观察系统在极限状态下的表现,即负载超出预期平均水平时会怎样。
AWS 同样把“没有把负载推到预期上限之外”列为反模式。
假设结果如下:基线阶段通过;压力阶段用户数超过约 140 后,p95 急剧上升;恢复阶段通过。你们俩都说对了一部分。这个设计能扛住当前负载,但存在上限,而现在双方都看得到上限在哪里。
这里的恢复阶段只测了一个水平,即 25 个用户,所以只能说明系统在这个水平下能否恢复。要完整回答架构师的问题,就把它换成一个分多级降低负载的 ramping-vus,比如 150、100、50,最后 25,并记录 p95 首次回到 800 ms 以下的那一级。
在客户现场自己动手的步骤
尽早开始。Ed Glas 在 Microsoft 博客的一篇文章中指出,许多性能问题出在架构层面,不是上线前临时就能修好的。如果架构师有顾虑,就在这周测,而不是等到上线那周。
测量之前先灌入足够的数据。正如 Ed Glas 所说,许多性能问题只有在数据量很大时才会出现。一个只有 10,000 条记录的数据库,没法检验“这个数据库将来要存几千万条记录”的担忧。
先测单个组件,再测整个系统。Google SRE 指出,每个组件都有自己的崩溃点,都需要单独做负载测试;Ed Glas 则建议从单一事务开始,再到混合事务,最后才是整个系统。
在上面的示例中,场景保持不变,只把 BASE_URL 指向一个只执行数据库查询的端点。如果它比整个系统更早崩溃,你就找到了罪魁祸首。
最后,把结果写下来。架构决策记录(ADR)记录一项架构决策,以及背后的理由、权衡和后果。一份类似“保留同步设计,实测上限约 140 个用户,实际负载达到 100 时引入队列”的 ADR,能把测试结果变成一项有条件的承诺,三个月后谁也不用从头再吵一遍。
让联合测试适得其反的错误
最常见的错误是自己写好场景,再请客户来看结果。没有参与设计测试的客户,不会信任测试结果。让架构师来选峰值负载和测试环境,即使你觉得他的数字有点悲观。
第二个错误是使用编造的查询。一份只有五条查询、反复循环的列表会全部命中缓存,p95 会好得可疑。向客户要一份真实的、已脱敏的查询样本。
第三个错误是把压力阶段当成必须通过的考试。如果你回避把系统推到崩溃点,架构师会认为你害怕答案。找到崩溃点是双方共同的收获,而不是你的失败。
最后一个错误是忘了恢复阶段。高峰期变慢、之后自行恢复的系统,和一崩就起不来的系统截然不同。直接问架构师更担心哪种情况,由他的回答决定恢复阶段要测得多细。
把它写进简历
简历上不要只写“进行了负载测试”,可以这样写:与客户架构团队共同设计联合负载测试,在运行前商定 p95 阈值,定位数据库崩溃点,并用 ADR 记录决策。
面试 FDE 岗位时,准备好讲一段这样的经历,因为它说明你能用数字而不是嗓门来化解与客户的技术分歧。
本周练习:回想上一次你和别人在性能问题上意见不合的情形。写下双方本该事先商定的三个数字,再写一个用来检验它们的三阶段场景。下次再有人双臂交叉说“它会崩”,你就有现成的回答:“好,我们一起看看它在哪里崩。”
这篇文章对你有帮助吗?
感谢你的反馈!
7 个信源
- Testing for Reliability (Google SRE book)
- Addressing Cascading Failures (Google SRE book)
- A pattern for successful load testing: Component Load Testing | Microsoft Learn · 2007-02-17
- REL12-BP03 Test scalability and performance requirements (AWS Well-Architected)
- Thresholds in k6: Pass/Fail Criteria for Tests
- Load test types (Grafana k6 docs)
- Architectural Decision Records (ADRs)