9进程
1、更好的使用node
- 充分利用多核cpu服务器
- 保证进程的健壮性、稳定性:单线程一旦异常、引起珍格格进程崩溃
2、服务器模型的变迁
date | 进程数 | 特点 | qps(每秒查询率) | 服务器 |
---|---|---|---|---|
石器时代:同步 | 1 | 一个请求N秒 | 1/N | |
青铜时代:复制进程 | M | 进程数上限M | M/N | |
白银时代:多线程 | M*L(线程数) | 一个线程服务一个请求;每个线程占用内存;频繁切换上下文; | M*L/N | Apache:C10k问题 |
黄金时代:事件驱动 | Cpu的利用率、健壮性;php:没有线程的支持、每个请求建立独立的上下文;Node:所有的请求的上下文是统一的、CPU的计算能力 | Node与Nginx、Php |
3、多进程架构
实现了只监听一个端口,多进程服务的架构
Master-Worker模式,又称主从模式(9.3.1/master.js和worker.js)
jsvar fork = require("child_process").fork; var cpus = require("os").cpus(); console.log(cpus.length); for (var i = 0; i < cpus.length; i++) { fork("./worker.js"); } // ps aux | grep worker.js
jsvar http = require("http"); http .createServer(function (req, res) { res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Hello World\n"); }) .listen(Math.round((1 + Math.random()) * 1000), "127.0.0.1");
创建子进程
- child_process 四个方法的区别spawn、exec、execFile、fork(9.3.2/child.js)
jsvar cp = require("child_process"); // 4个方法,均会返回子进程对象 // 执行命令 cp.spawn("node", ["worker.js"]); // 执行命令 // 可以指定timeout属性设置超时时间,一旦超时将会杀死 cp.exec("node worker.js", function (err, stdout, stderr) { // some code }); // 执行文件 // 可以指定timeout属性设置超时时间,一旦超时将会杀死 // 如果js文件,首行加 #! /usr/bin/env node cp.execFile("worker.js", function (err, stdout, stderr) { // some code }); // 只需制定要执行的js文件模块 cp.fork("./worker.js"); // exec、execFile、fork后面3种方法都是spawn()的延伸应用
通信:
- 进行send发送,message事件接受(9.3.3/IPC/)
- 通信原理:IPC
- Inter-Process Communication,即进程间通信
- 使用的是管道(pipe)技术
- 实现:
- Windows 命名管道(named pipe)实现,
- *nix Unix Domain Socket实现
- 原理:
- 1、父进程再创建子进程前,会创建IPC通道并监听它;
- 2、再去创建子进程,通过NODE_CHANNEL_FD告诉子进程IPC通道的文件描述符;
- 3、子进程启动时,根据文件描述符,连接IPC通道;
- 系统内核完成的通信,不经过网络层
- 句柄:
- 遇到的问题:服务器监听各自端口,如果都监听同一个端口,报错,希望多进程监听一个端口
- 解决思路:代理方案
- 主进程监听主端口(80)
- 子进程监听不同的端口
- 主进程接到请求,代理到不同的进程中
- 不足:主从关系至少要2个文件描述符,影响系统的扩展能力
- 解决思路二:句柄
- 只使用一个描述符
child.send(message, [sendHandle])
sendHandle句柄- 句柄是:一种可以用来标识资源的引用,它的内部包含了指向对象的文件描述符
- 去掉代理,主进程接受到socket后,将这个socket发送给工作进程,解决描述符浪费的问题
- 单个子模块(9.3.3/handle-single-child)
- 多个子模块(9.3.3/handle-mutl-child)
- http(9.3.3/handle-http)主进程关闭服务器的监听,只让子进程来处理
- 句柄发送到底如何做到可以只监听一个端口的呢
- 过程:
- 1、send()方法在将消息发送到IPC管道前,将消息组装成两个对象,一个参数是handle,另一个是message。
- 2、message对象在写入到IPC管道时也会通过JSON.stringify()进行序列化,最终都是字符串
- 3、子进程链接ipc通道,读取父进程发的消息,将字符串通过JSON.parse()解析还原为对象后
- 这个过程中,消息对象还要被进行过滤处理,如果message.cmd值为NODE_HANDLE,它将取出message.type值和得到的文件描述符一起还原出一个对应的对象,还原过程
- 发送的的实际上是句柄描述符,接受的是同一个,这个过程中JSON转化了一遍
- 独立进程中的socket描述符不相同,监听同一个端口报错,底层规定:就不同的进程,可以就相同的网卡、端口进行监听,套接字可以被不用的进程复用,因此问题解决
4、集群稳定之路
为了多进程服务的稳定,也要考虑工作进程存活状态管理、平滑启动、配置动态载入等
- 进程事件
- error、exit、close、disconnect
- child.kill()/process.kill(pid,'SIGTERM')
- SIGTERM是软件终止信号
- 子模块接收到这个事件,process.exit(1)
- 自动重启(9.4/restore)
- 主进程中,接收到一个子进程退出,重启一个新的进程继续
- 子进程监听uncaughtException事件,一旦触发,立即停止
- 自杀信号:
- 有可能子进程等待退出,新进程未启动,
- 得知要退出就发出
自杀信号
, - 主进程接收到,就启动新的(在前)
- 达到平滑重启
- 设置一个超时退出
- 限量重启(9.4/restore-limit)
- 解决:无限制频繁重启
- 添加日志、监控系统、进行报警
- 负载均衡
- Round-Robin 轮叫调度
- 避免CPU和I/O繁忙差异导致的负载不均衡
- 状态共存
- 第三方数据存储
- 数据发生变化,需要一种机制通知各个子进程
- 轮询(不建议)
- 主动通知
- 第三方数据存储
5、Cluster 模块(9.5/cluster)
- 事实上cluster模块就是child_process和net模块的组合应用
- 一个主进程只能管理一组工作进程
- child_process 则可以更灵活地控制工作进程,甚至控制多组工作进程