Skip to content

9进程

1、更好的使用node

  • 充分利用多核cpu服务器
  • 保证进程的健壮性、稳定性:单线程一旦异常、引起珍格格进程崩溃

2、服务器模型的变迁

date进程数特点qps(每秒查询率)服务器
石器时代:同步1一个请求N秒1/N
青铜时代:复制进程M进程数上限MM/N
白银时代:多线程M*L(线程数)一个线程服务一个请求;每个线程占用内存;频繁切换上下文;M*L/NApache:C10k问题
黄金时代:事件驱动Cpu的利用率、健壮性;php:没有线程的支持、每个请求建立独立的上下文;Node:所有的请求的上下文是统一的、CPU的计算能力Node与Nginx、Php

3、多进程架构

实现了只监听一个端口,多进程服务的架构

  • Master-Worker模式,又称主从模式(9.3.1/master.js和worker.js)

    js
    var 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
    js
    var 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)
    js
    var 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 则可以更灵活地控制工作进程,甚至控制多组工作进程