软件架构第 5 部分:测量、改变、再测量:负载测试、真实考试晚上和最终架构
七个测量、一个真实考试晚上和一个 scale-in 错误:负载测试、容量模型和诚实 soak 计划如何塑造 NovaCommerce 最终架构、成本以及解决方案架构师清单。第 5 部分结束系列。
一个虚构名称下的真实数字:一个前端服务器、两个固定后端、一个数据库 node 和两个关闭的 standby。第 1 部分定义高可用性并展示瓶颈如何一直移动。
Author
Anichur Rahaman

Part 1 of 5
从单服务器到考试日就绪在 2026 年 9 月 25 日,学生在线考试期间开始看到 HTTP 429 错误,"请求过多"。造成问题的不是服务器不足。问题出在一条规则上:速率限制器按 IP 地址计数,而一所学校可能有数百个学生共享一个地址。对限制器而言,整个校舍看起来像一个非常没有耐心的人。
十一天后,2026 年 10 月 6 日凌晨 02:28,后端负载测试返回了 2,854 个应用错误。每一条都说同样的话:连接 Valkey 超时。这是另一层的问题,另一种原因,也需要另一种修复。之后是纯粹的 CPU 饱和,这是真正需要更多服务器才能解决的瓶颈。
这个序列就是这个项目的真实故事:瓶颈一直在移动。加服务器不是架构。它是几种解决方案中的一种,也是最后一种。
在 2026 年 9 月下旬到 10 月 7 日之间,我们将一个运行在 DigitalOcean 上的 Laravel 和 Next.js 平台从一个前端服务器和两个固定后端升级到准备好应对定时考试高峰的架构。本部分涵盖起点:业务问题、我们继承的系统、为什么它不够用、对我们来说 "高可用性" 意味着什么,以及最初的测试告诉我们什么。
在我们开始之前:NovaCommerce 是这个案例研究中的虚构名称。我省略了任何能够识别该公司、其服务器或其学生的信息。
这是五部分案例研究 "从单服务器到考试日就绪" 的第 1 部分。NovaCommerce 是虚构名称;架构、数字和错误都是真实的。
NovaCommerce 是一个在线平台,销售课程并为学生运行定时在线考试。用户注册、付款、参加实时考试、查看结果并使用 AI 学习辅助。
大多数网络流量是一个山坡:上午上升,晚上下降。考试流量是一个悬崖。想象一个音乐厅的门。几个小时什么都不发生,然后门打开,所有人同时到达。当定时考试开始时,数千名学生在同一分钟内涌入平台。
好消息是这个悬崖有时间表。我们知道它什么时候来。我在关于 流量峰值自动扩展的指南 中用通用术语描述了这个优势。本系列是另一半:一个真实的平台,带有真实的数字。
业务给了我们四个要求,用简洁的语言表达。
本系列中的每个技术选择都可以追溯到这四行中的一行。
以下是我们在 2026 年 10 月 5 日之前发现的设置,在一个 DigitalOcean 地域和一个私有网络中。

前端是一个单一 droplet,有 8 个 vCPU 和 16 GB 内存,运行一个 Next.js 容器。域名直接指向它,TLS 来自 droplet 上的 certbot。没有 load balancer。如果那个服务器宕了,网站就下线了。
它也浪费了金钱。一个 Node.js 进程大约在一个核心上渲染,所以大部分八个 vCPU 都闲置了:一个有八个收银台但只有一个收银员的商店。
一个 DigitalOcean load balancer 管理两个固定的后端 droplet,每个有 4 vCPU 和 8 GB。应用是 Laravel(PHP-FPM)在 Docker 中运行在 nginx 后面,每个 node 有两个容器:web 用于 nginx,app 用于 php-fpm。队列(Horizon)、调度器和 Reverb(WebSocket 服务器用于实时更新)运行在一个 worker 上。
load balancer 按其自己的固定 ID 针对每个后端,而不是按标签。这是有名单的宾客名单和一条说 "任何戴着员工徽章的人" 的规则之间的区别。有了名单,新服务器永远无法自行加入。必须有人编辑列表。
这里有好消息,它比什么都重要。后端 web node 已经是无状态的了。会话、缓存和队列存在托管的 Valkey 中,日志去往标准错误。一个不拥有任何东西的服务器就是一个可以删除的服务器。没有这个属性,本系列的其余部分就不可能发生。
数据库是一个托管 MySQL "Advanced" node,有 8 vCPU 和 32 GB:单个 node,没有 standby。
然后有两个 "standby" droplet,保作备份。它们被关闭了。一个关闭的 droplet 仍然花钱,启动它需要几分钟。这就像在城外一个被锁起来的车库里放的备用轮胎:它存在,你为它付款,但当你卡在路上时它也帮不上忙。这不是高可用性。
两个较小的问题坐在角落里。load balancer 的 TLS 证书是手动上传的,有过期日期,所以续期是一个有人必须记住的重复任务。而且账户的 droplet 限制是 25,这在两个池可以增长并替换 node 时会成为问题(第 2 部分)。
| 设置的部分 | 如何构建的 | 压力或故障下会发生什么 |
|---|---|---|
| 前端 | 一个 8 vCPU / 16 GB droplet,一个 Next.js 容器,没有 load balancer | 如果它宕了网站就下线;大多数核心闲置,因为一个 Node 进程大约使用一个核心 |
| 后端 | 两个固定的 4 vCPU / 8 GB droplet,由 load balancer 管理,按 ID 针对 | 没有自动增长;一个新服务器永远无法自行加入 |
| 数据库 | 一个托管 MySQL node,8 vCPU / 32 GB,没有 standby | 如果 node 故障就没有第二个 node 接管 |
| Standby | 两个 droplet,关闭状态 | 关闭时计费;启动需要几分钟 |
看看那个表中缺少什么:一个 bug。没有什么坏掉了。设置按其设计的方式工作。问题是定时悬崖和一个无法失去服务器、无法自己添加一个、并保持备用容量关闭的设置之间的不匹配。你无法修补不匹配。你必须改变系统的形状,那就是架构的意思。
"高可用性" 是那些每个人都点头但没人定义的短语之一。在我们改变任何东西之前,我们写下了它对这个平台意味着什么。它归结为四行。
没有单个服务器的丢失会导致网站下线。数据库在 node 丢失后仍能幸存。部署和缩减对学生是无形的。容量在考试开始前就准备好,不是五分钟后。
我喜欢一个可以用问题测试的定义。我们能拔掉任何一个服务器的电源并继续服务吗?数据库 node 故障后考试能继续吗?我们能在不被学生察觉的情况下发布版本吗?第一个学生点击 "开始" 时容量是否已经就位?
两条业务规则在其上面:提交从不丢失,成本保持理性。这个图表将每个要求映射到必须交付它的层以及本系列中覆盖该层的部分。

定义的最后一行,"不是五分钟后",是最让人疼痛的。在我们的测试中,自动扩展在 CPU 达到 99% 后大约七分钟添加了第一个额外 node。七分钟在考试在一分钟内开始时是很长的。容量必须在悬崖之前就准备好,而不是在悬崖之后。
随着定义的写下,我们做了方法要求的:先测量。在 9 月下旬到 10 月 6 日之间,我们观察生产并运行负载测试,发现了连续的三个问题。它们看起来无关,那就是问题所在。

在生产中测量:学生在考试期间获得 HTTP 429。全局 API 速率限制器按 IP 计键,因为针对基于令牌的学生的默认 auth guard 是空的,所以限制器无法判断学生是谁。一所学校或移动运营商将数百个学生放在一个 NAT 地址后面,整个建筑共享一个桶。
状态:Implemented。我们将 API 限制器按学生或讲师账户计键,只有客人按 IP。更多服务器无法帮助;规则是在说不,不是硬件。这个 bug 的一个近亲在 10 月 7 日在路由级别的限流中回来,那个故事在第 4 部分。
在 2026 年 10 月 6 日 02:28,后端负载测试返回了 2,854 个应用 500 错误。每一个都是 RedisException: Operation timed out 在连接时,connect timeout 是 5 秒。Valkey(4 GB,primary 加 standby)在负载下无法足够快速地接受连接。
应用对它的使用方式加重了压力。每个请求都向 Valkey 打开了一个新的 TLS 连接(大约每个请求 5.4 ms CPU)和另一个到 MySQL(大约 3.3 ms)。想象一个商店,每个顾客都被查询和逐次检查通过门。在一群人的面前,门成了队列。
状态:Implemented,然后 Tested。我们在原地调整了 Valkey 到 8 GB,两个 node(primary 和 standby),数据保留,主机不变。然后我们用从 25 到 500 请求/秒的逐步上升重新测试,从两个后端 node 开始,而池在负载下扩展:0 应用错误和 0 后端 5xx。在 load balancer 处有 143 错误,但仅在 node CPU 饱和时。那是预期的 "超出容量" 信号,不是 bug。数据层得到第 3 部分,Valkey 在第 4 部分回归。对于通用理论,见 负载下的数据层。
修复了前两个后,剩下的就是纯粹的算术。一个 4 vCPU / 8 GB 后端 node 服务大约 65 到 70 requests/s 的真实 API 混合,在全 CPU 时,大约每个请求 59 ms CPU。之后的每个容量决定都是建立在那个数字之上的。
这是三个瓶颈中唯一一个更多服务器真正解决的,即使在这里时间也是陷阱:第一个额外 node 在 CPU 触及 99% 后约七分钟到达。我们如何让容量提前到达是第 2 部分的主题。
所以顺序是一个限制器(代码),然后 Valkey 连接(数据层尺寸),然后后端 CPU(容量)。如果我们从添加服务器开始,限制器仍然会说 429,Valkey 仍然会超时。
在谈论新形状之前,知道旧形状的成本会有帮助。这些是旧 web 层的 DigitalOcean 列表价格(每月)。
| 项目 | 成本 | 它买了什么 |
|---|---|---|
| 旧 web 层:前端、两个后端、worker、一个 load balancer、两个 standby | 约 $416/月 | 一个有多个单点故障的运行网站 |
| 两个关闭的 standby droplet | $112/月 | 没有可用性:关闭时计费,启动需要几分钟 |
| 一个额外的后端 node,用于比较 | 约 $0.08/小时 | 真正服务请求的容量 |
那个 $112,web 层的四分之一多一点,是让我记住的数字。它购买了两个无法服务单个请求的服务器。pre-scaling 一个额外的后端 node 成本约 $0.08/小时,所以为一个晚上提高容量成本数美分。全月为关闭的容量付费是感觉安全的更贵方式。
一个诚实的注意:$416 只覆盖 web 层,不是数据库、Valkey 或存储,所以它不能与完整账单比较。在第 5 部分我会比较苹果和苹果。
我们在整个项目中重复的模式就是你刚才看到的:测量、找到瓶颈、理解原因、改变架构、再测试、再测量。添加服务器不是架构。架构是选择哪一层改变,以及为什么。
这里是五部分如何组合的。
| 部分 | 层 | 你会看到什么 |
|---|---|---|
| 1(这部分) | 起点 | 业务问题、旧设置、高可用性的定义、最初的瓶颈和旧成本 |
| 2 | 应用层 | 两个 load balancer、两个 autoscale 池、pre-scaling、基于镜像的部署和 rollout 课程 |
| 3 | 数据库 | 从一个 MySQL node 到一个有 standby 的 26 分钟窗口、读/写分离、带 pgvector 的 PostgreSQL |
| 4 | Valkey、worker、日志 | 队列、worker、日志、监控以及在真实考试期间犯的缩减错误 |
| 5 | 证明 | 2026 年 10 月 7 日的真实考试晚上、负载和 soak 测试,以及最终架构 |
我们也考虑了我们没有构建的想法,并标记了每一个。一个用于两个层的 load balancer 被考虑,然后拒绝。一个 "准备好在几秒内接管的温 standby" 被考虑,但 autoscale 池没有温池,所以我们使用已经服务的备用容量加 pre-scaling。Kubernetes 被考虑以后使用,没有实现。
不是所有东西都完成了,我会说。单个 worker 服务器是一个已知的单点故障;修复它被计划,没完成(第 4 部分)。一个正式的多小时 soak 测试也被计划(第 5 部分)。
目标作为清单,带有构建或测试每一行的部分。
接下来,在 第 2 部分,我们构建应用层:两个 load balancer、两个 autoscale 池、pre-scaling,以及从短 503 闪烁学到的 rollout 方法。
Series
从单服务器到考试日就绪About the Author
Anichur Rahaman
Continue Reading