深入理解ngx_http_upstream_vnswrr_module负载均衡模块

目录

  • 1. 引言
  • 2. 启用vnswrr负载均衡模块
  • 3. 源码剖析
    • 3.1 配置指令分析
    • 3.2 负载均衡算法配置初始化
    • 3.3 负载均衡请求上下文的初始化
    • 3.4 获取peer

1. 引言

   之前有讨论了nginx的swrr算法的两个问题,并引出了阿里tengine的vnswrr算法如何来克服swrr的问题。本文通过源码层面对ngx_http_upstream_vnswrr_module模块进行分析,来深入理解vnswrr负载均衡算法。关于swrr算法的思考可以查看《nginx upstream server主动健康检测模块添加https检测功能》。关于vnswrr的算法原理可以参考《阿里七层流量入口负载均衡算法演变之路》。

2. 启用vnswrr负载均衡模块

  配置指令的格式为:

指令:    vnswrr  [max_init=init_vode_num]
默认值:  -
上下文:   upstream

  其中init_vnode_num是初始化虚拟节点的数量,具体可以参考《阿里七层流量入口负载均衡算法演变之路》中**接入层 VNSWRR 算法(V2)**部分的描述。

  以5台rs服务器为例开启vnswrr,距离如下:

upstream {
    vnswrr 5;
    server 192.168.0.1 weight=1;
    server 192.168.0.2 weight=1;
    server 192.168.0.3 weight=3;
    server 192.168.0.4 weight=3;
    server 192.168.0.5 weight=5;
    server 192.168.0.6 weight=5;
}

3. 源码剖析

3.1 配置指令分析

   本模块定义了配置指令vnswrr,代码如下:

static ngx_command_t  ngx_http_upstream_vnswrr_commands[] = {

    { ngx_string("vnswrr"),
      NGX_HTTP_UPS_CONF|NGX_CONF_NOARGS|NGX_CONF_TAKE1,
      ngx_http_upstream_vnswrr,
      0,
      0,
      NULL },

      ngx_null_command
};

  以上定义了指令分析回调函数ngx_http_upstream_vnswrr, 其源码如下:

static char *
ngx_http_upstream_vnswrr(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_upstream_srv_conf_t            *uscf;
    ngx_http_upstream_vnswrr_srv_conf_t     *uvnscf;
    ngx_str_t                               *value;
    ngx_int_t                                max_init;

    uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);

    if (uscf->peer.init_upstream) {
        ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
                           "load balancing method redefined");
    }

	/* 将vnswrr的负载均衡算法配置初始化回调函数挂进去 */
    uscf->peer.init_upstream = ngx_http_upstream_init_vnswrr;

	/* 不象哈希负载均衡算法,本算法可以支持主备服务器 */
    uscf->flags = NGX_HTTP_UPSTREAM_CREATE
                  |NGX_HTTP_UPSTREAM_WEIGHT
                  |NGX_HTTP_UPSTREAM_BACKUP
                  |NGX_HTTP_UPSTREAM_MAX_FAILS
                  |NGX_HTTP_UPSTREAM_FAIL_TIMEOUT
#if defined(nginx_version) && nginx_version >= 1011005
                  |NGX_HTTP_UPSTREAM_MAX_CONNS
#endif
                  |NGX_HTTP_UPSTREAM_DOWN;

	/* 获取vnswrr的配置上下文 */
    uvnscf = ngx_http_conf_upstream_srv_conf(uscf,
                                ngx_http_upstream_vnswrr_module);

    value = cf->args->elts;

    max_init = 0;
	
	/* 如果有max_init参数,就从配置指令中解析初始虚拟节点数量 */
    if (cf->args->nelts > 1) {

        if (ngx_strncmp(value[1].data, "max_init=", 9) == 0) {

            max_init = ngx_atoi(&value[1].data[9], value[1].len - 9);

            if (max_init == NGX_ERROR) {

                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                                   "invalid parameter \"%V\"", &value[1]);

                return NGX_CONF_ERROR;
            }
        }
    }

    uvnscf->max_init = max_init;

    return NGX_CONF_OK;
}

3.2 负载均衡算法配置初始化

  nginx在解析完配置文件后,会为每个upstream调用前面设置好的init_upstream回调函数来初始化设置好的负载均衡算法,对于开启了vnswrr算法,则会回调ngx_http_upstream_init_vnswrr函数,该回调由3.1节中ngx_http_upstream_vnswrr函数设置。下面来分析一下ngx_http_upstream_init_vnswrr函数:

static ngx_int_t
ngx_http_upstream_init_vnswrr(ngx_conf_t *cf,
    ngx_http_upstream_srv_conf_t *us)
{
    ngx_http_upstream_rr_peers_t           *peers, *backup;
    ngx_http_upstream_vnswrr_srv_conf_t    *uvnscf, *ubvnscf;
    ngx_http_upstream_server_t             *server;
    ngx_uint_t                              i, g, bg, max_init;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, cf->log, 0, "init vnswrr");

	/* 借用round-robin的ngx_http_upstream_init_round_robin初始化peer链表 */
    if (ngx_http_upstream_init_round_robin(cf, us) != NGX_OK) {
        return NGX_ERROR;
    }

	/* 对于配置的每个server(包括主和备),计算配置的所有server权重的最大公约数 */
    g = 0;
    bg = 0;
    if (us->servers) {
        server = us->servers->elts;

        for (i = 0; i < us->servers->nelts; i++) {
            if (server[i].backup) {
                bg = ngx_http_upstream_gcd(bg, server[i].weight);
            } else {
                g = ngx_http_upstream_gcd(g , server[i].weight);
            }
        }
    }
    if (g == 0) {
        g = 1;
    }
    if (bg == 0) {
        bg = 1;
    }

    uvnscf = ngx_http_conf_upstream_srv_conf(us,
                                ngx_http_upstream_vnswrr_module);
    if (uvnscf == NULL) {
        return NGX_ERROR;
    }

    peers = (ngx_http_upstream_rr_peers_t *) us->peer.data;

    max_init = uvnscf->max_init;

	/* init_number为初始虚拟节点的序号
	   last_number为最后一次分配的虚拟节点的序号
	   last_peer为最后一次分配的peer的指针
	 */
    uvnscf->init_number = NGX_CONF_UNSET_UINT;
    uvnscf->last_number = NGX_CONF_UNSET_UINT;
    uvnscf->last_peer = NULL;
    uvnscf->next = NULL;
    uvnscf->gcd = g;

	/* 如果没有配置max_init,则设置为peer的数量
	   max_init最大为总的权重
	 */
    if (!max_init) {
        uvnscf->max_init = peers->number;

    } else if (max_init > peers->total_weight) {
        uvnscf->max_init = peers->total_weight;
    }

	/* 设置负载均衡请求上下文初始化回调函数 */
    us->peer.init = ngx_http_upstream_init_vnswrr_peer;

	/* 如果upstream是配置成带权重模式的,即所有服务器的weight不都等于1,则走正常vnswrr
	   算法,否则,退化为简单的round-robin算法。对于vnswrr,需要分配虚拟节点并进行初始化,
	   虚拟节点的数量是总权重除以上面算出的最大公约数。稍微思考一下,就知道这个是合理的,
	   譬如三台server,他们的权重都分别是2,4,6,那么其效果和1,2,3是一样的,
	   所以找到最大公约数,并把这个最大公约数除掉以后得到有效权重。
	*/
	
    if (peers->weighted) {
        uvnscf->vpeers = ngx_pcalloc(cf->pool,
                                    sizeof(ngx_http_upstream_rr_vpeers_t)
                                    * peers->total_weight / uvnscf->gcd);
        if (uvnscf->vpeers == NULL) {
            return NGX_ERROR;
        }
		/* 初始化一批虚拟节点,最多是max_init个虚拟节点,避免一次性初始化大量的虚拟节点
		   当值nginx的cpu突发overload
		*/
        ngx_http_upstream_init_virtual_peers(peers, uvnscf, 0, uvnscf->max_init);

    }

    /* 下面是backup服务器部分的初始化逻辑,和主服务器是一样的 */
    backup = peers->next;
    if (backup) {
        ubvnscf = ngx_pcalloc(cf->pool,
                              sizeof(ngx_http_upstream_vnswrr_srv_conf_t));
        if (ubvnscf == NULL) {
            return NGX_ERROR;
        }

        ubvnscf->init_number = NGX_CONF_UNSET_UINT;
        ubvnscf->last_number = NGX_CONF_UNSET_UINT;
        ubvnscf->last_peer = NULL;
        ubvnscf->gcd = bg;
        
        ubvnscf->max_init = max_init;

        if (!max_init) {
            ubvnscf->max_init = backup->number;

        } else if (max_init > backup->total_weight) {
            ubvnscf->max_init = backup->total_weight;
        }

		/* 把主服务器和backup服务器链起来 */
        uvnscf->next = ubvnscf;

        if (!backup->weighted) {
            return NGX_OK;
        }

        ubvnscf->vpeers = ngx_pcalloc(cf->pool,
                                      sizeof(ngx_http_upstream_rr_vpeers_t)
                                      * backup->total_weight / ubvnscf->gcd);
        if (ubvnscf->vpeers == NULL) {
            return NGX_ERROR;
        }

        ngx_http_upstream_init_virtual_peers(backup, ubvnscf, 0, 
                                             ubvnscf->max_init);
    }

    return NGX_OK;
}

  ngx_http_upstream_init_vnswrr函数的逻辑就是分别对主服务器和备服务器组进行加载操作,初始化一部分虚拟节点,详细的逻辑在源码中已经进行了注释,不再赘述。

3.3 负载均衡请求上下文的初始化

  当nginx接收到http请求需要连接上游服务器的时候,就会发起负载均衡请求上下文的初始化回调,对于vnswrr算法就是回调ngx_http_upstream_init_vnswrr_peer函数了。

static ngx_int_t
ngx_http_upstream_init_vnswrr_peer(ngx_http_request_t *r,
    ngx_http_upstream_srv_conf_t *us)
{
    ngx_http_upstream_vnswrr_srv_conf_t    *uvnscf;
    ngx_http_upstream_vnswrr_peer_data_t   *vnsp;

    uvnscf = ngx_http_conf_upstream_srv_conf(us,
                                          ngx_http_upstream_vnswrr_module);

	/* 创建请求上下文并进行初始化设置 */
    vnsp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_vnswrr_peer_data_t));
    if (vnsp == NULL) {
        return NGX_ERROR;
    }

    vnsp->uvnscf = uvnscf;
    r->upstream->peer.data = &vnsp->rrp;

	/* 因为本模块是依赖于round-robin模块的,譬如上游服务器的已分配状态等,
	   这里也需要调用ngx_http_upstream_init_round_robin_peer进行初始化 */
    if (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK) {
        return NGX_ERROR;
    }

	/* 设置获取peer的回调 */
    r->upstream->peer.get = ngx_http_upstream_get_vnswrr_peer;

    return NGX_OK;
}

  这里最关键的就是设置了获取peer的回调函数ngx_http_upstream_get_vnswrr_peer。

3.4 获取peer

  一切准备就绪后,nginx会在请求上游连接的时候调用ngx_event_connect_peer,而在ngx_event_connect_peer函数中将回调ngx_http_upstream_get_vnswrr_peer函数来获取目的服务器的地址信息。接下来来详细分析这个函数,源码如下:

static ngx_int_t
ngx_http_upstream_get_vnswrr_peer(ngx_peer_connection_t *pc, void *data)
{
    ngx_http_upstream_vnswrr_peer_data_t  *vnsp = data;

    ngx_int_t                              rc;
    ngx_uint_t                             i, n;
    ngx_http_upstream_rr_peer_t           *peer;
    ngx_http_upstream_rr_peers_t          *peers;
    ngx_http_upstream_rr_peer_data_t      *rrp;

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, pc->log, 0,
                   "get vnswrr peer, try: %ui", pc->tries);

    pc->cached = 0;
    pc->connection = NULL;

    rrp = &vnsp->rrp;

    peers = rrp->peers;
    ngx_http_upstream_rr_peers_wlock(peers);   /* 共享内存加写锁 */

    if (peers->single) {
	    /*对于只有一个peer的情况,如果这个peer没有down且连接数没有超过限制,
	      则直接分配这个peer*/
        peer = peers->peer;

        if (peer->down) {
            goto failed;
        }

#if defined(nginx_version) && nginx_version >= 1011005
        if (peer->max_conns && peer->conns >= peer->max_conns) {
            goto failed;
        }
#endif

#if (NGX_HTTP_UPSTREAM_CHECK)
        if (ngx_http_upstream_check_peer_down(peer->check_index)) {
            goto failed;
        }
#endif
        rrp->current = peer;

    } else {

        /* 如果有多个peer,则调用ngx_http_upstream_get_vnswrr获取peer信息 */

        peer = ngx_http_upstream_get_vnswrr(vnsp);

        if (peer == NULL) {
            goto failed;
        }

        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, pc->log, 0,
                       "get vnswrr peer, current: %p %i",
                       peer, peer->current_weight);
    }

	/* 将分配到的peer的地址写入到ngx_peer_connection_t(pc_中 */
    pc->sockaddr = peer->sockaddr;
    pc->socklen = peer->socklen;
    pc->name = &peer->name;
#if (T_NGX_HTTP_DYNAMIC_RESOLVE)
    pc->host = &peer->host;
#endif    

    peer->conns++;

	/* 释放上面加的写锁 */
    ngx_http_upstream_rr_peers_unlock(peers);

    return NGX_OK;

failed:

	/* 主服务器分配失败了,如果有备服务器,那么从备服务器进行分配 */
    if (peers->next) {

        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, pc->log, 0, "backup servers");

		/* 切换到备服务器组 */
        rrp->peers = peers->next;

        vnsp->uvnscf = vnsp->uvnscf ? vnsp->uvnscf->next : vnsp->uvnscf;

        n = (rrp->peers->number + (8 * sizeof(uintptr_t) - 1))
                / (8 * sizeof(uintptr_t));

        for (i = 0; i < n; i++) {
            rrp->tried[i] = 0;
        }
        
        /* 释放上面加的写锁 */
        ngx_http_upstream_rr_peers_unlock(peers);

		/* 递归调用本函数自己,重新进行一次获取peer的操作 */
        rc = ngx_http_upstream_get_vnswrr_peer(pc, vnsp);

	    /* 备服务器也分配失败,则返回NGX_BUSY */
        if (rc != NGX_BUSY) {
            return rc;
        }
        
		/* 重新加上写锁,在返回前释放 */
        ngx_http_upstream_rr_peers_wlock(peers);
    }
    
    /* 释放上面加的写锁 */
    ngx_http_upstream_rr_peers_unlock(peers);

    pc->name = peers->name;

    return NGX_BUSY;
}

  本函数针对如果只有一个peer的情况来说,就不需要再进行vnswrr算法了,反过来则进行vnswrr的分配操作,vnswrr算法调用了ngx_http_upstream_get_vnswrr函数进行实际的分配工作。下面就是vnswrr的最核心的代码了,源码如下:

static ngx_http_upstream_rr_peer_t *
ngx_http_upstream_get_vnswrr(ngx_http_upstream_vnswrr_peer_data_t  *vnsp)
{
    time_t                                  now;
    uintptr_t                               m;
    ngx_uint_t                              i, n, p, flag, begin_number;
    ngx_http_upstream_rr_peer_t            *peer, *best;
    ngx_http_upstream_rr_peers_t           *peers;
    ngx_http_upstream_rr_vpeers_t          *vpeers;
    ngx_http_upstream_rr_peer_data_t       *rrp;
    ngx_http_upstream_vnswrr_srv_conf_t    *uvnscf;

    now = ngx_time();

    best = NULL;

#if (NGX_SUPPRESS_WARN)
    p = 0;
#endif

    rrp = &vnsp->rrp;
    peers = rrp->peers;
    uvnscf = vnsp->uvnscf;
    vpeers = uvnscf->vpeers;

	/* last_number == NGX_CONF_UNSET_UINT
	   表示本worker进程第一次进入到ngx_http_upstream_get_vnswrr函数,
	   这里通过将init_number设置为一个随机值来避免多进程产生的“共振”效应。
	   初始化随机值这个机制在《阿里七层流量入口负载均衡算法演变之路》中有提到
	 */
    if (uvnscf->last_number == NGX_CONF_UNSET_UINT) {
        uvnscf->init_number = ngx_random() % peers->number;
		
		/* 如果是带权重模式,则使用了虚拟节点来进行负载均衡,
		   所以从虚拟节点中选取peer
		 */
        if (peers->weighted) {
            peer = vpeers[uvnscf->init_number].vpeer;

        } else {
            /* 如果是不带权重的模式,则没有虚拟节点,
               需要直接在peers列表中循环init_number次数,选择第nit_number个peer
            */
            for (peer = peers->peer, i = 0; i < uvnscf->init_number; i++) {
                peer = peer->next;
            }
        }

        uvnscf->last_number = uvnscf->init_number;
        uvnscf->last_peer = peer;
    }

    if (peers->weighted) {
        /* 如果当前初始化好的虚拟节点已经都被分配过一次了,并且还有没初始化过的虚拟节点,
           则再次分配虚拟节点,最多max_init个。 
        */
        if (uvnscf->vnumber != peers->total_weight / uvnscf->gcd
            && (uvnscf->last_number + 1 == uvnscf->vnumber))
        {
            n = peers->total_weight / uvnscf->gcd - uvnscf->vnumber;
            if (n > uvnscf->max_init) {
                n = uvnscf->max_init;
            }

            ngx_http_upstream_init_virtual_peers(peers, uvnscf, uvnscf->vnumber,
                                     n + uvnscf->vnumber);

        }

        /* 在虚拟节点循环队列中分配下一个vpeer
           begin_numer为当前分配的虚拟节点在虚拟节点循环队列中的序号
         */ 
        begin_number = (uvnscf->last_number + 1) % uvnscf->vnumber;
        peer = vpeers[begin_number].vpeer;

    } else {
		/* 如果是不带权重模式,那么直接通过peer链进行peer的分配
		   一个peer中有多个地址的,那么先分配这个peer的地址,
		   否则,找下一个peer,begin_number为当前分配的peer在peer列表中的序号
		 */
        if (uvnscf->last_peer && uvnscf->last_peer->next) {
            begin_number = (uvnscf->last_number + 1) % peers->number;
            peer = uvnscf->last_peer->next;

        } else {
            begin_number = 0;
            peer = peers->peer;
        }
    }

	/* 以下对上面分配的peer进行状态过滤,如果分配的peer不能用,
	   需要再往下循环获取下一个peer */
	/* 这里 i != begin_number || flag的判断用来检测是否已经循环了一圈回来了
	   循环了一圈回来的,那么所有的peer就已经遍历了,还是不能满足分配的需要。
	 */
    for (i = begin_number, flag = 1; i != begin_number || flag;
         i = peers->weighted
         ? ((i + 1) % uvnscf->vnumber) : ((i + 1) % peers->number),
         peer = peers->weighted
         ? vpeers[i].vpeer : (peer->next ? peer->next : peers->peer))
    {

        flag = 0;
        if (peers->weighted) {
			/* 这里也有可能分配的虚拟节点已经被遍历过一次了,并且还有没初始化过的虚拟节点,
           则再次分配虚拟节点,最多max_init个 */
            n = peers->total_weight / uvnscf->gcd - uvnscf->vnumber;
            if (n > uvnscf->max_init) {
                n = uvnscf->max_init;
            }

            if (n > 0) {
                ngx_http_upstream_init_virtual_peers(peers, uvnscf, uvnscf->vnumber,
                                        n + uvnscf->vnumber);
            }

            n = vpeers[i].rindex / (8 * sizeof(uintptr_t));
            m = (uintptr_t) 1 << vpeers[i].rindex % (8 * sizeof(uintptr_t));

        } else {
            n =  i / (8 * sizeof(uintptr_t));
            m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t));
        }

		/* 节点是否已经分配过的状态判断 */
        if (rrp->tried[n] & m) {
            continue;
        }

		/* 节点是否已经被设置为down状态判断 */
        if (peer->down) {
            continue;
        }

		/* 节点是否故障保护状态判断 */
        if (peer->max_fails
            && peer->fails >= peer->max_fails
            && now - peer->checked <= peer->fail_timeout)
        {
            continue;
        }

		/* 节点的当前在线连接是否超过限制判断 */
#if defined(nginx_version) && nginx_version >= 1011005
        if (peer->max_conns && peer->conns >= peer->max_conns) {
            continue;
        }
#endif

#if (NGX_HTTP_UPSTREAM_CHECK)
        if (ngx_http_upstream_check_peer_down(peer->check_index)) {
            continue;
        }
#endif

		/*  得到了分配好的节点 */
		best = peer;
        uvnscf->last_peer = peer;
        uvnscf->last_number = i;
        p = i;
        break;
    }

    if (best == NULL) {
        return NULL;
    }

    rrp->current = best;

	/* 在tried位表中设置当前节点已经被分配过 */
    if (peers->weighted) {
        n = vpeers[p].rindex / (8 * sizeof(uintptr_t));
        m = (uintptr_t) 1 << vpeers[p].rindex % (8 * sizeof(uintptr_t));

    } else {
        n = p / (8 * sizeof(uintptr_t));
        m = (uintptr_t) 1 << p % (8 * sizeof(uintptr_t));
    }

    rrp->tried[n] |= m;

    if (now - best->checked > best->fail_timeout) {
        best->checked = now;
    }

    return best;
}

  以上函数中,如果是不带权重的模式,那么就是最简单的round-robin分配机制,每次分配就循环往后前进一个peer,一个特别的地方就是第一次分配的时候设置了一个随机值位置,从这个随机位置开始进行正式分配,避免产生“共振”;如果是带权重的模式,那么才是真正的vnswrr算法,这个算法另外创建了虚拟节点,虚拟节点的总数量是总权重/各服务器权重的最大公约数,为了避免一次性集中分配虚拟节点导致CPU压力突发,所以每次最多分配max_init个数的虚拟节点。
  这是这些逻辑交织在一起,看上去ngx_http_upstream_get_vnswrr函数似乎有些复杂了。
  和不带权重的模式一样,它也会在第一次分配的时候设置一个随机值位置,从随机的虚拟节点开始分配,避免“共振”现象的发生。

  除了以上特别说明的部分,其他逻辑几乎就是round-robin代码的翻版,还是非常好理解的,本文列出的源码中也给出了注释,就不再赘述了。

你可能感兴趣的:(nginx学习,LINUX,c++开发,http,负载均衡,网络协议,swrr,vnswrr,nginx)