软件架构第 5 部分:测量、改变、再测量:负载测试、真实考试晚上和最终架构
七个测量、一个真实考试晚上和一个 scale-in 错误:负载测试、容量模型和诚实 soak 计划如何塑造 NovaCommerce 最终架构、成本以及解决方案架构师清单。第 5 部分结束系列。
NovaCommerce 案例研究第 4 部分:2,854 个 Valkey 超时、一个仍是单点故障的 worker、延迟 merit 卡 115 分钟的队列、说不的 AI 提供商,以及计数建筑而不是学生的速率限制。
Author
Anichur Rahaman

Part 4 of 5
从单服务器到考试日就绪10 月 6 日 02:28 的一个后端负载测试回来了 2,854 个应用错误。每一个都是同一句子,在连接时抛出:RedisException: Operation timed out。Web 服务器很健康,数据库很健康。说不的部分是那个安静服务保有会话、缓存和队列。
同一周产生了第二个数字:115。那是最缓慢的 merit 卡在考试后等待重新计算的分钟数。Worker 的 CPU 不是原因,WebSocket 也不是。Job 就是站在错误的线。
第 3 部分 覆盖了数据库。这部分覆盖把无状态舰队绑在一起的片段:Valkey、唯一固定 worker、队列、速率限制、日志和我们看的信号。它们在图表上看起来枯燥,它们给了我们许多我们真实的惊喜。下面每个部分是一个故障、原因和改变。
这是五部分案例研究 "从单服务器到考试日就绪" 的第 4 部分。NovaCommerce 是虚构名称;架构、数字和错误都是真实的。
一个可以在任何分钟被删除的 node 无法拥有任何东西。在这个平台所有者是一个短列表:托管 MySQL 用于应用数据,托管 PostgreSQL 与 pgvector 用于 AI embedding,Spaces 对象存储用于文件,托管 OpenSearch 用于日志,和托管 Valkey 用于所有小、快和共享的:会话、缓存和队列。
想象超市收银员。收银台抽屉和库存清单坐在后台办公室。一个收银员可以在班中途回家,下一个拿了车道,因为没什么在收银员的口袋里。

我们检查了这个而不是假设它。在 10 月 7 日后端 node 在 24 小时内向本地磁盘写 0 文件。上传,包括学生的书面答案 PDF,直接去到 Spaces;会话和缓存在 Valkey;日志去标准输出。Implemented 和验证。那就是为什么 autoscale 池可能在任何时刻删除任何 node。
| 状态 | 它活在哪里 | 它买了我们什么 |
|---|---|---|
| 会话 | Valkey(primary + standby) | 任何后端 node 可以服务任何学生 |
| 缓存 | Valkey | 一个缓存被每个 node 共享 |
| 排队 job | Valkey | 提交如果 worker 下线等待安全 |
| 上传、答案 PDF、媒体 | Spaces 与 CDN | 没文件曾坐在 node 磁盘上 |
| 日志 | 标准输出,装运到 OpenSearch | 它们活得比 node 久(后端完成,前端计划) |
| 应用数据 | 托管 MySQL | 覆盖在第 3 部分 |
缓存、会话和队列共享一个托管 Valkey 8 与 standby node。它的驱逐策略是 noeviction:当内存满时,Valkey 用错误拒绝新写,而不是安静地删除旧密钥。对一个纯缓存听起来倒。但这实例也保有会话和排队 job,我们宁可一个大声错误而不是一个 job 消失不留痕迹。我们远离那条边:在 10 月 7 日考试晚上它使用约 12.5% 它的内存。
回到 02:28。测试是打击一个 4 GB Valkey 的 primary 和 standby。内存不是问题。在负载下它无法快速接受新连接,每个等待长于 5 秒 connect timeout 的请求变成一个 500。
压力的一部分是我们的。每个请求打开了一个新 TLS 连接到 Valkey,我们测量约 5.4 ms CPU 每个请求(大约 3.3 ms 给 MySQL)。在 65 到 70 requests/s 一个后端 node 可以服务,握手单独成本大约一个核心的三分之一。那是回纸板算术,不是简介。
修复是调整大小,在原地完成:Valkey 从 4 GB 去到 8 GB,两个 node(primary 加 standby),带数据保留和主机名不变。没应用改变,没 cutover。Implemented。
然后我们再测量。Tested:从 25 到 500 requests/s 的逐步爬升,开始两个后端 node 而池扩展出,给 0 应用错误和 0 后端 5xx。Load balancer 展示 143 错误,但仅在 node CPU 饱和时:预期的 "超出容量" 信号,不是 bug。
有用的课程是瓶颈在几天中移动三次,每个移动需要一个不同类型的修复。
| 顺序 | 瓶颈 | 问题的类型 | 修复 |
|---|---|---|---|
| 1 | API 速率限制器按 IP 计键 | 代码和配置 | 按账户计键(Implemented) |
| 2 | Valkey 连接 | 数据层尺寸 | 4 GB 到 8 GB,2 node(Implemented) |
| 3 | 后端 CPU 每 node | 纯容量 | 更多 node,pre-scaled(Implemented) |
仅最后一行通过添加服务器解决。对于第二个,更多服务器会打开甚至更多连接到同一 Valkey。
worker 周围的一切扩展。Worker 本身不扩展。它是一个固定 droplet(4 vCPU,8 GB)而且它进行四个 job:
这就是为什么队列、调度器、WebSocket 服务器和 OMR 在 web node 上关闭:一个新 autoscale node 必须永不运行一个 job 两次。我们案例添加给通用规则的是 SMS 被绑到地址,不仅是进程。
它也是单点故障,我们平白说。如果这个 droplet 死了,SMS、调度器和考试处理停止。学生保持提交,它们的提交在 Valkey 中等待安全,那就是为什么队列在那。但没什么处理它们直到 worker 回来。
在 10 月 7 日考试晚上 worker 达到 46% CPU 的 0 失败 job,所以这是一个我们计划的风险,不是我们看过的故障。计划,带诚实标签:
| 步骤 | 为什么 | 状态 |
|---|---|---|
| 保留 IP,与 SMS 网关列入白名单 | 一个替换 worker 从已知地址发送 SMS | Planned |
| 小 standby worker | 某物准备接管队列和调度器 | Planned |
| 仅考试的爆发 worker 镜像,大考试前开始 | 额外队列容量在需要时 | Planned |
| Worker 的一个快照 | 重建从镜像开始,不从内存 | Implemented |
保留 IP 首先来:没它,standby worker 会从网关不接纳的地址发送 SMS。
在考试后,一个 "merit 重新计算" job 刷新每个学生的 merit 卡。在 10 月 6 日这些卡开始出现 10 到 115 分钟迟。
明显的嫌疑是 worker 的 CPU 和 WebSocket 服务器。都不是原因。
Merit job 很小:0.34 秒。它共享一个队列,由 2 个 worker 服务,有一个 AI job 约每个学生做一个语言模型调用 10 秒。在一个考试后,约 2,000 的那些 AI job 被排队,每个 merit job 坐在它们后。
算术显示规模。一个 AI job 花和约 29 个 merit job 一样长。2,000 它们约 20,000 秒工作,跨 2 worker 那约三个小时的线。等待 10 到 115 分钟适配那张图。
它是超市的快速通道:一条面包不应该等在一个完整推车后,我们为每个人构建了一个收银台。

Implemented:一个单独队列通道及其自己容器,仅为 AI narrative,有 2 副本。Merit 和位置 job 永不等语言模型,merit 卡约一分钟准备。已经等待的 AI job 用一个原子脚本移到新通道,所以每个 job 在每一时刻正好在一个地方。
修复是拓扑,不是马力。工作类型一个队列的通用规则在 队列和 Worker 在规模;什么吓着我们是缓慢 job 看起来多无害。
新通道保护了 merit 卡。它没让 AI job 更快。通道撞到提供商自己的速率限制,约 7 到 10 呼叫一分钟,并一次提供商账户就用完了信用。
两个是同一故障:外部服务说 "不是现在"。一个重试循环打击它仅烧预算并填充失败 job 表,所以通道被重新设计被耐心。Implemented 和现场在 worker 上 10 月 6 日以来:
| 机制 | 设置 | 它做什么 |
|---|---|---|
| 共享速率限制 | 8 每分钟跨所有 worker | 保持呼叫在提供商允许里面 |
| 释放,不失败 | 指数退避,1 到 15 分钟,有抖动 | 一个被拒 job 回到队列;抖动阻止它们一起回来 |
| 重试窗口 | 直到 12 小时 | Job 保持尝试好久在冲之后 |
| 预算退款 | 月 AI 预算计数 | 一个被拒呼叫不被计费 |
| 回滚 | 模板文本 | 学生同时看到通用 narrative |
学生永不看错误。模板文本在他们打开页面时在那里,真实 narrative 在它的轮时替换它。
它被测试为真实在一天内。提供商账户用完了信用,约 3,800 narrative 堆积为延迟 job,没任何失败。下一天下午信用被添加,第一个新 narrative 在分钟内被写,没重启也没手工重放。
算术显示为什么窗口是小时:2,000 narrative 在 8 一分钟是约四个小时工作。几分钟的窗口会掉落大多数。
共享限制器重要。两个副本各自步调在 4 一分钟工作直到某人添加第三个。一个共享计数保持总数诚实然而多少 worker 存在。
一个速率限制器只和它计数的东西一样好。我们在两层得到这个错误,两次症状都是学生在考试中间接收 HTTP 429。
| 何时 | 什么被计数 | 什么出了问题 | 修复 |
|---|---|---|---|
| 9 月 25 日 | 全局 API 限制,按 IP 计键(基于令牌学生的空默认防卫) | 学校或移动运营商把数百学生放在一个 NAT IP 后:一个共享桶 | 按学生或讲师账户计键,IP 仅对客人(Implemented) |
| 10 月 7 日 | 路由限制比如 "30 每分钟",仍由客户 IP | 后端看一个前端代理 IP 给每个人:74% 呼叫到一个仪表板端点得 429 | 按学生每路由计,客人按 IP;之后 0 这样 429(Implemented) |
每个浏览器 API 呼叫去通过前端代理,所以一个按 IP 规则把整个考试大厅当作一个访问者。
两个修复来了与测试失败没修复;第二被部署一个 node 在一个时间零宕机。Planned:从前端到后端在一个签名头通过真实学生 IP,所以每个按 IP 规则和每个日志线是精确。
一个更小风险坐附近。后端仍信任旧前端服务器的 IP,那个我们删除的。如果云曾给那地址给某人别人,他们可能假学生 IP。我们在 10 月 7 日用排水一个 node 方法移除它,再一次零宕机。Implemented。信任列表是承诺列表;删除那些其所有者消失的。
有 autoscaling,你想检查的机器常常消失了。所以日志必须离开 node 当它们被写时。我们的容器仅写到标准输出和标准错误,Fluent Bit 装运后端容器日志到托管 OpenSearch,ELK 风格堆栈:一个可搜索地方被保持在 node 消失后。Implemented 对后端 node。通用方法在 高容量系统的可观测性。
它为自己付钱在容量工作。DigitalOcean 的二分钟平均把 10 月 7 日后端峰值在 63 requests/s;每分钟日志说约 69。平均隐藏峰值,容量模型需要峰值。
有间隙。前端 node 仍保持它们的 nginx 日志在本地磁盘,约 440 MB 一天,失去它们当 node 被移除。Planned:Fluent Bit 在前端 node 也一样。直到那时,前端是一个层其中缩减抹掉证据。
我们的 ops 控制台是只读的:它样品并永不改变任何东西。它记录 CPU 每 node 和每容器、池 CPU、MySQL 线程运行、连接和锁等待、Valkey 内存、客户端和每秒操作,队列健康。DigitalOcean 自己的监控添加每秒请求和在 load balancer 的响应类。
在考试期间我们不会凝视那所有。我们一次每分钟读一个概要:学生开始和提交,请求和 5xx 每 node,池 CPU,worker 负载和失败 job。少数字,每个绑到一个决定。

循环有小组移动集,在 第 2 部分 描述:
/lb-health 返回 503,load balancer 在大约 30 秒内停止发送新请求。在 10 月 7 日两个池的完整 rollout 中,一个正常运行时间探针在真实页面上每 2 秒:219 探针,0 错误。那是循环关闭:改变、看、测量。
在 10 月 7 日真实考试期间,某人降低了后端池最小。DigitalOcean 移除了一个服务 node 不排水,约 360 请求在 2 分钟内失败。
它是人为失滑和平台行为在一次:autoscale 缩减不排水。规则现在在 runbook 中(Implemented 作为 runbook 规则):在考试前提高最小,在之后仅降低。我们知道它的限制:一个 runbook 规则依赖于某人在考试中间记住它。它是防卫,不是锁。
在 第 5 部分,系列的最后,我们把它放在一起:10 月 7 日真实考试晚上与生产数字、容量模型、月成本,和我们仍欠自己的负载和 soak 测试。
Series
从单服务器到考试日就绪About the Author
Anichur Rahaman
Continue Reading