延续上篇文章骚年,Koa和Webpack了解一下?javascript
本篇文章主要讲述的是如何经过Node建立一个稳定的web服务器,若是你看到这里想起了pm2等工具,那么你能够先抛弃pm2,进来看看,若是有哪些不合适的地方,恳请您指出。css
- 如何利用多核CPU资源。
- 多个工做进程的存活状态管理。
- 工做进程的平滑重启。
- 进程错误处理。
- 工做进程限量重启。
经过在单机上部署多个Node服务,而后监听不一样端口,经过一台Nginx负载均衡。html
这种作法通常用于多台机器,在服务器集群时,采用这种作法,这里咱们不采用。java
经过单机启动一个master进程,而后fork多个子进程,master进程发送句柄给子进程后,关闭监听端口,让子进程来处理请求。node
这种作法也是Node单机集群广泛的作法。git
所幸的是,Node在v0.8版本新增的cluster模块,让咱们没必要使用child_process一步一步的去处理Node集群这么多细节。github
因此本篇文章讲述的是基于cluster模块解决上述的问题。web
首先建立一个Web服务器,Node端采用的是Koa框架。没有使用过的能够先去看下 ===> 传送门api
下面的代码是建立一个基本的web服务须要的配置,看过上篇文章的能够先直接过滤这块代码,直接看后面。服务器
const Koa = require('koa'); const app = new Koa(); const koaNunjucks = require('koa-nunjucks-2'); const koaStatic = require('koa-static'); const KoaRouter = require('koa-router'); const router = new KoaRouter(); const path = require('path'); const colors = require('colors'); const compress = require('koa-compress'); const AngelLogger = require('../angel-logger') const cluster = require('cluster'); const http = require('http'); class AngelConfig { constructor(options) { this.config = require(options.configUrl); this.app = app; this.router = require(options.routerUrl); this.setDefaultConfig(); this.setServerConfig(); } setDefaultConfig() { //静态文件根目录 this.config.root = this.config.root ? this.config.root : path.join(process.cwd(), 'app/static'); //默认静态配置 this.config.static = this.config.static ? this.config.static : {}; } setServerConfig() { this.port = this.config.listen.port; //cookie签名验证 this.app.keys = this.config.keys ? this.config.keys : this.app.keys; } } //启动服务器 class AngelServer extends AngelConfig { constructor(options) { super(options); this.startService(); } startService() { //开启gzip压缩 this.app.use(compress(this.config.compress)); //模板语法 this.app.use(koaNunjucks({ ext: 'html', path: path.join(process.cwd(), 'app/views'), nunjucksConfig: { trimBlocks: true } })); this.app.use(async (ctx, next) => { ctx.logger = new AngelLogger().logger; await next(); }) //访问日志 this.app.use(async (ctx, next) => { await next(); // console.log(ctx.logger,'loggerloggerlogger'); const rt = ctx.response.get('X-Response-Time'); ctx.logger.info(`angel ${ctx.method}`.green,` ${ctx.url} - `,`${rt}`.green); }); // 响应时间 this.app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; ctx.set('X-Response-Time', `${ms}ms`); }); this.app.use(router.routes()) .use(router.allowedMethods()); // 静态资源 this.app.use(koaStatic(this.config.root, this.config.static)); // 启动服务器 this.server = this.app.listen(this.port, () => { console.log(`当前服务器已经启动,请访问`,`http://127.0.0.1:${this.port}`.green); this.router({ router, config: this.config, app: this.app }); }); } } module.exports = AngelServer; 复制代码
在启动服务器以后,将this.app.listen
赋值给this.server
,后面会用到。
通常咱们作单机集群时,咱们fork
的进程数量是机器的CPU数量。固然更多也不限定,只是通常不推荐。
const cluster = require('cluster'); const { cpus } = require('os'); const AngelServer = require('../server/index.js'); const path = require('path'); let cpusNum = cpus().length; //超时 let timeout = null; //重启次数 let limit = 10; // 时间 let during = 60000; let restart = []; //master进程 if(cluster.isMaster) { //fork多个工做进程 for(let i = 0; i < cpusNum; i++) { creatServer(); } } else { //worker进程 let angelServer = new AngelServer({ routerUrl: path.join(process.cwd(), 'app/router.js'),//路由地址 configUrl: path.join(process.cwd(), 'config/config.default.js') //默认读取config/config.default.js }); } // master.js //建立服务进程 function creatServer() { let worker = cluster.fork(); console.log(`工做进程已经重启pid: ${worker.process.pid}`); } 复制代码
使用进程的方式,其实就是经过cluster.isMaster
和cluster.isWorker
来进行判断的。
主从进程代码写在一块可能也不太好理解。这种写法也是Node官方的写法,固然也有更加清晰的写法,借助cluster.setupMaster
实现,这里不去详细解释。
经过Node执行代码,看看究竟发生了什么。
首先判断cluster.isMaster
是否存在,而后循环调用createServer()
,fork4个工做进程。打印工做进程pid。
cluster
启动时,它会在内部启动TCP服务,在cluster.fork()
子进程时,将这个TCP服务端socket
的文件描述符发送给工做进程。若是工做进程中存在listen()
监听网络端口的调用,它将拿到该文件的文件描述符,经过SO_REUSEADDR端口重用,从而实现多个子进程共享端口。
通常来讲,master进程比较稳定,工做进程并非太稳定。
由于工做进程处理的是业务逻辑,所以,咱们须要给工做进程添加自动重启的功能,也就是若是子进程由于业务中不可控的缘由报错了,并且阻塞了,此时,咱们应该中止该进程接收任何请求,而后优雅的关闭该工做进程。
//超时 let timeout = null; //重启次数 let limit = 10; // 时间 let during = 60000; let restart = []; if(cluster.isMaster) { //fork多个工做进程 for(let i = 0; i < cpusNum; i++) { creatServer(); } } else { //worker let angelServer = new AngelServer({ routerUrl: path.join(process.cwd(), 'app/router.js'),//路由地址 configUrl: path.join(process.cwd(), 'config/config.default.js') //默认读取config/config.default.js }); //服务器优雅退出 angelServer.app.on('error', err => { //发送一个自杀信号 process.send({ act: 'suicide' }); cluster.worker.disconnect(); angelServer.server.close(() => { //全部已有链接断开后,退出进程 process.exit(1); }); //5秒后退出进程 timeout = setTimeout(() => { process.exit(1); },5000); }); } // master.js //建立服务进程 function creatServer() { let worker = cluster.fork(); console.log(`工做进程已经重启pid: ${worker.process.pid}`); //监听message事件,监听自杀信号,若是有子进程发送自杀信号,则当即重启进程。 //平滑重启 重启在前,自杀在后。 worker.on('message', (msg) => { //msg为自杀信号,则重启进程 if(msg.act == 'suicide') { creatServer(); } }); //清理定时器。 worker.on('disconnect', () => { clearTimeout(timeout); }); } 复制代码
咱们在实例化AngelServer
后,获得angelServer
,经过拿到angelServer.app
拿到Koa
的实例,从而监听Koa的error
事件。
当监听到错误发生时,发送一个自杀信号process.send({ act: 'suicide' })
。 调用cluster.worker.disconnect()
方法,调用此方法会关闭全部的server,并等待这些server的 'close'事件执行,而后关闭IPC管道。
调用angelServer.server.close()
方法,当全部链接都关闭后,通往该工做进程的IPC管道将会关闭,容许工做进程优雅地死掉。
若是5s的时间尚未退出进程,此时,5s后将强制关闭该进程。
Koa的app.listen
是http.createServer(app.callback()).listen();
的语法糖,所以能够调用close方法。
worker监听message
,若是是该信号,此时先重启新的进程。 同时监听disconnect
事件,清理定时器。
正常来讲,咱们应该监听process
的uncaughtException
事件,若是 Javascript 未捕获的异常,沿着代码调用路径反向传递回事件循环,会触发 'uncaughtException' 事件。
可是Koa
已经在middleware外边加了tryCatch
。所以在uncaughtException捕获不到。
在这里,还得特别感谢下大深海老哥,深夜里,在群里给我指点迷津。
经过自杀信号告知主进程可使新链接老是有进程服务,可是依然仍是有极端的状况。 工做进程不能无限制的被频繁重启。
所以在单位时间规定只能重启多少次,超过限制就触发giveup事件。
//检查启动次数是否太过频繁,超过必定次数,从新启动。 function isRestartNum() { //记录重启的时间 let time = Date.now(); let length = restart.push(time); if(length > limit) { //取出最后10个 restart = restart.slice(limit * -1); } //1分钟重启的次数是否太过频繁 return restart.length >= limit && restart[restart.length - 1] - restart[0] < during; } 复制代码
同时将createServer修改为
// master.js //建立服务进程 function creatServer() { //检查启动是否太过频繁 if(isRestartNum()) { process.emit('giveup', length, during); return; } let worker = cluster.fork(); console.log(`工做进程已经重启pid: ${worker.process.pid}`); //监听message事件,监听自杀信号,若是有子进程发送自杀信号,则当即重启进程。 //平滑重启 重启在前,自杀在后。 worker.on('message', (msg) => { //msg为自杀信号,则重启进程 if(msg.act == 'suicide') { creatServer(); } }); //清理定时器。 worker.on('disconnect', () => { clearTimeout(timeout); }); } 复制代码
默认的是操做系统抢占式,就是在一堆工做进程中,闲着的进程对到来的请求进行争抢,谁抢到谁服务。
对因而否繁忙是由CPU和I/O决定的,可是影响抢占的是CPU。
对于不一样的业务,会有的I/O繁忙,但CPU空闲的状况,这时会形成负载不均衡的状况。
所以咱们使用node的另外一种策略,名为轮叫制度。
cluster.schedulingPolicy = cluster.SCHED_RR;
复制代码
固然建立一个稳定的web服务还须要注意不少地方,好比优化处理进程之间的通讯,数据共享等等。
本片文章只是给你们一个参考,若是有哪些地方写的不合适的地方,恳请您指出。
完整代码请见Github。
参考资料:深刻浅出nodejs