实现Node.js集群

单线程的Node.js

        Node.js的代码是在单线程的环境执行的。单线程环境下编程更简单,没有线程安全问题和线程切换的消耗问题。
但是也因为单线程,它的缺点很明显:1. 无法胜任CPU密集型的任务。2. 无法充分利用服务器的资源。

        单线程的Node.js性能怎么样呢?Node.js在IO密集型的任务情况下高并发表现很不错。Node.js的底层采用了事件循环机制支持异步非阻塞的IO调用。而且其实Node.js底层的libuv、libeio、libev库是有多个工作线程的。时间循环机制模型图:

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

        虽然Node.js单线程的表现不错,但是随着用户的增多,QPS的瓶颈很快就会表现出来。Node.js原生的child_process模块支持我们创建子进程来处理任务,不过使用子进程需要我们对现有项目进行改造,有的情况下需要在进程间通信来获取执行结果,代价似乎有些大了。如果有在有限改造的现有代码的情况下可以充分利用服务器的资源就好了。

Node.js的多进程方案

        1. 原生的cluster模块。cluster模块可以让拥有一个master进程和多个worker进程,并且多个进程监听同一个端口。多个worker进程使用Round Robin策略进行均衡负载。一般情况下,子进程的数量和cpu数量相同,充分利用服务器的核心。同时master进程需要管理调度子进程,如何更好的管理好子进程也是一个问题。

        2. 使用PM2管理。PM2是一个强大的Node.js进程管理工具。它的如下几个特性:

  1. 守护进程,进程异常关闭自动重启。
  2. 负载均衡,监听同一个端口在多个进程间使用Round Robin策略实现负载均衡。
  3. 热重启,使用reload命令,可以实现进程热重启。
  4. 监控,使用monit命令可以方便监控各个进程的情况。

        pm2显然比原生的cluster好用多了,为我们省去了管理子进程的麻烦。cluster和pm2都好用,我们是不是简单改造一下就可以高枕无忧了?我们需要注意这几个坑:

  1. 进程间是不能共享内存的。
  2. 会不会有任务被执行多遍的情况。
  3. 服务是不是无状态的,用户的两次请求交给不同进程处理会不会有问题?

        先看看第2个问题。项目中可能有这样的需求,每到0点统计一下数据,如果每个进程都执行,就有可能导致数据被重复记录。这种情况怎么处理呢? 在cluster和pm2中我们都可以获取到进程的id,指定某一个进程做处理就好了。

        那么第1、3个问题怎么处理呢? 我们可能创建了一个web服务,然后把用户的某个状态保持在内存中,但是当用户发送第二个请求时,请求被交给了别的进程处理,这时候取不到之前的状态就导致了处理出错。面对这种情况,就需要一个缓存服务帮我们处理的数据了。现在应用得最多的就是Redis了,Redis的数据处理速度很快,基本上可以忽略延迟当本地内存来使用了。我们可以把用的session数据存在Redis里,实现一个无状态的服务,可以使用Redis的有序集合实现一个全服的排行榜功能,可以使用发布/订阅功能实现进程间的通信等等,我实在太喜欢Redis了,不过本篇就不过多介绍Redis了。使用Redis可以更好的解决第2个问题,我们可以使用SETNX命令,让多个进程去争抢一个锁,抢到锁的一个进程处理任务就可以了。

Node.js集群

        多进程仅仅是充分利用了某个服务器的资源,那么如果一台服务器扛不住了呢? 如何扩展,实现一个Node.js集群?

        方案1:Nginx均衡负载。我们可以在多台服务器上启动服务,使用Nginx代理实现均衡负载。多台服务器最好在同一个局域网, 通过局域网IP进行反向代理。
        方案2:DNS均衡负载。那么如果性能再次达到瓶颈,需要继续扩展服务器,但是这时nginx也扛不住了怎么办。我们可以使用DNS服务商提供的DNS均衡负载,将流量分发到不同的服务器上。
        方案3:Kubernetes + docker。我们在实际部署过程中第一步就是要为将要运行的服务部署环境,这个步骤漫长而且容易出问题。docker虚拟化容器技术帮我们解决了这个问题,不管你是什么服务,只需要将环境打包进镜像,一次打包后,在其他机器上部署只需要安装虚拟服务即可。我们可以使用Kubernetes管理docker集群,这样pm2工具也可以舍弃了。Kubernetes拥有很多强大的功能,不过我是一个新手,只会打包docker镜像然后交给我们的运维同学去部署。最进在看这本书入门https://yeasy.gitbooks.io/docker_practice/kubernetes/

总结

        实现一个Node.js的关键在于服务是否为无状态,利用强大的Redis,可以像胶水一样,让我们创建出高可用高性能的web服务。自知水平有限,写这篇文章分享我目前的一些心得来抛砖引玉。未来的路很长,学海无涯,与君共勉。

参考:
https://itnext.io/multi-threading-and-multi-process-in-node-js-ffa5bb5cde98
https://codeburst.io/how-node-js-single-thread-mechanism-work-understanding-event-loop-in-nodejs-230f7440b0ea https://yeasy.gitbooks.io/docker_practice/kubernetes/

分享

Author | 何小亮

全栈开发工程师(Node.js,Golang).