一致性哈希 (Consistent Hash)

最后更新于:2022-04-01 10:13:48

Nginx版本:1.9.1 我的博客:[http://blog.csdn.net/zhangskd](http://blog.csdn.net/zhangskd)   **算法介绍**   当后端是缓存服务器时,经常使用一致性哈希算法来进行负载均衡。 使用一致性哈希的好处在于,增减集群的缓存服务器时,只有少量的缓存会失效,回源量较小。 在nginx+ats / haproxy+squid等CDN架构中,nginx/haproxy所使用的负载均衡算法便是一致性哈希。   我们举个例子来说明一致性哈希的好处。 假设后端集群包含三台缓存服务器,A、B、C。 请求r1、r2落在A上。 请求r3、r4落在B上。 请求r5、r6落在C上。 使用一致性哈希时,当缓存服务器B宕机时,r1/r2会仍然落在A上,r5/r6会仍然落在C上, 也就是说这两台服务器上的缓存都不会失效。r3/r4会被重新分配给A或者C,并产生回源。 使用其它算法,当缓存服务器B宕机时,r1/r2不再落在A上,r5/r6不再落在C上了。 也就是说A、B、C上的缓存都失效了,所有的请求都要回源。   这里不介绍一致性哈希算法的基本原理,如果不了解,先花个10分钟看下这篇文章: [http://www.codeproject.com/Articles/56138/Consistent-hashing](http://www.codeproject.com/Articles/56138/Consistent-hashing)   在分析模块代码之前,先来看下nginx所实现的一致性哈希算法。   **1. 初始化upstream块** 主要工作是创建和初始化真实节点、创建和初始化虚拟节点。 其中真实节点是使用round robin的方法创建的。   Q:总共有多少个虚拟节点,一个真实节点对应多少个虚拟节点? 累加真实节点的权重,算出总的权重值total_weight,虚拟节点的个数一般为total_weight * 160。 一个权重为weight的真实节点,对应的虚拟节点数为weight * 160。   Q:对于每一个真实节点,是如何创建其对应的虚拟节点的? 1. 真实节点的server成员是其server指令的第一个参数,首先把它解析为HOST和PORT。     base_hash = crc32(HOST 0 PORT)     一个真实节点对应weight * 160个虚拟节点,对于每个虚拟节点来说,base_hash都是一样的。 2. 为了使每个虚拟节点的hash值都不同,又引入了PREV_HASH,它是上一个虚拟节点的hash值。     hash = crc32(base_hash PREV_HASH) 3. 虚拟节点的server成员,指向真实节点的server成员。如此一来,通过比较虚拟节点和真实节点的    server成员是否相同,可以判断它们是否是相对应的。   创建和初始化好虚拟节点数组后,对其中的虚拟节点按照hash值进行排序,对于hash值相同的虚拟节点,只保留第一个。 经过上述步骤,我们得到一个所有虚拟节点组成的数组,其元素的hash值有序而不重复。也就是说,ring建立起来了。   **2. 初始话请求的负载均衡数据** 根据hash指令第一个参数的实时值KEY,KEY一般是$host$uri之类的,计算出本次请求的哈希值。 hash = crc32(KEY) 根据请求的哈希值,在虚拟节点数组中,找到“顺时针方向”最近的一个虚拟节点,其索引为i。 什么叫顺时针方向最近?就是point[i - 1].hash < hash <= point[i].hash。 本次请求就落在该虚拟节点上了,之后交由其对应的真实节点来处理。   **3. 选取真实节点** 在peer.init中,已经知道请求落在哪个虚拟节点上了。 在peer.get中,需要查找虚拟节点对应的真实节点。 根据虚拟节点的server成员,在真实节点数组中查找server成员相同的、可用的真实节点。 如果找不到,那么沿着顺时针方向,继续查找下一个虚拟节点对应的真实节点。 如果找到了一个,那么就是它了。 如果找到了多个,使用轮询的方法从中选取一个。   **4. 缺陷和改进** 一个虚拟节点和一个真实节点,是依据它们的server成员来关联的。 这会出现一种情况,一个虚拟节点对应了多个真实节点,因为: 如果server指令的第一个参数为域名,可能解析为多个真实节点,那么这些真实节点的server成员都是一样的。 对于一个请求,计算其KEY的hash值,顺时针找到最近的虚拟节点后,发现该虚拟节点对应了多个真实节点。 使用哪个真实节点呢?本模块就使用轮询的方法,来从多个真实节点中选一个。 但我们知道使用一致性哈希的场景中,真实节点一般是缓存服务器。 一个虚拟节点对应多个真实节点,会导致一个文件被缓存在多个缓存服务器上。 这会增加磁盘的使用量,以及回源量,显然不是我们希望看到的。   解决这个问题的方法其实很简单,就是虚拟节点和真实节点通过name成员来建立关联。 因为就算对应同一条server配置,server的第一个参数为域名,各个真实节点的name成员也是唯一的。 这样一来,找到了一个虚拟节点,就能找到一个唯一的真实节点,不会有上述问题了。   **数据结构**   **1. 真实节点** 就是采用round robin算法所创建的后端服务器,类型为ngx_http_upstream_rr_peer_t。 需要注意的是,如果server指令的第一个参数是IP和端口,那么一条server指令只对应一个真实节点。 如果server指令的第一个参数是域名,一条server指令可能对应多个真实节点。 它们的server成员是相同的,可以通过name成员区分。 ~~~ struct ngx_http_upstream_rr_peer_s { struct sockaddr *sockaddr; /* 后端服务器的地址 */ socklen_t socklen; /* 地址的长度*/ ngx_str_t name; /* 后端服务器地址的字符串,server.addrs[i].name */ ngx_str_t server; /* server的名称,server.name */ ngx_int_t current_weight; /* 当前的权重,动态调整,初始值为0 */ ngx_int_t effective_weight; /* 有效的权重,会因为失败而降低 */ ngx_int_t weight; /* 配置项指定的权重,固定值 */ ngx_uint_t conns; /* 当前连接数 */ ngx_uint_t fails; /* "一段时间内",已经失败的次数 */ time_t accessed; /* 最近一次失败的时间点 */ time_t checked; /* 用于检查是否超过了"一段时间" */ ngx_uint_t max_fails; /* "一段时间内",最大的失败次数,固定值 */ time_t fail_timeout; /* "一段时间"的值,固定值 */ ngx_uint_t down; /* 服务器永久不可用的标志 */ ... ngx_http_upstream_rr_peer_t *next; /* 指向下一个后端,用于构成链表 */ ... } ngx_http_upstream_rr_peer_t; ~~~ ngx_http_upstream_rr_peers_t表示一组后端服务器,比如一个后端集群。 ~~~ struct ngx_http_upstream_rr_peers_s { ngx_uint_t number; /* 后端服务器的数量 */ ... ngx_uint_t total_weight; /* 所有后端服务器权重的累加值 */ unsigned single:1; /* 是否只有一台后端服务器 */ unsigned weighted:1; /* 是否使用权重 */ ngx_str_t *name; /* upstream配置块的名称 */ ngx_http_upstream_rr_peers_t *next; /* backup服务器集群 */ ngx_http_upstream_rr_peer_t *peer; /* 后端服务器组成的链表 */ }; ~~~   **2. 虚拟节点** 一个真实节点,一般会对应weight * 160个虚拟节点。 虚拟节点的server成员,指向它所归属的真实节点的server成员,如此一来找到了一个虚拟节点后, 就能找到其归属的真实节点。 但这里有一个问题,通过一个虚拟节点的server成员,可能会找到多个真实节点,而不是一个。 因为如果server指令的第一个参数为域名,那么多个真实节点的server成员都是一样的。 ~~~ typedef struct { uint32_t hash; /* 虚拟节点的哈希值 */ ngx_str_t *server; /* 虚拟节点归属的真实节点,对应真实节点的server成员 */ } ngx_http_upstream_chash_point_t; typedef struct { ngx_uint_t number; /* 虚拟节点的个数 */ ngx_http_upstream_chash_point_t point[1]; /* 虚拟节点的数组 */ } ngx_http_upstream_chash_points_t; typedef struct { ngx_http_complex_value_t key; /* 关联hash指令的第一个参数,用于计算请求的hash值 */ ngx_http_upstream_chash_points_t *points; /* 虚拟节点的数组 */ } ngx_http_upstream_chash_points_t; ~~~   **3. 请求的一致性哈希数据** ~~~ typedef struct { /* the round robin data must be first */ ngx_http_upstream_rr_peer_data_t rrp; /* round robin的per request负载均衡数据 */ ngx_http_upstream_hash_srv_conf_t *conf; /* server配置块 */ ngx_str_t key; /* 对于本次请求,hash指令的第一个参数的具体值,用于计算本次请求的哈希值 */ ngx_uint_t tries; /* 已经尝试的虚拟节点数 */ ngx_uint_t rehash; /* 本算法不使用此成员 */ uint32_t hash; /* 根据请求的哈希值,找到顺时方向最近的一个虚拟节点,hash为该虚拟节点在数组中的索引 */ ngx_event_get_peer_pt get_rr_peer; /* round robin算法的peer.get函数 */ } ngx_http_upstream_hash_peer_data_t; ~~~ round robin的per request负载均衡数据。 ~~~ typedef struct { ngx_http_upstream_rr_peers_t *peers; /* 后端集群 */ ngx_http_upstream_rr_peer_t *current; /* 当前使用的后端服务器 */ uintptr_t *tried; /* 指向后端服务器的位图 */ uintptr_t data; /* 当后端服务器的数量较少时,用于存放其位图 */ } ngx_http_upstream_rr_peer_data_t; ~~~   **指令的解析函数**   在一个upstream配置块中,如果有hash指令,且它只带一个参数,则使用的负载均衡算法为哈希算法,比如: hash $host$uri; 在一个upstream配置块中,如果有hash指令,且它带了两个参数,且第二个参数为consistent,则使用的 负载均衡算法为一致性哈希算法,比如: hash $host$uri consistent;   这说明hash指令所属的模块ngx_http_upstream_hash_module同时实现了两种负载均衡算法,而实际上 哈希算法、一致性哈希算法完全可以用两个独立的模块来实现,它们本身并没有多少关联。 哈希算法的实现比较简单,类似之前分析过的ip_hash,接下来分析的是一致性哈希算法。   hash指令的解析函数主要做了: 把hash指令的第一个参数,关联到一个ngx_http_complex_value_t变量,之后可以通过该变量获取参数的实时值。 指定此upstream块中server指令支持的属性。 根据hash指令携带的参数来判断是使用哈希算法,还是一致性哈希算法。如果hash指令的第二个参数为"consistent", 则表示使用一致性哈希算法,指定upstream块的初始化函数uscf->peer.init_upstream。 ~~~ static char *ngx_http_upstream_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_upstream_hash_srv_conf_t *hcf = conf; ngx_str_t *value; ngx_http_upstream_srv_conf_t *uscf; ngx_http_compile_complex_value_t ccv; value = cf->args->elts; ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); /* 把hash指令的第一个参数,关联到一个ngx_http_complex_value_t变量, * 之后可以通过该变量获取参数的实时值。 */ ccv.cf = conf; ccv.value = &value[1]; ccv.complex_value = &hcf->key; if (ngx_http_compile_complex_value(&ccv) != NGX_OK) return NGX_CONF_ERROR; /* 获取所在的upstream{}块 */ 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"); /* 指定此upstream块中server指令支持的属性 */ uscf->flags = NGX_HTTP_UPSTREAM_CREATE | NGX_HTTP_UPSTREAM_WEIGHT | NGX_HTTP_UPSTREAM_MAX_FAILS | NGX_HTTP_UPSTREAM_FAIL_TIMEOUT | NGX_HTTP_UPSTREAM_DOWN; /* 根据hash指令携带的参数来判断是使用哈希算法,还是一致性哈希算法。 * 每种算法都有自己的upstream块初始化函数。 */ if (cf->args->nelts == 2) uscf->peer.init_upstream = ngx_http_upstream_init_hash; else if (ngx_strcmp(value[2].data, "consistent") == 0) uscf->peer.init_upstream = ngx_http_upstream_init_chash; else ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid parameter \"%V\"", &value[2]); return NGX_CONF_OK; } ~~~   **初始化upstream块**   执行完指令的解析函数后,紧接着会调用所有HTTP模块的init main conf函数。 在执行ngx_http_upstream_module的init main conf函数时,会调用所有upstream块的初始化函数。 对于使用一致性哈希的upstream块,其初始化函数(peer.init_upstream)就是上一步中指定 ngx_http_upstream_init_chash,它主要做了: 调用round robin的upstream块初始化函数来创建和初始化真实节点 指定per request的负载均衡初始化函数peer.init 创建和初始化虚拟节点数组,使该数组中的虚拟节点有序而不重复 ~~~ static ngx_int_t ngx_http_upstream_init_chash(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us) { u_char *host, *port, c; size_t host_len, port_len, size; uint32_t hash, base_hash; ngx_str_t *server; ngx_uint_t npoints, i, j; ngx_http_upstream_rr_peer_t *peer; ngx_http_upstream_rr_peers_t *peers; ngx_http_upstream_chash_points_t *points; ngx_http_upstream_hash_srv_conf_t *hcf; union { uint32_t value; u_char byte[4]; } prev_hash; /* 使用round robin的upstream块初始化函数,创建和初始化真实节点 */ if (ngx_http_upstream_init_round_robin(cf, us) != NGX_OK) return NGX_ERROR: /* 重新设置per request的负载均衡初始化函数 */ us->peer.init = ngx_http_upstream_init_chash_peer; peers = us->peer.data; /* 真实节点的集群 */ npoints = peers->total_weight * 160; /* 一共创建npoints个虚拟节点 */ size = sizeof(ngx_http_upstream_chash_points_t) + sizeof(ngx_http_upstream_chash_point_t) * (npoints - 1); points = ngx_palloc(cf->pool, size); if (points == NULL) return NGX_ERROR; points->number = 0; /* 初始化所有的虚拟节点 */ for (peer = peers->peer; peer; peer = peer->next) { server = &peer->server; /* server指令的第一个参数, server.name */ /* Hash expression is compatible with Cache::Memcached::Fast: * crc32(HOST 0 PORT PREV_HASH). */ if (server->len >= 5 && ngx_strncasecmp(server->data, (u_char *) "unix:", 5) == 0) { host = server->data + 5; host_len = server->len - 5; port = NULL; port_len = 0; goto done; } /* 把每个peer的server成员,解析为HOST和PORT */ for (j = 0; j < server->len; j++) { c = server->data[server->len - j - 1]; if (c == ":") { host = server->data; host_len = server->len - j - 1; port = server->data + server->len - j; port_len = j; goto done; } if (c < '0' || c > '9') /* 表示没有指定端口 */ break; } host = server->data; host_len = server->len; port = NULL; port_len = 0; done: /* 根据解析peer的server成员所得的HOST和PORT,计算虚拟节点的base_hash值 */ ngx_crc32_init(base_hash); ngx_crc32_update(&base_hash, host, host_len); ngx_crc32_update(&base_hash, (u_char *) "", 1); /* 空字符串包含字符\0 */ ngx_crc32_update(&base_hash, port, port_len); /* 对于归属同一个真实节点的虚拟节点,它们的base_hash值相同,而prev_hash不同 */ prev_hash.value = 0; npoints = peer->weight * 160; for (j = 0; j < npoints; j++) { hash = base_hash; ngx_crc32_update(&hash, prev_hash.byte, 4); ngx_crc32_final(hash); points->point[points->number].hash = hash; /* 虚拟节点的哈希值 */ points->point[points->number].server = server; /* 虚拟节点所归属的真实节点,对应真实节点的server成员 */ points->number++; #if (NGX_HAVE_LITTLE_ENDIAN) prev_hash.value = hash; #else prev_hash.byte[0] = (u_char) (hash & 0xff); prev_hash.byte[1] = (u_char) ((hash >> 8) & 0xff); prev_hash.byte[2] = (u_char) ((hash >> 16) & 0xff); prev_hash.byte[3] = (u_char) ((hash >> 24) & 0xff); #endif } } /* 使用快速排序,使虚拟节点数组的元素,按照其hash值从小到大有序 */ ngx_qsort(points->point, points->number, sizeof(ngx_http_upstream_chash_point_t), ngx_http_upstream_chash_cmp_points); /* 如果虚拟节点数组中,有多个元素的hash值相同,只保留第一个 */ for (i = 0, j = 1; j < points->number; j++) if (points->point[i].hash != points->point[j].hash) points->point[++i] = points->point[j]; /* 经过上述步骤后,虚拟节点数组中的元素,有序而不重复 */ points->number = i + 1; hcf = ngx_http_conf_upstream_srv_conf(us, ngx_http_upstream_hash_module); hcf->points = points; /* 保存虚拟节点数组 */ return NGX_OK; } ~~~ ~~~ static int ngx_libc_cdel ngx_http_upstream_chash_cmp_points(const void *one, const void *two) { ngx_http_upstream_chash_point_t *first = (ngx_http_upstream_chash_point_t *) one; ngx_http_upstream_chash_point_t *second = (ngx_http_upstream_chash_point_t *) two; if (first->hash < second->hash) return -1; else if (first->hash > second->hash) return 1; else return 0; } ~~~   **初始化请求的负载均衡数据**    收到一个请求后,一般使用的反向代理模块(upstream模块)为ngx_http_proxy_module, 其NGX_HTTP_CONTENT_PHASE阶段的处理函数为ngx_http_proxy_handler,在初始化upstream机制的 ngx_http_upstream_init_request函数中,调用在第二步中指定的peer.init,主要用于初始化请求的负载均衡数据。 对于一致性哈希,peer.init实例为ngx_http_upstream_init_chash_peer,主要做了: 首先调用hash算法的per request负载均衡初始化函数,创建和初始化请求的负载均衡数据。 重新指定peer.get,用于选取一个真实节点来处理本次请求。 获取的本请求对应的hash指令的第一个参数值,计算请求的hash值。 寻找第一个hash值大于等于请求的哈希值的虚拟节点,即寻找“顺时针方向最近”的一个虚拟节点。 ~~~ static ngx_int_t ngx_http_upstream_init_chash_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us) { uint32_t hash; ngx_http_upstream_hash_srv_conf_t *hcf; ngx_http_upstream_hash_peer_data_t *hp; /* 调用hash算法的per request负载均衡初始化函数,创建和初始化请求的负载均衡数据 */ if (ngx_http_upstream_init_hash_peer(r, us) != NGX_OK) return NGX_ERROR; /* 重新指定peer.get,用于选取一个真实节点 */ r->upstream->peer.get = ngx_http_upstream_get_chash_peer; hp = r->upstream->peer.data; hcf = ngx_http_conf_upstream_srv_conf(us, ngx_http_upstream_hash_module); /* 根据获取的本请求对应的hash指令的第一个参数值,计算请求的hash值 */ hash = ngx_crc32_long(hp->key.data, hp->key.len); /* 根据请求的hash值,找到顺时针方向最近的一个虚拟节点,hp->hash记录此虚拟节点 * 在数组中的索引。 */ hp->hash = ngx_http_upstream_find_chash_point(hcf->points, hash); return NGX_OK: } ~~~ hash算法的per request负载均衡初始化函数。 ~~~ static ngx_int_t ngx_http_upstream_init_hash_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us) { ngx_http_upstream_hash_srv_conf_t *hcf; ngx_http_upstream_hash_peer_data_t *hp; hp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_hash_peer_data_t)); if (hp == NULL) return NGX_ERROR: /* 调用round robin的per request负载均衡初始化函数 */ r->upstream->peer.data = &hp->rrp; if (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK) return NGX_ERROR; r->upstream->peer.get = ngx_http_upstream_get_hash_peer; hcf = ngx_http_conf_upstream_srv_conf(us, ngx_http_upstream_hash_module); /* 获取本请求对应的hash指令的第一个参数值,用于计算请求的hash值 */ if (ngx_http_complex_value(r, &hcf->key, &hp->key) != NGX_OK) return NGX_ERROR; ... hp->conf = hcf; hp->tries = 0; hp->rehash = 0; hp->hash = 0; hp->get_rr_peer = ngx_http_upstream_get_round_robin_peer; /* round robin的peer.get函数 */ return NGX_OK; } ~~~ 我们知道虚拟节点数组是有序的,事先已按照虚拟节点的hash值从小到大排序好了。 现在使用二分查找,寻找第一个hash值大于等于请求的哈希值的虚拟节点,即“顺时针方向最近”的一个虚拟节点。 ~~~ static ngx_uint_t ngx_http_upstream_find_chash_point(ngx_http_upstream_chash_points_t *points, uint32_t hash) { ngx_uint_t i, j, k; ngx_http_upstream_chash_point_t *point; /* find first point >= hash */ point = &points->point[0]; i = 0; j = points->number;' while(i < j) { k = (i + j) / 2; if (hash > point[k].hash) i = k + 1; else if (hash < point[k].hash) j = k; else return k; } return i; } ~~~   **选取一个真实节点**   一般upstream块中会有多个真实节点,那么对于本次请求,要选定哪一个真实节点呢? 对于一致性哈希算法,选取真实节点的peer.get函数为ngx_http_upstream_get_chash_peer。   其实在peer.init中,已经找到了该请求对应的虚拟节点了: 根据请求对应的hash指令的第一个参数值,计算请求的hash值。 寻找第一个哈希值大于等于请求的hash值的虚拟节点,即“顺时针方向最近”的一个虚拟节点。   在peer.get中,需查找此虚拟节点对应的真实节点。 根据虚拟节点的server成员,在真实节点数组中查找server成员一样的且可用的真实节点。 如果找不到,那么沿着顺时针方向,继续查找下一个虚拟节点对应的真实节点。 如果找到一个真实节点,那么就是它了。 如果找到多个真实节点,使用轮询的方法从中选取一个。 ~~~ static ngx_http_upstream_get_chash_peer(ngx_peer_connection_t *pc, void *data) { ngx_http_upstream_hash_peer_data_t *hp = data; /* 请求的负载均衡数据 */ time_t now; intptr_t m; ngx_str_t *server; ngx_int_t total; ngx_uint_t i, n, best_i; ngx_http_upstream_rr_peer_t *peer, *best; ngx_http_upstream_chash_point_t *point; ngx_http_upstream_chash_points_t *points; ngx_http_upstream_hash_srv_conf_t *hcf; ... pc->cached = 0; pc->connection = NULL: now = ngx_time(); hcf = hp->conf; points = hcf->points; /* 虚拟节点数组 */ point = &points->point[0]; /* 指向第一个虚拟节点 */ for ( ; ; ) { /* 在peer.init中,已根据请求的哈希值,找到顺时针方向最近的一个虚拟节点, * hash为该虚拟节点在数组中的索引。 * 一开始hash值肯定小于number,之后每尝试一个虚拟节点后,hash++。取模是为了防止越界访问。 */ server = point[hp->hash % points->number].server; best = NULL; best_i = 0; total = 0; /* 遍历真实节点数组,寻找可用的、该虚拟节点归属的真实节点(server成员相同), * 如果有多个真实节点同时符合条件,那么使用轮询来从中选取一个真实节点。 */ for (peer = hp->rrp.peers->peer, i = 0; peer; peer = peer->next, i++) { /* 检查此真实节点在状态位图中对应的位,为1时表示不可用 */ n = i / (8 * sizeof(uintptr_t)); m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t)); if (hp->rrp.tried[n] & m) continue; /* server指令中携带了down属性,表示后端永久不可用 */ if (peer->down) continue; /* 如果真实节点的server成员和虚拟节点的不同,表示虚拟节点不属于此真实节点 */ if (peer->server.len != server->len || ngx_strncmp(peer->server.data, server->data, server->len) != 0) continue; /* 在一段时间内,如果此真实节点的失败次数,超过了允许的最大值,那么不允许使用了 */ if (peer->max_fails && peer->fails >= peer->max_fails && now - peer->checked <= peer->fail_timeout) continue; peer->current_weight += peer->effective_weight; /* 对每个真实节点,增加其当前权重 */ total += peer->effective_weight; /* 累加所有真实节点的有效权重 */ /* 如果之前此真实节点发生了失败,会减小其effective_weight来降低它的权重。 * 此后又通过增加其effective_weight来恢复它的权重。 */ if (peer->effective_weight < peer->weight) peer->effective_weight++; /* 选取当前权重最大者,作为本次选定的真实节点 */ if (best == NULL || peer->current_weight > best->current_weight) { best = peer; best_i = i; } } /* 如果选定了一个真实节点 */ if (best) { best->current_weight -= total; /* 如果使用了轮询,需要降低选定节点的当前权重 */ goto found; } hp->hash++; /* 增加虚拟节点的索引,即“沿着顺时针方向” */ hp->tries++; /* 已经尝试的虚拟节点数 */ /* 如果把所有的虚拟节点都尝试了一遍,还找不到可用的真实节点 */ if (hp->tries >= points->number) return NGX_BUSY; } found: /* 找到了和虚拟节点相对应的、可用的真实节点了 */ hp->rrp.current = best; /* 选定的真实节点 */ /* 保存选定的后端服务器的地址,之后会向这个地址发起连接 */ pc->sockaddr = peer->sockaddr; pc->socklen = peer->socklen; pc->name = &peer->name; best->conns++; /* 更新checked时间 */ if (now - best->checked > best->fail_timeout) best->checked = now; n = best_i / (8 * sizeof(uintptr_t)); m = (uintptr_t) 1 << best_i % (8 * sizeof(uintptr_t)); /* 对于本次请求,如果之后需要再次选取真实节点,不能再选取同一个了 */ hp->rrp->tried[n] |= m; return NGX_OK; } ~~~    
';

最少连接 (least_conn)

最后更新于:2022-04-01 10:13:45

Nginx版本:1.9.1 我的博客:[http://blog.csdn.net/zhangskd](http://blog.csdn.net/zhangskd)   **算法介绍**   我们知道轮询算法是把请求平均的转发给各个后端,使它们的负载大致相同。 这有个前提,就是每个请求所占用的后端时间要差不多,如果有些请求占用的时间很长,会导致其所在的后端 负载较高。在这种场景下,把请求转发给连接数较少的后端,能够达到更好的负载均衡效果,这就是least_conn算法。   least_conn算法很简单,首选遍历后端集群,比较每个后端的conns/weight,选取该值最小的后端。 如果有多个后端的conns/weight值同为最小的,那么对它们采用加权轮询算法。   **指令的解析函数**   在一个upstream配置块中,如果有least_conn指令,表示使用least connected负载均衡算法。 least_conn指令的解析函数为ngx_http_upstream_least_conn,主要做了: 指定初始化此upstream块的函数uscf->peer.init_upstream 指定此upstream块中server指令支持的属性 ~~~ static char *ngx_http_upstream_least_conn(ngx_conf_t *cf, ngx_command_t *cmd, void *ctx) { ngx_http_upstream_srv_conf_t *uscf; /* 获取所在的upstream{}块 */ 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"); /* 此upstream块的初始化函数 */ uscf->peer.init_upstream = ngx_http_upstream_init_least_conn; /* 指定此upstream块中server指令支持的属性 */ uscf->flags = NGX_HTTP_UPSTREAM_CREATE | NGX_HTTP_UPSTREAM_WEIGHT | NGX_HTTP_UPSTREAM_MAX_FAILS | NGX_HTTP_UPSTREAM_FAIL_TIMEOUT | NGX_HTTP_UPSTREAM_DOWN | NGX_HTTP_UPSTREAM_BACKUP; return NGX_CONF_OK; } ~~~ 以下是upstream块中server指令可支持的属性 NGX_HTTP_UPSTREAM_CREATE:检查是否重复创建,以及必要的参数是否填写 NGX_HTTP_UPSTREAM_WEIGHT:server指令支持weight属性 NGX_HTTP_UPSTREAM_MAX_FAILS:server指令支持max_fails属性 NGX_HTTP_UPSTREAM_FAIL_TIMEOUT:server指令支持fail_timeout属性 NGX_HTTP_UPSTREAM_DOWN:server指令支持down属性 NGX_HTTP_UPSTREAM_BACKUP:server指令支持backup属性   **初始化upstream块**   执行完指令的解析函数后,紧接着会调用所有HTTP模块的init main conf函数。 在执行ngx_http_upstream_module的init main conf函数时,会调用所有upstream块的初始化函数。 对于使用least_conn的upstream块,其初始化函数(peer.init_upstream)就是上一步中指定 ngx_http_upstream_init_least_conn,它主要做了: 调用round robin的upstream块初始化函数来创建和初始化后端集群,保存该upstream块的数据 指定per request的负载均衡初始化函数peer.init   因为脏活累活都让round robin的upstream块初始化函数给干了,所以ngx_http_upstream_init_least_conn很简单。 ~~~ static ngx_int_t ngx_http_upstream_init_least_conn(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us) { ngx_log_debug0(NGX_LOG_DEBUG_HTTP, cf->log, 0, "init least conn"); /* 使用round robin的upstream块初始化函数,创建和初始化后端集群 */ if (ngx_http_upstream_init_round_robin(cf, us) != NGX_OK) return NGX_ERROR; /* 重新设置per request的负载均衡初始化函数 */ us->peer.init = ngx_http_upstream_init_least_conn_peer; return NGX_OK; } ~~~   **初始化请求的负载均衡数据**    收到一个请求后,一般使用的反向代理模块(upstream模块)为ngx_http_proxy_module, 其NGX_HTTP_CONTENT_PHASE阶段的处理函数为ngx_http_proxy_handler,在初始化upstream机制的 ngx_http_upstream_init_request函数中,调用在第二步中指定的peer.init,主要用于初始化请求的负载均衡数据。 对于least_conn,peer.init实例为ngx_http_upstream_init_least_conn_peer,主要做了: 调用round robin的peer.init来初始化请求的负载均衡数据 重新指定peer.get,用于从集群中选取一台后端服务器   least_conn的per request负载均衡数据和round robin的完全一样,都是一个ngx_http_upstream_rr_peer_data_t实例。 ~~~ static ngx_int_t ngx_http_upstream_init_least_conn_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us) { ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "init least conn peer"); /* 调用round robin的per request负载均衡初始化函数 */ if (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK) return NGX_ERROR; /* 指定peer.get,用于从集群中选取一台后端 */ r->upstream->peer.get = ngx_http_upstream_get_least_conn_peer; return NGX_OK; } ~~~   **选取一台后端服务器**   一般upstream块中会有多台后端,那么对于本次请求,要选定哪一台后端呢? 这时候第三步中r->upstream->peer.get指向的函数就派上用场了: 采用least connected算法,从集群中选出一台后端来处理本次请求。 选定后端的地址保存在pc->sockaddr,pc为主动连接。 函数的返回值: NGX_DONE:选定一个后端,和该后端的连接已经建立。之后会直接发送请求。 NGX_OK:选定一个后端,和该后端的连接尚未建立。之后会和后端建立连接。 NGX_BUSY:所有的后端(包括备份集群)都不可用。之后会给客户端发送502(Bad Gateway)。 ~~~ static ngx_int_t ngx_http_upstream_get_least_conn_peer(ngx_peer_connection_t *pc, void *data) { ngx_http_upstream_rr_peer_data_t *rrp = data; /* 请求的负载均衡数据 */ time_t now; uintptr_t m; ngx_int_t rc, total; ngx_uint_t i, n, p, many; ngx_http_upstream_rr_peer_t *peer, *best; ngx_http_upstream_rr_peers_t *peers; ... /* 如果集群只包含一台后端,那么就不用选了 */ if (rrp->peers->single) return ngx_http_upstream_get_round_robin_peer(pc, rrp); pc->cached = 0; pc->connection = NULL; now = ngx_time(); peers = rrp->peers; /* 后端集群 */ best = NULL; total = 0; ... /* 遍历后端集群 */ for (peer = peers->peer, i = 0; peer; peer = peer->next, i++) { /* 检查此后端在状态位图中对应的位,为1时表示不可用 */ n = i / (8 * sizeof(uintptr_t)); m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t)); if (rrp->tried[n] & m) continue; /* server指令中携带了down属性,表示后端永久不可用 */ if (peer->down) continue; /* 在一段时间内,如果此后端服务器的失败次数,超过了允许的最大值,那么不允许使用此后端了 */ if (peer->max_fails && peer->fails >= peer->max_fails && now - peer->checked <= peer->fail_timeout) continue; /* select peer with least number of connections; if there are multiple peers * with the same number of connections, select based on round-robin. */ /* 比较各个后端的conns/weight,选取最小者; * 如果有多个最小者,记录第一个的序号p,且设置many标志。 */ if (best == NULL || peer->conns * best->weight < best->conns * peer->weight) { best = peer; many = 0; p = i; } else if (peer->conns * best->weight == best->conns * peer->weight) many = 1; } /* 找不到可用的后端 */ if (best == NULL) goto failed; /* 如果有多个后端的conns/weight同为最小者,则对它们使用轮询算法 */ if (many) { for (peer = best, i = p; peer; peer->peer->next, i++) { /* 检查此后端在状态位图中对应的位,为1时表示不可用 */ n = i / (8 * sizeof(uintptr_t)); m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t)); /* server指令中携带了down属性,表示后端永久不可用 */ if (peer->down) continue; /* conns/weight必须为最小的 */ if (peer->conns * best->weight != best->conns * peer->weight) continue; /* 在一段时间内,如果此后端服务器的失败次数,超过了允许的最大值,那么不允许使用此后端了 */ if (peer->max_fails && peer->fails >= peer->max_fails && now - peer->checked <= peer->fail_timeout) continue; peer->current_weight += peer->effective_weight; /* 对每个后端,增加其当前权重 */ total += peer->effective_weight; /* 累加所有后端的有效权重 */ /* 如果之前此后端发生了失败,会减小其effective_weight来降低它的权重。 * 此后在选取后端的过程中,又通过增加其effective_weight来恢复它的权重。 */ if (peer->effective_weight < peer->weight) peer->effective_weight++; /* 选取当前权重最大者,作为本次选定的后端 */ if (best == NULL || peer->current_weight > best->current_weight) { best = peer; p = i; } } } best->current_weight -= total; /* 如果使用轮询,要降低选定后端的当前权重 */ /* 更新checked时间 */ if (now - best->checked > best->fail_timeout) best->checked = now; /* 保存选定的后端服务器的地址,之后会向这个地址发起连接 */ pc->sockaddr = best->sockaddr; pc->socklen = best->socklen; pc->name = &best->name; best->conns++; /* 增加选定后端的当前连接数 */ n = p / (8 * sizeof(uintptr_t)); m = (uintptr_t) 1 << p % (8 * sizeof(uintptr_t)); rrp->tried[n] |= m; /* 对于此请求,如果之后需要再次选取后端,不能再选取这个后端了 */ return NGX_OK; failed: /* 如果不能从集群中选取一台后端,那么尝试备用集群 */ if (peers->next) { ... rrp->peers = peers->next; n = (rrp->peers->number + (8 * sizeof(uintptr_t) - 1)) / (8 * sizeof(uintptr_t)); for (i = 0; i < n; i++) rrp->tried[i] = 0; /* 重新调用本函数 */ rc = ngx_http_upstream_get_least_conn_peer(pc, rrp); if (rc != NGX_BUSY) return rc; } /* all peers failed, mark them as live for quick recovery */ for (peer = peers->peer; peer; peer = peer->next) { peer->fails = 0; } pc->name = peers->name; return NGX_BUSY; } ~~~    
';

保持会话 (ip_hash)

最后更新于:2022-04-01 10:13:43

Nginx版本:1.9.1 我的博客:[http://blog.csdn.net/zhangskd](http://blog.csdn.net/zhangskd)   **算法介绍**   ip_hash算法的原理很简单,根据请求所属的客户端IP计算得到一个数值,然后把请求发往该数值对应的后端。 所以同一个客户端的请求,都会发往同一台后端,除非该后端不可用了。ip_hash能够达到保持会话的效果。 ip_hash是基于round robin的,判断后端是否可用的方法是一样的。   第一步,根据客户端IP计算得到一个数值。 hash1 = (hash0 * 113 + addr[0]) % 6271; hash2 = (hash1 * 113 + addr[1]) % 6271; hash3 = (hash2 * 113 + addr[2]) % 6271; hash3就是计算所得的数值,它只和初始数值hash0以及客户端的IP有关。   第二步,根据计算所得数值,找到对应的后端。 w = hash3 % total_weight; while (w >= peer->weight) {     w -= peer->weight;     peer = peer->next;     p++; } total_weight为所有后端权重之和。遍历后端链表时,依次减去每个后端的权重,直到w小于某个后端的权重。 选定的后端在链表中的序号为p。因为total_weight和每个后端的weight都是固定的,所以如果hash3值相同, 则找到的后端相同。   **指令的解析函数**   在一个upstream配置块中,如果有ip_hash指令,表示使用ip_hash负载均衡算法。 ip_hash指令的解析函数为ngx_http_upstream_ip_hash,主要做了: 指定初始化此upstream块的函数peer.init_upstream 指定此upstream块中server指令支持的属性 ~~~ static char *ngx_http_upstream_ip_hash (ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_upstream_srv_conf_t *uscf; /* 获取对应的upstream配置块 */ 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"); /* 指定初始化此upstream块的函数 */ uscf->peer.init_upstream = ngx_http_upstream_init_ip_hash; /* 指定此upstream块中server指令支持的属性 */ uscf->flags = NGX_HTTP_UPSTREAM_CREATE | NGX_HTTP_UPSTREAM_WEIGHT | NGX_HTTP_UPSTREAM_MAX_FAILS | NGX_HTTP_UPSTREAM_FAIL_TIMEOUT | NGX_HTTP_UPSTREAM_DOWN; return NGX_CONF_OK; } ~~~ 以下是upstream块中server指令可支持的属性 NGX_HTTP_UPSTREAM_CREATE:检查是否重复创建,以及必要的参数是否填写 NGX_HTTP_UPSTREAM_WEIGHT:server指令支持weight属性 NGX_HTTP_UPSTREAM_MAX_FAILS:server指令支持max_fails属性 NGX_HTTP_UPSTREAM_FAIL_TIMEOUT:server指令支持fail_timeout属性 NGX_HTTP_UPSTREAM_DOWN:server指令支持down属性 NGX_HTTP_UPSTREAM_BACKUP:server指令支持backup属性   **初始化upstream块**   执行完指令的解析函数后,紧接着会调用所有HTTP模块的init main conf函数。 在执行ngx_http_upstream_module的init main conf函数时,会调用所有upstream块的初始化函数。 对于使用ip_hash的upstream块,其初始化函数(peer.init_upstream)就是上一步中指定的 ngx_http_upstream_init_ip_hash。它主要做了: 调用默认的初始化函数ngx_http_upstream_init_round_robin来创建和初始化后端集群,保存该upstream块的数据 指定初始化请求的负载均衡数据的函数peer.init   因为脏活累活都让默认的函数给干了,所以ngx_http_upstream_init_ip_hash的代码就几行:) ~~~ static ngx_int_t ngx_http_upstream_init_ip_hash (ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us) { if (ngx_http_upstream_init_round_robin(cf, us) != NGX_OK) return NGX_ERROR; us->peer.init = ngx_http_upstream_init_ip_hash_peer; /* 初始化请求负载均衡数据的函数 */ return NGX_OK; } ~~~   **初始化请求的负载均衡数据**    收到一个请求后,一般使用的反向代理模块(upstream模块)为ngx_http_proxy_module, 其NGX_HTTP_CONTENT_PHASE阶段的处理函数为ngx_http_proxy_handler,在初始化upstream机制的 ngx_http_upstream_init_request函数中,调用在第二步中指定的peer.init,主要用于初始化请求的负载均衡数据。 对于ip_hash,peer.init实例为ngx_http_upstream_init_ip_hash_peer,主要做了: 调用round robin的per request负载均衡初始化函数,创建和初始化其per request负载均衡数据,即iphp->rrp。 重新指定peer.get,用于从集群中选取一台后端服务器。 保存客户端的地址,初始化ip_hash的per request负载均衡数据。   ip_hash的per request负载均衡数据的结构体为ngx_http_upstream_ip_hash_peer_data_t。 ~~~ typedef struct { ngx_http_upstream_rr_peer_data_t rrp; /* round robin的per request负载均衡数据 */ ngx_uint_t hash; /* 根据客户端IP计算所得的hash值 */ u_char addrlen; /* 使用客户端IP的后三个字节来计算hash值 */ u_char *addr; /* 客户端的IP */ u_char tries; /* 已经尝试了多少次 */ ngx_event_get_peer_pt get_rr_peer; /* round robin算法的peer.get函数 */ } ngx_http_upstream_ip_hash_peer_data_t; ~~~ ~~~ static ngx_int_t ngx_http_upstream_init_ip_hash_peer (ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us) { struct sockaddr_in *sin; ... ngx_http_upstream_ip_hash_peer_data_t *iphp; /* 创建ip_hash的per request负载均衡数据的实例 */ iphp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_ip_hash_peer_data_t)); if (iphp == NULL) return NGX_ERROR; /* 首先调用round robin的per request负载均衡数据的初始化函数, * 创建和初始化round robin的per request负载均衡数据实例,即iphp->rrp。 */ r->upstream->peer.data = &iphp->rrp; if (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK) return NGX_ERROR: /* 重新指定peer.get,用于从集群中选取一台后端服务器 */ r->upstream->peer.get = ngx_http_upstream_get_ip_hash_peer; /* 客户端的地址类型 */ switch(r->connection->sockaddr->sa_family) { case AF_INET: sin = (struct sockaddr_in *) r->connection->sockaddr; iphp->addr = (u_char *) &sin->sin_addr.s_addr; /* 客户端的IP */ iphp->addrlen = 3; /* 使用客户端IP的后三个字节来计算hash值 */ break; #if (NGX_HAVE_INET6) ... #endif default: iphp->addr = ngx_http_upstream_ip_hash_pseudo_addr; iphp->addrlen = 3; } iphp->hash = 89; iphp->tries = 0; iphp->get_rr_peer = ngx_http_upstream_get_round_robin_peer; /* 保存round robin的peer.get函数 */ } ~~~   **选取一台后端服务器**   一般upstream块中会有多台后端,那么对于本次请求,要选定哪一台后端呢? 这时候第三步中r->upstream->peer.get指向的函数就派上用场了: 采用ip_hash算法,从集群中选出一台后端来处理本次请求。 选定后端的地址保存在pc->sockaddr,pc为主动连接。 函数的返回值: NGX_DONE:选定一个后端,和该后端的连接已经建立。之后会直接发送请求。 NGX_OK:选定一个后端,和该后端的连接尚未建立。之后会和后端建立连接。 NGX_BUSY:所有的后端(包括备份集群)都不可用。之后会给客户端发送502(Bad Gateway)。 ~~~ static ngx_int_t ngx_http_upstream_get_ip_hash_peer (ngx_peer_connection_t *pc, void *data) { ngx_http_upstream_ip_hash_peer_data_t *iphp = data; /* 请求的负载均衡数据 */ time_t now; ngx_int_t w; uintptr_t m; ngx_uint_t i, n, p, hash; ngx_http_upstream_rr_peer_t *peer; ... /* 如果只有一台后端,或者尝试次数超过20次,则使用轮询的方式来选取后端 */ if (iphp->tries > 20 || iphp->rrp.peers->single) { return iphp->get_rr_peer(pc, &iphp->rrp); } now = ngx_time(); pc->cached = 0; pc->connection = NULL; hash = iphp->hash; /* 本次选取的初始hash值 */ for ( ; ; ) { /* 根据客户端IP、本次选取的初始hash值,计算得到本次最终的hash值 */ for (i = 0; i < (ngx_uint_t) iphp->addrlen; i++) hash = (hash * 113 + iphp->addr[i]) % 6271; /* total_weight和weight都是固定值 */ w = hash % iphp->rrp.peers->total_weight; peer = iphp->rrp.peers->peer; /* 第一台后端 */ p = 0; while (w >= peer->weight) { w -= peer->weight; peer = peer->next; p++; } /* 检查第此后端在状态位图中对应的位,为1时表示不可用 */ n = p / (8 * sizeof(uintptr_t)); m = (uintptr_t) 1 << p % (8 * sizeof(uintptr_t)); if (iphp->rrp.tried[n] & m) goto next; /* 检查后端是否永久不可用 */ if (peer->down) goto next; /* 在一段时间内,如果此后端服务器的失败次数,超过了允许的最大值,那么不允许使用此后端了 */ if (peer->max_fails && peer->fails >= peer->max_fails && now - peer->checked <= peer->fail_timeout) goto next; break; next: /* 增加已尝试的次数,如果超过20次,则使用轮询的方式来选取后端 */ if (++iphp->tries > 20) return iphp->get_rr_peer(pc, &iphp->rrp); } iphp->rrp.current = peer; /* 选定的可用后端 */ /* 保存选定的后端服务器的地址,之后会向这个地址发起连接 */ pc->sockaddr = peer->sockaddr; pc->socklen = peer->socklen; pc->name = &peer->name; peer->conns++; /* 更新checked时间 */ if (now - peer->checked > peer->fail_timeout) peer->checked = now; iphp->rrp.tried[n] |= m; /* 对于此请求,如果之后需要再次选取后端,不能再选取这个后端了 */ iphp->hash = hash; /* 保存hash值,下次可能还会用到 */ return NGX_OK: } ~~~  
';

加权轮询 (Weighted Round Robin) 下篇

最后更新于:2022-04-01 10:13:41

Nginx版本:1.9.1 我的博客:[http://blog.csdn.net/zhangskd](http://blog.csdn.net/zhangskd)   上篇blog讲述了加权轮询算法的原理、以及负载均衡模块中使用的数据结构,接着我们来看看加权轮询算法的具体实现。   **指令的解析函数**   如果upstream配置块中没有指定使用哪种负载均衡算法,那么默认使用加权轮询。 也就是说使用加权轮询算法,并不需要特定的指令,因此也不需要实现指令的解析函数。 而实际上,和其它负载均衡算法不同(比如ip_hash),加权轮询算法并不是以模块的方式实现的, 而是作为Nginx框架的一部分。   **初始化upstream块**   在执行ngx_http_upstream_module的init main conf函数时,会遍历所有upstream配置块,调用它们 事先指定的初始化函数。对于一个upstream配置块,如果没有指定初始化函数,则调用加权轮询算法 提供的upstream块初始化函数 - ngx_http_upstream_init_round_robin。   来看下ngx_http_upstream_module。 ~~~ ngx_http_module_t ngx_http_upstream_module_ctx = { ... ngx_http_upstream_init_main_conf, /* init main configuration */ ... }; ~~~ ~~~ static char *ngx_http_upstream_init_main_conf(ngx_conf_t *cf, void *conf) { ... /* 数组的元素类型是ngx_http_upstream_srv_conf_t */ for (i = 0; i < umcf->upstreams.nelts; i++) { /* 如果没有指定upstream块的初始化函数,默认使用round robin的 */ init = uscfp[i]->peer.init_upstream ? uscfp[i]->peer.init_upstream : ngx_http_upstream_init_round_robin; if (init(cf, uscfp[i] != NGX_OK) { return NGX_CONF_ERROR; } } ... } ~~~ ngx_http_upstream_init_round_robin做的工作很简单: 指定请求的负载均衡初始化函数,用于初始化per request的负载均衡数据。 创建和初始化后端集群、备份集群。 ~~~ ngx_int_t ngx_http_upstream_init_round_robin (ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us) { ngx_url_t u; ngx_uint_t i, j, n, w; ngx_http_upstream_server_t *server; ngx_http_upstream_rr_peer_t *peer, **peerp; ngx_http_upstream_rr_peers_t *peers, *backup; /* 指定请求的负载均衡初始化函数,用于初始化per request的负载均衡数据 */ us->peer.init = ngx_http_upstream_init_round_robin_peer; /* upstream配置块的servers数组,在解析配置文件时就创建好了 */ if (us->servers) { server = us->servers->elts; n = 0; w = 0; /* 数组元素类型为ngx_http_upstream_server_t,对应一条server指令 */ for (i = 0; i < us->servers->nelts; i++) { if (server[i].backup) continue; n += server[i].naddrs; /* 所有后端服务器的数量 */ w += server[i].naddrs * server[i].weight; /* 所有后端服务器的权重之和 */ } if (n == 0) { /* 至少得有一台后端吧 */ ... return NGX_ERROR; } /* 创建一个后端集群的实例 */ peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_rr_peers_t)); ... /* 创建后端服务器的实例,总共有n台 */ peer = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_rr_peer_t) * n); ... /* 初始化集群 */ peers->single = (n == 1); /* 是否只有一台后端 */ peers->number = n; /* 后端服务器的数量 */ peers->weight = (w != n); /* 是否使用权重 */ peers->total_weight = w; /* 所有后端服务器权重的累加值 */ peers->name = &us->host; /* upstream配置块的名称 */ n = 0; peerp = &peers->peer; /* 初始化代表后端的结构体ngx_http_upstream_peer_t. * server指令后跟的是域名的话,可能对应多台后端. */ for(i = 0; i < us->servers->nelts; i++) { if (server[i].backup) continue; for (j = 0; j < server[i].naddrs; j++) { peer[n].sockaddr = server[i].addrs[j].sockaddr; /* 后端服务器的地址 */ peer[n].socklen = server[i].addrs[j].socklen; /* 地址的长度*/ peer[n].name = server[i].addrs[j].name; /* 后端服务器地址的字符串 */ peer[n].weight = server[i].weight; /* 配置项指定的权重,固定值 */ peer[n].effective_weight = server[i].weight; /* 有效的权重,会因为失败而降低 */ peer[n].current_weight = 0; /* 当前的权重,动态调整,初始值为0 */ peer[n].max_fails = server[i].max_fails; /* "一段时间内",最大的失败次数,固定值 */ peer[n].fail_timeout = server[i].fail_timeout; /* "一段时间"的值,固定值 */ peer[n].down = server[i].down; /* 服务器永久不可用的标志 */ peer[n].server = server[i].name; /* server的名称 */ /* 把后端服务器组成一个链表,第一个后端的地址保存在peers->peer */ *peerp = &peer[n]; peerp = &peer[n].next; n++; } } us->peer.data = peers; /* 保存后端集群的地址 */ } /* backup servers */ /* 创建和初始化备份集群,peers->next指向备份集群,和上述流程类似,不再赘述 */ ... /* an upstream implicitly defined by proxy_pass, etc. */ /* 如果直接使用proxy_pass指令,没有定义upstream配置块 */ if (us->port == 0) { ... return NGX_ERROR; } ngx_memzero(&u, sizeof(ngx_url_t)); u.host = us->host; u.port = us->port; /* 根据URL解析域名 */ if (ngx_inet_resolve_host(cf->pool, &u) != NGX_OK) { ... return NGX_ERROR; } n = u.naddrs; /* 共有n个后端 */ /* 接下来创建后端集群,并进行初始化,和上述流程类似,这里不再赘述 */ ... return NGX_OK; } ~~~   **初始化请求的负载均衡数据**   当收到一个请求后,一般使用的反向代理模块(upstream模块)为ngx_http_proxy_module, 其NGX_HTTP_CONTENT_PHASE阶段的处理函数为ngx_http_proxy_handler,在初始化upstream机制的 函数ngx_http_upstream_init_request中,调用在第二步中指定的peer.init,主要用于: 创建和初始化该请求的负载均衡数据块 指定r->upstream->**peer.get**,用于从集群中选取一台后端服务器(这是我们最为关心的) 指定r->upstream->**peer.free**,当不用该后端时,进行数据的更新(不管成功或失败都调用) 指定r->upstream->peer.tries,请求最多允许尝试这么多个后端 ~~~ ngx_int_t ngx_http_upstream_init_round_robin_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us) { ngx_uint_t n; ngx_http_upstream_rr_peer_data_t *rrp; /* 创建请求的负载均衡数据块 */ rrp = r->upstream->peer.data; if (rrp == NULL) { rrp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_rr_peer_data_t)); if (rrp == NULL) return NGX_ERROR; r->upstream->peer.data = rrp; /* 保存请求负载均衡数据的地址 */ } rrp->peers = us->peer.data; /* upstream块的后端集群 */ rrp->current = NULL; n = rrp->peers->number; /* 后端的数量 */ /* 如果存在备份集群,且其服务器数量超过n */ if (rrp->peers->next && rrp->peers->next->number > n) { n = rrp->peers->next->number; } /* rrp->tried指向后端服务器的位图,每一位代表一台后端的状态,0表示可用,1表示不可用。 * 如果后端数较少,直接使用rrp->data作为位图。如果后端数较多,则需要申请一块内存。 */ if (n <= 8 *sizeof(uintptr_t)) { rrp->tried = &rrp->data; rrp->data = 0; } else { n = ( n + (8 * sizeof(uintptr_t) - 1)) / (8 * sizeof(uintptr_t)); /* 向上取整 */ rrp->tried = ngx_pcalloc(r->pool, n * sizeof(uintptr_t)); if (rrp->tried == NULL) { return NGX_ERROR; } } r->upstream->peer.get = ngx_http_upstream_get_round_robin_peer; /* 指定peer.get,用于从集群中选取一台后端服务器 */ r->upstream->peer.free = ngx_http_upstream_free_round_robin_peer; /* 指定peer.free,当不用该后端时,进行数据的更新 */ r->upstream->peer.tries = ngx_http_upstream_tries(rrp->peers); /* 指定peer.tries,是请求允许尝试的后端服务器个数 */ ... return NGX_OK; } #define ngx_http_upstream_tries(p) ((p)->number + ((p)->next ? (p)->next->number : 0)) ~~~   **选取一台后端服务器**   一般upstream块中会有多台后端,那么对于本次请求,要选定哪一台后端呢? 这时候第三步中r->upstream->peer.get指向的函数就派上用场了: 采用加权轮询算法,从集群中选出一台后端来处理本次请求。 选定后端的地址保存在pc->sockaddr,pc为主动连接。 函数的返回值: NGX_DONE:选定一个后端,和该后端的连接已经建立。之后会直接发送请求。 NGX_OK:选定一个后端,和该后端的连接尚未建立。之后会和后端建立连接。 NGX_BUSY:所有的后端(包括备份集群)都不可用。之后会给客户端发送502(Bad Gateway)。 ~~~ ngx_int_t ngx_http_upstream_get_round_robin_peer(ngx_peer_connection_t *pc, void *data) { ngx_http_upstream_rr_peer_data_t *rrp = data; /* 请求的负载均衡数据 */ ngx_int_t rc; ngx_uint_t i, n; ngx_http_upstream_rr_peer_t *peer; ngx_http_upstream_rr_peers_t *peers; ... pc->cached = 0; pc->connection = NULL; peers = rrp->peers; /* 后端集群 */ ... /* 如果只有一台后端,那就不用选了 */ if (peers->single) { peer = peers->peer; if (peer->down) goto failed; rrp->current = peer; } else { /* there are several peers */ /* 调用ngx_http_upstream_get_peer来从后端集群中选定一台后端服务器 */ peer = ngx_http_upstream_get_peer(rrp); if (peer == NULL) goto failed; ... } /* 保存选定的后端服务器的地址,之后会向这个地址发起连接 */ pc->sockaddr = peer->sockaddr; pc->socklen = peer->socklen; pc->name = &peer->name; peer->conns++; /* 增加选定后端的当前连接数 */ ... return NGX_OK; failed: /* 如果不能从集群中选取一台后端,那么尝试备用集群 */ if (peers->next) { ... rrp->peers = peers->next; n = (rrp->peers->number + (8 * sizeof(uintptr_t) - 1)) / (8 * sizeof(uintptr_t)); for (i = 0; i < n; i++) rrp->tried[i] = 0; /* 重新调用本函数 */ rc = ngx_http_upstream_get_round_robin_peer(pc, rrp); if (rc != NGX_BUSY) return rc; } /* all peers failed, mark them as live for quick recovery */ for (peer = peers->peer; peer; peer = peer->next) { peer->fails = 0; } pc->name = peers->name; return NGX_BUSY; } ~~~ ngx_http_upstream_get_peer用于从集群中选取一台后端服务器。 ~~~ static ngx_http_upstream_rr_peer_t *ngx_http_upstream_get_peer(ngx_http_upstream_rr_peer_data_t *rrp) { time_t now; uintptr_t m; ngx_int_t total; ngx_uint_t i, n, p; ngx_http_upstream_rr_peer_t *peer, *best; now = ngx_time(); best = NULL; total = 0; ... /* 遍历集群中的所有后端 */ for (peer = rrp->peers->peer, i = 0; peer; peer = peer->next, i++) { n = i / (8 * sizeof(uintptr_t)); m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t)); /* 检查该后端服务器在位图中对应的位,为1时表示不可用 */ if (rrp->tried[n] & m) continue; /* 永久不可用的标志 */ if (peer->down) continue; /* 在一段时间内,如果此后端服务器的失败次数,超过了允许的最大值,那么不允许使用此后端了 */ if (peer->max_fails && peer->fails >= peer->max_fails && now - peer->checked <= peer->fail_timeout) continue; peer->current_weight += peer->effective_weight; /* 对每个后端,增加其当前权重 */ total += peer->effective_weight; /* 累加所有后端的有效权重 */ /* 如果之前此后端发生了失败,会减小其effective_weight来降低它的权重。 * 此后在选取后端的过程中,又通过增加其effective_weight来恢复它的权重。 */ if (peer->effective_weight < peer->weight) peer->effective_weight++; /* 选取当前权重最大者,作为本次选定的后端 */ if (best == NULL || peer->current_weight > best->current_weight) { best = peer; p = i; } } if (best == NULL) /* 没有可用的后端 */ return NULL; rrp->current = best; /* 保存本次选定的后端 */ n = p / (8 * sizeof(uintptr_t)); m = (uintptr_t) 1 << p % (8 * sizeof(uintptr_t)); /* 对于本次请求,如果之后需要再次选取后端,不能再选取这个后端了 */ rrp->tried[n] |= m; best->current_weight -= total; /* 选定后端后,需要降低其当前权重 */ /* 更新checked时间 */ if (now - best->checked > best->fail_timeout) best->checked = now; return best; } ~~~ **释放一台后端服务器**   当不再使用一台后端时,需要进行收尾处理,比如统计失败的次数。 这时候会调用第三步中r->upstream->peer.get指向的函数。函数参数state的取值: 0,请求被成功处理 NGX_PEER_FAILED,连接失败 NGX_PEER_NEXT,连接失败,或者连接成功但后端未能成功处理请求   一个请求允许尝试的后端数为pc->tries,在第三步中指定。当state为后两个值时: 如果pc->tries不为0,需要重新选取一个后端,继续尝试,此后会重复调用r->upstream->peer.get。 如果pc->tries为0,便不再尝试,给客户端返回502错误码(Bad Gateway)。 ~~~ void ngx_http_upstream_free_round_robin_peer(ngx_peer_connection_t *pc, void *data, ngx_uint_t state) { ngx_http_upstream_rr_peer_data_t *rrp = data; /* 请求的负载均衡数据 */ time_t now; ngx_http_upstream_rr_peer_t *peer; ... peer = rrp->current; /* 当前使用的后端服务器 */ if (rrp->peers->single) { peer->conns--; /* 减少后端的当前连接数 */ pc->tries = 0; /* 不能再继续尝试了 */ return; } /* 如果连接后端失败了 */ if (state & NGX_PEER_FAILED) { now = ngx_time(); peer->fails++; /* 一段时间内,已经失败的次数 */ peer->accessed = now; /* 最近一次失败的时间点 */ peer->checked = now; /* 用于检查是否超过了“一段时间” */ /* 当后端出错时,降低其有效权重 */ if (peer->max_fails) peer->effective_weight -= peer->weight / peer->max_fails; /* 有效权重的最小值为0 */ if (peer->effective_weight < 0) peer->effective_weight = 0; } else { /* mark peer live if check passed */ /* 说明距离最后一次失败的时间点,已超过fail_timeout了,清零fails */ if (peer->accessed < peer->checked) peer->fails = 0; } peer->conns--; /* 更新后端的当前连接数 */ if (pc->tries) pc->tries--; /* 对于一个请求,允许尝试的后端个数 */ } ~~~   **判断后端是否可用**   **相关的变量的定义** ngx_uint_t fails; /* 一段时间内,已经失败的次数 */ time_t accessed; /* 最近一次失败的时间点 */ time_t checked; /* 用于检查是否超过了“一段时间” */ ngx_uint_t max_fails; /* 一段时间内,允许的最大的失败次数,固定值 */ time_t fail_timeout; /* “一段时间”的长度,固定值 */ ~~~ ngx_http_upstream_get_peeer /* 在一段时间内,如果此后端服务器的失败次数,超过了允许的最大值, * 那么在此后的一段时间内不允许使用此后端了。 */ if (peer->max_fails && peer->fails >= peer->max_fails && now - peer->checked <= peer->fail_timeout) continue; ... /* 选定本后端了 */ if (now - best->checked > best->fail_timeout) best->checked = now; ~~~ ~~~ ngx_http_upstream_free_round_robin_peer if (state & NGX_PEER_FAILED) { peer->fails++; peer->accessed = now; peer->checked = now; ... } else if (peer->accessed < peer->checked) peer->fails = 0; ~~~ 相关变量的更新 accessed:释放peer时,如果发现后端出错了,则更新为now。 checked:释放peer时,如果发现后端出错了,则更新为now。选定该peer时,如果now - checked > fail_timeout,则更新为now。 fails:释放peer时,如果本次成功了且accessed < checked,说明距离最后一次失败的时间点,已超过fail_timeout了,清零fails。   **上述变量的准备定义** fails并不是“一段时间内”的失败次数,而是两两间时间间隔小于“一段时间”的连续失败次数。 max_fails也不是“一段时间内”允许的最大失败次数,而是两两间的时间间隔小于“一段时间”的最大失败次数。 举例说明,假设fail_timeout为10s,max_fails为3。 10s内失败3次,肯定会导致接下来的10s不可用。 27s内失败3次,也可能导致接下来的10s不可用,只要3次失败两两之间的时间间隔为9s。   下图用来简要说明    ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-22_56f0c8a54d085.jpg)    
';

加权轮询 (Weighted Round Robin) 上篇

最后更新于:2022-04-01 10:13:38

Nginx版本:1.9.1 我的博客:[http://blog.csdn.net/zhangskd](http://blog.csdn.net/zhangskd)   **算法介绍**   来看一个简单的Nginx负载均衡配置。 ~~~ http { upstream cluster { server a weight=5; server b weight=1; server c weight=1; } server { listen 80; location / { proxy_pass http://cluster; } } } ~~~ 当在upstream配置块中没有指定使用的负载均衡算法时,默认使用的是加权轮询。 按照上述配置,Nginx每收到7个客户端的请求,会把其中的5个转发给后端a,把其中的1个转发给后端b, 把其中的1个转发给后端c。   这就是所谓的加权轮询,看起来很简单,但是最早使用的加权轮询算法有个问题,就是7个请求对应的 后端序列是这样的:{ c, b, a, a, a, a, a },会有5个连续的请求落在后端a上,分布不太均匀。 目前使用的加权轮询叫做平滑的加权轮询(smooth weighted round-robin balancing),它和前者的区别是: 每7个请求对应的后端序列为 { a, a, b, a, c, a, a },转发给后端a的5个请求现在分散开来,不再是连续的。   摘录此算法的描述: On each peer selection we increase current_weight of each eligible peer by its weight, select peer with greatest current_weight and reduce its current_weight by total number of weight points distributed among peers. To preserve weight reduction in case of failures the effective_weight variable was introduced, which usually matches peer's weight, but is reduced temoprarily on peer failures.[1]   每个后端peer都有三个权重变量,先解释下它们的含义。 **(1) weight** 配置文件中指定的该后端的权重,这个值是固定不变的。 **(2) effective_weight** 后端的有效权重,初始值为weight。 在释放后端时,如果发现和后端的通信过程中发生了错误,就减小effective_weight。 此后有新的请求过来时,在选取后端的过程中,再逐步增加effective_weight,最终又恢复到weight。 之所以增加这个字段,是为了当后端发生错误时,降低其权重。 **(3) current_weight** 后端目前的权重,一开始为0,之后会动态调整。那么是怎么个动态调整呢? 每次选取后端时,会遍历集群中所有后端,对于每个后端,让它的current_weight增加它的effective_weight, 同时累加所有后端的effective_weight,保存为total。 如果该后端的current_weight是最大的,就选定这个后端,然后把它的current_weight减去total。 如果该后端没有被选定,那么current_weight不用减小。   弄清了三个weight字段的含义后,加权轮询算法可描述为: 1. 对于每个请求,遍历集群中的所有可用后端,对于每个后端peer执行:     peer->current_weight += peer->effecitve_weight。     同时累加所有peer的effective_weight,保存为total。 2. 从集群中选出current_weight最大的peer,作为本次选定的后端。 3. 对于本次选定的后端,执行:peer->current_weight -= total。   上述描述可能不太直观,来看个例子。 现在使用以下的upstream配置块: upstream backend {     server a weight=4;     server b weight=2;     server c weight=1; } 按照这个配置,每7个客户端请求中,a会被选中4次、b会被选中2次、c会被选中1次,且分布平滑。 我们来算算看是不是这样子的。 initial current_weight of a, b, c is { 0, 0, 0 } ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-22_56f0c8a51631d.jpg)   通过上述过程,可得以下结论: 1. 7个请求中,a、b、c分别被选取了4、2、1次,符合它们的权重值。 2. 7个请求中,a、b、c被选取的顺序为a, b, a, c, a, b, a,分布均匀,权重大的后端a没有被连续选取。 3. 每经过7个请求后,a、b、c的current_weight又回到初始值{ 0, 0, 0 },因此上述流程是不断循环的。 这个平滑的加权轮询算法背后应该有数学论证,这里就不继续研究了:)   **本模块的数据结构**   ngx_http_upstream_rr_peer_t   表示一台后端服务器。peer就是对端,指的是上游服务器端。 ~~~ struct ngx_http_upstream_rr_peer_s { struct sockaddr *sockaddr; /* 后端服务器的地址 */ socklen_t socklen; /* 地址的长度*/ ngx_str_t name; /* 后端服务器地址的字符串,server.addrs[i].name */ ngx_str_t server; /* server的名称,server.name */ ngx_int_t current_weight; /* 当前的权重,动态调整,初始值为0 */ ngx_int_t effective_weight; /* 有效的权重,会因为失败而降低 */ ngx_int_t weight; /* 配置项指定的权重,固定值 */ ngx_uint_t conns; /* 当前连接数 */ ngx_uint_t fails; /* "一段时间内",已经失败的次数 */ time_t accessed; /* 最近一次失败的时间点 */ time_t checked; /* 用于检查是否超过了"一段时间" */ ngx_uint_t max_fails; /* "一段时间内",最大的失败次数,固定值 */ time_t fail_timeout; /* "一段时间"的值,固定值 */ ngx_uint_t down; /* 服务器永久不可用的标志 */ ... ngx_http_upstream_rr_peer_t *next; /* 指向下一个后端,用于构成链表 */ ... } ngx_http_upstream_rr_peer_t; ~~~   ngx_http_upstream_rr_peers_t 表示一组后端服务器,比如一个后端集群。 ~~~ struct ngx_http_upstream_rr_peers_s { ngx_uint_t number; /* 后端服务器的数量 */ ... ngx_uint_t total_weight; /* 所有后端服务器权重的累加值 */ unsigned single:1; /* 是否只有一台后端服务器 */ unsigned weighted:1; /* 是否使用权重 */ ngx_str_t *name; /* upstream配置块的名称 */ ngx_http_upstream_rr_peers_t *next; /* backup服务器集群 */ ngx_http_upstream_rr_peer_t *peer; /* 后端服务器组成的链表 */ }; ~~~ ngx_http_upstream_rr_peer_data_t 保存每个请求的负载均衡数据。 ~~~ typedef struct { ngx_http_upstream_rr_peers_t *peers; /* 后端集群 */ ngx_http_upstream_rr_peer_t *current; /* 当前使用的后端服务器 */ uintptr_t *tried; /* 指向后端服务器的位图 */ uintptr_t data; /* 当后端服务器的数量较少时,用于存放其位图 */ } ngx_http_upstream_rr_peer_data_t; ~~~   **通用的数据结构**   以下是所有负载均衡模块都会使用到的一些数据结构。 ngx_http_upstream_server_t   表示upstream配置块中的一条server指令。 ~~~ typedef struct { ngx_str_t name; /* 服务器的名称 */ ngx_addr_t *addrs; /* 服务器地址的数组,因为同一个域名可能解析为多个IP */ ngx_uint_t naddrs; /* 服务器地址数组的元素个数 */ ngx_uint_t weight; /* 服务器的权重 */ ngx_uint_t max_fails; /* 一段时间内,访问失败的次数超过此值,判定服务器不可用 */ time_t fail_timeout; /* 上述“一段时间”的长度 */ unsigned down:1; /* 服务器不可用的标志 */ unsigned backup:1; /* 服务器为备用的标志 */ } ngx_http_upstream_server_t; ~~~   **server指令** Syntax: server address [parameters]; Context: upstream Defines the address and other parameters of a server. The address can be specified as domain name or IP address, with an optional port, or... If a port is not specified, the port 80 is used. A domain name that resolves to serveral IP addresses defines multiple servers at once.   **server指令支持如下参数** *weight* = number     sets the weight of the server, by default 1. *max_fails* = number     By default, the number of unsuccessful attempts is set to 1.     The zero value disables the accounting of attempts. *fail_timout* = number     By default it is set to 10 seconds. *backup*     marks the server as a backup server. *down*     marks the server as permanently unavailable.   ngx_peer_connection_t   表示本机和后端的连接,也叫主动连接,用于upstream机制。 ~~~ struct ngx_peer_connection_s { ngx_connection_t *connection; /* 后端连接 */ struct sockaddr *sockaddr; /* 后端服务器的地址 */ socklen_t socklen; /* 后端服务器地址的长度 */ ngx_str_t *name; /* 后端服务器的名称 */ ngx_uint_t tries; /* 对于一个请求,允许尝试的后端服务器个数 */ ngx_event_get_peer_pt get; /* 负载均衡模块实现,用于选取一个后端服务器 */ ngx_event_free_peer_pt free; /* 负载均衡模块实现,用于释放一个后端服务器 */ void *data; /* 请求的负载均衡数据,一般指向ngx_http_upstream_<name>_peer_data_t */ ... ngx_addr_t *local; /* 本机地址 */ int rcvbuf; /* 套接字接收缓冲区的大小 */ ngx_log_t *log; unsigned cached:1; unsigned log_error:2; }; ~~~ ngx_peer_connection_t *pc; pc->get 就是负载均衡模块中,用于选取后端服务器的函数。 当选定一台后端服务器时,把它的地址信息保存在pc->sockaddr、pc->socklen、pc->name。 pc->tries表示对于一个请求,最多能尝试多少个后端。当尝试一个后端失败时,会调用pc->free, 一个主要目的就是更新pc->tries,比如pc->tries--。如果pc->tries降到0,就不再尝试了。 在请求的负载均衡数据初始化函数peer.init中,会给该请求创建一个ngx_http_upstream_<name>_peer_data_t实例, 用于保存该请求的负载均衡数据,pc->data就是该实例的地址。   **ngx_http_upstream_peer_t**   保存upstream块的数据,是负载均衡中一个很重要的结构体。 ~~~ typedef struct { /* upstream块的初始化函数,ngx_http_upstream_module创建main配置时调用。 * 针对每个upstream块。 */ ngx_http_upstream_init_pt init_upstream; /* request在初始化upstream机制时调用,初始化该请求的负载均衡数据。 * 针对每个request。 */ ngx_http_upstream_init_peer_pt init; void *data; /* 保存upstream块的数据 */ } ngx_http_upstream_peer_t; ~~~ upstream块的数据,在解析配置文件时就创建和初始化了。 如果写了一个新的负载均衡模块,则需要在它的指令解析函数中指定init_upstream的值, 用来创建和初始化包含该指令的upstream配置块的数据。   **ngx_http_upstream_srv_conf_t**   ngx_http_upstream_module的server块。 ~~~ struct ngx_http_upstream_srv_conf_s { ngx_http_upstream_peer_t peer; /* upstream块的数据 */ void **srv_conf; /* 所有HTTP模块的server conf */ ngx_array_t *server; /* upstream块的server数组,元素类型为ngx_http_upstream_server_t */ ngx_uint_t flags; /* upstream块的server指令支持的参数 */ ngx_str_t host; /* upstream块的名称 */ u_char *file_name; ngx_uint_t line; in_port_t port; /* 使用的端口 */ in_port_t default_port; /* 默认的端口 */ ngx_uint_t no_port; ... }; #define ngx_http_conf_upstream_srv_conf(uscf, module) uscf->srv_conf[module.ctx_index] ~~~   **Reference**   [1].[https://github.com/phusion/nginx/commit/27e94984486058d73157038f7950a0a36ecc6e35](https://github.com/phusion/nginx/commit/27e94984486058d73157038f7950a0a36ecc6e35)  
';

整体架构

最后更新于:2022-04-01 10:13:36

Nginx版本:1.9.1 我的博客:[http://blog.csdn.net/zhangskd](http://blog.csdn.net/zhangskd) **什么是负载均衡** 我们知道单台服务器的性能是有上限的,当流量很大时,就需要使用多台服务器来共同提供服务,这就是所谓的集群。 负载均衡服务器,就是用来把经过它的流量,按照某种方法,分配到集群中的各台服务器上。这样一来不仅可以承担 更大的流量、降低服务的延迟,还可以避免单点故障造成服务不可用。一般的反向代理服务器,都具备负载均衡的功能。 负载均衡功能可以由硬件来提供,比如以前的F5设备。也可以由软件来提供,LVS可以提供四层的负载均衡(利用IP和端口), Haproxy和Nginx可以提供七层的负载均衡(利用应用层信息)。   来看一个最简单的Nginx负载均衡配置。 ~~~ http { upstream cluster { server srv1; server srv2; server srv3; } server { listen 80; location / { proxy_pass http://cluster; } } } ~~~ 通过上述配置,Nginx会作为HTTP反向代理,把访问本机的HTTP请求,均分到后端集群的3台服务器上。 此时使用的HTTP反向代理模块是ngx_http_proxy_module。 一般在upstream配置块中要指明使用的负载均衡算法,比如hash、ip_hash、least_conn。 这里没有指定,所以使用了默认的HTTP负载均衡算法 - 加权轮询。   **负载均衡流程图**   在描述负载均衡模块的具体实现前,先来看下它的大致流程:   ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-22_56f0c89f13acb.jpg)   **负载均衡模块**   Nginx目前提供的负载均衡模块: ngx_http_upstream_round_robin,加权轮询,可均分请求,是默认的HTTP负载均衡算法,集成在框架中。 ngx_http_upstream_ip_hash_module,IP哈希,可保持会话。 ngx_http_upstream_least_conn_module,最少连接数,可均分连接。 ngx_http_upstream_hash_module,一致性哈希,可减少缓存数据的失效。   以上负载均衡模块的实现,基本上都遵循一套相似的流程。   **1. 指令的解析函数** 比如least_conn、ip_hash、hash指令的解析函数。 这些函数在解析配置文件时调用,主要用于: 检查指令参数的合法性 指定peer.init_upstream函数指针的值,此函数用于初始化upstream块。   **2. 初始化upstream块** 在执行完指令的解析函数后,紧接着会调用所有HTTP模块的init main conf函数。 在执行ngx_http_upstream_module的init main conf函数时,会调用所有upstream块的初始化函数, 即在第一步中指定的peer.init_upstream,主要用于: 创建和初始化后端集群,保存该upstream块的数据 指定peer.init,此函数用于初始化请求的负载均衡数据   来看下ngx_http_upstream_module。 ~~~ ngx_http_module_t ngx_http_upstream_module_ctx = { ... ngx_http_upstream_init_main_conf, /* init main configuration */ ... }; ~~~ ~~~ static char *ngx_http_upstream_init_main_conf(ngx_conf_t *cf, void *conf) { ... /* 数组的元素类型是ngx_http_upstream_srv_conf_t */ for (i = 0; i < umcf->upstreams.nelts; i++) { /* 如果没有指定upstream块的初始化函数,默认使用round robin的 */ init = uscfp[i]->peer.init_upstream ? uscfp[i]->peer.init_upstream : ngx_http_upstream_init_round_robin; if (init(cf, uscfp[i] != NGX_OK) { return NGX_CONF_ERROR; } } ... } ~~~ **3. 初始化请求的负载均衡数据块** 当收到一个请求后,一般使用的反向代理模块(upstream模块)为ngx_http_proxy_module, 其NGX_HTTP_CONTENT_PHASE阶段的处理函数为ngx_http_proxy_handler,在初始化upstream机制的 函数ngx_http_upstream_init_request中,调用在第二步中指定的peer.init,主要用于: 创建和初始化该请求的负载均衡数据块 指定r->upstream->peer.get,用于从集群中选取一台后端服务器(这是我们最为关心的) 指定r->upstream->peer.free,当不用该后端时,进行数据的更新(不管成功或失败都调用)   请求的负载均衡数据块中,一般会有一个成员指向对应upstream块的数据,除此之外还会有自己独有的成员。 "The peer initialization function is called once per request. It sets up a data structure that the module will use as it tries to find an appropriate backend server to service that request; this structure is persistent across backend re-tries, so it's a convenient place to keep track of the number of connection failures, or a computed hash value. By convention, this struct is called ngx_http_upstream_<module_name>_peer_data_t." **4. 选取一台后端服务器** 一般upstream块中会有多台后端,那么对于本次请求,要选定哪一台后端呢? 这时候第三步中r->upstream->peer.get指向的函数就派上用场了: 采用特定的算法,比如加权轮询或一致性哈希,从集群中选出一台后端,处理本次请求。  选定后端的地址保存在pc->sockaddr,pc为主动连接。 函数的返回值: NGX_DONE:选定一个后端,和该后端的连接已经建立。之后会直接发送请求。 NGX_OK:选定一个后端,和该后端的连接尚未建立。之后会和后端建立连接。 NGX_BUSY:所有的后端(包括备份集群)都不可用。之后会给客户端发送502(Bad Gateway)。   **5. 释放一台后端服务器** 当不再使用一台后端时,需要进行收尾处理,比如统计失败的次数。 这时候会调用第三步中r->upstream->peer.get指向的函数。 函数参数state的取值: 0,请求被成功处理 NGX_PEER_FAILED,连接失败 NGX_PEER_NEXT,连接失败,或者连接成功但后端未能成功处理请求   一个请求允许尝试的后端数为pc->tries,在第三步中指定。当state为后两个值时: 如果pc->tries不为0,需要重新选取一个后端,继续尝试,此后会重复调用r->upstream->peer.get。 如果pc->tries为0,便不再尝试,给客户端返回502错误码(Bad Gateway)。   **在upstream模块的回调**   负载均衡模块的功能是从后端集群中选取一台后端服务器,而具体的反向代理功能是由upstream模块实现的, 比如和后端服务器建立连接、向后端服务器发送请求、处理后端服务器的响应等。 我们来看下负载均衡模块提供的几个钩子函数,是在upstream模块的什么地方回调的。   Nginx的HTTP反向代理模块为ngx_http_proxy_module,其NGX_HTTP_CONTENT_PHASE阶段的处理函数为 ngx_http_proxy_handler,每个请求的upstream机制是从这里开始的。 ~~~ ngx_http_proxy_handler ngx_http_upstream_create /* 创建请求的upstream实例 */ ngx_http_upstream_init /* 启动upstream机制 */ ngx_htp_upstream_init_request /* 负载均衡模块的入口 */ uscf->peer.init(r, uscf) /* 第三步,初始化请求的负载均衡数据块 */ ... ngx_http_upstream_connect /* 可能会被ngx_http_upstream_next重复调用 */ ngx_event_connect_peer(&u->peer); /* 连接后端 */ pc->get(pc, pc->data); /* 第四步,从集群中选取一台后端 */ ... /* 和后端建连成功后 */ c = u->peer.connection; c->data = r; c->write->handler = ngx_http_upstream_handler; /* 注册的连接的读事件处理函数 */ c->read->handler = ngx_http_upstream_handler; /* 注册的连接的写事件处理函数 */ u->write_event_handler = ngx_http_upstream_send_request_handler; /* 写事件的真正处理函数 */ u->read_event_handler = ngx_http_upstream_process_header; /* 读事件的真正处理函数 */ ~~~ 选定后端之后,在和后端通信的过程中如果发生了错误,会调用ngx_http_upstream_next来继续尝试其它的后端。 ~~~ ngx_http_upstream_next if (u->peer.sockaddr) { if (ft_type == NGX_HTTP_UPSTREAM_FT_HTTP_403 || ft_type == NGX_HTTP_UPSTREAM_FT_HTTP_404) state = NGX_PEER_NEXT; else state = NGX_PEER_FAILED; /* 第五步,释放后端服务器 */ u->peer.free(&u->peer, u->peer.data, state); u->peer.sockaddr = NULL; } ~~~   **Reference**   [1]. [http://www.evanmiller.org/nginx-modules-guide.html#proxying](http://www.evanmiller.org/nginx-modules-guide.html#proxying) [2]. [http://tengine.taobao.org/book/chapter_05.html#id5](http://tengine.taobao.org/book/chapter_05.html#id5)  
';

前言

最后更新于:2022-04-01 10:13:34

> 原文出处:[Nginx开发](http://blog.csdn.net/column/details/sknginx.html) 作者:[zhangskd](http://blog.csdn.net/zhangskd) **本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!** # Nginx开发 > Nginx的开发和源码分析
';