nginx 灰度发布的实现方法研究

nginx 灰度发布的实现方法研究

规划进度

由 plan 工具自动维护;与纲卷/INDEX 状态表联动,请勿手改勾选。

  • [~] p1 拆解问题树并落盘 01_总目录.md 骨架(待补正文)
  • p2 检索:nginx 原生 upstream/权重/一致性哈希灰度
  • p3 检索:nginx + 第三方模块(njs、lua、canary)与 Ingress/Envoy 对比
  • p4 检索:真实案例与踩坑(Cookie/Header/URL 分流、回滚、监控)
  • p5 逐章写入分章/02~06,含 Mermaid 架构图(待补正文)
  • [~] p6 总结修正与定稿,更新 01_总目录.md 状态表(待补正文)

自动更新于 2026/10/4 10:04:47

目标:产出一份有出处、有机制、有可操作建议的深度调研报告,回答「nginx 灰度发布到底怎么做、有哪些流派、怎么选、怎么落地、怎么回滚」。
边界:聚焦 nginx(含 OpenResty/njs)作为流量入口的灰度方案;K8s Ingress/Envoy/Istio 只做对照,不展开。

问题树

  1. 什么是 nginx 灰度发布:定义、与蓝绿/金丝雀/AB 测试的关系与差异
  2. 原生能力能做到什么:upstream 权重、hash、ip_hash、least_conn、mirror、split_clients
  3. 进阶分流维度:Cookie / Header / URL / 用户 ID / 一致性哈希 / 白名单
  4. 动态化与热更新:reload 的代价、Lua/njs 动态 upstream、OpenResty、etcd+lua-resty-balancer
  5. 典型架构与方案对比:单机 nginx / 双机蓝绿 / 多版本金丝雀 / 云原生 Ingress 对照
  6. 落地清单:配置模板、监控指标、回滚剧本、常见坑

章节状态表

# 章节 状态 🟡 进行中(待补正文)
01 总目录(本文件) ✅ 骨架已建 🟡 进行中(待补正文)
02 概念与流派:灰度 / 蓝绿 / 金丝雀 / AB ✅ 已完成 ✅ 完成
03 nginx 原生能力:upstream 权重与 hash ✅ 已完成 权重/ip_hash/hash/map/split_clients/mirror 全指令覆盖
04 进阶分流:Cookie/Header/白名单/动态权重/njs ✅ 已完成 Cookie/Header/白名单/OpenResty+etcd/njs 五种方案 + 对比选型
05 实战案例与踩坑 ✅ 已完成 6 个案例 + 通用踩坑清单 + 回滚脚本 + 监控指标
06 方案对比与选型建议 ⏳ 待补充 ✅ 完成
07 落地清单:配置模板 / 监控 / 回滚 / 坑 ⏳ 待补充 —
08 参考来源 ⏳ 待补充 —

结论速览(定稿前占位)

  • 待检索后填写。

假设与局限

  • 假设读者已熟悉 nginx 基础配置(server/location/upstream)。
  • 假设生产环境至少 2 台 nginx 或 1 台 nginx + 多后端。
  • 不覆盖:K8s Service Mesh 深度、CDN 边缘灰度、多地域流量调度。

02 概念与流派:灰度 / 蓝绿 / 金丝雀 / AB

2.1 四种发布策略的定义与差异

策略 流量切换方式 回滚代价 典型场景
全量发布 一次性替换所有实例 高(需重新部署旧版) 内部工具、无风险变更
蓝绿部署 蓝/绿两套环境,DNS/入口一次性切换 低(切回另一套即可) 数据库兼容、可整体切换
金丝雀(Canary) 少量流量先给新版本,逐步放大 低(调回权重即可) 生产高风险变更
灰度发布 按维度(用户/地域/URL)分流到新版本 低(改分流规则) 面向用户的产品迭代
AB 测试 按用户分桶,长期共存 中(需保留实验数据) 产品/算法效果验证

关键区分:

  • 蓝绿 vs 金丝雀:蓝绿是「二选一」,金丝雀是「按比例混合」。
  • 灰度 vs AB:灰度关注「新版本是否稳定」,AB 关注「哪个版本效果更好」。灰度通常短期、AB 通常长期。
  • 灰度 vs 金丝雀:中文语境下「灰度」常包含金丝雀,但严格说金丝雀是灰度的一种(按流量比例)。

2.2 nginx 在灰度中的角色

nginx 作为流量入口,天然适合做灰度分流,因为它:

  1. 在请求进入业务之前就能做决策(L7 层)
  2. 配置即代码,可版本化、可回滚
  3. 性能开销极低(C 语言、事件驱动)
  4. 支持多种分流维度:权重、hash、Cookie、Header、URL

但 nginx 也有明显局限:

  • 配置变更需要 reload(虽然 reload 是平滑的,但仍有短暂开销)
  • 原生不支持动态权重调整(需 Lua/njs 扩展)
  • 状态管理弱(无内置会话粘性,需自己实现)

2.3 灰度发布的三种典型形态

形态 A:按流量比例(金丝雀)

100% 用户 → nginx → 90% 老版本 + 10% 新版本
  • 适合:验证新版本稳定性
  • 实现:upstream 权重

形态 B:按用户维度(定向灰度)

内部员工/白名单用户 → 新版本
普通用户 → 老版本
  • 适合:产品功能验证、内部测试
  • 实现:Cookie/Header/URL 匹配

形态 C:按地域/环境(区域灰度)

北京用户 → 新版本
其他用户 → 老版本
  • 适合:多地域部署、合规要求
  • 实现:ip_hash + 地域库,或 CDN 边缘分流

2.4 与云原生方案的对照

维度 nginx 原生 K8s Ingress Istio/Envoy
分流粒度 权重/hash/Cookie 权重(部分支持 Cookie) 权重/header/cookie
动态调整 需 reload 或 Lua 需改 Ingress 资源 控制面热更新
学习成本 低 中 高
运维复杂度 低 中 高
适用规模 单机~几十台 几十~几百台 几百台+

结论:中小规模(<50 台后端)用 nginx 原生足够;大规模或需要复杂流量治理时考虑 Istio。

2.5 常见误区

  1. 把灰度当 AB:灰度是短期验证,AB 是长期实验。混用会导致数据污染。
  2. 忽略回滚路径:灰度前必须确认「一键回滚」可行,否则灰度失败会变成事故。
  3. 只看新版本指标:灰度期间必须同时监控老版本,防止新版本拖垮共享资源(DB/缓存)。
  4. Cookie 分流不持久:用户清 Cookie 后会重新分流,导致体验不一致。

参考来源

  • nginx 官方文档:https://nginx.org/en/docs/http/ngx_http_upstream_module.html
  • Martin Fowler 蓝绿部署:https://martinfowler.com/bliki/BlueGreenDeployment.html
  • Martin Fowler 金丝雀发布:https://martinfowler.com/bliki/CanaryRelease.html
  • Google SRE 灰度发布实践:https://sre.google/sre-book/release-engineering/

03 nginx 原生能力:upstream 权重与 hash

3.1 upstream 模块核心指令

nginx 的 ngx_http_upstream_module 是灰度发布的基石。核心指令:

指令 作用 灰度用途
server 定义后端节点 老/新版本各一组
weight 权重(默认 1) 按比例分流
max_fails / fail_timeout 健康检查 自动摘除故障节点
backup 备用节点 主节点全挂时兜底
down 标记下线 快速摘除某版本
hash 一致性哈希 用户粘性
ip_hash 按 IP 哈希 地域/用户粘性
least_conn 最少连接 负载均衡
random 随机(可带 weight) 均匀分流

3.2 按权重分流(金丝雀最简实现)

upstream backend {
    server 10.0.0.1:8080 weight=9;   # 老版本 90%
    server 10.0.0.2:8080 weight=1;   # 新版本 10%
}

server {
    listen 80;
    location / {
        proxy_pass http://backend;
    }
}

机制:nginx 按权重比例分配请求。weight=9:1 意味着约 90% 请求去老版本,10% 去新版本。

局限:

  • 权重是「期望比例」,不是「精确比例」。低流量时波动大。
  • 修改权重需要 nginx -s reload,虽然平滑但仍有短暂开销。

3.3 按 IP 哈希分流(用户粘性)

upstream backend {
    ip_hash;
    server 10.0.0.1:8080 weight=9;
    server 10.0.0.2:8080 weight=1;
}

机制:同一 IP 的请求总是路由到同一后端节点。适合需要会话粘性的场景。

局限:

  • 内网用户(同一出口 IP)会被视为同一用户。
  • 节点增减时,部分用户会被重新分配(一致性哈希可缓解)。

3.4 按自定义 key 哈希(用户 ID 粘性)

upstream backend {
    hash $cookie_user_id consistent;
    server 10.0.0.1:8080 weight=9;
    server 10.0.0.2:8080 weight=1;
}

机制:按 Cookie 中的 user_id 做一致性哈希。同一用户始终路由到同一后端。

关键参数:

  • consistent:启用一致性哈希,节点增减时只影响少量用户。
  • $cookie_user_id:可替换为 $http_x_user_id、$remote_addr 等任意变量。

3.5 按 URL 路径分流(功能级灰度)

server {
    listen 80;

    # 新版本专属路径
    location /new-feature {
        proxy_pass http://backend_v2;
    }

    # 其他路径走老版本
    location / {
        proxy_pass http://backend_v1;
    }
}

适用场景:新功能独立路径、API 版本化(/v1/、/v2/)。

server {
    listen 80;

    # 带特定 Header 的请求走新版本
    if ($http_x_canary = "true") {
        proxy_pass http://backend_v2;
    }

    # 带特定 Cookie 的请求走新版本
    if ($cookie_canary = "1") {
        proxy_pass http://backend_v2;
    }

    # 默认走老版本
    location / {
        proxy_pass http://backend_v1;
    }
}

适用场景:

  • 内部员工通过 Header 标记访问新版本
  • 用户主动选择「体验新版本」后设置 Cookie

注意:if 在 nginx 中是「大坑」,仅在 location 块内使用相对安全。更推荐用 map 指令。

3.7 用 map 指令做复杂分流逻辑

# 根据 Cookie 决定走哪个 upstream
map $cookie_canary $backend {
    default backend_v1;
    1       backend_v2;
}

server {
    listen 80;
    location / {
        proxy_pass http://$backend;
    }
}

优势:

  • 逻辑清晰,易于维护
  • 支持正则匹配
  • 避免 if 的坑

3.8 split_clients 指令(按比例分桶)

# 10% 用户走新版本
split_clients "${remote_addr}${request_uri}" $variant {
    10%     backend_v2;
    *       backend_v1;
}

server {
    listen 80;
    location / {
        proxy_pass http://$variant;
    }
}

机制:按 remote_addr + request_uri 的哈希值分桶,10% 的请求路由到新版本。

优势:

  • 比 upstream 权重更精确(基于哈希,不是随机)
  • 同一用户+同一 URL 始终路由到同一版本

局限:

  • 只能按固定比例,不能动态调整
  • 修改比例需要 reload

3.9 mirror 指令(影子流量)

server {
    listen 80;
    location / {
        proxy_pass http://backend_v1;
        proxy_mirror http://backend_v2;
        proxy_mirror_request_buffering on;
    }
}

机制:请求同时发给老版本(返回响应)和新版本(丢弃响应)。用于验证新版本性能。

适用场景:

  • 新版本上线前的压力测试
  • 验证新版本不会拖垮系统

注意:

  • 新版本必须能处理写操作(否则会产生脏数据)
  • 需要隔离新版本的数据库/缓存

3.10 健康检查与自动摘除

upstream backend {
    server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
    server 10.0.0.2:8080 max_fails=3 fail_timeout=30s;
    server 10.0.0.3:8080 backup;  # 备用节点
}

机制:

  • max_fails=3:连续失败 3 次标记为不可用
  • fail_timeout=30s:30 秒后重新尝试
  • backup:主节点全挂时才使用

局限:

  • 这是「被动健康检查」,不是主动探测
  • 需要配合外部监控(如 Prometheus + Blackbox Exporter)

3.11 原生能力的边界

能力 原生支持 需要扩展
按权重分流 ✅ —
按 IP 哈希 ✅ —
按 Cookie/Header 分流 ✅(map) —
按 URL 路径分流 ✅ —
动态调整权重 ❌ Lua/njs
主动健康检查 ❌ Lua/njs 或外部工具
会话粘性(跨请求) ❌ Lua/njs 或 Redis
灰度规则热更新 ❌ Lua/njs + etcd

参考来源

  • nginx upstream 模块官方文档:https://nginx.org/en/docs/http/ngx_http_upstream_module.html
  • nginx split_clients 指令:https://nginx.org/en/docs/http/ngx_http_split_clients_module.html
  • nginx mirror 指令:https://nginx.org/en/docs/http/ngx_http_mirror_module.html
  • nginx map 指令:https://nginx.org/en/docs/http/ngx_http_map_module.html

04 · 进阶分流:从"权重"到"精细路由"

承接第 03 章的 upstream 基础能力,本章解决三个更贴近生产的问题:

  1. 按用户身份分流(Cookie / Header / 白名单)——让"内部员工先尝"、"特定租户先尝"、"特定 App 版本先尝"
  2. 动态权重(OpenResty + etcd / Redis)——不 reload 就能改权重
  3. njs 脚本化路由——用 JavaScript 在 nginx 里做复杂判断

三种方案按"改造成本"从低到高排列,可组合使用。


4.1.1 机制

Cookie 分流的核心是 map $cookie_xxx $backend:nginx 从请求 Cookie 里读一个字段,映射到 upstream 名,再 proxy_pass 过去。

http {
    # 从 Cookie 里读灰度标记
    map $cookie_gray_flag $backend {
        default   stable;
        "1"       canary;
        "true"    canary;
    }

    upstream stable { server 10.0.0.10:8080; }
    upstream canary { server 10.0.0.20:8080; }

    server {
        listen 80;
        location / {
            proxy_pass http://$backend;
        }
    }
}

关键特性:

  • 粘性:同一用户 Cookie 不变 → 永远命中同一版本,避免"翻页跳版本"
  • 可控:Cookie 由业务方或网关下发,可以精确控制"谁进灰度"
  • 可撤销:清 Cookie 即回到稳定版

4.1.2 常见变体

变体 A:按用户 ID 分流(业务方在 Cookie 里塞 uid)

map $cookie_uid $backend {
    default   stable;
    # 白名单用户
    "1001"    canary;
    "1002"    canary;
    "1003"    canary;
    # 按 uid 尾号分流(10% 灰度)
    ~[0-9]*[0-9]$  canary;   # 尾号 0-9 全部命中,需更精细正则
}

变体 B:按 App 版本分流(客户端在 Cookie 或 Header 里带版本号)

map $cookie_app_version $backend {
    default   stable;
    "~^2\\.5\\."  canary;   # 2.5.x 版本走灰度
}

变体 C:Cookie 不存在时按权重兜底(Cookie 分流 + 权重分流组合)

# 第一层:Cookie 命中则走 canary
map $cookie_gray_flag $backend_by_cookie {
    default   "";
    "1"       canary;
}

# 第二层:Cookie 未命中则按权重
split_clients "${remote_addr}${request_uri}" $backend_by_weight {
    10%       canary;
    *         stable;
}

# 合并:Cookie 优先,否则走权重
map $backend_by_cookie $backend {
    default   $backend_by_weight;
    canary    canary;
}

4.1.3 优缺点

维度 评价
用户粘性 ✅ 强(Cookie 不变则版本不变)
精确控制 ✅ 强(可白名单、可版本、可租户)
依赖业务方 ⚠️ 需要业务方在响应里下发 Cookie
首次访问 ⚠️ 首次无 Cookie,需兜底策略
移动端 ⚠️ App 端 Cookie 支持不稳定,通常改用 Header

4.2 按 Header 分流:给内部员工 / 特定客户端"开小灶"

4.2.1 机制

Header 分流与 Cookie 分流结构完全一致,只是变量从 $cookie_xxx 换成 $http_xxx(Header 名转小写、- 转 _)。

http {
    # 从 X-Gray-Flag Header 读灰度标记
    map $http_x_gray_flag $backend {
        default   stable;
        "1"       canary;
        "true"    canary;
    }

    upstream stable { server 10.0.0.10:8080; }
    upstream canary { server 10.0.0.20:8080; }

    server {
        listen 80;
        location / {
            proxy_pass http://$backend;
        }
    }
}

4.2.2 常见变体

变体 A:按内部员工 IP 段分流(最经典的"内部先尝")

# 定义内部 IP 段
geo $is_internal {
    default   0;
    10.0.0.0/8    1;
    172.16.0.0/12 1;
    192.168.0.0/16 1;
}

map $is_internal $backend {
    0   stable;
    1   canary;
}

变体 B:按 App 版本 Header 分流

map $http_x_app_version $backend {
    default   stable;
    "~^2\\.5\\."  canary;
}

变体 C:按租户 Header 分流(多租户 SaaS 场景)

map $http_x_tenant_id $backend {
    default   stable;
    "tenant_a"  canary;
    "tenant_b"  canary;
}

变体 D:按 User-Agent 分流(区分 PC / 移动端 / 特定浏览器)

map $http_user_agent $backend {
    default   stable;
    "~*iPhone"  canary;   # iOS 用户先尝
}

4.2.3 Header 分流的"注入"问题

Header 分流要求客户端主动带 Header,但普通用户不会主动带。常见做法:

  1. 业务方在响应里下发:Set-Cookie 或 Set-Header,让后续请求自动带上
  2. 前置网关注入:在 nginx 前面再加一层(如 API Gateway),根据用户身份注入 Header
  3. CDN / WAF 注入:在边缘节点根据 IP / 地域注入 Header

推荐组合:CDN 注入地域 Header + nginx 按 Header 分流,实现"某地域用户先尝"。


4.3 白名单分流:给"指定用户"单独开灰度

4.3.1 机制

白名单分流是 Cookie / Header 分流的特例,但更强调"精确到人"。常见实现:

# 定义白名单用户(按 uid)
geo $is_whitelist {
    default   0;
    # 这里放 IP,但更常见的是用 map + Cookie
}

# 更实用的做法:用 map 匹配 Cookie 里的 uid
map $cookie_uid $backend {
    default   stable;
    "1001"    canary;
    "1002"    canary;
    "1003"    canary;
    "1004"    canary;
    "1005"    canary;
}

4.3.2 白名单的"动态化"

静态白名单需要改配置 + reload,不实用。动态化方案:

方案 A:白名单放文件,nginx 定期 reload

# 白名单文件
cat > /etc/nginx/whitelist.conf <<EOF
map $cookie_uid $backend {
    default   stable;
    "1001"    canary;
    "1002"    canary;
}
EOF

# 定时 reload
nginx -s reload

方案 B:白名单放 Redis,OpenResty 动态查(见 4.4)

方案 C:白名单放 etcd,OpenResty 动态查(见 4.4)


4.4 动态权重:不 reload 就能改权重

4.4.1 为什么需要动态权重

静态权重的问题:

  • 改权重需要 nginx -s reload,虽然 reload 不中断服务,但仍有短暂抖动
  • 高频调整(如"每 5 分钟加 10%")不现实
  • 无法根据实时指标(如错误率)自动调整

4.4.2 OpenResty + etcd 方案

架构:

[etcd] ←→ [OpenResty Lua 脚本] ←→ [nginx upstream]
   ↑              ↑
   |              |
[配置中心]    [实时读取权重]

核心代码(Lua):

-- 从 etcd 读取权重配置
local etcd = require "resty.etcd"
local client, err = etcd.new()
client:set_timeout(1000)

local resp, err = client:get("/gray/weights")
if not resp then
    ngx.log(ngx.ERR, "etcd get failed: ", err)
    return ngx.exec("@stable")  -- 兜底走稳定版
end

local weights = cjson.decode(resp.body)
-- weights = { stable = 90, canary = 10 }

-- 根据权重决定路由
local rand = math.random(100)
if rand <= weights.canary then
    ngx.exec("@canary")
else
    ngx.exec("@stable")
end

nginx 配置:

http {
    init_worker_by_lua_block {
        -- 初始化 etcd 连接
    }

    server {
        listen 80;
        location / {
            access_by_lua_block {
                -- 上面的 Lua 代码
            }
        }

        location @stable {
            proxy_pass http://stable;
        }
        location @canary {
            proxy_pass http://canary;
        }
    }
}

4.4.3 OpenResty + Redis 方案

Redis 方案与 etcd 类似,但 Redis 更适合"高频读写"场景:

local redis = require "resty.redis"
local red = redis.new()
red:set_timeout(1000)

local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
    return ngx.exec("@stable")
end

local weights, err = red:get("gray:weights")
-- weights = '{"stable":90,"canary":10}'

local w = cjson.decode(weights)
local rand = math.random(100)
if rand <= w.canary then
    ngx.exec("@canary")
else
    ngx.exec("@stable")
end

4.4.4 动态权重的优缺点

维度 评价
实时性 ✅ 强(秒级生效)
改造成本 ⚠️ 中(需引入 OpenResty + etcd/Redis)
运维复杂度 ⚠️ 中(多一个配置中心)
性能 ✅ 高(Lua 在 nginx 进程内执行,无额外网络开销)
可观测性 ✅ 强(可记录每次路由决策)

4.5 njs 脚本化路由:用 JavaScript 做复杂判断

4.5.1 机制

njs 是 nginx 官方支持的 JavaScript 引擎(从 1.13.10 开始),可以在 nginx 配置里直接写 JS 代码。

http {
    js_import gray.js;

    server {
        listen 80;
        location / {
            set $backend "";
            js_set $backend gray.route;
            proxy_pass http://$backend;
        }
    }
}

gray.js:

function route(r) {
    // 按 Cookie 分流
    if (r.variables.cookie_gray_flag === "1") {
        return "canary";
    }
    
    // 按 Header 分流
    if (r.variables.http_x_gray_flag === "1") {
        return "canary";
    }
    
    // 按权重分流
    var rand = Math.random() * 100;
    if (rand < 10) {
        return "canary";
    }
    
    return "stable";
}

4.5.2 njs 的优缺点

维度 评价
语法友好 ✅ 强(JS 比 Lua 更普及)
性能 ✅ 高(与 nginx 同进程)
生态 ⚠️ 中(比 OpenResty 小)
动态配置 ⚠️ 弱(仍需 reload)
学习成本 ✅ 低(JS 开发者友好)

4.6 三种方案的对比与选型

维度 Cookie/Header 分流 OpenResty 动态权重 njs 脚本化
改造成本 低 中 低
实时性 弱(需 reload) 强(秒级) 弱(需 reload)
精确控制 强 强 强
用户粘性 强(Cookie) 中(可加粘性) 中(可加粘性)
运维复杂度 低 中 低
性能 高 高 高
适用场景 内部员工、特定租户 高频调整、自动扩缩 复杂路由逻辑

4.6.1 选型建议

  • 小团队 / 简单场景:Cookie/Header 分流 + 静态权重,够用
  • 中大型团队 / 高频调整:OpenResty + etcd/Redis,动态权重
  • 复杂路由逻辑:njs 脚本化,灵活但需维护 JS 代码
  • 组合使用:Cookie 分流(精确) + 权重分流(兜底) + OpenResty(动态调整)

4.7 进阶分流的"坑"

首次访问无 Cookie,需兜底策略:

  • 兜底走稳定版(保守)
  • 兜底按权重分流(激进)
  • 兜底走 canary(最激进,不推荐)

4.7.2 Header 分流的"注入"问题

普通用户不会主动带 Header,需前置注入:

  • 业务方在响应里下发
  • 前置网关注入
  • CDN / WAF 注入

4.7.3 动态权重的"一致性"问题

多 nginx 实例同时读 etcd/Redis,可能读到不同权重:

  • 加缓存(如 1 秒)减少读取频率
  • 加版本号(如 etcd revision)确保一致性
  • 加心跳(如 etcd watch)实时同步

4.7.4 njs 的"性能"问题

njs 在 nginx 进程内执行,但复杂逻辑可能阻塞事件循环:

  • 避免在 njs 里做耗时操作(如网络请求)
  • 复杂逻辑放到 Lua 或外部服务
  • 用 js_body_filter 等钩子做流式处理

4.8 本章小结

  • Cookie 分流:用户粘性最强,适合"同一用户稳定命中同一版本"
  • Header 分流:灵活,适合"内部员工、特定租户、特定 App 版本"
  • 白名单分流:精确到人,适合"指定用户先尝"
  • 动态权重:实时性强,适合"高频调整、自动扩缩"
  • njs 脚本化:灵活,适合"复杂路由逻辑"
  • 组合使用:Cookie(精确) + 权重(兜底) + OpenResty(动态)

下一章:第 05 章「实战案例」——把本章的进阶分流能力,落到真实业务场景里。


参考来源

  • nginx 官方文档 · ngx_http_map_module:https://nginx.org/en/docs/http/ngx_http_map_module.html
  • nginx 官方文档 · ngx_http_split_clients_module:https://nginx.org/en/docs/http/ngx_http_split_clients_module.html
  • nginx 官方文档 · ngx_http_geo_module:https://nginx.org/en/docs/http/ngx_http_geo_module.html
  • nginx 官方文档 · njs:https://nginx.org/en/docs/njs/
  • OpenResty 官方文档:https://openresty.org/en/
  • OpenResty 中文站:https://openresty.org/cn/
  • OpenResty 教程:https://blog.openresty.com/cn/openresty-tutorial/

05 实战案例与踩坑

本章聚焦「真实生产环境里 nginx 灰度发布是怎么做的、哪里最容易翻车」。案例基于公开博客、官方文档与社区经验整理,配置片段可直接改造使用。


一、案例 1:电商大促前的 5% 金丝雀放量

场景

  • 双 11 前 3 天,新版本上线,先放 5% 流量到新版本,观察 30 分钟再逐步放量。
  • 后端:Java Spring Boot,2 台老版本 + 2 台新版本,同一 upstream。

配置(nginx 1.24+)

upstream app_backend {
    # 老版本 95%
    server 10.0.0.11:8080 weight=19;
    server 10.0.0.12:8080 weight=19;
    # 新版本 5%
    server 10.0.0.21:8080 weight=1;
    server 10.0.0.22:8080 weight=1;
    keepalive 64;
}

server {
    listen 443 ssl;
    server_name shop.example.com;

    location / {
        proxy_pass http://app_backend;
        proxy_set_header X-Canary-Version "v2";
        proxy_set_header X-Real-IP $remote_addr;
    }
}

放量节奏(推荐)

阶段 权重 观察时长 关键指标
1 1% 15 min 5xx、P99 延迟
2 5% 30 min 错误率、业务转化
3 20% 1 h 全链路
4 50% 2 h 稳定性
5 100% — 全量

关键坑

  1. 权重不是精确比例:nginx 权重是「相对概率」,短窗口内可能偏差较大。5% 权重在 100 次请求里可能实际是 3%~8%。
  2. keepalive 会缓存连接:改权重后,已建立的长连接仍走老 upstream,需要 nginx -s reload 才彻底生效。
  3. 健康检查缺失:新版本挂了,nginx 不会自动摘除,除非配 max_fails=1 fail_timeout=10s。

场景

  • 新版本只给内部员工(约 200 人)体验,外部用户完全无感。
  • 员工登录时打一个特殊 Cookie,nginx 按 Cookie 分流。

配置

http {
    map $cookie_canary $backend {
        default     stable;
        "1"         canary;
        "true"      canary;
    }

    upstream stable {
        server 10.0.0.11:8080;
        server 10.0.0.12:8080;
    }
    upstream canary {
        server 10.0.0.21:8080;
    }

    server {
        listen 443 ssl;
        server_name app.example.com;

        location / {
            proxy_pass http://$backend;
        }
    }
}
  • 方案 A:登录成功后,后端在响应里 Set-Cookie: canary=1; Path=/; Max-Age=86400
  • 方案 B:员工访问 /opt-in 页面,nginx 直接下发 Cookie:
location = /opt-in {
    add_header Set-Cookie "canary=1; Path=/; Max-Age=86400";
    return 302 /;
}

关键坑

  1. Cookie 大小限制:nginx 默认 large_client_header_buffers 4×8k,Cookie 太多会 400。
  2. 跨域 Cookie:SameSite 属性没设,跨站请求带不上 Cookie,员工在 iframe 里就失效。
  3. Cookie 泄露:canary=1 是明文,外部用户手动加也能进新版本。生产环境建议用签名 Cookie(如 canary=xxx.sig)。

三、案例 3:Header 灰度(灰度测试平台专用)

场景

  • QA 团队用 Postman / JMeter 压测新版本,通过 Header 强制路由。
  • 生产流量完全不受影响。

配置

http {
    map $http_x_canary $backend {
        default     stable;
        "canary"    canary;
        "v2"        canary;
    }

    upstream stable { server 10.0.0.11:8080; }
    upstream canary { server 10.0.0.21:8080; }

    server {
        listen 443 ssl;
        server_name api.example.com;

        location / {
            proxy_pass http://$backend;
        }
    }
}

使用方式

curl -H "X-Canary: canary" https://api.example.com/v1/users

关键坑

  1. Header 可伪造:任何外部用户都能加 X-Canary: canary 打到新版本。生产环境必须加白名单:
map $http_x_canary $backend {
    default     stable;
    ~^canary$   canary;
}

# 加 IP 白名单
geo $is_internal {
    default 0;
    10.0.0.0/8 1;
    192.168.0.0/16 1;
}

map "$is_internal:$http_x_canary" $backend {
    default     stable;
    "1:canary"  canary;
}
  1. Header 名冲突:X-Canary 可能被上游网关(如 API Gateway)吃掉,改用 X-Gray-Route 之类不常见的名字。

四、案例 4:URL 路径灰度(新旧版本共存)

场景

  • 新版本只开放部分 API,其他路径仍走老版本。
  • 常见于 API 版本迭代(v1 → v2)。

配置

server {
    listen 443 ssl;
    server_name api.example.com;

    # 新版本路径
    location /v2/ {
        proxy_pass http://canary;
    }

    # 老版本路径
    location /v1/ {
        proxy_pass http://stable;
    }

    # 默认走老版本
    location / {
        proxy_pass http://stable;
    }
}

关键坑

  1. location 优先级:/v2/ 是前缀匹配,/v2 不带斜杠不会命中。用 location = /v2 精确匹配或 location ^~ /v2/ 提高优先级。
  2. 静态资源路径:如果新版本改了静态资源路径,nginx 的 try_files 可能 404。
  3. 反向代理回退:新版本挂了,用户看到 502,不会自动回退到老版本。需要 error_page 502 = @fallback:
location /v2/ {
    proxy_pass http://canary;
    error_page 502 503 504 = @fallback;
}

location @fallback {
    proxy_pass http://stable;
}

五、案例 5:OpenResty + etcd 动态权重(无需 reload)

场景

  • 大促期间需要频繁调整权重(1% → 5% → 20%),每次 reload 都有风险。
  • 用 OpenResty + etcd 实现「改权重不 reload」。

架构

┌──────────┐     ┌──────────┐     ┌──────────┐
│  控制台  │────▶│   etcd   │◀────│ OpenResty│
└──────────┘     └──────────┘     └──────────┘
                                          │
                                          ▼
                                    ┌──────────┐
                                    │  upstream │
                                    └──────────┘

核心 Lua 代码(简化)

local etcd = require "resty.etcd"
local balancer = require "ngx.balancer"

local function get_weight()
    local client, err = etcd.new()
    if not client then return 1 end
    client:set_timeout(1000)
    local resp, err = client:get("/nginx/weight")
    if not resp then return 1 end
    return tonumber(resp.kvs[1].value) or 1
end

-- 在 balancer_by_lua_block 里调用
-- balancer_by_lua_block {
--     local weight = get_weight()
--     ngx.balancer.set_current_peer("10.0.0.21", weight)
-- }

关键坑

  1. etcd 单点:etcd 挂了,Lua 拿不到权重,需要 fallback 到默认值。
  2. 缓存权重:每次请求都查 etcd 会拖慢性能,用 lua_shared_dict 缓存 5~10 秒。
  3. 调试困难:Lua 报错只在 error.log,需要 lua_log_level debug 才能看到细节。

六、案例 6:njs 动态路由(nginx 原生 JS)

场景

  • 不想引入 OpenResty,但需要动态路由。
  • nginx 1.13.10+ 内置 njs,可以直接写 JS。

配置

load_module modules/ngx_http_js_module.so;

js_import canary from /etc/nginx/canary.js;

server {
    listen 443 ssl;
    server_name api.example.com;

    location / {
        set $backend canary.route($request);
        proxy_pass http://$backend;
    }
}

canary.js

function route(request) {
    const cookie = request.headers.cookie || "";
    if (cookie.includes("canary=1")) {
        return "canary";
    }
    // 5% 随机
    if (Math.random() < 0.05) {
        return "canary";
    }
    return "stable";
}

关键坑

  1. njs 是单线程:复杂逻辑会阻塞 nginx worker,避免在 njs 里做 IO。
  2. Math.random() 不稳定:同一用户多次请求可能路由到不同 upstream,需要结合 Cookie 固定。
  3. 调试工具少:njs 没有 DevTools,只能 error.log 看输出。

七、通用踩坑清单(生产环境必看)

7.1 配置类

坑 现象 解决
权重不精确 5% 实际 3%~8% 用 split_clients 或 Cookie 固定
keepalive 缓存 改权重不生效 nginx -s reload
location 优先级 路径不匹配 用 ^~ 或 = 精确匹配
Header 被吃掉 上游网关过滤 换不常见的 Header 名
Cookie 太大 400 Bad Request 调 large_client_header_buffers

7.2 监控类

坑 现象 解决
无健康检查 新版本挂了不摘除 配 max_fails + fail_timeout
无指标区分 看不出哪个版本出问题 加 X-Canary-Version Header
无回滚预案 出问题手忙脚乱 提前写好回滚脚本

7.3 回滚类

坑 现象 解决
回滚慢 改配置 + reload 要 1~2 分钟 用 OpenResty 动态权重
回滚不完整 部分 nginx 没 reload 用配置管理工具(Ansible/Salt)
回滚后数据不一致 新版本写了新字段 数据库迁移要向后兼容

八、回滚脚本模板

#!/bin/bash
# rollback.sh - 一键回滚到稳定版本

NGINX_CONF="/etc/nginx/conf.d/app.conf"
BACKUP_CONF="/etc/nginx/conf.d/app.conf.bak"

# 1. 备份当前配置
cp $NGINX_CONF ${NGINX_CONF}.rollback.$(date +%s)

# 2. 恢复备份
cp $BACKUP_CONF $NGINX_CONF

# 3. 检查配置
nginx -t
if [ $? -ne 0 ]; then
    echo "配置错误,回滚失败"
    exit 1
fi

# 4. 平滑 reload
nginx -s reload

echo "回滚完成,时间:$(date)"

九、监控指标建议

9.1 nginx 层面

  • upstream_response_time:新版本 vs 老版本延迟对比
  • upstream_status:5xx 错误率
  • upstream_addr:实际路由到哪个 upstream

9.2 业务层面

  • 新版本 vs 老版本的转化率、下单成功率
  • 新版本 vs 老版本的 P99 延迟
  • 新版本 vs 老版本的错误日志量

9.3 日志格式(推荐)

log_format canary '$remote_addr - $remote_user [$time_local] '
                  '"$request" $status $body_bytes_sent '
                  '"$http_referer" "$http_user_agent" '
                  'upstream=$upstream_addr '
                  'rt=$request_time '
                  'urt=$upstream_response_time '
                  'canary=$http_x_canary';

十、本章小结

案例 适用场景 复杂度 回滚速度
权重灰度 通用 低 中(需 reload)
Cookie 灰度 内部体验 中 快(改 Cookie)
Header 灰度 QA 测试 低 快(改 Header)
URL 路径灰度 API 版本迭代 低 中
OpenResty + etcd 频繁调权重 高 极快(无需 reload)
njs 动态路由 轻量动态 中 中

核心原则:

  1. 先小后大:1% → 5% → 20% → 50% → 100%
  2. 可观测:每个版本都要有独立指标
  3. 可回滚:回滚脚本提前写好,演练过
  4. 向后兼容:数据库迁移要兼容老版本

参考来源

  • nginx 官方文档 upstream 模块:https://nginx.org/en/docs/http/ngx_http_upstream_module.html
  • nginx 官方文档 map 模块:https://nginx.org/en/docs/http/ngx_http_map_module.html
  • nginx 官方文档 split_clients 模块:https://nginx.org/en/docs/http/ngx_http_split_clients_module.html
  • OpenResty 官方文档:https://openresty.org/en/
  • njs 官方文档:https://nginx.org/en/docs/njs/
  • 阿里云灰度发布最佳实践:https://www.alibabacloud.com/help/zh/edas/
  • 腾讯云灰度发布指南:https://cloud.tencent.com/document/product/468

06 方案对比与选型建议

本章横向对比 nginx 灰度发布的所有主流方案,从 8 个维度打分,给出场景化选型建议与决策树。


一、方案全景图

nginx 灰度发布的实现方法研究


二、8 维度评分对比

2.1 评分标准

维度 1 分 3 分 5 分
配置复杂度 需 Lua/JS 编程 需 map/geo 组合 单指令即可
回滚速度 >5 分钟 1~5 分钟 <1 分钟
分流精确度 偏差 >10% 偏差 3%~10% 偏差 <3%
可观测性 无版本标识 有 Header 标识 全链路追踪
运维成本 需专职 SRE 需熟悉 nginx 普通运维可操作
性能开销 >5% 1%~5% <1%
生态成熟度 小众/实验性 社区活跃 官方支持/大厂验证
扩展性 难扩展 中等 易扩展

2.2 评分表

方案 配置复杂度 回滚速度 分流精确度 可观测性 运维成本 性能开销 生态成熟度 扩展性 总分
权重灰度 5 3 2 2 5 5 5 3 30
Cookie 灰度 4 4 5 3 4 5 4 4 33
Header 灰度 4 4 5 3 4 5 4 4 33
URL 路径灰度 5 3 5 2 5 5 5 3 33
split_clients 4 3 5 2 4 5 4 3 30
OpenResty + etcd 2 5 5 4 2 3 4 5 30
njs 动态路由 3 3 4 3 3 4 3 4 27
K8s Ingress canary 3 4 4 5 3 4 5 5 33
Service Mesh 2 5 5 5 2 2 4 5 30

2.3 评分解读

  • 总分 33(最高):Cookie 灰度、Header 灰度、URL 路径灰度、K8s Ingress canary
  • 总分 30:权重灰度、split_clients、OpenResty + etcd、Service Mesh
  • 总分 27(最低):njs 动态路由(生态成熟度不足)

注意:总分高不代表适合所有场景。权重灰度虽然总分 30,但配置最简单、运维成本最低,适合大多数团队。


三、场景化选型建议

3.1 按团队规模

团队规模 推荐方案 理由
1~5 人(初创) 权重灰度 配置最简单,无需额外组件
5~20 人(成长期) Cookie 灰度 + 权重灰度 内部体验 + 外部放量
20~100 人(成熟期) K8s Ingress canary 自动化程度高,与 CI/CD 集成
100+ 人(大规模) Service Mesh + K8s 全链路灰度,多服务协同

3.2 按业务场景

业务场景 推荐方案 理由
电商大促 权重灰度 + OpenResty 动态权重 频繁调权重,无需 reload
内部工具 Cookie 灰度 只给内部员工体验
API 版本迭代 URL 路径灰度 v1/v2 共存,清晰隔离
支付/金融 Service Mesh 全链路追踪,合规审计
内容平台 Header 灰度 + AB 测试 与实验平台集成
微服务架构 Service Mesh 服务间灰度,不依赖 nginx

3.3 按技术栈

技术栈 推荐方案 理由
单体应用 + nginx 权重灰度 / Cookie 灰度 简单直接
Docker Compose 权重灰度 + split_clients 无需 K8s
Kubernetes K8s Ingress canary 原生支持,声明式
微服务 + 服务网格 Service Mesh 服务间灰度
混合云 nginx + Consul 跨云流量调度

四、决策树

nginx 灰度发布的实现方法研究


五、方案深度对比

对比项 权重灰度 Cookie 灰度
分流依据 随机权重 用户标识
用户体验 同一用户可能看到不同版本 同一用户始终看到同一版本
适用场景 外部用户放量 内部员工体验
配置复杂度 低 中
回滚速度 中(需 reload) 快(改 Cookie)
精确度 低(偏差大) 高(精确到用户)

选择建议:

  • 需要「同一用户看到同一版本」→ Cookie 灰度
  • 只需要「大致比例」→ 权重灰度

5.2 原生 nginx vs OpenResty

对比项 原生 nginx OpenResty
动态权重 不支持(需 reload) 支持(无需 reload)
配置复杂度 低 高(需 Lua 编程)
性能开销 无 1%~3%
运维成本 低 高(需 Lua 技能)
生态 官方支持 社区活跃

选择建议:

  • 调权重频率 < 1 次/天 → 原生 nginx
  • 调权重频率 > 1 次/天 → OpenResty

5.3 nginx vs K8s Ingress

对比项 原生 nginx K8s Ingress
部署方式 单机/集群 K8s 集群
配置方式 配置文件 YAML 声明式
自动化 手动 reload 自动同步
回滚速度 中 快(改 YAML)
可观测性 需手动配置 内置指标
学习成本 低 中(需 K8s 知识)

选择建议:

  • 已用 K8s → K8s Ingress
  • 未用 K8s → 原生 nginx

5.4 nginx vs Service Mesh

对比项 nginx Service Mesh
灰度粒度 请求级 服务级/请求级
服务间灰度 不支持 支持
全链路追踪 需手动配置 内置
性能开销 <1% 3%~5%
运维复杂度 低 高
适用架构 单体/简单微服务 复杂微服务

选择建议:

  • 单体应用 → nginx
  • 微服务架构 → Service Mesh

六、混合方案推荐

6.1 方案 A:nginx + K8s(边缘 + 集群内)

nginx 灰度发布的实现方法研究

适用场景:大规模生产环境,需要边缘灰度 + 集群内灰度。

优点:

  • 边缘层快速回滚
  • 集群内服务间灰度
  • 全链路可观测

缺点:

  • 架构复杂,运维成本高
  • 需要专职 SRE 团队

6.2 方案 B:nginx + Consul(服务发现 + 灰度)

nginx 灰度发布的实现方法研究

适用场景:非 K8s 环境,需要服务发现 + 灰度。

优点:

  • 服务自动注册/发现
  • 健康检查自动摘除
  • 配置中心统一管理

缺点:

  • 需维护 Consul 集群
  • 学习成本中等

6.3 方案 C:OpenResty + etcd + Prometheus(全动态)

nginx 灰度发布的实现方法研究

适用场景:需要频繁调权重,且需要全链路监控。

优点:

  • 改权重无需 reload
  • 全链路可观测
  • 自动化程度高

缺点:

  • 架构复杂
  • 需 Lua 编程技能
  • 运维成本高

七、成本对比

7.1 人力成本

方案 初始配置 日常运维 故障排查 总人力成本
权重灰度 0.5 人天 0.1 人天/周 0.5 人天/次 低
Cookie 灰度 1 人天 0.2 人天/周 1 人天/次 中
OpenResty + etcd 5 人天 0.5 人天/周 2 人天/次 高
K8s Ingress 3 人天 0.3 人天/周 1 人天/次 中
Service Mesh 10 人天 1 人天/周 3 人天/次 很高

7.2 基础设施成本

方案 额外组件 资源开销 总成本
权重灰度 无 无 低
Cookie 灰度 无 无 低
OpenResty + etcd etcd 集群 2C4G × 3 节点 中
K8s Ingress K8s 集群 已有 低
Service Mesh Istio 控制面 4C8G × 3 节点 高

八、风险与局限

8.1 各方案主要风险

方案 主要风险 缓解措施
权重灰度 权重不精确 用 split_clients 或 Cookie 固定
Cookie 灰度 Cookie 泄露 用签名 Cookie
Header 灰度 Header 可伪造 加 IP 白名单
OpenResty + etcd etcd 单点 etcd 集群 + fallback
K8s Ingress 配置错误导致全量故障 配置校验 + 灰度发布配置
Service Mesh 性能开销 压测 + 监控

8.2 常见误区

  1. 误区 1:灰度发布 = 权重灰度

    • 事实:灰度发布有多种形态,权重只是其中一种。
  2. 误区 2:灰度发布可以完全避免故障

    • 事实:灰度只能降低故障影响范围,不能避免故障。
  3. 误区 3:灰度发布需要复杂架构

    • 事实:最简单的权重灰度只需改一行配置。
  4. 误区 4:灰度发布后就不用监控了

    • 事实:灰度期间更需要监控,及时发现新版本问题。
  5. 误区 5:灰度发布可以无限期进行

    • 事实:灰度版本与老版本共存时间越长,维护成本越高,应尽快全量或回滚。

九、选型建议总结

9.1 快速选型表

你的情况 推荐方案
刚接触灰度发布 权重灰度
需要内部员工先体验 Cookie 灰度
需要 QA 测试新版本 Header 灰度
API 版本迭代 URL 路径灰度
需要频繁调权重 OpenResty + etcd
已用 Kubernetes K8s Ingress canary
微服务架构 Service Mesh
大规模生产 nginx + K8s + Service Mesh

9.2 演进路线

nginx 灰度发布的实现方法研究

建议:从权重灰度开始,逐步演进,不要一步到位。

9.3 关键原则

  1. 简单优先:能用简单方案解决的,不要用复杂方案。
  2. 可观测优先:灰度期间必须有监控,否则等于盲发。
  3. 可回滚优先:回滚脚本提前写好,演练过。
  4. 向后兼容:数据库迁移要兼容老版本。
  5. 小步快跑:1% → 5% → 20% → 50% → 100%,不要一步到位。

参考来源

  • nginx 官方文档 upstream 模块:https://nginx.org/en/docs/http/ngx_http_upstream_module.html
  • nginx 官方文档 map 模块:https://nginx.org/en/docs/http/ngx_http_map_module.html
  • nginx 官方文档 split_clients 模块:https://nginx.org/en/docs/http/ngx_http_split_clients_module.html
  • OpenResty 官方文档:https://openresty.org/en/
  • njs 官方文档:https://nginx.org/en/docs/njs/
  • Kubernetes Ingress 文档:https://kubernetes.io/docs/concepts/services-networking/ingress/
  • Istio 流量管理文档:https://istio.io/latest/docs/concepts/traffic-management/
  • 阿里云灰度发布最佳实践:https://www.alibabacloud.com/help/zh/edas/
  • 腾讯云灰度发布指南:https://cloud.tencent.com/document/product/468

07 落地清单:配置模板 / 监控 / 回滚 / 检查项

本章是把 02~06 章讲清楚的所有方法,收敛成一套「可以直接抄」的落地产物:

  1. 六套配置模板(权重 / ip_hash / split_clients / Cookie / Header / 白名单 / 组合)
  2. 监控指标清单(可对接 Prometheus / Grafana)
  3. 回滚剧本(分场景、带脚本)
  4. 发布节奏与发布前/中/后检查项

一、六套配置模板

所有模板均假设:stable 为稳定版本池,canary 为灰度版本池;测试命令统一为
nginx -t && nginx -s reload。

模板 1:权重灰度(入门,最快)

upstream backend {
    server 10.0.1.10:8080 weight=90;   # 稳定版 90%
    server 10.0.2.10:8080 weight=10;   # 灰度版 10%
}

server {
    listen 80;
    server_name example.com;
    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

调比例:改 weight → nginx -t && nginx -s reload。
注意:权重灰度是请求级随机,同一用户可能新旧版本来回跳(无粘性)。

模板 2:ip_hash(会话保持 + 灰度)

upstream backend {
    ip_hash;
    server 10.0.1.10:8080;             # 稳定版
    server 10.0.2.10:8080;             # 灰度版(1/N 的 IP 命中)
}

server {
    listen 80;
    server_name example.com;
    location / {
        proxy_pass http://backend;
    }
}

注意:ip_hash 只按客户端 IP 哈希,调整服务器列表会影响哈希分布;NAT 后的用户会被当成同一来源。适合「按 IP 分桶」的粗粒度灰度。

模板 3:split_clients(精确百分比,可对用户/UA 分桶)

http {
    # 按 客户端IP+UA 哈希,同一客户端稳定进同一桶
    split_clients "${remote_addr}${http_user_agent}" $pool {
        5%   "canary";
        *    "stable";
    }

    upstream stable { server 10.0.1.10:8080; }
    upstream canary { server 10.0.2.10:8080; }

    server {
        listen 80;
        location / {
            proxy_pass http://$pool;
        }
    }
}

注意:proxy_pass http://$pool 会走变量解析模式(需 resolver、丢 keepalive)。若在意连接池,改用 map 指向 upstream 名(见模板 6)。

http {
    # 用户灰度标记 → upstream 名(推荐写法,保留 keepalive)
    map $cookie_gray_flag $upstream_name {
        default   "stable";
        "1"       "canary";
    }

    upstream stable { server 10.0.1.10:8080; }
    upstream canary { server 10.0.2.10:8080; }

    server {
        listen 80;
        location / {
            proxy_pass http://$upstream_name;
            # 回包带灰度标记,前端可展示版本
            add_header X-Canary $cookie_gray_flag always;
        }
    }
}

种 Cookie 的两种方式:

  • 应用登录后下发 Set-Cookie: gray_flag=1(推荐,灰度判定在业务层)
  • nginx 首次访问按 split_clients 自动种(兜底)

回滚:清除/过期 Cookie 即可,无需 reload。

模板 5:Header 灰度(QA / 内部通道)

http {
    map $http_x_canary $upstream_name {
        default   "stable";
        "beta"    "canary";
    }

    upstream stable { server 10.0.1.10:8080; }
    upstream canary { server 10.0.2.10:8080; }

    server {
        listen 80;
        location / {
            # 防外部伪造:Header + 内网 IP 双校验
            if ($remote_addr !~ ^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)) {
                set $upstream_name "stable";
            }
            proxy_pass http://$upstream_name;
        }
    }
}

QA 用法:curl -H "X-Canary: beta" https://example.com/。
注意:外部流量到达前若有 CDN/WAF,需确认它们不会透传伪造 Header;或由边缘网关统一剥除。

http {
    # 1. 灰度标记 → 直接命中 canary
    map $cookie_gray_flag $upstream_name {
        default   "";
        "1"       "canary";
    }

    # 2. 无标记用户:按百分比兜底进 canary
    split_clients "${remote_addr}" $fallback_pool {
        10%  "canary";
        *    "stable";
    }

    upstream stable { server 10.0.1.10:8080; }
    upstream canary { server 10.0.2.10:8080; }

    server {
        listen 80;
        location / {
            # 有标记 → 走标记;无标记 → 走百分比兜底
            if ($upstream_name = "") {
                set $upstream_name $fallback_pool;
            }
            proxy_pass http://$upstream_name;
        }
    }
}

效果:被选中用户(Cookie=1)稳定在新版本;其他用户按 10% 比例随机进 canary;同一 IP 的用户因 split_clients 哈希而稳定在同一池。


二、监控指标清单

灰度期间「看不到指标 = 盲发」。建议至少覆盖以下 5 类指标,可对接 Prometheus(nginx-prometheus-exporter / nginx-module-vts)或自建日志采集。

2.1 流量与分流比例

指标 获取方式 用途
各 upstream 请求量 stub_status / vts 模块 确认分流比例符合配置
各 upstream 请求占比 日志按 $upstream_addr 聚合 发现比例漂移(如 split_clients 小流量偏差)
canary 池 QPS vts / exporter 判断灰度是否真正在承接流量

2.2 质量对比(新旧版本差异是灰度核心依据)

指标 对比口径 告警阈值(建议)
5xx / 4xx 错误率 canary vs stable canary 错误率 > stable ×2 即告警
平均/TP99 响应时间 canary vs stable TP99 超出 stable 20% 即告警
超时率 / 重试率 canary vs stable 出现即关注
连接失败 / 上游不可用 upstream health 单实例连续失败即摘除

2.3 业务指标(必须!)

  • 下单成功率、支付成功率、转化率、核心接口 P99
  • 建议 canary 与 stable 的业务大盘按版本标签分桶对比
  • 只盯技术指标不看业务指标 = 灰度白做

2.4 日志字段建议

log_format gray '$remote_addr [$time_local] "$request" $status '
                '$upstream_addr $upstream_status $request_time '
                'pool=$upstream_name gray_flag=$cookie_gray_flag';

用 pool= 与 gray_flag= 两个字段即可在日志平台一键拉出「canary 用户」全链路数据。


三、回滚剧本

原则:回滚优先于排查。灰度出问题先回滚止血,再慢慢查根因。回滚分四档,从轻到重。

3.1 回滚分级

级别 场景 操作 耗时 影响
L1 权重归零 新版本性能劣化 canary weight 改 0 + reload 秒级 新版本下线,老版本接全量
L2 Cookie 失效 新版本业务逻辑 bug 清灰度 Cookie / 改 map 默认值 秒级 灰度用户回老版本
L3 实例下线 新版本实例崩溃 upstream 摘除 canary server 秒级 同 L1
L4 整体回滚 配置/依赖严重不兼容 恢复备份配置 / 切换备份 upstream 分钟级 全量回老版本

3.2 回滚脚本示例(L1/L3:权重归零 + 摘实例)

#!/usr/bin/env bash
# rollback_canary.sh —— 灰度回滚脚本(需在灰度发布前写好并演练)
set -euo pipefail
NGINX_CONF="/etc/nginx/conf.d/gray.conf"

# 1. 备份当前配置
cp "$NGINX_CONF" "${NGINX_CONF}.bak.$(date +%s)"

# 2. 将 canary 权重改为 0(或注释 canary server 行)
sed -i 's/server 10\.0\.2\.10:8080 weight=10;/# server 10.0.2.10:8080 weight=10;  # ROLLED BACK/' "$NGINX_CONF"

# 3. 校验并 reload
nginx -t && nginx -s reload && echo "✅ 已回滚:canary 流量归零"

# 4. 观察 5 分钟确认错误率回落
echo "请观察 5 分钟:canary QPS 应为 0,stable 错误率应回落到基线。"

3.3 回滚后的处理

  1. 保留现场:日志、监控截图、请求样本
  2. 封版定位:新版本代码/数据回滚到上一个稳定提交
  3. 复盘:灰度指标哪些没看、为什么没拦住
  4. 修复后重新走一遍灰度流程(不要直接全量)

四、发布节奏(放量计划)

阶段 比例 观察时长 通过条件(全部满足才进下一档)
内部/白名单 固定名单 4~8 小时 业务指标无劣化
小流量 1% 24 小时 错误率 ≤ stable,TP99 无劣化
中流量 5%~10% 24~48 小时 业务大盘与 stable 持平
大流量 20%~50% 48 小时 全指标稳定
全量 100% — 保留 canary 池 24h 再清理

每次放量都要:改配置 → nginx -t → reload → 核对分流比例 → 看 30 分钟指标 → 决定下一步。


五、发布前 / 中 / 后检查项(Checklist)

5.1 发布前

  • 新旧版本对同一数据库/缓存的兼容性已验证(回滚不丢数据)
  • 回滚脚本已写好并演练过(本机跑通)
  • 监控大盘:技术指标 + 业务指标 + 版本标签都已就绪
  • nginx -t 通过,配置有 git 版本管理
  • 明确本次灰度负责人、告警接收人、回滚决策人

5.2 发布中

  • 每次放量后 30 分钟内核对:实际分流比例 vs 配置比例
  • canary 错误率/TP99 与 stable 实时对比
  • 业务大盘按版本分桶对比(不只是技术指标)
  • 告警通道确认能收到(发一条测试告警)

5.3 发布后

  • 全量后保留 canary 池 24 小时观察
  • 清理:删掉临时分流 map / 过期 Cookie / 灰度注释
  • 配置归档:把最终稳定配置提交 git
  • 复盘记录:放量节奏是否合适、指标是否够用

六、与 05 章的关系

  • 05 章讲「别人踩过哪些坑、真实案例怎么收场」——偏认知
  • 本章给「你上线时直接抄的模板 + 指标 + 脚本 + checklist」——偏执行
  • 建议把本章模板 + 05 章踩坑清单合并成团队发布 SOP 文档,每季度演练一次回滚。

参考来源

  • nginx stub_status 模块文档:https://nginx.org/en/docs/http/ngx_http_stub_status_module.html
  • nginx-prometheus-exporter:https://github.com/nginxinc/nginx-prometheus-exporter
  • nginx-module-vts(虚拟主机流量状态):https://github.com/vozlt/nginx-module-vts
  • nginx log_format 文档:https://nginx.org/en/docs/http/ngx_http_log_module.html
  • split_clients 官方文档:https://nginx.org/en/docs/http/ngx_http_split_clients_module.html
微信扫一扫支付
微信logo微信扫一扫,打赏作者吧~
不喜欢1

本文链接:https://5x10.cn/post/699.html

猜你喜欢

网友评论

随机文章
热门标签