https://wenfh2020.com/2021/10/10/nginx-thundering-herd-accept-mutex/
由主进程创建的 listen socket,要被 fork 出来的子进程共享,但是为了避免多个子进程同时争抢共享资源,nginx 采用一种策略:使得多个子进程,同一时段,只有一个子进程能获取资源,就不存在共享资源的争抢问题。
成功获取锁的,能获取一定数量的资源,而其它没有成功获取锁的子进程,不能获取资源,只能等待成功获取锁的进程释放锁后,nginx 多进程再重新进入锁竞争环节。
- 探索惊群 ①
- 探索惊群 ② - accept
- 探索惊群 ③ - nginx 惊群现象
- 探索惊群 ④ - nginx - accept_mutex(★)
- 探索惊群 ⑤ - nginx - NGX_EXCLUSIVE_EVENT
- 探索惊群 ⑥ - nginx - reuseport
- 探索惊群 ⑦ - 文件描述符透传
1. 配置
nginx 通过修改配置开启 accept_mutex 功能特性。
1
2
3
4
5
6
|
# vim /usr/local/nginx/conf/nginx.conf
events {
...
accept_mutex on;
...
}
|
2. 解决方案
2.1. 负载均衡
nginx 子进程通过抢共享锁 实现负载均衡,现在用下面的伪代码去理解它的实现原理。
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
|
int main() {
efd = epoll_create();
while (1) {
if (is_disabled) {
...
/* 不抢,但是为了避免一直不抢,也要递减它的 disable 程度。*/
is_disabled = reduce_disabled();
} else {
/* 抢。*/
if (try_lock()) {
/* 抢锁成功,epoll 关注 listen_fd 的 POLLIN 事件。 */
if (!is_locked) {
epoll_ctl(efd, EPOLL_CTL_ADD, listen_fd, ...);
is_locked = true;
}
} else {
if (is_locked) {
/* 抢锁失败,epoll 不再关注 listen_fd 事件。 */
epoll_ctl(efd, EPOLL_CTL_DEL, listen_fd, ...);
is_locked = false;
}
}
}
/* 超时等待链接资源到来。 */
n = epoll_wait(...)
if (n > 0) {
if (is_able_to_accept) {
/* 链接资源到来,取出链接。*/
client_fd = accept();
/* 每次取出链接后,重新检查 disabled 值。*/
is_disabled = check_disabled();
}
}
if (is_locked) {
unlock();
}
}
return 0;
}
|
nginx 通过 ngx_accept_disabled 负载均衡数值控制抢锁的时机,每次 accept 完链接资源后,都检查一下它。
1
|
ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;
|
connection_n 最大连接数是固定的;free_connection_n 空闲连接数是变化的。只有在 ngx_accept_disabled > 0 的情况下,进程才不愿意抢锁,换句话说,就是已使用链接大于总链接的 7/8 了,空闲链接快用完了,原来拥有锁的进程才不会频繁去抢锁。
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
|
/* src/event/ngx_event.c */
ngx_int_t ngx_accept_disabled; /* 资源分配负载均衡值。 */
/* src/event/ngx_event_accept.c */
void ngx_event_accept(ngx_event_t *ev) {
...
do {
...
#if (NGX_HAVE_ACCEPT4)
if (use_accept4) {
s = accept4(lc->fd, &sa.sockaddr, &socklen, SOCK_NONBLOCK);
} else {
s = accept(lc->fd, &sa.sockaddr, &socklen);
}
#else
s = accept(lc->fd, &sa.sockaddr, &socklen);
#endif
...
/* 每次 accept 链接资源后,都检查一下负载均衡数值。*/
ngx_accept_disabled = ngx_cycle->connection_n / 8
- ngx_cycle->free_connection_n;
c = ngx_get_connection(s, ev->log);
...
} while (ev->available);
}
/* src/event/ngx_event.c */
void ngx_process_events_and_timers(ngx_cycle_t *cycle) {
...
if (ngx_use_accept_mutex) {
if (ngx_accept_disabled > 0) {
/* ngx_accept_disabled > 0,说明很少空闲链接了,放弃抢锁。 */
ngx_accept_disabled--;
} else {
/* 通过锁竞争,获得获取资源的权限。 */
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
...
}
}
...
}
|
2.2. 独占资源
2.2.1. 概述
核心逻辑在这个函数 ngx_trylock_accept_mutex,获得锁的子进程,可以将共享的 listen socket 通过 epoll_ctl 添加到事件驱动进行监控,当有资源到来时,子进程通过 epoll_wait 获得通知处理。而没有获得锁的子进程的 epoll 没有关注 listen socket 的事件,所以它们的 epoll_wait 是不会通知 listen socket 的事件。
2.2.2. 源码分析
通过调试查看函数调用的堆栈工作流程。
1
2
3
4
5
6
7
8
9
10
|
# 子进程获取锁添加然后 listen socket 逻辑。
ngx_trylock_accept_mutex (cycle=0x72a6a0) at src/event/ngx_event_accept.c:323
# 子进程循环处理网络事件和时钟事件函数。
0x0000000000442059 in ngx_process_events_and_timers (cycle=0x72a6a0) at src/event/ngx_event.c:223
# 子进程工作逻辑。
0x000000000044f7c2 in ngx_worker_process_cycle (cycle=0x72a6a0, data=0x0) at src/os/unix/ngx_process_cycle.c:719
0x000000000044c804 in ngx_spawn_process (cycle=0x72a6a0, proc=0x44f714 <ngx_worker_process_cycle>, data=0x0, name=0x4da39f "worker process", respawn=-3) at src/os/unix/ngx_process.c:199
0x000000000044eb1e in ngx_start_worker_processes (cycle=0x72a6a0, n=2, type=-3) at src/os/unix/ngx_process_cycle.c:344
0x000000000044e31c in ngx_master_process_cycle (cycle=0x72a6a0) at src/os/unix/ngx_process_cycle.c:130
0x000000000040bdcf in main (argc=1, argv=0x7fffffffe578) at src/core/nginx.c:383
|
参考:gdb 调试 nginx(附视频)
可以通过下面源码分析查看抢锁的流程。
1
2
3
4
5
6
7
8
9
10
11
12
|
ngx_worker_process_cycle
|-- ngx_process_events_and_timers
|-- ngx_trylock_accept_mutex
if |-- ngx_shmtx_trylock
|-- ngx_enable_accept_events
|-- ngx_add_event
|-- epoll_ctl(efd, EPOLL_CTL_ADD, listen_fd, ...);
else |-- ngx_disable_accept_events
|-- ngx_del_event
|-- epoll_ctl(efd, EPOLL_CTL_DEL, listen_fd, ...);
|-- ngx_process_events
|-- ngx_shmtx_unlock
|
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
|
/* src/event/ngx_event.c */
ngx_shmtx_t ngx_accept_mutex; /* 进程共享互斥锁。 */
ngx_uint_t ngx_use_accept_mutex; /* accept_mutex 开启状态。 */
ngx_uint_t ngx_accept_mutex_held; /* 表示当前进程是否可以获取资源。 */
ngx_int_t ngx_accept_disabled; /* 资源分配负载均衡值。 */
/* src/os/unix/ngx_process_cycle.c
* 子进程循环处理事件。*/
static void ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data) {
...
for ( ;; ) {
...
ngx_process_events_and_timers(cycle);
...
}
}
/* src/event/ngx_event.c
* 定时器事件和网络事件处理。*/
void ngx_process_events_and_timers(ngx_cycle_t *cycle) {
...
if (ngx_use_accept_mutex) {
/* 当 ngx_accept_disabled 越小,那么就越快执行抢锁的逻辑。 */
if (ngx_accept_disabled > 0) {
ngx_accept_disabled--;
} else {
/* 通过锁竞争,获得获取资源的权限。 */
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
...
}
}
...
/* 处理事件。 */
(void) ngx_process_events(cycle, timer, flags);
...
if (ngx_accept_mutex_held) {
/* 释放锁。 */
ngx_shmtx_unlock(&ngx_accept_mutex);
}
...
}
/* src/event/ngx_event_accept.c */
ngx_int_t ngx_trylock_accept_mutex(ngx_cycle_t *cycle) {
/* 尝试获得锁。 */
if (ngx_shmtx_trylock(&ngx_accept_mutex)) {
...
if (ngx_accept_mutex_held && ngx_accept_events == 0) {
return NGX_OK;
}
/* 将 listen socket 添加到 epoll 事件驱动里。 */
if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
ngx_shmtx_unlock(&ngx_accept_mutex);
return NGX_ERROR;
}
ngx_accept_events = 0;
/* 修改持锁的状态。 */
ngx_accept_mutex_held = 1;
return NGX_OK;
}
if (ngx_accept_mutex_held) {
/* 获取锁失败,如果之前是曾经成功获取锁的,不能再获取资源了,将 listen socket 从 epoll 里删除。 */
if (ngx_disable_accept_events(cycle, 0) == NGX_ERROR) {
return NGX_ERROR;
}
/* 改变持锁的状态。 */
ngx_accept_mutex_held = 0;
}
return NGX_OK;
}
/* 子进程 epoll_ctl 关注 listen socket 事件。 */
ngx_int_t ngx_enable_accept_events(ngx_cycle_t *cycle) {
ngx_uint_t i;
ngx_listening_t *ls;
ngx_connection_t *c;
ls = cycle->listening.elts;
for (i = 0; i < cycle->listening.nelts; i++) {
c = ls[i].connection;
...
/* 将共享的 listen socket 通过 epoll_ctl 添加到子进程的 epoll 中,
* 当该 socket 有新的链接进来,epoll_wait 会通知处理。 */
if (ngx_add_event(c->read, NGX_READ_EVENT, 0) == NGX_ERROR) {
return NGX_ERROR;
}
}
return NGX_OK;
}
/* 子进程 epoll_ctl 取消关注 listen socket 事件。 */
static ngx_int_t ngx_disable_accept_events(ngx_cycle_t *cycle, ngx_uint_t all) {
ngx_uint_t i;
ngx_listening_t *ls;
ngx_connection_t *c;
ls = cycle->listening.elts;
for (i = 0; i < cycle->listening.nelts; i++) {
c = ls[i].connection;
...
/* 子进程将共享的 listen socket 从 epoll 中删除,不再关注它的事件。 */
if (ngx_del_event(c->read, NGX_READ_EVENT, NGX_DISABLE_EVENT)
== NGX_ERROR)
{
return NGX_ERROR;
}
}
return NGX_OK;
}
|
2.2.3. 抢锁成功率
很多时候,原来抢到锁的进程,大概率会重新抢到锁,原因在于 抢锁时机。
- 原来抢到锁的进程,在抢到锁后会先处理完事件(
ngx_process_events),然后才会释放锁,在这个过程中,其它进程一直抢不到:因为它们都是盲目地抢,不知道锁什么时候释放,而抢到锁的进程它释放锁后,自己马上抢回,相对于其它进程盲目地抢,它的成功率更高。
- 原来抢到锁的进程,什么时候才会不抢呢,就是要满足这个条件:ngx_accept_disabled > 0。因为 ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n,一般情况下,当已使用链接超过了 7/8 了,也就是说空闲链接快用完了,才不愿意抢锁了。如果配置的链接总数很大,那么预分配的空闲链接没那么快用完,那么原进程就一直抢,因为它一释放锁就马上去抢,它抢到锁的成功率自然高!
所以基于上面两个条件,可能会导致:有些进程很忙,有些进程比较闲。
3. 缺点
- nginx 是多进程框架,accept_mutex 解决惊群的策略,使得在同一个时间段,多个子进程始终只有一个子进程可以 accept 链接资源,这样,不能充分利用其它子进程进行并发处理,在密集的短链接场景中,链接的吞吐将会遇到瓶颈。
- 避免了内核抢锁问题,转换为应用层抢锁,虽然抢的频率降低,但是进程多了,抢锁效率依然是个问题。
- 通过
ngx_accept_disabled 去解决负载均衡问题,因为上述抢锁时机问题,可能会导致某个子进程长时间占用锁,其它子进程得不到 accept 链接资源的机会。
通过 nginx 的更新日志,我们发现 2016 年这个 accept_mutex 功能被默认关闭。
1
2
3
4
|
Changes with nginx 1.11.3 26 Jul 2016
*) Change: now the "accept_mutex" directive is turned off by default.
...
|
4. 参考
- “惊群”,看看nginx是怎么解决它的
在说nginx前,先来看看什么是“惊群”?简单说来,多线程/多进程(linux下线程进程也没多大区别)等待同一个socket事件,当这个事件发生时,这些线程/进程被同时唤醒,就是惊群.可以想见,效率很 ...
- Nginx学习之一-惊群现象
惊群问题(thundering herd)的产生 在建立连接的时候,Nginx处于充分发挥多核CPU架构性能的考虑,使用了多个worker子进程监听相同端口的设计,这样多个子进程在accept建立新连 ...
- 【转载】“惊群”,看看nginx是怎么解决它的
原文:http://blog.csdn.net/russell_tao/article/details/7204260 在说nginx前,先来看看什么是“惊群”?简单说来,多线程/多进程(linux下 ...
- 【Nginx】惊群问题
转自:江南烟雨 惊群问题的产生 在建立连接的时候,Nginx处于充分发挥多核CPU架构性能的考虑,使用了多个worker子进程监听相同端口的设计,这样多个子进程在accept建立新连接时会有争抢,这会 ...
- Nginx模型 & 惊群问题
这篇写的不错 http://www.cnblogs.com/linguoguo/p/5511293.html Nginx为啥性能高-多进程异步IO模型 1. 对于每个worker进程来说,独立的进程, ...
- NGINX怎样处理惊群的
写在前面 写NGINX系列的随笔,一来总结学到的东西,二来记录下疑惑的地方,在接下来的学习过程中去解决疑惑. 也希望同样对NGINX感兴趣的朋友能够解答我的疑惑,或者共同探讨研究. 整个NGINX系列 ...
- Nginx惊群处理
惊群:是指在多线程/多进程中,当有一个客户端发生链接请求时,多线程/多进程都被唤醒,然后只仅仅有一个进程/线程处理成功,其他进程/线程还是回到睡眠状态,这种现象就是惊群. 惊群是经常发生现在serve ...
- Nginx中的惊群现象解决方法
*什么是惊群现象?Nginx中用了什么方法来避免这种问题的发生?本篇就解决这两个问题...→_→* 惊群现象的定义与危害 在Nginx中,每一个worker进程都是由master进程fork出来的.m ...
- Nginx惊群问题
Nginx惊群问题 "惊群"概念 所谓惊群,可以用一个简单的比喻来说明: 一群等待食物的鸽子,当饲养员扔下一粒谷物时,所有鸽子都会去争抢,但只有少数的鸽子能够抢到食物, 大部分鸽子 ...
- nginx&http 第三章 惊群
惊群:概念就不解释了. 直接说正题:惊群问题一般出现在那些web服务器上,Linux系统有个经典的accept惊群问题,这个问题现在已经在内核曾经得以解决,具体来讲就是当有新的连接进入到accept队 ...
随机推荐
- 基于FPGA的数字钟设计---第三版---郝旭帅电子设计团队
本篇为各位朋友介绍基于FPGA的数字钟设计---第三版. 功能说明: 在数码管上面显示时分秒(共计六个数码管,前两个显示小时:中间两个显示分钟:最后两个显示秒). 利用按键可以切换24/12小时制(默 ...
- 2020-11-05:谈一下TCP的拥塞控制。
福哥答案2020-11-05: 所谓拥塞控制,是指防止过多的数据注入网络,保证网络中的路由器或链路不致过载.出现拥塞时,端点并不了解到拥塞发生的细节,对通信连接的端点来说,拥塞旺旺表现为通信时延的增加 ...
- SQL Server系列:系统函数之聚合函数
聚合函数:指对一组值执行计算,并返回单个值.除了 Count(统计函数) 外,聚合函数都会忽略 Null 值 聚合函数经常与 SELECT 语句的 GROUP BY 子句一起使用 1.Avg():返回 ...
- Spring Cloud 学习推荐
学习 Spring Boot Spring tutorials | Java Web Development, Spring Cloud Programming tutorials Spring Bo ...
- 4种Python中基于字段的不使用元类的ORM实现方法
本文分享自华为云社区<Python中基于字段的不使用元类的ORM实现>,作者: 柠檬味拥抱 . 不使用元类的简单ORM实现 在 Python 中,ORM(Object-Relational ...
- 一种DWS迁移Oracle的CONNECT BY语法的方案
摘要:本文提供一种GaussDB DWS迁移CONNECT BY语法方案. 本文分享自华为云社区<GaussDB(DWS)迁移 - oracle兼容 -- CONNECT BY迁移>,作者 ...
- Materialize MySQL引擎:MySQL到Click House的高速公路
摘要: MySQL到ClickHouse数据同步原理及实践 引言 熟悉MySQL的朋友应该都知道,MySQL集群主从间数据同步机制十分完善.令人惊喜的是,ClickHouse作为近年来炙手可热的大数据 ...
- 【“互联网+”大赛华为云赛道】IoT命题攻略:仅需四步,轻松实现场景智能化设计
摘要:仅需四步,轻松实现场景智能化设计,作品开发超轻松. 本文分享自华为云社区<["互联网+"大赛华为云赛道]IoT命题攻略:仅需四步,轻松实现场景智能化设计>,作者: ...
- 开心档之MySQL 导入数据
MySQL 导入数据 本章节我们为大家介绍几种简单的 MySQL 导入数据命令. 1.mysql 命令导入 使用 mysql 命令导入语法格式为: mysql -u用户名 -p密码 < 要导入的 ...
- 火山引擎 DataTester:抖音的设计团队是如何用 A/B 测试实现高效优化的?
更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 对 C 端产品而言,产品的每一个细节设置都或多或少影响着用户的产品体验,本文介绍字节跳动的 A/B 实验文化的同时 ...