在这篇文章里,我想要带您了解我们优化我们基于Node.js构建的高流量GraphQL前端API——Aurora的过程。运行在Google Kubernetes Engine上,我们通过改进资源利用率和代码效率,成功将Pod数量减少了超过30%,而没有增加延迟。
我会分享哪些有效、哪些无效,以及背后的原因。无论你是否面临类似的问题,或者只是对真实世界的Node.js优化感兴趣,你都能在这里找到可以应用到你自己的项目上的实用见解。
标题:最初的问题:闲置资源在高峰期,Aurora 每秒大约处理 1,000 个请求(RPS),运行着大约 100 个 pod。这意味着每个 pod 每秒大约处理 10 个请求。使用 node-inspector 对单个 Aurora pod 的 CPU 使用情况进行分析显示,CPU 有 70% 的时间是空闲的。
由于 Node.js 是在单一主线程上运行,并且一次只能执行一个任务——无论是处理请求还是执行后台任务——高闲置时间表明该 pod 并未充分利用其可用的 CPU 资源,这说明。请参见下图,以简化的方式了解 Node.js 事件循环(参考文章《在 Node.js 中运行 CPU 密集型任务》)。
这引发了一个重要的问题:这些 pod 是否能在不增加延迟的情况下每秒处理更多的请求?如果是这样的话,我们就可以减少 pod 的数量,节省计算资源。我们决定来验证一下这个想法。
我们是如何提升性能的我们从两个方面来提升性能:
- 外部: 我们将Node.js应用程序视为黑盒,通过优化系统级参数而不改变代码来优化性能。我们调整了Horizontal Pod Autoscaler (HPA)的目标,以减少pod的数量并增加每个pod的每秒请求数(RPS)。我们还尝试调整了V8引擎的内存设置,使用NODEARGS (V8是Node.js内置的JavaScript引擎)_。虽然这减少了pod的数量,但它导致了应用程序延迟的增加。
- 内部: 我们深入内部,解决了性能瓶颈。这使我们能够将延迟恢复至历史水平,但运行的pod数量更少了。
让我们来仔细看看每一种方法。
内存管理:调整 HPA(Horizontal Pod Autoscaler)和 Node.js 内存堆 设置 调整HPA的目标我们从调整 Aurora 的水平 pod 自动伸缩器(HPA)的 CPU 目标和 pod 请求的 CPU 开始。这样做是为了减少运行中的 pod 数量,因为我们使用的 HPA 是根据当前的 CPU 利用率来确定 pod 数量的。为了谨慎起见,我们每次调整的增量为 100 毫核。这使得 pod 的数量大约减少了 30%。我们注意到 Aurora 的 p50 延迟增加了 20%,这确实出乎意料且令人不快,然而,p95 和 p99 的延迟保持稳定。
调整了HPA的目标之后,我们观察到运行的Pod(容器组)数量减少了,但P50延迟变高了,因此我们开始寻找其他方法来降低延迟,而不再增加Pod的数量。
调整 Node.js 内存设置我们的下一步是专注于优化垃圾收集的过程,这也会影响应用程序的性能。我们假定,由于Aurora有多个内存缓存,我们可以通过调整旧堆和新堆大小的阈值来反映出架构的布局,从而更高效地利用内存。
V8 的内存使用显示了老代和新生代的稳定状态,下降部分反映了垃圾收集时的情况。
通过使用 node-gcstats,我们发现老年代的使用从未超过 600 MiB,但我们设置的最大老年代空间为 1536 MiB。基于这一发现,我们打算减少老年代空间并增加年轻代空间,基于以下假设:
- 鉴于先前的内存使用情况未超过600 MiB,我们没有预期到这次变更会对垃圾回收(GC)或整体性能产生负面影响。
- 预计增大新空间会对GC和性能产生积极影响。通过增大新空间,需要从新空间移至旧空间的对象会减少,从而减少GC运行的次数。
我们决定逐步地实施这些更改,从只减少旧空间大小开始,以便观察每个变化的影响。
生产测试中的坑我们部署了一个变更,将max-old-space-size
从1536 MB减少到600 MB,这首先在测试环境中应用,没有观察到任何影响。然而,当该更改发布到生产环境时,我们发现延迟显著上升。老年代空间使用量降至300 MB,年轻代空间使用量翻了一倍,导致垃圾回收器行为变得不稳定。
1点到2点,这段时间的系统指标,显示了V8内存使用量、垃圾收集的高峰以及Aurora的延迟变长等方面。
有趣的是,这个问题被我们金丝雀分析遗漏了。我们有一条错误配置的规则,将高于某个阈值的峰值视为噪声而非真正的增加,从而未能被及时发现。更糟糕的是,由于回滚流程存在问题,我们无法迅速回滚。总共花费了30分钟恢复。这句话恰如其分。
从错误中吸取教训:受控测试的重要性这件事凸显了在 staging 环境中模拟真实生产中的负载的重要性,以确保生产环境中能得到可预测的结果。为应对这一挑战,我们开始做负载测试,在 staging 环境中运行与目标每秒请求数(RPS)相匹配的测试,从而能够可靠地评估更改,而并不会影响到生产环境的稳定性。
代码优化之道:揭开黑盒的面纱我们也检查了代码效率的潜在问题。我们发现,在每个请求中,我们传递了一个大约42KiB的实验数据对象,这导致事件循环的主要线程阻塞。为了解决这个问题,我们更新了逻辑,在每10分钟从下游获取该对象后进行过滤,然后再将过滤后的对象传递给下游库。这大大减少了对象的大小和相关的CPU负载,将延迟恢复到原来的水平,同时运行的pod数量也减少了。
性能分析发现 Node.js 事件循环的关键瓶颈,尤其是在高流量时,__wbindgen_json_serialize
函数消耗了大量的 CPU 资源,影响了整体性能表现。
我们主要学到的是如何减少pod数量而不影响性能。在进行了与JSON相关的变化的第二天,晚上的性能测试中的pod数量达到了顶峰123,而一个月前是169。这表明pod使用量减少了27%。因此,我们可以看到pod的使用量减少了27%。
值得注意的是,尽管负载增加,每秒请求数(RPS)在夜间负载测试中达到了2.42K,相比之前的1.63K,请求量却有所减少。在一天中的其他时间,Pod数量的差异更加明显。平均Pod数量只有50,相比一个月前同一时间的122,减少了59%。尤其是在22:00 BST的峰值时段,这种减少尤为明显。
比较当前和过去配置中的Pod的数量和每秒请求数(RPS),显示出Pod数量的减少趋势,同时保持甚至更高的请求数量。
展望未来,我们不断进步虽然我们已经取得了一些进步,但我们还觉得还有很多可以改进的地方。我们还在继续寻找优化Aurora性能的方法,包括进一步优化我们的内存管理设置,并通过找出并解决代码中的低效问题。
最后的总结我们的旅程强调了在高流量环境下进行性能相关更改时的谨慎和受控测试的重要性。它还强调了从不同角度进行优化的价值,包括系统配置和代码效率的不同方面。
我们非常激动地继续学习和提升 Aurora 的性能,通过分享经验,我们希望为 Node.js 应用程序优化的讨论做出贡献,并鼓励大家也分享各自的优化经验。
如果你对我们做的事情感兴趣,并且觉得自己能帮助我们这段旅程,可以考虑申请加入loveholidays的行列,<strong>https://careers.loveholidays.com/</strong>