软件架构第 5 部分:测量、改变、再测量:负载测试、真实考试晚上和最终架构
七个测量、一个真实考试晚上和一个 scale-in 错误:负载测试、容量模型和诚实 soak 计划如何塑造 NovaCommerce 最终架构、成本以及解决方案架构师清单。第 5 部分结束系列。
如何重建考试平台的应用层:两个 load balancer、两个 autoscale 池、每个 node 两个容器的 Next.js 前端、基于标签的后端、落后 5 到 8 分钟的 CPU 指标,以及有 219 个探针和 0 个错误的 rollout。
Author
Anichur Rahaman

Part 2 of 5
从单服务器到考试日就绪我们将 DNS 切换到新前端的一个小时后,旧服务器仍在接收约 36% 的流量。我们已经将 TTL 降低到 300 秒。我们已经通过 hosts 文件条目测试了新设置。尽管如此,超过三分之一的访问者直接走过新前门,因为他们的 resolver 记住了旧地址。
那个数字就是旧服务器保持活跃作为我们的返回方式的原因。它也总结了这个阶段:每一步在纸上看起来简单,每一步都隐藏了一个衡量的惊喜。
在第 1 部分中,我们发现最初的瓶颈是一个速率限制器和 Valkey 连接的洪水,不是服务器短缺。只有在那之后添加服务器才成为正确的工具,即使这样也需要一个架构。本文涵盖了它:两个 load balancer、两个 autoscale 池、重建的前端、任何 node 都能被替换的后端,以及不丢弃请求的部署方法。那方法的第一个版本就丢弃了。
这是五部分案例研究 "从单服务器到考试日就绪" 的第 2 部分。NovaCommerce 是虚构名称;架构、数字和错误都是真实的。
在 第 1 部分 中,瓶颈移动了三次。首先是一个按 IP 地址计数学生的速率限制器,所以一所学校共享一个桶并获得 HTTP 429。然后是 Valkey,其中负载测试返回了 2,854 个应用错误,因为每个请求都打开了一个到 Valkey 的新 TLS 连接,而 Valkey 无法快速接受它们。只有第三个,纯粹的后端 CPU,才是更多服务器解决的类型:一个 4 vCPU / 8 GB node 服务大约 65 到 70 requests/s 的真实 API 混合。
添加 node 到前两个会使它们更糟。每个新 node 都会运行相同的限制器代码并打开自己的 Valkey 连接。我们会为更多服务器付费并看到相同的错误,只是速度更快。所以本文是关于第三个瓶颈,做正确。
我们的第一个草图使用单个 load balancer 来处理一切。一个比两个便宜,照看的东西也更少。状态:被考虑,然后被拒绝。
一个 DigitalOcean load balancer 无法按主机名或 URL 路径路由,并且每个都恰好针对一个标签,这意味着一组服务器。前端和后端 node 是不同的组,有不同的尺寸、健康检查和缩放规则。这是一个接待处,给每个访问者同一个房间列表。
DNS 已经将网站和 API 分成了单独的域,所以分离是免费的:每个层一个 load balancer,每个都在自己的池前面。状态:Implemented。两个加在一起成本 $48/月。
早期我们写下了一个吸引人的要求:一个能在 3 到 4 秒内接管流量的温 standby。它不存在这里。DigitalOcean autoscale 池没有温池,一个新 droplet 需要几分钟才能被创建、启动、检查并由 load balancer 接纳。一个在门口等待的出租车和你必须打电话的出租车是不同的产品。
所以我们做了两件无聊的事。
我们也考虑了 Kubernetes(DOKS)用于秒级缩放。这意味着运行更多,所以我们把它留给以后。状态:被考虑,没实现。
旧的前端是一个 8 vCPU / 16 GB droplet 运行一个 Next.js 容器,TLS 由 droplet 上的 certbot 和没有 load balancer。如果它死了,网站就下线了。它也安静地浪费了钱:Node.js 进程大约在一个核心上渲染,所以大部分这八个 CPU 什么都做不了。一个超市有八个收银台但只有一个收银员。
新设计运行几个小的、相同的 node,而不是一个大的。图 1 显示了整个应用层;我们从左边走过。

每个 node 运行 nginx 在两个相同的 Next.js 容器前,一个每 vCPU。重要的设置:
least_conn 在两个容器间平衡:每个请求去到有最少活跃连接的容器。restart: always 和循环日志。/lb-health 仅在 Next.js 应答时通过,所以一个有死应用的 node 离开轮转,即使 nginx 活着。HTTPS 在前端 load balancer 结束,云防火墙让前端 node 仅从那个 load balancer 接受端口 80。
我们在旧服务器旁边构建新前端,并通过我们自己机器上的 hosts 文件条目测试它,所以真实域对我们指向新 load balancer,对所有人都没有。我们对比了 12 个真实页面与旧服务器,降低了 DNS TTL 到 300 秒并切换了。
旧服务器保留用于回滚,因为指向 DNS 回来会是整个撤销。我们比预期需要它更久:一个小时后它仍在接收约 36% 来自缓存 DNS 的流量。TTL 是一个请求,不是一个命令。早期销毁服务器会把约三分之一的访问者发送到一个不再应答的地址。
两个前端 node 服务约 356 requests/s,p95 为 0.05 秒,0 错误,CPU 为 55 到 60%。池首先使用专用 CPU droplet。那个测试显示了充足的余地,所以我们移到了有 2 vCPU / 4 GB 的共享 AMD droplet,约 $28/月每个。一个测量,不是猜测,让较便宜的 node 安全。
| 之前 | 之后 | |
|---|---|---|
| 服务器 | 1 个 droplet,8 vCPU / 16 GB,1 个 Next.js 容器 | 2 到 10 个 node 的池,2 vCPU / 4 GB 共享 AMD,2 个容器每个 |
| 如果一个服务器死了 | 网站下线 | 另一个 node 继续服务 |
| 用 2 个 node 测量 | 渲染限制到约一个核心 | 约 356 requests/s,p95 0.05 s,0 错误,CPU 55 到 60% |
之前,后端 load balancer 按 ID 指向两个 droplet。第三个服务器永远无法自行加入,因为 load balancer 只认识两个名字。我们改变了目标从 ID 到标签。这是来宾名单和员工徽章的区别:任何戴着徽章的人都进来。
这个改变是一个 API 更新,保留了 load balancer 的每个其他设置,在切换期间有 0 错误。从那时起,任何池创建的 node 都带着标签,自行加入 load balancer,并落在同一个基于标签的防火墙和数据库访问规则下。HTTPS 端到端运行:load balancer 重新加密到私有网络上的每个后端。每个 node 从一个黄金快照启动。
| 前端池 | 后端池 | |
|---|---|---|
| Node | 2 vCPU / 4 GB 共享 AMD,约 $28/月 | 4 vCPU / 8 GB,约 $56/月 |
| 最小 / 最大 | 2 / 10 | 2 / 10 |
| Scale-out 规则 | 70% 平均 CPU,冷却 5 分钟 | 70% 平均 CPU(后来 55%),冷却 5 分钟 |
| 健康检查 | /lb-health 在 Next.js 应答时通过 | /lb-health 仅由 nginx 应答 |
web node 已经是无状态的,我们再次在相信池之前检查了。会话、缓存和队列存在托管 Valkey,日志去往标准错误,上传,包括学生的书面答案 PDF,去到 Spaces 对象存储。在 10 月 7 日我们验证了它:后端 node 在 24 小时内向磁盘写 0 文件。那就是为什么任何 node 可以在任何时刻被删除。
每个后端 node 仅运行 web(nginx)和 app(php-fpm)。队列、调度器、WebSocket 服务器和 OMR 在 web node 上关闭,所以额外服务器永远不会运行一个 job 两次。它们都存活在两个池外的单个固定 worker 上,一个已知的单点故障,第 4 部分回到它。
在后端,/lb-health 仅由 nginx 应答,从不启动框架,所以健康检查不能被 PHP 或数据库减速。它也给了我们一个排水开关。要把一个 node 从服务中取出,我们让 /lb-health 返回 503。load balancer 在大约 30 秒内停止向它发送新请求,已在运行的请求正常结束。
在 10 月 5 日,在 21:17 到 21:33 之间,我们测试了 autoscaling 本身。k6 重放了真实考试峰值请求混合,仅 GET 请求所以没什么被写:300 到 450 requests/s,然后约 750 requests/s 6 分钟。
| 我们看的东西 | 结果 |
|---|---|
| 后端池 | 2 到 3 个 node,在 21:29 当池平均达到 75% |
| 前端池 | 2 到 3 个 node,在 21:30,在 71% |
| 错误和超时 | 0 服务器错误,0 超时;新 node 自行加入 load balancer |
| 总体 p95 | 132 到 153 ms |
| API p95 | 320 到 705 ms,而后端坐在接近 90% 在第三个 node 加入之前 |
| HTTP 429 | 约 4%:所有负载来自一个测试 IP,所以按 IP 限制做了它们的工作 |
看看 API p95,320 到 705 ms。那是等待的价格。后端坐在接近 90% CPU 直到第三个 node 加入,因为 DigitalOcean 的 CPU 指标落后真实负载 5 到 8 分钟。在早期的后端测试中,第一个额外 node 在 CPU 触及 99% 后大约 7 分钟出现。autoscaler 没坏。它在读旧新闻。
滞后也以另一种方向工作。当负载停止时,指标仍看起来高,后端简短扩展到 4 个 node。这是一个淋浴水龙头:你更进一步转向它因为还没什么改变,然后热水突然到达。

通用论证在 流量峰值自动扩展 中;这里它有我们自己的数字在后面。对于定时考试,我们在提前约 45 分钟提高最小值,并仅在之后降低它。autoscaling 保持打开作为计划外的安全网。测试状态:Tested。
池 node 是可丢弃的,所以没人手工编辑一个:它在下一次缩减时消失,下一个新 node 会从快照启动,不是从编辑。我们的流程:
我们保留最后 3 个好镜像用于回滚,并永不在考试小时内回滚一个模板。我们在每个池更新时传递每个设置,所以没什么无声地重置。账户的 25 个 droplet 限制也必须在两个池能在 rollout 期间到达它们的最大时被提高,当旧和新 node 同时存在。
第一个后端 rollout 是一个纯粹的模板改变,它产生了一个短 503 错误的爆发。DigitalOcean 删除了旧 droplet,而 load balancer 仍在把请求路由给它们。这就像在主人仍在那里招待客人时关闭旧餐厅。
修复是停止依靠提供商的事件顺序,并作为受保护的序列运行 rollout。图 2 显示了它。

/lb-health。/lb-health 返回 503。之后两个池的完整 rollout,在 10 月 7 日,在真实页面上每 2 秒运行一个正常运行时间探针。结果:219 个探针和 0 个错误。状态:Tested,然后 fixed。
内存是前端上的安静风险。这些 node 没有交换,每个容器可能使用直到 4 GB node 的 1.5 GB。在 10 月 7 日我们添加了一个 2 GB 交换文件,swappiness 10,所以内核偏好 RAM,并将其烤进前端镜像。后端 node 已经有 4 GB 交换。同一组改变覆盖了每个 node 上的加强。
| 区域 | 就位了什么 |
|---|---|
| 访问 | 所有 node 上的仅密钥 SSH 和 Fail2Ban |
| 网络 | 按标签的云防火墙:前端 node 仅从它们的 load balancer 接受端口 80;后端 node 仅接受 80 和 443 |
| 进程 | 容器作为非 root 用户运行它们的请求服务进程 |
| 秘密 | 秘密 env 文件,模式 600 |
| 数据库 | 应用用户仅有 SELECT、INSERT、UPDATE 和 DELETE,没有 DDL |
防火墙跟随标签,所以池在午夜中间创建的一个 node 得到它的规则在它存在的时刻。没人需要记住什么。状态:Implemented 在 10 月 7 日。
一个更多规则,在一个真实考试晚上学到并在之后的部分告诉:缩减不会自行排水。
| 片段 | 状态 |
|---|---|
| 两个 load balancer、两个 autoscale 池、黄金镜像部署、交换和加强 | Implemented |
| 约 750 requests/s 的 autoscale 测试;第一个 rollout 和它的 503 闪烁 | Tested,然后 fixed |
| 一个两个层的 load balancer | Rejected |
| 几秒内的温 standby;Kubernetes(DOKS) | Considered;DOKS 以后 |
| 静态资源从 CDN;前端 node 上的 Fluent Bit,其 nginx 日志与 node 消失 | Planned |
应用层现在能增长并被替换而没人注意。下一个问题是学生答案是否存活:数据库。在 第 3 部分 我们在计划的 26 分钟窗口移动 MySQL,我们见到了保持写到错误数据库的遗留管理面板。
Series
从单服务器到考试日就绪About the Author
Anichur Rahaman
Continue Reading