《深入剖析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,并释放资源。
超时管理有两个要点:
- 超时事件对象的组织,ngx使用红黑树。
- 超时事件对象的超时检测。有两种方法:
(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),并调用超时回调函数。再处理下一个即将超时的节点,直到某个节点未超时 ,所有检测完毕。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?