探究 Node.js 中的 drain 事件

435 查看

探究 Node.js 中的 drain 事件

起因

最近在用 Node.js 写一些网络请求相关的代码时,频繁在一些开源代码中看到 drain 事件的使用,于是我也依葫芦画瓢写到了自己的代码里面:

实际放到线上测试的时候发现,在一些情况下,drain 事件真的会被触发,那到底什么时候会触发 drain 事件呢?drain 事件能用来做什么呢?本着打破砂锅问到底的精神,我决定探究一番。

TLDR

请直接跳到小结部分。

探究

最简单的办法就是查文档。因为我写的是网络请求相关的代码,那么我就先翻越了 net 和 socket 相关的部分。在 Node.js 官方文档中,对于 socket.write 有这么一部分描述:

Returns true if the entire data was flushed successfully to the kernel buffer. Returns false if all or part of the data was queued in user memory. ‘drain’ will be emitted when the buffer is again free.

也就是说,drain 事件是和 socket.write 的返回值强关联的,那么我们可以做一个简单的实验(只写关键部分):

可是无论我怎么运行这部分代码,返回值总是 true,drain 事件没有被触发。那为啥线上就能触发呢?按照文档所说,只有全部或者部分数据被缓冲在了内存里面才会返回 false。那问题又来了,什么时候数据才会被缓冲呢?

既然是被缓冲了,那最先猜测到就是数据流量太大。就像每天上下班高峰期的文一西路那样,一旦车流量太大,前面的路口塞满了,交警就会让后面的车停下来。

好,那我们加大“车”流量(为节省篇幅,部分代码省略):

服务端代码:

客户端代码:

但运行多次之后发现仍旧没有看到任何 drain 事件的迹象。难道次数不够?随着我继续增大 i 的最大值,直到遇到(libuv) kqueue(): Too many open files in system的错误时候,我仍旧没看到 drain 事件。

逼我用绝招。看 Node.js 源代码!

因为 socket.write 实际上是调用的 Stream.write(参考此处源代码),最后我们在 Stream.write 的代码中找到了一丝端倪:

可以看到当要写的数据的长度大于 highWaterMark (字面理解:高水位线)的时候,那么 Stream.write 就会返回 false,也就会触发 drain 事件了。

那这个高水位线具体是多少呢?可以继续看代码

可是无论我怎么运行这部分代码,返回值总是 true,drain 事件没有被触发。那为啥线上就能触发呢?按照文档所说,只有全部或者部分数据被缓冲在了内存里面才会返回 false。那问题又来了,什么时候数据才会被缓冲呢?

既然是被缓冲了,那最先猜测到就是数据流量太大。就像每天上下班高峰期的文一西路那样,一旦车流量太大,前面的路口塞满了,交警就会让后面的车停下来。

好,那我们加大“车”流量(为节省篇幅,部分代码省略):

服务端代码:

客户端代码:

但运行多次之后发现仍旧没有看到任何 drain 事件的迹象。难道次数不够?随着我继续增大 i 的最大值,直到遇到(libuv) kqueue(): Too many open files in system的错误时候,我仍旧没看到 drain 事件。

逼我用绝招。看 Node.js 源代码!

因为 socket.write 实际上是调用的 Stream.write(参考此处源代码),最后我们在 Stream.write 的代码中找到了一丝端倪:

可以看到当要写的数据的长度大于 highWaterMark (字面理解:高水位线)的时候,那么 Stream.write 就会返回 false,也就会触发 drain 事件了。

那这个高水位线具体是多少呢?可以继续看代码

默认值是 16KB,看来还是挺大的啊。所以回想一下刚才我们的实验程序,一个是写的数据比较小,另外一个是实验代码中的服务器端没有复杂逻辑,数据处理的也比较快,我们仍旧拿刚才的车流量的例子,虽然车很多很多,但是如果每辆车都开得飞快,那路也不会堵。只有当一些车比较慢,影响到了后面车的速度的时候,整体速度就会下来,就变堵了。

根据这个思路,我们换成下面这个实验: