什么是负载均衡?分配负载以达到优化资源使用、最大化吞吐量、最小化响应时间和避免过载的目的。使用具有负载平衡功能的多个服务器组件而不是单个组件可通过冗余提高可靠性。负载均衡服务通常由专门的软件和硬件来执行。主要作用是将大量作业合理分配到多个作业单元执行,解决互联网架构中的高并发和高可用问题。-维基负载均衡(LoadBalance)是基于网络协议分层,通过网络协议中的处理,将负载作业合理分配给多个运行单元。因此,网络协议层2/3/4/7层负载均衡有不同的负载均衡策略。负载均衡的实现分为软/硬。顾名思义:一种是通过软件实现,成本低,灵活简单,缺点是受服务器性能影响。通过硬件实现,性能优于软负载均衡,但成本较高。nodejs可以做什么先看下面这个请求链接图(比如有很多实现方式、策略、架构等)DNS、VIP、Nginx服务的负载均衡底层服务(云端)或者运维已经做好内置,因此节点开发无需过多关心Nginx对web服务集群的负载均衡。可以使用upstream模块为关键节点单服务负载均衡配置不同的策略。主流程分配给多个子流程。这就是软负载平衡。如果节点服务要通过RPC调用其他远程服务,为了不影响其他服务,需要将RPC分发到其他服务的不同节点。结论:从上面我们可以看出3和4是nodejs服务可以做的事情,即服务负载均衡和rpc负载均衡服务负载均衡我们先来了解下nodejs的集群模块。下面是nodejs官方集群示例代码app.jsconstcluster=require('cluster');consthttp=require('http');constnumCPUs=require('os').cpus().length;if(cluster.isMaster){console.log(`Master${process.pid}正在运行`);//Forkworkers.for(leti=0;i{console.log(`worker${worker.process.pid}died`);});}else{//Worker可以共享任何TCP连接//在这种情况下它是一个HTTP服务器http.createServer((req,res)=>{res.writeHead(200);res.end('he你好世界\n');}).listen(8000);console.log(`Worker${process.pid}started`);}启动app.js,当前执行进程为主线程然后fork和cpu个数相同的worker进程worker进程执行process.argv[1]文件默认,即app.js非master进程启动http服务器时,每个worker进程启动一个1.如何监听同一个端口第一个问题:为什么一个进程server能监听那么多同一个端口?第一个(除Windows外所有平台上的默认方法)是循环方法,其中主进程侦听端口,接受新连接并以循环方式将它们分发给工作进程,其中一些内置-insmarts避免工作进程超载。第一种方法(也是除Windows以外的所有平台的默认方法)是循环方法。主进程负责监听端口,接收到新的连接后,循环分发给worker进程。该发行版使用一些内置技巧来防止工作进程任务过载。第二种方法是主进程创建监听套接字并将其发送给感兴趣的工作人员。然后工作人员直接接受传入的连接。第二种做法是master进程创建listensocket后,发送给感兴趣的worker进程,worker进程直接负责接收连接。理论上,第二种方法应该提供最佳性能。然而在实践中,由于操作系统调度程序的变化无常,分布往往非常不平衡。已经观察到负载,其中超过70%的所有连接仅在两个进程中结束,总共八个。从理论上讲,第二种方法应该是最有效的。但在现实中,由于操作系统调度机制的难以捉摸,分布会变得不稳定。八个进程中的两个可能会分担70%的负载。官方支持两种方法。实际上,主进程负责监听端口,子进程会fork一个句柄到主线上,通过循环分发或者监听发送的方式与worker进程通信,交替处理任务。2、进程间如何通信第二个问题:进程间如何通信?1.主进程和子进程主进程和子进程通过IPCapp.js进行通信constcluster=require('cluster');consthttp=require('http');constnumCPUs=require('os').cpus().length;if(cluster.isMaster){console.log(`Master${process.pid}isrunning`);//分叉工人。for(leti=0;i{console.log(`worker${worker.process.pid}died`);});cluster.on('listening',(worker)=>{//发送给workerworker.send({message:'frommaster'})});for(constidincluster.workers){cluster.workers[id].on('message',(data)=>{//由worker接收console.log('mastermessage:',data)});}}else{//Workers可以共享任何TCP连接//在这种情况下它是一个HTTP服务器http.createServer((req,res)=>{res.writeHead(200);res.end('helloworld\n');}).listen(8000);console.log(`Worker${process.pid}启动`);//发给masterprocess.send({message:'fromworker'})process.on('message',(data)=>{//master接收console.log('workermessage',data)})}这是通过node原生的ipc通信,ipc通信的方式有很多种。node的原始ipcchannelshellstdin/stdoutsocketpipemessagequeues2,子进程与子进程一对多,可以通过父进程一对一分发,可以通过ipc进行通信3.如何实现进程负载均衡第三个问题:如何实现进程负载均衡?服务器集群的负载均衡已经交由上层处理(Nginx、DNS、VIP等),那么节点服务是如何做的呢?集群采用round-robin算法策略将http请求分发到不同的worker进程。关于负载均衡算法,下一章《nodejs负载均衡(二):RPC负载均衡》会讲到4.服务异常退出怎么办?第四个问题:服务异常退出怎么办?一般情况下,可以使用try/catch来捕获异常错误,但是如果错过了node中的异常捕获,可能会导致整个流程崩溃。使用try/catch就足够了吗?异常会冒泡到事件循环,触发uncaughtException事件,可以防止程序退出节点异常。默认是打印stderr并以代码1退出,这会触发退出事件。当异常退出时,主线程监测到worker死亡,可以重新fork一个新的。workerTips:exit事件和SignalEvents现在看一下graceful.js的大概实现,下节会有完整的代码,完整案例见graceful-shutdown-example'usestrict';module.exports=options=>{const{processKillTimeout=3000,server}=options;让throwErrorTimes=0process.on('uncaughtException',function(err){throwErrorTimes+=1;console.log('====uncaughtException====');console.error(err)if(throwErrorTimes>1){返回;}关闭()});functionclose(){server.close(()=>{//...做某事})}};五、如何顺利退出第五个问题:如何顺利退出?发布时,多台机器分组发布,保证服务不会无法访问,但是:用户访问的是离线服务,如何保证用户是在等待返回离线服务的请求?一个worker服务异常退出,如何顺利重启一个worker?一个平滑退出的大致流程:forkworker监听worker状态,worker异常退出,refork监听mastersignal退出信号,worker退出前kill掉所有worker,退出前关闭server和worker子进程//master.js'usestrict';constcluster=require('cluster');constkillTree=require('./kill-tree');constnumCPUs=require('os').cpus().length;//constnumCPUs=1;letstopping=假的;安慰。log(`Master${process.pid}isrunning`);cluster.setupMaster({exec:'worker.js',//silent:true,});//Forkworkers.for(leti=0;i{worker.on('message',data=>{//worker接收console.log(`${worker.process.pid}mastermessage:`,data);});});//KillallworkersasyncfunctiononMasterSignal(){if(stopping)return;停止=真;constkillsCall=Object.keys(cluster.workers).map(id=>{constworker=cluster.workers[id];returnkillTree(worker.process.pid);});awaitPromise.all(killsCall);}//kill(2)Ctrl-C//kill(3)Ctrl-\//kill(15)default//Masterexit['SIGINT','SIGQUIT','SIGTERM'].forEach(signal=>{process.once(signal,onMasterSignal);});//终止主进程process.once('exit',()=>{console.log(`Masterabouttoexit`);});//Worker正在监听cluster.on('listening',(worker,address)=>{//发送给workerworker.send({message:'frommaster'});});cluster.on('disconnect',worker=>{console.log(`${worker.id}disconnect`);});//Workerdiedcluster.on('exit',(worker,code,signal)=>{console.log(`Worker${worker.process.pid}死亡,代码:${code},信号:${signal}`);worker.removeAllListeners();//killTree(worker.process.pid,function(err){//console.log(err)//});//停止服务器if(stopping)return;console.log('====Refork====');//重新创建一个新的workercluster.fork();});setTimeout(()=>{cluster.workers[1].send({action:'throwerror',});},600);//worker.js'usestrict';consthttp=require('http');const{fork}=require('child_process');constgraceful=require('./graceful');fork('./child');//Worker可以共享任何TCP连接//在这种情况下,它是一个HTTP服务器constserver=http.createServer((req,res)=>{//服务异常尝试{thrownewError('Happenederror');}catch(err){res.writeHead(200);res.end(`${err.stack.toString()}`);}//console.log(res)//res.setHeader('Content-Type','application/json');//res.setHeader('Access-Control-Allow-Origin','*');//res.writeHead(200);//res.end(JSON.stringify({成功:真}));}).listen(8000);优雅({server,});//发送到masterprocess.send({message:'fromworker',//server});process.on('message',data=>{//master接收if(data.action&&data.action==='throwerror'){//进程抛出异常thrownewError('Killmyself');}console.log('Workermessage',data);});**//graceful.js'usestrict';constcluster=require('cluster');constkillTree=require('./kill-tree');module.exports=options=>{const{processKillTimeout=3000,server}=选项;letthrowErrorTimes=0process.on('SIGTERM',functiononSigterm(){console.info(`Onlygracefulshutdown,worker${process.pid}`)close()})process.on('uncaughtException',function(err){throwErrorTimes+=1;console.log('====uncaughtException====');console.error(err)if(throwErrorTimes>1){return;}close()});functionclose(){server.on('request',(req,res)=>{//关闭http请求req.shouldKeepAlive=false;res.shouldKeepAlive=false;if(!res._header){//关闭套接字连接res.setHeader('Connection','close');}});if(processKillTimeout){consttimer=setTimeout(()=>{//杀死所有子进程killTree(process.pid,()=>{//工作进程退出process.exit(1);})},processKillTimeout);timer.unref&&timer.unref();}constworker=cluster.worker;if(worker){try{server.close(()=>{try{worker.send({message:'disconnect'});worker.disconnect();}catch(err){console.error('Erroron工人断开连接');}});}catch(err){console.error('服务器关闭时出错');}}}};优雅地查看完整案例-shutdown-example6。守护进程或主进程挂了怎么办?第六个问题:守护进程或者主进程挂了怎么办?防止单点故障,提供主从备份服务器7、主动停止服务通过系统命令获取当前节点进程信息过滤停止脚本进程,获取启动脚本进程killmaster进程,发送SIGTERMmaster进程监听SIGTERM,开始killworker,停止server//stop.jsconstmain=async()=>{constcommand=isWin?'wmicPathwin32_processWhere"Name=\'node.exe\'"GetCommandLine,ProcessId'://command,cmd是args的别名,不是POSIX标准,所以我们使用args'ps-eo"pid,args"|grepnode';}//...main().then((result)=>{result.forEach((item)=>{process.kill(item.pid,'SIGTERM')//killTree(item.pid)});})//master.js//kill(2)Ctrl-C//kill(3)Ctrl-\//kill(15)default//Masterexit['SIGINT','SIGQUIT','SIGTERM'].forEach(signal=>{process.once(signal,onMasterSignal);});有关完整案例,请参阅优雅关闭示例。要真正实现一个合理的节点负载均衡框架,还需要做好worker管理和IPC通信机制,不同系统的兼容性,docker,sticky模式等等,接下来说说《nodejs负载均衡(二):RPC负载均衡》的实现在下一章中。