[转帖]探索惊群 ③ - 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 编写 ...
随机推荐
- 牛刀小试基本语法,Go lang1.18入门精炼教程,由白丁入鸿儒,go lang基本语法和变量的使用EP02
书接上回,Go lang1.18首个程序的运行犹如一声悠扬的长笛,标志着并发编程的Go lang巨轮正式开始起航.那么,在这艘巨轮之上,我们首先该做些什么呢?当然需要了解最基本的语法,那就是基础变量的 ...
- 干货分享丨从MPG 线程模型,探讨Go语言的并发程序
摘要:Go 语言的并发特性是其一大亮点,今天我们来带着大家一起看看如何使用 Go 更好地开发并发程序. 我们都知道计算机的核心为 CPU,它是计算机的运算和控制核心,承载了所有的计算任务.最近半个世纪 ...
- 【“互联网+”大赛华为云赛道】CloudIDE命题攻略:明确业务场景,快速开发插件
摘要:基于华为云CloudIDE和插件开发框架自行设计并开发插件. IDE是每个开发人员必备的生产工具,一款好的IDE + 插件的组合,除了帮助开发者把编写代码.组织项目.编译运行放在一个环境中外,还 ...
- GaussDB(DWS) NOT IN优化技术解密:排他分析场景400倍性能提升
摘要:本文针对8.1.2版本中的NOT IN场景的Mixed-HashJoin新技术进行介绍.该技术在GaussDB(DWS)与招商银行的联创项目中落地,为招商银行的批量作业带来了总体15%的性能提升 ...
- HBuilderX获取iOS证书的打包步骤
简介: 目前app开发,很多企业都用H5框架来开发,而uniapp又是这些h5框架里面最成熟的,因此hbuilderx就成为了开发者的首选.然而,打包APP是需要证书的,那么这个证书又是如何获得呢? ...
- 如何配置Apple推送证书 push证书
转载:如何配置Apple推送证书 push证书 想要制作push证书,就需要使用快捷工具appuploader工具制 作证书,然后使用Apple的推送功能配置push证书,就可以得到了.PS:pu ...
- ipa文件怎么安装到iPhone手机上?
ipa文件怎么安装到iPhone手机上? 无需越狱帮你把ipa文件安装到苹果手机上 E86苹果签名简介:点击可查看 很多人都知道apk文件是安卓的app应用程序文件名,但有人知道苹果ios的app ...
- 负载均衡 —— SpringCloud Netflix Ribbon
Ribbon 简介 Ribbon 是 Netfix 客户端的负载均衡器,可对 HTTP 和 TCP 客户端的行为进行控制.为 Ribbon 配置服务提供者地址后,Ribbon 就可以基于某种负载均衡算 ...
- python发送邮件+多人+附件 !!!!
import smtplib import os from email.header import Header from email.mime.text import MIMEText # shen ...
- 浅谈locust 性能压测使用
1. 基本介绍 Locust是一个开源的负载测试工具,用于模拟大量用户并发访问一个系统或服务,以评估其性能和稳定性.编写语言为Python,可通过Python来自定义构建性能压测场景脚本.Locust ...