定义
- 惊群问题是指在多线程(或多进程)场景下,有多个线程在等待某一资源可用,一旦这个资源可用,那么所有等待这个资源的线程都会被唤醒,但是资源只有一份,那么只有一个线程获得这个资源,其它线程都获取失败
- 惊群问题导致了不必要的线程唤醒,实际上只有一个线程能获取这份资源,那么理想情况下只唤醒一个线程就行了。而唤醒多个线程导致了不必要的线程调度,造成系统开销
经典的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)
{
if(ngx_shmtx_trylock(&ngx_accept_mutex)) {
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;
}
if(ngx_accept_mutex_held) {
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中删除