10月阅读周·JavaScript异步编程设计快速响应的网络应用:cluster带来的Node版worker
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读九个月。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》、《JavaScript权威指南》。
当前阅读周书籍:《JavaScript异步编程设计快速响应的网络应用》。
cluster带来的Node版worker
在Node的早期阶段,有很多API竞相抢食多线程这块肥肉,其中大多数 API的实现都很蠢笨,不断要求用户搞出多个服务器实例以监听不同的 TCP端口,然后再通过代理回钩到真正的端口。直到发行了 0.6版本,才推出了一个支持多个进程绑定至同一端口的标准 API:cluster(群集)。
通常情况下,会为了追求最佳性能而使用cluster按每颗CPU内核分化出一个进程(尽管每个进程是否能真正得到属于自己的内核仍完全取决于底层的操作系统)。
Multithreading/cluster.js
var cluster = require('cluster');
if (cluster.isMaster) {
// 分化出worker 对象
var coreCount = require('os').cpus().length;
for (var i = 0; i < coreCount; i++) {
cluster.fork();
}
// 绑定death 事件
cluster.on('death', function (worker) {
console.log('Worker ' + worker.pid + ' has died');
});
} else {
// 立即死去
process.exit();
}
输出如下:
Worker 15330 has died
Worker 15332 has died
Worker 15329 has died
Worker 15331 has died
其中每行输出对应一颗CPU内核。
这段代码看似莫名其妙,其玄妙之处在于,网页版 worker 对象会加载一个独立的脚本,而Node版worker对象则由cluster.fork()把运行自己的同一个脚本再次加载成一个独立的进程。运行中的脚本要想知道自己是主进程还是 worker 对象,唯一的办法就是检查cluster.isMaster。
为什么做出这样的设计决策呢?因为Node版多线程技术的使用情况与浏览器端有很大不同。浏览器可以将任意多余的线程降格为后台任务,而Node服务器则要留出计算资源以保障其主要任务:处理请求。
(使用 child_process.fork也可以将外部脚本加载为独立的进程来运行。除了子进程不能分享TCP端口之外,child_process.fork的功能几乎和 cluster.fork 完全一样,事实上cluster 内在里就使用了child_process。)
Node版worker的交互接口
和网页版worker对象一样,cluster分化出的worker对象通过发送消息事件来与主进程交流,反之亦然。不过,这里的API稍有不同。
Multithreading/clusterMessage.js
var cluster = require('cluster');
if (cluster.isMaster) {
// 分化worker 对象
var coreCount = require('os').cpus().length;
for (var i = 0; i < coreCount; i++) {
var worker = cluster.fork();
worker.send('Hello, Worker!');
worker.on('message', function (message) {
if (message._queryId) return;
console.log(message);
});
}
} else {
process.send('Hello, main process!');
process.on('message', function (message) {
console.log(message);
});
}
输出如下:
Hello, main process!
Hello, main process!
Hello, Worker!
Hello, Worker!
Hello, main process!
Hello, Worker!
Hello, main process!
Hello, Worker!
这里的输出次序是不可预知的,因为每个线程都竞相抢占console.log。(大家必须用Ctrl+C快捷键手动结束此进程。)
和网页版worker一样,Node版worker对象的API也是对称的,此端的send调用会触发彼端的'message'事件。但需要注意的是,send的参数(更确切的说法是参数的序列化副本)直接由'message'事件指定,而不是附加作为事件的data属性。
注意到主消息处理器的这行代码了吗?
if (message._queryId) return;
Node有时会基于worker对象发送自己的消息,发送命令始终形如:
{ cmd:'online', _queryId:1, _workerId:1 }
忽略这些内部消息也没什么危险,但要知道这些消息会在后台施展一些重要的魔法。其中最著名的魔法是:多个 worker 对象试图监听(listen)一个TCP端口时,Node利用内部消息来允许分享该端口。
Node版worker对象的局限性
大多数情况下,cluster对象和网页版worker对象遵守同样的规则:有一个主线程和多个 worker 线程,它们之间的交流基于一些带有序列化对象或附连字符串的事件。不过,网页版 worker 在浏览器端显然是二等公民,而Node版worker却几乎拥有主线程的所有权利和权限,但以下这些除外(但不限于这些):
- 关停应用程序的能力;
- 孵化出更多worker对象的能力;
- 彼此交流的能力。
这使得主线程必须承担起作为所有线程间通信之中转中心的重任。幸运的是,这种不便可以通过像Roly Fentanes之Clusterhub这样的库抽象出去。
总结
worker对象为何成为Node的一个有机组成部分,以及它允许服务器充分利用多颗内核而无需运行多个应用实例。Node API之cluster 支持并发运行同一个脚本(一个主进程和任意多个worker进程)。为了尽可能减少线程间通信的开销,线程间分享的状态应该存储在像Redis这样的外部数据库中。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)