Linux thundering herd

惊群的定义(http://en.wikipedia.org/wiki/Thundering_herd_problem):
The thundering herd problem occurs when a large number of processes waiting for an event are awoken when that event occurs, but only one process is able to proceed at a time. After the processes wake up, they all demand the resource and a decision must be made as to which process can continue. After the decision is made, the remaining processes are put back to sleep, only to all wake up again to request access to the resource.

另一个被引用很多的定义,意思一样(http://www.catb.org/jargon/html/T/thundering-herd-problem.html):
Scheduler thrashing. This can happen under Unix when you have a number of processes that are waiting on a single event. When that event (a connection to the web server, say) happens, every process which could possibly handle the event is awakened. In the end, only one of those processes will actually be able to do the work, but, in the meantime, all the others wake up and contend for CPU time before being put back to sleep. Thus the system thrashes briefly while a herd of processes thunders through. If this starts to happen many times per second, the performance impact can be significant.

在《UNIX Network Programming Volume 1》中,advanced sockets部分,也有说明。

这里有两点需要强调一下:
1) 定义中的“event”,是广义的event,不局限于epoll这种典型的async-event-driven-model中指的event。比如,来了个connection,也就是一个event发生了。
2) 这个定义是classical thundering herd problem的定义,不同于nginx所解决的thundering-herd-like problem(类似与“经典惊群”,这个名称是我起的。当我们用中文说nginx惊群相关的问题时,通常只说“惊群”,可能引起混淆)。因为在大多数情况下,classical thundering herd problem,不需要专门解决。之所以说大多数情况,实际上是取决于nginx所依赖的Linux kernel。

================================================================
== 第一部分 经典惊群问题 ==
================================================================


下面,先说一下classical thundering herd,以及与其相关的系统调用accept。
等说完这个,再说一下nginx的thundering-herd-like problem。

先上一段pseudo codes:
// parent process:
// create socket
listen_fd = socket(...);
bind(listen_fd, ...);
listen(listen_fd, ...);
fork();

// child processes:
// COW listen_fd
conn_fd = accept(listen_fd, ...);

因为COW,每个child中的listen_fd,实际上都是parent中的listen_fd,即同一个TCP socket。当多个process在同一个TCP socket上accept时,kernel把这些process(struct task_struct)放到同一个waiting queue(wait_queue_t)里。


接下来的处理,按照不同的kernel版本分别来看。


先看kernel v2.2.10,这个版本的内核还没有解决classical thundering herd。

当一个connection到来,需要被accept时,accept调用了struct sock的3个成员变量,分别是write_space、data_ready、state_change,它们都是函数指针。在初始化struct sock时,这3个成员变量默认被初始化为了tcp_write_space、sock_def_readable、sock_def_wakeup这3个函数。这3个函数,都调用了wake_up_interruptible。wake_up_interruptible实际是一个宏,封装了__wake_up这个函数。__wake_up函数会遍历fd的waiting queue,唤醒里面所有等待进程。而最终只有一个进程能够真正accept到这个connection,并被从waiting queue中删除,其他的被唤醒的进程,重新在waiting queue中sleep。这就是classical thundering herd。


include/net/sock.h:
struct sock {
    // ...
    // all kinds of fields
    // ...

    /* Callbacks */
    void (*state_change)(struct sock *sk);
    void (*data_ready)(struct sock *sk, int bytes);
    void (*write_space)(struct sock *sk);
    void (*error_report)(struct sock *sk);

    int (*backlog_rcv) (struct sock *sk, struct sk_buff *skb);
    void (*destruct)(struct sock *sk);
};

net/core/sock.c:
void sock_init_data(struct socket *sock, struct sock *sk)
{
    // ...
    // some init operatoins
    // ...

    sk->state_change        =       sock_def_wakeup;
    sk->data_ready          =       sock_def_readable;
    sk->write_space         =       sock_def_write_space;
    sk->error_report        =       sock_def_error_report;
    sk->destruct            =       sock_def_destruct;

    // ...
    // more init operations
    // ...
}

void sock_def_wakeup(struct sock *sk)
{
    if(!sk->dead)
        wake_up_interruptible(sk->sleep);
}

include/linux/sched.h:
extern void FASTCALL(__wake_up(struct wait_queue ** p, unsigned int mode));
#define wake_up_interruptible(x) __wake_up((x),TASK_INTERRUPTIBLE)

__wake_up的实现,在kernel/sched.c中,看看相关代码片段,很明显全部唤醒:
while (next != head) {
    p = next->task;
    next = next->next;
    if (p->state & mode) {
        /*
         * We can drop the read-lock early if this
         * is the only/last process.
         */
        if (next == head) {
            read_unlock(&waitqueue_lock);
            wake_up_process(p);
            goto out;
        }
        wake_up_process(p);
    }
}

还需要说一下TASK_INTERRUPTIBLE宏,以及其他几个相关宏的定义。因为惊群问题的最终解决方案,不是一蹴而就的,中间有过渡。过渡的方案,就和这组宏的定义有点关系。
这些宏也定义在include/linux/sched.h,比如:
#define TASK_INTERRUPTIBLE      1


紧接着,看一个过渡版本,kernel v2.2.26。
先看include/linux/sched.h,与v2.2.10的相比,增加了一个宏:
#define TASK_EXCLUSIVE          32

再看kernel/sched.c中,__wake_up的代码片段:
best_exclusive = 0;
do_exclusive = mode & TASK_EXCLUSIVE;
while (next != head) {
    p = next->task;
    next = next->next;
    if (p->state & mode) {
        if (do_exclusive && p->task_exclusive) {
            if (best_exclusive == NULL)
                best_exclusive = p;
        }
        else {
            wake_up_process(p);
        }
    }
}
if (best_exclusive)
    wake_up_process(best_exclusive);

为了解决惊群,struct task_struct增加了一个task_exclusive字段,include/linux/sched.h:
unsigned int task_exclusive;    /* task wants wake-one semantics in __wake_up() */

再结合新增加的宏,这个版本的__wake_up保证了两点,一是正常的process(即未设置task_exclusive字段)都被唤醒,二是只有第一个exclusive process被唤醒,其他还都留在waiting queue中。以此解决惊群。


最后,看kernel v2.6.34,最终的解决方案。之所以看这个版本,是因为baidu都是用的2.6.X的版本。

include/linux/sched.h中,TASK_EXCLUSIVE宏已经没有了。
而在wait_queue_t的定义中,增加了一个类似的宏,include/linux/wait.h:
struct __wait_queue {
    unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01
    void *private;
    wait_queue_func_t func;
    struct list_head task_list;
};

include/linux/wait.h中,__wake_up的原型也改变了:
void __wake_up(wait_queue_head_t *q, unsigned int mode, int nr, void *key);

kernel/sched.c:
void __wake_up(wait_queue_head_t *q, unsigned int mode,
               int nr_exclusive, void *key)
{
    // ...
    // some operations
    // ...

    __wake_up_common(q, mode, nr_exclusive, 0, key);

    // ...
}

/*
 * 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.
 *
 * ...
 * ...
 */
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;
    }
}

这代码的逻辑,和v2.2.26中类似,遍历waiting queue,唤醒normal process,唤醒指定的exclusive process,退出。


到此为止,我觉得已把classical thundering herd说清楚了,尽管比较粗略。要想更深入了解,需要细追kernel相关部分代码,it's a big job。


================================================================
== 第二部分 新惊群问题 以及nginx的处理 ==
================================================================


下面开始说nginx的新惊群(classical-thundering-herd-like),kernel 2.6 + epoll。实际上由select和poll而产生的新惊群现象,本质上和epoll类似。多说一句,我认为这是kernel authors有意为之,他们认为就该这样。而判断是否应该“解决”这个新惊群问题,以及怎么解决,由user codes决定。

原则上,解决这个新惊群问题,有两种方案,一是去除其可能发生的条件,二是串行化,排队来,避免争抢,这通常会用到锁机制。

第一种方案的典型代表,是lighttpd。其官方推荐的进程模型,就是一个master+一个worker,这种情形下,压根不存在“群”这个东西,就无所谓“惊”了。
第二种方案,nginx是代表之一。下面我们主要说这个。


从使用说起。nginx允许用户选择是否处理新惊群问题。为了解决新惊群问题,nginx需要做必要的计算和锁操作,这些都是有开销的。所以,如果用户认为其使用场景,可以不care新惊群问题,可以disable掉nginx这方面的处理。但默认是打开的。
在nginx的core module中,有一个accept_mutex指令,可以打开或关闭新惊群处理机制。这个指令,只能出现在event {}中。定义如下:
event/ngx_event.c中,
static ngx_command_t  ngx_event_core_commands[] = {
    // ...
    // definitions of some event core directives
    // ...

    { ngx_string("accept_mutex"),
      NGX_EVENT_CONF|NGX_CONF_FLAG,
      ngx_conf_set_flag_slot,
      0,
      offsetof(ngx_event_conf_t, accept_mutex),
      NULL },

    // ...
};

在这里,我想对“accept_mutex”这个名字,吐嘈一下。乍一看,它是为了accept而mutext,貌似是为了解决classical thundering herd。而实际上,在nginx的实现中,它并不是直接为accept,而是直接为epoll_ctl。可以简单的认为是这样:epoll_wait --> epoll_ctl --> epoll_wait --> accept。九曲回肠,才到accept。

为了方便讨论,先在这里列出与accept_mutex相关的几个变量,它们都声明在event/ngx_event.h中,定义在event/ngx_event.c中,都是全局的:
extern ngx_atomic_t          *ngx_accept_mutex_ptr;
extern ngx_shmtx_t            ngx_accept_mutex;
extern ngx_uint_t             ngx_use_accept_mutex;

extern ngx_uint_t             ngx_accept_mutex_held;

extern ngx_int_t              ngx_accept_disabled;


下面的讨论,只基于accept_mutex打开的前提下。

nginx的启动、工作流程,可以简单用ngx_module_t中的几个字段来描述:init_master、init_module、init_process。

init_master时,nginx在其master进程里,创建、绑定、建立监听的socket。init_master可以认为是协议(TCP/IP)层面的处理,后面将要提到的init_module,可以认为是nginx内部“业务逻辑”的处理。
具体来说,ngx_cycle_t是对nginx的进程最终的数据结构,它贯穿于一个nginx进程的生命周期。nginx在启动时,会对其对应的ngx_cycle做初次初始化操作或者再初始化操作(如restart、reload时)。在对ngx_cycle_t操作时,会进行典型的TCP server初始化操作。简单流程如下:
main(): core/nginx.c
        |
       \|/
ngx_init_cycle(): core/ngx_cycle.c
        |
       \|/
ngx_open_listening_sockets(): core/ngx_connection.c

在ngx_open_listening_sockets中,socket --> setsockopt --> bind --> listen。注意,用来监听的socket fd在此已经生成。

init_module时,会遍历编译时生成的ngx_modules数组,并对其中的各个模块做处理。因为和我们讨论的问题相关性很小,不细说。


重点说init_process。因为master已经建立了用来监听的socket fd,workers被fork出来后,如果它想,其实已经可以直接处理网络event,甚至进而accept了。但woker现在做不做这些事,就会受accept_mutex这个指令的值的影响。
现在看ngx_event_process_init函数,在event/ngx_event.c中。这个函数用来初始化worker的event操作。

这个函数中,和accept_mutex相关的片段:

// 根据accept_mutex指令的值,设置与accept_mutex相关的变量
    if (ccf->master && ccf->worker_processes > 1 && ecf->accept_mutex) {
        ngx_use_accept_mutex = 1;
        ngx_accept_mutex_held = 0;
        ngx_accept_mutex_delay = ecf->accept_mutex_delay;

    } else {
        ngx_use_accept_mutex = 0;
    }

// 遍历所有用来监听的socket
    for (i = 0; i < cycle->listening.nelts; i++) {
        // ...

        rev->handler = ngx_event_accept;//注意

        if (ngx_use_accept_mutex) {
            continue;
        }

        if (ngx_event_flags & NGX_USE_RTSIG_EVENT) {
            if (ngx_add_conn(c) == NGX_ERROR) {
                return NGX_ERROR;
            }

        } else {
            if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {
                return NGX_ERROR;
            }
        }

        // ...
    }

如果打开了accept_mutex,就continue了。如果没有打开,就ngx_add_event。也就是说,如果打开了accept_mutex,就在将来的某个时候,再ngx_add_event。总之,就是早晚肯定要ngx_add_event。所以,我们看ngx_add_event做了什么。

在event/ngx_event.h中,ngx_add_event被定义为了一个宏:
#define ngx_add_event        ngx_event_actions.add

ngx_event_actions是一个ngx_event_actions_t类型的全局变量。ngx_event_actions_t定义在event/ngx_event.h中,显然,它是为了兼容不同平台的不同事件机制。

直接看epoll,在event/module/ngx_epoll_module.c中。

static ngx_int_t
ngx_epoll_init(ngx_cycle_t *cycle, ngx_msec_t timer)
{
    // ...
    ngx_event_actions = ngx_epoll_module_ctx.actions;
    // ...
}

ngx_event_module_t  ngx_epoll_module_ctx = {
    &epoll_name,
    ngx_epoll_create_conf,               /* create configuration */
    ngx_epoll_init_conf,                 /* init configuration */

    {
        ngx_epoll_add_event,             /* add an event */
        ngx_epoll_del_event,             /* delete an event */
        ngx_epoll_add_event,             /* enable an event */
        ngx_epoll_del_event,             /* disable an event */
        ngx_epoll_add_connection,        /* add an connection */
        ngx_epoll_del_connection,        /* delete an connection */
        NULL,                            /* process the changes */
        ngx_epoll_process_events,        /* process the events */
        ngx_epoll_init,                  /* init the events */
        ngx_epoll_done,                  /* done the events */
    }
};

static ngx_int_t
ngx_epoll_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags)
{
    // ...
    // epoll_ctl
    // ...
}

现在应该清楚了,可以说,ngx_add_event就是epoll_ctl with ops of EPOLL_CTL_ADD and EPOLL_CTL_MOD。

现在再看,在init_process以后的什么时候调用了ngx_add_event。
按照常识,可以预见,应该是在worker的时间处理循环中。
实际上,正如我们的预见,在类似与“while (1) { epoll_wait(); // ... }”,但是要复杂的多。

看event/ngx_event.c中的ngx_process_events_and_timers函数:
    if (ngx_use_accept_mutex) {
        if (ngx_accept_disabled > 0) {
            ngx_accept_disabled--;

        } else {
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
                return;
            }

            if (ngx_accept_mutex_held) {
                flags |= NGX_POST_EVENTS;

            } else {
                if (timer == NGX_TIMER_INFINITE
                    || timer > ngx_accept_mutex_delay)
                {
                    timer = ngx_accept_mutex_delay;
                }
            }
        }
    }

    delta = ngx_current_msec;

    (void) ngx_process_events(cycle, timer, flags);

    delta = ngx_current_msec - delta;

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "timer delta: %M", delta);

    if (ngx_posted_accept_events) {
        ngx_event_process_posted(cycle, &ngx_posted_accept_events);
    }

    if (ngx_accept_mutex_held) {
        ngx_shmtx_unlock(&ngx_accept_mutex);
    }

    if (delta) {
        ngx_event_expire_timers();
    }

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "posted events %p", ngx_posted_events);

    if (ngx_posted_events) {
        if (ngx_threaded) {
            ngx_wakeup_worker_thread(cycle);

        } else {
            ngx_event_process_posted(cycle, &ngx_posted_events);
        }
    }

其中的关键代码是,调用ngx_trylock_accept_mutex。

event/ngx_event_accept.c中:
ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
    // ...
    if (ngx_shmtx_trylock(&ngx_accept_mutex)) {

        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                       "accept mutex locked");

        if (ngx_accept_mutex_held
            && ngx_accept_events == 0
            && !(ngx_event_flags & NGX_USE_RTSIG_EVENT))
        {
            return NGX_OK;
        }

        if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
            ngx_shmtx_unlock(&ngx_accept_mutex);
            return NGX_ERROR;
        }

        ngx_accept_events = 0;
        ngx_accept_mutex_held = 1;

        return NGX_OK;
    }

    // ...
}

static ngx_int_t
ngx_enable_accept_events(ngx_cycle_t *cycle)
{
    // ...
            if (ngx_add_event(c->read, NGX_READ_EVENT, 0) == NGX_ERROR) {
                return NGX_ERROR;
            }
    // ...
}

至此,貌似一切都明了了。但其实还有一点,注意event/ngx_event.c中ngx_process_events_and_timers函数对ngx_accept_disabled变量的操作。

回忆一下,在ngx_event_process_init函数中,把listening fd的read event的handler设置为了ngx_event_accept。

看event/ngx_event_accept.c中ngx_event_accept的实现:
void
ngx_event_accept(ngx_event_t *ev)
{
    // ...

#if (NGX_HAVE_ACCEPT4)
        if (use_accept4) {
            s = accept4(lc->fd, (struct sockaddr *) sa, &socklen,
                        SOCK_NONBLOCK);
        } else {
            s = accept(lc->fd, (struct sockaddr *) sa, &socklen);
        }
#else
        s = accept(lc->fd, (struct sockaddr *) sa, &socklen);
#endif

        // ...

            if (err == NGX_EMFILE || err == NGX_ENFILE) {
                // ...
                ngx_accept_disabled = 1;
                // ...
            }

            // ...

        ngx_accept_disabled = ngx_cycle->connection_n / 8
                              - ngx_cycle->free_connection_n;

        // ...
}


总结如下:
首先,nginx对各个woker有负载均衡的处理,如果某些worker足够闲(通过ngx_accept_disabled变量表示),则可以参与竞争accept_mutext;
然后,争夺到accept_mutext的worker,可以把listening fd的加入到其事件模型中。

以此,来避免了新惊群问题。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值