1、nginx-轮询
| 12
 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();
 
 
 best = NULL;
 total = 0;
 
 
 #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));
 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;
 }
 }
 
 
 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
| 12
 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:
 }
 
 | 
| 12
 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);
 
 
 ngx_http_upstream_rr_peers_rlock(iphp->rrp.peers);
 
 
 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;
 }
 
 
 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++;
 }
 
 
 n = p / (8 * sizeof(uintptr_t));
 m = (uintptr_t) 1 << p % (8 * sizeof(uintptr_t));
 
 
 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);
 
 
 ngx_http_upstream_rr_peer_lock(iphp->rrp.peers, peer);
 
 
 if (peer->down) {
 ngx_http_upstream_rr_peer_unlock(iphp->rrp.peers, peer);
 goto next;
 }
 
 
 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;
 }
 
 
 if (peer->max_conns && peer->conns >= peer->max_conns) {
 ngx_http_upstream_rr_peer_unlock(iphp->rrp.peers, peer);
 goto next;
 }
 
 
 break;
 
 next:
 
 
 if (++iphp->tries > 20) {
 ngx_http_upstream_rr_peers_unlock(iphp->rrp.peers);
 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++;
 
 
 if (now - peer->checked > peer->fail_timeout) {
 peer->checked = now;
 }
 
 
 ngx_http_upstream_rr_peer_unlock(iphp->rrp.peers, peer);
 ngx_http_upstream_rr_peers_unlock(iphp->rrp.peers);
 
 
 iphp->rrp.tried[n] |= m;
 iphp->hash = hash;
 
 
 return NGX_OK;
 }
 
 | 
3、hash
使用:
| 12
 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。以下是该过程的详细说明:

- 复杂值初始化:
- 在 Nginx 配置阶段,当遇到 hash $http_x_forwarded_for;配置时,相关的配置处理函数(如ngx_http_upstream_hash)将初始化一个ngx_http_complex_value_t结构体,这里即hcf->key。
 
- 请求处理阶段:
- 当一个请求到达并需要进行上游处理时,ngx_http_upstream_init_hash_peer函数被调用。
 
- 执行复杂值:
- ngx_http_complex_value函数被用来执行- hcf->key中定义的复杂值,这个复杂值就是- $http_x_forwarded_for。
 
- 计算哈希键:
- ngx_http_complex_value函数解析- $http_x_forwarded_for,这通常意味着它将获取请求的- X-Forwarded-For头的值。
 
- 值的变化:
- 在执行 ngx_http_complex_value(r, &hcf->key, &hp->key)之前,hp->key是未初始化的。
- 执行后,如果函数返回 NGX_OK,则hp->key将包含X-Forwarded-For头的值,这可能是一个单一的 IP 地址或者一个 IP 地址列表,具体取决于X-Forwarded-For头的内容。
 
- 错误处理:
- 如果 ngx_http_complex_value函数返回NGX_ERROR,这通常意味着在尝试获取或计算X-Forwarded-For头的值时出现了问题,比如内存分配失败。在这种情况下,ngx_http_upstream_init_hash_peer函数将返回NGX_ERROR,导致当前请求的上游处理初始化失败。
 
- 调试日志:
- 如果 ngx_http_complex_value成功执行,将记录一条调试日志,显示 “upstream hash key” 以及计算出的键值。
 
- 继续处理:
- 如果 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 将被赋予相应的值;如果获取头信息失败或在执行过程中遇到错误,请求处理将被中止,并返回错误状态。
初始化:
| 12
 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
 };
 
 | 
设置值:
| 12
 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) {
 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;
 }
 
 
 | 
应用:
| 12
 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_tngx_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 变化而失效。
 
- 工作范式
- 初始化阶段:为每台后端 server 生成一个唯一的 SID(MD5);
- 请求阶段:检查客户端是否携带有效 SID,无则走正常负载均衡并记下所选 server;
- 响应阶段:向客户端下发 Set-Cookie: <name>=<SID>,或同等手段;
- 后续请求:客户端自动携带该 SID,Nginx 直接根据 SID 映射到固定的后端 server。
 
- 优缺点对比
- 优点:无需后端共享存储、可精确控制;支持 HTTPS 下的 Secure、HttpOnly;可与健康检查一起使用;兼容多路径、多子域场景。
- 缺点:Cookie 丢失或被清除后会重新分配;首次请求仍然有调度抖动;对 Cookie 攻击(Rebinding)需加签名或加密。
 
4.2、使用示例
示例 1:基础 Cookie 粘性
| 12
 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:基于自定义变量的路由
| 12
 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:多子应用共存
| 12
 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 变量、domain、path、expires 等)存入相应的配置结构体(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),并完成内部哈希表或链表等数据结构的初始化,以便后续快速查找与路由。整个流程确保:
- 指令参数 在配置阶段被正确解析并保存在模块配置里;
- 回调注册 在解析完成后被挂载到 Nginx upstream 的初始化流程中;
- SID 生成 和相关数据结构在配置加载时一次性完成,不再在请求路径上重复计算。
3.2 SID 的生成与作用
SID(Session ID)用于唯一标识后端服务器,确保同一用户的请求被路由到相同的服务器。在 ngx_http_upstream_session_sticky_set_sid 函数中,SID 是通过对服务器名称进行 MD5 哈希计算生成的:
| 12
 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 自定义)。例如:
| 12
 3
 
 | GET /resource HTTP/1.1Host: 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:
- 在内部维护的服务器列表(每个元素保存了启动时由 ngx_http_upstream_session_sticky_set_sid计算好的固定 SID)中进行查找。
- 若找到了与该 SID 完全相同的条目,则直接将请求路由到对应的后端服务器,实现会话粘性。
- 若未找到匹配项(可能因为后端下线、SID 被篡改或过期),模块会退回到默认的负载均衡算法(如轮询 round‑robin),并在后续响应阶段下发新的 SID
这种基于从整体 Cookie 头部提取特定字段并直接映射到服务器的方式,既符合 HTTP 状态管理规范,也保证了高效的请求转发。
3.5 响应中的 Cookie 设置
在响应阶段,若检测到“无有效 SID”或首次访问,模块会在 ngx_http_session_sticky_header_filter 中插入一个 Set-Cookie 头,将所选服务器的 SID 返回给客户端:
| 12
 3
 
 | HTTP/1.1 200 OKContent-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并可附加属性,如Path、Domain、Expires等
- 浏览器在后续同域且满足路径匹配的请求中,会自动在请求头 Cookie中包含此session=...项(仅发送name=value部分,属性不会带回)
- 这就完成了从“无 SID → 选 server → 下发 SID → 客户端携带 SID → 再次路由”的完整闭环。
4.4、总结
Tengine 的 session_sticky 模块通过以下三步实现会话粘性路由:
- 配置初始化阶段:为每个后端 server 生成并固定一个唯一的 SID(MD5 哈希)
- 请求阶段:从整体 Cookie头部中提取与配置键名一致的项(如session),将其值作为 SID,与后端列表匹配,实现“粘性”路由。
- 响应阶段:在首次或失效情况下,通过 Set-Cookie下发选中服务器的 SID,确保客户端在后续请求中带上正确的session项
这种设计既遵循了 HTTP 状态管理机制,又在 Nginx 的高性能代理框架中保证了极低的路由开销和高度的可扩展性。只需合理配置 cookie、path、domain、expires 等参数,即可满足大多数有状态应用的会话粘性需求。
| 12
 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),且未做任何作用域区分时,会在浏览器端出现 “不断被覆盖” 的问题:
- **首次访问 - /A**:
 
- **随后访问 - /B**:
 
- 浏览器行为: - 
- 若两条 Set-Cookie指令同名同域同路径,则后者直接覆盖前者;若路径不同,浏览器会分别存储两条条目,但在请求时只发送与当前路径最匹配的那一条。
 
- 结果: - 
- 访问 /A、/B时 Cookie 值交替覆盖或切换,粘性路由无法生效
 
3、 解决办法
方案一:不同 Cookie 名称
为不同 upstream 指定互不干扰的 Cookie 名称,确保浏览器分别存储:
| 12
 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 属性,浏览器会区分存储:
| 12
 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 + 变量路由
将所有后端合并到一个组,通过 map 或 route 变量决定路由,然后用单一 Sticky 管理会话:
| 12
 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 维护粘性 。