nginx源码分析—reuseport的使用

本文主要介绍nginx中reuseport的使用,文中代码较多,阅读本文需要读者对nginx事件模块以及nginx配置过程有了解。

由于nginx比较复杂,且笔者对nginx的理解有限,文章难免存在疏忽之处,敬请指出,万分感谢!

一、reuseport的配置及解析

reuseport通过listen指令来配置,配置如下:

listen 443 reuseport;

listen指令由ngx_http_core_module模块的ngx_http_core_listen() 函数进行解析。
ngx_http_core_module.c的相关解析命令如下:

{ ngx_string("listen"),
      NGX_HTTP_SRV_CONF|NGX_CONF_1MORE, //listen指令只能配置在server块中,至少携带一个参数(443,reuseport等等)
      ngx_http_core_listen, //解析listen指令的函数
      NGX_HTTP_SRV_CONF_OFFSET, 
      0,
      NULL },

由于篇幅限制,下面仅介绍reuseport相关的解析,不完全介绍listen指令的解析。

// 解析listen指令携带的参数,从第二个参数开始(第一个参数为ip+port)
for (n = 2; n < cf->args->nelts; n++) {
	...
	if (ngx_strcmp(value[n].data, "bind") == 0) {
       lsopt.set = 1;
       lsopt.bind = 1; //强制对此ip+port进行bind操作,如不配置bind,可能会忽略此ip+port,后续会介绍忽略的情况
       continue;
    }
	
	//解析到reuseport配置,将reuseport标志置为1,同时将bind也置为1
	if (ngx_strcmp(value[n].data, "reuseport") == 0) {
        lsopt.reuseport = 1;
        lsopt.set = 1;
        lsopt.bind = 1;
        continue;
    }
	...
}

二、创建listening结构

master解析完http block之后会掉用ngx_http_optimize_servers()函数对配置的ip+port进行相应的处理,主要进行下面两个操作:

1、检查所有当前http block下的listen配置是否存在重复,即是否存在两个listen指令配置了相同的ip + port + server_name。如果存在重复的配置则会忽略靠后的ip+port+server_name的配置。检查由ngx_http_server_names()函数完成,此处并不详细介绍,有兴趣可自行查阅相应代码。

2、根据解析的配置创建listening结构,master后续监听的端口均由listening获得,每个listening结构对应一次bind + listening操作,此处重点介绍listening的创建。

首先ngx_http_optimize_servers()函数调用ngx_http_init_listening()函数,ngx_http_init_listening()代码如下:

static ngx_int_t
ngx_http_init_listening(ngx_conf_t *cf, ngx_http_conf_port_t *port)
{
    ngx_uint_t                 i, last, bind_wildcard;
    ngx_listening_t           *ls;
    ngx_http_port_t           *hport;
    ngx_http_conf_addr_t      *addr;

/*
以下是port的存储结构,一个port对应多个addrs(即port+ip),一个addr对应多个server(即server_name)
port - > addr1----|-----> server1
         addr2    |-----> server2
         addr3
    	  ...

*/

    addr = port->addrs.elts; //取出port的所有addr,一个port可能对应多个ip+port
    last = port->addrs.nelts; //port中addr的个数

	//每一个port的addr都已经排序了(插入排序),ip为通配符(INADDR_ANY)的addr在最后面,配置了bind的addr在前面
    if (addr[last - 1].opt.wildcard) {
        addr[last - 1].opt.bind = 1;
        bind_wildcard = 1;

    } else {
        bind_wildcard = 0;
    }

    i = 0;

    while (i < last) {
   
        //该port存在内容为INADDR_ANY+port的addr,若当前addr(ip+port)没有配置bind,则不需要显示地对当前ip+addr进行监听
        if (bind_wildcard && !addr[i].opt.bind) { 
            i++;
            continue;
        }

        ls = ngx_http_add_listening(cf, &addr[i]); //为当前ip+port创建一个listening结构,用于监听
        if (ls == NULL) {
            return NGX_ERROR;
        }

        hport = ngx_pcalloc(cf->pool, sizeof(ngx_http_port_t));
        if (hport == NULL) {
            return NGX_ERROR;
        }

       /*
       以下代码将上面提到的不需要监听的ip+port的相关配置存储到最后的addr(INADDR_ANY+port)中,便于收到连接请求时,可以找到正确的配置。
       但是存在一个问题,对于ip+port存在多个server的情况,在建立连接时ngx_http_init_connection()中只能取到ip+port的default_server。
       */
        ls->servers = hport;

        hport->naddrs = i + 1;

        switch (ls->sockaddr->sa_family) {

#if (NGX_HAVE_INET6)
        case AF_INET6:
            if (ngx_http_add_addrs6(cf, hport, addr) != NGX_OK) {
                return NGX_ERROR;
            }
            break;
#endif
        default: /* AF_INET */
            if (ngx_http_add_addrs(cf, hport, addr) != NGX_OK) {
                return NGX_ERROR;
            }
            break;
        }
        //对配置了reuseport的ip+port,将对应的listening结构拷贝worker_processes - 1份,便于每个worker保存独立的fd,避免使用互斥锁
        if (ngx_clone_listening(cf, ls) != NGX_OK) {
            return NGX_ERROR;
        }

        addr++;
        last--;
    }

    return NGX_OK;
}

ngx_clone_listening()代码如下:

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) { //只拷贝配置了reuseport的listening
        return NGX_OK;
    }

    ols = *ls;

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

    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; //对应的监听套接字只能由第n个worker进程处理,避免worker间相互干扰
    }

#endif

    return NGX_OK;
}

三、创建监听套接字

master解析完配置之后并创建listening之后,开始创建监听套接字。对每一个listening结构会创建对应的监听套接字,再设置reuseport属性,最后进行bind和listen操作。具体代码如下:

首先调用ngx_open_listening_sockets()创建套接字,并进行bind和listen操作

ngx_int_t
ngx_open_listening_sockets(ngx_cycle_t *cycle)
{
    int               reuseaddr;
    ngx_uint_t        i, tries, failed;
    ngx_err_t         err;
    ngx_log_t        *log;
    ngx_socket_t      s;
    ngx_listening_t  *ls;

    reuseaddr = 1;
#if (NGX_SUPPRESS_WARN)
    failed = 0;
#endif

    log = cycle->log;

    /* TODO: configurable try number */

    for (tries = 5; tries; tries--) { //重试5次
        failed = 0;

        /* for each listening socket */

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

            if (ls[i].ignore) {
                continue;
            }

#if (NGX_HAVE_REUSEPORT)

            if (ls[i].add_reuseport) {  //增加reuseport属性,仅用于reload和平滑升级场景

                /*
                 * to allow transition from a socket without SO_REUSEPORT
                 * to multiple sockets with SO_REUSEPORT, we have to set
                 * SO_REUSEPORT on the old socket before opening new ones
                 */

                int  reuseport = 1;

                if (setsockopt(ls[i].fd, SOL_SOCKET, SO_REUSEPORT,
                               (const void *) &reuseport, sizeof(int))
                    == -1)
                {
                    ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_socket_errno,
                                  "setsockopt(SO_REUSEPORT) %V failed, ignored",
                                  &ls[i].addr_text);
                }

                ls[i].add_reuseport = 0;
            }
#endif

            if (ls[i].fd != (ngx_socket_t) -1) {
                continue;
            }

            if (ls[i].inherited) {

                /* TODO: close on exit */
                /* TODO: nonblocking */
                /* TODO: deferred accept */

                continue;
            }

            s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0); //创建套接字
           
           ...
           
#if (NGX_HAVE_REUSEPORT)

            if (ls[i].reuseport && !ngx_test_config) {  //设置reuseport
                int  reuseport;
                reuseport = 1;
                setsockopt(s, SOL_SOCKET, SO_REUSEPORT, (const void *) &reuseport, sizeof(int);
                
            }
#endif
            bind(s, ls[i].sockaddr, ls[i].socklen); //band操作
              
            if (ls[i].type != SOCK_STREAM) {
                ls[i].fd = s;
                continue;
            }

            listen(s, ls[i].backlog); //listen操作
            ls[i].listen = 1;
            ls[i].fd = s;
        }

        if (!failed) {
            break;
        }

        /* TODO: delay configurable */

        ngx_log_error(NGX_LOG_NOTICE, log, 0,
                      "try again to bind() after 500ms");

        ngx_msleep(500);
    }

    if (failed) {
        ngx_log_error(NGX_LOG_EMERG, log, 0, "still could not bind()");
        return NGX_ERROR;
    }

    return NGX_OK;
}

至此,对于每一个配置reuseport的ip+addr,都为其创建了worker_processes个监听套接字,接下来介绍worker进程如何使用这些套接字。

四、worker对监听套接字的使用

对于每一个没有配置reuseport的监听套接字,打开文件描述符表中,通过继承的方式每个worker都维护一个fd,但是内核层面仅仅维护一个表项(打开文件描述符表由所有进程共享)。正是由于这个原因导致惊群问题,早期nginx通过互斥锁(共享内存+原子操作)来避免惊群。worker调用epoll_wait之前请求锁,只有成功获得锁,才会通过epoll监听fd。由于worker进程间需要竞争锁,性能不高。

如果配置了reuseport的监听套接字,每个worker进程拥有一个独立的fd,worker进程间互不干扰,在内核层面实现负载均衡,效率更高。worker进程启动之后,会调用ngx_event_process_init()对事件模块进行初始化,代码如下:

static ngx_int_t
ngx_event_process_init(ngx_cycle_t *cycle)
{
    ngx_uint_t           m, i;
    ngx_event_t         *rev, *wev;
    ngx_listening_t     *ls;
    ngx_connection_t    *c, *next, *old;
    ngx_core_conf_t     *ccf;
    ngx_event_conf_t    *ecf;
    ngx_event_module_t  *module;

    ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
    ecf = ngx_event_get_conf(cycle->conf_ctx, ngx_event_core_module);

    if (ccf->master && ccf->worker_processes > 1 && ecf->accept_mutex) { //worker进程判断是否使用互斥锁
        ngx_use_accept_mutex = 1;
        ngx_accept_mutex_held = 0;
        ngx_accept_mutex_delay = ecf->accept_mutex_delay;

    } else {
        ngx_use_accept_mutex = 0;
    }
    
    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) { //worker仅处理属于自己的监听套接字
            continue;
        }
#endif

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

        if (c == NULL) {
            return NGX_ERROR;
        }

        c->type = ls[i].type;
        c->log = &ls[i].log;

        c->listening = &ls[i];
        ls[i].connection = c;

        rev = c->read;

        rev->log = c->log;
        rev->accept = 1;

        rev->handler = (c->type == SOCK_STREAM) ? ngx_event_accept
                                                : ngx_event_recvmsg;

#if (NGX_HAVE_REUSEPORT)

        if (ls[i].reuseport) {
            if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {  //将监听套接字加入epoll,LT模式
                return NGX_ERROR;
            }

            continue;
        }

#endif

		//若使用互斥锁,worker进程仅在获得互斥锁时才将监听套接字加入epoll
        if (ngx_use_accept_mutex) { 
            continue;
        }

		//若不使用互斥锁,worker进程直接将监听套接字加入epoll
        if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {
            return NGX_ERROR;
        }
    }

    return NGX_OK;
}

若worker进程若使用互斥锁,在成功获得互斥锁时,将监听套接字加入epoll;获得锁失败时,将监听套接字从epoll中移除。具体代码如下:

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");

        if (ngx_accept_mutex_held && ngx_accept_events == 0) {
            return NGX_OK;
        }

        if (ngx_enable_accept_events(cycle) == NGX_ERROR) { //成功获得锁时,将监听套接字加入epoll
            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);

    if (ngx_accept_mutex_held) {
    	//若没有获得锁,将监听套接字从epoll中移除(配置reuseport的套接字除外)
        if (ngx_disable_accept_events(cycle, 0) == NGX_ERROR) {
            return NGX_ERROR;
        }

        ngx_accept_mutex_held = 0;
    }

    return NGX_OK;
}


static ngx_int_t
ngx_enable_accept_events(ngx_cycle_t *cycle)
{
    ngx_uint_t         i;
    ngx_listening_t   *ls;
    ngx_connection_t  *c;

    ls = cycle->listening.elts;
    for (i = 0; i < cycle->listening.nelts; i++) { //将所有监听套接字加入epoll, LT模式

        c = ls[i].connection;

        if (c == NULL || c->read->active) {
            continue;
        }

        if (ngx_add_event(c->read, NGX_READ_EVENT, 0) == NGX_ERROR) {
            return NGX_ERROR;
        }
    }

    return NGX_OK;
}


static ngx_int_t
ngx_disable_accept_events(ngx_cycle_t *cycle, ngx_uint_t all)
{
    ngx_uint_t         i;
    ngx_listening_t   *ls;
    ngx_connection_t  *c;

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

        c = ls[i].connection;

        if (c == NULL || !c->read->active) {
            continue;
        }

#if (NGX_HAVE_REUSEPORT)

        /*
         * do not disable accept on worker's own sockets
         * when disabling accept events due to accept mutex
         */

        if (ls[i].reuseport && !all) { //配置reuseport的监听套接字仍旧通过epoll监听
            continue;
        }

#endif

        if (ngx_del_event(c->read, NGX_READ_EVENT, NGX_DISABLE_EVENT)
            == NGX_ERROR)
        {
            return NGX_ERROR;
        }
    }

    return NGX_OK;
}

五、reload过程中reuseport的使用

master进行reload的时候会调用ngx_init_cycle,这里仅给出ngx_init_cycle部分代码:

ngx_cycle_t *
ngx_init_cycle(ngx_cycle_t *old_cycle)
{
	...
	if (ngx_conf_parse(&conf, &cycle->conf_file) != NGX_CONF_OK) { //master重新解析配置,生成新的listening数组
        environ = senv;											   //其中关于ip+port的配置保存在listening中
        ngx_destroy_cycle_pools(&conf);
        return NULL;
    }	
    ...
    if (old_cycle->listening.nelts) { //如果老的listening非空,则将其中的监听套接字直接转移到新的listening里面
        ls = old_cycle->listening.elts;
        for (i = 0; i < old_cycle->listening.nelts; i++) {
            ls[i].remain = 0;
        }

        nls = cycle->listening.elts;
        for (n = 0; n < cycle->listening.nelts; n++) { //遍历新的listening中的每一项

            for (i = 0; i < old_cycle->listening.nelts; i++) {//遍历老的listening的每一项,找到ip+port相同的项,转移监听套接字到新的listening
                if (ls[i].ignore) {
                    continue;
                }

                if (ls[i].remain) {
                    continue;
                }

                if (ls[i].type != nls[n].type) {
                    continue;
                }

                if (ngx_cmp_sockaddr(nls[n].sockaddr, nls[n].socklen,
                                     ls[i].sockaddr, ls[i].socklen, 1)
                    == NGX_OK)
                {
                    /*
                    将监听套接字转移给新的listening,由于old worker没有退出,在某个时间段内,
                    新的的worker进程和老的worker进程指向相同的打开文件描述符表项,造成新老进程之间的惊群问题。
					*/
                    nls[n].fd = ls[i].fd; 
                    nls[n].previous = &ls[i];
                    ls[i].remain = 1;

                    if (ls[i].backlog != nls[n].backlog) { //由于listen操作的第二个参数变化,需要重新listen该监听套接字
                        nls[n].listen = 1;
                    }


#if (NGX_HAVE_REUSEPORT)
                    if (nls[n].reuseport && !ls[i].reuseport) { //增加reuseport属性
                        nls[n].add_reuseport = 1;
                    }
#endif
                    break;
                }
            }

            if (nls[n].fd == (ngx_socket_t) -1) {
                nls[n].open = 1;
            }
        }
    } else { //老的listening为空,则解析出来的所有ip+port需要创建新的监听套接字
        ls = cycle->listening.elts;
        for (i = 0; i < cycle->listening.nelts; i++) {
            ls[i].open = 1;
        }
    }
    
    if (ngx_open_listening_sockets(cycle) != NGX_OK) { //创建监听套接字,并对其进行bind+listen操作
        goto failed;
    }
}

只要某一次配置了reuseport,后续的reload操作即使没有配置reuseport,对应的ip+port仍然具有reuseport属性,但是解析配置是一个ip+port仅对应一个listening结构,实际上还是所有worker共用一个监听套接字,需要通过mutex来避免惊群。

六、平滑升级过程中reuseport的使用

平滑升级用于替换nginx可执行文件,老的master收到信号进行平滑升级时,首先fork新进程,新进程然后调用exec替换进程上下文,得到新的master进程,老的master的所有监听套接字通过环境变量传给新的master。

新的master进程起来之后,进入main函数,main会调用ngx_add_inherited_sockets来获取父进程设置的"NGINX"环境变量。该环境变量保存的是父进程的监听套接字,套接字之间通过冒号分割,最后加上一个分号。

static ngx_int_t
ngx_add_inherited_sockets(ngx_cycle_t *cycle)
{
    u_char           *p, *v, *inherited;
    ngx_int_t         s;
    ngx_listening_t  *ls;

    inherited = (u_char *) getenv(NGINX_VAR);

    if (inherited == NULL) {
        return NGX_OK;
    }

    ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0,
                  "using inherited sockets from \"%s\"", inherited);

    if (ngx_array_init(&cycle->listening, cycle->pool, 10,
                       sizeof(ngx_listening_t))
        != NGX_OK)
    {
        return NGX_ERROR;
    }

    for (p = inherited, v = p; *p; p++) {
        if (*p == ':' || *p == ';') {
            s = ngx_atoi(v, p - v); //取出监听套接字的fd
            if (s == NGX_ERROR) {
                ngx_log_error(NGX_LOG_EMERG, cycle->log, 0,
                              "invalid socket number \"%s\" in " NGINX_VAR
                              " environment variable, ignoring the rest"
                              " of the variable", v);
                break;
            }

            v = p + 1;

            ls = ngx_array_push(&cycle->listening);  //创建新的listening结构
            if (ls == NULL) {
                return NGX_ERROR;
            }

            ngx_memzero(ls, sizeof(ngx_listening_t));

            ls->fd = (ngx_socket_t) s; //将获取的fd保存至listening结构
        }
    }

    if (v != p) {
        ngx_log_error(NGX_LOG_EMERG, cycle->log, 0,
                      "invalid socket number \"%s\" in " NGINX_VAR
                      " environment variable, ignoring", v);
    }

    ngx_inherited = 1;

    return ngx_set_inherited_sockets(cycle); //获取监听套接字的各种属性,并将这些属性保存至对应的listening
}

ngx_int_t
ngx_set_inherited_sockets(ngx_cycle_t *cycle)
{
    size_t                     len;
    ngx_uint_t                 i;
    ngx_listening_t           *ls;
    socklen_t                  olen;

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

	/*此处会获取各种属性,篇幅问题,此处仅给出reuseport相关代码*/

#if (NGX_HAVE_REUSEPORT)

        reuseport = 0;
        olen = sizeof(int);

        if (getsockopt(ls[i].fd, SOL_SOCKET, SO_REUSEPORT,
                       (void *) &reuseport, &olen) //获取reuseport属性
            == -1)
        {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_socket_errno,
                          "getsockopt(SO_REUSEPORT) %V failed, ignored",
                          &ls[i].addr_text);

        } else {
            ls[i].reuseport = reuseport ? 1 : 0; //获取的reuseport属性保存至listening
        }

#endif

    }

    return NGX_OK;
}

从父进程中继承得到的listening作为old_listening传至ngx_init_cycle,ngx_init_cycle函数中会重新解析配置生成新的listening数组,新的listening数组从old_listening数组中获取父进程中的监听套接字,这部分代码上面已经提到,此处省略。

再次声明:由于nginx本身较复杂,笔者可能理解有误,敬请指出,万分感谢!

你可能感兴趣的:(Nginx,nginx源码分析)