Nginx深入详解之多进程网络模型,用共享锁解决惊群问题

一、进程模型
        Nginx之所以为广大码农喜爱,除了其高性能外,还有其优雅的系统架构。与Memcached的经典多线程模型相比,Nginx是经典的多进程模型。Nginx启动后以daemon的方式在后台运行,后台进程包含一个master进程和多个worker进程,具体如下图:

Nginx深入详解之多进程网络模型,用共享锁解决惊群问题
图1 Nginx多进程模型

        master进程主要用来管理worker进程,具体包括如下4个主要功能:
        (1)接收来自外界的信号。
        (2)向各worker进程发送信号。
        (3)监控woker进程的运行状态。
        (4)当woker进程退出后(异常情况下),会自动重新启动新的woker进程。
        woker进程主要用来处理网络事件,各个woker进程之间是对等且相互独立的,它们同等竞争来自客户端的请求,一个请求只可能在一个woker进程中处理,woker进程个数一般设置为机器CPU核数。

二、进程控制
        对Nginx进程的控制主要是通过master进程来做到的,主要有两种方式:
        (1)手动发送信号
        从图1可以看出,master接收信号以管理众woker进程,那么,可以通过kill向master进程发送信号,比如kill -HUP pid用以通知Nginx从容重启。所谓从容重启就是不中断服务:master进程在接收到信号后,会先重新加载配置,然后再启动新进程开始接收新请求,并向所有老进程发送信号告知不再接收新请求并在处理完所有未处理完的请求后自动退出。
        (2)自动发送信号
        可以通过带命令行参数启动新进程来发送信号给master进程,比如./nginx -s reload用以启动一个新的Nginx进程,而新进程在解析到reload参数后会向master进程发送信号(新进程会帮我们把手动发送信号中的动作自动完成)。当然也可以这样./nginx -s stop来停止Nginx。

三、网络事件
         Nginx(多进程)采用异步非阻塞的方式来处理网络事件,类似于Libevent(单进程单线程),具体过程如下图:

Nginx深入详解之多进程网络模型,用共享锁解决惊群问题
图2 Nginx网络事件

        master进程先建好需要listen的socket后,然后再fork出多个woker进程,这样每个work进程都可以去accept这个socket。当一个client连接到来时,所有accept的work进程都会受到通知,但只有一个进程可以accept成功,其它的则会accept失败。Nginx提供了一把共享锁accept_mutex来保证同一时刻只有一个work进程在accept连接,从而解决惊群问题。当一个worker进程accept这个连接后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这样一个完成的请求就结束了。

 

四、Nginx架构

  Nginx全称是什么? Nginx ("engine x") 是一个高性能的 HTTP和反向代理服务器,也是一个 IMAP/POP3/SMTP代理服务器。

五、daemon守护线程
  nginx在启动后,在unix系统中会以daemon的方式在后台运行,后台进程包含一个master进程和多个worker进程。
  当然nginx也是支持多线程的方式的,只是我们主流的方式还是多进程的方式,也是nginx的默认方式。
  master进程主要用来管理worker进程,包含:接收来自外界的信号,向各worker进程发送信号,监控worker进程的运行状态,当worker进程退出后(异常情况下),会自动重新启动新的worker进程。
  worker进程则是处理基本的网络事件。多个worker进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的。一个请求,只可能在一个worker进程中处理,一个worker进程,不可能处理其它进程的请求。
  worker进程的个数是可以设置的,一般我们会设置与机器cpu核数一致。更多的worker数,只会导致进程来竞争cpu资源了,从而带来不必要的上下文切换。而且,nginx为了更好的利用多核特性,具有cpu绑定选项,我们可以将某一个进程绑定在某一个核上,这样就不会因为进程的切换带来cache的失效。

六、惊群现象
  每个worker进程都是从master进程fork过来。在master进程里面,先建立好需要listen的socket之 后,然后再fork出多个worker进程,这样每个worker进程都可以去accept这个socket(当然不是同一个socket,只是每个进程 的这个socket会监控在同一个ip地址与端口,这个在网络协议里面是允许的)。一般来说,当一个连接进来后,所有在accept在这个socket上 面的进程,都会收到通知,而只有一个进程可以accept这个连接,其它的则accept失败。

七、相对于线程,采用进程的优点
  进程之间不共享资源,不需要加锁,所以省掉了锁带来的开销。
  采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master进程则很快重新启动新的worker进程。
  编程上更加容易。

八、多线程的问题
  而多线程在多并发情况下,线程的内存占用大,线程上下文切换造成CPU大量的开销。想想apache的常用工作方式(apache 也有异步非阻塞版本,但因其与自带某些模块冲突,所以不常用),每个请求会独占一个工作线程,当并发数上到几千时,就同时有几千的线程在处理请求了。这对 操作系统来说,是个不小的挑战,线程带来的内存占用非常大,线程的上下文切换带来的cpu开销很大,自然性能就上不去了,而这些开销完全是没有意义的。

九、异步非阻塞
  异步的概念和同步相对的,也就是不是事件之间不是同时发生的。
  非阻塞的概念是和阻塞对应的,阻塞是事件按顺序执行,每一事件都要等待上一事件的完成,而非阻塞是如果事件没有准备好,这个事件可以直接返回,过一段时间再进行处理询问,这期间可以做其他事情。但是,多次询问也会带来额外的开销。
  总的来说,Nginx采用异步非阻塞的好处在于:
· 不需要创建线程,每个请求只占用少量的内存
· 没有上下文切换,事件处理非常轻量

淘宝tengine团队说测试结果是“24G内存机器上,处理并发请求可达200万”。

 

十、好书推荐

《Nginx完全开发指南:使用C、C++和OpenResty》

《深入理解Nginx:模块开发与架构解析(第2版)》

 

---

Libevent友情知识:https://github.com/libevent/libevent

支持Libevent运转的就是一个大循环,这个主循环体现在event_base_loop(Event.c/1533)函数里,该函数的执行流程如下:

Nginx深入详解之多进程网络模型,用共享锁解决惊群问题
图1 event_base_loop主循环

        上图的简单描述就是:
        (1)校正系统当前时间。
        (2)将当前时间与存放时间的最小堆中的时间依次进行比较,将所有时间小于当前时间的定时器事件从堆中取出来加入到活动事件队列中。
        (3)调用I/O封装(比如:Epoll)的事件分发函数dispatch函数,以当前时间与时间堆中的最小值之间的差值(最小堆取最小值复杂度为O(1))作为Epoll/epoll_wait(Epoll.c/dispatch/407)的timeout值,在其中将触发的I/O和信号事件加入到活动事件队列中。

        (4)调用函数event_process_active(Event.c/1406)遍历活动事件队列,依次调用注册的回调函数处理相应事件。

附上event_base_loop源码如下:

 
  1. int event_base_loop(struct event_base *base, int flags)

  2. {

  3. const struct eventop *evsel = base->evsel;

  4. struct timeval tv;

  5. struct timeval *tv_p;

  6. int res, done, retval = 0;

  7.  
  8. /* Grab the lock. We will release it inside evsel.dispatch, and again

  9. * as we invoke user callbacks. */

  10. EVBASE_ACQUIRE_LOCK(base, th_base_lock);

  11.  
  12. if (base->running_loop) {

  13. event_warnx("%s: reentrant invocation. Only one event_base_loop"

  14. " can run on each event_base at once.", __func__);

  15. EVBASE_RELEASE_LOCK(base, th_base_lock);

  16. return -1;

  17. }

  18.  
  19. base->running_loop = 1;

  20.  
  21. clear_time_cache(base);

  22.  
  23. if (base->sig.ev_signal_added && base->sig.ev_n_signals_added)

  24. evsig_set_base(base);

  25.  
  26. done = 0;

  27.  
  28. #ifndef _EVENT_DISABLE_THREAD_SUPPORT

  29. base->th_owner_id = EVTHREAD_GET_ID();

  30. #endif

  31.  
  32. base->event_gotterm = base->event_break = 0;

  33.  
  34. while (!done) {

  35. base->event_continue = 0;

  36.  
  37. /* Terminate the loop if we have been asked to */

  38. if (base->event_gotterm) {

  39. break;

  40. }

  41.  
  42. if (base->event_break) {

  43. break;

  44. }

  45.  
  46. timeout_correct(base, &tv);

  47.  
  48. tv_p = &tv;

  49. if (!N_ACTIVE_CALLBACKS(base) && !(flags & EVLOOP_NONBLOCK)) {

  50. timeout_next(base, &tv_p);

  51. } else {

  52. /*

  53. * if we have active events, we just poll new events

  54. * without waiting.

  55. */

  56. evutil_timerclear(&tv);

  57. }

  58.  
  59. /* If we have no events, we just exit */

  60. if (!event_haveevents(base) && !N_ACTIVE_CALLBACKS(base)) {

  61. event_debug(("%s: no events registered.", __func__));

  62. retval = 1;

  63. goto done;

  64. }

  65.  
  66. /* update last old time */

  67. gettime(base, &base->event_tv);

  68.  
  69. clear_time_cache(base);

  70.  
  71. res = evsel->dispatch(base, tv_p);

  72.  
  73. if (res == -1) {

  74. event_debug(("%s: dispatch returned unsuccessfully.",

  75. __func__));

  76. retval = -1;

  77. goto done;

  78. }

  79.  
  80. update_time_cache(base);

  81.  
  82. timeout_process(base);

  83.  
  84. if (N_ACTIVE_CALLBACKS(base)) {

  85. int n = event_process_active(base);

  86. if ((flags & EVLOOP_ONCE)

  87. && N_ACTIVE_CALLBACKS(base) == 0

  88. && n != 0)

  89. done = 1;

  90. } else if (flags & EVLOOP_NONBLOCK)

  91. done = 1;

  92. }

  93. event_debug(("%s: asked to terminate loop.", __func__));

  94.  
  95. done:

  96. clear_time_cache(base);

  97. base->running_loop = 0;

  98.  
  99. EVBASE_RELEASE_LOCK(base, th_base_lock);

  100.  
  101. return (retval);

  102. }