Nginx解决惊群问题和负载均衡

什么是惊群问题

Nginx的master进程开始监听web端口,fork出多个worker子进程,这些子进程开始监听同一个端口,假定一段时间没有用户连入服务器,某一时刻恰好所有的子进程都进入休眠等待新连接的到来(阻塞在epoll_wait),这时有一个用户向服务器发来了连接,内核在收到SYN包后激活所有子进程,但是此时只有一个进程成功执行accept建立新连接,其它子进程accept失败,会重新进入休眠,他们的唤醒也是多余的,引发不必要的进程上下文切换,增加系统开销。

Nginx解决惊群问题的方法

既然“惊群”是多个子进程同一时刻监听同一个端口引起的,那么解决的方法也很简单,就是,同一时刻只能有一个进程监听端口,这样就不会发生“惊群”了。此时新连接事件只能唤醒正在监听的唯一一个进程

如何保证一个时刻只能有一个worker进程监听端口呢?Nginx设置了一个accept_mutex锁,在使用accept_mutex锁时,只有进程成功调用ngx_trylock_accept_mutex方法获取锁后才可以监听端口。

下面分析一下ngx_trylock_accept_mutex函数

ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
    if (ngx_shmtx_trylock(&ngx_accept_mutex)) {//使用进程间同步锁,试图获取accept_mutex锁返回1时表示成功拿到锁,返回0表示失败,这个获取锁的过程是非阻塞的
//一旦锁被其他worker进程占用就会立即返回

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

        if (ngx_accept_mutex_held//ngx_accept_mutex_held是一个标志位,当它为1时,说明进程已经获取到锁了,
            && ngx_accept_events == 0
            && !(ngx_event_flags & NGX_USE_RTSIG_EVENT))
        {
            return NGX_OK;
        }

        if (ngx_enable_accept_events(cycle) == NGX_ERROR) {//将所有监听连接的读事件都添加到当前epoll事件驱动模块中
            ngx_shmtx_unlock(&ngx_accept_mutex);
            return NGX_ERROR;
        }

        ngx_accept_events = 0;
        ngx_accept_mutex_held = 1;将标志位置为1,说明已经获取锁

        return NGX_OK;
    }

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "accept mutex lock failed: %ui", ngx_accept_mutex_held);

    if (ngx_accept_mutex_held) {
        if (ngx_disable_accept_events(cycle) == NGX_ERROR) {
            return NGX_ERROR;
        }

        ngx_accept_mutex_held = 0;//没有获取到锁时必须设置为0
    }

    return NGX_OK;
}

因此在调用ngx_trylock_accept_mutex方法后,要么是唯一获取到锁并且监听端口上的新连接事件,要么是没有获取到锁,不会收到新连接事件。

如果该进程没有获取到锁,那么在接下来的process_events方法只能处理已有连接上的事件;如果获取到了锁,调用process_events时就会处理已有连接上的事件和新连接事件,但是,什么时候来释放锁呢?一个worker进程上可能有许多活跃的连接,处理这些连接上的事件会占用很多时间,那么很长时间不释放accept_mutex锁,其他worker进程就很难获得新连接的机会

那么该如何解决长时间占用ngx_accept_mutex锁问题呢?就要依靠ngx_posted_accept_events队列和ngx_posted_events队列了。

下面分析一下ngx_process_events代码

static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
   
    for (i = 0; i < events; i++) {
        c = event_list[i].data.ptr;

        instance = (uintptr_t) c & 1;
        c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1);

        rev = c->read;

        
        }
        //取得发生一个事件
        revents = event_list[i].events;

      
        //记录wait的错误返回状态
        if (revents & (EPOLLERR|EPOLLHUP)) {
            ngx_log_debug2(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                           "epoll_wait() error on fd:%d ev:%04XD",
                           c->fd, revents);
        }


        //该事件是一个读事件,并该连接上注册的读事件是active的
        if ((revents & (EPOLLERR|EPOLLHUP))
             && (revents & (EPOLLIN|EPOLLOUT)) == 0)
        {
          
            revents |= EPOLLIN|EPOLLOUT;
        }

        if ((revents & EPOLLIN) && rev->active) {

            if ((flags & NGX_POST_THREAD_EVENTS) && !rev->accept) {
                rev->posted_ready = 1;

            } else {
                rev->ready = 1;
            }
            //事件放入到相应的队列中
            if (flags & NGX_POST_EVENTS) {
                queue = (ngx_event_t **) (rev->accept ?//新连接事件,加入到新连接事件队列
                               &ngx_posted_accept_events : &ngx_posted_events);

                ngx_locked_post_event(rev, queue);

            } else {
                rev->handler(rev);//没有获取到锁直接调用回调函数
            }
        }

        wev = c->write;

        if ((revents & EPOLLOUT) && wev->active) {//非新连接事件,加入到post事件队列,延后执行

            if (c->fd == -1 || wev->instance != instance) {

                /*
                 * the stale event from a file descriptor
                 * that was just closed in this iteration
                 */

                ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                               "epoll: stale event %p", c);
                continue;
            }

            if (flags & NGX_POST_THREAD_EVENTS) {
                wev->posted_ready = 1;

            } else {
                wev->ready = 1;
            }

            if (flags & NGX_POST_EVENTS) {
                ngx_locked_post_event(wev, &ngx_posted_events);

            } else {
                wev->handler(wev);
            }
        }
    }

    ngx_mutex_unlock(ngx_posted_events_mutex);

    return NGX_OK;
}
这样讲事件分为两个队列,在后面的ngx_process_timers_and_events函数中,调用完ngx_process_events函数收集完事件后,优先一次调用accept队列中事件的回调函数,然后释放锁,最后调用post队列中的事件的回调函数

负载均衡问题

什么是负载均衡?

在多个worker进程争抢处理一个新连接事件时,一定只有一个进程可以成功建立连接,那么如果有的子进程很勤奋他们抢着处理了大部分连接,而有的运气不好只处理了很少的连接,这对多核CPU是很不利的,因为子进程应该是平等的,每个子进程应该尽量独占一个cpu核心,子进程间负载不均衡会影响整个服务性能。

如何实现负载均衡呢?

与惊群问题一样,只有打开了accept_mutex锁才会实现worker子进程间的负载均衡。实现方法是设置一个负载均衡阀值,全局变量ngx_accept_disabled。

在ngx_event_accept中会对其赋值

//accept到一个新的连接以后,就重新计算ngx_accept_disabled的值。它主要用来做负载均衡使用
        ngx_accept_disabled = ngx_cycle->connection_n / 8
                              - ngx_cycle->free_connection_n;
在启动时,此阀值就是一个负数,其值为连接总数的7/8,其实它的用法很简单,当阀值为负数时,不会进行触发负载均衡操作,但是当它是正数时,就会触发负载均衡操作,当为正数是时,当前进程将不再处理新连接事件,而是将阀值ngx_accept_disabled减一

ngx_process_timers_and_events函数中

 //是否使用accept互斥体。accept mutex的作用就是避免惊群,同时实现负载均衡
    if (ngx_use_accept_mutex) {
        if (ngx_accept_disabled > 0) {  //大于0说明该进程接收的连接过多,放弃一次争抢accept mutex的机会
            ngx_accept_disabled--;

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

上面代码说明,当前使用连接达到总连接数的7/8时,就不会再处理新连接事件。达到7/8以下才会再处理新连接事件。这时,worker进程就会减少处理新连接事件的机会,而比较空闲的进程就会有机会去处理新连接事件,以达到均衡负载。



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值