nginx 事件模块(6) - 负载均衡

2018-07-27 20:25:24

早期 nginx 引以为傲的一项功能就是负载均衡,可以不依赖操作系统在各个 worker 进程之间均匀的处理连接,充分利用 cpu。但是多年的实验证明,它会拖累整个服务器的性能。

我相信如果在多核cpu里满压力测试过的朋友会发现,cpu怎么都打不满,就是锁的原因。

所以从 1.11.3 开始,nginx 默认关闭了负载均衡功能。

如果 linux 内核版本在 3.9 以上,根本不需要负载均衡,可以使用在listen 上指定 reuseport ,这样内核帮我们均衡,效率非常高。

不过对于我们来说, 研究 nginx 负载均衡机制的工作原理还是很有意义的,可以学习它的设计思想。

开启负载均衡

accept_mutex on 可以开启负载均衡,不过前提必须是多个 worker进程。

// 使用master/worker多进程,使用负载均衡
    if (ccf->master && ccf->worker_processes > 1 && ecf->accept_mutex) {
        // 设置全局变量
        // 使用负载均衡,刚开始未持有锁,设置抢锁的间隔等待时间
        // 默认间隔时间为500ms
        ngx_use_accept_mutex = 1;
        ngx_accept_mutex_held = 0;
        ngx_accept_mutex_delay = ecf->accept_mutex_delay;

    } else {
        // 单进程、未明确指定负载均衡,就不使用负载均衡
        ngx_use_accept_mutex = 0;
    }

如果启用了负载均衡,那么 worker 进程不会立即把监听端口加入 epoll,只有抢到锁的进程才有资格从监听端口获取读事件接受连接。

accept 锁的持有者是唯一的,所以任意时刻只有一个进程在监听端口的连接事件。

if (ngx_use_accept_mutex) {
        // ngx_accept_disabled = ngx_cycle->connection_n / 8
        //                      - ngx_cycle->free_connection_n;
        // ngx_accept_disabled是总连接数的1/8-空闲连接数
        // 也就是说空闲连接数小于总数的1/8,那么就暂时停止接受连接
        if (ngx_accept_disabled > 0) {

            // 但也不能永远不接受连接,毕竟还是有空闲连接的,所以每次要减一
            ngx_accept_disabled--;

        } else {
            // 尝试获取负载均衡锁,开始监听端口
            // 如未获取则不监听端口
            // 内部调用ngx_enable_accept_events/ngx_disable_accept_events
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
                // 如果监听失败,那么直接结束函数,不处理epoll事件
                return;
            }

            // ngx_trylock_accept_mutex执行成功
            // 使用变量ngx_accept_mutex_held检查是否成功获取了锁

            // 确实已经获得了锁,接下来的epoll的事件需要加入延后队列处理
            // 这样可以尽快释放锁给其他进程,提高运行效率
            if (ngx_accept_mutex_held) {

                // 加上NGX_POST_EVENTS标志
                // epoll获得的所有事件都会加入到ngx_posted_events
                // 待释放锁后再逐个处理,尽量避免过长时间持有锁
                flags |= NGX_POST_EVENTS;

            } else {
                // 未获取到锁
                // 要求epoll无限等待,或者等待时间超过配置的ngx_accept_mutex_delay
                // 也就是说nginx的epoll不会等待超过ngx_accept_mutex_delay的500毫秒
                // 如果epoll有事件发生,那么此等待时间无意义,epoll_wait立即返回
                if (timer == NGX_TIMER_INFINITE
                    || timer > ngx_accept_mutex_delay)
                {
                    // epoll的超时时间最大就是ngx_accept_mutex_delay
                    // ngx_accept_mutex_delay = ecf->accept_mutex_delay;
                    // 如果时间精度设置的太粗,那么就使用这个时间,500毫秒
                    timer = ngx_accept_mutex_delay;
                }
            }
        }
    }   //ngx_use_accept_mutex
ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
    // 尝试锁定共享内存锁
    // 非阻塞,会立即返回
    if (ngx_shmtx_trylock(&ngx_accept_mutex)) {

        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                       "accept mutex locked");

        // 锁成功

        // 之前已经持有了锁,那么就直接返回,继续监听端口
        // ngx_accept_events在epoll里不使用
        // rtsig在nginx 1.9.x已经删除
        if (ngx_accept_mutex_held && ngx_accept_events == 0) {
            return NGX_OK;
        }

        // 之前没有持有锁,需要注册epoll事件监听端口

        // 遍历监听端口列表,加入epoll连接事件,开始接受请求
        if (ngx_enable_accept_events(cycle) == NGX_ERROR) {

            // 如果监听失败就需要立即解锁,函数结束
            ngx_shmtx_unlock(&ngx_accept_mutex);
            return NGX_ERROR;
        }

        // 已经成功将监听事件加入epoll

        // 设置已经获得锁的标志
        ngx_accept_events = 0;
        ngx_accept_mutex_held = 1;

        return NGX_OK;
    }

    // try失败,未获得锁,极小的消耗

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "accept mutex lock failed: %ui", ngx_accept_mutex_held);

    // 未获得锁
    // 但之前持有锁,也就是说之前在监听端口
    if (ngx_accept_mutex_held) {
        // 遍历监听端口列表,删除epoll监听连接事件,不接受请求,避免惊群
        if (ngx_disable_accept_events(cycle, 0) == NGX_ERROR) {
            return NGX_ERROR;
        }

        // 设置未获得锁的标志
        ngx_accept_mutex_held = 0;
    }

    return NGX_OK;
}

为了避免长期持有锁导致其他进程抢不到,nginx 使用了标志位 NGX_POST_EVENTS,要求函数 ngx_process_events() 不立即处理事件,而是把事件放入延后队列。

// 有读事件,且读事件是可用的
        if ((revents & EPOLLIN) && rev->active) {

            if (revents & EPOLLRDHUP) {
                rev->pending_eof = 1;
            }

            rev->available = 1;

            // 读事件可用
            rev->ready = 1;

            // 检查此事件是否要延后处理
            // 如果使用负载均衡且抢到accept锁,那么flags里有NGX_POST_EVENTS标志
            // 1.9.x使用reuseport,那么就不延后处理
            if (flags & NGX_POST_EVENTS) {
                // 是否是接受请求的事件,两个延后处理队列
                queue = rev->accept ? &ngx_posted_accept_events
                                    : &ngx_posted_events;

                // 暂不处理,而是加入延后处理队列
                // 加快事件的处理速度,避免其他进程的等待
                ngx_post_event(rev, queue);

            } else {
                // 立即处理
                rev->handler(rev);
            }
        }
// 有写事件,且写事件是可用的
    if ((revents & EPOLLOUT) && wev->active) {

        // 写事件可用
        wev->ready = 1;

        // 检查此事件是否要延后处理
        if (flags & NGX_POST_EVENTS) {
            // 暂不处理,而是加入延后处理队列
            ngx_post_event(wev, &ngx_posted_events);

        } else {
            // 直接处理
            wev->handler(wev);
        }
     }

启用负载均衡后,所有事件会加入到 ngx_posted_accept_events 和 ngx_posted_events 队列里,前者专门存放 accept 事件,后者存放普通的读写事件。

nginx 会优先处理 accept 事件。

ngx_event_process_posted(cycle, &ngx_posted_accept_events);

    // 释放锁,其他进程可以获取,再监听端口
    if (ngx_accept_mutex_held) {
        // 释放负载均衡锁
        // 其他进程最多等待ngx_accept_mutex_delay毫秒后
        ngx_shmtx_unlock(&ngx_accept_mutex);
    }

    // 如果消耗了一点时间,那么看看是否定时器里有过期的
    if (delta) {
        // 遍历定时器红黑树,找出所有过期的事件,调用handler处理超时
        // 其中可能有的socket读写超时,那么就结束请求,断开连接
        ngx_event_expire_timers();
    }

    // 接下来处理延后队列里的事件,即调用事件的handler(ev),收发数据
    // in ngx_event_posted.c
    // 这里因为要处理大量的事件,而且是简单的顺序调用,所以可能会阻塞
    // nginx大部分的工作量都在这里
    // 注意与accept的函数是相同的,但队列不同,即里面的事件不同
    ngx_event_process_posted(cycle, &ngx_posted_events);

对于 linux epoll 来说,通常一次 accept 一个连接,所以 ngx_posted_accept_events 队列里最多只会有相当于监听端口数量的事件,非常少,可以较快处理完以便可以立即释放锁让其他 worker 进程有机会去抢锁。

流程图大致如下:


备注

1.测试环境centos7 64位,nginx版本为 1.14.0。
2..原文地址http://www.freecls.com/a/2712/e0


©著作权归作者所有
收藏
推荐阅读
简介
天降大任于斯人也,必先苦其心志。