Nginx——事件驱动机制(惊群问题,负载均衡)

事件框架处理流程

每个worker子进程都在ngx_worker_process_cycle方法中循环处理事件,处理分发事件则在ngx_worker_process_cycle方法中调用ngx_process_events_and_timers方法,循环调用该方法就是 在处理所有事件,这正是事件驱动机制的核心。该方法既会处理普通的网络事件,也会处理定时器事件。

ngx_process_events_and_timers方法中核心操作主要有以下3个:

1)  调用所使用事件驱动模块实现的process_events方法,处理网络事件

2)  处理两个post事件队列中的事件,实际上就是分别调用ngx_event_process_posted(cycle, &ngx_posted_accept_events)和ngx_event_process_posted(cycle,&ngx_posted_events)方法

3)  处理定时事件,实际上就是调用ngx_event_expire_timers()方法

下面是ngx_process_events_and_timers方法中的时间框架处理流程图以及源代码,可以结合理解:

源代码如下:

void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
    ngx_uint_t  flags;
    ngx_msec_t  timer, delta;
	/*如果配置文件中使用了timer_resolution配置项,也就是ngx_timer_resolution值大于0,
	则说明用户希望服务器时间精度为ngx_timer_resolution毫秒。这时,将ngx_process_changes
	的timer参数设为-1,告诉ngx_process_change方法在检测时间时不要等待,直接搜集所有已经
	就绪的时间然后返回;同时将flag参数初始化为0,它是在告诉ngx_process_changes没有任何附加
	动作*/
    if (ngx_timer_resolution) {
        timer = NGX_TIMER_INFINITE;
        flags = 0;

    } else {
	/*如果没有使用timer_resolution,那么将调用ngx_event_find_timer()方法,获取最近一个将要
	触发的时间距离现在有多少毫秒,然后把这个值赋予timer参数,告诉ngx_process_change方法在
	检测事件时如果没有任何事件,最多等待timer毫秒就返回;将flag参数设置为UPDATE_TIME,告诉
	ngx_process_change方法更新换成的时间*/
        timer = ngx_event_find_timer();
        flags = NGX_UPDATE_TIME;

#if (NGX_THREADS)

        if (timer == NGX_TIMER_INFINITE || timer > 500) {
            timer = 500;
        }

#endif
    }
	 /*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 {
			/*获取锁失败,意味着既不能让当前worker进程频繁的试图抢锁,也不能让它经过太长事件再去抢锁
              下面的代码:即使开启了timer_resolution时间精度,牙需要让ngx_process_change方法在没有新
			  事件的时候至少等待ngx_accept_mutex_delay毫秒之后再去试图抢锁 而没有开启时间精度时,
			  如果最近一个定时器事件的超时时间距离现在超过了ngx_accept_mutex_delay毫秒,也要把timer设
			  置为ngx_accept_mutex_delay毫秒,这是因为当前进程虽然没有抢到accept_mutex锁,但也不能让
			  ngx_process_change方法在没有新事件的时候等待的时间超过ngx_accept_mutex_delay,这会影响
			  整个负载均衡机制*/
                if (timer == NGX_TIMER_INFINITE
                    || timer > ngx_accept_mutex_delay)
                {
                    timer = ngx_accept_mutex_delay;
                }
            }
        }
    }
	//计算ngx_process_events消耗的时间
    delta = ngx_current_msec;
	//linux下,调用ngx_epoll_process_events函数开始处理
    (void) ngx_process_events(cycle, timer, flags);
	//函数处理消耗时间
    delta = ngx_current_msec - delta;

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "timer delta: %M", delta);
	//如果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);
    }
	//如果ngx_process_events消耗的时间大于0,那么这时可能有新的定时器事件触发
    if (delta) {
		//处理定时事件
        ngx_event_expire_timers();
    }

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "posted events %p", ngx_posted_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处于充分发挥多核CPU架构性能的考虑,使用了多个worker子进程监听相同端口的设计,这样多个子进程在accept建立新连接时会有争抢,这会带来的“惊群”问题,子进程数量越多越明显,这会造成系统性能的下降。

master进程开始监听Web端口,fork出多个worker子进程,这些子进程同时监听同一个Web端口。一般情况下,有多少CPU核心就有配置多少个worker子进程,这样所有的worker子进程都在承担着Web服务器的角色,从而发挥多核机器的威力。假设现在没有用户连入服务器,某一时刻恰好所有的子进程都休眠且等待新连接的系统调用,这时有一个用户向服务器发起了连接,内核在收到TCP的SYN包时,会激活所有的休眠worker子进程。最终只有最先开始执行accept的子进程可以成功建立新连接,而其他worker子进程都将accept失败。这些accept失败的子进程被内核唤醒是不必要的,他们被唤醒会的执行很可能是多余的,那么这一时刻他们占用了本不需要占用的资源,引发了不必要的进程切换,增加了系统开销。

很多操作系统的最新版本的内核已经在事件驱动机制中解决了惊群问题,但Nginx作为可移植性极高的web服务器,还是在自身的应用层面上较好的解决了这一问题。既然惊群是个多子进程在同一时刻监听同一个端口引起的,那么Nginx的解决方法也很简单,它规定了同一时刻只能有唯一一个worker子进程监听Web端口,这样就不会发生惊群了,此时新连接时间就只能唤醒唯一正在监听端口的worker子进程。

如何限制在某一时刻仅能有一个子进程监听web端口呢?在打开accept_mutex锁的情况下,只有调用ngx_trylock_accept_mutex方法后,当前的worker进程才会去试着监听web端口。

该方法具体实现如下:

ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
	/*
	使用进程间的同步锁,试图获取accept_mutex。注意,ngx_trylock_accept_mutex返回1表示成功
	拿到锁,返回0表示获取锁失败。这个获取所的过程是非阻塞的,此时一旦锁被其他worker子进程占
	用,该方法会立刻返回。
	*/
    if (ngx_shmtx_trylock(&ngx_accept_mutex)) {

        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                       "accept mutex locked");
		/*如果获取到accept_mutex锁,但ngx_accept_mutex_held为1,则立刻返回。ngx_accept_mutex_held
		是一个标志位,当它为1时,表示当前进程已经获取到锁了*/
        if (ngx_accept_mutex_held
            && ngx_accept_events == 0
            && !(ngx_event_flags & NGX_USE_RTSIG_EVENT))
        {
			//ngx_accept_mutex锁之前已经获取到了,立刻返回
            return NGX_OK;
        }
		//将所有监听连接的事件添加到当前epoll等事件驱动模块中
        if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
			/*既然将监听句柄添加到事件驱动模块失败,就必须释放ngx_accept_mutex锁*/
            ngx_shmtx_unlock(&ngx_accept_mutex);
            return NGX_ERROR;
        }
		/*经过ngx_enable_accept_events方法的调用,当前进程的时间驱动模块已经开始监
		听所有的端口,这时需要把ngx_accept_mutex_heald标志置为1,方便本进程的其他模
		块了解它目前已经获取到了锁*/
        ngx_accept_events = 0;
        ngx_accept_mutex_held = 1;

        return NGX_OK;
    }
	/*如果ngx_shmtx_trylock返回0,则表明获取ngx_accept_mutex锁失败,这时如果
	ngx_accept_mutex_held标志还为1,即当前进程还在获取到锁的状态,这显然不正确,需要处理*/
    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "accept mutex lock failed: %ui", ngx_accept_mutex_held);

    if (ngx_accept_mutex_held) {
		/*ngx_disable_accept_events(会将所有监听连接的读事件从事件驱动模块中移除*/
        if (ngx_disable_accept_events(cycle) == NGX_ERROR) {
            return NGX_ERROR;
        }
		/*在没有获取到ngx_accept_mutex锁时,必须把ngx_accept_mutex_held置为0*/
        ngx_accept_mutex_held = 0;
    }

    return NGX_OK;
}

在上面的代码中,ngx_accept_mutex是进程间的同步锁(见http://blog.csdn.net/walkerkalr/article/details/38237147),ngx_accept_mutex_held是当前进程的一个全局变量,他们的定义如下:

ngx_shmtx_t		ngx_accept_mutex;
ngx_uint_t		ngx_accept_mutex_held;

因此,在调用ngx_try_accept_mutex方法后,如果没有获取到锁,当前进程调用process_events时只能处理已有连接上的事件。如果唯一获取到锁且其epoll等事件驱动模块开始监控web端口上的新连接事件。这种情况下,调用process_events时就会既处理已有连接上的事件,也处理新连接的事件,但这样的话,什么时候释放ngx_accept_mutex锁呢?如果等到这批事件全部执行完,由于这个worker进程上可能有很多活跃的连接,处理这些连接上的事件会占用很长时间,也就是说,会很长时间都没有释放ngx_accept_mutex锁,这样,其他worker进程就很难得到处理新连接的机会。

如何解决长时间占用ngx_accept_mutex的问题呢?这就要依靠ngx_posted_accept_events队列(存放新连接事件的队列)和ngx_posted_events队列(存放普通事件的队列)。实际上ngx_posted_accepted_events队列和ngx_posted_events队列把事件进行了归类,以使先处理ngx_posted_accept_events队列中的事件,处理完后就要释放ngx_accept_mutex锁,接着再处理ngx_posted_events队列中的时间,这样就大大减少了ngx_accept_mutex锁占用的时间。

负载均衡

在建立连接时,在多个子进程争抢处理一个新连接时间时,一定只有一个worker子进程最终会成功简历连接,随后,它会一直处理这个连接直到连接关闭。那么,如果有的子进程很勤奋,他们抢着建立并处理了大部分连接,而有的子进程则运气不好,只处理了少量连接,这对多核CPU架构下的应用是很不利的,因为子进程之间应该是平等的,每个子进程应该尽量独占一个CPU核心。子进程间负载不均衡,必定会影响整个服务的性能。

与惊群问题的解决方法一样,只有打开了accept_mutex锁,才能实现子进程间的负载均衡。在这里,初始化了一个全局变量ngx_accept_disabled,他就是负载均衡机制实现的关键阈值,实际上它就是一个整型数据。

ngx_int_t             ngx_accept_disabled;

这个阈值是与连接池中连接的使用密切相关的,在建立连接时会进行赋值,如下所示

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

因此,在启动时该阈值是一个负值,其绝对值是连接总数的7/8。其实ngx_accept_disabled的用法很简单,当它为负数时,不会触发负载均衡操作,正常获取accept锁,试图处理新连接。而当ngx_accept_disabled是正数时,就会触发Nginx进行负载均衡操作了,nginx此时不再处理新连接事件,取而代之的仅仅是ngx_accept_disabled值减1,,这表示既然经过一轮事件处理,那么相对负载肯定有所减小,所以要相应调整这个值。如下所示

if (ngx_accept_disabled > 0) {
            ngx_accept_disabled--;
        } else {
            //调用ngx_trylock_accept_mutex方法,尝试获取accept锁
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
                return;
            }

Nginx各worker子进程间的负载均衡仅在某个worker进程处理的连接数达到它最大处理总数的7/8时才会触发,这时该worker进程就会减少处理新连接的机会,这样其他较空闲的worker进程就有机会去处理更多的新连接,以达到整个web服务器的均衡效果。

Nginx——事件驱动机制(惊群问题,负载均衡)

时间: 2024-10-07 12:29:43

Nginx——事件驱动机制(惊群问题,负载均衡)的相关文章

在Linux上使用Nginx为Solr集群做负载均衡

在Linux上使用Nginx为Solr集群做负载均衡 在Linux上搭建solr集群时需要用到负载均衡,但测试环境下没有F5 Big-IP负载均衡交换机可以用,于是先后试了weblogic的proxyserver,和Apache,效果均很差,两台服务器做了负载均衡之后并发响应速度还没单台不做负载均衡的速度快.最后使用nginx,效果很不错,下面将在Linux上安装步骤记述一下. 1        安装准备 nginx软件版本:nginx 1.2.5 安装包:rhel_nginx-1.2.5-1.

用Nginx搭建IIS集群实现负载均衡

长话短说,我们用Nginx来搭建一个简单的集群,实现Web应用的负载均衡,架构图如下: 两台Web服务器,一台静态资源服务器,因为是演示,我们以网站形式部署在本机IIS中 一台Nginx代理服务器,安装到本机的Linux虚拟机中,参考 CentOS下Nginx安装与配置 网站部署 新建三个文件夹,用于网站部署 在IIS中新建三个站点,分别指向以上三个目录,两台WEB端口是9527,9528,静态资源站点端口9529 新建两个HTML网页 index.html,内容分别是 This is loca

Nginx实现tomcat集群进行负载均衡

一.背景 随着业务量和用户数量的激增,单一的tomcat部署应用已经无法满足性能需求,而且对于每次发布项目期间服务不可用的问题也凸显,既然出现了这个问题,那么我们本文就借助nginx来完美的解决这个问题. 二.基本概念 1.说明:关于Nginx的概念和介绍以及Centos7下安装步骤,请移步:Centos7安装Nginx实战 2.正向代理和反向代理 假设我们给定客户端A.代理服务器B.以及最终服务器C 正向代理:代理服务器B来代替客户端A来访问最终服务器C并将最终结果转发给客户端A,站在客户端A

nginx给consul集群配置负载均衡

upstream consul { server 127.0.0.1:8501; server 127.0.0.1:8502; server 127.0.0.1:8503; } server { listen 80; server_name consul.test.com;#服务域名,需要填写你的服务域名 location / { proxy_pass http://consul;#请求转向consul服务器列表 proxy_set_header Host $host; proxy_set_he

Nginx实现集群的负载均衡配置过程详解

Nginx实现集群的负载均衡配置过程详解 Nginx 的负载均衡功能,其实实际上和 nginx 的代理是同一个功能,只是把代理一台机器改为多台机器而已. Nginx 的负载均衡和 lvs 相比,nginx属于更高级的应用层,不牵扯到 ip 和内核的修改,它只是单纯地把用户的请求转发到后面的机器上.这就意味着,后端的 RS 不需要配置公网. 一.实验环境 Nginx 调度器 (public 172.16.254.200 privite 192.168.0.48)RS1只有内网IP (192.168

夺命雷公狗---linux NO:41 linux下nginx的集群与负载均衡

集群,比如买了一代推服务器下来放一块他也就相当聚群,负载均衡就是让这一大堆服务器帮忙来平均干活他就是所谓的负载均衡,如下图所示: 比如我用192.168.8.155充当与服务器A,  192.168.8.166   和  192.168.8.177  来充当  pic 主机1  和pic  主机2,如下所示: 然后开始修改 服务器A(192.168.8.155)  conf  目录下的  nginx.conf  文件即可: upstream imgserver{ server 192.168.8

nginx反向代理tomcat集群达到负载均衡,同时使用proxy_cache做web缓存

Nginx最早是作为一款优秀的反向代理软件,以高并发下的卓越性能被越来越多的用户所用户,国内最早研究nginx的是张宴,该大牛的博客地址:http://zyan.cc/ .但是随着需求的发展,nginx的功能已经不再单单是反向代理,现在已经更倾向作为web容器. Nginx从0.7.48版本开始,支持了类似Squid的缓存功能.Nginx的Web缓存服务主要由proxy_cache相关指令集和fastcgi_cache相关指令集构成,前者用于反向代理时,对后端内容源服务器进行缓存,后者主要用于对

架构之路:nginx与IIS服务器搭建集群实现负载均衡(三)

参考网址:https://blog.csdn.net/zhanghan18333611647/article/details/50811980 [前言] 在<架构之路:nginx与IIS服务器搭建集群实现负载均衡(二)>中提到有好多有趣的地方,接下来就为大家一块儿讲讲在深入研究过程中遇到那些有趣的事情. ·实战之行--发现问题 ·探索之旅--寻出问题原因 ·解决之道--解决问题 [实战之行] 在<架构之路:nginx与IIS服务器搭建集群实现负载均衡(二)>中做了小Demo,当时做

架构之路:nginx与IIS服务器搭建集群实现负载均衡(二)

[前言] 在<架构之路:nginx与IIS服务器搭建集群实现负载均衡(一)>中小编简单的讲解了Nginx的原理!俗话说:光说不练假把式.接下来,小编就和大家一起来做个小Demo来体会一下Nginx的神奇之处. [准备工作] ·安装一款文本编辑器(这里以Notepad++为例) ·下载Nginx(这里以Nginx-1.4.7为例,其他版本操作相同) ·建两个简单网页:在文件夹test1新建一个html页内容为--我是Test1,在文件夹test2新建一个html页内容为--我是Test2) ·将