|
1 | 1 | # 进程
|
2 | 2 |
|
| 3 | +JS 是单线程执行的,但是我们可以启动多个进程来执行,nodejs 中子进程管理以及进程守候是非常重要的知识点。 |
3 | 4 |
|
| 5 | +----- |
4 | 6 |
|
| 7 | +## 目录 |
| 8 | + |
| 9 | +- 线程 vs 进程 |
| 10 | +- 为何要启用多进程 |
| 11 | +- child_process |
| 12 | +- cluster |
| 13 | + |
| 14 | +---- |
| 15 | + |
| 16 | +## 线程 vs 进程 |
| 17 | + |
| 18 | +**进程(Process)** 是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。 **线程(Thread)** 是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己不拥有系统资源,它与同属一个进程的其他的线程共享进程所拥有的全部资源。 |
| 19 | + |
| 20 | +在 mac os 或者 linux 系统中运行`top`命令,可以看到如下列表,这些就是进程。windows 系统中的任务管理器,可以看到各个启动的软件的列表,也是进程。通过这些列表,我们都能看到每个进程 CPU 使用率,内存占用,符合上文对于进程的描述(独立分配、调度资源) |
| 21 | + |
| 22 | +``` |
| 23 | +PID COMMAND %CPU TIME #TH #WQ #PORT MEM PURG CMPRS PGRP PPID STATE BOOSTS |
| 24 | +12080 top 9.1 00:00.62 1/1 0 23 3672K 0B 0B 12080 12059 running *0[1] |
| 25 | +12059 zsh 0.0 00:00.16 1 0 19 3368K 0B 0B 12059 12058 sleeping *0[1] |
| 26 | +12058 login 0.0 00:00.04 2 1 30 1904K 0B 0B 12058 12057 sleeping *0[9] |
| 27 | +12057 iTerm2 0.0 00:00.03 2 1 30 2364K 0B 0B 12057 945 sleeping *0[1] |
| 28 | +12055 lsof 0.0 00:00.00 1 0 8 232K 0B 0B 12054 12054 sleeping *0[1] |
| 29 | +12054 lsof 0.0 00:00.84 1 0 19 6004K 0B 0B 12054 70 sleeping *0[1] |
| 30 | +12040 QuickLookSat 0.0 00:00.40 5 1 92 10M 232K 0B 12040 1 sleeping 0[12] |
| 31 | +12012 quicklookd 0.0 00:00.61 4 1 90 4716K 216K 0B 12012 1 sleeping 0[14] |
| 32 | +``` |
| 33 | + |
| 34 | +线程是进程中更小的单位,我们无法通过工具直观的看到。一个进程至少启动一个线程,或者启动若干个线程(多线程)。JS 是单线程运行的,我们无法通过 JS 代码新启动一个线程(java 就可以),但是可以新启动一个进程。 |
| 35 | + |
| 36 | +注意,新启动一个进程是比较耗费资源的,不应频繁启动。如果遇到需要频繁启动新进程的需求,应该考虑其他的解决方案(我曾经就遇到过,差点入坑)。 |
| 37 | + |
| 38 | +---- |
| 39 | + |
| 40 | +## 为何要启用多进程 |
| 41 | + |
| 42 | +第一,现在的服务器都是多核 CPU ,**启动多进程可以有效提高 CPU 利用率**,否则 CPU 资源就白白浪费了。一般会根据 CPU 的核数,启动数量相同的进程数。 |
| 43 | + |
| 44 | +> PS:和开发客户端程序不同,开发 server 端程序时,要时刻注意“节省”和“压榨”,通俗一点就是“抠门”。“节省”就是尽量减少计算次数(时间复杂度)、内存使用(空间复杂度);“压榨”就是尽量多的合理利用起现有的资源,CPU、内存和硬盘等。有时你在开发和测试时候,对于“节省”和“压榨”看不出效果,但是一旦上线访问量增大,效果将会越来越明显。 |
| 45 | +
|
| 46 | +第二,受到 v8 引擎的垃圾回收算法的限制,**nodejs 能使用的系统内存是受限制的**(64 位最多使用 1.4GB ,32 位最多使用 0.7GB)。**如何突破这种限制呢?—— 多进程**。因为每个进程都是一个新的 v8 实例,都有权利重新分配、调度资源。 |
| 47 | + |
| 48 | +---- |
| 49 | + |
| 50 | +## child_process |
| 51 | + |
| 52 | +[child_process](http://nodejs.cn/api/child_process.html) 提供了创建子进程的方法 |
| 53 | + |
| 54 | +- `spawn` |
| 55 | +- `exec` |
| 56 | +- `execFile` |
| 57 | +- `fork` |
| 58 | + |
| 59 | +```js |
| 60 | +var cp = require('child_process') |
| 61 | +cp.spawn('node', ['worker.js']) |
| 62 | +cp.exec('node worker.js', function (err, stdout, stderr) { |
| 63 | + // todo |
| 64 | +}) |
| 65 | +cp.execFile('worker.js', function (err, stdout, stderr) { |
| 66 | + // todo |
| 67 | +}) |
| 68 | +cp.fork('./worker.js') |
| 69 | +``` |
| 70 | + |
| 71 | +进程之间的通讯,代码如下。跟前端`WebWorker`类似,使用`on`监听(此前讲过的自定义事件),使用`send`发送。 |
| 72 | + |
| 73 | +```js |
| 74 | +// parent.js |
| 75 | +var cp = require('child_process') |
| 76 | +var n = cp.for('./sub.js') |
| 77 | +n.on('message', function (m) { |
| 78 | + console.log('PARENT got message: ' + m) |
| 79 | +}) |
| 80 | +n.send({hello: 'workd'}) |
| 81 | + |
| 82 | +// sub.js |
| 83 | +process.on('message', function (m) { |
| 84 | + console.log('CHILD got message: ' + m) |
| 85 | +}) |
| 86 | +process.send({foo: 'bar'}) |
| 87 | +``` |
| 88 | + |
| 89 | +--- |
| 90 | + |
| 91 | +## cluster |
| 92 | + |
| 93 | +cluster 模块允许设立一个主进程和若干个 worker 进程,由主进程监控和协调 worker 进程的运行。worker 之间采用进程间通信交换消息,**cluster模块内置一个负载均衡器,采用 Round-robin 算法协调各个 worker 进程之间的负载**。运行时,所有新建立的链接都由主进程完成,然后主进程再把 TCP 连接分配给指定的 worker 进程。 |
| 94 | + |
| 95 | +```js |
| 96 | +const cluster = require('cluster') |
| 97 | +const os = require('os') |
| 98 | +const http = require('http') |
| 99 | + |
| 100 | +if (cluster.isMaster) { |
| 101 | + console.log('是主进程') |
| 102 | + const cpus = os.cpus() // cpu 信息 |
| 103 | + const cpusLength = cpus.length // cpu 核数 |
| 104 | + for (let i = 0; i < cpusLength; i++) { |
| 105 | + // fork() 方法用于新建一个 worker 进程,上下文都复制主进程。只有主进程才能调用这个方法 |
| 106 | + // 该方法返回一个 worker 对象。 |
| 107 | + cluster.fork() |
| 108 | + } |
| 109 | +} else { |
| 110 | + console.log('不是主进程') |
| 111 | + // 运行该 demo 之后,可以运行 top 命令看下 node 的进程数量 |
| 112 | + // 如果电脑是 4 核 CPU ,会生成 4 个子进程,另外还有 1 个主进程,一共 5 个 node 进程 |
| 113 | + // 其中, 4 个子进程受理 http-server |
| 114 | + http.createServer((req, res) => { |
| 115 | + res.writeHead(200) |
| 116 | + res.end('hello world') |
| 117 | + }).listen(8000) // 注意,这里就不会有端口冲突的问题了!!! |
| 118 | +} |
| 119 | +``` |
| 120 | + |
| 121 | +维护进程健壮性,**通过 Cluster 能监听到进程退出,然后自动重启,即自动容错**,这就是进程守候。 |
| 122 | + |
| 123 | +```js |
| 124 | +if (cluster.isMaster) { |
| 125 | + const num = os.cpus().length |
| 126 | + console.log('Master cluster setting up ' + num + ' workers...') |
| 127 | + for (let i = 0; i < num; i++) { |
| 128 | + // 按照 CPU 核数,创建 N 个子进程 |
| 129 | + cluster.fork() |
| 130 | + } |
| 131 | + cluster.on('online', worker => { |
| 132 | + // 监听 workder 进程上线(启动) |
| 133 | + console.log('worker ' + worker.process.pid + ' is online') |
| 134 | + }) |
| 135 | + cluster.on('exit', (worker, code, signal) => { |
| 136 | + // 兼容 workder 进程退出 |
| 137 | + console.log('worker ' + worker.process.pid + ' exited with code: ' + code + ' and signal: ' + signal) |
| 138 | + // 退出一个,即可立即重启一个 |
| 139 | + console.log('starting a new workder') |
| 140 | + cluster.fork() |
| 141 | + }) |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +示例看似简单,但是实际应用还是尽量使用成熟的工具,例如 [pm2](https://www.npmjs.com/package/pm2),可以自己去看文档使用。 |
| 146 | + |
| 147 | +--- |
| 148 | + |
| 149 | +## 总结 |
| 150 | + |
| 151 | +总要明白线程和进程的区别、联系,以及为何使用多进程,后面的 API 用法相对比较简单。 |
0 commit comments