Active 是 libuv handle 的一种状态。一般某种 handle 都会存在对应的 uv_xxx_start() 和 uv_xxx_stop() 方法。在调用 uv_xxx_start() 后 handle 就会进入 active 状态;而在调用 uv_xxx_stop 方法后,handle 就会被 deactivate。因此,通过找到所有的 Active Handles,可以一定程度上了解目前 Nodejs 进程正在或将要干什么事儿。
而在 Nodejs 进程运行时,会通过调用 uv_loop_alive 来检查是否存在 active handles & requests,从而判断进程是否需要退出。
do {
uv_run(env->event_loop(), UV_RUN_DEFAULT);
per_process::v8_platform.DrainVMTasks(isolate_);
more = uv_loop_alive(env->event_loop());
if (more && !env->is_stopping()) continue;
if (!uv_loop_alive(env->event_loop())) {
EmitBeforeExit(env.get());
}
more = uv_loop_alive(env->event_loop());
} while (more == true && !env->is_stopping());
上面代码中 uv_run 和 uv_loop_alive 两个方法都与是否存在 active handle 有关。Nodejs 进程是否退出也与它们的执行情况有关。 uv_loop_alive 方法会判断是否有 active 状态的 handles 和 requests。
uv_run 则是会不断循环从各个“队列”中取任务执行,在 UV_RUN_DEFAULT 模式下,只有在没有 active handle/request 时,该方法才会跳出循环并返回:
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
...
while (r != 0 && loop->stop_flag == 0) {
...
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
...
return r;
}
1. 引言
近期 KNode 在做 Active Handles/Requests 信息的按需采集功能。在 Active Handles 中包含了 timer 的信息采集,这里的 timer 就是指的通过
setTimeout
/setInterval
设置的定时器,以及 Nodejs 内置 JavaScript 模块中创建的定时器等。而 Nodejs 在 timer 实现上与其他异步资源的代码结构不太一致,做了特定的优化。由于部分同学可能对 Nodejs 中的 Handles 和 Requests 不太熟悉,所以本文会分为上下两篇:
上篇先介绍 Active Handles & Requests 和的基本概念作为铺垫,不会涉及 timer 部分。
下篇会介绍 timer “与众不同”的地方,以及 Nodejs 为其定制的优化。
2. 什么是 Handles 和 Requests?
提到 Nodejs 中的 Handles 和 Requests 时,我们指的就是 libuv 中的 Handles 和 Requests 概念。
可以大致理解为 Handle 会”指代“一系列 I/O 操作或 timer 等,例如用于 TCP 的
uv_tcp_t
,用于定时器的uv_timer_t
。libuv 中还有一个与其类似的概念是的 Request,它与 Handle 的主要区别就是生命周期的长短。例如我们最常见的 DNS 查询(
uv_getaddrinfo_t
)和文件读写(uv_fs_t
)就都是 Request。Nodejs 中的一个核心依赖就是 libuv。它通过 libuv 来实现基于 event loop 模型的异步 I/O,同时抹平了系统间的差异。在之前司内的「Nodejs监控与排障实践」分享上,也简单提到了由于 Handle 会“指代”某个工作,因此通过 Handle(与 Request)的信息也可以一定程度上反应出 Nodejs 目前在忙些什么事儿。
我们在 userland 里做的各种 I/O 操作,最后很多都会直接对应到 libuv 中的 Handle。
上面是从 Handle 角度来看,我们使用 Nodejs 时的大致情况。
日常工作中我们大多都是在 userland 写代码(包括引入的 npm 包)。这些代码会引用(require)各个 Nodejs 内置模块,也就是图中的 internal javascript module。然后最底层是通过 libuv 的 public API 来创建 Handle 并进行异步 I/O。为了能让 js 层使用到最底层的 uv 库,Nodejs 中有一部分代码会负责做 Handle 的包装,加上一些简单的处理逻辑,再通过 v8 binding 机制暴露给 Nodejs 的内部 js 模块,也就是图中 Handle Wrap 和 v8 bindings 这部分。
Handle Wrap 在 node-core 中就是指的 src 目录下实现的 ProcessWrap / FSEventWrap 这些,它们是对 libuv 中 handle 的封装,都继承自 HandleWrap 抽象类。下图是 HandleWrap 和 ReqWrap(libuv Request 的 Wrap)类相关的类继承关系。
其中浅蓝色部分的 AsyncWrap 是对 Nodejs 中异步操作的抽象,会用来做异步的追踪,例如在 userland 中使用的 async_hooks 中的一些功能就会依赖到这部分。绿色部分的 BaseObject 和 MemoryRetainer 则是用于那些在 JavaScript 层和 C/C++ 层有对应关系的对象时,帮助管理对象的生命周期。
3. 什么是 Active 状态?
上面介绍了 Handles 和 Requests。下面说说 Active 的概念。
Active 是 libuv handle 的一种状态。一般某种 handle 都会存在对应的
uv_xxx_start()
和uv_xxx_stop()
方法。在调用uv_xxx_start()
后 handle 就会进入 active 状态;而在调用uv_xxx_stop
方法后,handle 就会被 deactivate。因此,通过找到所有的 Active Handles,可以一定程度上了解目前 Nodejs 进程正在或将要干什么事儿。而在 Nodejs 进程运行时,会通过调用 uv_loop_alive 来检查是否存在 active handles & requests,从而判断进程是否需要退出。
上面代码中
uv_run
和uv_loop_alive
两个方法都与是否存在 active handle 有关。Nodejs 进程是否退出也与它们的执行情况有关。uv_loop_alive
方法会判断是否有 active 状态的 handles 和 requests。uv_run
则是会不断循环从各个“队列”中取任务执行,在UV_RUN_DEFAULT
模式下,只有在没有 active handle/request 时,该方法才会跳出循环并返回:从那个上面代码可以看到,DEFAULT 模式下
uv__loop_alive
的返回值将决定 event loop 是否持续。而uv_loop_alive
内部实际调用的也是uv__loop_alive
。这就是为什么下面这段代码在运行后,Nodejs 进程不会退出
因为在 Nodejs 进程中存在一个 TCP 的 active handle。
4. unref handles
但 libuv 中有一个特殊的操作叫 unref(解引用),可以让实际 “active” 的 handle 不会在
uv_loop_alive
中被统计到。每个 event loop 中会维持一个 active_handles 属性用于计数(requests 也类似),在
uv_loop_alive()
中会通过uv__has_active_handles
宏来判断是否大于 0。而使用
uv_unref()
来解引用,最后就会通过uv__active_handle_rm
来将该计数减一。但这只是减少了
active_handles
计数,并不会影响 handle 的执行。所以只要进程还未退出,handle 的工作就会正常做。而 Nodejs 也将 unref 操作封装暴露到了 userland 里,基本上如果我们在 userland 中创建的对象有对应的 handle,都会在其上存在一个.unref()
的方法。例如还是上面这段代码,当我们稍作修改这个时候如果再去运行,进程立刻就退出了。
5. 什么时候会用到
unref
?也许你会奇怪,为什么需要
unref
呢?像上面这段代码,创建了一个 HTTP 服务监听端口,是不希望退出的。但可以考虑另一个场景:最简版本:
但这个版本的一大问题就是,任务结束后进程并不会退出,因为进程中有一个会“永久”存活的 timer handle。处理这种问题有几个可行方式:
doBatchAsyncTasks
结束后直接调用process.exit()
来主动退出;doBatchAsyncTasks
后,通过clearInterval()
来清除定时器。但这两种方法,都会导致设计上的耦合 —— 异步任务和定时上报需要嵌套对方的资源。
另一种更简便的方式,就是使用
.unref()
:这在你实现一个 library 时可能会更典型。如果你的 library 中创建了一个“永久”的 active handle,而其独立存活并无意义的话,就可能导致使用它的进程无法在预期状态下退出。而这种情况 1、2 这两种方法可能就不太合适, 使用 unref 就会好些。
6. 如何采集 Active Handles?
那我们如何获取当前进程中的 Active Handles 呢?
一个最简单的思路是,在所有 handle 创建的地方(或者统一入口),存储其信息;然后在所有 stop 的地方删除掉存储的对象。但由于 Nodejs 运行过程中 handles 的数量并不少,创建也很频繁(很多时候和 QPS 成正比),所以这种方式在运行时性能与内存占用上都有明显缺陷。
因此会借助目前 Nodejs 已有的能力。其本身已经存储了创建的 Handles 与 Requests 对象。这样只有在按需触发采集时才会有性能与内存开销,采集完毕后又恢复如初。
在 Nodejs 中有两个比较可行的「采集点」:
uv_walk()
这个 public API 来遍历某个 event loop 中所有的 Handles,包括 active/inactive、ref/unref。对于第一种方法,可以传入一个回调函数来处理遍历到的 handles。libuv 内部会把 handles 保存在 event loop 的
handle_queue
上,通过遍历该队列 libuv 就可以拿到所有 handles。第二种方式,则可以通过 Nodejs Environment 对象上的
handle_wrap_queue()
方法来获取HandleWrapQueue
的指针,它是一个双向链表,用存储当前环境下所有的 HandleWrap 对象。由于之前我们提到的,两者存在对应关系,所以基本上从这两处都可以获取到所需的 handle 详情信息。
7. 例外情况:定时器(timer)
通过上面的方式采集,有一个非常重要的例外,就是定时器(timer):
HandleWrapQueue
中是没有TimerWrap
的,所以通过这种方式,是拿不到 js 层设置的定时器的;uv_walk
中拿到的 timer handle,与 js 层设置定时器的情况也差异极大,并不能反映其情况。所以,为什么
HandleWrapQueue
中没有TimerWrap
?为什么 libuv 中的 timer handle 信息与 js 的 timer 对不上呢?这两个问题会留到「Nodejs 中的 Active Handle 与 Timer 优化(下)」中继续介绍。
总结
最后总结一下本文内容:
HandleWrap
和ReqWrap
。.unref()
方法可以在不“取消” handle 工作的同时,解除该 handle 的 active 计数。uv_walk
或env->handle_queue()
来收集大多数 handle 信息。timer 主要来源于两块:
setTimeout
、setInterval
http.request
方法发送请求时去设置的 timeout本文主要包含两个部分: