《深入剖析ngx》—— 事件管理

1. 综述

ngx 是事件驱动,没有事件,ngx会一直阻塞在 epoll_wait 或 sigsuspend 上,ngx的事件有 IO事件,定时器事件。

2. 多路IO模型

ngx对多路复用IO进行了封装。
封装为 ngx_event_action_t 结构体,该结构体主要属性为 回调函数

为了方便使用,ngx定义了一些宏

如此使用多路IO时,无需关心具体的IO接口,只需要用 ngx_add_event() 等

ngx绑定epoll到 ngx_event_action_t,就是 给 ngx_event_action_t 属性赋值。

如此, ngx_event_action_t 就是 ngx_epoll_module_ctx.actions。

而调用这个 init 函数是在

其中 ecf->use 保存 解析 配置文件 use 指令获得的值,若使用 use epoll ,则这里 module 为 ngx_event_epoll_module,如此就调用了 ngx_event_epoll_module.init ,也就是绑定了 ngx_event_action_t 为 epoll.

所以我们得到如下框图

3. epoll

接口如下

epoll有 LT 和 ET 模式,LT是默认工作模式,支持 no-block , block,ET 只支持 no-block,ET效率高,
为了避免 ET模式下,读取所有数据,通常逻辑如下,
设置被监听套接字为 no-block,加入epoll,
epoll_wait,得到事件,
读取套接字,直到返回 EAGAIN(对于 面向包/令牌的文件,比如数据包套接字接口,规范式终端)或是 read(),write()返回的数据长度小于请求的数据长度(对于面向流的文件,比如pipe, FIFO,流套接口),才重新监听epoll

4. 负载均衡

ngx是多工作进程模型,所以可能出现 一个进程负责 1 个请求,一个进程负责 400 个请求的情况,为了避免这种情况,需要实现负载均衡。

4.1 客户端请求的负载均衡

ngx工作进程处理请求的源头是 监听套接字 接受客户端请求,但是 若多个工作进程 拥有监听套接字,当请求到来,则会出现进程同时抢请求的情况,这被称为 惊群。
ngx通过给监听套接字上锁的方法解决了这个问题。

ngx 定义了名为 ngx_use_accept_mutex 的全局变量。他是负载均衡的关键。

这个变量在每个工作进程初始化时,完成初始化。

只有多进程环境下,并开启了负载均衡才将 ngx_use_accept_mutex = 1

ngx默认开启负载均衡,用户若想避免负载均衡实现导致到开销 可以手动关闭。

若开启了负载均衡,则不会静态 添加监听套接字 到事件监控机制。

ngx_process_events_and_timers()完成 添加 监听套接字 到监控机制。
本函数根据 进程的负载情况 进行 监听套接字的 的持有和释放,若本进程繁忙则,不持有监听套接字,若空闲,则持有监听套接字。
监听套接字的持有 和 释放 需要 锁机制 实现同步和互斥。
ngx_process_events_and_timers 是位于 工作进程 的 大循环里,所以 持有监听套接字 是动态的。

ngx_accept_disabled 是记录当前进程的繁忙程度,若超过最大连接数的7/8,则为过载。
若进程过载,则不再持有监听套接字。若没有过载,则尝试争得锁,也就是监听套接字。

若争锁失败,则将监听套接字从事件监控中删除(若以前已经过监听套接字),若争锁成功,则添加监听套接字到事件监控(若以前没有得到监听套接字)。

如下若, ngx_accept_mutex_held 表示 是否持有锁。

  320 ngx_int_t
  321 ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
  322 {
  323     if (ngx_shmtx_trylock(&ngx_accept_mutex)) {
  324
  325         ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
  326                        "accept mutex locked");
  327
  328         if (ngx_accept_mutex_held && ngx_accept_events == 0) {
  329             return NGX_OK;
  330         }
  331
  332         if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
  333             ngx_shmtx_unlock(&ngx_accept_mutex);
  334             return NGX_ERROR;
  335         }
  336
  337         ngx_accept_events = 0;
  338         ngx_accept_mutex_held = 1;
  339
  340         return NGX_OK;
  341     }
  342
  343     ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
  344                    "accept mutex lock failed: %ui", ngx_accept_mutex_held);
  345
  346     if (ngx_accept_mutex_held) {
  347         if (ngx_disable_accept_events(cycle, 0) == NGX_ERROR) {
  348             return NGX_ERROR;
  349         }
  350
  351         ngx_accept_mutex_held = 0;
  352     }
  353
  354     return NGX_OK;
  355 }

若持有锁,则将 flag 变量添加 NGX_POST_EVENT标记。表示将所有事件延后处理。
因为 持有锁的,应该尽快使用资源,尽快释放锁,所以 将所有事件延后处理,可以对事件进行优先级排序,以尽快处理 锁相关事件,完成后立即释放锁,再处理其他事件。
没有 持有锁的,将 事件阻塞 设置较短时间,以尽快跳出阻塞,尝试获得锁。

若持有锁,将事件添加到链表,延迟处理,这里分了两个类别的事件,监听套接字事件(加入ngx_posted_accept_events链表),其他事件(加入ngx_posted_events链表)

ngx_process_events() 已经将事件缓存,若有监听套接字事件,则快于其他事件处理,完成后立即释放锁。

5. 多核绑定

为了提高cache命中率, ngx 提供 worker_cpu_affinity 指令,提高cpu亲和度。
使用 ps -F 选项可以看到 进程使用的cpu,PSR域

6. 超时管理

对于有些事件需进行超时管理,即 等待的事件没有在指定时间到达,就需要对这些情况做些处理。
如,客户端请求建立连接后,ngx等待读取报文头,但可能一直没有获得报文头,则应该认为这是非法请求,直接返回错误信息404,并释放资源。

超时管理有两个要点:

  1. 超时事件对象的组织,ngx使用红黑树。
  2. 超时事件对象的超时检测。有两种方法:
    (1) 设置一个定时器,每隔一段时间遍历树
    (2) 根据当前最早超时时间,设置一个定时器。

ngx为事件对象添加了如下超时属性

timedout 表示是否已经超时
timer_set 表示是否已经加入树,进行超时监控。
timer 是红黑树的节点

为了遍历树,ngx每个工作定义了如下变量。

ngx_event_timer_rbtree 是树对象,可以得到根节点。
ngx_event_timer_sentinel 是哨兵节点

初始化红黑树是在 ngx_event_process_init 内,所以每个工作进程有自己的树。

对于每个新建立的连接,会将connect对象加入超时管理
ngx_http_init_connection()

将事件对象加入超时管理,设置超时时间 c->listening->post_accept_timeout 。

ngx_add_timer() 完成将一个超时管理加入红黑树,首先比较key字段记录的超时时刻,判断超时事件是否已经加入树,若已经加入则调用 ngx_del_timer() 删除节点,在调用 ngx_rbtree_insert() 将超时事件对象加入树。

对于树上超时节点的管理有两种方法,
使用哪种方法取决于配置指令 timer_resolution

当 ngx_timer_resolution 不为0,则使用方案1(周期检查超时)

可以看出设置了两个变量:
flags 为 0 ,表示没有附加逻辑
timer 为 NGX_TIMER_INFINITE, 表示 事件阻塞为永久,
当 事件阻塞为永久时,由于 ngx 设置了定时器,所以能实现周期检查超时。

ngx_event_process_init() 设置了定时器

SIGALRM 的回调函数会设置 ngx_event_timer_alarm = 1。

由于 ngx_event_timer_alarm 为 1 , 所以会调用 ngx_time_update()

由于更新了时间,所以 ngx_event_expire_timers() 会执行

方案2
将 timer设置为最快方法超时事件的值,具体在 ngx_event_find_timer() ,用这个值设置 事件阻塞的超时时间,由于 flag 为 NGX_UPDATE_TIME,所以 ngx_time_update() 会执行,所以每次事件处理都会更新时间,若 客户端请求频率高,则导致高频率调用 gettimeofday() ,这是方案2的缺点。

那么如何管理超时事件节点?
由于使用红黑树,所以不需要遍历所有节点,只需要找到最近即将超时的对象,判断是否超时,若超时将其移出树,并设置超时标记(ev->timeout = 1),并调用超时回调函数。再处理下一个即将超时的节点,直到某个节点未超时 ,所有检测完毕。

posted on   开心种树  阅读(921)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

统计

点击右上角即可分享
微信分享提示