“惊群”这个名词是我阅读Nginx时第一次接触到的,也算是学到了一点点知识吧。
对于惊群的概念简单描述一下:通常场景一个端口P1只能被一个进程A监听,所以端口P1发的事件都会被该进程A所处理。但是,如果进程A通过系统调用fork(),创建子进程B,那么进程B也能够监听端口P1。这样就可以实现多进程监听同一个端口并且进入阻塞状态。这样就引发了一个问题,当客户端发起TCP连接的时候,那么到底由谁来负责处理Accept事件呢?总不能多个进程同时处理?最终只能有一个进程来处理Accept事件,也就是说当Accept事件来了,操作系统会把所有进程都唤醒(之前是阻塞状态),这么多进程同时去抢占,抢到进程处理后续流程,没有抢到的进程继续阻塞。就是所谓的惊群。
这种方式白白浪费cpu资源,切换进程/线程上下文。
既然在同一时刻只能有一个进程能够处理,那么何不加锁进行同步操作呢?对,这就是Nginx实现的方式,而且这是目前仅有的方式。其实Linux在2.6以后的版本已经完美解决了惊群问题,所以我们在编写服务端程序时,可以忽律该问题。但是Nginx是跨平台的一个软件,为了保证有效性,Nginx自己实现了一套机制。
Nginx为了解决惊群问题从两个方面做了工作:负载均衡和互斥锁。
在Nginx中有两种负载均衡:
类别 | 作用 |
进程级负载均衡(前端负载均衡) | 主要用于接收客户端连接,即Accept事件。这个是为了解决惊群问题的一个优化点。 |
服务级负载均衡(后端负载均衡) | 主要用于访问后台服务,例如mysql,apache等。这个是我们通常所说的负载均衡。 |
Nginx解决惊群相关代码如下:
/* 解决惊群 */
if (ngx_use_accept_mutex)
{
if (ngx_accept_disabled > 0)
{//实现worker进程间负载均衡
ngx_accept_disabled--;
}
else
{//解决惊群,通过进程间同步锁
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR)
{
return;
}
if (ngx_accept_mutex_held)
{
flags |= NGX_POST_EVENTS;
}
else
{
if (timer == NGX_TIMER_INFINITE || timer > ngx_accept_mutex_delay)
{
timer = ngx_accept_mutex_delay;
}
}
}
}
只有ngx_use_accept_mutex是1时表示开启负载均衡和惊群处理。 为什么说负载均衡能够减少惊群冲突呢?
Nginx内部实现,当一个worker进程已经服务连接数达到7/8*connetctions(最大连接数的八分之七)时,不在处理新的连接事件(Accept事件),也就是说不会去竞争锁,即不会把listening socket添加到自己的事件驱动中。也就能够减少惊群冲突。
全局变量ngx_accept_disabled初始值为负数,当处理一个新的Accept事件则变量就加1。具体代码如下:
void
ngx_event_accept(ngx_event_t *ev)
{
...
/* 负数 */
ngx_accept_disabled = ngx_cycle->connection_n / 8
- ngx_cycle->free_connection_n;
c = ngx_get_connection(s, ev->log);//获取新连接 并且free_connection_n减一
...
}
对于新的连接请求(Accept事件)处理函数是ngx_event_accept,当成功获取connection对象后free_connection_n就是减1,其中connection_n始终不变。举例说明:在Nginx刚启动完毕时(没有处理一个新连接)最大处理连接数connection_n=1024,free_connection_n=1024,那么ngx_accept_disabled=-896(负数,八分之七)。当处理一个新的连接之后,free_connection_n变为1023,那么ngx_accept_disabled=-895。
上面的负载均衡只是减少冲突的可能性,但是并不能彻底解决问题,因此Nginx通过互斥锁(Nginx锁采用的共享内存方式)解决惊群问题。其原理是:只有获取到锁的那个进程才能接受新的TCP连接事件(Accept事件),具体实现如下:
ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
if (ngx_shmtx_trylock(&ngx_accept_mutex)) {//异步方式 尝试加锁 加锁成功返回1
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"accept mutex locked");
if (ngx_accept_mutex_held && ngx_accept_events == 0) {
return NGX_OK;
}
/* 只有获取到锁 才能将listen socket 添加到自己的事件驱动中 */
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_accept_mutex_held = 0 表示上一次没有获得锁(非本次) 也就是说该进程
* 连续两次获取锁失败
* ngx_accept_mutex_held = 1 表示上一次获得锁但是本次获得锁失败,这个时候需要
* 将listen socket 移除事件驱动本进程不得继续accept事件
*/
if (ngx_accept_mutex_held) {
if (ngx_disable_accept_events(cycle, 0)==NGX_ERROR) {//将listen socket移除时间循环
return NGX_ERROR;
}
ngx_accept_mutex_held = 0;//修改标志位
}
return NGX_OK;
}
举例说明:经过这个函数处理之后,进程B获得了锁,会把listen socket加入到自己的事件驱动中,以后新连接均由该进程B服务而原先获得锁的进程A要把listen socket从自己的事件驱动中删除。
开启互斥锁有三个条件:
1、Nginx服务模式必须是master/worker模式
2、worker进程数大于1
3、nginx.conf配置文件开启,accept_mutex配置,举例说明如下:
//nginx.conf配置文件
worker_processes 5;
events {
worker_connections 1024;
accept_mutex on;
}
代码逻辑判断如下所示:
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;
}
【前提】进程A是上次竞争锁成功(ngx_accept_mutex_held=1),进程B上次竞争锁失败(ngx_accept_mutex_held=0)。
【目前情况】此时进程A和进程B同时竞争锁,即同时执行ngx_trylock_accept_mutex->ngx_shmtx_trylock。竞争结果是进程A失败,进程B成功,那么进程B需要的做的工作是将listen socket加入自己的事件驱动epoll中。
【问题】当进程B把listen socket加到epoll完成后且进程B还没有把listen socket从epoll中移除,就在这个时候客户端发起新连接请求(Accept事件),此应该由谁处理呢?
希望有了解的网友能够留言给我,谢谢。
这里介绍了Nginx解决惊群的原理,后面开始介绍Nginx核心内容--HTTP框架。