vislee / leevis.com

Blog
87 stars 13 forks source link

nginx reuseport 实现 #126

Open vislee opened 7 years ago

vislee commented 7 years ago

概述

新版本的Linux支持了reuseport(注意区分reuseaddr),允许多个套接字bind 到相同的ip和port上了。内核为每个套接字分配一个链接队列。当有链接到达,内核根据tcp四元组hash到一个队列。 这样做的好处是:1,内核分配链接到一个链接队列,有效防止了惊群。2,内核根据四元组和bind的进程hash,其实做了一次负载均衡。3,为每个bind的套接字分配一个链接队列,无形中增大了backlog。

最近我们有个类似秒杀活动的页面,瞬间并发高达几十万。没开启reuseport的时候backlog配置6万。秒杀开始的时候80端口基本无响应,backlog满了,client的syn包被丢弃,此时服务器负载并不高。开启了reuseport每个链接队列等待accept的链接就高达2万。服务器负载已经非常高了。开启reuseport合理配置backlog是可以提高服务器性能的。

nginx的实现

配置

只要你linux服务器支持了reuseport这个特性,nginx配置非常简单。只需要在你要开启reuseport监听的命令后添加reuseport就可以开启了。 开启了以后你用ss看就发现每个进程都监听对应的端口。

代码分析

在ngx_http_block函数中,解析完毕http{} 的配置后,调用ngx_http_optimize_servers处理绑定监听的端口和ip地址,以及对应的虚拟主机。 ngx_http_optimize_servers调用了ngx_http_init_listening函数初始化监听的结构体。 ngx_http_init_listening又调用了ngx_clone_listening函数。该函数就是为开启了reuseport的监听复制监听结构体的。


ngx_int_t
ngx_clone_listening(ngx_conf_t *cf, ngx_listening_t *ls)
{
#if (NGX_HAVE_REUSEPORT)

    ngx_int_t         n;
    ngx_core_conf_t  *ccf;
    ngx_listening_t   ols;

    if (!ls->reuseport) {
        return NGX_OK;
    }

    ols = *ls;

    ccf = (ngx_core_conf_t *) ngx_get_conf(cf->cycle->conf_ctx,
                                           ngx_core_module);

    // 已经添加了一个监听结构体,所以n从1开始,为每个进程再复制一个
    for (n = 1; n < ccf->worker_processes; n++) {

        /* create a socket for each worker process */

        ls = ngx_array_push(&cf->cycle->listening);
        if (ls == NULL) {
            return NGX_ERROR;
        }

        *ls = ols;
        // 进程编号
        ls->worker = n;
    }

#endif

    return NGX_OK;
}

在函数ngx_init_cycle中调用了ngx_open_listening_sockets函数,bind并listen上述添加到cf->cycle->listening里面的ip和port。上述clone函数会为开启reuseport的端口每个进程复制了一份,所以对于开启了reuseport的端口就会根据进程数bind了响应的次数。

初始化好配置后,main函数调用ngx_master_process_cycle函数启动工作进程,启动工作进程请参考nginx的master和worker模式。 最后在每个工作进程中调用了ngx_worker_process_init函数,该函数会循环调用每个模块的init_process方法。

而ngx_event_core_module模块的ngx_event_process_init会添加套接字监听事件,而开启了reuseport的端口会有多个套接字,而每个进程只添加属于自己的监听套接字。

    ls = cycle->listening.elts;
    for (i = 0; i < cycle->listening.nelts; i++) {

#if (NGX_HAVE_REUSEPORT)
        if (ls[i].reuseport && ls[i].worker != ngx_worker) {
            // 对应端口开启了reuseport,每个进程跳过不属于自己处理的套接字
            continue;
        }
#endif

        c = ngx_get_connection(ls[i].fd, cycle->log);