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 只做对照,不展开。
问题树
- 什么是 nginx 灰度发布:定义、与蓝绿/金丝雀/AB 测试的关系与差异
- 原生能力能做到什么:upstream 权重、hash、ip_hash、least_conn、mirror、split_clients
- 进阶分流维度:Cookie / Header / URL / 用户 ID / 一致性哈希 / 白名单
- 动态化与热更新:reload 的代价、Lua/njs 动态 upstream、OpenResty、etcd+lua-resty-balancer
- 典型架构与方案对比:单机 nginx / 双机蓝绿 / 多版本金丝雀 / 云原生 Ingress 对照
- 落地清单:配置模板、监控指标、回滚剧本、常见坑
章节状态表
| # | 章节 | 状态 | 🟡 进行中(待补正文) |
|---|---|---|---|
| 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 作为流量入口,天然适合做灰度分流,因为它:
- 在请求进入业务之前就能做决策(L7 层)
- 配置即代码,可版本化、可回滚
- 性能开销极低(C 语言、事件驱动)
- 支持多种分流维度:权重、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 常见误区
- 把灰度当 AB:灰度是短期验证,AB 是长期实验。混用会导致数据污染。
- 忽略回滚路径:灰度前必须确认「一键回滚」可行,否则灰度失败会变成事故。
- 只看新版本指标:灰度期间必须同时监控老版本,防止新版本拖垮共享资源(DB/缓存)。
- 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/)。
3.6 按 Header/Cookie 分流(定向灰度)
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 基础能力,本章解决三个更贴近生产的问题:
- 按用户身份分流(Cookie / Header / 白名单)——让"内部员工先尝"、"特定租户先尝"、"特定 App 版本先尝"
- 动态权重(OpenResty + etcd / Redis)——不 reload 就能改权重
- njs 脚本化路由——用 JavaScript 在 nginx 里做复杂判断
三种方案按"改造成本"从低到高排列,可组合使用。
4.1 按 Cookie 分流:让"同一用户"稳定命中同一版本
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,但普通用户不会主动带。常见做法:
- 业务方在响应里下发:
Set-Cookie或Set-Header,让后续请求自动带上 - 前置网关注入:在 nginx 前面再加一层(如 API Gateway),根据用户身份注入 Header
- 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 进阶分流的"坑"
4.7.1 Cookie 分流的"首次访问"问题
首次访问无 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% | — | 全量 |
关键坑
- 权重不是精确比例:nginx 权重是「相对概率」,短窗口内可能偏差较大。5% 权重在 100 次请求里可能实际是 3%~8%。
- keepalive 会缓存连接:改权重后,已建立的长连接仍走老 upstream,需要
nginx -s reload才彻底生效。 - 健康检查缺失:新版本挂了,nginx 不会自动摘除,除非配
max_fails=1 fail_timeout=10s。
二、案例 2:内部员工先体验(Cookie 灰度)
场景
- 新版本只给内部员工(约 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;
}
}
}
员工如何拿到 Cookie
- 方案 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 /;
}
关键坑
- Cookie 大小限制:nginx 默认
large_client_header_buffers4×8k,Cookie 太多会 400。 - 跨域 Cookie:
SameSite属性没设,跨站请求带不上 Cookie,员工在 iframe 里就失效。 - 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
关键坑
- 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;
}
- 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;
}
}
关键坑
- location 优先级:
/v2/是前缀匹配,/v2不带斜杠不会命中。用location = /v2精确匹配或location ^~ /v2/提高优先级。 - 静态资源路径:如果新版本改了静态资源路径,nginx 的
try_files可能 404。 - 反向代理回退:新版本挂了,用户看到 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)
-- }
关键坑
- etcd 单点:etcd 挂了,Lua 拿不到权重,需要 fallback 到默认值。
- 缓存权重:每次请求都查 etcd 会拖慢性能,用
lua_shared_dict缓存 5~10 秒。 - 调试困难: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";
}
关键坑
- njs 是单线程:复杂逻辑会阻塞 nginx worker,避免在 njs 里做 IO。
- Math.random() 不稳定:同一用户多次请求可能路由到不同 upstream,需要结合 Cookie 固定。
- 调试工具少: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% → 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/
- 阿里云灰度发布最佳实践:https://www.alibabacloud.com/help/zh/edas/
- 腾讯云灰度发布指南:https://cloud.tencent.com/document/product/468
06 方案对比与选型建议
本章横向对比 nginx 灰度发布的所有主流方案,从 8 个维度打分,给出场景化选型建议与决策树。
一、方案全景图
二、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 | 跨云流量调度 |
四、决策树
五、方案深度对比
5.1 权重灰度 vs Cookie 灰度
| 对比项 | 权重灰度 | 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(边缘 + 集群内)
适用场景:大规模生产环境,需要边缘灰度 + 集群内灰度。
优点:
- 边缘层快速回滚
- 集群内服务间灰度
- 全链路可观测
缺点:
- 架构复杂,运维成本高
- 需要专职 SRE 团队
6.2 方案 B:nginx + Consul(服务发现 + 灰度)
适用场景:非 K8s 环境,需要服务发现 + 灰度。
优点:
- 服务自动注册/发现
- 健康检查自动摘除
- 配置中心统一管理
缺点:
- 需维护 Consul 集群
- 学习成本中等
6.3 方案 C:OpenResty + etcd + Prometheus(全动态)
适用场景:需要频繁调权重,且需要全链路监控。
优点:
- 改权重无需 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:灰度发布 = 权重灰度
- 事实:灰度发布有多种形态,权重只是其中一种。
-
误区 2:灰度发布可以完全避免故障
- 事实:灰度只能降低故障影响范围,不能避免故障。
-
误区 3:灰度发布需要复杂架构
- 事实:最简单的权重灰度只需改一行配置。
-
误区 4:灰度发布后就不用监控了
- 事实:灰度期间更需要监控,及时发现新版本问题。
-
误区 5:灰度发布可以无限期进行
- 事实:灰度版本与老版本共存时间越长,维护成本越高,应尽快全量或回滚。
九、选型建议总结
9.1 快速选型表
| 你的情况 | 推荐方案 |
|---|---|
| 刚接触灰度发布 | 权重灰度 |
| 需要内部员工先体验 | Cookie 灰度 |
| 需要 QA 测试新版本 | Header 灰度 |
| API 版本迭代 | URL 路径灰度 |
| 需要频繁调权重 | OpenResty + etcd |
| 已用 Kubernetes | K8s Ingress canary |
| 微服务架构 | Service Mesh |
| 大规模生产 | nginx + K8s + Service Mesh |
9.2 演进路线
建议:从权重灰度开始,逐步演进,不要一步到位。
9.3 关键原则
- 简单优先:能用简单方案解决的,不要用复杂方案。
- 可观测优先:灰度期间必须有监控,否则等于盲发。
- 可回滚优先:回滚脚本提前写好,演练过。
- 向后兼容:数据库迁移要兼容老版本。
- 小步快跑: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 章讲清楚的所有方法,收敛成一套「可以直接抄」的落地产物:
- 六套配置模板(权重 / ip_hash / split_clients / Cookie / Header / 白名单 / 组合)
- 监控指标清单(可对接 Prometheus / Grafana)
- 回滚剧本(分场景、带脚本)
- 发布节奏与发布前/中/后检查项
一、六套配置模板
所有模板均假设:
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)。
模板 4:Cookie 灰度(用户级精确分流)
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;或由边缘网关统一剥除。
模板 6:组合方案(Cookie 精确分流 + 权重兜底)
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 回滚后的处理
- 保留现场:日志、监控截图、请求样本
- 封版定位:新版本代码/数据回滚到上一个稳定提交
- 复盘:灰度指标哪些没看、为什么没拦住
- 修复后重新走一遍灰度流程(不要直接全量)
四、发布节奏(放量计划)
| 阶段 | 比例 | 观察时长 | 通过条件(全部满足才进下一档) |
|---|---|---|---|
| 内部/白名单 | 固定名单 | 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

微信扫一扫,打赏作者吧~









网友评论