深入浅出nodejs(玩转进程)

多进程构架

面对单进程单线程对多核使用不足问题,前人经验是启动多个进程,理想状态下每个进程各自利用一个cpu,以此实现多核cpu的利用,node 提供child_process 模块,利用fork函数实现进程的复制。
例如

// worker.js 文件
var http = require('http')
http.createServer(function (req, res) {
  res.writeHead(200, {'Content.type': 'text/plain'})
  res.end('hello word')
}).listen(Math.round((1 + Math.random()) * 1000), '127.0.0.1')

// master.js 文件
var fork = require('child_process').fork
var cpus = require('os').cpus()
for (var i = 0; i < cpus.length; i++) {
  fork('./worker.js')
}

这段代码会根据当前机器的cpu数量启动对应数量的node进程
下图著名的master-worker模式(主从模式)

WechatIMG7.jpeg

尽管Node提供了fork()供我们复制进程使每个cpu都用上,但是依然要切记fork()进程是昂贵的,node通过事件驱动的方式在单线程上解决了高并发问题,这里启动多个进程只是为了充分利用cpu资源,而不是为了解决并发问题

创建子进程

child_process模块提供了4个方法创建子进程

  • spawn() 启动一个子进程来执行命令。例如 cp.spawn('node', 'worker.js')
  • exec() 启动一个子进程来执行命令,与spawn不同的是,它有一个回调函数来获知子进程的状况。例如cp.exec('node worker.js', function(){})
  • execFile() 启动一个子进程来执行可执行文件。例如cp.execFile('worker.js', function() {})
  • fork() 需要指定可执行文件的路径。例如cp.fork('./worker.js')

进程间通信

主进程和工作进程之间通过onmessage()和postMessage()进行通信,由send()方法实现发送数据,message事件实现监听发来的数据。例如

// parent.js
var cp = require('child_process')
var n = cp.fork('./sub.js')
n.on('message', function (data){
  console.log(data)
})
n.send('lalalala')
// sub.js
process.on('message', function () {})
process.send('hahahahah')
进程通信原理

通过fork()或其他api创建子进程后,为了实现父子进程通信,父子进程之间会创建IPC通道,通过通道,父子进程之间才能通过message和send()传递信息。
IPC即进程间的通信,由libuv提供。
如图


WechatIMG9.jpeg

父进程在实际创建子进程之前,会创建IPC通道并监听它,然后才真正创建子进程,并通过环境变量告诉子进程这个IPC通道的文件描述符,子进程在启动时根据文件描述符连接这个已经存在的IPC通道。

句柄传递

当启动子进程时无法监听同一个端口
发送句柄解决这个问题
代码如下

// parent.js
var cp = require('child_process')
var child1 = cp.fork('./child.js')
var child2 = cp.fork('./child.js')
var server = require('net').createServer()
server.listen(1234, function () {
  child1.send('server', server)
  child2.send('server', server)
  server.colse()
})
// child.js
var cp = require('child_process')
var child1 = cp.fork('./child.js')
var child2 = cp.fork('./child.js')
var server = require('net').createServer()
server.listen(8081, function (req, res) {
  child1.send('server', server, req)
  child2.send('server', server, req)
  server.close()
})
process.on('message', function (m, tcp) {
  if (m === 'server') {
    tcp.on('connection', function (socket) {
        server.emit('connection', socket,)
    })
  }
})

发送到IPC管道中的实际上是我们要发送的句柄的管道描述符,这个message对象在写入IPC管道时也会通过JSON.stringfly() 进行序列化,所以最终发送到IPC管道中的信息都是字符串。
连接了IPC通道的子进程可以读取到父进程发来的消息,将字符串JSON.parse() 还原为对象后,才触发message事件将消息体传递给应用层使用,并和得到的文件描述符一起还原对应的对象。目前node只支持特定的几种类型的句柄。

监听共同的端口

我们在独立启动的进程中,TCP服务端的socket套接字的文件描述符并不相同,导致监听相同的端口会抛出异常。
但对于发送句柄还原出来的服务而言,它们的描述符是相同的,所以可以监听相同的端口,当多个进程监听相同端口时,文件描述符同一时间只能被某个进程应用,也就是说这些进程服务是抢占式的。

进程的稳定

除了message事件外,还有如下事件

  • error : 当子进程无法被创建、无法被杀死、无法发送消息时触发
  • exit : 子进程退出时触发。如果是子进程是正常退出事件的第一个参数为退出码,如果非正常退出则为null. 如果通过kill杀死,则会有第二个参数,为杀死进程时的信号
  • close: 在子进程的标准输入输出流终止时触发
  • disconnect: 在父进程或子进程中调用disconect()方法触发,disconnect()方法关闭监听IPC通道
自动重启
// master.js
var fork = require('child_process').fork
var cpus = require('os').cups()
var server = require('net').createrServer()
server.listen(1337)
var workers = {}
var createWorker = function () {
  var worker = fork(__dirname + 'worker.js')
  // 退出是重启新的进程
  worker.on('exit', function () {
    console.log('worker' + worker.pid + 'exited')
    delete workers[worker.pid]
    createrWorker()
  })
  // 句柄发送
  worker.send('server', server)
  workers[worker.pid] = worker
}
for (var i = 0; i < cpus.length; i++) {
  createrWorker()
}
// 进行自己退出时让所有工作进程退出
process.on('exit', function () {
   for (var pid in workers) {
      workers[pid].kill()
   }
})
// worker.js
var http = require('http')
var server = http.createrServer(function (req, res) {
  res.writeHead(200, {'Content-type': 'text/plain'})
  res.end('lalalalala')
})
var worker;
process.on('message', function (m, tcp) {
  if (m === 'server') {
    worker = tcp;
    worker.on('connection', function (socket){
      server.emit('connection', socket)
    })
  }
})
// 监听未捕获的异常
process.on('uncaughtException', function () {
  // 停止接收新的连接,等待所有连接都断开后触发exit()
  worker.close(function () {
    process.exit(1)   // 1 未捕获的致命异常
  })
})

自杀信号
在极端情况下,所有的工作进程都停止接收新的连接,全都处于等待退出的状态,但在等到进程完全退出才重启的过程中有可能存在没有进程为新用户服务的情景。
解决方式:

// worker.js
// 监听未捕获的异常
process.on('uncaughtException', function () {
  // 发送自杀信号
  process.send({act: 'suicide'})
  // 停止接收新的连接,等待所有连接都断开后触发exit()
  worker.close(function () {
    process.exit(1)   // 1 未捕获的致命异常
  })
})
// master.js
var fork = require('child_process').fork
var cpus = require('os').cups()
var server = require('net').createrServer()
server.listen(1337)
var workers = {}
var createWorker = function () {
  var worker = fork(__dirname + 'worker.js')
  worker.on('message', function (m) {
    if (m.cat === 'suicide') {
        createrWorker()
    }
  })
  // 退出是重启新的进程
  worker.on('exit', function () {
    console.log('worker' + worker.pid + 'exited')
    delete workers[worker.pid]
    //createrWorker()
  })
  // 句柄发送
  worker.send('server', server)
  workers[worker.pid] = worker
}
for (var i = 0; i < cpus.length; i++) {
  createrWorker()
}
// 进行自己退出时让所有工作进程退出
process.on('exit', function () {
   for (var pid in workers) {
      workers[pid].kill()
   }
})

对套接字的理解

socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄。应用程
序通常通过"套接字"向网络发出请求或者应答网络请求。

要通过互联网进行通信,你至少需要一对套接字,其中一个运行于客户机端,我们称之为
ClientSocket,另一个运行于服务器端,我们称之为ServerSocket。根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个 步骤:

  • 服务器监听
  • 客户端请求
  • 连接确认。

服务器监听 是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的
状态,实时监控网络状态。
客户端请求 是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接
字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的
地址和端口号,然后就向服务器端套接字提出连接请求。
连接确认 是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它
就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦
客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他
客户端套接字的连接请求。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容