之前就接触过惊群的概念,虽然理解,但仍然不够深入。这两天看网络编程内容时,惊群这个词又出现在眼前。

惊群

它是什么呢?怎么产生的呢?

可以先看一下这篇文章 http://blog.dothinkings.com/wp/?p=630

看完之后,惊群现象应该了解了。

网络编程

一般我们理解的网络编程是这样的,主进程bind-> listen -> accept, 将accept返回的socket用子进程处理。

还有一种情况是这样的,主进程bind -> listen -> fork, 在listen之后fork,多个子进程共享listen socket, 每个子进程都accept。这样就会产生惊群。

第一种情况下,只用一个进程去accept新连接,能够避免惊群,但是效率低下,因为这个进程只能用来accept连接。对多核机器来说,仅有一个进程去accept,这也是程序员在自己创造accept瓶颈。所以,需要多进程处理accept事件,即第二种情况。而nginx也是采用了第二种方式。那它是如何避免惊群的呢?

其实,在linux2.6内核上,accept系统调用已经不存在惊群了。但是很不幸,通常我们的程序没那么简单,不会愿意阻塞在accept调用上,我们还有许多其他网络读写事件要处理,linux下我们使用epoll解决非阻塞socket。所以,即使accept调用没有惊群了,我们也还得处理惊群这事,因为epoll有这问题。如果我们在子进程内不是阻塞调用accept,而是用epoll_wait,就会发现,新连接过来时,多个子进程都会在epoll_wait后被唤醒,然后发现自己accept失败。

下面是nginx代码分析

void  
ngx_process_events_and_timers(ngx_cycle_t *cycle)  
{  
。。。 。。。  
    //ngx_use_accept_mutex表示是否需要通过对accept加锁来解决惊群问题。当nginx worker进程数>1时且配置文件中打开accept_mutex时,这个标志置为1  
    if (ngx_use_accept_mutex) {  
            //ngx_accept_disabled表示此时满负荷,没必要再处理新连接了,我们在nginx.conf曾经配置了每一个nginx worker进程能够处理的最大连接数,当达到最大数的7/8时,ngx_accept_disabled为正,说明本nginx worker进程非常繁忙,将不再去处理新连接,这也是个简单的负载均衡  
        if (ngx_accept_disabled > 0) {  
            ngx_accept_disabled--;  
  
        } else {  
                //获得accept锁,多个worker仅有一个可以得到这把锁。获得锁不是阻塞过程,都是立刻返回,获取成功的话ngx_accept_mutex_held被置为1。拿到锁,意味着监听句柄被放到本进程的epoll中了,如果没有拿到锁,则监听句柄会被从epoll中取出。  
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {  
                return;  
            }  
  
                        //拿到锁的话,置flag为NGX_POST_EVENTS,这意味着ngx_process_events函数中,任何事件都将延后处理,会把accept事件都放到ngx_posted_accept_events链表中,epollin|epollout事件都放到ngx_posted_events链表中  
            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;  
                }  
            }  
        }  
    }  
。。。 。。。  
        //linux下,调用ngx_epoll_process_events函数开始处理  
    (void) ngx_process_events(cycle, timer, flags);  
。。。 。。。  
        //如果ngx_posted_accept_events链表有数据,就开始accept建立新连接  
    if (ngx_posted_accept_events) {  
        ngx_event_process_posted(cycle, &ngx_posted_accept_events);  
    }  
  
        //释放锁后再处理下面的EPOLLIN EPOLLOUT请求  
    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);  
        //然后再处理正常的数据读写请求。因为这些请求耗时久,所以在ngx_process_events里NGX_POST_EVENTS标志将事件都放入ngx_posted_events链表中,延迟到锁释放了再处理。  
    if (ngx_posted_events) {  
        if (ngx_threaded) {  
            ngx_wakeup_worker_thread(cycle);  
  
        } else {  
            ngx_event_process_posted(cycle, &ngx_posted_events);  
        }  
    }  
}

从上面的注释可以看到,无论有多少个nginx worker进程,同一时刻只能有一个worker进程在自己的epoll中加入监听的句柄。这个处理accept的nginx worker进程置flag为NGX_POST_EVENTS,这样它在接下来的ngx_process_events函数(在linux中就是ngx_epoll_process_events函数)中不会立刻处理事件,延后,先处理完所有的accept事件后,释放锁,然后再处理正常的读写socket事件。我们来看下ngx_epoll_process_events是怎么做的:

static ngx_int_t  
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)  
{  
。。。 。。。  
    events = epoll_wait(ep, event_list, (int) nevents, timer);  
。。。 。。。  
    ngx_mutex_lock(ngx_posted_events_mutex);  
  
    for (i = 0; i < events; i++) {  
        c = event_list[i].data.ptr;  
  
。。。 。。。  
  
        rev = c->read;  
  
        if ((revents & EPOLLIN) && rev->active) {  
。。。 。。。  
//有NGX_POST_EVENTS标志的话,就把accept事件放到ngx_posted_accept_events队列中,把正常的事件放到ngx_posted_events队列中延迟处理  
            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) {  
。。。 。。。  
//同理,有NGX_POST_EVENTS标志的话,写事件延迟处理,放到ngx_posted_events队列中  
            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_use_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;  
}

当nginx worker数量大于1时,也就是多个进程可能accept同一个监听的句柄,这时如果配置文件中accept_mutex开关打开了,就将ngx_use_accept_mutex置为1。
再看看有些负载均衡作用的ngx_accept_disabled是怎么维护的,在ngx_event_accept函数中:

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

表明,当已使用的连接数占到在nginx.conf里配置的worker_connections总数的7/8以上时,ngx_accept_disabled为正,这时本worker将ngx_accept_disabled减1,而且本次不再处理新连接。

最后,我们看下ngx_trylock_accept_mutex函数是怎么玩的

ngx_int_t  
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)  
{  
//ngx_shmtx_trylock是非阻塞取锁的,返回1表示成功,0表示没取到锁  
    if (ngx_shmtx_trylock(&ngx_accept_mutex)) {  
  
//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_mutex_held置为1,表示拿到锁了,返回  
        ngx_accept_events = 0;  
        ngx_accept_mutex_held = 1;  
  
        return NGX_OK;  
    }  
  
//处理没有拿到锁的逻辑,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;  
}

简单了说,就是同一时刻只允许一个nginx worker在自己的epoll中处理监听句柄。它的负载均衡也很简单,当达到最大connection的7/8时,本worker不会去试图拿accept锁,也不会去处理新连接,这样其他nginx worker进程就更有机会去处理监听句柄,建立新连接了。而且,由于timeout的设定,使得没有拿到锁的worker进程,去拿锁的频繁更高。

现有方案

除了上面的两种处理方案外,还有现在比较常见的一种方案,采用一个进程或线程去监听,使用epoll方式进行io处理,接收到连接请求后,然后将请求投递给多个worker来处理,worker是进程或线程。由于使用epoll方式,性能上可以满足网络IO需求,同时也避免了惊群现象。

发表评论

电子邮件地址不会被公开。 必填项已用*标注