sensai2.0

322
15

这篇依旧是《深入浅出Node.js》进程一章的阅读笔记

序. Node.js广泛应用于Webserver的构建中,由于其单进程单线程的缘故,单实例在多核CPU的server中无法有效利用运算资源,所以往往部署时都是根据核数起相应的进程数。


1. 监听UnixDomainSocket

之前一直不知道,后来研究Node.js cluster模块的时候,无意发现了net.Server的listen方法支持监听unix domain socket。那么根据这个思路,可以实现一个基于unix domain socket的“伪http”服务器。

代码如下:

#!/usr/bin/env node                               
const http = require('http')                      
const path = require('path')                      
                                                  
const server = http.createServer((req, res) => {  
  res.end('Hello World')                          
})                                                
                                                  
server.listen(path.join(__dirname, 'server.sock'))
process.on('SIGINT', () => {                      
  console.log('process exit!\n')                  
  server.close()                                  
})                                                
因为管道文件不像TCP管道那样自动关闭并释放file descriptor,所以使用unix domain socket监听HTTP时一定需要记得在进程结束时手动调用server.close()

用curl进行测试的时候需要加上--unix-socket ./server.sock


各种资料文档都声称unix domain socket(简称uds)的传输速度比tcp要快出许多,唯一的问题就是不能跨机器通信。

如果前端机nginx和业务机node在同一台机器上的话,从理论上讲nodejs监听uds要比监听本地某端口的效率来的高。


2. Cluster模式

http://sensai.powerpigger.cc/sensai/site/article/319

之前写的一篇文章主要就是分析了Cluster模式的实现原理,并通过Node.js的child_process模块简单的实现了一个cluster模式的多进程Node server。

Cluster模式的核心就是多个进程共享同一个server socket。


2.1 Node下的基本实现

Node下提供了Cluster模块用以实现cluster模式的多进程server,代码如下:

#!/usr/bin/env node
const cluster = require('cluster')
const http = require('http')
const os = require('os')

const cpus = os.cpus().length

if (cluster.isMaster) {
  for (let i = 0; i < cpus; i++) {
    cluster.fork()
  }
} else {
  http.createServer((req, res) => {
    res.writeHead('200', {'Content-Type': 'text/plain'})
    res.end(`handled by ${process.pid}\n`)
  }).listen('8080')
}


cluster模块比之前一文所用的方式更加抽象更加反人类,看上去更像是开了4个server。

当然实质上只有一个server socket


2.2  群现象与Round-Robin算法

先前child_process实现的多进程server有一个问题,即socket文件描述符被多个子进程同时监听,那么当新的连接到达时,多个子进程会同时被唤醒,os会频繁切换上下文,然而由于socket listen状态的原子性,最终只有一个进程最终能够取得连接,那么剩下的N-1个进程被唤醒就是白白浪费了cpu资源(进程唤醒、os切换上下文等cpu时间片),这种情况叫做“惊群现象”。

和之前一篇文章用内置child_process实现的多进程server略有不同,cluster模块实现的多进程server内部采用了round-robin算法来避免“惊群现象”

下面一图截自js层的源代码,可以看到在cluster子进程有两种socket处理模式:round-robin和shared listen socket模式


在类unix系统中,默认使用的是round-robin的算法

与shared listen socket不同,在round-robin模式下,只有父进程监听socket,在连接建立后主进程通过与子进程建立的unix domain socket连接将连接句柄发给子进程,每个子进程依次排队领取连接句柄(这就是称作round-robin的原因)。


2.3 cluster模块的局限性

与子进程共享监听server socket相比,round-robin避免了惊群模式,但是父子进程之前需要维护一个ipc管道(nodejs unix下是unix domain socket, windows下是named pipe)

node.js cluster模块的局限性在于unix domain socket传递socket句柄的实现上,由于发送消息和句柄序列化这一块逻辑是在js层实现的,所以cluster模块在多连接高并发的场合有一定的性能问题。


3. Fork模式

与cluster模式相比,fork模式就非常简单粗暴了:每个子进程监听一个socket文件描述符,各个子进程之前互不干扰,父子进程之前也不再有过多的耦合和通信。

这样父子进程之前甚至可以不需要ipc通信,父进程可以直接使用signal来管理子进程的开关和重启。

以下代码实现了一个fork mode多进程server:

// master.js                                                  
#!/usr/bin/env node                                           
const cp = require('child_process')                          
const os = require('os')                                                                                 
for (let i = 0, cpus = os.cpus().length; i < cpus; i++) {     
  cp.fork('./worker.js').send({port: 8080 + Number.parseInt(i)})
  console.log(`${i} forked!`)                                 
}                                                               
                                                                
// slave.js                                               
const http = require('http')                                                                               
console.log(`set up pid: ${process.pid}`)                                                                                   
process.on('message', message => {
  console.log(message) 
  http.createServer((req, res) => { 
    res.end(`Hello world!<br>from: ${process.pid}`)
  }).listen(message.port)  
})                                                              
父进程fork了4个子进程,并且初始化时sendMessage给子进程4个监听的端口8080~8083

当然fork模式的问题在于没有一个统一的出口端口,所以必须需要前级进行反向代理——当然在生产环境中,前端机是必不可少的。


4. Fork模式与Cluster模式的结合

所以最优的模式就是结合Fork与Cluster模式:用nginx前端机实现cluster与load-balance,所有的node业务机全部使用fork模式。

如果前端机与node在同一个节点上,那么node可以监听unix domain socket来避免反向代理时tcp三次握手的过程从而提高性能(如开头的写法)。

前端机的upstream模块配置代码如下:

# upstream cluster                                
upstream backend {                                
    server unix:/tmp/server-1.sock;               
    server unix:/tmp/server-2.sock;               
    server unix:/tmp/server-3.sock;               
    server unix:/tmp/server-4.sock;               
}                                                 
                                                  
# proxy pass to unix domain socket in fork mode   
server {                                          
    listen       8009;                            
    server_name  localhost;                       
                                                  
    location / {                                  
        proxy_set_header Host      $host;         
        proxy_set_header X-Real-IP $remote_addr;  
        proxy_pass http://backend;                
    }                                             
}                                                 
而node层可以使用pm2来开启fork模式,pm2配置json代码如下:

// ecosystem.config.js               
module.exports = {                 
  apps : [{                        
      name      : 'node-process-1',
      script    : 'server.js',     
      env: { INDEX: '1' }          
    }, {                           
      name      : 'node-process-2',
      script    : 'server.js',     
      env: { INDEX: '2' }          
    }, {                           
      name      : 'node-process-3',
      script    : 'server.js',     
      env: { INDEX: '3' }          
    }, {                           
      name      : 'node-process-4',
      script    : 'server.js',     
      env: { INDEX: '4' }          
    }                              
  ]                                
}                                  
使用 pm2 start ecosystem.config.js 来以fork mode启动server,并且可以将监听对象放置在env环境变量中,最终业务机代码可以这样写:

const http = require('http')                             
const path = require('path')                             
const fs = require('fs')                                 
                                                         
const server = http.createServer((req, res) => {         
  res.end('Hello World')                                 
})                                                       
                                                         
const index = process.env.INDEX                          
const udsPath = path.join('/tmp', `server-${index}.sock`)
                                                         
server.listen(udsPath, () => {                           
  fs.chmodSync(udsPath, '777')                           
})                                                       
                                                         
process.on('SIGINT', () => {                             
  console.log('process exit!\n')                         
  server.close()                                         
})                                                       
最终多进程server架构的形态如下:




回复

对话列表

×