Node.js 是一个基于事件的平台。这意味着在 Node 中发生的一切都是基于对事件的反应。经过 Node 的事件处理机制遍历一系列回调。html
事件的回调,这一切都由一个名为 libuv 的库来处理,它提供了一种称为事件循环的机制。前端
这个事件循环多是平台中最被误解的概念。当咱们说起事件循环监测的主题时,咱们花了不少精力来正确地理解咱们实际监视的内容。node
在本文中,我将带你们从新认知事件循环是如何工做以及它是如何正确地监视。react
Libuv 是向 Node.js 提供事件循环的库。在 libuv 背后的关键人物 Bert Belder 的精彩的演讲 Node 交互的主题演讲 中,演讲开头他使用 Google 图像搜索展现了各类不一样方式描述事件循环的图片,可是他指出大部分图片描绘的都是错误的。linux
让咱们来看看最流行的误解。android
用户的 JavaScript 代码运行在主线程上面,而另开一个线程运行事件循环。每次异步操做发生时,主线程将把工做交给事件循环线程,一旦完成,事件循环线程将通知主线程执行回调。ios
只有一个线程执行 JavaScript 代码,事件循环也运行在这个线程上面。回调的执行(在运行的 Node.js 应用程序中被传入、后又被调用的代码都是一个回调)是由事件循环完成地。稍后咱们会深刻讨论。git
异步操做,像操做文件系统,向外发送 HTTP 请求以及与数据库通讯等都是由 libuv 提供的线程池处理的。github
Libuv 默认使用四个线程建立一个线程池来完成异步工做。今天的操做系统已经为许多 I/O 任务提供了异步接口(例子 AIO on Linux)。算法
只要有可能,libuv 将使用这些异步接口,避免使用线程池。
这一样适用于像数据库这样的第三方子系统。在这里,驱动程序的做者宁愿使用异步接口,而不是使用线程池。
简而言之:只有没有其余方式可使用时,线程池才将会被用于异步 I/O 。
事件循环采用先进先出的方式执行异步任务,相似于队列,当一个任务执行完毕后调用对应的回调函数。
虽然涉及到相似队列的结构,事件循环并非采用栈的方式处理任务。事件循环做为一个进程被划分为多个阶段,每一个阶段处理一些特定任务,各阶段轮询调度。
为了真正地了解事件循环,咱们必须明白各个阶段都完成了哪些工做。 但愿 Bert Belder 不介意,我直接拿了他的图片来讲明事件循环是如何工做的:
事件循环的执行能够分红 5 个阶段,让咱们来讨论这些阶段。更加深刻的解释见 Node.js 官网
经过 setTimeout() 和 setInterval() 注册的回调会在此到处理。
大部分回调将在这部分被处理。Node.js 中大多数用户代码都在回调中处理(例如,对传入的 http 请求触发级联的回调)。
对接着要处理的的事件进行新的轮询。
此到处理全部由 setImmediate() 注册的回调。
这里处理全部‘结束’事件的回调。
咱们看到,事实上在 Node 应用程序中进行的全部事件都将经过事件循环运行。这意味着若是咱们能够从中得到指标,相应地咱们能够分析出有关应用程序总体运行情况和性能的宝贵信息。
没有现成的 API 能够从事件循环中获取运行时指标,所以每一个监控工具都提供本身的指标,让咱们来看看都有些什么。
每次的记录数。
一个刻度的时间。
因为咱们的代理做为本机模块运行,所以这是比较容易地添加探测器为咱们提供这些信息。
当咱们在不一样的负载下进行第一次测试时,结果使人惊讶 - 让我举例说明一下:
在如下状况下,我正在调用一个 express.js 应用程序,对其余 http 服务器进行外拨呼叫。
有如下 4 中状况:
没有传入请求
使用 apache bench 工具我一次建立了 5 个并发请求
一次 10 个并发请求
为了模拟出一个很慢的后端,咱们让被调用的 http 服务器在 1s 后返回数据。这样形成请求等待后端返回数据,被堆积在 Node 中,产生背压。
事件循环执行阶段
若是咱们看看获得的图表,咱们能够作一个有趣的观察:
若是应用程序处于空闲状态,这意味着没有执行任何任务(定时器、回调等),此时全速运行这些阶段是没有意义的,事件循环就这种状况会在在轮询阶段阻塞一段时间以等待新的外部事件进入。
这也意味着,无负载下的度量(低频,高持续时间)与在高负载下与慢后端相关的应用程序类似。
咱们还看到,该演示应用程序在场景中运行得“最好”的是并发 5 个请求。
所以,标记频率和标记持续时间须要基于每秒并发请求量进行度量。
虽然这些数据已经为咱们提供了一些有价值的看法,但咱们仍然不知道在哪一个阶段花费时间,所以咱们进一步研究并提出了另外两个指标。
这个度量衡量线程池处理异步任务所需的时间。
高工做处理的延迟表示一个繁忙/耗尽的线程池。
为了测试这个指标,我建立了一个使用 Sharp 的模块来处理图像的 express 路由。 因为图像处理开销太大,Sharp 利用线程池来实现。
经过 Apache bench 发起 5 个并发请求到具备图像处理功能的路由与没有使用图片处理的路由有很大不一样,能够直接从图表上能够看到。
事件循环延迟测量在经过 setTimeout(X) 调度的任务真正获得处理以前须要多长时间。
事件循环高延迟表示事件循环正忙于处理回调。
为了测试这个指标,我建立了一个 express 路由使用了一个很是低效的算法来计算斐波那契。
运行具备 5 个并发链接的 Apache bench,具备计算斐波那契功能的路由显示此刻回调队列处于繁忙状态。
咱们清楚地看到,这四个指标能够为咱们提供宝贵的看法,并帮助您更好地了解 Node.js 的内部工做。
这些需求仍然须要在更大的图片中去观察,以使其有意义。所以,咱们正在收集信息以将这些数据归入咱们的异常检测。
固然,在不了解如何从可能的行动中解决问题的状况下,衡量标准自己就不会有太大的帮助。当事件循环快耗尽时,这里有几个提示。
事件循环耗尽
Node.js 应用程序在单个线程上运行。在多核机器上,这意味着负载不会分布在全部内核上。使用 Node 附带的 cluster module 能够轻松地为每一个 CPU 生成一个子进程。每一个子进程维护本身的事件循环,主进程在全部子进程之间透明地分配负载。
如上所述,libuv 将建立一个大小为 4 的线程池。经过设置环境变量 UV_THREADPOOL_SIZE 能够覆盖线程池的默认大小。
虽然这能够解决 I/O 绑定应用程序上的负载问题,我建议屡次负载测试,由于较大的线程池可能仍然耗尽内存或 CPU 。
若是 Node.js 花费太多时间参与 CPU 繁重的操做,开一些服务进程处理这些繁重任务或者针对某些特定任务使用其它语言编写服务也是一个可行的选择。
咱们总结一下咱们在这篇文章中学到的内容:
对我来讲,毫无疑问,咱们今天刚刚在市场上构建了最全面的事件循环监控解决方案,我很是高兴在将来几个星期内,这个惊人的新功能将推向全部客户。
咱们一流的 Node.js 代理团队为了作好事件循环监控尽了很大努力。这篇博客文章中提出的大部分发现都是基于他们对 Node.js 内部运做的深刻了解。 我要感谢 Bernhard Liedl ,Dominik Gruber ,GerhardStöbich 和 Gernot Reisinger 全部的工做和支持。
我但愿这篇文章使你们在事件循环上有新的认知。请在 Twitter 上关注我 @dkhan。我很乐意回答您在 Twitter 里或下面评论区中的提出的一切问题。
最后和以往同样:下载免费试用版去监控您的完整堆栈,包括Node.js。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划。