epoll惊群效应深度剖析
前情提要
我们一个基于Nginx+uWSGI+python的服务最近在高峰期经常会遇到负载高导致一些请求报错的情况,在单机qps只有差不多2000-3000左右的时候内核的cpu占用竟然高达超过20%,内核每秒上下文切换超过200w次,分析之后发现是nginx+uwsgi引发了惊群效应,导致性能急剧下降,通过上锁解决惊群问题之后服务恢复。基于这个排查过程,再加上我之前写过的关于epoll的分析最后也把惊群效应一笔带过,当时没有写完整,那咱这次就好好聊聊这个话题,我会先详细分析一下惊群效应产生的原因,然后拿nginx和uwsgi出来讨论一下他们各自对这种问题是如何处理的,他们的方案优劣是怎样的 本文关于源码的分析分别基于:linux 2.6及4.5内核、nginx1.8及1.16,uWSGI2.20
还不明白的朋友可以回头看看这个Nginx源码惊群视频讲解:Nginx惊群
1. 不使用epoll/select的情况下多进程是如何共享端口监听的?
不使用多路复用的情况下,进程要接受tcp连接必然要调用accept并且被阻塞,直到有一条连接到达,在这之前无法做别的事情,也即是说单个进程一次只能处理一条连接,业务处理完成之后调用close关闭连接,然后继续等待accept,循环往复,这种情况下是无法实现高并发的,所以一般会使用多进程再来同时处理更多的连接,多进程一般情况下有两种模式
第一种是由一个主进程进行accept监听,接受一个连接之后再fork出一个子进程,把连接丢给子进程去进行业务处理,然后主进程继续监听,这个是最简单的模式,由于只有一个进程在使用accept进行监听,不涉及多进程争抢的问题,当tcp连接事件到达后也只会唤醒这个监听进程,自然也不存在惊群效应
第二种形式是由主进程fork出一批子进程,子进程继承了父进程的这个监听端口,大家共享,然后一起监听。这里面就涉及到当多个进程在阻塞状态中等待同一个端口事件时内核的行为,接下来重点分析一下这个场景
// 进程调用accept时会进入inet_csk_accept,这是accept的核心所在
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct sock *newsk;
int error;
lock_sock(sk);
/* We need to make sure that this socket is listening,
* and that it has something pending.
*/
error = -EINVAL;
// 确认socket处于监听状态
if (sk->sk_state != TCP_LISTEN)
goto out_err;
/* Find already established connection */
/*接下来要找到一个建立好的连接*/
if (reqsk_queue_empty(&icsk->icsk_accept_queue)) { // 如果sock的连接队列是空
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
/* If this is a non blocking socket don't sleep */
error = -EAGAIN;
if (!timeo) // 如果设置了非阻塞模式则直接返回,err是喜闻乐见的-EAGAIN
goto out_err;
// 如果处于阻塞模式,则进入inet_csk_wait_for_connect,进程将处于阻塞状态,直接到新到的连接唤醒
error = inet_csk_wait_for_connect(sk, timeo);
if (error)
goto out_err;
}
// 到这里,连接队列会有至少一条可用连接用到返回
newsk =