定义

  • 惊群问题是指在多线程(或多进程)场景下,有多个线程在等待某一资源可用,一旦这个资源可用,那么所有等待这个资源的线程都会被唤醒,但是资源只有一份,那么只有一个线程获得这个资源,其它线程都获取失败
  • 惊群问题导致了不必要的线程唤醒,实际上只有一个线程能获取这份资源,那么理想情况下只唤醒一个线程就行了。而唤醒多个线程导致了不必要的线程调度,造成系统开销

经典的accept惊群问题

  • 在Linux的早期版本,accept函数存在惊群问题
  • 考虑一个场景,父进程通过socket、bind、listen创建了一个监听描述符,然后fork出多个子进程,所有进程都accept这个监听描述符,并在没有新连接到来时陷入阻塞。每当有新连接到来时,所有阻塞在accept的进程都会被唤醒,然后其中一个进程accept成功返回,其它进程返回EAGAIN错误
  • 上面的场景就是经典的accept惊群问题
  • 现在的Linux版本已经不存在这样的accept惊群问题了,也就是说,在多个进程阻塞在accept时,新连接的到来只会唤醒一个进程。但是,这不意味着惊群问题已经解决了

epoll惊群

  • 如上所述,accept系统调用已经不存在惊群问题,但是epoll上还存在惊群问题。
  • 考虑一个场景,父进程创建了一个监听描述符,通过epoll_ctl将该监听套接字加入到epoll中,然后fork出多个子进程,每个子进程都在epoll_wait中阻塞等待监听描述符的可读事件(新连接到来),当有新连接到来时,所有子进程都会被唤醒。
  • Linux内核处理了accept惊群现象,但是没处理epoll惊群现象,可能是因为accept只能被一个进程处理;而epoll监听的描述符也许可以被多个进程处理,比如一个文件可能会由多个进程进行读写。因此内核无法判断是否需要处理epoll的惊群现象

Nginx对惊群问题的处理

  • Nginx的策略是,规定了同一时刻只能有唯一一个worker子进程监听Web端口,此时新连接事件只能唤醒一个进程,不会有惊群现象
  • 具体使用accept_mutex锁的方式实现
ngx_int_t ngx_trylock_accept_mutex(ngx_cycle_t* cycle)
{
    /* 尝试获取accept_mutex锁,如果可以获得,返回1;不可以获得直接返回0,不会阻塞 */
    if(ngx_shmtx_trylock(&ngx_accept_mutex)) {
        /* accept_mutex锁之前已经获取到了,直接返回 */
        if(ngx_accept_mutex_held 
           && ngx_accept_events == 0 
           && !(ngx_event_flags & NGX_USE_RTSIG_EVENT)) {
            return NGX_OK;
        }
        /* 把所有监听连接的读事件添加到当前的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;
    }

    if(ngx_accept_mutex_held) {
        /* 将监听连接的读事件从epoll中删除 */
        if(ngx_disable_accept_events(cycle) == NGX_ERROR) {
            return NGX_ERROR;
        }
        ngx_accept_mutex_held = 0;
    }
    return NGX_OK;
}
  • 从上面的代码可以看出,任意时刻只有一个进程能获得accept_mutex锁,获得accept_mutex锁的进程能监听web端口;无法获得accept_mutex锁的进程会把监听套接字从其epoll中删除
Logo

更多推荐