侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

Nginx学习笔记之事件驱动框架处理流程

linux  /  管理员 发布于 7年前   127

ngx_event_core_module模块的ngx_event_process_init方法对事件模块做了一些初始化。其中包括将“请求连接”这样一个读事件对应的处理方法(handler)设置为ngx_event_accept函数,并将此事件添加到epoll模块中。当有新连接事件发生时,ngx_event_accept就会被调用。大致流程是这样:

worker进程在ngx_worker_process_cycle方法中不断循环调用ngx_process_events_and_timers函数处理事件,这个函数是事件处理的总入口。

ngx_process_events_and_timers会调用ngx_process_events,这是一个宏,相当于ngx_event_actions.process_events,ngx_event_actions是个全局的结构体,存储了对应事件驱动模块(这里是epoll模块)的10个函数接口。所以这里就是调用了ngx_epoll_module_ctx.actions.process_events函数,也就是ngx_epoll_process_events函数来处理事件。

ngx_epoll_process_events调用Linux函数接口epoll_wait获得“有新连接”这个事件,然后调用这个事件的handler处理函数来对这个事件进行处理。

在上面已经说过handler已经被设置成了ngx_event_accept函数,所以就调用ngx_event_accept进行实际的处理。

下面分析ngx_event_accept方法,它的流程图如下所示:

经过精简的代码如下,注释中的序号对应上图的序号:

voidngx_event_accept(ngx_event_t *ev){ socklen_t  socklen; ngx_err_t  err; ngx_log_t  *log; ngx_uint_t  level; ngx_socket_t  s; ngx_event_t  *rev, *wev; ngx_listening_t  *ls; ngx_connection_t *c, *lc; ngx_event_conf_t *ecf; u_char  sa[NGX_SOCKADDRLEN];  if (ev->timedout) {  if (ngx_enable_accept_events((ngx_cycle_t *) ngx_cycle) != NGX_OK) {   return;  }   ev->timedout = 0; }  ecf = ngx_event_get_conf(ngx_cycle->conf_ctx, ngx_event_core_module);  if (ngx_event_flags & NGX_USE_RTSIG_EVENT) {  ev->available = 1;  } else if (!(ngx_event_flags & NGX_USE_KQUEUE_EVENT)) {  ev->available = ecf->multi_accept; }  lc = ev->data; ls = lc->listening; ev->ready = 0;  do {  socklen = NGX_SOCKADDRLEN;   /* 1、accept方法试图建立连接,非阻塞调用 */  s = accept(lc->fd, (struct sockaddr *) sa, &socklen);   if (s == (ngx_socket_t) -1)  {   err = ngx_socket_errno;    if (err == NGX_EAGAIN)   {    /* 没有连接,直接返回 */    return;   }    level = NGX_LOG_ALERT;    if (err == NGX_ECONNABORTED) {    level = NGX_LOG_ERR;    } else if (err == NGX_EMFILE || err == NGX_ENFILE) {    level = NGX_LOG_CRIT;   }    if (err == NGX_ECONNABORTED) {    if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {     ev->available--;    }     if (ev->available) {     continue;    }   }    if (err == NGX_EMFILE || err == NGX_ENFILE) {    if (ngx_disable_accept_events((ngx_cycle_t *) ngx_cycle)     != NGX_OK)    {     return;    }     if (ngx_use_accept_mutex) {     if (ngx_accept_mutex_held) {      ngx_shmtx_unlock(&ngx_accept_mutex);      ngx_accept_mutex_held = 0;     }      ngx_accept_disabled = 1;     } else {     ngx_add_timer(ev, ecf->accept_mutex_delay);    }   }    return;  }   /* 2、设置负载均衡阈值 */  ngx_accept_disabled = ngx_cycle->connection_n / 8        - ngx_cycle->free_connection_n;   /* 3、从连接池获得一个连接对象 */  c = ngx_get_connection(s, ev->log);   /* 4、为连接创建内存池 */  c->pool = ngx_create_pool(ls->pool_size, ev->log);   c->sockaddr = ngx_palloc(c->pool, socklen);   ngx_memcpy(c->sockaddr, sa, socklen);   log = ngx_palloc(c->pool, sizeof(ngx_log_t));   /* set a blocking mode for aio and non-blocking mode for others */  /* 5、设置套接字属性为阻塞或非阻塞 */  if (ngx_inherited_nonblocking) {   if (ngx_event_flags & NGX_USE_AIO_EVENT) {    if (ngx_blocking(s) == -1) {     ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_socket_errno,         ngx_blocking_n " failed");     ngx_close_accepted_connection(c);     return;    }   }   } else {   if (!(ngx_event_flags & (NGX_USE_AIO_EVENT|NGX_USE_RTSIG_EVENT))) {    if (ngx_nonblocking(s) == -1) {     ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_socket_errno,         ngx_nonblocking_n " failed");     ngx_close_accepted_connection(c);     return;    }   }  }   *log = ls->log;   c->recv = ngx_recv;  c->send = ngx_send;  c->recv_chain = ngx_recv_chain;  c->send_chain = ngx_send_chain;   c->log = log;  c->pool->log = log;   c->socklen = socklen;  c->listening = ls;  c->local_sockaddr = ls->sockaddr;  c->local_socklen = ls->socklen;   c->unexpected_eof = 1;   rev = c->read;  wev = c->write;   wev->ready = 1;   if (ngx_event_flags & (NGX_USE_AIO_EVENT|NGX_USE_RTSIG_EVENT)) {   /* rtsig, aio, iocp */   rev->ready = 1;  }   if (ev->deferred_accept) {   rev->ready = 1;   }   rev->log = log;  wev->log = log;   /*   * TODO: MT: - ngx_atomic_fetch_add()   *  or protection by critical section or light mutex   *   * TODO: MP: - allocated in a shared memory   *   - ngx_atomic_fetch_add()   *  or protection by critical section or light mutex   */   c->number = ngx_atomic_fetch_add(ngx_connection_counter, 1);   if (ls->addr_ntop) {   c->addr_text.data = ngx_pnalloc(c->pool, ls->addr_text_max_len);   if (c->addr_text.data == NULL) {    ngx_close_accepted_connection(c);    return;   }    c->addr_text.len = ngx_sock_ntop(c->sockaddr, c->socklen,c->addr_text.data,ls->addr_text_max_len, 0);   if (c->addr_text.len == 0) {    ngx_close_accepted_connection(c);    return;   }  }   /* 6、将新连接对应的读写事件添加到epoll对象中 */  if (ngx_add_conn && (ngx_event_flags & NGX_USE_EPOLL_EVENT) == 0) {   if (ngx_add_conn(c) == NGX_ERROR) {    ngx_close_accepted_connection(c);    return;   }  }   log->data = NULL;  log->handler = NULL;   /* 7、TCP建立成功调用的方法,这个方法在ngx_listening_t结构体中 */  ls->handler(c);  } while (ev->available); /* available标志表示一次尽可能多的建立连接,由配置项multi_accept决定 */}

Nginx中的“惊群”问题

Nginx一般会运行多个worker进程,这些进程会同时监听同一端口。当有新连接到来时,内核将这些进程全部唤醒,但只有一个进程能够和客户端连接成功,导致其它进程在唤醒时浪费了大量开销,这被称为“惊群”现象。Nginx解决“惊群”的方法是,让进程获得互斥锁ngx_accept_mutex,让进程互斥地进入某一段临界区。在该临界区中,进程将它所要监听的连接对应的读事件添加到epoll模块中,使得当有“新连接”事件发生时,该worker进程会作出反应。这段加锁并添加事件的过程是在函数ngx_trylock_accept_mutex中完成的。而当其它进程也进入该函数想要添加读事件时,发现互斥锁被另外一个进程持有,所以它只能返回,它所监听的事件也无法添加到epoll模块,从而无法响应“新连接”事件。但这会出现一个问题:持有互斥锁的那个进程在什么时候释放互斥锁呢?如果需要等待它处理完所有的事件才释放锁的话,那么会需要相当长的时间。而在这段时间内,其它worker进程无法建立新连接,这显然是不可取的。Nginx的解决办法是:通过ngx_trylock_accept_mutex获得了互斥锁的进程,在获得就绪读/写事件并从epoll_wait返回后,将这些事件归类放入队列中:

新连接事件放入ngx_posted_accept_events队列
已有连接事件放入ngx_posted_events队列

代码如下:

if (flags & NGX_POST_EVENTS){ /* 延后处理这批事件 */ queue = (ngx_event_t **) (rev->accept ? &ngx_posted_accept_events : &ngx_posted_events);  /* 将事件添加到延后执行队列中 */ ngx_locked_post_event(rev, queue);}else{ rev->handler(rev); /* 不需要延后,则立即处理事件 */}

写事件做类似处理。进程接下来处理ngx_posted_accept_events队列中的事件,处理完后立即释放互斥锁,使该进程占用锁的时间降到了最低。

Nginx中的负载均衡问题

Nginx中每个进程使用了一个处理负载均衡的阈值ngx_accept_disabled,它在上图的第2步中被初始化:

ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;

它的初值为一个负数,该负数的绝对值等于总连接数的7/8.当阈值小于0时正常响应新连接事件,当阈值大于0时不再响应新连接事件,并将ngx_accept_disabled减1,代码如下:

if (ngx_accept_disabled > 0){  ngx_accept_disabled--;}else{ if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {  return; } ....}

这说明,当某个进程当前的连接数达到能够处理的总连接数的7/8时,负载均衡机制被触发,进程停止响应新连接。

参考:

《深入理解Nginx》 P328-P334.


  • 上一条:
    Fastdfs与nginx进行压缩图片比率
    下一条:
    5种nginx负载均衡配置方法分享
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • 在Linux系统中使用Iptables实现流量转发功能流程步骤(0个评论)
    • vim学习笔记-入门级需要了解的一些快捷键(0个评论)
    • 在centos7系统中实现分区并格式化挂载一块硬盘到/data目录流程步骤(0个评论)
    • 在Linux系统种查看某一个进程所占用的内存命令(0个评论)
    • Linux中grep命令中的10种高级用法浅析(0个评论)
    • 近期文章
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 在go语言中实现IP/CIDR的ip和netmask互转及IP段形式互转及ip是否存在IP/CIDR(0个评论)
    • PHP 8.4 Alpha 1现已发布!(0个评论)
    • Laravel 11.15版本发布 - Eloquent Builder中添加的泛型(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2016-11
    • 2017-07
    • 2017-10
    • 2017-11
    • 2018-01
    • 2018-02
    • 2020-03
    • 2020-04
    • 2020-05
    • 2020-06
    • 2021-02
    • 2021-03
    • 2021-04
    • 2021-06
    • 2021-07
    • 2021-08
    • 2021-09
    • 2021-10
    • 2021-11
    • 2021-12
    • 2022-01
    • 2022-03
    • 2022-04
    • 2022-08
    • 2022-11
    • 2022-12
    • 2023-01
    • 2023-02
    • 2023-03
    • 2023-06
    • 2023-07
    • 2023-10
    • 2023-12
    • 2024-01
    • 2024-04
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客