关注点:HTTP Cookie 的格式;负载均衡 (loadbalancer) 类型模块的编写;Haproxy 是如 何实现类似的功能的;
此模块通过给客户端请求添加 Cookie 并通过检查这个 Cookie 的值判断请求应该发往哪
个后端应用服务器。它的功能和 ip_hash
模块类似,但是有些场景下 ip_hash
不太
合适:
Using a persistance by IP (with the
ip_hash
upstream module) is maybe not a good idea because there could be situations where a lot of different browsers are coming with the same IP address (behind proxies) and the load balancing system won't be fair.Using a cookie to track the upstream server makes each browser unique.
sticky
模块只适合浏览器支持 Cookie 的场景。当 sticky
模块碰到不适用的情况
时,会切换到 Nginx 默认的 Round-Robin 模式 或者 返回 "Bad Gateway" (no_fallback
为 1)。
Cookie 格式
Set-Cookie: name=value;expiry;path;domain;secure;http-only # response
Cookie: name=value; name2=value2 # request
-
Besides the name-value pair, servers can also set those cookie attributes: a cookie domain, a path, expiration time or maximum age, secure flag and httponly flag.
-
Browsers will not send cookie attribute back to the server. They will only send the cookie's name-value pare.
-
Cookie attributes are used by browsers to determine when to delete a cookie, block a cookie or whether to send a cookie to the servers.
模块用法
upstream {
sticky;
server 127.0.0.1:9000;
server 127.0.0.1:9001;
server 127.0.0.1:9002;
}
sticky
指令支持的选项:
sticky [name=route] [domain=] [path=] [expires=1h] [hash=index|md5|sha1] \
[hmac hmac_key=private_key] [no_fallback];
HMAC
is used to simultaneously verify both the data integrity and the authentication of a message. 基本相当于加了 salt 的 hash 算法。
数据结构
-
ngx_http_upstream_main_conf_t
- 存储配置文件所有upstream {}
配置块。typedef struct { ... ngx_array_t upstreams; /* of ngx_http_upstream_srv_conf_t */ } ngx_http_upstream_main_conf_t;
-
ngx_http_upstream_srv_conf_t
- 对应upstream {}
配置块。struct ngx_http_upstream_srv_conf_s { ngx_http_upstream_peer_t peer; /* 此 upstream {} peer 相关操作 */ ... ngx_array_t *servers; /* of ngx_http_upstream_server_t */ ... };
-
ngx_http_upstream_peer_t
- 所属upstream {}
peer 的初始化和选择逻辑。typedef struct { ngx_http_upstream_init_pt init_upstream; ngx_http_upstream_init_peer_pt init; /* 请求相关的后端服务器使用 状态初始化函数 */ void *data; /* 负载均衡相关的运行时数据 */ } ngx_http_upstream_peer_t;
-
init_upstream
在配置解析时由负载均衡模块设置,并在配置解析完成后 在init_main
阶段由ngx_http_upstream_init_main_conf
函数调用,它会将后 端服务器信息构造成负载均衡模块运行时需要使用的数据结构 (hash表、有序数组等)。 -
init_upstream
还将设置init
函数和data
。init
函数用于构造请求相 关的后端服务器使用状态结构体,并设置用于操作 (选择、释放) 后端服务器的回调 函数。
-
-
ngx_http_upstream_server_t
- 对应upstream {}
作用域里的一个server
配 置行信息,其中的后端服务器信息被整理后转存成ngx_http_upstream_rr_peer_t
类型 变量,其中每个此类型变量对应一个ngx_http_upstream_server_t
中的ngx_addr_t
。typedef struct { ngx_addr_t *addrs; 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;
-
ngx_http_upstream_rr_peer_t
- 代表 RR 模式下的一个后端应用服务器信息 (IP, weight, fail等)typedef struct { struct sockaddr *sockaddr; socklen_t socklen; ngx_str_t name; /* 来自配置参数 */ ngx_int_t current_weight; /* 初始值为 0 */ ngx_int_t effective_weight; /* 初始值和 weight 相等 */ ngx_int_t weight; /* 来自配置参数 */ ngx_uint_t fails; /* 此 peer 的失败次数 */ 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;
-
ngx_http_upstream_rr_peers_t
- RR 模式下,所有后端应用服务器信息的存储结构。struct ngx_http_upstream_rr_peers_s { ngx_uint_t number; /* naddrs of primary or backup */ ngx_uint_t total_weight; /* naddrs of primary or backup */ ... /* 指向标识为 backup 类型的后端应用服务器信息 */ ngx_http_upstream_rr_peers_t *next; ngx_http_upstream_rr_peer_t peer[1]; };
负载均衡模块涉及到的相似的数据结构忒多,最后大致绘个结构关系图(sticky
模块
基于Nginx 标配的 Round-Robin 模块,图中把 rr
相关的结构体也一并画出):
ngx_http_upstream_main_conf_t
+ upstreams --> [ ngx_http_upstream_srv_conf_t, ... ]
|
+ servers --> [ ngx_http_upstream_server_t, ... ]
|
+ peer (ngx_http_upstream_peer_t)
+ init_upstream
+ init
+ data (ngx_http_upstream_rr_peers_t)
+ peer (ngx_http_upstream_rr_peer_t)
+ next (ngx_http_upstream_rr_peers_t)
/
backup (ngx_http_upstream_rr_peers_t) if any
在 rr
结构体基础上,sticky
提供了根据摘要值选择后端服务的功能,它在 rr
结
果的基础上,提供了存储摘要信息的结构体:
typedef struct {
ngx_http_upstream_rr_peer_t *rr_peer; /* 指向实际对象 */
ngx_str_t digest;
} ngx_http_sticky_peer_t;
typedef struct {
ngx_str_t cookie_name;
ngx_str_t cookie_domain;
ngx_str_t cookie_path;
time_t cookie_expires;
ngx_str_t hmac_key;
ngx_http_sticky_misc_hash_pt hash;
ngx_http_sticky_misc_hmac_pt hmac;
ngx_uint_t no_fallback;
ngx_http_sticky_peer_t *peers;
} ngx_http_sticky_srv_conf_t;
初始化
负载均衡模块指令只能定义于 upstream {}
作用域中,upstream {}
目前又只能用
于 http {}
中。虽然这个作用域中不能再定义 server {}
或者 location {}
等子
作用域,为了和 http {}
作用域的其它部分保持一致,Nginx 基本上把 upstream {}
作用域等同于 server {}
看待 (基本一样的解析处理方式)。
static char *
ngx_http_upstream(ngx_conf_t *cf, ngx_command_t *cmd, void *dummy)
{
...
ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_conf_ctx_t));
...
ctx->main->conf = http_ctx->main_conf;
ctx->srv_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
...
ctx->srv_conf[ngx_http_upstream_module.ctx_index] = uscf;
uscf->srv_conf = ctx->srv_conf;
ctx->loc_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
...
/* each module's create_srv_conf and create_loc_conf */
pcf = *cf;
cf->ctx = ctx;
cf->cmd_type = NGX_HTTP_UPS_CONF;
rv = ngx_conf_parse(cf, NULL);
*cf = pcf;
...
}
sticky
模块 (以及 Nginx 自带的 ip_hash
模块) 在定义配置指令时,并未指明配
置指令所在模块的配置结构体偏移量,这样的做法会使指令解析处理函数的第三个参数不
可用:
/* cf->ctx */
typedef struct {
void **main_conf;
void **srv_conf;
void **loc_conf;
} ngx_http_conf_ctx_t;
{ ngx_string("sticky"),
NGX_HTTP_UPS_CONF|NGX_CONF_ANY,
ngx_http_sticky_set, /* set */
0, /* conf */
0, /* offset */
NULL },
static ngx_int_t
ngx_conf_handler(ngx_conf_t *cf, ngx_int_t last)
{
...
else if (cf->ctx) {
confp = *(void **) ((char *) cf->ctx + cmd->conf);
if (confp) {
conf = confp[ngx_modules[i]->ctx_index];
}
}
rv = cmd->set(cf, cmd, conf);
...
}
-
cmd->conf
为 NULL 的情况下,confp
实际指向了main_conf
。但是此模块并未 创建HTTP MAIN
级别的配置项,所以conf
值为 NULL。 -
实际上,上一条的分析没有任何意义。因为未设置
ngx_command_t::conf
的时候,就 应该把指令解析处理函数的第三个参数当成无效参数看待。 -
sticky
模块在解析处理函数中使用ngx_http_conf_get_module_srv_conf
函数找 到其HTTP SRV
级别的配置结构体ngx_http_sticky_srv_conf_t
。
ngx_http_sticky_set
是该模块的唯一一个指令处理回调函数,它解析整个指令行,并
设置 ngx_http_sticky_srv_conf_t
结构体;同时,设置上节提到的 init_upstream
函数指针为 ngx_http_init_upstream_sticky
。
配置文件的 http {}
解析完毕后,ngx_http_block
开始调用各个 HTTP 模块的
init_main_conf
回调函数,对模块按刚刚得到的配置进行初始化操作。这时 upstream
模块的 ngx_http_upstream_init_main_conf
调用 ngx_http_init_upstream_sticky
构造后端应用服务器信息的存储结构:
for (i = 0; i < umcf->upstreams.nelts; i++) {
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;
}
}
static ngx_int_t
ngx_http_init_upstream_sticky(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)
{
ngx_uint_t i;
ngx_http_sticky_srv_conf_t *conf;
ngx_http_upstream_rr_peers_t *rr_peers;
...
ngx_http_upstream_init_round_robin(cf, us);
...
rr_peers = us->peer.data;
...
us->peer.init = ngx_http_init_sticky_peer;
conf = ngx_http_conf_upstream_srv_conf(us, ngx_http_sticky_module);
...
/* if 'index', no need to alloc and generate digest */
if (!conf->hash && !conf->hmac) {
conf->peers = NULL;
return NGX_OK;
}
conf->peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_sticky_peer_t)
* rr_peers->number);
...
for (i = 0; i < rr_peers->number; i++) {
conf->peers[i].rr_peer = &&rr_peers->peer[i];
if (conf->hmac) {
conf->hmac(cf->poo., rr_peers->peer[i].sockaddr,
rr_peers->peer[i].socklen, &conf->hmac_key,
&conf->peers[i].digest);
} else {
conf->hash(cf->pool), rr_peers->peer[i].sockaddr,
rr->peers->peer[i].socklen, &conf->peers[i].digest);
}
}
return NGX_OK;
}
到此,sticky
模块初始化过程结束。
获取Peers信息
负载均衡的目的就是将不同的请求按一定的规则 (rr
, ip_hash
, sticky
等) 分发
到不同的后端应用服务器上,所以,每个独立的请求都需要维护 Nginx 提供给自己的可用
后端地址,以继续后面的操作 (连接、请求、接收响应)。
每个请求和负载均衡相关的数据结构如下:
struct ngx_http_request_s {
...
ngx_http_upstream_t *upstream;
...
};
struct ngx_http_upstream_s {
...
ngx_peer_connection_t peer;
...
};
struct ngx_peer_connection_s {
ngx_connection_t *connection;
...
ngx_uint_t tries; /* rrp->peers->number */
ngx_event_get_peer_pt get; /* ngx_http_get_sticky_peer */
ngx_event_free_peer_pt free; /* ngx_http_upstream_free_round_robin_peer */
void *data; /* ngx_http_sticky_peer_data_t */
...
};
typedef struct {
ngx_http_upstream_rr_peers_t *peers; /* 所有后端应用服务器信息 */
ngx_uint_t current;
uintptr_t *tried; /* bitmap,bit 值表示该请求是否
已经尝试过对应的 peer */
uintptr_t data;
} ngx_http_upstream_rr_peer_data_t;
typedef struct {
ngx_http_upstream_rr_peer_data_t rrp; /* fallback 的实现保障 */
ngx_event_get_peer_pt get_rr_peer; /* 备份 RR 的 get 方法 */
ngx_int_t selected_peer;
ngx_uint_t no_fallback;
ngx_http_sticky_srv_conf_t *sticky_conf;
ngx_http_request_t *request;
} ngx_http_sticky_peer_data_t;
上面的这几个结构体由 upstream
模块在准备后端连接 (ngx_http_upstream_init
)
时创建和初始化,其中 ngx_peer_connection_t
的部分和负载均衡相关的成员变量由
上面提到的各负载均衡模块实现的 init_upstream
完成:
static void
ngx_http_upstream_init_request(ngx_http_request_t *r)
{
...
if (uscf->peer.init(r, uscf) != NGX_OK) {
...
}
ngx_http_upstream_connect(r, u);
/* TODO */
}
static ngx_int_t
ngx_http_init_sticky_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us)
{
ngx_http_sticky_peer_data_t *iphp;
...
iphp = ngx_palloc(r->pool, sizeof(ngx_http_sticky_peer_data_t));
...
r->upstream->peer.data = &iphp->rrp;
...
ngx_http_upstream_init_round_robin_peer(r, us);
...
iphp->get_rr_peer = ngx_http_upstream_get_round_robin_peer;
r->upstream->peer.get = ngx_get_sticky_peer;
iphp->selected_peer = -1;
iphp->no_fallback = 0;
...
if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies,
&iphp->sticky_conf->cookie_name,
&route) != NGX_DECLINED)
{
if (iphp->sticky_conf->hash || iphp->sticky_conf->hmac) {
for (i = 0; i < iphp->rrp.peers->number; i++) {
/* bingo */
iphp->selected_peer = i;
return NGX_OK;
}
} else {
n = ngx_atoi(route.data, route.len);
if (n == NGX_ERROR) {
...
} else if (n >= 0 && n < (ngx_int_t) iphp->rrp.peers->number) {
...
iphp->selected_peer = n;
return NGX_OK;
}
}
}
return NGX_OK;
}
对上述代码的补充说明:
-
ngx_http_upstream_init_round_robin_peer
对ngx_http_upstream_rr_peer_data_t
进行初始化:peers
指向保存有全部peer
信息的结构体;tried
初始化为bitmap
,每个bit
的值表示对应的peer
是否被使用过。ngx_int_t ngx_http_upstream_init_round_robin_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us) { ... rrp = r->upstream->peer.data; ... rrp->peers = us->peer.data; rrp->current = 0; ... rrp->tried = /* ... */; }
-
ngx_http_upstream_init_round_robin_peer
对ngx_peer_connection_t
中的回调 函数字段进行赋值。ngx_int_t ngx_http_upstream_init_round_robin_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us) { ... r->upstream->peer.get = ngx_http_upstream_get_round_robin_peer; r->upstream->peer.free = ngx_http_upstream_free_round_robin_peer; r->upstream->peer.tries = rrp->peers->number; ... }
-
根据
request
的 Cookie 值查找对应的peer
,并将其在peers
中编号赋给selected_peer
变量。未能成功匹配到合适的peer
时,selected_peer
值为 -1。 -
ngx_http_sticky_peer_data_t
类型的iphp
存储着sticky
模块的状态,并且 它是在ngx_http_upstream_rr_peer_data_t
类型上的封装,通过r->upstream->peer.data
并进行适当的类型转换就可以引用到。 -
r->upstream->peer.get = ngx_get_sticky_peer;
重新指定了get
函数,并将 RR 模块设置的get
函数保存到iphp->get_rr_peer
中。
经过上面的这些处理,请求 r
就和后端 peer
信息关联了起来。
RR模式
本节介绍一下 sticky
模块依赖的 RR 负载均衡模块相关的字段含义和根据权重进行
peer
选择的算法。
-
peer->down
- 当前peer
是否在配置文件中就被标识为不可用状态。在ip_hash
模式下对下线的服务加上此标识,就不会改变其它peer
的hash
值。 -
pc->tries
- 该请求还可以再尝试连接peer
的次数 (也就是剩余可用的peer
个数)。 -
peer->checked
-peer
最近一次检查点 (记录最近一次失败或成功时间点)。 -
peer->accessed
-peer
最近一次失败时间点。 -
peer->max_fails
- The number of unsuccessful attempts to communicate with the server that should happen in the duration set by thefail_timeout
parameter to consider the server unavailable for a duration also set by thefail_timeout
parameter.-
peer
累计失败次数超过max_fails
并且最近一次失败在fail_timeout
内, 不再选用此peer
if (peer->fails >= peer->max_fails && now - peer->checked <= peer->fail_timeout) { continue; }
-
在
fail_timeout
间隔内,此peer
未失败 (未被选用或成功连接),认为其可 用/* get */ if (now - best->checked > best->fail_timeout) { best->checked = now; }
-
如果此
peer
确实可用 (在fail_timeout
中再未失败),重置其失败计数if (peer->accessed < peer->checked) { peer->fails = 0; }
-
如果此
peer
依然不可用,标记失败状态peer->fails++; peer->accessed = now; peer->checked = now;
-
-
根据权值选择
peer
的算法说明 (1.0.15-1.2.9 之间的某个版本开始使用):changeset 4621:c90801720a0c author Maxim Dounin <mdounin@mdounin.ru> date Mon, 14 May 2012 09:57:20 +0000 (20 months ago) Upstream: smooth weighted round-robin balancing. For edge case weights like { 5, 1, 1 } we now produce { a, a, b, a, c, a, a } sequence instead of { c, b, a, a, a, a, a } produced previously. Algorithm is as follows: 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 abababababababaaadddafdafdsafmong peers. In case of { 5, 1, 1 } weights this gives the following sequence of current_weight's: a b c 0 0 0 (initial state) 5 1 1 (a selected) -2 1 1 (before returning a) 3 2 2 (a selected) -4 2 2 (before returning a) 1 3 3 (b selected) 1 -4 3 (before returning b) 6 -3 4 (a selected) -1 -3 4 (before returning b) 4 -2 5 (c selected) 4 -2 -2 (before returning c) 9 -1 -1 (a selected) 2 -1 -1 (before returning c) 7 0 0 (a selected) 0 0 0 (before returning a) To preserve weight reduction in case of failures the effective_weight variable was introduced, which usually matches peer's weight, but is reduced temporarily on peer failures. 上述算法在不考虑 peer 失败情况的实现: peers->peer[n].effective_weight = server[i].weight; ... for (i = 0; i < rrp->peers->number; i++) { ... peer->current_weight += peer->effective_weight; total += peer->effective_weight; ... if (best == NULL || peer->current_weight > best->current_weight) { best = peer; } } ... best->current_weight -= total;
连接和失败
再回到 sticky
模块,upstream
模块使用 sticky
模块选取到合适的 peer
后,
开始尝试连接 peer
。
peer
选取操作由 r->upstream.peer.get
,即 ngx_http_get_sticky_peer
函数完
成。
如果此请求并未带有任何 sticky
模块能够识别的 Cookie,ngx_http_get_sticky_peer
会调用 RR 模块的 get
方法获取 peer
,这种情况下,sticky
还需要给请求响应添
加新的 Set-Cookie
字段标识此 peer
,以便此客户端下次访问时直接使用。
在 peer
连接失败时,sticky
根据 no_fallback
标志决定是否再次调用 RR 模块
得到另外一个 peer
。
几个事实
-
对任何请求来说,
peer
的选择都按照primary
,backup
的顺序进行。 -
peer
是否失效,由upstream
模块的使用者定义,比如,通过proxy_next_upsteam
。 目前upstream
能够区分的peer
失败类型有:#define NGX_HTTP_UPSTREAM_FT_ERROR 0x00000002 #define NGX_HTTP_UPSTREAM_FT_TIMEOUT 0x00000004 #define NGX_HTTP_UPSTREAM_FT_INVALID_HEADER 0x00000008 #define NGX_HTTP_UPSTREAM_FT_HTTP_500 0x00000010 #define NGX_HTTP_UPSTREAM_FT_HTTP_502 0x00000020 #define NGX_HTTP_UPSTREAM_FT_HTTP_503 0x00000040 #define NGX_HTTP_UPSTREAM_FT_HTTP_504 0x00000080 #define NGX_HTTP_UPSTREAM_FT_HTTP_404 0x00000100 #define NGX_HTTP_UPSTREAM_FT_UPDATING 0x00000200 #define NGX_HTTP_UPSTREAM_FT_BUSY_LOCK 0x00000400 #define NGX_HTTP_UPSTREAM_FT_MAX_WAITING 0x00000800 #define NGX_HTTP_UPSTREAM_FT_NOLIVE 0x40000000 #define NGX_HTTP_UPSTREAM_FT_OFF 0x80000000
-
peer
的失败处理都由ngx_http_upstream_next
完成,它会调用负载均衡模块提供 的free
回调函数。 -
注释太多会影响代码可读性。
遗留问题
sticky
模块未考虑primary
后端peer
全部失效,backup
被启用的情况?sticky
模块还有许多可以修改的地方,留作以后的项目。
Comments
不要轻轻地离开我,请留下点什么...