通过在 NestJS 中引入拦截器将响应时间提高 10 倍
通过在 NestJS 中引入拦截器将响应时间提高 10 倍
编写可扩展且快速的 API 可能是一项挑战,今天我想向您介绍我在其中实现的一种技术 巢穴 (Node.js) 必须处理大量传入请求的应用程序。
此方法用于特定用例——当您的 API 在客户端请求期间执行一些非业务逻辑时。我们将审查卸载该工作的方法,并将改进整体响应时间和您的 API 可以处理的每分钟请求数。
示例用例:
- 日志记录
- 指标收集
- 后台作业队列
- 客户端将通过 WebSocket 接收响应
我有一个 准备运行一些基准测试的演示应用程序 .这是一个带有可变条件的简单 API 路由。
我的例子是围绕 Redis 数据库 和 公牛队列 模块。尽管此技术的其他应用程序是可能的,但取决于您选择的服务:Redis Streams、SNS、SQS、RabbitMQ、Kafka 或任何常规数据库。
问题
Node.js 是后端 JavaScript 代码的单线程运行时。
你总是会听到这样的建议:“不要阻塞事件循环”。
这意味着什么?
让我们看一个简单的例子,一个请求进来,它到达你的控制器,你做一些工作,然后返回一个响应。
A basic API request-response workflow
这个例子中的一切都是同步执行的,所以此时事件循环被阻塞了。访问您的 API 的请求越多 - 每个新客户端等待响应的时间越长(假设您的资源有限)。
这张图中突出的一件事是:客户不关心我们使用我们所做的任何背景工作。 日志 API
,那么他为什么要等待它完成呢?如果 日志 API
是对 3rd 方服务的调用——然后它增加了另一个可能影响您的客户的故障点。
解决方案 1 — 将日志处理卸载到队列
我们知道 Redis 很快,所以排队作业也很快,对吧?
让我们引入一个简单的队列,并在后台发送这些日志。
A workflow where the client doesn’t have to wait for Logs API to finish
Redis 以非常快的内存存储而闻名,大多数时候将作业插入 Redis DB 可以在 1 毫秒内完成。
与数据库相比 插入
, Redis 的写入速度要快几倍。仅此更改就会为您带来很多性能。但是我们知道这个操作也阻塞了事件循环,它非常快,但仍然阻塞。
这种控制器的一个例子:
解决方案 2 — 将日志处理卸载到拦截器
拦截器
是后端开发人员也称之为 中间件
.
什么是中间件?
它是位于请求和响应之间的一段代码。从这个意义上说,我们的路由处理程序也是中间件,但 NestJS 使用了稍微不同的命名—— 拦截器
.
您可以在请求到达您的控制器之前执行自定义代码,而且很棒的是,中间件可以在响应发送后执行。
高级思想是——拦截器获取请求信息并仅在发送响应时执行阻塞操作。这样一来,传入的请求就不会因 Node.js 进程繁忙而受到限制,并且应用程序可以在后台进行异步处理(与 i/o 相关)。
A workflow where the client doesn’t have to wait for Logs API and queue to finish
当然,这会增加通过另一个代码块传递请求的开销,但在大多数情况下,这会进一步提高应用程序的可伸缩性。
所以让我们看看一些数字。
测试工具和场景
自动炮 — 一个加载测试 API 的好工具
雷迪斯 — 一个内存数据库,主要用于缓存、队列和发布/订阅,但它还有很多其他很酷的模块,可以作为应用程序的全栈数据库。
我将在 4 个不同的场景中运行这两种解决方案:
- 模拟作业添加延迟
5ms
和设置超时
- 模拟作业添加延迟
25毫秒
和设置超时
- localhost Redis 队列(零网络延迟)
- 远程 Redis 队列(一些网络延迟)
并具有 3 个不同的并发级别:
- 每秒 x10 个请求
- 每秒 x50 个请求
- 每秒 x100 个请求
基准图例:
转速
— 每分钟 API 处理请求数(更高 — 更好)潜伏
— API 路由的响应时间(更低——更好)
测试套件——添加作业延迟 5 毫秒
Test suite results with 5ms synthetic delay. RPM — higher is better. Latency — lower is better.
似乎这里没有明显的赢家,但使用 Interceptor 的路线执行 x10 并发性能提高 8 倍(延迟)和 5 倍(RPM) .其他并发测试——我的单节点进程资源受到限制,因此两种方法的结果非常相似(阻塞进程在非常高的并发性上领先一点)。
想象一下分布式/负载平衡部署(或集群节点应用程序)。每个进程都将在 x10 结果区域的某个地方起作用,因此在我看来,Interceptor 解决方案是一个明显的赢家。
测试套件——添加作业延迟 25 毫秒
Test suite results with 25ms synthetic delay. RPM — higher is better. Latency — lower is better.
随着我们的 3rd 方服务响应时间的增长,阻塞解决方案遇到了越来越多的问题。
通知 RPM 已下降 x2-x3
对比 5ms
测试运行。延迟也增加到 x2
最低限度。
拦截器解决方案的数字看起来相同 - 太棒了!无论我们的 3rd 方服务响应多长时间——我们的 API 仍然很快。
越来越真实
现在让我们做一些接近真实世界的测试。我们知道数据库插入速度很慢,因此我希望此类测试的结果具有相似的量级。
但是我想检查一下Redis,它通常非常快,可以在1ms以下插入数据。
我的设置: 巢穴 , 公牛 队列管理器,异步任务将作业插入 Redis 队列。
测试套件——Redis 队列、本地主机
Test suite results with Redis localhost. RPM — higher is better. Latency — lower is better.
令人惊讶的是,使用 Redis localhost 似乎没有太大区别,在控制器中阻塞作业队列甚至更快。
现在请记住,我们正在使用 0 的网络延迟进行测试,以便在这种情况下发挥作用。
测试套件——Redis 远程, 我是我
在真正的产品部署中,您可能会使用 Redis 的托管版本,并且它不会与您的应用程序位于同一主机上,因此网络延迟很重要。最好的情况是同一个区域/VPC。
我使用了一个 8 美元的集群 重做实验室 .
Test suite results with Redis remote. RPM — higher is better. Latency — lower is better.
同样,这些结果证实了我最初的合成延迟运行。非阻塞工作流程,即使是非常高速的插入,也比阻塞好得多。
现在网络延迟成为最大的限制因素,注意阻塞版本的 延迟时间 几乎增加到 100毫秒 在所有测试用例中。
这里最引人注目的一点是低 x10 并发。带拦截器的路由 响应速度快 x50 并且可以处理 x40 更多请求 .
您会注意到,控制器的拦截器版本的 RPM 在所有测试套件中都是相似的——那是因为它刚刚达到了我的 Node.js (1 CPU) 资源限制。因此,在使用应用程序的集群版本时,还有更多改进的潜力。
结论
我们已经看到了改进 延迟范围从 8 倍到 50 倍 .为了 RPM 它在 6x 到 40x 的范围内 .
保守一点,可以肯定地说,将 Interceptor(中间件)用于非任务关键型工作负载(例如日志记录、指标和后台作业)至少可以提高 10 倍的性能。
如果您正在处理一个中型或大型项目,最好检查您的一些控制器以了解这些内容并节省处理资源。
对于 RPM 较低的小型项目,我不会打扰,但请记住这个解决方案以备将来使用。
带有测试代码的存储库在这里: https://github.com/dkhorev/nestjs-interceptor-benchmark-demo
基准记录: https://docs.google.com/spreadsheets/d/1oGZdzZ6qbJRvCNqaiw5jtvR2uQOSE5ZgAfmiIW5IzTE/edit?usp=sharing
希望这会有所帮助。祝你好运,工程快乐!
An interceptor 😃
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明