One self-hosted console to run your entire business — commerce, ERP, HRM, CRM & manufacturing

第 4 部分:Valkey、队列和日志,先坏掉的安静部分

NovaCommerce 案例研究第 4 部分:2,854 个 Valkey 超时、一个仍是单点故障的 worker、延迟 merit 卡 115 分钟的队列、说不的 AI 提供商,以及计数建筑而不是学生的速率限制。

Author

Anichur Rahaman

1 天前13 min read3 views
第 4 部分:Valkey、队列和日志,先坏掉的安静部分

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 用于所有小、快和共享的:会话、缓存和队列。

想象超市收银员。收银台抽屉和库存清单坐在后台办公室。一个收银员可以在班中途回家,下一个拿了车道,因为没什么在收银员的口袋里。

分层图:前端池、后端池和在私有网络线上固定 worker,和下面五个存储:Valkey、MySQL、PostgreSQL 与 pgvector、Spaces 和 OpenSearch
节点在上面拥有什么。所有必须在 node 被删除后活下来的东西在下面五个存储之一。

我们检查了这个而不是假设它。在 10 月 7 日后端 node 在 24 小时内向本地磁盘写 0 文件。上传,包括学生的书面答案 PDF,直接去到 Spaces;会话和缓存在 Valkey;日志去标准输出。Implemented 和验证。那就是为什么 autoscale 池可能在任何时刻删除任何 node。

状态它活在哪里它买了我们什么
会话Valkey(primary + standby)任何后端 node 可以服务任何学生
缓存Valkey一个缓存被每个 node 共享
排队 jobValkey提交如果 worker 下线等待安全
上传、答案 PDF、媒体Spaces 与 CDN没文件曾坐在 node 磁盘上
日志标准输出,装运到 OpenSearch它们活得比 node 久(后端完成,前端计划)
应用数据托管 MySQL覆盖在第 3 部分

一个 Valkey,特意

缓存、会话和队列共享一个托管 Valkey 8 与 standby node。它的驱逐策略是 noeviction:当内存满时,Valkey 用错误拒绝新写,而不是安静地删除旧密钥。对一个纯缓存听起来倒。但这实例也保有会话和排队 job,我们宁可一个大声错误而不是一个 job 消失不留痕迹。我们远离那条边:在 10 月 7 日考试晚上它使用约 12.5% 它的内存。

2,854 超时:当共享内存无法应答

回到 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。

有用的课程是瓶颈在几天中移动三次,每个移动需要一个不同类型的修复。

顺序瓶颈问题的类型修复
1API 速率限制器按 IP 计键代码和配置按账户计键(Implemented)
2Valkey 连接数据层尺寸4 GB 到 8 GB,2 node(Implemented)
3后端 CPU 每 node纯容量更多 node,pre-scaled(Implemented)

仅最后一行通过添加服务器解决。对于第二个,更多服务器会打开甚至更多连接到同一 Valkey。

一个 worker,和仅它能做的 job

worker 周围的一切扩展。Worker 本身不扩展。它是一个固定 droplet(4 vCPU,8 GB)而且它进行四个 job:

  • 队列。默认、考试、三个通知队列、OMR 和注册报告,由 Horizon 运行。
  • 调度器。定时 task 必须精确运行一次。
  • WebSocket。Reverb 服务实时更新。它的端口仅对后端 node 打开,按标签。
  • 所有 SMS。SMS 网关将单个 IP 地址列入白名单,所以 SMS 仅能从这台机器离开。

这就是为什么队列、调度器、WebSocket 服务器和 OMR 在 web node 上关闭:一个新 autoscale node 必须永不运行一个 job 两次。我们案例添加给通用规则的是 SMS 被绑到地址,不仅是进程。

它也是单点故障,我们平白说。如果这个 droplet 死了,SMS、调度器和考试处理停止。学生保持提交,它们的提交在 Valkey 中等待安全,那就是为什么队列在那。但没什么处理它们直到 worker 回来。

在 10 月 7 日考试晚上 worker 达到 46% CPU 的 0 失败 job,所以这是一个我们计划的风险,不是我们看过的故障。计划,带诚实标签:

步骤为什么状态
保留 IP,与 SMS 网关列入白名单一个替换 worker 从已知地址发送 SMSPlanned
小 standby worker某物准备接管队列和调度器Planned
仅考试的爆发 worker 镜像,大考试前开始额外队列容量在需要时Planned
Worker 的一个快照重建从镜像开始,不从内存Implemented

保留 IP 首先来:没它,standby worker 会从网关不接纳的地址发送 SMS。

等待 115 分钟的 merit 卡

在考试后,一个 "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 分钟适配那张图。

它是超市的快速通道:一条面包不应该等在一个完整推车后,我们为每个人构建了一个收银台。

前后图:一个共享队列其中小 merit job 等在约 2,000 个十秒 AI job 后,对比单独的通道其中 AI 通道由共享限制器 8 每分钟步调
同一 job,不同拓扑:在右 merit job 永不见 AI job,AI 通道有自己步调。

改变

Implemented:一个单独队列通道及其自己容器,仅为 AI narrative,有 2 副本。Merit 和位置 job 永不等语言模型,merit 卡约一分钟准备。已经等待的 AI job 用一个原子脚本移到新通道,所以每个 job 在每一时刻正好在一个地方。

修复是拓扑,不是马力。工作类型一个队列的通用规则在 队列和 Worker 在规模;什么吓着我们是缓慢 job 看起来多无害。

然后 AI 提供商说不

新通道保护了 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。信任列表是承诺列表;删除那些其所有者消失的。

活得比 node 久的日志

有 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。少数字,每个绑到一个决定。

左到右循环:来自 ops 控制台、load balancer 指标和 OpenSearch 日志的信号、一次每分钟的概要,然后行为比如排水一个 node、回滚镜像和 pre-scale,然后重新测量
信号供给概要,概要触发几个已知移动之一,每个事件回来作为 runbook 规则。

在信号上行动

循环有小组移动集,在 第 2 部分 描述:

  • 排水 node。让 /lb-health 返回 503,load balancer 在大约 30 秒内停止发送新请求。
  • 回滚镜像。我们保持最后三个好镜像,我们永不在考试小时内回滚一个模板。
  • Pre-scale。在一个大考试前提高池最小约 45 分钟。反应式缩放仅是安全网,因为 DigitalOcean 的 CPU 指标滞后真实负载 5 到 8 分钟。

在 10 月 7 日两个池的完整 rollout 中,一个正常运行时间探针在真实页面上每 2 秒:219 探针,0 错误。那是循环关闭:改变、看、测量。

缩减错误

在 10 月 7 日真实考试期间,某人降低了后端池最小。DigitalOcean 移除了一个服务 node 不排水,约 360 请求在 2 分钟内失败。

它是人为失滑和平台行为在一次:autoscale 缩减不排水。规则现在在 runbook 中(Implemented 作为 runbook 规则):在考试前提高最小,在之后仅降低。我们知道它的限制:一个 runbook 规则依赖于某人在考试中间记住它。它是防卫,不是锁。

我们学到了什么

  • 无状态 node 仅和在它们后面的共享存储一样安全。为连接调大 Valkey,不仅内存。
  • 一个队列每类工作。一个 0.34 秒 job 必须永不等在一个 10 秒后。
  • 当一个提供商限制你,步调呼叫、用抖动退避、重试数小时、退款预算和显示回滚。
  • 检查每个速率限制器计数什么。学生是单位,不是建筑也不是代理。
  • 装运日志从 node 当它们被写,关闭前端间隙在它花费事件前。
  • 写下你的单点故障有状态在每个旁边。我们的是一个 worker,有计划修复和没假装。
  • Autoscale 缩减不排水。在考试前提高最小。

在 第 5 部分,系列的最后,我们把它放在一起:10 月 7 日真实考试晚上与生产数字、容量模型、月成本,和我们仍欠自己的负载和 soak 测试。

About the Author

Anichur Rahaman

Continue Reading