[转帖]探索惊群 ③ - nginx 惊群现象
https://wenfh2020.com/2021/09/29/nginx-thundering-herd/
本文将通过测试,重现 nginx(1.20.1) 的惊群现象,并深入 Linux (5.0.1) 内核源码,剖析惊群原因。
- 探索惊群 ①
- 探索惊群 ② - accept
- 探索惊群 ③ - nginx 惊群现象(★)
- 探索惊群 ④ - nginx - accept_mutex
- 探索惊群 ⑤ - nginx - NGX_EXCLUSIVE_EVENT
- 探索惊群 ⑥ - nginx - reuseport
- 探索惊群 ⑦ - 文件描述符透传
1. nginx 惊群现象
在不配置 nginx 处理惊群特性的情况下,通过 strace 命令观察 nginx 的系统调用日志。
在 ubuntu 14.04 系统,由简单的 telnet 测试可见:有的进程被唤醒后获取资源失败——惊群现象发生了!
先不配置 accept_mutex,reuseport 等特性。
- telnet 测试命令。
1 |
telnet 127.0.0.1 80 |
- nginx 工作流程。
1 |
# strace -f -s 512 -o /tmp/nginx.log /usr/local/nginx/sbin/nginx |

- 惊群现象。
1 |
79982 epoll_wait(10, <unfinished ...> |
2. 原因
惊群现象出现,有的子进程被唤醒但是并没有 accept 到链接资源。原因:
两个子进程通过 epoll_ctl 添加关注了主进程创建的 socket,当该 listen socket 没有资源时,子进程都通过 epoll_wait 进入了阻塞睡眠状态。也就是子进程分别往 socket.wq 等待队列添加了各自的等待事件。
因为添加的方式是 add_wait_queue,而不是 add_wait_queue_exclusive,add_wait_queue 并没有设置 WQ_FLAG_EXCLUSIVE 排它唤醒标识,所以当 listen socket 的资源到来时,内核通过 __wake_up_common 去唤醒两个子进程去 accept 获取资源。
如果只有一个链接资源,那么 nginx 的两个子进程被唤醒,当然只有一个子进程能成功,另外一个则无功而返。
- socket 结构。
1 |
/* include/linux/net.h*/ |
- 进程在 epoll_ctl 关注 listen socket 时,添加了当前进程的等待事件到 socket.wq 等待队列,进程的 epoll 唤醒回调函数 ep_poll_callback 与 socket 关联起来了。
1 |
/* fs/eventpoll.c */ |
- 当 listen socket 的资源到来,唤醒等待的进程。因为 add_wait_queue 没有添加 WQ_FLAG_EXCLUSIVE 标识,所以两个子进程被唤醒。
1 |
/* kernel/sched/wait.c |
3. 原理
3.1. 基本原理
先捋一捋这个通知唤醒的工作流程:tcp 产生链接资源后唤醒阻塞等待的子进程去 accept 获取。
tcp 协议的链接是通过三次握手实现的,而完整的链接资源是服务端在第三次握手中产生的,服务端会将新的链接资源存储在 listen socket 的完全队列中。
nginx 作为高性能服务程序,在 Linux 系统,它处理网络事件时,一般会采用 epoll 事件驱动。它通过
epoll_wait等待事件,当通过 epoll_ctl 关注的 tcp listen socket 产生事件时,阻塞等待的 epoll_wait 被唤醒去 accept 链接资源。
3.2. 等待唤醒流程
- 进程通过 epoll_ctl 监控 listen socket 的 EPOLLIN 事件。
- 进程通过 epoll_wait 阻塞等待监控的 listen socket 事件触发,然后返回。
- tcp 第三次握手,服务端产生新的链接资源。
- 内核将链接资源保存到 listen socket 的完全队列中。
- 内核唤醒步骤2的进程去 accept 获取 listen socket 完全队列中的链接资源。

3.3. 内核原理
通过下图,了解一下服务端 tcp 的第三次握手和 epoll 内核的等待唤醒工作流程。

- 进程通过 epoll_create 创建 eventpoll 对象。
- 进程通过 epoll_ctl 添加关注 listen socket 的 EPOLLIN 可读事件。
- 接步骤 2,epoll_ctl 还将 epoll 的 socket 唤醒等待事件(唤醒函数:ep_poll_callback)通过 add_wait_queue 函数添加到 socket.wq 等待队列。
当 listen socket 有链接资源时,内核通过 __wake_up_common 调用 epoll 的 ep_poll_callback 唤醒函数,唤醒进程。
- 进程通过 epoll_wait 等待就绪事件,往 eventpoll.wq 等待队列中添加当前进程的等待事件,当 epoll_ctl 监控的 socket 产生对应的事件时,被唤醒返回。
- 客户端通过 tcp connect 链接服务端,三次握手成功,第三次握手在服务端进程产生新的链接资源。
- 服务端进程根据 socket.wq 等待队列,唤醒正在等待资源的进程处理。例如 nginx 的惊群现象,__wake_up_common 唤醒等待队列上的两个等待进程,调用 ep_poll_callback 去唤醒 epoll_wait 阻塞等待的进程。
- ep_poll_callback 唤醒回调会检查 listen socket 的完全队列是否为空,如果不为空,那么就将 epoll_ctl 监控的 listen socket 的节点 epi 添加到
就绪队列:eventpoll.rdllist,然后唤醒 eventpoll.wq 里通过 epoll_wait 等待的进程,处理 eventpoll.rdllist 上的事件数据。 - 睡眠在内核的 epoll_wait 被唤醒后,内核通过 ep_send_events 将就绪事件数据,从内核空间拷贝到用户空间,然后进程从内核空间返回到用户空间。
- epoll_wait 被唤醒,返回用户空间,读取 listen socket 返回的 EPOLLIN 事件,然后 accept listen socket 完全队列上的链接资源。
【注意】 有了 socket.wq 为啥还要有 eventpoll.wq 啊?因为 listen socket 能被多个进程共享,epoll 实例也能被多个进程共享!
添加等待事件流程:
epoll_ctl -> listen socket -> add_wait_queue <+ep_poll_callback+> -> socket.wq ==> epoll_wait -> eventpoll.wq
唤醒流程:
tcp_v4_rcv -> socket.wq -> __wake_up_common -> ep_poll_callback -> eventpoll.wq -> wake_up_locked -> epoll_wait -> accept
4. 内核源码分析
4.1. TCP 三次握手
客户端主动链接服务端,TCP 三次握手成功后,服务端产生新的 tcp 链接资源,内核将唤醒 socket.wq 上的等待进程,通过 accept 从 listen socket 上的 全链接队列 中获取 TCP 链接资源。

参考:《[内核源码] 网络协议栈 - tcp 三次握手状态》 《[内核源码] 网络协议栈 - listen (tcp)》
1 |
/* include/net/sock.h */ |
4.2. epoll
4.2.1. epoll_wait 逻辑
epoll_wait 它的核心实现逻辑并不复杂,先添加进程的等待事件,然后检查就绪队列是否有就绪事件,如果没有就绪事件就睡眠等待,如果有事件就唤醒,将就绪事件从内核空间拷贝到用户空间,然后删除进程的等待事件。
1 |
#------------------- *用户空间* --------------------------- |
1 |
/* fs/eventpoll.c */ |
4.2.2. epoll_wait 睡眠等待逻辑
epoll_wait 通过 __add_wait_queue_exclusive 函数添加 WQ_FLAG_EXCLUSIVE 排它性唤醒属性的等待事件到等待队列,表明当 ep_poll_callback 回调函数被调用时(请看下面 ep_poll 源码的英文注释),拥有 epoll fd 的进程只能有一个被唤醒处理资源(有可能有多个进程共享 epoll,而 nginx 每个子进程都有自己独立的 epoll 实例,不共享)。通过__remove_wait_queue 函数删除对应的等待事件。
1 |
/* include/linux/wait.h */ |
4.2.3. epoll_wait 唤醒流程
4.2.3.1. socket 注册唤醒函数
epoll_ctl -> listen socket -> add_wait_queue <+ep_poll_callback+> -> socket.wq
函数调用堆栈。
通过函数堆栈,可以发现:epoll 里的进程睡眠唤醒函数 ep_poll_callback,与 tcp socket 关联起来了,睡眠事件添加到 socket 的睡眠队列里,当 socket 有对应的就绪事件,就会触发对应的函数,在这里就会触发 ep_poll_callback。
1 |
init_waitqueue_func_entry() (/root/linux-5.0.1/include/linux/wait.h:89) |
- 内核源码。
1 |
/* epoll 结构对象。*/ |
4.2.3.2. epoll_wait 唤醒
tcp_v4_rcv -> socket.wq -> __wake_up_common -> ep_poll_callback -> eventpoll.wq -> wake_up_locked -> epoll_wait
1 |
/* |
5. 压测
httpclient <–> nginx <–> httpserver
nginx 作为代理,httpclient 模拟多个短链接发包,测试 nginx 的惊群问题。

5.1. 测试环境
| cpu 核心 | 内存 | 系统 | nginx 版本 | nginx 子进程个数 | 测试并发数 |
|---|---|---|---|---|---|
| 4 | 4g | ubuntu(14.04) | 1.20.1 | 2 | 10000 |
5.2. 测试源码
用 golang 实现的简单的测试 demo,源码详见:github。
- 测试服务:httpserver,简单的接收数据和回复数据。
1 |
package main import ( |
- 测试客户端:httpclient,通过 golang 多协程,模拟多个客户端进行简单的数据发送和接收。
1 |
package main import ( |
- 运行测试客户端 httpclient,并发 1w 个短链接。
1 |
./httpclient --cnt 10000 |
5.3. nginx
- nginx 转发配置。
1 |
# vim /usr/local/nginx/conf/nginx.conf worker_processes 2; |
- nginx 启动后,主进程和子进程的运行情况。
1 |
root 79980 1 0 22:28 ? 00:00:00 nginx: master process /usr/local/nginx/sbin/nginx |
nginx 测试结果。
从 strace 统计的系统调用数据可见:79982 子进程的 accept4 系统调用有 195 个错误,因为进程被唤醒后,异步调用 accept4 去获取资源,有时它获取资源失败了,返回
EAGAIN错误,也就是说进程被唤醒后做了无用功。因为 strace 监控进程,还要写日志,处理速度应该会比正常的慢,所以测试客户端并发 1w 个,但是监控的进程只调用了 accept4 处理了 1356 个,失败了 195 个。
1356 - 195 = 1161,刚好是 nginx accept 成功了新的连接,然后转发数据到目标服务 connect 的系统调用次数。
因为 connect 也是异步的,所以调用后马上会返回错误,这是正常的;在 connect 前,accept 的新 socket 已经被 epoll_ctl 关注了,所以 connect 的结果会通过 epoll_wait 返回。
1 |
# strace -C -T -ttt -p 79982 -o strace.log |
5.4. 惊群影响
惊群使得部分进程唤醒做了无用功,我们对比一下惊群与非惊群两个场景的数据。
惊群的系统资源损耗总体上要比非惊群的高,参考两个场景的 vmstat 虚拟内存统计数据:in 中断数据和 cs 上下文切换数据。
开启了 4 个 nginx 子进程,进行压力测试。
这里压测比较简单,只查看了部分数据,也不太严谨,至于系统负载和CPU使用率,有兴趣的朋友可以在实际应用场景中再观察对比。
- 压测脚本。用 shell 脚本简单调用了上面的 httpclient 测试客户端进行测试。
1 |
#!/bin/bash
test() {
|
- nginx 惊群数据,主要看 in 中断次数,cs 上下文切换次数。
1 |
# vmstat 1 |
- 开启 reuseport 避免惊群的特性,nginx 数据。
1 |
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- |
- 对比两个场景的中断数据。(th_in:惊群,re_in:非惊群。)

- 对比两个场景的上下文切换数据。(th_cs:惊群,re_cs:非惊群。)

6. 小结
- 惊群本质是进程睡眠和唤醒问题,重点理解 tcp 结合 epoll 睡眠和唤醒的时机以及工作流程。
- 避免惊群,内核源码需要重点理解
WQ_FLAG_EXCLUSIVE标识的作用。
7. 参考
- Nginx的accept_mutex配置
- Nginx 是如何解决 epoll 惊群的
- 关于ngx_trylock_accept_mutex的一些解释
- linux性能诊断-perf
- 牛逼的Linux性能剖析—perf
- NGINX Reverse Proxy
- nginx实现请求转发
- test_epoll_thundering_herd
[转帖]探索惊群 ③ - nginx 惊群现象的更多相关文章
- Nginx惊群处理
惊群:是指在多线程/多进程中,当有一个客户端发生链接请求时,多线程/多进程都被唤醒,然后只仅仅有一个进程/线程处理成功,其他进程/线程还是回到睡眠状态,这种现象就是惊群. 惊群是经常发生现在serve ...
- Nginx惊群问题
Nginx惊群问题 "惊群"概念 所谓惊群,可以用一个简单的比喻来说明: 一群等待食物的鸽子,当饲养员扔下一粒谷物时,所有鸽子都会去争抢,但只有少数的鸽子能够抢到食物, 大部分鸽子 ...
- nginx集群报错“upstream”directive is not allow here 错误
nginx集群报错“upstream”directive is not allow here 错误 搭建了一个服务器, 采用的是nginx + apache(多个) + php + mysql(两个) ...
- Tomcat集群,Nginx集群,Tomcat+Nginx 负载均衡配置,Tomcat+Nginx集群
Tomcat集群,Nginx集群,Tomcat+Nginx 负载均衡配置,Tomcat+Nginx集群 >>>>>>>>>>>> ...
- Redis+Tomcat+Nginx集群实现Session共享,Tomcat Session共享
Redis+Tomcat+Nginx集群实现Session共享,Tomcat Session共享 ============================= 蕃薯耀 2017年11月27日 http: ...
- 扎实基础之从零开始-Nginx集群分布式.NET应用
1 扎实基础之快速学习Nginx Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,并在一个BSD-like 协议下发行.其特点是占有内存少 ...
- Nginx集群及代理的应用
目录 1 大概思路... 1 2 了解Nginx及文档资源... 1 3 Nginx命令模块及进程结构... 2 4 解读Nginx配置... 3 5 ...
- Nginx集群之WCF分布式局域网应用
目录 1 大概思路... 1 2 Nginx集群WCF分布式局域网结构图... 1 3 关于WCF的BasicHttpBinding. 1 4 编写WC ...
- Nginx集群之WCF分布式身份验证(支持Soap)
目录 1 大概思路... 1 2 Nginx集群之WCF分布式身份验证... 1 3 BasicHttpBinding.ws2007HttpBinding. 2 4 ...
- Nginx集群之WCF大文件上传及下载(支持6G传输)
目录 1 大概思路... 1 2 Nginx集群之WCF大文件上传及下载... 1 3 BasicHttpBinding相关配置解析... 2 4 编写 ...
随机推荐
- 简单介绍JDK、JRE、JVM三者区别
简单介绍JDK vs JRE vs JVM三者区别 文编|JavaBuild 哈喽,大家好呀!我是JavaBuild,以后可以喊我鸟哥,嘿嘿!俺滴座右铭是不在沉默中爆发,就在沉默中灭亡,一起加油学习, ...
- LeetCode 947. 移除最多的同行或同列石头 并查集
传送门 思路 干货太干就不太好理解了,以下会有点话痨( ̄▽ ̄)" 首先题目给了一个二维stones数组,存储每个石子的坐标,因为在同行或者同列的石子最终可以被取到只剩下一个,那么我们将同行同 ...
- 获取yml自定义内容的方式
yml内容 yml: login: name: zhangsan age: 18 pass: 123456 方式一: 创建实体类 @Configuration @ConfigurationProper ...
- 深入浅出Sqoop之迁移过程源码分析
[摘要]Sqoop是一种用于在Apache Hadoop和结构化数据存储(如关系数据库)之间高效传输批量数据的工具 .本文将简单介绍Sqoop作业执行时相关的类及方法,并将该过程与MapReduce的 ...
- 大数据处理黑科技:揭秘PB级数仓GaussDB(DWS) 并行计算技术
摘要:通过这篇文章,我们了解了GaussDB(DWS)并行计算技术的原理以及调优策略.希望广大开发者朋友们能够在实践中尝试该技术,更好地进行性能优化. 随着硬件系统的越来越好,数据库运行的CPU.磁盘 ...
- 多语言ASR?没有什么听不懂,15种语言我全都要
摘要:在这篇博文中,我们介绍来自Google的一篇论文<Scaling End-to-End Models for Large-Scale Multilingual ASR>,来看看如何构 ...
- 本地已存在jar包,maven打包还是去下载
解决方法 在pom文件里面添加以下: file后修改为本地仓库的位置 oss file:C:\Users\admin\.m2\repository <pluginRepositories> ...
- 如何通过appuploader把ipa文件上传到App Store教程步骤
iOS APP上架App Store其中一个步骤就是要把ipa文件上传到App Store! 下面进行步骤介绍! 利用Appuploader这个软件,可以在Windows.Linux或Mac系统中 ...
- 火山引擎DataLeap如何解决SLA治理难题(三): 平台架构与未来展望
更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 平台架构总结 火山引擎 DataLeap SLA平台整体主要分为基础组件.规划式治理服务.响应式治理服务三大块,系 ...
- JQuery 弹出模态窗口
index.html <!DOCTYPE html> <html> <head> <!-- Contact Form CSS files --> < ...