通过在 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

A basic API request-response workflow

这个例子中的一切都是同步执行的,所以此时事件循环被阻塞了。访问您的 API 的请求越多 - 每个新客户端等待响应的时间越长(假设您的资源有限)。

这张图中突出的一件事是:客户不关心我们使用我们所做的任何背景工作。 日志 API ,那么他为什么要等待它完成呢?如果 日志 API 是对 3rd 方服务的调用——然后它增加了另一个可能影响您的客户的故障点。

解决方案 1 — 将日志处理卸载到队列

我们知道 Redis 很快,所以排队作业也很快,对吧?

让我们引入一个简单的队列,并在后台发送这些日志。

A workflow where client doesn’t have to wait for Logs API to finish

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

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 版权协议,转载请附上原文出处链接和本声明

本文链接:https://www.qanswer.top/36820/47061609

posted @ 2022-09-16 09:47  哈哈哈来了啊啊啊  阅读(658)  评论(0编辑  收藏  举报