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

2020 年 3 月 02 日

在优化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


2020 年 3 月 02 日 14:501853
用户头像
小智 InfoQ 主编

发布了 395 篇内容, 共 306.8 次阅读, 收获喜欢 1709 次。

关注

评论 1 条评论

发布
暂无评论
发现更多内容

你真的懂AQS吗?透彻理解AQS源码分析系列之AQS基础一

InfoQ_d2212957090d

AQS

Spring 5 中文解析核心篇-IoC容器之自定义Bean性质

青年IT男

Spring5 Spring Framework

Spring 5 中文解析核心篇-IoC容器之基于注解的容器配置

青年IT男

Spring5 Spring Framework

敏捷转型谁先动:老总,项目经理or团队

华为云开发者社区

项目管理 敏捷 敏捷开发 团队 华为云

Spring 5 中文解析核心篇-IoC容器之ApplicationContext与BeanFactory

青年IT男

Spring5 Spring Framework

Spring 5 中文解析核心篇-IoC容器之Spring AOP API

青年IT男

Spring5 Spring Framework

软件工程开发之道:了解能力和复杂度是前提

华为云开发者社区

软件工程 开发 项目 代码 复杂度

话题讨论 | 你的桌面上会放哪些有趣的小摆件?

InfoQ写作平台

工作 话题讨论 办公室

手把手带你写Node.JS版本小游戏

华为云开发者社区

Java node.js Node 华为云 剪刀石头布

手把手教程:基于环信4小时开发一个视频会议APP【附源码】

环信

Spring 5 中文解析核心篇-IoC容器之IoC容器和Bean概述

青年IT男

Spring5 Spring Framework

Spring 5 中文解析核心篇-IoC容器之依赖关系

青年IT男

Spring5 Spring Framework

日防夜防,“隐私窃贼“难防? 手把手教你如何使用OPPO手机管理应用权限

OPPO安全

安全 隐私保护 数据隐私 sdk

区块链支付系统开发,数字货币支付承兑商APP模式搭建

13530558032

Spring 5 中文解析核心篇-IoC容器之SpEL表达式

青年IT男

Spring5 Spring Framework

Spring 5 中文解析核心篇-IoC容器之AOP编程(上)

青年IT男

Spring5 Spring Framework

Spring 5 中文解析核心篇-IoC容器之AOP编程(下)

青年IT男

Spring5 Spring Framework

从RainbowBridge看Js与Java交互中的安全漏洞

OPPO安全

安全 安全漏洞

基于环信sdk在uni-app框架中快速开发一款多平台社交Demo

环信

区块链数字货币交易所开发,数字交易平台搭建

13530558032

Spring 5 中文解析核心篇-IoC容器之Bean作用域

青年IT男

Spring5 Spring Framework

Spring 5 中文解析核心篇-IoC容器之基于Java容器配置

青年IT男

Spring5 Spring Framework

Redis 持久化 --RDB

是老郭啊

redis redis持久化 aof rdb

CSS常用样式——绘制各种角度的三角形(1)

程序员学院

CSS css3 CSS小技巧

Spring 5 中文解析核心篇-IoC容器之BeanDefinition继承与容器拓展点

青年IT男

Spring5 Spring Framework

Spring 5 中文解析核心篇-IoC容器之JSR330标准注解

青年IT男

Spring5 Spring Framework

Spring 5 中文解析核心篇-IoC容器之Environment抽象

青年IT男

Spring5 Spring Framework

Spring 5 中文解析核心篇-IoC容器之类路径扫描和组件管理

青年IT男

Spring5 Spring Framework

车队管理软件

samhuang

企业表格技术与风险指标补录系统

Geek_Willie

SpreadJS

为什么企业自主开发软件时,都会使用统一的模块化框架式开发平台?

Philips

敏捷开发 代码优化 框架设计 框架开发 标准框架

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