构建一个不会过载的 Node.JS 服务器 - Node.JS 假日季,第 5 部分

这是 Mozilla 的身份团队在 Node.JS 假日季系列 中的第 5 集,共 12 集。在本篇文章中,我们将再次讨论 Node.JS 应用程序的扩展。

如何在面对不可能的负载下,构建一个能够持续运行的 Node.JS 应用程序?

本篇文章介绍了一种技术和一个实现该技术的库,所有内容都被浓缩到以下五行代码中

var toobusy = require('toobusy');

app.use(function(req, res, next) {
  if (toobusy()) res.send(503, "I'm busy right now, sorry.");
  else next();
});

为什么还要费心?

如果您的应用程序对人们很重要,那么花点时间考虑一下灾难场景是值得的。这些是好的灾难,您的项目会成为社交媒体的宠儿,您会从每天一万个用户变成一百万个用户。经过一些准备,您就可以构建一个服务,即使在流量峰值超过其容量数量级的期间也能保持运行。如果您放弃了这些准备,那么您的服务会在最不恰当的时候变得完全不可用 - 当每个人都在观看的时候

另一个考虑合法流量峰值的好理由是恶意流量峰值:缓解 DoS 攻击 的第一步是构建不会过载的服务器。

服务器在负载下

为了说明没有考虑流量峰值的应用程序的行为方式,我 构建了一个应用程序服务器,它有一个 HTTP API,消耗 5ms 的处理器时间,分布在五个异步函数调用中。按照设计,该服务器的单个实例能够每秒处理 200 个请求。

这大致类似于一个典型的请求处理程序,它可能进行一些日志记录、与数据库交互、渲染模板并流出结果。以下是当我们线性增加连接尝试次数时,服务器延迟和 TCP 错误的图表

对这次运行数据的分析讲述了一个清晰的故事

该服务器没有响应:在最大容量的 6 倍(1200 个请求/秒)下,服务器变得笨拙,平均请求延迟为 40 秒

这些故障很糟糕:超过 80% 的 TCP 故障和高延迟,用户将在长达一分钟的等待后看到一个令人困惑的故障。

优雅地失败

接下来,我在 同一个应用程序中加入了本文开头提供的代码。这段代码使服务器能够检测到负载何时超过其容量,并主动拒绝请求。以下图表描述了该服务器在当我们线性增加连接尝试次数时的性能

Your server with limits

图表中没有描述的一件事是在这次运行中返回的 503(服务器太忙)响应的数量,它随着连接尝试次数的增加而稳定地增加。那么我们从该图表和基础数据中学到了什么呢?

主动限制增加了鲁棒性:在负载超过其容量数量级的条件下,应用程序仍然能够保持合理的行为。

成功和失败都很迅速:平均响应时间在大多数情况下保持在 10 秒以下。

这些故障并不糟糕:通过主动限制,我们有效地将缓慢的笨拙故障(TCP 超时)转换为快速的有意的故障(立即的 503 响应)。

需要说明的是,构建一个返回 HTTP 503 响应(“服务器太忙”)的服务器,要求您的接口向用户呈现一个合理的提示信息。通常这只是一项非常简单的任务,而且应该很熟悉,因为许多流行的网站都这样做。

如何使用它

node-toobusy 在 npmgithub 上可用。安装完成后 (npm install toobusy),只需引入它即可

var toobusy = require('toobusy');

在引入该库的时刻,它将开始主动监控进程,并确定进程何时“太忙”。然后,您可以在应用程序的关键点检查进程是否过载

// The absolute first piece of middle-ware we would register, to block requests
// before we spend any time on them.
app.use(function(req, res, next) {
  // check if we're toobusy() - note, this call is extremely fast, and returns
  // state that is cached at a fixed interval
  if (toobusy()) res.send(503, "I'm busy right now, sorry.");
  else next();
});

这种对 node-toobusy 的应用为您提供了基本的负载鲁棒性,您可以 调整 和自定义它以适合您的应用程序设计。

它是如何工作的

我们如何可靠地确定一个 Node 应用程序是否太忙?

事实证明,这比您想象的要有趣,尤其是在您考虑到 node-toobusy 试图开箱即用地为任何 Node 应用程序工作时。为了理解所采用的方法,让我们回顾一下一些不起作用的方法

查看当前进程的处理器使用率:我们可以使用像你在 top 中看到的那样的数字 - Node 进程在处理器上执行的时间百分比。一旦我们有了确定这一点的方法,我们就可以说使用率超过 90% 就是“太忙”。当机器上有多个进程正在消耗资源,而你的 Node 应用程序没有一个完整的单处理器可用时,这种方法就会失效。在这种情况下,您的应用程序永远不会被注册为“太忙”,并且会以上面解释的方式严重失败。

将系统负载与当前使用率结合起来:为了解决这个问题,我们可以检索当前的系统负载,并在我们的“太忙”判定中考虑这一点。我们可以获取系统负载,并考虑可用处理核心的数量,然后确定我们的 Node 应用程序有多少个处理器的百分比可用!这种方法很快就变得复杂起来,需要系统特定的扩展,并且没有考虑到诸如进程优先级之类的事情。

我们想要的是一个更简单的解决方案,它可以正常工作。该解决方案应该得出这样的结论:当 Node.js 进程无法及时地处理请求时,它就是太忙了 - 这是一个与服务器上运行的其他进程的细节无关的标准。

node-toobusy 采取的方法是测量事件循环延迟。请记住,Node.JS 的核心是一个事件循环。要完成的工作会排队,并且在每次循环迭代中都会被处理。当 Node.js 进程变得过载时,队列会增长,并且有更多要完成的工作,而不是能够完成的工作。可以通过确定完成一小部分工作通过事件队列需要多长时间来了解 Node.js 进程的过载程度。node-toobusy 库为 libuv 提供了一个回调,该回调应该每 500 毫秒调用一次。从调用之间实际经过的时间中减去 500ms,就可以得到一个简单的事件循环延迟的度量。

简而言之,node-toobusy 通过测量事件循环延迟来确定主机进程的繁忙程度,这是一种简单而强大的技术,无论主机上运行的其他进程是什么,它都能正常工作。

当前状态

node-toobusy 是一个非常新的库,它通过测量事件循环延迟来简化构建不会过载的服务器:试图解决确定 Node.js 应用程序是否太忙的通用问题。此处描述的所有测试服务器以及文章中使用的负载生成工具都在 github 上可用。

在 Mozilla,我们目前正在评估将这种方法应用于 Persona 服务,并希望随着我们学习而对其进行改进。我期待着您的反馈 - 可以在本文的评论区、身份邮件列表github 问题 中发表。

系列中的以前的文章

这是 关于 Node.js 的 12 篇文章系列 的第五部分。以前的文章是


33 条评论

  1. Simon

    不错!

    能够在两种情况下显示成功请求的数量会很棒。

    服务器是否使用这种“快速失败”方法成功回复了更多请求?我想不是,但它如何比较?

    2013 年 1 月 15 日 下午 11:40

    1. Lloyd Hilaiel

      好问题。事实上,这是目前实现的库的一个潜在弱点。以下是两种情况下的成功响应图表:http://cl.ly/image/3n0b252W3e2Z

      请注意,当启用`toobusy`并且流量超过容量时,与关闭时相比,只有大约 70% 的请求量能够成功处理。

      我认为,对决定何时以及阻止多少请求的算法进行一些调整可以改善这种情况:https://github.com/lloyd/node-toobusy/blob/master/toobusy.cc#L26-L31

      随时创建问题来跟踪此问题,我有很多想法想在那里分享。感谢您的评论!

      2013 年 1 月 16 日 上午 08:59

  2. Mark

    您如何在 node.js 应用程序中跟踪 TCP 错误?

    2013 年 1 月 15 日 下午 13:35

    1. Lloyd Hilaiel

      为了这些图表的目的,我在负载生成客户端中跟踪了它们:https://github.com/lloyd/node-toobusy/blob/master/examples/load.js#L55

      2013 年 1 月 16 日 上午 09:01

  3. Randall A. Gordon

    软件工程和用户体验的统一。我太喜欢了!我会将它牢记在心,用于未来的项目。

    2013 年 1 月 15 日 下午 15:06

  4. Don Park

    FYI,node.js 的 setTimeout 只是默认事件循环上 uv_timer 的包装器。

    2013 年 1 月 15 日 下午 18:46

    1. Lloyd Hilaiel

      这是一个有效的观察 - 隐含的问题是:用原生代码实现这个库真的有意义吗?

      当我迁移到原生代码进行实现时,我实际上希望解决客户端代码必须调用 `.shutdown()` 才能使它们的 node 应用程序优雅关闭的事实。我有一些想法,但还没有实现。

      在实现此功能之前,我的答案是,我不确定!

      感谢您的发布。

      2013 年 1 月 16 日 上午 09:06

  5. bryant chou

    很棒的库!我们一直在寻找类似的东西。在我的 jmeter 测试中,它似乎完全按照它所说的那样做。我将把它软发布到一个 EC2 盒子上,该盒子每秒接收约 1000 次请求,看看魔法数字是多少。

    2013 年 1 月 15 日 下午 19:10

    1. Lloyd Hilaiel

      Yo Bryant - 请报告并发布您的发现。我很想知道这对各种不同的应用程序有效。

      (附注:感谢您的代码贡献!https://github.com/lloyd/node-toobusy/pull/5)

      2013 年 1 月 16 日 下午 16:55

      1. bryant chou

        我们有一个 CPU/内存密集型 node webapp - 这就是我认为这种负载控制方法非常适合的原因。此外,这是一个巨大的业务安全保障,因为如果我们在一定时间后没有响应 API 请求,我们可能会对自己进行 DDoS 攻击(我们有数百万部手机访问我们的各种端点)。

        到目前为止一切顺利,我们的 webserver 在高峰期间的延迟峰值为 30 毫秒,我们一直在密切关注它。我也有一些关于可能调整算法的想法,如果我觉得它值得添加,我会提交另一个 pull!

        2013 年 1 月 17 日 下午 17:13

        1. Lloyd Hilaiel

          bryant,有趣的是,我最初发现 persona 的最佳点是 20 毫秒。我们的两个“热进程”的峰值约为 30 毫秒。

          我最初的猜测是,我构建的用于选择默认参数的人工测试过程不是对现实世界应用程序的很好表示(通过事件循环的次数太少?模拟的工作太多?)

          2013 年 1 月 31 日 上午 09:40

  6. Digital Planets

    不错的东西,您是否考虑过将请求重定向到托管该应用程序的另一台服务器?

    2013 年 1 月 15 日 下午 19:41

    1. Lloyd Hilaiel

      当然!您部署的更高层(代理/路由)可以检测到 503 响应,并暂时将 node 从轮换中剔除。或者,您可以在 0.2.0 版中使用 `.lag()` 函数,让 node 以某种方式向路由层指示它何时遇到问题。好主意,我认为。

      2013 年 1 月 16 日 下午 17:01

  7. Kevin

    我想知道这在 Heroku 上的表现如何。有什么想法吗?

    2013 年 1 月 16 日 上午 01:12

    1. Lloyd Hilaiel

      我不知道,但我很好奇!请分享您的发现?

      2013 年 1 月 16 日 上午 09:08

  8. Kai

    关于 Dart 的类似文章呢?

    2013 年 1 月 16 日 上午 06:45

  9. Charlie Hoover

    ….

    事件循环中的延迟真的是服务器被请求过载的决定性因素吗?这不会是编写阻塞代码/不正确的控制流的结果吗?

    非常酷/有趣的技巧,尽管如此

    2013 年 1 月 16 日 上午 07:26

    1. Lloyd Hilaiel

      好点。如果您编写一个运行 50-100 毫秒的循环,那么您将获得误报。我在帖子中没有提到这个警告。

      对于执行此操作的应用程序,我建议重新设计一下,也许类似这样?https://hacks.mozilla.ac.cn/2012/11/fully-loaded-node-a-node-js-holiday-season-part-2/

      2013 年 1 月 16 日 上午 09:11

  10. Chris Saari

    @lloydhilaiel 我喜欢 AppEngine 如何根据响应时间的变化而不是系统负载进行扩展。

    2013 年 1 月 16 日 上午 08:35

    1. Lloyd Hilaiel

      嗨,老朋友!因此,这里的假设是事件循环延迟与响应时间的缓慢之间存在紧密联系。我猜这取决于您在堆栈中的位置,以及您使用哪种策略?

      对于 AppEngine,想法可能是对底层应用程序(在代理层)透明地实现负载管理吗?

      2013 年 1 月 16 日 下午 16:54

  11. Scott Donnelly

    Lloyd,干得好,谢谢 - 我一定会使用它。

    2013 年 1 月 16 日 下午 14:16

  12. Eric

    Lloyd,你正在做的这系列文章很棒 - 谢谢你的发布!期待接下来的文章。

    另外一个无关紧要的想法 - 您是否可以考虑在该博客的评论中添加 Persona 支持?;

    2013 年 1 月 16 日 下午 15:09

    1. Lloyd Hilaiel

      Eric,这是一个非常好的主意。我将去摇动几个笼子;)

      2013 年 1 月 16 日 下午 16:40

  13. Mario Pareja

    如果我错了,请纠正我,但这是否只解决了 CPU 绑定系统的这个问题。如果是系统 I/O 绑定,事件循环基本上将是清晰的,而 toobusy 将在 500 毫秒后立即收到其预期的回调。

    我是否遗漏了什么,或者在实践中是否并非如此,因为 I/O 操作以足够快的速度完成来阻塞事件循环?

    无论如何,这些都是实现细节,API 和理念很棒!

    2013 年 1 月 16 日 下午 16:05

    1. Lloyd Hilaiel

      你没有错!如果您想根据过载的数据库(例如)返回抢占式故障(HTTP 503),那么这个库将无济于事。

      2013 年 1 月 16 日 下午 16:43

  14. Fizer Khan

    我使用多个 express 应用程序来构建模块化架构。
    我是否需要将 toobusy 添加到所有 express 应用程序,或者将其添加到父 express 应用程序将被其他 express 应用程序继承?

    2013 年 1 月 20 日 上午 07:12

    1. Lloyd Hilaiel

      在 persona 中,我们有六个不同的 node 进程。我已经将 toobusy 添加到它们中的所有 - 尽管在实践中,可能只有一两个是极端负载下的瓶颈?

      当您更改应用程序配置(添加更多一类节点或添加/更改硬件)时,热进程可能会发生变化,因此让所有组件都能优雅地中断是我们到目前为止采用的方式。

      2013 年 1 月 31 日 上午 09:37

  15. Pete

    感谢您的工作!我担心事件循环延迟有时可能是由于其他进程或 GC 扫描导致的小故障,即使在中等负载下也会意外地给出 503?因此,也许在触发之前允许几秒钟的预热时间会更安全。

    2013 年 1 月 20 日 下午 14:33

    1. bryant chou

      我们也观察到了这一点,一个繁忙的 node 应用程序可能会报告延迟超过水位线,如果有很多事情正在发生(例如,BG GC 操作)。我正在考虑添加一种方法,只有在 toobusy() 超过水位线 X 次时才返回 true,以防止这种情况的发生

      2013 年 1 月 21 日 下午 16:38

      1. Lloyd Hilaiel

        这不是一个坏主意,也是阻尼的动机 - https://github.com/lloyd/node-toobusy/blob/master/toobusy.cc#L18

        很好奇您在实际应用程序中 GC 时长的最大值/最小值/平均值是多少?

        2013 年 1 月 31 日 上午 09:34

  16. bryant chou

    约 80-100 毫秒

    2013 年 1 月 31 日 下午 19:51

  17. Matthew

    Lloyd,您知道这是否与内置的集群模块配合良好?如果在子进程中检查 toobusy(),它会仅报告该进程吗?

    2013 年 3 月 8 日 上午 03:04

    1. Lloyd Hilaiel

      是的!每个进程都有不同的事件循环,因此 toobusy() 在内置的集群模块下应该表现良好。

      2013 年 3 月 11 日 下午 13:29

本文的评论已关闭。