nginx&http 第三章 惊群

惊群:概念就不解释了。

直接说正题:惊群问题一般出现在那些web服务器上,Linux系统有个经典的accept惊群问题,这个问题现在已经在内核曾经得以解决,具体来讲就是当有新的连接进入到accept队列的时候,内核唤醒且仅唤醒一个进程来处理。

/*
 * The core wakeup function. Non-exclusive wakeups (nr_exclusive == 0) just
 * wake everything up. If it's an exclusive wakeup (nr_exclusive == small +ve
 * number) then we wake all the non-exclusive tasks and one exclusive task.
 *
 * There are circumstances in which we can try to wake a task which has already
 * started to run but is not in state TASK_RUNNING. try_to_wake_up() returns
 * zero in this (rare) case, and we handle it by continuing to scan the queue.
 */
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
            int nr_exclusive, int wake_flags, void *key)
{
    wait_queue_t *curr, *next;

    list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
        unsigned flags = curr->flags;

        if (curr->func(curr, mode, wake_flags, key) &&
                (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
            break;
    }
}

添加了一个WQ_FLAG_EXCLUSIVE标记,告诉内核进行排他性的唤醒,即唤醒一个进程后即退出唤醒的过程,问题得以解决。???(目前只对epoll ET有效)

多路复用的需求让select,poll,epoll等事件模型更为受到欢迎,所谓的事件模型即阻塞在事件上,
内核仅仅通知发生了某件事,具体发生了什么事,则有处理进程或者线程自己来poll。
如此一来,这个事件模型(无论其实现是select,poll,还是epoll)便可以一次搜集多个事件,从而满足多路复用的需求。

Linux 3.x 中epoll的惊群问题?:https://www.zhihu.com/question/24169490/answers/updated

首先看下惊群的原因:

ep_insert的时候会调用,revents = ep_item_poll(epi, &epq.pt);

//epi代表target file,即被监听的文件,poll()返回就绪事件的掩码,赋给revents.epi->ffd.file->f_op->poll(epi->ffd.file, pt) & epi->event.events;

其实就是调用被监控文件(epoll里叫“target file”)的poll方法, 而这个poll其实就是调用poll_wait(还记得poll_wait吗?每个支持poll的设备驱动程序都要调用的), 最后就是调用ep_ptable_queue_proc。

(注:f_op->poll()一般来说只是个wrapper, 它会调用真正的poll实现, 拿UDP的socket来举例, 这里就是这样的调用流程: f_op->poll(), sock_poll(), udp_poll(), datagram_poll(), sock_poll_wait()。)

这是比较难解的一个调用关系,因为不是语言级的直接调用。

 

sock_poll_wait(file, sk_sleep(sk), wait);

static inline wait_queue_head_t *sk_sleep(struct sock *sk)
{
    BUILD_BUG_ON(offsetof(struct socket_wq, wait) != 0);
    return &rcu_dereference_raw(sk->sk_wq)->wait;// 在sk->sk_wq 上挂载回调函数ep_ptable_queue_proc---》ep_poll_callback
}

 

事件发生,唤醒相关文件句柄睡眠队列的entry,调用其回调

 

假设一个TCP Listen socket上来了一个连接请求,已经完成了三次握手,内核希望通知epoll_wait返回,然后去取accept。
内核在wakeup这个socket的sk_wq时,最终会调用到ep_poll_callback回调,ep_poll_callback中会调用:

 /*
     * Wake up ( if active ) both the eventpoll wait list and the ->poll()
     * wait list.
     */ //如果等待进程队列不为空的话,唤醒在该epoll上的等待进程
    if (waitqueue_active(&ep->wq)) {
        if ((epi->event.events & EPOLLEXCLUSIVE) &&
                    !((unsigned long)key & POLLFREE)) {
            switch ((unsigned long)key & EPOLLINOUT_BITS) {
            case POLLIN:
                if (epi->event.events & POLLIN)
                    ewake = 1;
                break;
            case POLLOUT:
                if (epi->event.events & POLLOUT)
                    ewake = 1;
                break;
            case 0:
                ewake = 1;
                break;
            }
        }
        wake_up_locked(&ep->wq);
    } 
既然“就绪链表”中有了新成员,则唤醒阻塞在epoll_wait系统调用的task去处理。注意,如果本来epi已经在“就绪队列”了,这里依然会唤醒并处理的

但是唤醒epoll睡眠队列的task,搜集并上报数据时会调用ep_send_events 向用户态上报事件,其中调用ep_scan_ready_list:

ep_scan_ready_list 中会调用如下代码:

//如果rdllist链表非空,尝试唤醒ep->wq和ep->poll_wait等待队列
if (!list_empty(&ep->rdllist)) {
        /*
         * Wake up (if active) both the eventpoll wait list and
         * the ->poll() wait list (delayed after we release the lock).
         */
        if (waitqueue_active(&ep->wq))
            wake_up_locked(&ep->wq);
        if (waitqueue_active(&ep->poll_wait))
            pwake++;
    }

也就是: 如果“就绪链表”上仍有未处理的epi,且有进程阻塞在epoll句柄的睡眠队列,则唤醒它!(这将是LT惊群的根源)

epoll的LT和ET以及相关问题:

    • LT水平触发
      如果事件来了,不管来了几个,只要仍然有未处理的事件,epoll都会通知你。
    • ET边沿触发
      如果事件来了,不管来了几个,你若不处理或者没有处理完,除非下一个事件到来,否则epoll将不会再通知你
    • 所以ET 要用非阻塞读取fd 直到读取完毕

 

一般http server写法: https://blog.csdn.net/rzytc/article/details/50529691 

// 否则会阻塞在IO系统调用,导致没有机会再epoll
set_socket_nonblocking(fd);
epfd = epoll_create(1);
event.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
while (1) {
    epoll_wait(epfd, events, 1, xx);
    ... // 危险区域!如果有共享同一个epfd的进程/线程调用epoll_wait,它们也将会被唤醒!
    // 这个accept将会有多个进程/线程调用,如果并发请求数很少,那么将仅有几个进程会成功:
    // 1. 假设accept队列中有n个请求,则仅有n个进程能成功,其它将全部返回EAGAIN (Resource temporarily unavailable)
    // 2. 如果n很大(即增加请求负载),虽然返回EAGAIN的比率会降低,但这些进程也并不一定取到了epoll_wait返回当下的那个预期的请求。
    csd = accept(fd, &in_addr, &in_len); 
    ...
}

 

如https://blog.csdn.net/dog250/article/details/80837278 分析如下:

LT的描述“如果事件来了,不管来了几个,只要仍然有未处理的事件,epoll都会通知你。”,显然,epoll_wait刚刚取到事件的时候的时候,不可能马上就调用accept去处理,事实上,逻辑在epoll_wait函数调用的ep_poll中还没返回的,这个时候,显然符合“仍然有未处理的事件”这个条件,显然这个时候为了实现这个语义,需要做的就是通知别的同样阻塞在同一个epoll句柄睡眠队列上的进程!在实现上,这个语义由两点来保证:

 

  1. 保证1:在LT模式下,“就绪链表”上取出的epi上报完事件后会重新加回“就绪链表”;
  2. 保证2:如果“就绪链表”不为空,且此时有进程阻塞在同一个epoll句柄的睡眠队列上,则唤醒它。
  3. ep_scan_ready_list()
    {
        // 遍历“就绪链表”
        ready_list_for_each() {
            list_del_init(&epi->rdllink);
            revents = ep_item_poll(epi, &pt);
            // 保证1
            if (revents) {
                __put_user(revents, &uevent->events);
                if (!(epi->event.events & EPOLLET)) {
                    list_add_tail(&epi->rdllink, &ep->rdllist);
                }
            }
        }
        // 保证2
        if (!list_empty(&ep->rdllist)) {
            if (waitqueue_active(&ep->wq))
                wake_up_locked(&ep->wq);
        }

     

假设LT模式下有10个进程共享同一个epoll句柄,此时来了一个请求client进入到accept队列,我们发现上述的1和2是一个循环唤醒的过程:

1).假设进程a的epoll_wait首先被ep_poll_callback唤醒,那么满足1和2,则唤醒了进程B;
2).进程B在处理ep_scan_ready_list的时候,发现依然满足1和2,于是唤醒了进程C….
3).上面1)和2)的过程一直到之前某个进程将client取出,此时下一个被唤醒的进程在ep_scan_ready_list中的ep_item_poll调用中将得不到任何事件,此时便不会再将该epi加回“就绪链表”了,LT水平触发结束,结束了这场悲伤的梦!

所用解决惊群方法之一:让不同进程的epoll_waitI调用互斥即可。对于非listen socket 可以这样使用,但是对于文件 I/O fd那就不好说了,有时就是为了多个进程读

 

ET边沿触发模式的问题:

epoll  ET模式下还是存在惊群的。 1、对于进程间共享epoll实例,并且使用ET模式注册了同一个监听套接字,当第一个连接到达时,唤醒进程A,进程A在accept之前时候第二个连接达到,唤醒进程B,然后进程A醒来通过循环进行accept了两条连接,最后进程B醒来发现accept失败。 2、对于每个进程单独拥有epoll实例,但是共同注册同一个监听套接字,即使是ET模式,也会产生惊群!!!

 

 

目前有很多是:便出现了create listener+fork这种模型,

fd = create_listen_socket();
for (i = 0; i < N; i++) {
    if (fork() == 0) {
        // 继承了父进程的文件描述符
        server(fd);
    }

这种模型在处理同一个socket的时候,必须互斥,同时内核必须防止潜在的惊群效应,因为互斥的要求,有且仅有一个进程可以处理特定的请求。这就对编程造成了极大的干扰。

目前reuseport出现解决此问题。

对于epoll 惊群问题,可以由如下解决方案:

1、类似于accept 解决方式? 是不是方法不对??

在调用epoll_wait(2)的时候,设置的epoll的等待队列回调函数是default_wake_function,添加队列的时候调用的是__add_wait_queue_exclusive()。
ep_poll_callback()中唤醒操作调用的是wake_up_locked(&ep->wq),最终会调用__wake_up_common,后者会判断exclusive标志:

因为__wake_up_common()的调用是从wake_up_locked()开始的,__wake_up_common的各个参数值为:

  • q: struct eventpoll.wq
  • mode: TASK_NORMAL
  • nr_exclusive:1
  • wake_flags: 0
  • key:NULL。
局部变量curr的值可以通过epoll_wait()的源码得到,具体为:
  • curr->flags: WQ_FLAG_EXCLUSIVE
  • curr->func: default_wake_function
default_wake_function调用的是try_to_wake_up。而try_to_wake_up只有在要唤醒的进程状态不是TASK_NORMAL时才会返回0,TASK_NORMAL的定义是(TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE)。

因此__wake_up_common里的if条件会在第一次判断的时候就满足,唤醒一个进程后便返回了,是不是以为只会唤醒一个进程??

那为什么实际测试会发现有多个进程被唤醒呢?
原因就在于这个唯一被唤醒的进程。

当某个等待在epoll实例上的进程被唤醒后,最终会进入到ep_scan_ready_list() 这个函数中,ep_scan_ready_list()会以回调方式调用ep_send_events_proc()来将数据复制到用户空间。而ep_scan_ready_list()函数在返回之前会再次判断epoll的就绪链表rdllist是否为空,如果不为空的话,就会再唤醒其他进程!下面就是ep_scan_ready_list()返回之前的判断操作:

if (!list_empty(&ep->rdllist)) {
    /*
     * Wake up (if active) both the eventpoll wait list and
     * the ->poll() wait list (delayed after we release the lock).
     */
    if (waitqueue_active(&ep->wq))
        wake_up_locked(&ep->wq);
    if (waitqueue_active(&ep->poll_wait))
        pwake++;
}

在水平触发方式下,从就绪链表中移出来的文件描述符,如果当前仍有事件就绪(可读、可写等),会在复制到用户空间后被再次添加到就绪链表中:

所以LT不行 ET可以

2、linux 3.x 引入的reuseport

3、类似于reuseport 一样,设置多个listen fd 监听不同的port, 实现tproxy 功能!!

 

 



-

惊群问题一般出现在那些web服务器上,曾经Linux系统有个经典的accept惊群问题困扰了大家非常久的时间,这个问题现在已经在内核曾经得以解决,具体来讲就是当有新的连接进入到accept队列的时候,内核唤醒且仅唤醒一个进程来处理,这是通过以下的代码来实现的:
作者:紫衣仙女
链接:https://www.imooc.com/article/40708
来源:慕课网
惊群问题一般出现在那些web服务器上,曾经Linux系统有个经典的accept惊群问题困扰了大家非常久的时间,这个问题现在已经在内核曾经得以解决,具体来讲就是当有新的连接进入到accept队列的时候,内核唤醒且仅唤醒一个进程来处理,这是通过以下的代码来实现的:
作者:紫衣仙女
链接:https://www.imooc.com/article/40708
来源:慕课网
惊群问题一般出现在那些web服务器上,曾经Linux系统有个经典的accept惊群问题困扰了大家非常久的时间,这个问题现在已经在内核曾经得以解决,具体来讲就是当有新的连接进入到accept队列的时候,内核唤醒且仅唤醒一个进程来处理,这是通过以下的代码来实现的:
作者:紫衣仙女
链接:https://www.imooc.com/article/40708
来源:慕课网
惊群问题一般出现在那些web服务器上,曾经Linux系统有个经典的accept惊群问题困扰了大家非常久的时间,这个问题现在已经在内核曾经得以解决,具体来讲就是当有新的连接进入到accept队列的时候,内核唤醒且仅唤醒一个进程来处理
作者:紫衣仙女
链接:https://www.imooc.com/article/40708
来源:慕课网
惊群问题一般出现在那些web服务器上,曾经Linux系统有个经典的accept惊群问题困扰了大家非常久的时间,这个问题现在已经在内核曾经得以解决,具体来讲就是当有新的连接进入到accept队列的时候,内核唤醒且仅唤醒一个进程来处理
作者:紫衣仙女
链接:https://www.imooc.com/article/40708
来源:慕课网
posted @ 2019-11-13 21:39  codestacklinuxer  阅读(273)  评论(0编辑  收藏  举报