负载均衡算法解析

1、nginx-轮询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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();

// 初始化最佳服务器为 NULL 和总权重为 0
best = NULL;
total = 0;

// 避免编译器警告,如果未使用变量 p
#if (NGX_SUPPRESS_WARN)
p = 0;
#endif

// 遍历所有后端服务器
for (peer = rrp->peers->peer, i = 0;
peer;
peer = peer->next, i++)
{
// 计算位数组索引和位掩码
n = i / (8 * sizeof(uintptr_t)); // 索引为当前服务器编号除以每个 uintptr_t 能存储的位数
m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t)); // 计算位掩码

// 如果当前服务器已经被尝试过,则跳过
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;
}

// 如果服务器达到最大连接数限制,则跳过
if (peer->max_conns && peer->conns >= peer->max_conns) {
continue;
}

// 增加服务器的当前权重,并将其加入总权重
peer->current_weight += peer->effective_weight;
total += peer->effective_weight;

// 如果服务器的有效权重小于其声明的权重,则增加有效权重
if (peer->effective_weight < peer->weight) {
peer->effective_weight++;
}

// 选择当前权重最高的服务器作为最佳服务器
if (best == NULL || peer->current_weight > best->current_weight) {
best = peer;
p = i; // 记录最佳服务器的索引
}
}

// 如果没有找到合适的服务器,返回 NULL
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;

// 如果当前时间距离最佳服务器的上次检查时间超过失败超时时间,则更新检查时间
if (now - best->checked > best->fail_timeout) {
best->checked = now;
}

// 返回选择的最佳服务器
return best;
}

2、nginx-ip_hash

1
2
3
4
5
6
7
8
9
10
upstream rrBackend {
ip_hash;
server localhost:8001 weight=1;
server localhost:8002 weight=2;
server localhost:8003 weight=3;
}

location /rr {
proxy_pass http://rrBackend;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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;

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

// 对轮询节点的peers进行读锁定
ngx_http_upstream_rr_peers_rlock(iphp->rrp.peers);

// 如果尝试次数超过20次或者只有一个后端节点,则直接返回轮询算法的结果
if (iphp->tries > 20 || iphp->rrp.peers->single) {
ngx_http_upstream_rr_peers_unlock(iphp->rrp.peers);
return iphp->get_rr_peer(pc, &iphp->rrp);
}

now = ngx_time();

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

hash = iphp->hash;

for ( ;; ) {

// 计算哈希值,这里只取地址的前三位
for (i = 0; i < (ngx_uint_t) iphp->addrlen; i++) {
hash = (hash * 113 + iphp->addr[i]) % 6271;
}

// 对总权重取余,使得请求更加均匀的分散到server
w = hash % iphp->rrp.peers->total_weight;
peer = iphp->rrp.peers->peer;
p = 0;

// 遍历peers,找到权重匹配的peer,权重值越大,越容易得到这个请求
while (w >= peer->weight) {
w -= peer->weight;
peer = peer->next;
p++;
}

// 检查这个peer是否被尝试过
n = p / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << p % (8 * sizeof(uintptr_t));

// 如果已经尝试过这个peer,则跳过
if (iphp->rrp.tried[n] & m) {
goto next;
}

ngx_log_debug2(NGX_LOG_DEBUG_HTTP, pc->log, 0,
"get ip hash peer, hash: %ui %04XL", p, (uint64_t) m);

// 对选定的peer进行加锁
ngx_http_upstream_rr_peer_lock(iphp->rrp.peers, peer);

// 如果peer处于down状态,则解锁并跳过
if (peer->down) {
ngx_http_upstream_rr_peer_unlock(iphp->rrp.peers, peer);
goto next;
}

// 如果peer失败次数超过阈值并且检查时间在失败超时时间内,则解锁并跳过
if (peer->max_fails
&& peer->fails >= peer->max_fails
&& now - peer->checked <= peer->fail_timeout)
{
ngx_http_upstream_rr_peer_unlock(iphp->rrp.peers, peer);
goto next;
}

// 如果peer的连接数超过最大连接数限制,则解锁并跳过
if (peer->max_conns && peer->conns >= peer->max_conns) {
ngx_http_upstream_rr_peer_unlock(iphp->rrp.peers, peer);
goto next;
}

// 如果没有上述情况,则选择此peer
break;

next:

// 如果尝试次数超过20次,则返回轮询算法的结果
if (++iphp->tries > 20) {
ngx_http_upstream_rr_peers_unlock(iphp->rrp.peers);
return iphp->get_rr_peer(pc, &iphp->rrp);
}
}

// 设置当前选择的peer
iphp->rrp.current = peer;

// 设置pc结构体的字段,以反映选定的peer
pc->sockaddr = peer->sockaddr;
pc->socklen = peer->socklen;
pc->name = &peer->name;

// 增加peer的连接数
peer->conns++;

// 更新peer的检查时间
if (now - peer->checked > peer->fail_timeout) {
peer->checked = now;
}

// 解锁选定的peer
ngx_http_upstream_rr_peer_unlock(iphp->rrp.peers, peer);
ngx_http_upstream_rr_peers_unlock(iphp->rrp.peers);

// 在trie中标记已经尝试过这个peer
iphp->rrp.tried[n] |= m;
iphp->hash = hash;

// 函数返回成功
return NGX_OK;
}

3、hash

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
upstream myapp {
hash $http_x_real_ip; # 使用 X-Real-IP 头部的值进行哈希
server backend1.example.com weight=1;
server backend2.example.com weight=2;
}

//或者

upstream myapp {
hash $http_x_forwarded_for; # 使用 $http_x_forwarded_for 头部的值进行哈希
server backend1.example.com weight=1;
server backend2.example.com weight=2;
}

当在 Nginx 配置中使用 hash $http_x_forwarded_for; 作为负载均衡的键时,hash用的就是真实客户端ip,ngx_http_complex_value 函数将用于计算 X-Forwarded-For HTTP 请求头的值,并将该值赋给 hp->key。以下是该过程的详细说明:

图3.1

  1. 复杂值初始化:
    • 在 Nginx 配置阶段,当遇到 hash $http_x_forwarded_for; 配置时,相关的配置处理函数(如 ngx_http_upstream_hash)将初始化一个 ngx_http_complex_value_t 结构体,这里即 hcf->key
  2. 请求处理阶段:
    • 当一个请求到达并需要进行上游处理时,ngx_http_upstream_init_hash_peer 函数被调用。
  3. 执行复杂值:
    • ngx_http_complex_value 函数被用来执行 hcf->key 中定义的复杂值,这个复杂值就是 $http_x_forwarded_for
  4. 计算哈希键:
    • ngx_http_complex_value 函数解析 $http_x_forwarded_for,这通常意味着它将获取请求的 X-Forwarded-For 头的值。
  5. 值的变化:
    • 在执行 ngx_http_complex_value(r, &hcf->key, &hp->key) 之前,hp->key 是未初始化的。
    • 执行后,如果函数返回 NGX_OK,则 hp->key 将包含 X-Forwarded-For 头的值,这可能是一个单一的 IP 地址或者一个 IP 地址列表,具体取决于 X-Forwarded-For 头的内容。
  6. 错误处理:
    • 如果 ngx_http_complex_value 函数返回 NGX_ERROR,这通常意味着在尝试获取或计算 X-Forwarded-For 头的值时出现了问题,比如内存分配失败。在这种情况下,ngx_http_upstream_init_hash_peer 函数将返回 NGX_ERROR,导致当前请求的上游处理初始化失败。
  7. 调试日志:
    • 如果 ngx_http_complex_value 成功执行,将记录一条调试日志,显示 “upstream hash key” 以及计算出的键值。
  8. 继续处理:
    • 如果 ngx_http_complex_value 成功,函数将继续执行,hp->key 将用于后续的哈希计算和对等体选择过程。

总结来说,if (ngx_http_complex_value(r, &hcf->key, &hp->key) != NGX_OK) { return NGX_ERROR; } 这段代码是用来检查 ngx_http_complex_value 函数是否成功执行,并根据执行结果决定是否继续处理请求。如果 X-Forwarded-For 头存在且格式正确,hp->key 将被赋予相应的值;如果获取头信息失败或在执行过程中遇到错误,请求处理将被中止,并返回错误状态。

初始化:

1
2
3
4
5
6
7
8
9
10
11
tatic ngx_command_t  ngx_http_upstream_hash_commands[] = {

{ ngx_string("hash"),
NGX_HTTP_UPS_CONF|NGX_CONF_TAKE12,
ngx_http_upstream_hash,
NGX_HTTP_SRV_CONF_OFFSET,
0,
NULL },

ngx_null_command
};

设置值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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));

ccv.cf = cf;
ccv.value = &value[1];
ccv.complex_value = &hcf->key;

if (ngx_http_compile_complex_value(&ccv) != NGX_OK) { //变量替换值,比如:hash $http_x_forwarded_for;
return NGX_CONF_ERROR;
}

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

uscf->flags = NGX_HTTP_UPSTREAM_CREATE
|NGX_HTTP_UPSTREAM_WEIGHT
|NGX_HTTP_UPSTREAM_MAX_CONNS
|NGX_HTTP_UPSTREAM_MAX_FAILS
|NGX_HTTP_UPSTREAM_FAIL_TIMEOUT
|NGX_HTTP_UPSTREAM_DOWN;

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_ERROR;
}

return NGX_CONF_OK;
}

应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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;
}

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

if (ngx_http_complex_value(r, &hcf->key, &hp->key) != NGX_OK) {
return NGX_ERROR;
}

ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, //这里日志会输出
"upstream hash key:\"%V\"", &hp->key);

hp->conf = hcf;
hp->tries = 0;
hp->rehash = 0;
hp->hash = 0;
hp->get_rr_peer = ngx_http_upstream_get_round_robin_peer;

return NGX_OK;
}

4、sticky

4.1、简介

Sticky(会话粘性)模块的核心目标是在负载均衡场景下,将同一客户端的所有请求“粘”到一台后端服务器上,避免因多次随机或轮询调度引起的会话状态丢失或不一致。它通过在客户端与 Nginx 之间交换一个小巧的标识符(通常是 HTTP Cookie,也可选用 HTTP Header 或 URL 参数)来实现这一机制。

  • 模块演进
    • 最早的实现往往基于客户端 IP(IP Hash),但在 NAT、代理大量存在的环境里,IP 并不能准确区分用户,且不支持同一用户多设备切换场景。
    • 基于 Cookie 的 Sticky 引入后端可控的标识符(SID),无需共享后端 session 存储,也不会因客户端 IP 变化而失效。
  • 工作范式
    1. 初始化阶段:为每台后端 server 生成一个唯一的 SID(MD5);
    2. 请求阶段:检查客户端是否携带有效 SID,无则走正常负载均衡并记下所选 server;
    3. 响应阶段:向客户端下发 Set-Cookie: <name>=<SID>,或同等手段;
    4. 后续请求:客户端自动携带该 SID,Nginx 直接根据 SID 映射到固定的后端 server。
  • 优缺点对比
    • 优点:无需后端共享存储、可精确控制;支持 HTTPS 下的 Secure、HttpOnly;可与健康检查一起使用;兼容多路径、多子域场景。
    • 缺点:Cookie 丢失或被清除后会重新分配;首次请求仍然有调度抖动;对 Cookie 攻击(Rebinding)需加签名或加密。

4.2、使用示例

示例 1:基础 Cookie 粘性

1
2
3
4
5
6
7
8
9
10
11
12
13
upstream backend {
server 10.0.0.1:8080;
server 10.0.0.2:8080;
session_sticky cookie=session_id expires=24h path=/;
}
server {
listen 80;
server_name example.com;

location / {
proxy_pass http://backend;
}
}
  • 说明:使用名为 session_id 的 Cookie,实现 24 小时粘性。

示例 2:基于自定义变量的路由

1
2
3
4
5
6
7
8
9
10
11
map $remote_addr $route_hash {
default "";
192.168.0.0/16 backend1;
10.10.0.0/16 backend2;
}

upstream backend {
server 10.0.0.1:8080 route=backend1;
server 10.0.0.2:8080 route=backend2;
session_sticky cookie=sb route=$route_hash;
}
  • 说明:通过 map 指令自定义 $route_hash,客户端 IP 在特定网段下直接粘到指定后端。

示例 3:多子应用共存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
nginx复制编辑upstream appA {
server a1:8000; server a2:8000;
session_sticky cookie=appA_sid path=/appA domain=.example.com;
}
upstream appB {
server b1:9000; server b2:9000;
session_sticky cookie=appB_sid path=/appB domain=.example.com;
}
server {
listen 443 ssl;
server_name example.com;

location /appA/ { proxy_pass http://appA; }
location /appB/ { proxy_pass http://appB; }
}
  • 说明:不同子应用使用不同 Cookie 名与 Path,互不干扰。

4.3、原理分析

3.1 配置解析与初始化

当 Nginx 启动或执行热加载时,会依次解析所有配置文件中的指令。对于 session_sticky 指令,Nginx 会调用模块中定义的处理函数 ngx_http_upstream_session_sticky,将用户在 upstream {} 块中指定的参数(如 cookie 名称、route 变量、domainpathexpires 等)存入相应的配置结构体(ngx_http_upstream_ss_srv_conf_t)。

随后,Nginx 会在整体 HTTP 模块的主初始化函数 ngx_http_upstream_init_main_conf 中,遍历每一个 upstream 块,并对其调用 peer.init_upstream 回调。由于 session_sticky 在上一步已将 uscf->peer.init_upstream 指向了 ngx_http_upstream_session_sticky_init_upstream,此时就会执行该函数。

ngx_http_upstream_session_sticky_init_upstream 中,模块会为当前 upstream 下的每一个后端 server 调用 ngx_http_upstream_session_sticky_set_sid,利用服务器名称(或自定义的 route 参数)生成并缓存一个唯一的 SID(Session ID),并完成内部哈希表或链表等数据结构的初始化,以便后续快速查找与路由。整个流程确保:

  1. 指令参数 在配置阶段被正确解析并保存在模块配置里;
  2. 回调注册 在解析完成后被挂载到 Nginx upstream 的初始化流程中;
  3. SID 生成 和相关数据结构在配置加载时一次性完成,不再在请求路径上重复计算。

3.2 SID 的生成与作用

SID(Session ID)用于唯一标识后端服务器,确保同一用户的请求被路由到相同的服务器。在 ngx_http_upstream_session_sticky_set_sid 函数中,SID 是通过对服务器名称进行 MD5 哈希计算生成的:

1
2
3
4
ngx_md5_init(&md5);
ngx_md5_update(&md5, s->name->data, s->name->len);
ngx_md5_final(buf, &md5);
ngx_hex_dump(s->sid.data, buf, 16);

生成的 SID 是一个 32 字节的十六进制字符串,与服务器绑定,确保在服务器重启或配置重载后保持不变。这种设计保证了在 Nginx 重启或热加载配置时,相同服务器始终对应相同的 SID,而不同服务器则拥有不同的 SID,从而实现稳定的一一绑定。

SID 会存储在 ngx_http_ss_server_t 结构的 sid 字段中,用于后续请求的快速匹配。在请求处理阶段,模块无需再次进行哈希计算,只需将客户端 Cookie 中传回的 SID 与各后端 sid 字段进行对比,即可迅速定位到目标服务器,实现高效的粘性路由。

3.3 请求中的 Cookie 读取

当客户端发送请求时,模块会在 ngx_http_session_sticky_get_cookie 中处理整个 Cookie 头部(HTTP 请求头中的单行,包含多对 name=value),并 从中查找键名与配置中一致的项(默认为 session,也可通过 cookie=xxx 自定义)。例如:

1
2
3
GET /resource HTTP/1.1
Host: www.example.com
Cookie: session=dcf207e1a28a5e6f4b2d9fc0d1e5e6c3; userid=42; theme=light
  • 按照 RFC 6265 的定义,Cookie 头部由以分号+空格分隔的 cookie-pair 列表组成;服务器解析时会拆分此头部,获得各个 name=value
  • 模块代码会遍历所有 cookie-pair,比较键名(session)与配置值,一旦匹配便提取该项的 值 作为 SID
  • 若客户端未携带该键名的 Cookie,或提取到的值与任何后端服务器的预生成 SID 不匹配,则视为“无有效 SID”。

3.4 使用 Cookie 值进行路由

ngx_http_upstream_session_sticky_get_peer 中,模块根据提取到的 SID:

  1. 在内部维护的服务器列表(每个元素保存了启动时由 ngx_http_upstream_session_sticky_set_sid 计算好的固定 SID)中进行查找。
  2. 若找到了与该 SID 完全相同的条目,则直接将请求路由到对应的后端服务器,实现会话粘性。
  3. 若未找到匹配项(可能因为后端下线、SID 被篡改或过期),模块会退回到默认的负载均衡算法(如轮询 round‑robin),并在后续响应阶段下发新的 SID

这种基于从整体 Cookie 头部提取特定字段并直接映射到服务器的方式,既符合 HTTP 状态管理规范,也保证了高效的请求转发。


3.5 响应中的 Cookie 设置

在响应阶段,若检测到“无有效 SID”或首次访问,模块会在 ngx_http_session_sticky_header_filter 中插入一个 Set-Cookie 头,将所选服务器的 SID 返回给客户端:

1
2
3
HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: session=dcf207e1a28a5e6f4b2d9fc0d1e5e6c3; Path=/; Domain=.example.com; Expires=Wed, 15 May 2025 12:00:00 GMT
  • 根据Set-Cookie 规范,每个 Set-Cookie 头仅设置一对 cookie-pair 并可附加属性,如 PathDomainExpires
  • 浏览器在后续同域且满足路径匹配的请求中,会自动在请求头 Cookie 中包含此 session=... 项(仅发送 name=value 部分,属性不会带回)
  • 这就完成了从“无 SID → 选 server → 下发 SID → 客户端携带 SID → 再次路由”的完整闭环。

4.4、总结

Tengine 的 session_sticky 模块通过以下三步实现会话粘性路由:

  1. 配置初始化阶段:为每个后端 server 生成并固定一个唯一的 SID(MD5 哈希)
  2. 请求阶段:从整体 Cookie 头部中提取与配置键名一致的项(如 session),将其值作为 SID,与后端列表匹配,实现“粘性”路由。
  3. 响应阶段:在首次或失效情况下,通过 Set-Cookie 下发选中服务器的 SID,确保客户端在后续请求中带上正确的 session

这种设计既遵循了 HTTP 状态管理机制,又在 Nginx 的高性能代理框架中保证了极低的路由开销和高度的可扩展性。只需合理配置 cookiepathdomainexpires 等参数,即可满足大多数有状态应用的会话粘性需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
                客户端请求进来


┌────────────────────────────┐
│ 检查请求中是否带有 sticky cookie │
└────────────────────────────┘

┌─────────────┴─────────────┐
▼ ▼
有 cookie 没有 cookie
│ │
▼ ▼
提取 cookie 中的 sid 从 upstream 选择后端(如 round-robin)
│ │
▼ ▼
查找 sid 是否匹配某个 server 为选中的 server 计算 sid
│ │
┌─────┴──────┐ │
▼ ▼ ▼
匹配成功 匹配失败 设置 sid 到响应 Set-Cookie
│ │ │
▼ ▼ ▼
请求被转发到对应 server fallback 转发 响应客户端


客户端保存 sticky cookie

4.5、实际的问题

1、 SID 生成原理

session_sticky 模块中,每台后端服务器的 SID 在配置初始化阶段一次性生成,不再在请求处理时重复计算。生成逻辑为:将服务器名称(s->name,既可以是 IP:端口,也可以是自定义的 route 值)作为输入,调用 Nginx 自带的 MD5 接口计算哈希,然后将 16 字节的二进制哈希转换为 32 字节的十六进制字符串,存入 s->sid 字段。这种设计保证了,只要配置不变,同一服务器永久对应同一 SID,即便 Nginx 重启或热加载配置,SID 也保持不变,便于后续快速匹配

2、 多 upstream + 同名 Cookie 导致的覆盖场景

当在同一域名下为多个不同路径或业务的 upstream 块都启用了相同 Cookie 名称(如 session),且未做任何作用域区分时,会在浏览器端出现 “不断被覆盖” 的问题:

  1. **首次访问 /A**:

    • 后端组 backendA 随机选取一台服务器,响应中下发

      1
      2
      3
      HTTP/1.1 200 OK
      Content-Type: text/html
      Set-Cookie: session=cf9b71f0c789b0ae845b65e5e3a171a5; Path=/; Domain=example.com; Expires=Wed, 15 May 2025 12:00:00 GMT
  2. **随后访问 /B**:

    • 后端组 backendB 随机选取一台服务器,响应中也下发

      1
      2
      3
      HTTP/1.1 200 OK
      Content-Type: text/html
      Set-Cookie: session=cf9b71f0c789b0ae845b65e5e3a171a5; Path=/; Domain=example.com; Expires=Wed, 15 May 2025 12:00:00 GMT
  3. 浏览器行为

    • 若两条 Set-Cookie 指令同名同域同路径,则后者直接覆盖前者;若路径不同,浏览器会分别存储两条条目,但在请求时只发送与当前路径最匹配的那一条。
  4. 结果

    • 访问 /A/B 时 Cookie 值交替覆盖或切换,粘性路由无法生效

3、 解决办法

方案一:不同 Cookie 名称

为不同 upstream 指定互不干扰的 Cookie 名称,确保浏览器分别存储:

1
2
3
4
5
6
7
8
9
10
upstream backendA {
session_sticky cookie=SESSION_A route=$route_hash;
server a1:8081 route=a1;
server a2:8081 route=a2;
}
upstream backendB {
session_sticky cookie=SESSION_B route=$route_hash;
server b1:8082 route=b1;
server b2:8082 route=b2;
}
  • backendA 下发 SESSION_A=…backendB 下发 SESSION_B=…,互不覆盖

方案二:同名 Cookie + 不同 Path

在同一个域名下,用相同名称但为不同路径设置 path 属性,浏览器会区分存储:

1
2
3
4
5
6
7
8
9
10
upstream backendA {
session_sticky cookie=session path=/A domain=example.com expires=1h;
server a1:8081;
server a2:8081;
}
upstream backendB {
session_sticky cookie=session path=/B domain=example.com expires=1h;
server b1:8082;
server b2:8082;
}
  • 访问 /A 时只发送 session(Path=/A);访问 /B 时只发送对应的那条。

方案三:合并 upstream + 变量路由

将所有后端合并到一个组,通过 maproute 变量决定路由,然后用单一 Sticky 管理会话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
map $request_uri $target {
~^/A backendA;
~^/B backendB;
}
upstream backend {
server a1:8081 route=a1;
server a2:8081 route=a2;
server b1:8082 route=b1;
server b2:8082 route=b2;
session_sticky cookie=session route=$route_hash;
}

server {
location / {
proxy_pass http://backend;
}
}
  • 通过 $route_hash(可以映射自 $request_uri 或其他变量)实现对不同路径的路由,再由同一 Cookie 维护粘性 。

负载均衡算法解析
https://zjfans.github.io/2024/06/02/Nginx负载均衡算法解析/
作者
张三疯
发布于
2024年6月2日
许可协议