在说nginx前,先来看看什么是“惊群”?简单说来,多线程/多进程(linux下线程进程也没多大区别)等待同一个socket事件,当这个事件发生时,这些线程/进程被同时唤醒,就是惊群。可以想见,效率很低下,许多进程被内核重新调度唤醒,同时去响应这一个事件,当然只有一个进程能处理事件成功,其他的进程在处理该事件失败后重新休眠(也有其他选择)。这种性能浪费现象就是惊群。

惊群通常发生在server 上,当父进程绑定一个端口监听socket,然后fork出多个子进程,子进程们开始循环处理(比如accept)这个socket。每当用户发起一个TCP连接时,多个子进程同时被唤醒,然后其中一个子进程accept新连接成功,余者皆失败,重新休眠。

那么,我们不能只用一个进程去accept新连接么?然后通过消息队列等同步方式使其他子进程处理这些新建的连接,这样惊群不就避免了?没错,惊群是避免了,但是效率低下,因为这个进程只能用来accept连接。对多核机器来说,仅有一个进程去accept,这也是程序员在自己创造accept瓶颈。所以,我仍然坚持需要多进程处理accept事件。

其实,在linux2.6内核上,accept系统调用已经不存在惊群了(至少我在2.6.18内核版本上已经不存在)。大家可以写个简单的程序试下,在父进程中bind,listen,然后fork出子进程,所有的子进程都accept这个监听句柄。这样,当新连接过来时,大家会发现,仅有一个子进程返回新建的连接,其他子进程继续休眠在accept调用上,没有被唤醒。

但是很不幸,通常我们的程序没那么简单,不会愿意阻塞在accept调用上,我们还有许多其他网络读写事件要处理,linux下我们爱用epoll解决非阻塞socket。所以,即使accept调用没有惊群了,我们也还得处理惊群这事,因为epoll有这问题。上面说的测试程序,如果我们在子进程内不是阻塞调用accept,而是用epoll_wait,就会发现,新连接过来时,多个子进程都会在epoll_wait后被唤醒!

nginx就是这样,master进程监听端口号(例如80),所有的nginx worker进程开始用epoll_wait来处理新事件(linux下),如果不加任何保护,一个新连接来临时,会有多个worker进程在epoll_wait后被唤醒,然后发现自己accept失败。现在,我们可以看看nginx是怎么处理这个惊群问题了。

nginx的每个worker进程在函数ngx_process_events_and_timers中处理事件,(void) ngx_process_events(cycle, timer, flags);封装了不同的事件处理机制,在linux上默认就封装了epoll_wait调用。我们来看看ngx_process_events_and_timers为解决惊群做了什么:

  1. void
  2. ngx_process_events_and_timers(ngx_cycle_t *cycle)
  3. {
  4. 。。。 。。。
  5. //ngx_use_accept_mutex表示是否需要通过对accept加锁来解决惊群问题。当nginx worker进程数>1时且配置文件中打开accept_mutex时,这个标志置为1
  6. if (ngx_use_accept_mutex) {
  7. //ngx_accept_disabled表示此时满负荷,没必要再处理新连接了,我们在nginx.conf曾经配置了每一个nginx worker进程能够处理的最大连接数,当达到最大数的7/8时,ngx_accept_disabled为正,说明本nginx worker进程非常繁忙,将不再去处理新连接,这也是个简单的负载均衡
  8. if (ngx_accept_disabled > 0) {
  9. ngx_accept_disabled--;
  10. } else {
  11. //获得accept锁,多个worker仅有一个可以得到这把锁。获得锁不是阻塞过程,都是立刻返回,获取成功的话ngx_accept_mutex_held被置为1。拿到锁,意味着监听句柄被放到本进程的epoll中了,如果没有拿到锁,则监听句柄会被从epoll中取出。
  12. if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
  13. return;
  14. }
  15. //拿到锁的话,置flag为NGX_POST_EVENTS,这意味着ngx_process_events函数中,任何事件都将延后处理,会把accept事件都放到ngx_posted_accept_events链表中,epollin|epollout事件都放到ngx_posted_events链表中
  16. if (ngx_accept_mutex_held) {
  17. flags |= NGX_POST_EVENTS;
  18. } else {
  19. //拿不到锁,也就不会处理监听的句柄,这个timer实际是传给epoll_wait的超时时间,修改为最大ngx_accept_mutex_delay意味着epoll_wait更短的超时返回,以免新连接长时间没有得到处理
  20. if (timer == NGX_TIMER_INFINITE
  21. || timer > ngx_accept_mutex_delay)
  22. {
  23. timer = ngx_accept_mutex_delay;
  24. }
  25. }
  26. }
  27. }
  28. 。。。 。。。
  29. //linux下,调用ngx_epoll_process_events函数开始处理
  30. (void) ngx_process_events(cycle, timer, flags);
  31. 。。。 。。。
  32. //如果ngx_posted_accept_events链表有数据,就开始accept建立新连接
  33. if (ngx_posted_accept_events) {
  34. ngx_event_process_posted(cycle, &ngx_posted_accept_events);
  35. }
  36. //释放锁后再处理下面的EPOLLIN EPOLLOUT请求
  37. if (ngx_accept_mutex_held) {
  38. ngx_shmtx_unlock(&ngx_accept_mutex);
  39. }
  40. if (delta) {
  41. ngx_event_expire_timers();
  42. }
  43. ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
  44. "posted events %p", ngx_posted_events);
  45. //然后再处理正常的数据读写请求。因为这些请求耗时久,所以在ngx_process_events里NGX_POST_EVENTS标志将事件都放入ngx_posted_events链表中,延迟到锁释放了再处理。
  46. if (ngx_posted_events) {
  47. if (ngx_threaded) {
  48. ngx_wakeup_worker_thread(cycle);
  49. } else {
  50. ngx_event_process_posted(cycle, &ngx_posted_events);
  51. }
  52. }

从上面的注释可以看到,无论有多少个nginx worker进程,同一时刻只能有一个worker进程在自己的epoll中加入监听的句柄。这个处理accept的nginx worker进程置flag为NGX_POST_EVENTS,这样它在接下来的ngx_process_events函数(在linux中就是ngx_epoll_process_events函数)中不会立刻处理事件,延后,先处理完所有的accept事件后,释放锁,然后再处理正常的读写socket事件。我们来看下ngx_epoll_process_events是怎么做的:

  1. static ngx_int_t
  2. ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
  3. {
  4. 。。。 。。。
  5. events = epoll_wait(ep, event_list, (int) nevents, timer);
  6. 。。。 。。。
  7. ngx_mutex_lock(ngx_posted_events_mutex);
  8. for (i = 0; i < events; i++) {
  9. c = event_list[i].data.ptr;
  10. 。。。 。。。
  11. rev = c->read;
  12. if ((revents & EPOLLIN) && rev->active) {
  13. 。。。 。。。
  14. //有NGX_POST_EVENTS标志的话,就把accept事件放到ngx_posted_accept_events队列中,把正常的事件放到ngx_posted_events队列中延迟处理
  15. if (flags & NGX_POST_EVENTS) {
  16. queue = (ngx_event_t **) (rev->accept ?
  17. &ngx_posted_accept_events : &ngx_posted_events);
  18. ngx_locked_post_event(rev, queue);
  19. } else {
  20. rev->handler(rev);
  21. }
  22. }
  23. wev = c->write;
  24. if ((revents & EPOLLOUT) && wev->active) {
  25. 。。。 。。。
  26. //同理,有NGX_POST_EVENTS标志的话,写事件延迟处理,放到ngx_posted_events队列中
  27. if (flags & NGX_POST_EVENTS) {
  28. ngx_locked_post_event(wev, &ngx_posted_events);
  29. } else {
  30. wev->handler(wev);
  31. }
  32. }
  33. }
  34. ngx_mutex_unlock(ngx_posted_events_mutex);
  35. return NGX_OK;
  36. }

看看ngx_use_accept_mutex在何种情况下会被打开:

  1. if (ccf->master && ccf->worker_processes > 1 && ecf->accept_mutex) {
  2. ngx_use_accept_mutex = 1;
  3. ngx_accept_mutex_held = 0;
  4. ngx_accept_mutex_delay = ecf->accept_mutex_delay;
  5. } else {
  6. ngx_use_accept_mutex = 0;
  7. }

当nginx worker数量大于1时,也就是多个进程可能accept同一个监听的句柄,这时如果配置文件中accept_mutex开关打开了,就将ngx_use_accept_mutex置为1。

再看看有些负载均衡作用的ngx_accept_disabled是怎么维护的,在ngx_event_accept函数中:

  1. ngx_accept_disabled = ngx_cycle->connection_n / 8
  2. - ngx_cycle->free_connection_n;

表明,当已使用的连接数占到在nginx.conf里配置的worker_connections总数的7/8以上时,ngx_accept_disabled为正,这时本worker将ngx_accept_disabled减1,而且本次不再处理新连接。

最后,我们看下ngx_trylock_accept_mutex函数是怎么玩的:

  1. ngx_int_t
  2. ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
  3. {
  4. //ngx_shmtx_trylock是非阻塞取锁的,返回1表示成功,0表示没取到锁
  5. if (ngx_shmtx_trylock(&ngx_accept_mutex)) {
  6. //ngx_enable_accept_events会把监听的句柄都塞入到本worker进程的epoll中
  7. if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
  8. ngx_shmtx_unlock(&ngx_accept_mutex);
  9. return NGX_ERROR;
  10. }
  11. //ngx_accept_mutex_held置为1,表示拿到锁了,返回
  12. ngx_accept_events = 0;
  13. ngx_accept_mutex_held = 1;
  14. return NGX_OK;
  15. }
  16. //处理没有拿到锁的逻辑,ngx_disable_accept_events会把监听句柄从epoll中取出
  17. if (ngx_accept_mutex_held) {
  18. if (ngx_disable_accept_events(cycle) == NGX_ERROR) {
  19. return NGX_ERROR;
  20. }
  21. ngx_accept_mutex_held = 0;
  22. }
  23. return NGX_OK;
  24. }

OK,关于锁的细节是如何实现的,这篇限于篇幅就不说了,下篇帖子再来讲。现在大家清楚nginx是怎么处理惊群了吧?简单了说,就是同一时刻只允许一个nginx worker在自己的epoll中处理监听句柄。它的负载均衡也很简单,当达到最大connection的7/8时,本worker不会去试图拿accept锁,也不会去处理新连接,这样其他nginx worker进程就更有机会去处理监听句柄,建立新连接了。而且,由于timeout的设定,使得没有拿到锁的worker进程,去拿锁的频繁更高。

转载:http://blog.csdn.net/russell_tao/article/details/7204260

“惊群”,看看nginx是怎么解决它的的更多相关文章

  1. NGINX怎样处理惊群的

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

  2. accept与epoll惊群 转载

    今天打开 OneNote,发现里面躺着一篇很久以前写的笔记,现在将它贴出来. 1. 什么叫惊群现象 首先,我们看看维基百科对惊群的定义: The thundering herd problem occ ...

  3. 【转载】“惊群”,看看nginx是怎么解决它的

    原文:http://blog.csdn.net/russell_tao/article/details/7204260 在说nginx前,先来看看什么是“惊群”?简单说来,多线程/多进程(linux下 ...

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

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

  5. Nginx学习之一-惊群现象

    惊群问题(thundering herd)的产生 在建立连接的时候,Nginx处于充分发挥多核CPU架构性能的考虑,使用了多个worker子进程监听相同端口的设计,这样多个子进程在accept建立新连 ...

  6. 【Nginx】惊群问题

    转自:江南烟雨 惊群问题的产生 在建立连接的时候,Nginx处于充分发挥多核CPU架构性能的考虑,使用了多个worker子进程监听相同端口的设计,这样多个子进程在accept建立新连接时会有争抢,这会 ...

  7. Nginx惊群处理

    惊群:是指在多线程/多进程中,当有一个客户端发生链接请求时,多线程/多进程都被唤醒,然后只仅仅有一个进程/线程处理成功,其他进程/线程还是回到睡眠状态,这种现象就是惊群. 惊群是经常发生现在serve ...

  8. Nginx模型 & 惊群问题

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

  9. Nginx惊群问题

    Nginx惊群问题 "惊群"概念 所谓惊群,可以用一个简单的比喻来说明: 一群等待食物的鸽子,当饲养员扔下一粒谷物时,所有鸽子都会去争抢,但只有少数的鸽子能够抢到食物, 大部分鸽子 ...

随机推荐

  1. js运动 运动效果留言本

    <!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content ...

  2. dom 动态生产表格

    <!doctype html> <html> <head> <meta charset="utf-8"> <title> ...

  3. 麒麟OS剽窃

    今年对于我们的IT行业来说可以算是耻辱的一年. 首先是“汉芯丑闻”,上海交大研制了一个所谓的国内第一个完全拥有自主知识产 权的DSP芯片(数字信号微处理器)——“汉芯”,研制人陈进教授以此领取政府一亿 ...

  4. 实体框架 (EF) 入门 => 三、CodeFirst 支持的完整特性列表

    KeyAttribute 设置主键.如果为int类型,将自动设置为自增长列. 系统默认以Id或类名+Id作为主键.StringLengthAttribute 可设置最大最小长度以及验证提示信息等.最大 ...

  5. 统一入口的Ajax验证

    此前一直没有写博客的习惯,只是将一些心得和体会大致的用笔写在一个本子上,今天刚刚开通博客,就随便写一点吧! 关于服务端验证,大致可以分为登陆验证与功能权限验证,而以前端请求方式来区分的话,又可分为 1 ...

  6. 给windows 7安装文件添加USB3.0驱动

    给Air安装win7进入语言与区域选择之后,发现键盘触摸板都失灵.   原因:新款的 Macbook Air 2013 因为使用了 USB3.0 端口键盘和触摸板设备,所以在安装 Windows 7 ...

  7. 【Linux笔记】Linux目录结构

    [Linux笔记]Linux目录结构   本文内容整理自网络,以作参考. /:根目录,位于linux文件系统目录结构的顶层,一般根目录下只存放目录,不要存放文件,/etc./bin./dev./lib ...

  8. HTML5-企业宣传6款免费源码

    本文主要分享了6个很不错的HTML5宣传模板源码,这些生动具体的介绍了各个公司的产品及公司各类应用,有汽车.旅游.公司,房产等Query特效,大家一起来欣赏吧. 一.九秒教育企业海报(移动端)这次要分 ...

  9. magento目录结构精编版

    1 /app – 程序根目录 2 3 /app/etc – 全局配置文件目录 4 5 /app/code – 所有模块安装其模型和控制器的目录 6 7 /app/code/core – 核心代码或经过 ...

  10. 使用struts2实现文件下载

    <action name="downloadAction" class=""> <result type="stream" ...