菜鸟学习nginx之惊群处理

“惊群”这个名词是我阅读Nginx时第一次接触到的,也算是学到了一点点知识吧。

一、惊群

1.1、惊群定义

对于惊群的概念简单描述一下:通常场景一个端口P1只能被一个进程A监听,所以端口P1发的事件都会被该进程A所处理。但是,如果进程A通过系统调用fork(),创建子进程B,那么进程B也能够监听端口P1。这样就可以实现多进程监听同一个端口并且进入阻塞状态。这样就引发了一个问题,当客户端发起TCP连接的时候,那么到底由谁来负责处理Accept事件呢?总不能多个进程同时处理?最终只能有一个进程来处理Accept事件,也就是说当Accept事件来了,操作系统会把所有进程都唤醒(之前是阻塞状态),这么多进程同时去抢占,抢到进程处理后续流程,没有抢到的进程继续阻塞。就是所谓的惊群。

这种方式白白浪费cpu资源,切换进程/线程上下文。

1.2、如何解决?

既然在同一时刻只能有一个进程能够处理,那么何不加锁进行同步操作呢?对,这就是Nginx实现的方式,而且这是目前仅有的方式。其实Linux在2.6以后的版本已经完美解决了惊群问题,所以我们在编写服务端程序时,可以忽律该问题。但是Nginx是跨平台的一个软件,为了保证有效性,Nginx自己实现了一套机制。

二、Nginx惊群解决方案

Nginx为了解决惊群问题从两个方面做了工作:负载均衡和互斥锁。

2.1、负载均衡

在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。

2.2、加锁

上面的负载均衡只是减少冲突的可能性,但是并不能彻底解决问题,因此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从自己的事件驱动中删除。

2.3、什么场景开启互斥锁

开启互斥锁有三个条件:

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框架

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值