提升Node.js性能之SO_REUSEPORT的探讨

前言:多个进程不能同时绑定同一个IP和端口,这是早期Linux内核的一个限制,这个限制给服务器带来了很多不便之处,因为服务器的架构通常不是单进程的,尤其在多核的时代,但是3.9的内核带来了新的特征SO_REUSEPORT。不仅使得服务器的代码逻辑变得简单,对服务器的性能也提升了不少。SO_REUSEPORT的意义是支持同用户下的多个进程同时监听一个IP和端口,本文介绍在Node.js中支持SO_REUSEPORT,以提升Node.js的性能。

目前,Node.js的TCP模块还没有支持SO_REUSEPORT。但是服务器通常是多进程架构的,不管是早期Apache的一个进程处理一个请求的模式,还是现代基于epoll+单线程+多核的模式(Nginx、Redis、Node.js),整体上都是多进程的架构,这就会涉及到一个问题,多个进程如何同时处理请求。传统的方法主要有以下几种(具体可以参考服务器处理连接的架构演变)。
1 主进程接收请求,分发给子进程处理,这种模式的好处是不需要处理多进程同时监听同一个IP端口的问题,因为只有主进程监听。

2 通过fork绕过内核的检查机制。我们看一下一个服务器启动的代码。

int socketfd = socket();
bind(socketfd);
listen(socketfd);

其中在bind这一步,内核会检测IP端口的合法性,如果这个IP和端口已经被监听,那么就会报错,所以如果多个进程同时bind同一个IP端口,那么第二个进程就会失败。但是有办法可以绕过这个限制,那就是不执行bind,我们看看具体怎么做。
2.1 主进程执行bind,这时候的底层架构如下。

2.2 主进程fork多个子进程,这时候的架构如下。

我们看到,fork之后的子进程继承了主进程中的文件描述符fd。这时候子进程不需要执行bind也绑定到了和主进程同样的IP和端口。
2.3 子进程执行listen修改socket状态为监听状态。
这时候就成功地完成了多个进程同时绑定一个IP和端口。

3 通过文件描述符传递的方式绕过内核的检查机制。具体做法如下。
3.1 主进程执行bind
3.2 主进程fork多个子进程,但是不继承主进程fd(fork的时候设置O_CLOEXEC)。
3.3 主进程通过文件描述符传递的方式把fd传给子进程,架构和2.2中的一样。
3.4 子进程执行listen。

Node.js中支持1和3这两种方式,具体在Cluster模块实现。但是上面的几种方式虽然解决了多个进程监听同一个IP端口的问题,但是性能上会存在一些问题。第一种模式存在的问题是,只有主进程可以接收请求,子进程被动接受主进程传过来的请求。那么主进程接收请求的能力会成为服务器的瓶颈。第二第三种模式存在的问题是,当连接到来时,多个进程会同时被唤醒,从而竞争去处理这个连接,连接最终被哪个进程处理,这取决于内核的进程调度,剩下的进程会被无效唤醒,这就是著名的惊群现象,解决惊群现象方式目前有两种,一种是在应用层解决,比如nginx会控制同时只有一个进程阻塞在socket上,保证连接到来时,只有一个进程被唤醒。另一种是在内核解决,新版的内核支持设置标记保证每次只有一个进程被唤醒,哪个进程被唤醒取决于内核实现。这往往会带来服务器代码的复杂处理和多个进程处理连接的负载不均衡。比如在Nginx和Node.js中都有相关的处理(参考Node.js的UV_HANDLE_TCP_SINGLE_ACCEPT标记)。

这时候,SO_REUSEPORT出现了,SO_REUSEPORT更彻底地支持多个进程同时绑定同一个IP端口,架构如下。

SO_REUSEPORT使得每个进程拥有独立的socket和连接队列,内核不仅允许多个进程绑定同一个IP端口,同时在分配连接到对应socket的连接队列时可以做到负载均衡,这使得应用层变得简单了很多。应用层不再需要解决负载均衡的问题,更加不用为了绕过内核对绑定地址的检测想尽办法。这意味着Node.js的Cluster模块很多代码可以不要了。他只需要管理进程,不再需要处理绑定IP端口的问题,同时net模块也变得简单。

支持SO_REUSEPORT不仅(理论上)可以提高Node.js作为服务器的性能,同时也简化了代码的逻辑。不过对Node.js来说,Cluster模块无法从SO_REUSEPORT特性获益,因为Cluster模块的share工作模式本质是通过传递文件描述符的方式让多个进程共享socket的。而不是一个进程一个socket。那么如何从SO_REUSEPORT特性获益呢?直接使用child_process模块,fork多个子进程,每个子进程调用listen函数就行(如果Node.js真的支持SO_REUSEPORT的话,后续Cluster只是作为降级/兼容方案,Node.js可以暴露接口判断是否支持SO_REUSEPORT,不过应用层可能又要封装兼容的代码。。。)。

1 具体pr可以参考https://github.com/libuv/libuv/pull/3198
2 性能测试可以参考Nginx这个文章
3 SO_REUSEPORT的原理可以参考从内核看SO_REUSEPORT的实现(基于5.9.9)

猜你喜欢

转载自blog.csdn.net/THEANARKH/article/details/118253845