在本文中,我将分享我在 Node.js 中跟踪和修复高内存使用率的方法。
最近我收到了一张标题为“修复库 x 中的内存泄漏问题”的票证。该描述包括一个 Datadog 仪表板,其中显示了十几个因内存使用率过高并最终因 OOM(内存不足)错误而崩溃的服务,并且它们都有共同的 x 库。
我最近才接触到代码库(不到 2 周),这使得这项任务充满挑战性,也值得分享。
我开始使用两条信息:
下面是链接到票证的仪表板:
服务在 Kubernetes 上运行,很明显,服务会随着时间的推移积累内存,直到达到内存限制、崩溃(回收内存)并重新启动。
在本节中,我将分享我如何处理手头的任务,找出高内存使用率的罪魁祸首并随后修复它。
由于我对代码库相当陌生,我首先想了解代码、相关库的作用以及它应该如何使用,希望通过这个过程可以更容易地识别问题。不幸的是,没有适当的文档,但通过阅读代码和搜索服务如何利用该库,我能够理解它的要点。它是一个围绕 redis 流的库,并为事件生成和消费提供方便的接口。花了一天半的时间阅读代码,由于代码结构和复杂性(很多我不熟悉的类继承和rxjs),我无法掌握所有细节以及数据如何流动。
因此我决定暂停阅读,并在观察代码运行情况并收集遥测数据的同时尝试发现问题。
由于没有可用的分析数据(例如连续分析)可以帮助我进一步调查,因此我决定在本地复制该问题并尝试捕获内存配置文件。
我发现了几种在 Node.js 中捕获内存配置文件的方法:
由于不知道去哪里寻找,我决定运行我认为是库中最“数据密集”的部分,即 redis 流生产者和消费者。我构建了两个简单的服务,它们可以生成和使用来自 redis 流的数据,然后我继续捕获内存配置文件并比较一段时间内的结果。不幸的是,在对服务产生负载并比较配置文件几个小时后,我无法发现这两个服务中任何一个服务的内存消耗有任何差异,一切看起来都很正常。该库公开了一系列不同的接口以及与 Redis 流交互的方式。我很清楚,复制该问题比我预期的要复杂得多,尤其是在我对实际服务的特定领域知识有限的情况下。
所以问题是,如何找到合适的时机和条件来捕获内存泄漏?
如前所述,捕获内存配置文件的最简单、最方便的方法是对受影响的实际服务进行连续分析,但我没有这个选项。我开始研究如何至少利用我们的暂存服务(它们面临同样的高内存消耗),这将使我无需额外的努力即可捕获所需的数据。
我开始寻找一种将 Chrome DevTools 连接到其中一个正在运行的 Pod 并随着时间的推移捕获堆快照的方法。我知道内存泄漏发生在暂存阶段,因此,如果我能够捕获该数据,我希望能够至少发现一些热点。令我惊讶的是,有一种方法可以做到这一点。
执行此操作的过程
kubectl exec -it <nodejs-pod-name> -- kill -SIGUSR1 <node-process-id>
更多关于 Signal Events 中的 Node.js 信号
如果成功,您应该会看到来自服务的日志:
Debugger listening on ws://127.0.0.1:9229/.... For help, see: https://nodejs.org/en/docs/inspector
kubectl port-forward <nodejs-pod-name> 9229
如果没有,请确保您的目标发现设置正确设置
现在您可以开始捕获超时快照(时间段取决于发生内存泄漏所需的时间)并进行比较。 Chrome DevTools 提供了一种非常方便的方法来做到这一点。
您可以在记录堆快照中找到有关内存快照和 Chrome 开发工具的更多信息
创建快照时,主线程中的所有其他工作都会停止。根据堆内容,甚至可能需要一分多钟的时间。快照内置在内存中,因此它可以使堆大小加倍,从而导致填满整个内存,然后使应用程序崩溃。
如果您要在生产中获取堆快照,请确保从中获取快照的进程可以崩溃,而不会影响应用程序的可用性。
来自 Node.js 文档
回到我的例子,选择两个快照进行比较并按增量排序,我得到了您在下面看到的内容。
我们可以看到最大的正增量发生在字符串构造函数上,这意味着该服务在两个快照之间创建了很多字符串,但它们仍在使用中。现在的问题是它们是在哪里创建的以及谁在引用它们。幸运的是,捕获的快照包含了这些信息,也称为 Retainers。
在深入研究快照和永不缩小的字符串列表时,我注意到一种类似于 id 的字符串模式。单击它们,我可以看到引用它们的链对象 - 又名保留器。这是一个名为 sendEvents 的数组,其类名是我可以从库代码中识别出来的。哎呀,我们找到了罪魁祸首,一个不断增长的 id 列表,到目前为止我认为这些列表从未被发布过。我超时拍摄了一堆快照,这是唯一一个不断重新出现为具有较大正增量的热点的地方。
有了这些信息,我不需要尝试完全理解代码,而是需要关注数组的用途、何时填充和何时清除。在一个地方,代码将项目推送到数组,而在另一个地方,代码将项目弹出,这缩小了修复的范围。
可以安全地假设数组在应该清空的时候没有被清空。跳过代码的细节,基本上发生的事情是这样的:
你能看出这是怎么回事吗? ?当服务仅使用该库来生成事件时,sentEvents 仍会填充所有事件,但没有代码路径(使用者)用于清除它。
我修补了代码以仅跟踪生产者、消费者模式上的事件并部署到登台。即使存在暂存负载,很明显该补丁也有助于减少高内存使用率,并且没有引入任何回归。
当补丁部署到生产环境中时,内存使用量大幅减少,服务的可靠性得到提高(不再出现 OOM)。
一个很好的副作用是处理相同流量所需的 Pod 数量减少了 50%。
对于我来说,这是一个很好的学习机会,可以跟踪 Node.js 中的内存问题并进一步熟悉可用的工具。
我认为最好不要详细讨论每个工具的细节,因为这值得单独发表一篇文章,但我希望这对于任何有兴趣了解更多有关此主题或面临类似问题的人来说是一个很好的起点。
以上是跟踪 Node.js 中的高内存使用率的详细内容。更多信息请关注PHP中文网其他相关文章!