10月阅读周·JavaScript异步编程设计快速响应的网络应用:cluster带来的Node版worker

举报
叶一一 发表于 2024/10/22 09:45:54 2024/10/22
【摘要】 背景去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。没有计划的阅读,收效甚微。新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。这个“玩法”虽然常见且板正,但是有效,已经坚持阅读九个月。已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScri...

背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出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畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏️ | 留言📝

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。