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

第 2 部分:两个 Load Balancer、两个池,为什么添加服务器还不够

如何重建考试平台的应用层:两个 load balancer、两个 autoscale 池、每个 node 两个容器的 Next.js 前端、基于标签的后端、落后 5 到 8 分钟的 CPU 指标,以及有 219 个探针和 0 个错误的 rollout。

Author

Anichur Rahaman

5 天前13 min read5 views
第 2 部分:两个 Load Balancer、两个池,为什么添加服务器还不够

我们将 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,不是一个

我们的第一个草图使用单个 load balancer 来处理一切。一个比两个便宜,照看的东西也更少。状态:被考虑,然后被拒绝。

一个 DigitalOcean load balancer 无法按主机名或 URL 路径路由,并且每个都恰好针对一个标签,这意味着一组服务器。前端和后端 node 是不同的组,有不同的尺寸、健康检查和缩放规则。这是一个接待处,给每个访问者同一个房间列表。

DNS 已经将网站和 API 分成了单独的域,所以分离是免费的:每个层一个 load balancer,每个都在自己的池前面。状态:Implemented。两个加在一起成本 $48/月。

没有温池,所以我们把厨师留在厨房里

早期我们写下了一个吸引人的要求:一个能在 3 到 4 秒内接管流量的温 standby。它不存在这里。DigitalOcean autoscale 池没有温池,一个新 droplet 需要几分钟才能被创建、启动、检查并由 load balancer 接纳。一个在门口等待的出租车和你必须打电话的出租车是不同的产品。

所以我们做了两件无聊的事。

  • 已经服务的备用容量。两个池都有最小 2 个 node,并且两个都在 load balancer 内部。丢失一个会让另一个已经取流量。
  • Pre-scaling。在一个大考试前大约 45 分钟,我们提高池最小值,所以额外 node 被启动、检查并服务于第一个学生点击 "开始" 之前。一个额外的后端 node 成本约 $0.08/小时。

我们也考虑了 Kubernetes(DOKS)用于秒级缩放。这意味着运行更多,所以我们把它留给以后。状态:被考虑,没实现。

前端:八个 CPU 和一个收银员

旧的前端是一个 8 vCPU / 16 GB droplet 运行一个 Next.js 容器,TLS 由 droplet 上的 certbot 和没有 load balancer。如果它死了,网站就下线了。它也安静地浪费了钱:Node.js 进程大约在一个核心上渲染,所以大部分这八个 CPU 什么都做不了。一个超市有八个收银台但只有一个收银员。

新设计运行几个小的、相同的 node,而不是一个大的。图 1 显示了整个应用层;我们从左边走过。

架构图:学生到达前端 load balancer 和前端 autoscale 池,其 node 每个运行 nginx 和两个 Next.js 容器;API 调用去到后端 load balancer 和后端池,其 node 运行 nginx 和 php-fpm;托管数据服务和一个固定 worker 坐在池外
两个层,每个都有自己的 load balancer 和池。worker 是在两个池外的唯一固定服务器。

一个前端 node

每个 node 运行 nginx 在两个相同的 Next.js 容器前,一个每 vCPU。重要的设置:

  • 容器端口仅绑定到本地主机,所以只有 nginx 能到达容器。
  • nginx 用 least_conn 在两个容器间平衡:每个请求去到有最少活跃连接的容器。
  • 每个容器有 1.5 GB 内存上限,restart: always 和循环日志。
  • nginx 从 load balancer 获取真实客户端 IP,所以按学生的限制仍在工作,而不是看一个每个人的地址。
  • 一个微缓存服务静态文件、图片和主页而不唤醒 Next.js。
  • /lb-health 仅在 Next.js 应答时通过,所以一个有死应用的 node 离开轮转,即使 nginx 活着。

HTTPS 在前端 load balancer 结束,云防火墙让前端 node 仅从那个 load balancer 接受端口 80。

切换 DNS,有返回方式

我们在旧服务器旁边构建新前端,并通过我们自己机器上的 hosts 文件条目测试它,所以真实域对我们指向新 load balancer,对所有人都没有。我们对比了 12 个真实页面与旧服务器,降低了 DNS TTL 到 300 秒并切换了。

旧服务器保留用于回滚,因为指向 DNS 回来会是整个撤销。我们比预期需要它更久:一个小时后它仍在接收约 36% 来自缓存 DNS 的流量。TTL 是一个请求,不是一个命令。早期销毁服务器会把约三分之一的访问者发送到一个不再应答的地址。

负载测试和一个更便宜的 node

两个前端 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%

后端:从 droplet ID 到标签

之前,后端 load balancer 按 ID 指向两个 droplet。第三个服务器永远无法自行加入,因为 load balancer 只认识两个名字。我们改变了目标从 ID 到标签。这是来宾名单和员工徽章的区别:任何戴着徽章的人都进来。

这个改变是一个 API 更新,保留了 load balancer 的每个其他设置,在切换期间有 0 错误。从那时起,任何池创建的 node 都带着标签,自行加入 load balancer,并落在同一个基于标签的防火墙和数据库访问规则下。HTTPS 端到端运行:load balancer 重新加密到私有网络上的每个后端。每个 node 从一个黄金快照启动。

前端池后端池
Node2 vCPU / 4 GB 共享 AMD,约 $28/月4 vCPU / 8 GB,约 $56/月
最小 / 最大2 / 102 / 10
Scale-out 规则70% 平均 CPU,冷却 5 分钟70% 平均 CPU(后来 55%),冷却 5 分钟
健康检查/lb-health 在 Next.js 应答时通过/lb-health 仅由 nginx 应答

无状态 node,和那个不是的服务器

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 秒内停止向它发送新请求,已在运行的请求正常结束。

autoscale 测试和五分钟的谎言

在 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
总体 p95132 到 153 ms
API p95320 到 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。这是一个淋浴水龙头:你更进一步转向它因为还没什么改变,然后热水突然到达。

示意图:真实负载在 t0 上升,CPU 指标只在 5 到 8 分钟后追上,第三个 node 在指标穿过目标后加入,第四个 node 在负载已经停止后被添加
负载是一个台阶,指标是一条缓慢曲线,autoscaler 跟随曲线。这是效果的示意,不是录像。

通用论证在 流量峰值自动扩展 中;这里它有我们自己的数字在后面。对于定时考试,我们在提前约 45 分钟提高最小值,并仅在之后降低它。autoscaling 保持打开作为计划外的安全网。测试状态:Tested。

通过替换部署,永不编辑

池 node 是可丢弃的,所以没人手工编辑一个:它在下一次缩减时消失,下一个新 node 会从快照启动,不是从编辑。我们的流程:

  1. 改变一个 node 并测试它。
  2. 拍一个它的快照。
  3. 指向池模板到快照。
  4. DigitalOcean 创建新 node,然后删除旧的。

我们保留最后 3 个好镜像用于回滚,并永不在考试小时内回滚一个模板。我们在每个池更新时传递每个设置,所以没什么无声地重置。账户的 25 个 droplet 限制也必须在两个池能在 rollout 期间到达它们的最大时被提高,当旧和新 node 同时存在。

503 闪烁和受保护的 rollout

第一个后端 rollout 是一个纯粹的模板改变,它产生了一个短 503 错误的爆发。DigitalOcean 删除了旧 droplet,而 load balancer 仍在把请求路由给它们。这就像在主人仍在那里招待客人时关闭旧餐厅。

修复是停止依靠提供商的事件顺序,并作为受保护的序列运行 rollout。图 2 显示了它。

五个步骤的受保护 rollout 的时间线:仅改变镜像,等待新 node 通过健康检查,等待约 40 秒,首先排水旧 node,然后提供商删除它们;下面,路径显示旧 node 服务、排水和消失而新 node 启动、等待和服务
旧 node 在被删除之前被排水,所以 load balancer 永不发送一个请求到一个即将消失的 node。
  1. 仅在池模板上改变镜像。
  2. 等到每个新 node 应答 /lb-health。
  3. 等约 40 秒给 load balancer 接纳它们。
  4. 排水旧 node 首先:它们的 /lb-health 返回 503。
  5. DigitalOcean 在其冷却后移除旧 node,什么都不剩下来服务。

之后两个池的完整 rollout,在 10 月 7 日,在真实页面上每 2 秒运行一个正常运行时间探针。结果:219 个探针和 0 个错误。状态:Tested,然后 fixed。

在每个 node 上交换和加强

内存是前端上的安静风险。这些 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 日。

我们学到了什么

  • 添加服务器不是架构。限制器和 Valkey 需要代码改变和数据层修复;更多 node 只会复制问题。
  • 没有温池?自己构建热度。已经服务的备用容量,加上在一个大考试前约 45 分钟提高的最小值。
  • 为定时考试 pre-scale。CPU 指标落后真实负载 5 到 8 分钟,所以反应式 autoscaling 是安全网,不是计划。
  • 让 node 可丢弃。无状态 web node、按标签而不是 ID、按镜像而不是编辑替换。
  • 在你删除之前排水。受保护的 rollout 是一个过程,不是希望:219 个探针,0 个错误。
  • 保持旧东西直到它的流量离开。TTL 是一个请求;一个小时后旧服务器仍有 36%。

一个更多规则,在一个真实考试晚上学到并在之后的部分告诉:缩减不会自行排水。

每件事情的立场

片段状态
两个 load balancer、两个 autoscale 池、黄金镜像部署、交换和加强Implemented
约 750 requests/s 的 autoscale 测试;第一个 rollout 和它的 503 闪烁Tested,然后 fixed
一个两个层的 load balancerRejected
几秒内的温 standby;Kubernetes(DOKS)Considered;DOKS 以后
静态资源从 CDN;前端 node 上的 Fluent Bit,其 nginx 日志与 node 消失Planned

应用层现在能增长并被替换而没人注意。下一个问题是学生答案是否存活:数据库。在 第 3 部分 我们在计划的 26 分钟窗口移动 MySQL,我们见到了保持写到错误数据库的遗留管理面板。

About the Author

Anichur Rahaman

Continue Reading