# 客户架构师说系统会崩？那就一起压测

> 开跑之前先商定通过标准、环境和场景，质疑者就成了测试结果的共同作者。

原文: https://fdetimes.net/zh/guides/joint-load-test-client-architect/

架构评审已经进行了 40 分钟。你刚介绍完一个要部署在客户侧的文档搜索服务的设计，对方的首席架构师双臂交叉：“每个请求都同步经过同一个数据库，到了高峰期肯定会崩。”

你认为设计没有问题，他认为一定会崩。你们争论的是一件还没发生的事，谁也没法靠嘴赢，而多吵一分钟，客户对你的信任就少一分。

吵得更好也没用。更好的做法是，邀请他一起设计那个可能证明他是对的测试。优秀的前线部署工程师（FDE）和只会写代码的人，差别就在这项技能：把分歧变成一场实验，而评判标准由双方事先商定。

## 为什么测试能让争论降温？

因为它改变了问题本身。“会不会崩？”是信念之争；“在多大负载下崩、哪个组件先出问题？”是工程问题，而每个工程师都想知道答案。

在 Google，要安全地运行一个系统，必须先了解它的极限。Google 的 SRE 书中明确写道，工程师用压力测试找出 Web 服务的极限，负载测试能揭示崩溃点，这是容量规划的基础知识。

这样看，架构师的警告并不是麻烦，而是一条还没人写下来的测试需求。

当你说“您说得对，我们确实不知道崩溃点在哪里，一起把它找出来吧”，他就从批评者变成了共同作者。无论结果如何，测试方法从一开始就得到了他的认可。

**要点:** 不要试图赢下争论。和客户一起写清楚，在什么条件下其中一方算输。

## 阈值必须在开跑之前签字确认

危害最大的错误，是先跑测试，再讨论结果意味着什么。到那时，双方都会按对自己有利的方式解读图表。AWS Well-Architected 正是把这一点列为反模式：没有定义清晰、可度量的性能要求，例如响应时间、吞吐量和可扩展性目标。

Grafana k6 为此内置了一套机制：thresholds，即为每个指标声明的通过/失败标准。阈值就写在测试文件里，双方商定的内容因此保存在版本控制中，而不只是会议纪要里的一行字。

第二件要商定的事是在哪里跑。AWS 提醒，缩小规模的测试环境可能误判系统在生产环境中的表现。如果你在一台很小的预发布机器上跑，系统扛住了，架构师完全有理由不认这个结果。所以，让他来选环境。

## 一个完整的示例

沿用上面的场景，并假设一些数字。经过 20 分钟的讨论，双方商定：正常负载是 50 个并发用户，架构师担心的峰值是它的四倍。正常负载下的通过阈值是 p95 低于 800 ms、错误率低于 1%。

架构师又补充了一个你没想到的问题：如果系统在高峰期崩溃，负载要降到多少它才能自行恢复？Google 的 SRE 书也提出过同样的问题：当部分服务器在高负载下崩溃时，负载要降到多低，系统才能重新稳定？于是，场景分为三个阶段。

```javascript
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 岗位时，准备好讲一段这样的经历，因为它说明你能用数字而不是嗓门来化解与客户的技术分歧。

本周练习：回想上一次你和别人在性能问题上意见不合的情形。写下双方本该事先商定的三个数字，再写一个用来检验它们的三阶段场景。下次再有人双臂交叉说“它会崩”，你就有现成的回答：“好，我们一起看看它在哪里崩。”

**本周动手试试:**

- 在当前项目中挑一个端点，写下三个数字：可接受的 p95、最大错误率和高峰时段负载。问问客户方的同事是否认同。
- 在预发布环境中运行本文的三阶段 k6 场景，并检查预发布环境的数据量是否和生产环境一样大。
- 为团队最近一次争论过的架构决策写一页 ADR，写明用来平息争论的数字。

## 信源

- [Testing for Reliability (Google SRE book)](https://sre.google/sre-book/testing-reliability/)

- [Addressing Cascading Failures (Google SRE book)](https://sre.google/sre-book/addressing-cascading-failures/)

- [A pattern for successful load testing: Component Load Testing | Microsoft Learn](https://learn.microsoft.com/en-us/archive/blogs/edglas/a-pattern-for-successful-load-testing-component-load-testing)

- [REL12-BP03 Test scalability and performance requirements (AWS Well-Architected)](https://docs.aws.amazon.com/wellarchitected/latest/framework/rel_testing_resiliency_test_non_functional.html)

- [Thresholds in k6: Pass/Fail Criteria for Tests](https://grafana.com/docs/k6/latest/using-k6/thresholds/)

- [Load test types (Grafana k6 docs)](https://grafana.com/docs/k6/latest/testing-guides/test-types/)

- [Architectural Decision Records (ADRs)](https://adr.github.io/)
