accept_mutex与性能的关系 (nginx)
注:运行环境CentOS 6+
背景
在对启动了20个worker的nginx进行压力测试的时候发现:如果把配置文件中event配置块中的accept_mutex开关打开(1.11.3版本之前默认开),就会出现worker压力不均,少量的worker的cpu利用率达到了98%,大部分的worker的压力只有1%左右;如果把accept_mutex关掉,所有的worker的压力差别就不大,而且QPS会有大幅提升;
分析过程
- nginx的 1(master)+N(worker) 多进程模型:master在启动过程中负责读取nginx.conf中配置的监听端口,然后加入到一个cycle->listening数组中。
- init_cycle函数中会调用init_module函数,init_module函数会调用所有注册模块的module_init函数完成相关模块所需资源的申请以及其他一些工作;其中event模块的module_init函数申请一块共享内存用于存储accept_mutex锁信息以及连接数信息
12345678910111213141516171819202122
//////////// nginx/src/event/ngx_event.c ///////
shm.size = size;
shm.name.len =
sizeof
(
"nginx_shared_zone"
) - 1;
shm.name.data = (u_char *)
"nginx_shared_zone"
;
shm.
log
= cycle->
log
;
if
(ngx_shm_alloc(&shm) != NGX_OK) {
return
NGX_ERROR;
}
shared = shm.addr;
ngx_accept_mutex_ptr = (ngx_atomic_t *) shared;
ngx_accept_mutex.spin = (ngx_uint_t) -1;
if
(ngx_shmtx_create(&ngx_accept_mutex, (ngx_shmtx_sh_t *) shared,
cycle->lock_file.data)
!= NGX_OK)
{
return
NGX_ERROR;
}
所有的worker进程均是由master进程通过fork() 函数启动的,所以所有的worker进程也就继承了master进程所有打开的文件描述符(包括之前创建的共享内存的fd)以及变量数据(这其中就包括之前创建的accept_mutex锁)。worker启动的过程中会调用各个模块的process_init函数,其中event模块的process_init函数中就会将master配置好的listening数组加入到epoll监听的events中,这样初始阶段所有的worker的epoll监听列表中都包含listening数组中的fd。
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 | //////////// nginx/src/event/ngx_event.c /////// /* for each listening socket */ ls = cycle->listening.elts; for (i = 0; i < cycle->listening.nelts; i++) { #if (NGX_HAVE_REUSEPORT) if (ls[i].reuseport && ls[i].worker != ngx_worker) { continue ; } #endif c = ngx_get_connection(ls[i].fd, cycle-> log ); if (c == NULL) { return NGX_ERROR; } c->type = ls[i].type; c-> log = &ls[i]. log ; c->listening = &ls[i]; ls[i].connection = c; rev = c->read; rev-> log = c-> log ; rev->accept = 1; ............... |
当各个worker实际运行时,就会执行ngx_process_events_and_timers函数,这个函数会获取accept_mutex锁,同时所有的事件也会增加一个NGX_POST_EVENTS标识,其他获取不到的锁的worker就会将listening数组中fd从epoll监听列表中移除,这样下次listenling数组发生新的连接时,只有持有accept_mutex的worker才会响应。
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | //////////// nginx/src/event/ngx_event.c /////// if (ngx_use_accept_mutex) { if (ngx_accept_disabled > 0) { ngx_accept_disabled--; } else { if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) { return ; } if (ngx_accept_mutex_held) { flags |= NGX_POST_EVENTS; //增加NGX_POST_EVENTS标识 } else { if (timer == NGX_TIMER_INFINITE || timer > ngx_accept_mutex_delay) { timer = ngx_accept_mutex_delay; } } } } //////////////////////// ngx_int_t ngx_trylock_accept_mutex(ngx_cycle_t *cycle) { if (ngx_shmtx_trylock(&ngx_accept_mutex)) { ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle-> log , 0, "accept mutex locked" ); if (ngx_accept_mutex_held && ngx_accept_events == 0) { return NGX_OK; } if (ngx_enable_accept_events(cycle) == NGX_ERROR) { //将cycle->listening加入到当前worker进程的epoll ngx_shmtx_unlock(&ngx_accept_mutex); return NGX_ERROR; } ngx_accept_events = 0; ngx_accept_mutex_held = 1; return NGX_OK; } ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle-> log , 0, "accept mutex lock failed: %ui" , ngx_accept_mutex_held); if (ngx_accept_mutex_held) { if (ngx_disable_accept_events(cycle, 0) == NGX_ERROR) { //将cycle->listening从当前worker进程的epoll移除 return NGX_ERROR; } ngx_accept_mutex_held = 0; } return NGX_OK; } |
对于持有accept_mutex锁的worker进程,会将epoll返回的fd放到队列中,尽可能的从epoll中接受新连接。当不持有accept_mutex 锁时在对队列中的这些fd进行处理。
1 2 3 4 5 6 7 8 9 10 11 | ///////////////// nginx/src/event/modules/ngx_epoll_module.c ////////// if (flags & NGX_POST_EVENTS) { queue = rev->accept ? &ngx_posted_accept_events : &ngx_posted_events; ngx_post_event(rev, queue); } else { rev->handler(rev); } |
worker每次处理从epoll中读取新连接结束后,就会调用accept处理新建连接,并调用新连接的异步回掉函数进行处理,在处理新建连接的同时会实时统计1/8 * worker_connection - free_connecttion 的差值ngx_accept_disabled 。处理accept新连接结束后,就会释放accept_mutex锁,然后在去处理一些普通的连接请求。worker的下次cycle,会首先判断ngx_accept_disabled的值,如果大于0说明该worker当前处理连接数大于总连接数的7/8,不再参与accept_mutex的竞争。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | //////////// nginx/src/event/ngx_event.c /////////// ( void ) ngx_process_events(cycle, timer, flags); delta = ngx_current_msec - delta; ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle-> log , 0, "timer delta: %M" , delta); ngx_event_process_posted(cycle, &ngx_posted_accept_events); if (ngx_accept_mutex_held) { ngx_shmtx_unlock(&ngx_accept_mutex); } if (delta) { ngx_event_expire_timers(); } ngx_event_process_posted(cycle, &ngx_posted_events); |
结论
- 上述分析的主要是accept_mutex打开的情况。对于不打开的情况,比较简单,所有worker的epoll都会监听listening数组中的所有fd,所以一旦有新连接过来,就会出现worker“抢夺资源“的情况。对于分布式的大量短链接来讲,打开accept_mutex选项较好,避免了worker争夺资源造成的上下文切换以及try_lock的锁开销。但是对于传输大量数据的tcp长链接来讲,打开accept_mutex就会导致压力集中在某几个worker上,特别是将worker_connection值设置过大的时候,影响更加明显。因此对于accept_mutex开关的使用,根据实际情况考虑,不可一概而论。
- 根据分析我们的压测程序,发现是采用的长tcp连接的方式,然后调用http请求;而且worker_connection也比较大,这样就出现了accept_mutex打开worker负载不均造成QPS下降的问题。
- 目前新版的Linux内核中增加了EPOLLEXCLUSIVE选项,nginx从1.11.3版本之后也增加了对NGX_EXCLUSIVE_EVENT选项的支持,这样就可以避免多worker的epoll出现的惊群效应,从此之后accept_mutex从默认的on变成了默认off。
欢迎技术探讨:sxhlinux@163.com
分类:
源码分析
【推荐】博客园的心动:当一群程序员决定开源共建一个真诚相亲平台
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】Flutter适配HarmonyOS 5知识地图,实战解析+高频避坑指南
【推荐】开源 Linux 服务器运维管理面板 1Panel V2 版本正式发布
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· tomcat为什么假死了
· 聊一聊 Linux 上对函数进行 hook 的两种方式
· C# 锁机制全景与高效实践:从 Monitor 到 .NET 9 全新 Lock
· 一则复杂 SQL 改写后有感
· golang中写个字符串遍历谁不会?且看我如何提升 50 倍
· 完成微博外链备案,微博中直接可以打开园子的链接
· 推荐 3 种 .NET Windows 桌面应用程序自动更新解决方案
· .NET 10 支持Linux/Unix 的Shebang(Hashbang)
· 一个基于 .NET 开源、模块化 AI 图像生成 Web 用户界面
· 上周热点回顾(6.9-6.15)