这第九章也是个大重点
九、玩转进程
Node在选型时决定在V8引擎之上构建,也就意味着它的模型与浏览器类似。
本章关于进程的介绍和讨论将会解决如下两个问题:
- 单进程单线程并非完美,如今CPU基本均是多核的,真正的服务器(非VPS)往往还有多个CPU。——如何充分利用多核CPU服务器?
- 由于Node执行在单线程上,一旦单线程上抛出的异常没有捕获,将会引起整个进程的崩溃。——如何保证进程的健壮性和稳定性?
9.1 服务模型的变迁
9.1.1 石器时代:同步
9.1.2 青铜时代:复制进程
9.1.3 白银时代:多进程
9.1.4 黄金时代:事件驱动
9.2 多进程架构
面对单进程单线程对多核使用不足的问题,前人的经验是启动多进程即可。理想状态下每个进程各自利用一个CPU,以此实现多核CPU的利用。
Node提供了 child_process 模块,并且也提供了 child_process.fork()函数实现进程复制。
9.2.1 创建子进程
child_process 模块提供了4个方法用于创建子进程。
spawn(); // 启动一个子进程来执行命令。
exec(); // 启动一个子进程来执行命令,与spawn()不同的是其接口不同,它有一个回调函数获知子进程的状况。
execFile(); // 启动一个子进程来执行可执行文件。
fork(); // 与spawn()类似,不同点在于它创建的Node的子进程只需指定要执行的JavaScript文件模块即可。
- spawn()、exec()、execFile()不同点:后两者创建时可以指定timeout属性设置超时时间,一旦创建的进程超过设定的时间将会被杀死。
这里的可执行文件是指可以直接执行的文件,如果是JavaScript文件通过execFile()运行,它的首行内容必须添加如下代码:
#!/usr/bin/env node
尽管4种创建子进程的方式有些差别,但事实上后面三种方法都是spawn()的延伸应用。
9.2.2 进程间的通信
-
创建子进程后为了实现父子进程之间的通信,父与子之间会创建IPC通道,通过IPC通道父子进程之间才能通过
message
和send()
传递消息。 -
IPC(Inter-Process Communication)进程间的通信。实现进程间通信的技术也有很多,如:命名管道、匿名管道、socket、信号量、共享内存、消息队列、Domain Socket等。Node中实现IPC通道的是管道(pipe)技术。在Node中管道只是个抽象层面的称呼,具体实现由 libuv 提供,在Windows下有命名管道(named pipe)实现,*nix系统则采用Unix Domain Socket实现,表现在应用层上的进程间通信只有简单的 message 事件和 send() 方法。
-
与网络socket的行为比较类似,属于双向通信。不同的是他们在系统内核中就完成了进程间的通信,而不用经过实际的网络层,非常高效。
-
注意:只有启动的子进程是Node进程时,子进程才会根据环境变量去连接IPC通道,对于其他类型的子进程无法实现进程间的通信,除非其他进程也按约定去连接这个已经创建好的IPC通道。
9.2.3 句柄传递
send(message, [sendHandle])
方法除了能通过IPC发送数据外,还能发送句柄,第二个可选参数就是句柄。
什么是句柄?一种可以用来标识资源的引用,它的内部包含了指向对象的文件描述符。比如句柄可以用来标识一个服务器端socket对象、一个客户端socket对象、一个UDP套接字、一个管道等。P252
发送句柄意味着什么?在前一个问题中,我们可以去掉代理这种方案,使主进程接收到 socket 请求后,将这个socket直接发送给工作进程,而不是重新与工作进程之间建立新的socket连接来转发数据。文件描述符浪费的问题可以通过这样的方式轻松解决。P253
1、句柄发送与还原
2、端口共同监听
9.2.4 小结
至此,我们介绍了创建子进程、进程间通信的IPC通道实现、句柄在进程间的发送和还原、端口共用等细节。通过这些基础技术,用child_process模块在单机上搭建Node集群是件相当容易的事情。因此在多核CPU的环境下让Node进程能够充分利用资源不再是难题。
9.3 集群稳定之路
- 性能问题。
- 多个工作进程的存活状态管理。
- 工作进程的平滑重启。
- 配置或者静态数据的动态重新载入。
- 其他细节。
9.3.1 进程事件 P258
9.3.2 自动重启 P259
9.3.3 负载均衡 P264
9.3.4 状态共享 P265
9.4 Cluster 模块
前文介绍了child process模块中的大多数细节,以及如何通过这个模块构建强大的单机集群。如果熟知Node,也许你会惊讶为何迟迟不谈cluster模块。上述提及的问题,Node在v0.8版本时新增的cluster模块就能解决。在v0.8版本之前,实现多进程架构必须通过child process来实现,要创建单机Node集群,由于有这么多细节需要处理,对普通工程师而言是一件相对较难的工作,于是v0.8时直接引入了cluter模块,用以解决多核CPU的利用率问题,同时也提供了较完善的API,用以处理进程的健壮性问题。
P267
9.4.1 Cluster 工作原理
事实上cluster模块就是child process和net模块的组合应用。cluster启动时,如同我们在9.2.3节里的代码一样,它会在内部启动TCP服务器,在cluster.fork()子进程时,将这个TCP服务器端socket的文件描述符发送给工作进程。如果进程是通过cluster.fork()复制出来的,那么它的环境变量里就存在NODE_UNIOUE_ID如果作进中存在listen()侦听网络端口的调用,它将拿到该文件描述符,通过SO_REUSEADDR端口重用,从而实现多个子进程共享端口。对于普通方式启动的进程,则不存在文件描述符传递共享等事情。
在cluster内部隐式创建TCP服务器的方式对使用者来说十分透明,但也正是这种方式使得它无法如直接使用child_process那样灵活。在cluster模块应用中,一个主进程只能管理一组工作进程,如下图所示。( 书中P268)
对于自行通过child process来操作时,则可以更灵活地控制工作进程,甚至控制多组工作进程。其原因在于自行通过child process操作子进程时,可以隐式地创建多个TCP服务器使得子进程可以共享多个的服务器端socket。
9.4.2 Cluster 事件
对于健壮性处理,cluster模块也暴露了相当多的事件。
- fork:复制一个工作进程后触发该事件。
- online:复制好一个工作进程后,工作进程主动发送一条nline消息给主进程,主进程收到消息后,触发该事件。
- listening:工作进程中调用listen()(共享了服务器端Socket)后,发送一条listening消息给主进程,主进程收到消息后,触发该事件。
- disconnect:主进程和工作进程之间IPC通道断开后会触发该事件。
- exit:有工作进程退出时触发该事件。
- setup:cluster.setupMaster()执行后触发该事件。
这些事件大多跟child process模块的事件相关,在进程间消息传递的基础上完成的封装这些事件对于增强应用的健壮性已经足够了。
9.5 总结
尽管Node从单线程的角度来讲它有够脆弱的:既不能充分利用多核CPU资源,稳定性也无法得到保障。但是群体的力量是强大的,通过简单的主从模式,就可以将应用的质量提升一个档次。在实际的复杂业务中,我们可能要启动很多子进程来处理任务,结构甚至远比主从模式复杂,但是每个子进程应当是简单到只做好一件事,然后通过进程间通信技术将它们连接起来即可。这符合Unix的设计理念,每个进程只做一件事,并做好一件事,将复杂分解为简单,将简单组合成强大。
尽管通过child_process模块可以大幅提升Node的稳定性,但是一旦主进程出现问题所有子进程将会失去管理。在Node的进程管理之外,还需要用监听进程数量或监听日志的方式确保整个系统的稳定性,即使主进程出错退出,也能及时得到监控警报,使得开发者可以及时处理故障。
个人心得:这章学得我脑壳痛,下次再重新学一遍然。