`
russelltao
  • 浏览: 159199 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

“惊群”,看看nginx是怎么解决它的

 
阅读更多

在说nginx前,先来看看什么是“惊群”?简单说来,多线程/多进程(linux下线程进程也没多大区别)等待同一个socket事件,当这个事件发生时,这些线程/进程被同时唤醒,就是惊群。可以想见,效率很低下,许多进程被内核重新调度唤醒,同时去响应这一个事件,当然只有一个进程能处理事件成功,其他的进程在处理该事件失败后重新休眠(也有其他选择)。这种性能浪费现象就是惊群。


惊群通常发生在server 上,当父进程绑定一个端口监听socket,然后fork出多个子进程,子进程们开始循环处理(比如accept)这个socket。每当用户发起一个TCP连接时,多个子进程同时被唤醒,然后其中一个子进程accept新连接成功,余者皆失败,重新休眠。


那么,我们不能只用一个进程去accept新连接么?然后通过消息队列等同步方式使其他子进程处理这些新建的连接,这样惊群不就避免了?没错,惊群是避免了,但是效率低下,因为这个进程只能用来accept连接。对多核机器来说,仅有一个进程去accept,这也是程序员在自己创造accept瓶颈。所以,我仍然坚持需要多进程处理accept事件。


其实,在linux2.6内核上,accept系统调用已经不存在惊群了(至少我在2.6.18内核版本上已经不存在)。大家可以写个简单的程序试下,在父进程中bind,listen,然后fork出子进程,所有的子进程都accept这个监听句柄。这样,当新连接过来时,大家会发现,仅有一个子进程返回新建的连接,其他子进程继续休眠在accept调用上,没有被唤醒。


但是很不幸,通常我们的程序没那么简单,不会愿意阻塞在accept调用上,我们还有许多其他网络读写事件要处理,linux下我们爱用epoll解决非阻塞socket。所以,即使accept调用没有惊群了,我们也还得处理惊群这事,因为epoll有这问题。上面说的测试程序,如果我们在子进程内不是阻塞调用accept,而是用epoll_wait,就会发现,新连接过来时,多个子进程都会在epoll_wait后被唤醒!


nginx就是这样,master进程监听端口号(例如80),所有的nginx worker进程开始用epoll_wait来处理新事件(linux下),如果不加任何保护,一个新连接来临时,会有多个worker进程在epoll_wait后被唤醒,然后发现自己accept失败。现在,我们可以看看nginx是怎么处理这个惊群问题了。


nginx的每个worker进程在函数ngx_process_events_and_timers中处理事件,(void) ngx_process_events(cycle, timer, flags);封装了不同的事件处理机制,在linux上默认就封装了epoll_wait调用。我们来看看ngx_process_events_and_timers为解决惊群做了什么:

void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
。。。 。。。
    //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 {
            		//拿不到锁,也就不会处理监听的句柄,这个timer实际是传给epoll_wait的超时时间,修改为最大ngx_accept_mutex_delay意味着epoll_wait更短的超时返回,以免新连接长时间没有得到处理
                if (timer == NGX_TIMER_INFINITE
                    || timer > ngx_accept_mutex_delay)
                {
                    timer = ngx_accept_mutex_delay;
                }
            }
        }
    }
。。。 。。。
		//linux下,调用ngx_epoll_process_events函数开始处理
    (void) ngx_process_events(cycle, timer, flags);
。。。 。。。
		//如果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);
    }

    if (delta) {
        ngx_event_expire_timers();
    }

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "posted events %p", ngx_posted_events);
		//然后再处理正常的数据读写请求。因为这些请求耗时久,所以在ngx_process_events里NGX_POST_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 worker进程,同一时刻只能有一个worker进程在自己的epoll中加入监听的句柄。这个处理accept的nginx worker进程置flag为NGX_POST_EVENTS,这样它在接下来的ngx_process_events函数(在linux中就是ngx_epoll_process_events函数)中不会立刻处理事件,延后,先处理完所有的accept事件后,释放锁,然后再处理正常的读写socket事件。我们来看下ngx_epoll_process_events是怎么做的:

static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
。。。 。。。
    events = epoll_wait(ep, event_list, (int) nevents, timer);
。。。 。。。
    ngx_mutex_lock(ngx_posted_events_mutex);

    for (i = 0; i < events; i++) {
        c = event_list[i].data.ptr;

。。。 。。。

        rev = c->read;

        if ((revents & EPOLLIN) && rev->active) {
。。。 。。。
//有NGX_POST_EVENTS标志的话,就把accept事件放到ngx_posted_accept_events队列中,把正常的事件放到ngx_posted_events队列中延迟处理
            if (flags & NGX_POST_EVENTS) {
                queue = (ngx_event_t **) (rev->accept ?
                               &ngx_posted_accept_events : &ngx_posted_events);

                ngx_locked_post_event(rev, queue);

            } else {
                rev->handler(rev);
            }
        }

        wev = c->write;

        if ((revents & EPOLLOUT) && wev->active) {
。。。 。。。
//同理,有NGX_POST_EVENTS标志的话,写事件延迟处理,放到ngx_posted_events队列中
            if (flags & NGX_POST_EVENTS) {
                ngx_locked_post_event(wev, &ngx_posted_events);

            } else {
                wev->handler(wev);
            }
        }
    }

    ngx_mutex_unlock(ngx_posted_events_mutex);

    return NGX_OK;
}

看看ngx_use_accept_mutex在何种情况下会被打开:

    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;
    }

当nginx worker数量大于1时,也就是多个进程可能accept同一个监听的句柄,这时如果配置文件中accept_mutex开关打开了,就将ngx_use_accept_mutex置为1。

再看看有些负载均衡作用的ngx_accept_disabled是怎么维护的,在ngx_event_accept函数中:

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

表明,当已使用的连接数占到在nginx.conf里配置的worker_connections总数的7/8以上时,ngx_accept_disabled为正,这时本worker将ngx_accept_disabled减1,而且本次不再处理新连接。


最后,我们看下ngx_trylock_accept_mutex函数是怎么玩的:

ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
//ngx_shmtx_trylock是非阻塞取锁的,返回1表示成功,0表示没取到锁
    if (ngx_shmtx_trylock(&ngx_accept_mutex)) {

//ngx_enable_accept_events会把监听的句柄都塞入到本worker进程的epoll中
        if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
            ngx_shmtx_unlock(&ngx_accept_mutex);
            return NGX_ERROR;
        }
//ngx_accept_mutex_held置为1,表示拿到锁了,返回
        ngx_accept_events = 0;
        ngx_accept_mutex_held = 1;

        return NGX_OK;
    }

//处理没有拿到锁的逻辑,ngx_disable_accept_events会把监听句柄从epoll中取出
    if (ngx_accept_mutex_held) {
        if (ngx_disable_accept_events(cycle) == NGX_ERROR) {
            return NGX_ERROR;
        }

        ngx_accept_mutex_held = 0;
    }

    return NGX_OK;
}                              

OK,关于锁的细节是如何实现的,这篇限于篇幅就不说了,下篇帖子再来讲。现在大家清楚nginx是怎么处理惊群了吧?简单了说,就是同一时刻只允许一个nginx worker在自己的epoll中处理监听句柄。它的负载均衡也很简单,当达到最大connection的7/8时,本worker不会去试图拿accept锁,也不会去处理新连接,这样其他nginx worker进程就更有机会去处理监听句柄,建立新连接了。而且,由于timeout的设定,使得没有拿到锁的worker进程,去拿锁的频繁更高。


分享到:
评论

相关推荐

    详解nginx惊群问题的解决方式

    NGINX通过采用一种共享锁的机制来解决惊群问题。 在NGINX中,master进程负责监听配置文件中的端口,并创建多个子进程,这些子进程继承了master进程的内存数据和监听的端口,被称为worker进程。当客户端有新的连接...

    自动reload nginx解决nginx对动态域名不重新解析的Shell脚本

    Nginx作为一款广泛使用的反向代理和负载均衡器,它在配置文件中记录了服务的IP地址。然而,当这些IP发生变更时,Nginx不会自动更新其内部的DNS解析,导致请求无法正确路由到新的IP。为了解决这个问题,我们可以编写...

    nginx安装, 解决跨域问题

    **Nginx安装与解决跨域问题** 在现代Web开发中,由于浏览器的同源策略,跨域问题经常出现,限制了不同源之间的通信。Nginx作为一个高性能的反向代理服务器,常被用于处理此类问题。本篇将详细介绍如何在Linux环境下...

    Linux系统Nginx日志解决方案.docx

    Linux 系统 Nginx 日志解决方案 本文将详细介绍如何使用 Nginx、Promtail、Loki 和 Grafana 实现一个简单的 Nginx 日志展示解决方案。该解决方案旨在满足客户的需求,查看网站的访问情况,并且不依赖于 Google 或...

    nginx解决跨域问题的实例方法

    浏览器的同源策略限制了JavaScript从一个源获取另一个源的数据,而Nginx作为一个强大的反向代理服务器,可以有效地解决这个问题。本篇文章将详细介绍如何利用Nginx解决跨域问题。 首先,了解跨域的基本概念。跨域是...

    nginx1.18 nginx1.18 nginx1.18

    Nginx 1.18 是 Nginx Web 服务器的一个特定版本,它在 Linux 操作系统上运行。Nginx 是一个流行的开源 Web 服务器,以其高性能、高并发处理能力而闻名,常用于静态内容服务和反向代理。在这个版本中,Nginx 提供了...

    Nginx常见错误及解决方法.doc

    Nginx 常见错误及解决方法 Nginx 是一个流行的开源 Web 服务器软件,然而在实际使用中经常会遇到各种错误,影响服务器的稳定运行。下面将介绍 Nginx 中的一些常见错误及解决方法。 一、Nginx 启动错误 在安装 ...

    nginx跨域问题,解决多端口,多ip问题

    Nginx 跨域问题解决方案 Nginx 是一个流行的开源 Web 服务器软件,广泛应用于 Web 服务器管理。然而,在使用 Nginx 进行服务器管理时,经常会遇到跨域问题。跨域问题是指在不同的域名、端口或协议下,无法访问...

    基于ELK的nginx-qps监控解决方案.docx

    基于ELK的nginx-qps监控解决方案 在现代网络架构中,监控和日志分析是非常重要的组件之一。ELK(Elasticsearch、Logstash、Kibana) stack是当前最流行的日志分析解决方案之一。今天,我们将讨论基于ELK的nginx-qps...

    nginx正向代理解决非80端口请求

    nginx做正向代理,假设监听80端口,而一个用户请求的url带非80端口号,nginx会默认将该url请求转到80端口,百度了一番,网友提供的方法都无法解决问题,于是自己用lua解决了: 1、最多的解决方式就是在$host后面添加...

    使用docker部署nginx前后端解决跨域问题.docx

    使用docker部署nginx前后端解决跨域问题

    Nginx 反向代理解决JS跨域

    通过使用Nginx 反向代理来解决JS跨域问题 http://blog.csdn.net/mzhaocai/article/details/79238338

    Nginx启动、重启失败的一般解决方法和步骤

    今天在do的VPS配置Nginx虚拟主机时,修改配置文件后,重启Nginx后一直报告失败,但是不知道哪里错了,直觉觉得是配置文件配置错了,google了下解决方案。 解决方案 Nginx启动或重启失败,一般是因为配置文件出错了,...

    nginx-1.13.3,nginx1.13.3不存在信息泄漏漏洞安全稳定nginx版本

    Nginx 1.13.3 版本强调了安全稳定,这意味着它已经修复了之前版本可能存在的已知安全漏洞。信息泄漏漏洞是网络服务中的常见问题,可能导致敏感数据暴露,威胁到用户隐私和系统的整体安全性。在 Nginx 1.13.3 中,...

    利用Nginx反向代理解决跨域问题详解

    问题 在之前的分享的跨域资源共享的文章中,有提到... 基于与合作方后台的配合,利用nginx方向代理来满足浏览器的同源策略来实现跨域 实现方法 反向代理概念 反向代理(Reverse Proxy)方式是指以代理服务器来接受In

    nginx部署步骤,vue解决跨域

    vue 跨域解决 Linux上部署nginx

    ubuntu 1804 nginx 离线安装包

    本资源提供了一个适用于这种场景的解决方案,它包括了Ubuntu 18.04环境下Nginx的离线安装包。这个离线包旨在帮助运维人员以及学习离线部署的学生在不依赖网络的情况下完成Nginx的部署。 首先,我们需要了解Nginx。...

    Nginx-1.23.2.zip

    Nginx是一个高性能的Web服务器和反向代理服务器,它以其高效的并发处理能力、低内存占用和稳定性而闻名。在1.23.2版本中,Nginx继续提供了优化和改进,以满足不断变化的互联网需求。这个版本可能是对之前版本的bug...

    nginx 部署 vue 项目找不到js css文件的解决方法

    综上所述,解决nginx部署Vue项目时找不到js和css文件的问题,关键在于正确设置Webpack打包时资源引用的基础路径。对于vue-cli@3,你需要在vue.config.js中配置baseUrl;而对于vue-cli@2,则需要修改config/index.js...

Global site tag (gtag.js) - Google Analytics