金丝雀发布的核心不是把流量切一部分给新版本,而是切完之后有没有能力从日志、指标和链路中看出新版本到底健不健康。很多故障在1%流量时已经暴露,只是日志里新旧请求混在一起,没有版本维度,最终误判为偶发问题。Nginx作为流量入口,天然适合在接入层完成分流和日志标记。

一、用Nginx变量打通分流与访问日志
金丝雀发布的第一步是让新旧版本的请求在日志中能被区分。Nginx 可以在 split_clients 或 map 中生成一个 $canary_version 变量,这个变量既能决定请求转发到哪个 upstream,也能写进 access_log。这样同一个访问日志文件里,每一行都带有稳定版或金丝雀版的标记。
基于用户标识的随机分流更接近真实流量分布。比如按照客户端地址和 User-Agent 的哈希值,将 10% 的请求划入金丝雀池。Nginx 的 split_clients 指令适合做这类百分比切分。下面是一个完整的配置示例:
http {
log_format canary_log '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" canary_version=$canary_version request_time=$request_time upstream_time=$upstream_response_time';
split_clients "${remote_addr}${http_user_agent}" $canary_version {
10% canary;
* stable;
}
upstream backend_stable {
server 10.0.0.10:8080;
keepalive 32;
}
upstream backend_canary {
server 10.0.0.20:8080;
keepalive 32;
}
map $canary_version $upstream_pool {
canary backend_canary;
default backend_stable;
}
server {
listen 80;
access_log /var/log/nginx/access.log canary_log;
location / {
proxy_pass http://$upstream_pool;
proxy_set_header X-Canary-Version $canary_version;
}
}
}
split_clients 会根据第一个参数计算哈希,并按百分比映射到不同取值。map 再把 $canary_version 转成实际的 upstream 名称,proxy_pass 使用变量方式转发。最后 log_format 中显式输出 canary_version,后续统计时只需要按这个字段过滤。
如果希望更灵活地让测试用户主动进入金丝雀,也可以用请求头或 Cookie 分流。比如 map $http_x_canary_flag $canary_version { canary canary; default stable; },测试人员在请求上带上指定头即可命中新版本。这种方式不适合大范围灰度,但适合内部验证和定向放量。
二、渐进观察需要看哪些指标
日志打上标记只是开始,接下来要确定观察什么。金丝雀日志不能只看有没有报错,还要看错误率与稳定版本的差值。因为网络抖动、依赖超时等背景噪声在任何版本中都可能出现,只有对比才能降低误报。
建议把指标分为三类。第一类是请求级健康度,包括 HTTP 5xx 比例、业务错误码字段、部分 4xx 异常增长。第二类是延迟指标,重点关注 request_time 和 upstream_response_time 的 P95、P99。第三类是资源与依赖指标,例如上游连接复用率、慢查询数量、缓存命中率等。前两类可以直接从 Nginx 日志中获得,第三类需要结合应用日志。
观察窗口至少覆盖一个完整业务周期。比如 10% 流量观察 30 分钟,这段时间内金丝雀错误率如果超过稳定版本的 1.5 倍,或 P95 延迟增加超过 20%,就应暂停放量。不能只看绝对阈值,因为某些业务在夜间错误率本身就低,白天高,同时段对比更可靠。
使用 awk 可以快速做初步统计。以下命令统计访问日志中金丝雀版本的 5xx 数量:
grep 'canary_version=canary' /var/log/nginx/access.log | awk '$9 ~ /^5/ {count++} END {print count+0}'
这里假设日志格式中的 $status 是第 9 列。实际列顺序取决于日志格式定义,统计时需要先确认。更推荐的做法是接入 ELK、Loki、ClickHouse 等日志系统,通过结构化字段 canary_version 做聚合查询。
三、从日志统计到放量与回滚
有了版本维度的日志统计,放量策略就可以从拍脑袋变成数据驱动。典型路径是 2%、5%、10%、30%、100%,每一步观察 15 分钟到数小时不等。每次放量前查看上一阶段的错误率、P95 延迟和业务失败码,只有当金丝雀与稳定版本的差值在阈值内时,才继续调整权重。
调整权重只需修改 split_clients 中的百分比,然后执行 nginx -s reload。例如从 10% 提升到 30%,把配置改成 30% canary; * stable;。如果出现问题,最快的方式是把百分比改为 0% 或直接下线金丝雀 upstream。但要注意 reload 只是更新配置,不能打断已经建立的 keepalive 长连接,因此回滚后短时间内仍可能有少量旧连接命中金丝雀。
更进一步的实践是把日志告警与回滚动作串起来。监控系统可以每分钟查询金丝雀 5xx 比例,如果连续 3 个点超过稳定版本 2 倍,就触发告警;超过 5 个点则调用脚本修改 Nginx 配置并 reload。这个闭环不一定要完全自动化,但至少要做到告警信息里带出版本标记和错误样本,否则值班人员很难快速定位。
#!/bin/bash
CANARY_ERRORS=$(grep 'canary_version=canary' /var/log/nginx/access.log \
| awk '$9 ~ /^5/' | wc -l)
CANARY_TOTAL=$(grep 'canary_version=canary' /var/log/nginx/access.log | wc -l)
if [ "$CANARY_TOTAL" -gt 0 ]; then
RATE=$((CANARY_ERRORS * 100 / CANARY_TOTAL))
echo "canary_error_rate=$RATE%"
# 如果错误率超过阈值,调用配置工具将金丝雀权重降为0并reload
if [ "$RATE" -gt 5 ]; then
sed -i 's/10% canary;/0% canary;/' /etc/nginx/nginx.conf
nginx -s reload
fi
fi
这个脚本只是示例,实际环境中不建议对配置文件做危险的 sed 替换。更稳妥的做法是使用配置模板渲染后替换,并由配置管理平台记录变更历史。日志统计也可以从文件读取改为查询集中式日志系统,避免多台 Nginx 节点统计不全。
四、避开金丝雀日志的常见坑
第一个坑是标记变量为空。有的团队在 log_format 中写了版本字段,但没在对应的 server 或 location 中生成变量,最终日志里全部是空值。排查时先使用 curl -I 请求后直接看日志行,确认 canary_version= 后面是否有稳定版或金丝雀版。
第二个坑是 proxy_next_upstream。如果稳定版 upstream 中有机器超时,Nginx 可能自动把请求转发给下一个上游节点,而这个节点如果属于金丝雀池,就会造成版本信号混淆。建议在金丝雀观察阶段根据实际架构评估是否关闭或限制 proxy_next_upstream,或者确保它只在同一个版本池内重试。
第三个坑是 keepalive 长连接导致回滚延迟。Nginx 和后端建立的长连接不会因为 reload 立即断开,已建立的连接可能继续把请求送到旧的金丝雀节点。为了避免观察误差,可以在回滚后主动执行 nginx -s reload,并观察 keepalive_requests 或连接超时设置。如果业务敏感,可以临时将金丝雀 upstream 的 keepalive 关掉。
第四个坑是只统计 HTTP 状态码,忽略业务错误码。有些系统 5xx 很少,但响应体里包含 code:50001 之类的业务失败。此时需要在应用层输出结构化日志,并在 Nginx 侧通过 request_id 关联。Nginx 可以生成 $request_id 并传给后端,排查时同时检索 Nginx 日志和业务日志。
金丝雀发布的成熟度,最终取决于日志观察能力。版本标记、指标阈值、放量节奏和回滚闭环缺一不可。把 Nginx 的原生分流与日志标记用好,可以省去大量额外组件成本,也能让每一次灰度发布都留下可回溯的依据。