WdBly Blog

懂事、有趣、保持理智

周维的个人Blog

懂事、有趣、保持理智

站点概览

周维 | Jim

603927378@qq.com

推荐阅读

Node中的多进程

Node中的多进程

Node的模型架构与浏览器类似。我们的JavaScript代码将会运行在单个进程的单个线程上。

它带来的好处是:程序状态是单一的,在没有多线程的情况下没有锁、线程同步问题,操作系统在调度时也因为较少上下文的切换,可以很好地提高CPU的使用率

传统多线程架构

同步问题

多线程架构的同步问题:当一套代码在多个线程中执行时,不可避免的要处理各个线程对资源有共享的情况, 这些资源包括变量内存资源,文件,打印机外边设备等。这个时候就需要线程同步

当一个线程对共享资源进行读写时,阻止其它线程对资源进行读写的操作即为加锁,下面列出三种加锁方法。

  • 互斥锁:线程同步在java中其实是将一段代码声明为原子操作,用synchronized声明,对于原子操作,只能同时允许一个线程执行,失败则会进行回退(和数据库的原子性类似)。当有线程在执行此代码时,其它线程会阻塞并等待。

  • 可重入锁: 某些递归函数中操作了共享资源,如果直接声明为互斥锁。则会导致死锁问题。这是因为每次递归调用时都会申请锁导致的。可重入锁可以在每次递归调用自身时使用已经获得的锁。

  • 读写锁: 读写锁的特定是多个线程可以同时读,但是同一时间只能有一个线程写,且写的优先级较高,当有线程写时, 其它线程无法读。

多线程和单线程对比

  • CPU单位时间只能处理一个线程,多线程的架构不可避免需要线程切换,从而带来开销。且线程越多开销越大,CPU的真实使用率降低。

  • Node虽然也是多线程,但是线程总是有限,切换开销小,CPU使用率高,但是在多核CPU中的利用率低。

  • 单线程的容灾问题,一旦单线程上抛出的异常没有被捕获,将会引起整个进程的崩溃,这是Node需要解决的问题。

Node多进程+单线程模式

为了解决多核CPU利用率不足问题,Node提供了child_process模块实现子进程。

child_process子进程

  • spawn():启动一个子进程来执行命令。

  • exec():启动一个子进程来执行命令,与spawn()不同的是其接口不同,它有一个回调函数获知子进程的状况。

  • execFile():启动一个子进程来执行可执行文件。

  • fork():与spawn()类似,不同点在于它创建Node的子进程只需指定要执行的JavaScript文件模块即可。

使用如下:

var cp = require('child_process'); cp.spawn('node', ['worker.js']); cp.exec('node worker.js', function (err, stdout, stderr) { // some code }); cp.execFile('worker.js', function (err, stdout, stderr) { // some code }); cp.fork('./worker.js');

image.png

通过child_process启动子进程的工作方式符合master-worker的工作模式。
image.png

多进程模式同样会有的问题是,内存资源无法共享,如果要使用内存作为缓存,那么每个进程下都会维护一套缓存,非常浪费内存资源。

利用三方缓存库redismemcache可以实现多进程共享资源。

当可以共享资源后,又会带来一个和多线程类似的同步问题,redismemcache也会通过的机制来解决。

进程间通信

操作系统级进程间通信IPC(Inter-Process Communication)

在系统级,进程间通信(IPC)的方式有很多,如命名管道、匿名管道、socket、信号量、共享内存、消息队列、Domain Socket等,在Node中如下图所示。

image.png

在Node表现层使用IPC如下:

// parent.js var cp = require('child_process'); var n = cp.fork(__dirname + '/sub.js'); n.on('message', function (m) { console.log('PARENT got message:', m); }); n.send({hello: 'world'}); // sub.js process.on('message', function (m) { console.log('CHILD got message:', m); }); process.send({foo: 'bar'});

Node创建子进程之前,首先创建一个IPC通道并获得此通道的描述符。然后创建子进程并将IPC描述符传递给子进程。子进程最后通过连接该描述符对应的IPC管道从而完成进程间通信。

image.png

PS: 只有当子进程为Node的进程时才能连接IPC管道实现进程间通信

通过IPC发送句柄实现负载均衡

Nginx可以通过upstream配置负载均衡,将所有请求转发到本机其它进程或者其它集群服务器中进行处理。

在Node中也可通过IPC转发句柄的方式实现小型负载均衡。
image.png

管理子进程

  • Node的进程提供了状态事件管理进程: error, exit, close, disconect,通过这些事件,可以在主进程实现子进程出现错误自动重启功能,提升了Node应用的健壮性。
// 简单的自动重启 // master.js var fork = require('child_process').fork; var cpus = require('os').cpus(); var server = require('net').createServer(); 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]; createWorker(); }); // 句柄转发 worker.send('server', server); workers[worker.pid] = worker; console.log('Create worker. pid: ' + worker.pid); }; for (var i = 0; i < cpus.length; i++) { createWorker(); } // 进程自己退出时,让所有工作进程退出 process.on('exit', function () { for (var pid in workers) { workers[pid].kill(); } });

当然,进程的退出与重启并非这么简单,需要考虑的情况较多,如果进程异常需要退出,第一步应该断开新连接,等待已有连接退出后,再退出进程。

但是如果在退出进程后再去启动新进程接收用户连接,那么不可避免的在这段时间内会丢失部分连接。

可靠的方案是在进程异常后,马上开启一个新进程用户处理用户连接,异常进程处理完已有连接后自动退出。 这样就保证了整个Node应用的健壮性。

当我们的程序代码出错时,上述机制则会无线重启进程,可能把服务器搞崩溃,那么就需要一个限制重启的机制来处理这种情况。

资源共享

在Node中如何结合redis和memcache在进程间共享数据?

Cluster模块

为了解决上述使用child_process搭建集群带来的复杂度。Node提供Cluster模块,其底层使用的正是child_process模块。

// cluster.js var cluster = require('cluster'); cluster.setupMaster({ exec: "worker.js" }); var cpus = require('os').cpus(); for (var i = 0; i < cpus.length; i++) { cluster.fork(); }

参考资源:
《深入浅出Node.js》

提交

全部评论0

暂时没有评论...