Nginx的前端负载均衡

"竞争问题、worker负载均衡、惊群问题"

Posted by wangyapu on June 12, 2016

Nginx的前端负载均衡

进程模型

nginx进程模型图:

image

  • nginx采用多进程的方式,nginx启动后一个master进程管理多个worker进程,一般worker进程个数会设置和cpu核数一样。

  • master进程接收外界信号,发送信号各个worker进程,并监控各个worker进程的运行状态。如果有worker进程异常挂起或者退出,master进程会重新拉起或重新启动新的worker进程。

  • 各个worker进程是独立的,它们公平地接收来自客户端的请求。

nginx进程模型优点:

  • 每个worker进程是独立的,互不影响,不需要进行锁控制。
  • 一个worker进程挂了,不会停止对外服务,master进程会重新建立新的worker进程。
  • 相比于apache的工作方式,节省了大量的内存空间。

惊群现象

惊群现象就是一个fd的事件被触发时,所有等待这个fd的线程或进程都会被唤醒。如果多个工作进程同时监听某个套接口,一旦该套接口出现某客户端请求,就会引发所有拥有该套接口的工作进程去争抢这个请求,所有进程都得到返回,但是只有一个进程能争抢到,这种现象称为惊群。一般都是socket的accept()会导致惊群。

竞争问题

因为nginx是多进程模型,多个工作进程争抢一个套接口就会出现惊群现象。而且多个进程会竞争客户端连接,如果某个工作进程accept的机会较大,那么该工作进程的空闲连接会很快用完,其他工作进程的空闲连接剩余较多,甚至在空余连接用尽时会造成请求丢失,这种竞争是不公平的。那么nginx是如何解决上述问题的呢?

前端负载策略——accept_mutex锁:

nginx使用accept_mutex选项控制进程是否添加accept事件。以下是几个重要的代码片段:

ngx_event.c : ngx_event_process_init(ngx_cycle_t *cycle)

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;
}

ngx_event_accept.c : ngx_event_accept(ngx_event_t *ev)

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

ngx_event.c : ngx_process_events_and_timers(ngx_cycle_t *cycle)

if (ngx_use_accept_mutex) {
    // nginx.conf配置了每个nginx worker进程能够处理的最大连接数,当达到最大数的7/8时,ngx_accept_disabled > 0,此时处于满负荷,将不再去处理新连接
    if (ngx_accept_disabled > 0) {
        ngx_accept_disabled--;

    } else {
        //拿到锁,flag置为NGX_POST_EVENTS,意味着ngx_process_events函数中任何事件都将延后处理,会把accept事件都放到ngx_posted_accept_events链表中
        if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
            return;
        }

        if (ngx_accept_mutex_held) {
            flags |= NGX_POST_EVENTS;

        } else {
        
             //拿不到锁就不会处理监听的句柄,timer是传给epoll_wait的超时时间,修改为最大ngx_accept_mutex_delay意味着epoll_wait更短的超时返回,以免新连接长时间没有得到处理
            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);

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_event_process_posted(cycle, &ngx_posted_events);

ngx_event_accept.c : ngx_trylock_accept_mutex(ngx_cycle_t *cycle)

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;
        }

        //ngx_enable_accept_events会把监听的句柄都塞入到本worker进程的epoll中
        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;
    }

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "accept mutex lock failed: %ui", ngx_accept_mutex_held);
    
    //没有拿到锁ngx_disable_accept_events会把监听句柄从epoll中取出.
    if (ngx_accept_mutex_held) {
        if (ngx_disable_accept_events(cycle) == NGX_ERROR) {
            return NGX_ERROR;
        }

        ngx_accept_mutex_held = 0;
    }

    return NGX_OK;
}

可以看出,多个worker工作进程在同一时刻只有一个worker进程拿到accept_mutex锁,并向自己的epoll中加入监听的句柄,此时该worker进程flag置为NGX_POST_EVENTS,在ngx_process_events里NGX_POST_EVENTS标志将事件都放入ngx_posted_events链表中,延迟到锁释放了再处理,因为这些请求耗时相对较久。

总结

  • 以上描述的worker进程处理请求的原理表名nginx根本不会出现惊群现象。

  • ngx_accept_disabled的变量来控制worker进程是否去竞争accept_mutex锁,虽然nginx的accept_mutax锁方法给每个worker进程提供平等的机会来竞争锁,但这种方法得到实际结果却不一定平均。