延续上篇文章骚年,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