node中的多进程实现方式

2,779 阅读11分钟

node多进程案例

为什么要使用多进程?

  • 使用场景或者说使用的原因,node server 单进程也能跑,也能满足一定的业务需求,但是当访问人数达到一定数量的时候,会发现有些请求响应时间比较长,或者是超时,这时候就需要多进程,就像银行办理业务是多个窗口同时办理,并且每个客户办理业务时,各个窗口之间并不会相互影响,不用多进程就类似一个银行的网点只开通一个窗口办理业务,这就是使用多进程的原因;

node 实现多进程

  • node中的多进程有很多实现方式,从nodejs语言本身可以实现,利用k8s/docker/pm2也可以实现,不同的实现方式底层逻辑是什么样的,有什么优劣,更适合什么场景,nodejs不同场景中更适合使用哪种多进程的实现方式,以及这些方式的底层原理和对比,pm2的常用命令等等
  • 具体的不同场景选择的部署方式,详见node Puppeteer 生成pdf多进程部署

1.node中创建多进程的方式

1.1 spawn

const { spawn } = require("child_process");
const path = require("path");

const cp = spawn("node", ["sum.js"], {
  cwd: path.resolve(__dirname, "worker"),
  //   stdio: "ignore", // 如果填写的是ignore 默认会忽略掉 子进程的输出
  //   stdio: [ dout, process.stderr],子进程就会共享父进程的stdin stdout stderr; 等价于inherit 等价于 [0, 1, 2]
  stdio: "pipe", // 'pipe' 等价于 ['pipe','pipe', 'pipe']
});
cp.stdout.write("hello");

// 读流和写流 ipc
cp.stdout.on("data", function (data) {
  console.log(data.toString(), "----");
});

// 父子进程间的通信
// node主线程是单线程,可以开子进程
cp.on("close", () => {
  console.log("子进程关闭掉了");
});

cp.on("exit", () => {
  console.log("子进程退出了");
});

cp.on("error", () => {
  console.log("子进程出错了");
});

1.1.2 spawn 创建独立子进程

const { fork, spawn } = require("child_process");

const path = require("path");
console.log(process.pid);
const cp = spawn("node", ["write.js", "a", "b"], {
  cwd: path.resolve(__dirname, "worker"),
  stdio: "ignore", // 不管儿子的输入输出
  detached: true, // 独立进程
});
cp.unref(); // 断绝父子关系
// cp.ref() // 恢复关系
// 这样产生的子进程是个独立的子进程,这个子进程不能用父亲的输入输出

1.2 fork

const { fork } = require("child_process"); // fork 内部也是 基于spawn
const path = require("path");

if (cluster.isMaster) {
  // fork 会自动加上node, 特点就是默认就是node命令执行
  const worker = fork("sum.js", ["a", "b"], {
    cwd: path.resolve(__dirname, "worker"),
    stdio: [0, 1, 2, "ipc"], // 父子间通信使用三个标准方法和ipc通道
  });
  worker.send('hi there');
} else if (cluster.isWorker) {
  process.on('message', function(msg) {
    process.send(msg);
  });
}

1.3 execFile

const { execFile, exec } = require("child_process");
const path = require("path");

// 这种回调的方式 返回结果是buffer,结果不能太大,太大占用内存,最大 200 * 1024,最大200k
// 不会产生命令行,不能在命令行里面拿到结果
const cp = execFile(
  "ls",
   ["-ll"],
    {
     cwd: path.resolve(__dirname, "worker"),
   },
   function (err, stdout, stderr) {
     console.log("stdout", stdout);
  }
 );

1.4 exec

const cp = exec(
    "path",
    {
      cwd: path.resolve(__dirname, "worker"),
    },
    function (err, stdout, stderr) {
      console.log("stdout", stdout);
    }
  );

1.5 Docker Swarm

  • Swarm是Docker公司推出的用来管理docker集群的平台,几乎全部用GO语言来完成的开发的,代码开源在github.com/docker/swar… 它是将一群Docker宿主机变成一个单一的虚拟主机,Swarm使用标准的Docker API接口作为其前端的访问入口,换言之,各种形式的DockerClient(compose,docker-py等)均可以直接与Swarm通信,甚至Docker本身都可以很容易的与Swarm集成,这大大方便了用户将原本基于单节点的系统移植到Swarm上,同时Swarm内置了对Docker网络插件的支持,用户也很容易的部署跨主机的容器集群服务。
  • Docker Swarm 和 Docker Compose 一样,都是 Docker 官方容器编排项目,但不同的是,Docker Compose 是一个在单个服务器或主机上创建多个容器的工具,而 Docker Swarm 则可以在多个服务器或主机上创建容器集群服务,对于微服务的部署,显然 Docker Swarm 会更加适合。

Swarm的调度策略

Swarm在调度(scheduler)节点(leader节点)运行容器的时候,会根据指定的策略来计算最适合运行容器的节点,目前支持的策略有:spread, binpack, random.

1)Random

顾名思义,就是随机选择一个Node来运行容器,一般用作调试用,spread和binpack策略会根据各个节点的可用的CPU, RAM以及正在运行的容器的数量来计算应该运行容器的节点。

2)Spread

在同等条件下,Spread策略会选择运行容器最少的那台节点来运行新的容器,binpack策略会选择运行容器最集中的那台机器来运行新的节点。使用Spread策略会使得容器会均衡的分布在集群中的各个节点上运行,一旦一个节点挂掉了只会损失少部分的容器。

3)Binpack

Binpack策略最大化的避免容器碎片化,就是说binpack策略尽可能的把还未使用的节点留给需要更大空间的容器运行,尽可能的把容器运行在一个节点上面。

1.6 K8s

  • Kubernetes 看作是用来是一个部署镜像的平台
  • 可以用来操作多台机器调度部署镜像
  • 在 Kubernetes 中,可以使用集群来组织服务器的。集群中会存在一个 Master 节点,该节点是 Kubernetes 集群的控制节点,负责调度集群中其他服务器的资源。其他节点被称为 Node
  • 和Docker Swarm是同一类型的集群部署工具,k8s是google出的,目前市场上k8s比Docker Swarm 更热一些;
  • 使用k8s设置pod副本数量的方式来启动多进程,需要注意的是,这种情况需要node服务本身需要使用单进程架构;

node多进程方式对比

node中可以开启子进程, 目的是为了充分利用多核cpu,开启子进程 可以帮助我们计算一些 cpu密集型的操作

最常用的 execFile(文件) 和 fork(node 命令)

  1. spawn 可以使用流的方式进行进程间的通信,一点点传,可以接受大量数据;参数比较多,不方便使用;

  2. fork  用的比较多 叉子,基于spawn; 特点就是默认就是node命令执行;

  3. execFile 执行文件;执行这个文件,会把结果收集好,一起返回来(stdout)

  4. exec 会产生命令行,性能稍低; execFile和exec最大区别就是shell:true,开启命令行

  • exec是基于execFile, execFile是基于spawn的,fork也是基于spawn
  1. Docker Swarm 适用于公司本身部署容器就是用Docker Swarm部署,
  2. K8s 适用于公司本身部署容器就是用K8s部署,一般不会为了部署某个项目专门部署一套K8s服务或Docker Swarm服务;
  3. PM2, PM2模块是cluster模块的一个包装层

2.进程管理 - 监听同一个端口

  • 我们可以使用上面的方法启动多个进程,但是这多个进程监听的不是同一个端口,这就有问题了,如果一个node程序的不同进程监听的不是一个端口,那么当请求到nginx的时候,应该配置哪个端口呢,虽然也可以在nginx配置文件中手动指定所有进程监听的端口,但这无疑是杀鸡用了牛刀,并且也是不合理的,通常我们需要让多个进程监听一个端口,在主进程里面进行负载均衡,和请求分发;

2.1 cluster 集群方案1 - 主传子

主进程创建server,把主server 传入到子进程中,子进程创建自己的server,这个子server listen 主server

// 不同进程 监听同一个端口号
// 请求多的时候可以分担压力
const path = require("path");
const http = require("http");
const { fork } = require("child_process");
const cpus = require("os").cpus().length;

console.log("cpus", cpus);

// 先在主进程中启动一个服务
// 高并发都是io密集型可以;cpu密集型不适合
let server = http
  .createServer((req, res) => {
    res.end(process.pid + "main end处理");
  })
  .listen(4000);

for (let i = 0; i < cpus - 1; i++) {
  let cp = fork("server.js", {
    cwd: path.resolve(__dirname, "worker"),
    stdio: [0, 1, 2, "ipc"],
  });
  cp.send("server", server); // 可以在ipc模式下 第二个参数传入一个http服务 或tcp服务
}
  • worker/server.js
// worker/server.js
const http = require("http");
console.log("server start");

// 多进程监控同一个端口
process.on("message", function (data, server) {
  http
    .createServer((req, res) => {
      res.end(process.pid + "处理");
    })
    .listen(server);
});

2.2 cluster 集群方案2 主子共用一个文件

const cluster = require("cluster");
const http = require("http");
const cpus = require("os").cpus();
const path = require("path");

// cluster.isWorker 和 cluster.isMaster 相反,工作进程
if (cluster.isMaster) {
  for (let i = 0; i < cpus.length; i++) {
    cluster.fork(); // 调用的是 child_process.fork;
    //  会以当前文件创建子进程
    // 并且isMaster为false,此时就会执行else方法
  }
} else {
  console.log("子");
  http
  .createServer((req, res) => {
    res.end(process.pid + ": end");
    // 在集群的环境下可以监听同一个端口
  })
  .listen(4000);
}

2.3 cluser 集群方案3 主子分离,通过设置区分

  • 主进程只负责fork子进程,子进程里面创建服务
  • 可以创建cpus.length个子进程
const cluster = require("cluster");
const http = require("http");
const cpus = require("os").cpus();
const path = require("path");
// 入口文件
// 如果是主进程,由 process.env.NODE_UNIQUE_ID决定的

// 可以分开写,集群监听同一个端口
cluster.setupMaster({
  exec: path.resolve(__dirname, "worker/cluster.js"),
});
// 主干里面只负责fork,当前子进程里执行worker/cluster.js
for (let i = 0; i < cpus.length; i++) {
  cluster.fork();
}
cluster.on("exit", function (worker) {
  console.log("worker err:", worker.process.pid);
  cluster.fork();
});

2.4 pm2

PM2模块是cluster模块的一个包装层。它的作用是尽量将cluster模块抽象掉,让用户像使用单进程一样,部署多进程Node应用。

pm2: process manage

专门开启/重启进程
pm2 可以指定开启几个进程,挂了可以重启
后台执行,不占用命令行窗口

  • pm2-server.js
const http = require("http");
http
  .createServer((req, res) => {
    res.end(process.pid + ": end");
    // 在集群的环境下可以监听同一个端口
  })
  .listen(4000);

上面代码是标准的Node架设Web服务器的方式,然后用PM2从命令行启动这段代码。

pm2 start app.js -i 4

上面代码的i参数告诉PM2,这段代码应该在cluster_mode启动,且新建worker进程的数量是4个。如果i参数的值是0,那么当前机器有几个CPU内核,PM2就会启动几个worker进程。

如果一个worker进程由于某种原因挂掉了,会立刻重启该worker进程。

正常情况下,PM2采用fork模式新建worker进程,即主进程fork自身,产生一个worker进程。pm2 reload命令则会用spawn方式启动,即一个接一个启动worker进程,一个新的worker启动成功,再杀死一个旧的worker进程。采用这种方式,重新部署新版本时,服务器就不会中断服务。类似于k8s中的滚动更新

pm2 reload <脚本文件名>

pm2常用命令

pm2 start xx.js // 启动
pm2 restart xx.js // 重启
pm2 stop xx.js // 停止xx.js 进程
pm2 start xx.js --name my // 启动进程,并给这个进程一个名字
pm2 reload all // 重启所有worker进程
pm2 delete pid/name // 删除这个进程,找不到了,无法重启
pm2 kill pid // 默认删除所有进程,可指定进程
pm2 list // 查看所有进程
pm2 status // 查看进程状态
pm2 show <worker id> // 查看单个worker进程的详情


pm2 logs // 查看日志
pm2 logs 会自定管理日志
	.pm2/logs/8.pm2-server-out.log last 15 lines:
	.pm2/logs/8.pm2-server-error.log last 15 lines:
pm2 init // 初始化pm2 配置文件
pm2文档 https://pm2.keymetrics.io/docs/usage/quick-start/

pm2 start npm -- run dev // 用pm2 启动 npm 命令, 参数是 run dev

pm2配置文件

  • pm2-config.js
module.exports = {
    apps: [
      {
        name: "my1", // 进程别名
        script: "8.pm2-server.js", // 执行脚本
        watch: ".", // 监控的文件目录
        args: "paramsA paramsB",
        instances: "max", // 其多少个进程
        autorestart: true, // 自动重启
        watch: true, // 监控改变
        max_memory_restart: "1G", // 重启最大内存
        env: {
          // 开发和生产的环境变量
          NODE_ENV: "development",
        },
        env_production: {
          NODE_ENV: "production",
        },
      },
      // {
      //   script: "./service-worker/",
      //   watch: ["./service-worker"],
      // },
    ],
  
    deploy: {
      // 发布 这个功能用的比较少,一般用的docker
      // 如果直接发布,可以用pm2 发布,
      production: {
        user: "root",
        host: "39.106.12.146",
        ref: "origin/master",
        repo: "https://github.com/wangjainxi/clust-pro1.git",
        path: "/home", // 放到服务器的路径
        "pre-deploy-local": "",
        // 发布的时候需要npm install 安装依赖, 并通过配置文件重启,环境变量是production
        "post-deploy":
          "npm install && pm2 reload ecosystem.config.js --env production",
        "pre-setup": "",
      },
      development: {
        // xxx 同上,可以区分开发 测试 生产不同环境;走配置文件中的环境变量
        "post-deploy":
          "npm install && pm2 reload ecosystem.config.js --env development",
      },
    },
  };