[转帖]探索惊群 ③ - 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 编写 ...
随机推荐
- WMTS地图服务每一层级分辨率
目录 1. 概述 2. 详论 2.1. Web墨卡托 2.2. 大地经纬度 3. 参考 1. 概述 WMTS地图服务每一层级的分辨率是多少?关于这个问题以前推算过,但总是忘记了.网上查询又是一堆废话, ...
- 细说GaussDB(DWS)的2种查询优化技术
本文分享自华为云社区<GaussDB(DWS)查询优化技术大揭秘>,作者: 胡辣汤. 大数据时代,数据量呈爆发式增长,经常面临百亿.千亿数据查询场景,当数据仓库数据量较大.SQL语句执行效 ...
- 新晋“网红”Cat1 是什么
摘要:此Cat非彼Cat,它是今年物联网通信圈新晋网红"靓仔". 引言 今年5月,工信部发布了<关于深入推进移动物联网全面发展的通知>,明确提出推动存量2G.3G物联网 ...
- 华为云云原生视窗:一文回顾Q1精彩瞬间
摘要:一文速览2023年Q1期间华为云云原生相关动态信息. 华为云云原生动态 华为云海外首发CCI Serverless容器服务 在MWC23 巴展期间,华为云海外首发CCI Serverless容器 ...
- vue2升级vue3:Vue2/3插槽——vue3的jsx组件插槽slot怎么处理
插槽的作用 让用户可以拓展组件,去更好地复用组件和对其做定制化处理. Vue 实现了一套内容分发的 API,将<slot>元素作为承载分发内容的出口,这是vue文档上的说明.具体来说,sl ...
- 电商流量分析怎么做?试试这款数据工具 DataLeap!
更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 作为成熟的电商模式,货架场景可以让商家以更低的门槛入驻,让消费者完成更高销量的购买和复购. 在这一场景下,运营人员 ...
- C++正则表达式的初步使用
正则表达式(Regular Expressions),又被称为regex.regexp 或 RE,是一种十分简便.灵活的文本处理工具.它可以用来精确地找出某文本中匹配某种指定规则的内容.从C++11开 ...
- 【每日一题】35. [CQOI2009]中位数图 (前缀和,贡献值计算)
补题链接:Here 算法涉及:前缀和,贡献值计算 经典中位数计数问题,记得以前百度之星也出过类似的题,这道题有一个限定范围是要奇数区间的 我们很容易想到,奇数下标到偶数下标或者偶数下标到奇数下标的长度 ...
- node开发概述
一.Node开发概述 1. 为什么要学习服务器端开发 能够与后端程序员更加紧密的配合 网站业务逻辑前置,学习前端技术需要后端技术支撑(ajax) 扩宽知识视野,能够站在更高的角度审视整个项目 2. 服 ...
- C#设计模式08——桥接模式的写法
什么是C#桥接模式?桥接模式是一种结构型设计模式,它可以将抽象部分与实现部分分离,使它们可以独立地变化.这种模式通过将实现细节从抽象类中分离出来,从而让它们可以根据需要独立变化. 为什么要使用C#桥接 ...