当前位置: 首页 > 科技观察

在Node.js中使用SO_RESUEPORT

时间:2023-03-15 13:09:05 科技观察

前言:今天下载了最新版的Node.js代码,在Node.js的TCP模块中加入了SO_RESUEPORT能力。本文介绍具体实现。可以参考之前关于SO_RESUEPORT知识的文章或者网上的文章。1libuvSO_RESUEPORT是操作系统内核提供的能力,所以第一步是修改libuv。考虑到操作系统兼容性问题,目前只支持Linux系统。旧版MacOS也支持相关属性,但效果不如预期。新版本的MacOS确实支持它。考虑到Node.js几乎都是部署在Linux上的,所以可以先跟进Linux内核。首先修改deps/uv/include/uv.h。enumuv_tcp_flags{UV_TCP_IPV6ONLY=1,//支持SO_RESUEPORTflagsUV_TCP_REUSEPORT=2};然后修改deps/uv/src/unix/tcp.c。#ifdefined(SO_REUSEPORT)&&defined(__linux__)on=1;if((flags&UV_TCP_REUSEPORT)&&setsockopt(tcp->io_watcher.fd,SOL_SOCKET,SO_REUSEPORT,&on,sizeof(on)))returnUV__ERR(errno);#endifHere判断是否有是两个宏,SO_RESUEPORT只有在有一个的情况下才能使用。如果支持,通过setsockopt设置socket的SO_REUSEPORT标志,这是核心逻辑。2修改C++层修改底层Libuv后,继续修改C++层,因为这是一个可选属性,所以需要添加相关逻辑。修改src/tcp_wrap.cc。首先导出一个新常量#ifdefined(SO_REUSEPORT)&&defined(__linux__)NODE_DEFINE_CONSTANT(constants,UV_TCP_REUSEPORT);#endif在JS层,可以通过判断常量是否导出来判断系统是否支持SO_RESUEPORT。然后修改bind函数,因为我们再次bind的时候可以设置SO_RESUEPORT。templatevoidTCPWrap::Bind(constFunctionCallbackInfo&args,intfamily,std::functionuv_ip_addr){TCPWrap*wrap;ASSIGN_OR_RETURN_UNWRAP(&wrap,args.Holder(),args.GetReturnValue().Set(UV_EBADF));Environment*env=wrap->env();node::Utf8Valueip_address(env->isolate(),args[0]);intport;unsignedintflags=0;if(!args[1]->Int32Value(env->context()).To(&port))return;//ipv6支持ipv6Only和SO_RESUEPORTif(family==AF_INET6&&!args[2]->Uint32Value(env->context()).To(&flags)){return;//ipv4之前不支持任何flags,这里需要加上这个逻辑,因为我们需要支持SO_RESUEPORT}elseif(family==AF_INET4&&!args[2]->Uint32Value(env->context()).To(&flags)){return;}Taddr;interr=uv_ip_addr(*ip_address,port,&addr);if(err==0){err=uv_tcp_bind(&wrap->handle_,reinterpret_cast(&addr),flags);}args.GetReturnValue().Set(err);}C++主要完成透传flags的逻辑。3修改JS层修改JS层是最复杂的部分,主要是为了应用层的兼容性。也就是说,如果Node.js真的支持SO_RESUEPORT,那么在有些平台不支持SO_RESUEPORT的情况下,我们如何保证我们的代码能够在各种平台上运行。简单来说,如果我们的平台支持SO_RESUEPORT,我们可以启动多个子进程,分别执行下面的代码。consthttp=require('http');http.createServer((req,res)=>{res.end('hello');}).listen({port:8000,reuseport:true});这时候,只需要修改Node.js的net.js,将reuseport标签传递给C++层,再传递给Libuv,但是问题是如果我们这样写代码,是无法运行的在不支持SO_RESUEPORT的平台上,因为会导致重复监听Port报错。所以为了兼容性,我认为解决方案是使用Cluster模块。目前Cluster模块支持轮询和共享两种模式。然后我们可以添加另一种重用端口模式。这样做的好处是,一旦我们的平台不支持SO_RESUEPORT,我们就有可能降级到Node.jsnow模式。我们知道Cluster模块有两个原理。一种是主进程监控并将连接分发给子进程。另一种是主进程创建套接字,通过文件描述符传递传递给子进程。所有进程都是共享的。一个插座。让我们看看如何去做。首先修改lib/internal/cluster/primary.js。//添加这个逻辑if((message.addressType===4||message.addressType===6)&&(message.flags&TCPConstants.UV_TCP_REUSEPORT)){handle=newReusePort(key,address,message);}elseif(schedulingPolicy!==SCHED_RR||message.addressType==='udp4'||message.addressType==='udp6'){handle=newSharedHandle(key,address,message);}else{handle=newRoundRobinHandle(key,address,message);}我们在queryServer函数中添加了一个if逻辑。如果addressType是4或者6,说明是TCP协议,设置了UV_TCP_REUSEPORT(监听的时候传入),会走reuseport逻辑,剩下两个else就是当前Node.js的逻辑。让我们看看ReusePort.js做了什么。'usestrict';constassert=require('internal/assert');constnet=require('net');const{constants:TCPConstants}=internalBinding('tcp_wrap');module.exports=ReusePort;functionReusePort(key,address,{port,addressType,fd,flags}){this.key=key;this.workers=[];this.handles=[];this.list=[address,port,addressType,fd,flags];}重用端口。prototype.add=function(worker,send){assert(!this.workers.includes(worker));constrval=net._createServerHandle(...this.list);leterrno;lethandle;if(typeofrval==='number')errno=rval;elsehandle=rval;this.workers.push(worker);this.handles.push(handle);send(errno,null,handle);};ReusePort.prototype.remove=function(worker){constindex=this.workers.indexOf(worker);if(index===-1)returnfalse;//工人没有共享这个句柄.this.workers.splice(index,1);this.handles[index]。关闭();this.handles.splice(index,1);返回真;};我们只需要关注上面代码中的net._createServerHandle即可。在多个进程不能同时监听同一个端口的情况下,Node.js只会调用net._createServerHandle创建一个socket,然后多个进程共享。在这里,我们将为每个进程创建一个套接字。当子进程调用queryServer时,这个socket返回给子进程。我们暂时不需要关注其余的逻辑。最后看一下_createServerHandle的逻辑。constandle=newTCP(TCPConstants.SERVER);if(addressType===6){err=handle.bind6(address,port,flags);}else{err=handle.bind(address,port,flags||0);}_createServerHandle的逻辑是创建一个socket,并将IP和端口绑定到socket上。我们可以看到flags会传给C++层,C++层会传给LIbuv。这样,我们就完成了整个过程。整体流程如下。1子进程执行listen时,传入reuseport为true2子进程通过进程间通信请求主进程3主进程返回一个新的socket并绑定到对应地址4子进程执行listen启动服务器。4使用接下来,让我们看看如何使用它。首先,创建一个server.js。constcluster=require('cluster');constos=require('os');consthttp=require('http');constcpus=os.cpus().length;if(cluster.isPrimary){constmap={};for(leti=0;i{map[pid]++;});}process.on('SIGINT',()=>{console.log(map);});}else{http.createServer((req,res)=>{process.send(process.pid);res.end('hello');}).listen({reuseport:true,port:8000});}创建另一个客户端client.jsconsthttp=require('http');functionconnect(){setTimeout(()=>{http.get('http://localhost:8000/',(res)=>{console.log(res.statusCode);connect();});},50);}connect();客户端串行访问服务端,我们看到使用方式和当前的Node.jsCluster使用方式一样。就算我们把reuseport改成false或者在其他平台上运行也没有问题。效果如下。我们可以看到在reuseport的情况下,负载是相当均衡的。后记:目前都是传入参数来控制监听时是否开启SO_RESUEPORT。以后可以增加设置cluster.schedulingPolicy的方法来配合现在的共享和轮询方式。考虑到Cluster模块不是必须的,因为我们可以直接使用subprocess模块??监听同一个端口。所以通过listen函数来控制是非常有必要的。目前我通过修改Node.js内核大致体验了SO_RESUEPORT,然后对代码进行回顾和完善。