灰度发布
同一服务的旧版本和新版本可以同时运行。灰度发布通过入口规则或业务开关,让一部分请求先使用新版本,观察结果后再扩大使用范围。需要同时管理的是实例、流量、共享数据和恢复路径:新实例启动以后,哪类用户会进入它,异常时怎样停止影响,都要有明确答案。
发布策略控制的是副本还是请求
滚动、蓝绿与金丝雀
| 策略 | 改变的对象 | 解决的问题 | 额外准备 |
|---|---|---|---|
| 滚动更新 | 逐批替换实例 | 保留部分服务能力,减少停机 | 就绪检查、剩余容量、版本兼容 |
| 蓝绿发布 | 两组环境与入口指向 | 一组准备完成后整体切换 | 两组容量、会话、连接排空 |
| 金丝雀发布 | 候选及其请求集合 | 少量实际业务验证后扩大 | 选组规则、分版本指标、停止条件 |
| 功能开关 | 代码中的功能分支 | 部署与功能开放分开 | 开关鉴权、默认值、审计、清理 |
九个旧 Pod 和一个新 Pod,不能保证 10% 请求进入新版本。连接复用、负载分布、就绪状态和路由策略都会改变流量。滚动时的新旧实例共存,若没有观察窗口和分流控制,也没有自动成为金丝雀。
蓝绿切回旧组可以很快,数据库却可能已被新组写入。切换网络入口与撤销业务写入需要分别处理。功能开关只影响它控制的分支,也不能替代进程启动、容量和依赖验证。
分流粒度决定用户体验
按请求权重适合无会话、可并行处理的调用,同一用户可能连续进入不同版本。按用户或租户稳定分桶,则让同一主体在观察期持续使用同一版本。
外部请求
→ 认证入口验证身份,删除客户端自带路由头
→ 生成可信用户键 / 租户键
→ 用固定盐值计算分组
├── 稳定组 → 旧版本
└── 候选组 → 新版本
→ 分别记录状态、耗时、业务结果和实际处理版本公网请求中的 X-User-Id、X-Canary 或自报租户都能被用户修改。认证入口必须覆盖这些字段,应用也只能接受来自可信入口的身份。匿名用户可使用服务端签发的标识;IP 分组会让 NAT 后多人共用一个组,地址变化还会迁移用户。
用户集合比例与请求比例不同。大型租户即使只占一个分桶,也可能贡献大量写入。选择比例前要看业务分布,不能仅按用户数推算影响。
在本机验证 Nginx 分流与回退
准备不接触业务系统的实验
使用 Linux、Docker Compose、curl 和 Nginx1.30.4。发行线及包来源可查 Nginx 下载页。下载 实验配置,解压后在该目录执行:
mkdir -p runtime
cp canary.conf stable.conf failed.conf runtime/
cp runtime/canary.conf runtime/active.conf
docker compose config
docker compose up -d
docker compose exec gateway nginx -v
docker compose exec gateway nginx -t -c /lab/active.confCompose 只发布回环 18090 端口。Nginx 以 UID/GID101 运行,配置只读挂载,PID 和临时文件写入独立 tmpfs,不挂 Docker socket:
name: gray-lab
services:
gateway:
image: nginx:1.30.4-alpine
user: '101:101'
entrypoint: ['nginx', '-g', 'daemon off;', '-c', '/lab/active.conf']
ports:
- '127.0.0.1:18090:8080'
volumes:
- ./runtime:/lab:ro
read_only: true
tmpfs:
- /tmp:uid=101,gid=101,mode=0700
cap_drop: [ALL]
security_opt: ['no-new-privileges:true']三个 server 分别模拟入口、旧应用和新应用,后两个只监听容器回环。它们返回固定版本文本,仅验证代理分流,不代表 Spring Boot、数据库或实际业务通过测试。
宿主 127.0.0.1:18090
→ 容器 8080:代理入口
├── 127.0.0.1:8081:stable-v1
└── 127.0.0.1:8082:candidate-v2分桶配置怎样选择 upstream
canary.conf 完整内容如下:
worker_processes 1;
pid /tmp/nginx.pid;
error_log /dev/stderr notice;
events { worker_connections 256; }
http {
client_body_temp_path /tmp/client;
proxy_temp_path /tmp/proxy;
fastcgi_temp_path /tmp/fastcgi;
uwsgi_temp_path /tmp/uwsgi;
scgi_temp_path /tmp/scgi;
log_format rollout 'cohort=$cohort upstream=$upstream_addr status=$status upstream_status=$upstream_status rt=$request_time urt=$upstream_response_time';
access_log /dev/stdout rollout;
split_clients "${http_x_lab_user}:orders-v2" $bucket {
10% candidate;
* stable;
}
map $http_x_lab_user $cohort {
"" stable;
default $bucket;
}
upstream stable { server 127.0.0.1:8081; }
upstream candidate { server 127.0.0.1:8082; }
server {
listen 8080;
location / {
proxy_pass http://$cohort;
proxy_next_upstream off;
proxy_connect_timeout 1s;
proxy_read_timeout 3s;
add_header X-Lab-Cohort $cohort always;
add_header Cache-Control no-store always;
}
}
server {
listen 127.0.0.1:8081;
location / { default_type text/plain; return 200 "stable-v1\n"; }
}
server {
listen 127.0.0.1:8082;
location / { default_type text/plain; return 200 "candidate-v2\n"; }
}
}split_clients 对“演示用户键 + 固定盐值”计算哈希,将哈希空间的一段分给 candidate,其余为 stable。10% 便于实验观察,不是生产推荐值。相同键、盐和区间配置得到相同组;更换任一项都可能改变分组。split_clients说明了哈希与百分比规则。
map 将没有演示用户头的请求送入稳定组。proxy_pass 的变量只会得到两个已定义的 upstream 名称,不接受用户任意提供的目标主机。生产将两个模拟 server 换为独立应用,仍保留受控的 upstream 集合。
proxy_next_upstream off 使候选错误直接暴露,不重试到其他节点掩盖它。生产写接口还要结合请求是否已发送、业务幂等和客户端重试分析,相关选项见 代理模块。
查看一次请求,再统计不同用户
curl -q --noproxy '*' --fail-with-body -i \
http://127.0.0.1:18090/状态应为 200,X-Lab-Cohort 为 stable,正文 stable-v1。再给不同用户发送请求:
mkdir -p observations
: > observations/versions.txt
for i in $(seq 1 200); do
curl -q --noproxy '*' --fail-with-body --silent --show-error \
-H "X-Lab-User: user-$i" http://127.0.0.1:18090/ \
>> observations/versions.txt || break
done
wc -l observations/versions.txt
sort observations/versions.txt | uniq -c总行数应为 200,出现两类版本,候选占一部分。小样本不要求恰好 20 条;观察的是确定性分桶结果。遇到 HTTP 错误后文件可能混入错误正文,应先修复,再重新统计,不继续当完整成功样本使用。
从候选用户中选择一个键,检查同键稳定性:
CANARY_USER=''
for i in $(seq 1 200); do
version=$(curl -q --noproxy '*' --fail-with-body --silent --show-error \
-H "X-Lab-User: user-$i" http://127.0.0.1:18090/) || break
if [ "$version" = candidate-v2 ]; then
CANARY_USER="user-$i"
printf 'candidate key: %s\n' "$CANARY_USER"
break
fi
done
test -n "$CANARY_USER"
for i in $(seq 1 5); do
curl -q --noproxy '*' --fail-with-body --silent --show-error \
-H "X-Lab-User: $CANARY_USER" http://127.0.0.1:18090/
done五次都应返回 candidate-v2。演示字段只限回环实验,不作为公网身份依据。每次换随机用户发请求,也不能检验同一用户的稳定性。
候选失败与切回
failed.conf 只将候选模拟应用的返回状态改为 503。先校验,再替换活动文件并 reload:
docker compose exec gateway nginx -t -c /lab/failed.conf
cp runtime/failed.conf runtime/active.conf
docker compose exec gateway nginx -s reload -c /lab/active.confreload 后新 worker 接管需要时间。用原候选键重复观察,直到新配置生效。预期错误不加 fail 选项,分别检查传输退出码、HTTP 状态和正文:
http_code=$(curl -q --noproxy '*' --silent --show-error \
-H "X-Lab-User: $CANARY_USER" \
-o observations/failed-body.txt -w '%{http_code}' \
http://127.0.0.1:18090/)
transport_rc=$?
printf 'transport=%s http=%s\n' "$transport_rc" "$http_code"
cat observations/failed-body.txt生效后应为 transport=0、http=503、candidate-v2。无用户头的请求仍返回稳定版本的 200,说明异常限定在候选组。
stable.conf 保留 upstream,将 map 的 default 改成 stable。校验并应用:
docker compose exec gateway nginx -t -c /lab/stable.conf
cp runtime/stable.conf runtime/active.conf
docker compose exec gateway nginx -s reload -c /lab/active.conf
curl -q --noproxy '*' --fail-with-body \
-H "X-Lab-User: $CANARY_USER" http://127.0.0.1:18090/新 worker 接管后,原候选键也返回 stable-v1。随后验证无身份和其他键,再看日志是否仍有候选流量。结束后停止这份实验:
docker compose logs --tail=30 gateway
docker compose downdown 删除本实验的容器和网络,runtime 和 observations 文件保留。不要在其他项目目录执行清理。
将实验扩展到实际服务
权重、分桶和连接排空
Nginx upstream 的 weight 用于向可用服务器分配请求,不保证每个短窗口都严格符合比例。节点状态和负载均衡配置影响结果,见 upstream 模块。
用户分桶决定版本组,组内负载均衡决定具体实例。扩容候选组通常只改变组内分配;更换分桶盐值可能让大量用户换组。分开管理这两项配置,才能解释发布过程中的变化。
Nginx reload 尝试读入配置、启动新 worker,并让旧 worker 逐步退出。信号命令成功时,旧连接可能仍在处理。WebSocket、长轮询和大文件下载需要排空方案,进程行为见 Nginx 控制方式。刚切流就删除旧实例,仍可能中断请求。
灰度前确认候选能承担实际负载。少量用户也可能包含大租户、热点键和后台任务;候选只有一台时尤其容易饱和。扩容、限流和暂停放量应在持续错误扩散前执行。
观测候选与稳定版本
日志和指标至少区分实际处理版本、路由组与接口。入口记录 upstream 地址、状态、耗时,应用记录包内版本和请求关联 ID,才能发现“路由进了新组,应用仍是旧包”。日志变量见 Nginx 日志模块。
| 判断 | 观察的数据 | 常见误判 |
|---|---|---|
| 是否真的分流 | 分组请求数、upstream、应用版本 | 只看配置百分比 |
| 候选是否恶化 | 同接口、同窗口错误率和延迟分布 | 与全站平均值比较 |
| 样本是否足够 | 请求数、独立用户或租户、关键路径 | 没请求却因零错误通过 |
| 是否影响业务 | 订单成功率、重复写入、库存差异 | 只看 HTTP 200 |
| 是否容量不足 | CPU、线程等待、连接池、队列 | 只看进程存活 |
发布前保存稳定组基线,确定业务观察窗口和最低样本条件。阈值依据 SLO、负载和允许损失制定。“1% 等五分钟再到 5%”无法通用于夜间低流量、支付或租户极不均衡的系统。
监控数据缺失时暂停放量。错误预算明显消耗、业务结果异常或版本分布无法解释时,先停止扩张,必要时切回稳定组。发布页面绿色不能替代这些判断。
会话、缓存与调用链
本地会话会在切换版本后丢失。共享会话或无状态令牌还需要新旧代码都能解释字段与签名。缓存结构升级可使用版本化键,但两套缓存增加容量和回源压力,不能为了隔离灰度拖垮数据库。
边缘缓存可能使候选代码没有执行。实验用 no-store 便于观察;生产按业务控制缓存键,必要时区分组别并核对命中。不同用户的私有数据不能因共享键混用。
入口只分流第一跳。候选服务 A 调用服务 B 时,需要明确新旧组合是否兼容。全链路灰度可传播可信版本上下文并在下游选组;未覆盖的路由要有默认行为,不把内部候选头任意透传给第三方。
消息消费者和定时任务不经过 HTTP 入口,候选部署后可能立即消费或执行任务。它们要单独控制消费者组、订阅、分区和调度身份。流量镜像只适合能隔离副作用的请求,不直接复制付款、下单等写入。
数据库兼容决定回退能力
同库运行采用渐进变更:先加兼容字段或表,部署兼容新旧数据的代码,回填并对照验证,再切换读取路径。删除旧字段、收紧约束和改变语义,应等旧版本与回退窗口结束。
双写需要定义失败处理和权威数据来源;两处都调用写入仍可能只有一处成功。回填限速、记录进度并允许重试。灰度结束前确认旧代码能够读取新代码产生的数据;不能读取时,恢复可能需要向前修复或关闭功能,而非重启旧镜像。
停止候选流量只阻止后续请求。已有订单、消息、库存和外部支付动作,需要对账、补偿或人工处理。不要恢复整个数据库快照来撤销一小组用户的变更。
Kubernetes 和网关
Deployment 控制 Pod 替换,Service 选择后端,Gateway API 或网关负责请求路由。社区 ingress-nginx 已退休并停止维护,新部署不继续采用它作为默认入口;迁移背景见 Kubernetes 官方公告。Nginx 软件本身和其他厂商产品不因此退役。
支持 Gateway API 的控制器可通过 HTTPRoute backendRefs 在两个 Service 之间分流。下列片段要求两个 Service、Gateway、允许的路由绑定和对应端口已存在:
rules:
- backendRefs:
- name: orders-stable
port: 8080
weight: 90
- name: orders-canary
port: 8080
weight: 10权重按同一规则的总和解释,控制器需要支持相应能力。检查 HTTPRoute 的 Accepted、ResolvedRefs 等状态,再验证实际请求。Gateway API 分流指南给出完整对象示例。
Argo Rollouts 可以描述 setWeight、pause 和分析步骤;配置流量路由集成后控制入口权重,没有集成时可能仅用副本数量近似流量比例。Argo Canary区分这两种情况。分析需要真实指标提供方、最低样本、超时与失败处理,安装控制器不会自动补齐。
Spring Cloud Gateway、APISIX 或其他网关同样需要核对身份来源、规则传播、会话稳定性和恢复速度。不要让 CDN、Nginx、网关和应用各自随机分流,使最终比例无法解释。
按流量与业务状态恢复
配置改了,实际没变
先检查运行实例读取的配置:
docker compose exec gateway nginx -T -c /lab/active.conf
docker compose logs --tail=80 gateway
curl -q --noproxy '*' --fail-with-body -i \
-H "X-Lab-User: $CANARY_USER" http://127.0.0.1:18090/核对挂载路径、活动文件、map 默认值和 upstream。真实系统要固定入口、身份、协议和缓存条件,避免对照请求走了不同 CDN 或代理。加载失败时旧 worker 继续运行,修正文件后先 nginx -t,再 reload 与复测。
候选错误,稳定仍正常
暂停放量,保留失败请求 ID、版本、上游状态和应用异常。502 常见于连接失败或无可用后端,503 可能来自应用、网关或其他层;结合 upstream_status 和应用日志定位。
切回后,用此前候选身份复测,再检查版本分布与业务指标。连接排空前保留实例用于定位,避免中断在途请求;候选持续产生业务副作用时,按应急授权停止相应消费者或功能。
切回后仍异常
两组都失败时,检查共享数据库、缓存、消息、配置和入口。资源竞争可能持续,错误写入也可能被旧代码读取。此时反复切换只增加变量,应停止自动重试,定位共享依赖和已发生的业务影响。
发布记录保留新旧制品身份、路由配置、观察结果、回退动作和实际恢复版本。回退成功仍表示本次新版本发布失败,不自动转为继续放量。
中国大陆、内网和离线交付
Nginx 分流可在内网完成,不依赖公网 SaaS。准备网关镜像、新旧应用镜像、配置、证书信任、监控依赖和回退文件;Kubernetes 场景还需控制器镜像、CRD、Chart 与已验证 values。
联网准备机固定版本并导出,目标校验后导入或推入企业仓库。在无公网条件下再跑分流、日志、错误注入与恢复,避免镜像齐全但指标查询或审批仍依赖公网。传输见 离线部署,镜像身份与保留见 Harbor。
权威资料与规范地址
Nginx
- 发行版本:https://nginx.org/en/download.html
- split_clients:https://nginx.org/en/docs/http/ngx_http_split_clients_module.html
- upstream:https://nginx.org/en/docs/http/ngx_http_upstream_module.html
- 代理参数:https://nginx.org/en/docs/http/ngx_http_proxy_module.html
- 进程与 reload:https://nginx.org/en/docs/control.html
- 日志:https://nginx.org/en/docs/http/ngx_http_log_module.html
渐进交付
- Ingress NGINX 退役:https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement/
- Gateway API 分流:https://gateway-api.sigs.k8s.io/guides/user-guides/traffic-splitting/
- Argo Rollouts Canary:https://argo-rollouts.readthedocs.io/en/stable/features/canary/
