node集群搭建好之后,还需要考虑一些细节问题。

  • 性能问题
  • 多个工作进程的存活状态管理
  • 工作进程的平滑重启
  • 配置或者静态数据的动态重新载入
  • 其它细节

1 进程事件

Node子进程对象除了send()方法和messge事件外,还有如下事件:

  • error: 当子进程无法被复制创建、无法被杀死、无法发送消息时会触发改事件。
  • exit:子进程退出时触发改事件,子进程如果是正常退出,这个事件的第一个参数为退出码,否则为null。如果进程是通过kill方法被杀死的,会得到第二个参数,它表示杀死进程时
    的信号。
  • close:在子进程的标准输入输出流中止时触发该事件,参数与exit相同。
  • disconnect:在父进程或子进程中调用disconnect()方法时触发该事件,在调用该方法时将关闭监听IPC通道。

上述这些事件是在父进程能监听到的与子进程相关的事件。除了send()外,还能通过kill()方法给子进程发送消息。kill()方法并不能真正的将通过IPC相连的子进程杀死,它只是给子进程发送
了一个系统信号。默认情况下,父进程将通过kill()方法给子进程发送一个SIGTERM信号。它与进程默认的kill()方法类似。

// 子进程
child.kill([signal]); // 当前进程
process.kill(pid, [signal]);

2 自动重启

有了父子进程之间的相关事件后,就可以在这些关系之间创建出需要的机机制了。监听子进程的exit事件来获知其退出的信息,并在主进程中加入一些子进程管理的机制,比如重新启用一个

新的工作进程来继续服务。

实现代码如下所示:

master.js

var fork = require('child_process').fork;
var cpus = require('os').cpus(); var server = require('net').createServer();
server.listen(1337, () => {
console.log('server listen at port 1337');
}) var workers = {};
var createWorker = function() {
var worker = fork(__dirname + '/worker.js');
// 退出时重新启动新的进程
worker.on('exit', () => {
console.log('Worker ' + worker.pid + ' exited');
delete workers[worker.pid];
createWorker();
})
// 句柄转发
worker.send('server', server);
workers[worker.pid] = worker;
console.log('Create worker. pid: ' + worker.pid);
} for (var i = 0; i < cpus.length; i++) {
createWorker();
} // 进程自己退出时,让所有工作进程退出
process.on('exit', () => {
for (var pid in workers) {
workers[pid].kill();
}
})

worker.js

var http = require('http');
var server = http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/plain'
});
res.end('handled by child, pid is ' + process.pid + '\n');
}); var worker;
process.on('message', (m, tcp) => {
if (m === 'server') {
worker = tcp;
worker.on('connection', (socket) => {
server.emit('connection', socket);
})
}
}) process.on('uncaughtException', () => {
// 停止接受新的连接
worker.close(() => {
//所有已有的链接断开后,退出进程
process.exit(1);
});
})

上述代码的处理流程是,一旦有未捕获的异常出现,工作进程就会立即停止接收新的连接;当所有连接断开后,退出进程。主进程在侦听到工作进程的exit后,将会立即启动新的进程服务,以此

保证整个集群中总是有进程在为用户服务的。

2.1 自杀信号

  上述代码存在的问题是要等到所有连接断开后进程才退出,在极端情况下,所有工作进程都停止接收新的连接,全处在等待退出的状态。但在等到进程完全退出才重启的过程中,所有新来的请求

可能存在没有工作进程为新用户服务的情景,这会丢掉大部分请求。

  为此需要改进这个过程,不能等到工作进程退出后才重启新的工作进程。当然也不能暴力退出进程,因为这样会导致已连接的用户直接断开。于是在退出的流程中增加一个自杀连接,当所有的连接断开后才退出。主进程在接收到自杀信号后,立即创建新的工作进程服务。代码改动如下:

// worker.js
process.on('uncaughtException', () => {
process.send({act: 'suicide'});
// 停止接受新的连接
worker.close(() => {
//所有已有的链接断开后,退出进程
process.exit(1);
});
}) // master.js
var createWorker = function() {
var worker = fork(__dirname + '/worker.js');
// 启动新的进程
worker.on('message', (message) => {
if (message.act === 'suicide') {
createWorker();
}
});
// 退出时重新启动新的进程
worker.on('exit', () => {
console.log('Worker ' + worker.pid + ' exited');
delete workers[worker.pid];
})
// 句柄转发
worker.send('server', server);
workers[worker.pid] = worker;
console.log('Create worker. pid: ' + worker.pid);
}

至此我们完成了进程的平滑重启,一旦有异常出现,主进程就会创建新的工作进程来为用户服务,旧的进程一旦处理完了已有连接就自动断开。整个过程使得我们的应用稳定性和健壮性大大提高。

这里存在问题的是有可能我们的连接是长连接,不是HTTP服务的这种短连接,等待长时间断开可能需要较久的时间。为此为已有连接的断开设置一个超时时间是必要的,在限定时间里强制退出。

// worker.js
process.on('uncaughtException', () => {
process.send({act: 'suicide'});
// 停止接受新的连接
worker.close(() => {
//所有已有的链接断开后,退出进程
process.exit(1);
});
// 5秒后退出
setTimeout(() => {
process.exit(1);
}, 5000)
})

进程中如果出现未能捕获的异常,就意味着有那么一段代码在健壮性上是不合格的。为此退出进程前,通过日志记录下问题所在是必须要做的事情,它可以帮我们很好地定位和追踪代码异常出现的位置,如下所示:

process.on('uncaughtException', (err) => {
// 记录日志
logger.error(err);
process.send({act: 'suicide'});
// 停止接受新的连接
worker.close(() => {
//所有已有的链接断开后,退出进程
process.exit(1);
});
// 5秒后退出
setTimeout(() => {
process.exit(1);
}, 5000)
})

2.2 限量重启

通过自杀信号告知主进程可以使得新连接总是有进程服务,但是依然还是有极端的情况。工作进程不能无限制的被重启,如果启动的过程中就发生了错误,或者启动后接到连接就收到错误,

会导致工作进程被频繁重启,这种频繁重启不属于我们捕捉未知异常的情况,因为这种短时间内频繁重启已经不符合预期的设置,极有可能是程序编写的错误。

为了消除这种无意义的重启,在满足一定规则的限制下,不应当反复重启。比如在单位时间内规定只能重启多少次,超过限制就触发giveup事件,告知放弃重启工作进程这个重要事情。

为了完成限量重启的统计,引入一个队列来做标记,在每次重启工作进程之间进行打点并判断重启是否太过频繁,如下所示:

var fork = require('child_process').fork;
var cpus = require('os').cpus(); var server = require('net').createServer();
server.listen(1337, () => {
console.log('server listen at port 1337');
}) // 重启次数
var limit = 10;
// 时间单位
var during = 60000;
var restart = [];
var isTooFrequently = function() {
// 记录重启时间
var time = Date.now();
var length = restart.push(time);
if (length > limit) {
// 取出最后10个记录
restart = restart.slice(limit * -1);
}
//最后一次重启到前10次重启之间的时间间隔
return restart.length >= limit && restart[restart.length - 1] - restart[0] < during;
} var workers = {};
var createWorker = function() {
// 检查是否太过频繁
if (isTooFrequently()) {
// 触发giveup事件后,不再重启
process.emit('giveip', length, during);
return;
}
var worker = fork(__dirname + '/worker.js');
// 启动新的进程
worker.on('message', (message) => {
if (message.act === 'suicide') {
createWorker();
}
});
// 退出时重新启动新的进程
worker.on('exit', () => {
console.log('Worker ' + worker.pid + ' exited');
delete workers[worker.pid];
})
// 句柄转发
worker.send('server', server);
workers[worker.pid] = worker;
console.log('Create worker. pid: ' + worker.pid);
} for (var i = 0; i < cpus.length; i++) {
createWorker();
} // 进程自己退出时,让所有工作进程退出
process.on('exit', () => {
for (var pid in workers) {
workers[pid].kill();
}
})

giveup事件是比uncaughtException更严重的异常事件。uncaughtException只代表集群中某个工作进程退出,在整体性保证下,不会出现用户得不到服务的情况,但是这个giveup事件则表示

集群中没有任何进程服务了,十分危险。为了健壮性了考虑,我们应在giveup事件中添加重要日志,并让监控系统监视到这个严重错误,进而报警等。

3 负载均衡

  在多进程之间监听相同的接口,使得请求能够分散到多个进程上进行处理,这带来的好处是可以将CPU资源都调用起来。Node默认提供的机制是采用操作系统的抢占式策略。所谓的抢占式就是

在一堆工作进程中,闲着的进程对到来的请求进行争抢,谁抢到谁服务。

  一般而言,这种抢占式策略对大家是公平的,各个进程可以根据自己的繁忙度来进行抢占。但是对于node而言,需要分清的是它的繁忙是有CPU、I/O两个部分构成的,影响抢占的是CPU的繁忙度。对于不同的业务,可能存在I/O繁忙,而CPU较为空闲的情况,这可能造成某个进程能够抢到较多请求,形成负载不均衡的情况。

  为此Node在v0.11中提供了一种新的策略使得负载均衡更合理,这种新的策略叫Round-Robin,又叫轮叫调度。轮叫调度的工作方式是由主进程接受连接,将其一次分发给工作进程。分发的策略

是在N个工作进程中,每次选择第i = ( i + 1 ) mod n个进程来发送连接。

  Round-Robin非常简单,可以避免CPU和I/O繁忙差异导致的负载不均衡。Round-Robin策略也可以通过代理服务来实现,但是它会导致服务器上消耗的文件描述符是平常方式的两倍。

node 集群与稳定的更多相关文章

  1. ELK 性能(2) — 如何在大业务量下保持 Elasticsearch 集群的稳定

    ELK 性能(2) - 如何在大业务量下保持 Elasticsearch 集群的稳定 介绍 如何在大业务量下保持 Elasticsearch 集群的稳定? 内容 当我们使用 Elasticsearch ...

  2. master挂了的话pm2怎么处理 使用pm2方便开启node集群模式

    本文为转载 Introduction As you would probably know, Node.js is a platform built on Chrome's JavaScript ru ...

  3. node集群(cluster)

    使用例子 为了让node应用能够在多核服务器中提高性能,node提供cluster API,用于创建多个工作进程,然后由这些工作进程并行处理请求. // master.js const cluster ...

  4. Node.js 集群

    稳定性: 2 - 不稳定 单个 Node 实例运行在一个线程中.为了更好的利用多核系统的能力,可以启动 Node 集群来处理负载. 在集群模块里很容易就能创建一个共享所有服务器接口的进程. var c ...

  5. 基于k8s的集群稳定架构

    前言 我司的集群时刻处于崩溃的边缘,通过近三个月的掌握,发现我司的集群不稳定的原因有以下几点: 1.发版流程不稳定 2.缺少监控平台[最重要的原因] 3.缺少日志系统 4.极度缺少有关操作文档 5.请 ...

  6. 基于k8s的集群稳定架构-转载

    基于k8s的集群稳定架构-转载 前言 我司的集群时刻处于崩溃的边缘,通过近三个月的掌握,发现我司的集群不稳定的原因有以下几点: 1.发版流程不稳定 2.缺少监控平台[最重要的原因] 3.缺少日志系统 ...

  7. Redis 集群实现

    Nosql,作为程序员在当下不了解点儿,还真不行,出去聊起来别人就会说你土.那么就聊聊其中一个比较火的redis.redis单机版没得说,但是一直没有集群版,有也是山寨的.前段时间对redis的实现进 ...

  8. Redis集群明细文档

    Redis目前版本是没有提供集群功能的,如果要实现多台Redis同时提供服务只能通过客户端自身去实现(Memchached也是客户端实现分布式).目前根据文档已经看到Redis正在开发集群功能,其中一 ...

  9. Redis集群明细文档(转)

    相信很多用过Redis的同学都知道,Redis目前版本是没有提供集群功能的,只能单打独斗.如果要实现多台Redis同时提供服务只能通过客户端自身去实现.目前根据文档已经看到Redis正在开发集群功能, ...

随机推荐

  1. Python之Pulsar框架使用

    本文内容主要包含Pulsar的介绍和安装.初步使用.应用.常见示例等. 一. 介绍和安装 Pulsar是Python事件驱动并发框架:Pulsar具有高扩展性.高可用性的框架,它能够基于事件驱动的开源 ...

  2. Java反射学习二

    利用反射进行对象拷贝的例子 如下例程ReflectTester类进一步演示了Reflection API的基本使用方法. ReflectTester类有一个copy(Object object)方法, ...

  3. TensorFlow安装-Windows

    参考:https://blog.csdn.net/dou3516/article/details/77836459 一.安装环境 TensorFlow即可以支持CPU,也可以支持CPU+GPU.前者的 ...

  4. jmeter报错之“请在微信客户端打开链接”

    这是一个还没解决的问题,这里纯粹记录自己思考的过程,后续给自己参考. 先说明情景:对微信公众号的一个接口进行调用跑通,后续可能需要压测(是的,仅仅是调通一个接口而已o(╥﹏╥)o) 1.按照我理解的正 ...

  5. MapReduce开发程序,运行环境配置

    Hadoop主机:linux 开发环境主机:Win7 + Itellij 本地运行 1. 下载hadoop安装包,放到本地目录中. 2. 配置环境变量$HADOOP_HOME及$PATH=$HADOO ...

  6. 5分钟速成C++14多线程编程

    原文链接:Learn C++ Multi-Threading in 5 Minutes C++14的新的多线程架构非常简单易学,如果你对C或者C++很熟悉,那么本文非常适合你.作者用C++14作为基准 ...

  7. 使用git管理代码

    上传工程 1.登录github后,点击右上角带有+号的图标,输入仓库名创建仓库(Repository). 2.在项目文件夹下执行以下命令: touch README.md git init 如果工程中 ...

  8. JS window.onload 和模拟document.ready.

    hhhhhhhhhhhhhhhh hhhhhhhhhhhhhhhh ttttttttttttt 注意观察 事件执行的 先后顺序. 总的来说,window.onload()方法是必须等到页面内包括图片的 ...

  9. 20145209刘一阳《JAVA程序设计》第三周课堂测试

    第三周课堂测试 1.使用汇编语言编写指令时,用一些简单的容易记忆的符号来代替二进制指令,比机器语言更为方便,属于高级语言.(B) A .true B .false 2.下列说法正确的是(ABCD) A ...

  10. 18-[模块]-shutil

    shutil模块 高级的 文件.文件夹.压缩包 处理模块 (1)文件操作 shutil.copyfileobj(fsrc, fdst[, length]) 将文件内容拷贝到另一个文件中 import ...