在优化Node.js服务时学到的六课经验

在 Klarna,我们付出了很多努力来帮助我们的开发人员提供高质量和安全的服务。我们为开发人员提供的一项服务是一个运行 A/B 测试的平台。这个平台的一个关键组成部分是一组流程,用来针对每个传入的请求做出决定:将请求暴露给哪种测试(A 或 B)。这一流程进而确定了按钮用哪种颜色渲染、向用户显示哪种布局,甚至是要使用哪种第三方后端,等等。这些决定会直接影响用户体验。

这组流程中每一步的性能都是非常重要的,因为它会在 Klarna 生态系统的关键决策路径中同步使用。对这类流的一个典型要求是 99.9%的请求的处理延迟都要在个位数级别。为了确保我们能持续遵循这些要求,我们开发了一个性能测试管道来对该服务进行负载测试。

要点 1:性能测试可以为我们提供信心,让我们知道每个新版本的性能都不会打折扣。

尽管在过去两年时间里,我们在平台的生产环境下几乎没有遇到过任何性能问题,但这些测试确实揭示了一些问题。测试开始几分钟后,在中等且稳定的请求速率下,请求持续时间从正常范围激增至几秒钟的水平:

我们认为,虽然在生产环境中还没有发生这种情况,但现实负载“追上”这种模拟负载的压力也只是时间问题而已,因此这个问题是值得研究的。

要点 2:通过“增加”负载,我们可以在问题影响生产环境之前就将它们暴露出来。

要注意的另一件事是,问题大约需要两到三分钟才会出现。在第一个迭代中,我们仅运行了次两分钟测试。只有将测试持续时间延长到十分钟之后,我们才发现了这个问题。

要点 3:长时间的负载测试可以暴露出各种问题。如果一切正常,请尝试延长测试时间。

一般来说,我们使用以下指标来监控服务:每秒传入请求的数量、传入请求的持续时间以及错误率。这些指标可以很好地看出服务是否出现了问题。

但当服务出现异常时,这些指标不会提供任何见解。当问题浮现时,你需要知道瓶颈在哪里。为此,你需要监控 Node.js 运行时使用的资源。显而易见的指标是 CPU 和内存利用率,但是有时这些并不是实际的瓶颈所在。在我们的例子中,CPU 利用率很低,内存利用率也很低。

Node.js 使用的另一项资源是事件循环。就像我们需要知道进程正在使用多少兆字节的内存一样,我们也需要知道事件循环需要处理多少“任务”。事件循环是在名为“libuv”的 C++ 库中实现的。关于事件循环,这里有一个 Kenneth Gibson 的出色演讲

它为这些“任务”使用的术语叫做活动请求(Active Requests)。要追踪的另一个重要指标是活动句柄(Active Handles)的数量,也就是 Node.js 进程持有的打开文件句柄或套接字的数量。有关句柄类型的完整列表,请参见 libuv 文档

因此,如果测试正在使用 30 个连接,那么应该可以看到 30 个活动句柄。活动请求是这些句柄上待处理的操作数。哪些操作呢?完整列表可在 libuv 文档中找到,举例来说这些可以是读 / 写操作。

看一下服务报告的这些指标,就能发现有什么不对劲。尽管活动句柄的数量符合我们的预期(在此测试中为 30 个左右),但活动请求的数量实在太多了,竟然有数以万计:

不过我们仍然不知道队列中有哪些类型的请求。按照活动请求的类型细分后,图像就更加清晰了。在报告的指标中,有一种类型的请求非常突出:UV_GETADDRINFO。当 Node.js 尝试解析一个 DNS 名称时就会生成此类请求。

但是为什么会生成这么多 DNS 解析请求呢?原来我们正在使用的 StatsD 客户端尝试解析每个外发消息的主机名。公平地说,它确实提供了一种缓存 DNS 结果的选项,但是这一选项并不考虑该 DNS 记录的 TTL,而是无限期地缓存结果。因此,如果该记录在客户端已解析之后被更新,则客户端永远都不会知道。由于 StatsD 负载均衡器可能已使用其他 IP 重新部署,并且我们不能强制重启服务以更新 DNS 缓存,因此这种无限期缓存结果的方法对我们来说是不可行的。

我们想出的解决方案是在客户端外部添加适当的 DNS 缓存。给“DNS”模块打上猴子补丁并不难。结果要好得多:

要点 4:在考虑传出请求时不要忘记 DNS 解析。另外,不要忽略记录的 TTL——它真的会毁掉你的应用。

解决这个问题后,我们在服务中重新启用了更多功能并再次进行了测试。具体来说,我们启用了一个逻辑,该逻辑为每个传入请求生成一个到某个 Kafka 主题的消息。测试再次表明,在较长的一段时间内,响应时间(秒)出现了明显的峰值:

查看服务中的指标后发现了一个明显的问题,就是我们刚刚启用的功能——向 Kafka 生成消息的延迟非常高:

我们决定试着做一次小改进——将传出的消息排队在内存中,然后每秒分批刷新一次。再次运行测试,我们发现服务的响应时间有了明显的改善:

要点 5:批量 I/O 操作!即使在异步情况下,I/O 也很昂贵。

最后的说明:如果没有一种让运行结果可重复且一致的测试方法,那么上述测试也是做不到的。性能测试管道的第一个迭代的结果就是不一致的,所以也没法为我们提供信心。努力打造出合适的测试管道后,我们就能够尝试各种事物,实验补丁效果,最重要的是有信心证明我们正在寻求的数字不是偶然的产物。

要点 6:在尝试任何改进之前,你应该先做一次结果可以信赖的测试。

编辑:我收到了一些问题,询问在这里使用哪些工具来执行测试。我们在这里使用了下面这些工具:

负载由一个内部工具生成,其简化了以分布式模式运行 Locust 的过程。基本上,我们只需要运行一个命令,该工具就会启动负载生成器,为它们提供测试脚本,并将结果收集到 Grafana 的仪表板上,也就是上文中的黑色屏幕截图。这是测试中(客户端)的视角。

被测服务正在向 Datadog 报告指标,也就是上文中的白色屏幕截图。

原文链接

6 Lessons Learned From Optimizing The Performance of a Node.js Service

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/710AExebGkgEbtt5k4h2
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券