• 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标记,告诉内核进行排他性的唤醒,即唤醒一个进程后即退出唤醒的过程,问题得以解决。

    多路复用的需求让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边沿触发模式的问题以及解决

    ET模式不满足上述的“保证1”,所以不会将已经上报事件的epi重新链接回“就绪链表”,也就是说,只要一个“就绪队列”上的epi上的事件被上报了,它就会被删除出“就绪队列”。

    由于epi entry的callback即ep_poll_callback所做的事情仅仅是将该epi自身加入到epoll句柄的“就绪链表”,同时唤醒在epoll句柄睡眠队列上的task,所以这里并不对事件的细节进行计数,

    比如说,如果ep_poll_callback在将一个epi加入“就绪链表”之前发现它已经在“就绪链表”了,那么就不会再次添加,因此可以说,一个epi可能pending了多个事件,注意到这点非常重要!

    一个epi上pending多个事件,这个在LT模式下没有任何问题,因为获取事件的epi总是会被重新添加回“就绪链表”,那么如果还有事件,在下次check的时候总会取到。

    然而对于ET模式,仅仅将epi从“就绪链表”删除并将事件本身上报后就返回了,因此如果该epi里还有事件,则只能等待再次发生事件,进而调用ep_poll_callback时将该epi加入“就绪队列”。这意味着什么?

    这意味着,应用程序,即epoll_wait的调用进程必须自己在获取事件后将其处理干净后方可再次调用epoll_wait,否则epoll_wait不会返回,而是必须等到下次产生事件的时候方可返回。即,依然以accept为例,必须这样做:

    while (1) {
        epoll_wait(epfd, events, 64, xx);
        while ((csd = accept(sd, &in_addr, &in_len)) > 0) {
            do_something(...);
        } 
        ...

    目前有很多是:便出现了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++;
    }

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

    2、linux 3.x 引入的reuseport



    -

    惊群问题一般出现在那些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
    来源:慕课网
  • 相关阅读:
    npm 之 --save , -D,--save -dev的区别
    webpack 之 打包(最新版)
    npm 与 yarn 对比
    webpack 之 打包图片文件
    webpack 之 打包less文件
    javascript 之 Event Loop
    package.json中type的含义
    webpack 之 打包css文件操作
    常见问题 之 webpack打包css问题
    类方法和对象方法
  • 原文地址:https://www.cnblogs.com/codestack/p/11853416.html
Copyright © 2020-2023  润新知