惊群问题(thundering herd)的产生

在建立连接的时候,Nginx处于充分发挥多核CPU架构性能的考虑,使用了多个worker子进程监听相同端口的设计,这样多个子进程在accept建立新连接时会有争抢,这会带来著名的“惊群”问题,子进程数量越多越明显,这会造成系统性能的下降。
一般情况下,有多少CPU核心就有配置多少个worker子进程。假设现在没有用户连入服务器,某一时刻恰好所有的子进程都休眠且等待新连接的系统调用(如epoll_wait),这时有一个用户向服务器发起了连接,内核在收到TCP的SYN包时,会激活所有的休眠worker子进程。最终只有最先开始执行accept的子进程可以成功建立新连接,而其他worker子进程都将accept失败。这些accept失败的子进程被内核唤醒是不必要的,他们被唤醒会的执行很可能是多余的,那么这一时刻他们占用了本不需要占用的资源,引发了不必要的进程切换,增加了系统开销。
 
如何解决惊群问题-post事件处理机制
 
很多操作系统的最新版本的内核已经在事件驱动机制中解决了惊群问题,但Nginx作为可移植性极高的web服务器,还是在自身的应用层面上较好的解决了这一问题。
Nginx规定了同一时刻只有唯一一个worker子进程监听web端口,这一就不会发生惊群了,此时新连接事件只能唤醒唯一的正在监听端口的worker子进程。
如何限制在某一时刻是有一个子进程监听web端口呢?在打开accept_mutex锁的情况下,只有调用ngx_trylock_accept_mutex方法后,当前的worker进程才会去试着监听web端口。
那么,什么时候释放ngx_accept_mutex锁呢?
显然不能等到这批事件全部执行完。因为这个worker进程上可能有许多活跃的连接,处理这些连接上的事件会占用很长时间,其他worker进程很难得到处理新连接的机会。
如何解决长时间占用ngx_accept_mutex的问题呢?这就要依靠post事件处理机制,Nginx设计了两个队列:ngx_posted_accept_events队列(存放新连接事件的队列)和ngx_posted_events队列(存放普通事件的队列)。这两个队列都是ngx_event_t类型的双链表。定义如下:
  1. ngx_thread_volatile ngx_event_t  *ngx_posted_accept_events;
  2. ngx_thread_volatile ngx_event_t  *ngx_posted_events;
下面结合具体代码进行分析惊群问题的解决。
首先看worker进程中ngx_process_events_and_timers事件处理函数(src/event/ngx.event.c),它处于worker进程的ngx_worker_process_cycle方法中,循环处理时间,是事件驱动机制的核心,既会处理普通的网络事件,也会处理定时器事件。ngx_process_events_and_timers是Nginx实际处理web业务的方法,所有业务的执行都是由它开始的,它涉及Nginx完整的事件驱动机制!!特别重要~
  1. void
  2. ngx_process_events_and_timers(ngx_cycle_t *cycle)
  3. {
  4. ngx_uint_t  flags;
  5. ngx_msec_t  timer, delta;
  6. if (ngx_timer_resolution) {
  7. timer = NGX_TIMER_INFINITE;
  8. flags = 0;
  9. } else {
  10. timer = ngx_event_find_timer();
  11. flags = NGX_UPDATE_TIME;
  12. #if (NGX_THREADS)
  13. if (timer == NGX_TIMER_INFINITE || timer > 500) {
  14. timer = 500;
  15. }
  16. #endif
  17. }
  18. /*ngx_use_accept_mutex表示是否需要通过对accept加锁来解决惊群问题。当使用了master模式,nginx worker进程数>1时且配置文件中打开accept_mutex时,这个标志置为1
  19. 它在函数ngx_event_process_int中被设置,源代码为:
  20. if (ccf->master && ccf->worker_processes > 1 && ecf->accept_mutex) {
  21. ngx_use_accept_mutex = 1;
  22. ngx_accept_mutex_held = 0;
  23. ngx_accept_mutex_delay = ecf->accept_mutex_delay;
  24. } else {
  25. ngx_use_accept_mutex = 0;
  26. }*/
  27. if (ngx_use_accept_mutex) {
  28. //负载均衡处理
  29. if (ngx_accept_disabled > 0) {
  30. ngx_accept_disabled--;
  31. } else {
  32. //调用ngx_trylock_accept_mutex方法,尝试获取accept锁
  33. if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
  34. return;
  35. }
  36. //拿到锁
  37. if (ngx_accept_mutex_held) {
  38. /*给flags增加标记NGX_POST_EVENTS,这个标记作为处理时间核心函数ngx_process_events的一个参数,这个函数中所有事件将延后处理。会把accept事件都放到ngx_posted_accept_events链表中,epollin|epollout普通事件都放到ngx_posted_events链表中 */
  39. flags |= NGX_POST_EVENTS;
  40. } else {
  41. /*获取锁失败,意味着既不能让当前worker进程频繁的试图抢锁,也不能让它经过太长事件再去抢锁
  42. 下面的代码:即使开启了timer_resolution时间精度,牙需要让ngx_process_change方法在没有新事件的时候至少等待ngx_accept_mutex_delay毫秒之后再去试图抢锁
  43. 而没有开启时间精度时,如果最近一个定时器事件的超时时间距离现在超过了ngx_accept_mutex_delay毫秒,也要把timer设置为ngx_accept_mutex_delay毫秒,这是因为当前进程虽然没有抢到accept_mutex锁,但也不能让ngx_process_change方法在没有新事件的时候等待的时间超过ngx_accept_mutex_delay,这会影响整个负载均衡机制*/
  44. if (timer == NGX_TIMER_INFINITE
  45. || timer > ngx_accept_mutex_delay)
  46. {
  47. timer = ngx_accept_mutex_delay;
  48. }
  49. }
  50. }
  51. }
  52. //计算ngx_process_events消耗的时间
  53. delta = ngx_current_msec;
  54. //事件处理核心函数
  55. (void) ngx_process_events(cycle, timer, flags);
  56. delta = ngx_current_msec - delta;
  57. ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
  58. "timer delta: %M", delta);
  59. //ngx_posted_accept_events链表有数据,开始accept新连接
  60. if (ngx_posted_accept_events) {
  61. ngx_event_process_posted(cycle, &ngx_posted_accept_events);
  62. }
  63. //释放锁后再处理ngx_posted_events链表中的普通事件
  64. if (ngx_accept_mutex_held) {
  65. ngx_shmtx_unlock(&ngx_accept_mutex);
  66. }
  67. //如果ngx_process_events消耗的时间大于0,那么这是可能有新的定时器事件触发
  68. if (delta) {
  69. //处理定时器事件
  70. ngx_event_expire_timers();
  71. }
  72. ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
  73. "posted events %p", ngx_posted_events);
  74. //ngx_posted_events链表中有数据,进行处理
  75. if (ngx_posted_events) {
  76. if (ngx_threaded) {
  77. ngx_wakeup_worker_thread(cycle);
  78. } else {
  79. ngx_event_process_posted(cycle, &ngx_posted_events);
  80. }
  81. }
  82. }

上面代码中要进行说明的是,flags被设置后作为函数ngx_process_events方法的一个参数,在epoll模块中这个接口的实现方法是ngx_epoll_process_events(其具体代码见http://blog.csdn.net/xiajun07061225/article/details/9250341)。当falgs标志位含有nGX_POST_EVENTS时是不会立即调用事件的handler回调方法的,代码如下所示:

  1. //事件需要延后处理
  2. if (flags & NGX_POST_EVENTS) {
  3. /*如果要在post队列中延后处理该事件,首先要判断它是新连接时间还是普通事件
  4. 以确定是把它加入到ngx_posted_accept_events队列或者ngx_posted_events队列中。*/
  5. queue = (ngx_event_t **) (rev->accept ?
  6. &ngx_posted_accept_events : &ngx_posted_events);
  7. //将该事件添加到相应的延后队列中
  8. ngx_locked_post_event(rev, queue);
  9. } else {
  10. //立即调用事件回调方法来处理这个事件
  11. rev->handler(rev);
  12. }
通过上面的代码可以看出,先处理ngx_posted_accept_events队列中的事件,处理完毕后立即释放ngx_accept_mutex锁,接着再处理ngx_posted_events队列中事件。这样大大减少了ngx_accept_mutex锁占用的时间
下面看看ngx_trylock_accept_mutex的具体实现(src/event/ngx_event_accept.c):
  1. ngx_int_t
  2. ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
  3. {
  4. //尝试获取accept_mutex锁。注意是非阻塞的。返回1表示成功,返回0表示失败。
  5. //ngx_accept_mutex 定义:ngx_shmtx_t    ngx_accept_mutex;(ngx_shmtx_t是Nginx封装的互斥锁,用于经常间同步)
  6. if (ngx_shmtx_trylock(&ngx_accept_mutex)) {
  7. ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
  8. "accept mutex locked");
  9. //获取到锁,但是标志位ngx_accept_mutex_held为1,表示当前进程已经获取到锁了,立即返回。
  10. if (ngx_accept_mutex_held
  11. && ngx_accept_events == 0
  12. && !(ngx_event_flags & NGX_USE_RTSIG_EVENT))
  13. {
  14. return NGX_OK;
  15. }
  16. //将所有监听事件添加到当前的epoll等事件驱动模块中
  17. if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
  18. //添加失败,必须释放互斥锁
  19. ngx_shmtx_unlock(&ngx_accept_mutex);
  20. return NGX_ERROR;
  21. }
  22. //标志位设置
  23. ngx_accept_events = 0;
  24. //当前进程已经获取到锁
  25. ngx_accept_mutex_held = 1;
  26. return NGX_OK;
  27. }
  28. ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
  29. "accept mutex lock failed: %ui", ngx_accept_mutex_held);
  30. //获取锁失败,但是标志位ngx_accept_mutex_held仍然为1,即当前进程还处在获取到锁的状态,这是不正确的
  31. if (ngx_accept_mutex_held) {
  32. //将所有监听事件从事件驱动模块中移除
  33. if (ngx_disable_accept_events(cycle) == NGX_ERROR) {
  34. return NGX_ERROR;
  35. }
  36. //没有获取到锁,设置标志位
  37. ngx_accept_mutex_held = 0;
  38. }
  39. return NGX_OK;
  40. }
调用这个方法的结果是,要么唯一获取到锁且其epoll等事件驱动模块开始监控web端口上的新连接事件。这种情况下调用process_events方法时就会既处理已有连接上的事件,也处理新连接的事件。要么没有获取到锁,当前进程不会收到新连接事件。这种情况下process_events只处理已有连接上的事件。

Nginx学习之一-惊群现象的更多相关文章

  1. Nginx中的惊群现象解决方法

    *什么是惊群现象?Nginx中用了什么方法来避免这种问题的发生?本篇就解决这两个问题...→_→* 惊群现象的定义与危害 在Nginx中,每一个worker进程都是由master进程fork出来的.m ...

  2. pthread_cond_signal惊群现象

    1.如下代码所示: #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include < ...

  3. Redis 利用锁机制来防止缓存过期产生的惊群现象-转载自 http://my.oschina.net/u/1156660/blog/360552

    首先,所谓的缓存过期引起的“惊群”现象是指,在大并发情况下,我们通常会用缓存来给数据库分压,但是会有这么一种情况发生,那就是在一定时间 内生成大量的缓存,然后当缓存到期之后又有大量的缓存失效,导致后端 ...

  4. NGINX怎样处理惊群的

    写在前面 写NGINX系列的随笔,一来总结学到的东西,二来记录下疑惑的地方,在接下来的学习过程中去解决疑惑. 也希望同样对NGINX感兴趣的朋友能够解答我的疑惑,或者共同探讨研究. 整个NGINX系列 ...

  5. Accept 惊群现象测试perl脚本

    $uname -a Linux debian-11-34 3.16.0-4-amd64 #1 SMP Debian 3.16.7-ckt9-3~deb8u1 (2015-04-24) x86_64 G ...

  6. Nginx模型 & 惊群问题

    这篇写的不错 http://www.cnblogs.com/linguoguo/p/5511293.html Nginx为啥性能高-多进程异步IO模型 1. 对于每个worker进程来说,独立的进程, ...

  7. Linux网络编程“惊群”问题总结

    1.前言 我从事Linux系统下网络开发将近4年了,经常还是遇到一些问题,只是知其然而不知其所以然,有时候和其他人交流,搞得非常尴尬.如今计算机都是多核了,网络编程框架也逐步丰富多了,我所知道的有多进 ...

  8. Nginx学习笔记(一) Nginx架构

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

  9. epoll 惊群处理

    #include <sys/types.h> #include <sys/socket.h> #include <sys/epoll.h> #include < ...

随机推荐

  1. Mysql5.6版本内存占用过高解决方法[链接]

    传送门: http://blog.linsongzheng.com/archives/159.html

  2. 【前端vue开发】vue开发watch检测的使用

    <span style="color:#006600;"><div id="app"> <input type="tex ...

  3. Go 命令行总结

    go build:已当前目录作为package进行编译,将当前目录下的所有文件编译成package文件,文件名与所在的目录同名. go install: 分两步操作:1.先执行go build进行编译 ...

  4. opencv之dft及mat类型转换

    跑实验时用到dft这个函数,根据教程,需要先将其扩充到最优尺寸,但我用逆变换后发现得到的mat的维数竟然不一样.因此还是不要扩展尺寸了. 参考:http://www.xpc-yx.com/2014/1 ...

  5. 使用JS实现文字搬运工

    使用JS实现文字搬运工 效果图: 代码如下,复制即可使用: <!DOCTYPE html> <html><head><meta http-equiv=&quo ...

  6. [android]Intent跳转新的Activity可以传递数据过去

    两种方式: 一,直接通过Bundle对象来传递: 如果我们想要给“收件人”Activity说点什么的话,那么可以通过下面这封“E-mail”来将我们的消息传递出去 Intent intent=new ...

  7. MySql数据库 主从复制/共享 报错

    从 获取不到 共享主的数据, 错误信息: Waiting for master to send event 解决方案: // 1. 从V表获取PrNo的数据 select * from Vendor_ ...

  8. CVE-2009-3459

     Adobe Acrobat和Reader都是美国Adobe公司开发的非常流行的PDF文件阅读器.         Adobe Reader和Acrobat 7.1.4之前的7.x版本,8.1.7之前 ...

  9. JQuery中Table标签页和无缝滚动

    HTML代码: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <t ...

  10. activeMQ 持久化配置

    Mac 中安装activeMQ brew install activemq 启动 activemq start 控制台:在浏览器中输入url: http://localhost:8161/ A:持久化 ...