首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >生产环境翻过车之后,我才真正搞懂了 Nginx 金丝雀发布

生产环境翻过车之后,我才真正搞懂了 Nginx 金丝雀发布

作者头像
悠悠12138
发布2026-04-30 19:02:01
发布2026-04-30 19:02:01
120
举报

前言

说起金丝雀发布,我自己也是吃过亏的。之前在一家电商公司,有一次大促前夜做版本更新,直接全量切换,结果新版本有个缓存的 bug,商品价格显示异常,虽然只持续了十几分钟就回滚了,但那十几分钟的订单全部要人工核对,运营那边差点没把我们骂死。后来我们就开始研究灰度发布的方案,金丝雀发布就是其中最经典的一种。

金丝雀发布到底是个啥

这个名字听起来挺文艺的,其实来源很朴素。早年间煤矿工人下矿井之前,会带一只金丝雀进去,因为金丝雀对有毒气体特别敏感,如果金丝雀没事,说明矿井安全;如果金丝雀出了问题,矿工就赶紧撤。

放到软件发布的场景里,道理是一样的。你不是一次性把新版本推给所有用户,而是先放一小部分流量(比如 5%、10%)到新版本上,观察一段时间,看看有没有报错、性能有没有下降、用户反馈怎么样。如果一切正常,再逐步扩大流量比例,最终完成全量切换。如果新版本有问题,影响的也只是那一小部分用户,及时回滚就行了,损失可控。

跟全量发布比起来,金丝雀发布的好处很明显:

  • • 风险可控,出了问题影响面小
  • • 可以拿真实流量验证新版本,比测试环境靠谱多了
  • • 回滚方便,改个配置 reload 一下就行
  • • 不需要额外的硬件资源,Nginx 本身就能搞定

当然它也不是万能的,比如涉及到数据库结构变更的发布,单靠流量层面的灰度是不够的,还需要配合数据层面的兼容方案。不过大多数场景下,金丝雀发布已经够用了。

实现原理,其实不复杂

用 Nginx 做金丝雀发布,核心就是利用 Nginx 的反向代理和负载均衡能力,把流量按照一定的规则分发到不同的后端服务上。

简单来说就是你有两组后端服务:一组跑的是稳定的老版本(我们叫它 stable),一组跑的是待验证的新版本(我们叫它 canary)。Nginx 作为入口,根据你定义的规则,决定每个请求应该转发到哪一组。

规则可以有很多种玩法:

  • • 按权重分配:比如 90% 的流量走老版本,10% 走新版本
  • • 按特定用户分配:比如某些测试账号、内部员工的请求走新版本
  • • 按 Cookie 分配:带有特定 Cookie 的请求走新版本
  • • 按 Header 分配:请求头里带了特定标记的走新版本
  • • 按 IP 分配:某些 IP 段的请求走新版本

实际生产中,这几种方式经常组合使用。比如先让内部员工全部走新版本测试一轮,没问题了再按权重放 5% 的外部流量进来。

环境准备

我这里用一个比较贴近真实场景的例子来演示。假设我们有一个 Web 应用,当前稳定版本跑在 8080 端口,新版本跑在 8081 端口。Nginx 监听 80 端口作为入口。

服务器环境:

  • • CentOS 7 / Rocky Linux 8(Ubuntu 也一样,路径稍有不同)
  • • Nginx 1.20+(更低版本也行,基本功能都支持)
  • • 两个后端应用实例

先确认 Nginx 安装好了:

代码语言:javascript
复制
nginx -v
# nginx version: nginx/1.20.1

如果没装的话:

代码语言:javascript
复制
# CentOS/Rocky
yum install -y nginx

# Ubuntu/Debian
apt install -y nginx

为了演示方便,我用两个简单的后端来模拟。实际生产中这两个就是你的应用服务,可能是 Tomcat、Node.js、Go 服务之类的。

用 Python 快速起两个模拟服务:

代码语言:javascript
复制
# 终端1 - 模拟稳定版本(v1)
mkdir -p /tmp/stable && echo "This is STABLE version v1.0" > /tmp/stable/index.html
cd /tmp/stable && python3 -m http.server 8080

# 终端2 - 模拟金丝雀版本(v2)
mkdir -p /tmp/canary && echo "This is CANARY version v2.0" > /tmp/canary/index.html
cd /tmp/canary && python3 -m http.server 8081

方案一:基于权重的流量分配

这是最简单也是最常用的方式。通过 Nginx 的 upstream 模块,给不同的后端设置不同的权重,实现按比例分流。

编辑 Nginx 配置文件:

代码语言:javascript
复制
# /etc/nginx/conf.d/canary.conf

upstream backend {
    # 稳定版本,权重 9,大约承担 90% 的流量
    server 127.0.0.1:8080 weight=9;
    # 金丝雀版本,权重 1,大约承担 10% 的流量
    server 127.0.0.1:8081 weight=1;
}

server {
    listen 80;
    server_name your-domain.com;

    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

检查配置并重载:

代码语言:javascript
复制
nginx -t
nginx -s reload

验证一下效果,多请求几次看看分布:

代码语言:javascript
复制
for i in $(seq 1 20); do curl -s http://localhost; done

你会发现大概每 10 次请求里有 1 次会返回 canary 版本的内容,其余都是 stable 版本。

这种方式的好处是简单粗暴,配置改起来也方便。想调整比例的时候,改一下 weight 值,reload 就生效了。比如观察了一段时间没问题,想把金丝雀流量提到 30%:

代码语言:javascript
复制
upstream backend {
    server 127.0.0.1:8080 weight=7;
    server 127.0.0.1:8081 weight=3;
}

不过这种方式有个问题,就是同一个用户可能一会儿访问到老版本,一会儿访问到新版本,体验不太一致。如果你的应用对这个比较敏感,就需要用下面的方案了。

方案二:基于 Cookie 的精准分流

这个方案可以保证同一个用户始终访问同一个版本,体验更一致。思路是通过 Cookie 来标记用户应该走哪个版本。

代码语言:javascript
复制
# /etc/nginx/conf.d/canary_cookie.conf

upstream stable {
    server 127.0.0.1:8080;
}

upstream canary {
    server 127.0.0.1:8081;
}


server {
    listen 80;
    server_name your-domain.com;

    # 根据 cookie 判断走哪个版本
    set $backend "stable";

    if ($http_cookie ~* "canary=true") {
        set $backend "canary";
    }

    location / {
        proxy_pass http://$backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

这样配置之后,默认所有请求都走 stable 版本。只有当请求的 Cookie 里带了 canary=true 的时候,才会转发到 canary 版本。

怎么给用户种上这个 Cookie 呢?有几种方式:

1、手动在浏览器里加,适合内部测试:

打开浏览器开发者工具,在 Console 里执行:

代码语言:javascript
复制
document.cookie = "canary=true; path=/";

2、通过后端接口设置,适合灰度平台对接:

后端提供一个接口,根据用户 ID 或者其他规则,决定是否给用户种上 canary Cookie。

3、通过 Nginx 自身来做随机分配并种 Cookie:

代码语言:javascript
复制
# /etc/nginx/conf.d/canary_split.conf

upstream stable {
    server 127.0.0.1:8080;
}

upstream canary {
    server 127.0.0.1:8081;
}

# 利用 split_clients 模块按比例分配
split_clients "${remote_addr}${remote_port}" $variant {
    10%     canary;
    *       stable;
}

server {
    listen 80;
    server_name your-domain.com;

    # 如果已经有 cookie 了,就按 cookie 走
    set $target $variant;

    if ($http_cookie ~* "canary_version=canary") {
        set $target "canary";
    }

    if ($http_cookie ~* "canary_version=stable") {
        set $target "stable";
    }

    location / {
        # 给用户种上 cookie,下次来就固定走同一个版本了
        add_header Set-Cookie "canary_version=$target; path=/; max-age=3600";

        proxy_pass http://$target;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

这个配置用了 split_clients 模块,它会根据你指定的变量(这里用的是客户端 IP 和端口的组合)做哈希,然后按比例分配。第一次访问的时候,10% 的用户会被分到 canary,同时种上一个 Cookie。后续再访问的时候,就直接按 Cookie 走了,不会再重新分配。

split_clients 这个模块是 Nginx 自带的,不需要额外安装,很多人不知道有这个东西,其实挺好用的。

方案三:基于请求头的分流

这种方式在 API 网关场景下用得比较多。前端或者调用方在请求头里带上一个标记,Nginx 根据这个标记来决定路由。

代码语言:javascript
复制
# /etc/nginx/conf.d/canary_header.conf

upstream stable {
    server 127.0.0.1:8080;
}

upstream canary {
    server 127.0.0.1:8081;
}

server {
    listen 80;
    server_name your-domain.com;

    set $backend "stable";

    # 根据自定义请求头判断
    if ($http_x_canary = "true") {
        set $backend "canary";
    }

    # 也可以根据 User-Agent 判断,比如特定版本的 App
    if ($http_user_agent ~* "MyApp/2.0") {
        set $backend "canary";
    }

    location / {
        proxy_pass http://$backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

测试的时候带上自定义 Header:

代码语言:javascript
复制
# 走金丝雀版本
curl -H "X-Canary: true" http://localhost

# 走稳定版本(不带 header)
curl http://localhost

这种方式的好处是控制粒度很细,而且不依赖 Cookie,适合服务间调用的场景。我们之前做微服务灰度的时候就是用的这种方式,在网关层根据请求头做路由,Header 可以在整个调用链路上透传。

方案四:基于 IP 的分流

有时候你想让特定网段的用户(比如公司内网)先体验新版本,可以基于 IP 来做分流。

代码语言:javascript
复制
# /etc/nginx/conf.d/canary_ip.conf

upstream stable {
    server 127.0.0.1:8080;
}

upstream canary {
    server 127.0.0.1:8081;
}

# 定义一个 geo 变量,根据客户端 IP 来赋值
geo $canary_ip {
    default         0;
    192.168.1.0/24  1;    # 内网测试网段
    10.0.100.0/24   1;    # 办公网络
    # 也可以指定单个 IP
    203.0.113.50    1;
}

server {
    listen 80;
    server_name your-domain.com;

    set $backend "stable";

    if ($canary_ip = "1") {
        set $backend "canary";
    }

    location / {
        proxy_pass http://$backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

geo 模块也是 Nginx 内置的,不需要额外安装。它可以根据客户端 IP 地址来设置变量的值,支持 CIDR 格式的网段,用起来很方便。

不过要注意一点,如果你的 Nginx 前面还有一层负载均衡(比如云厂商的 SLB),那拿到的 remote_addr 可能是 SLB 的 IP 而不是真实客户端 IP。这时候需要用 http_x_forwarded_for 或者配置 set_real_ip_from 来获取真实 IP:

代码语言:javascript
复制
# 信任前置负载均衡的 IP
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

方案五:组合方案(生产推荐)

实际生产中,我一般会把上面几种方式组合起来用。比如下面这个配置,优先级是:请求头 > Cookie > IP > 默认权重分配。

代码语言:javascript
复制
# /etc/nginx/conf.d/canary_production.conf

upstream stable {
    server 127.0.0.1:8080;
}

upstream canary {
    server 127.0.0.1:8081;
}

geo $canary_by_ip {
    default 0;
    192.168.1.0/24 1;
}

split_clients "${remote_addr}" $canary_by_weight {
    5%  1;
    *   0;
}

map $http_x_canary $canary_by_header {
    "true"  1;
    default 0;
}

map $cookie_canary_version $canary_by_cookie {
    "canary" 1;
    default  0;
}

# 综合判断:任意一个条件命中就走 canary
map "$canary_by_header:$canary_by_cookie:$canary_by_ip:$canary_by_weight" $target_backend {
    "~1"    canary;     # 只要有一个是 1 就走 canary
    default stable;
}

server {
    listen 80;
    server_name your-domain.com;

    # 添加响应头,方便排查问题(生产环境可以去掉)
    add_header X-Backend $target_backend;

    location / {
        proxy_pass http://$target_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # 超时设置
        proxy_connect_timeout 5s;
        proxy_read_timeout 30s;
        proxy_send_timeout 30s;
    }

    # 健康检查接口(可选)
    location /health {
        access_log off;
        return 200 "OK";
    }
}

这个配置里有个小技巧,就是用 map 指令做多条件的组合判断。"~1" 是正则匹配,只要拼接出来的字符串里包含 1,就走 canary。这样写比嵌套一堆 if 要优雅得多,而且 Nginx 官方也不推荐在 location 里大量使用 if,容易出一些奇怪的问题。

另外我加了一个 X-Backend 的响应头,这个在调试的时候特别有用,你可以通过浏览器开发者工具或者 curl 看到当前请求走的是哪个版本:

代码语言:javascript
复制
curl -I http://localhost
# 看响应头里的 X-Backend 字段

生产环境上线之后记得把这个响应头去掉,或者改成只在内网可见,别把内部信息暴露出去了。

发布流程和注意事项

光有配置还不够,金丝雀发布是一个完整的流程,我把我们团队实际在用的流程分享一下。

发布前的准备工作:

  • • 确认新版本在测试环境已经充分验证过
  • • 准备好回滚方案,确认回滚操作步骤
  • • 通知相关人员(开发、测试、产品),约定观察时间
  • • 确认监控告警已经配置好,能及时发现问题

发布过程:

第一步,部署新版本到 canary 节点,但先不接入流量:

代码语言:javascript
复制
# 先把 canary 的权重设为 0 或者注释掉
upstream backend {
    server 127.0.0.1:8080 weight=10;
    server 127.0.0.1:8081 down;  # 先标记为 down
}

第二步,部署完成后,先让内部人员通过 Header 或 Cookie 的方式访问 canary 版本,做一轮冒烟测试。

第三步,冒烟测试通过后,开放小比例流量(比如 5%):

代码语言:javascript
复制
# 修改配置,开放 5% 流量
split_clients "${remote_addr}" $canary_by_weight {
    5%  1;
    *   0;
}
代码语言:javascript
复制
nginx -t && nginx -s reload

第四步,观察监控指标,重点关注:

  • • 错误率:新版本的 5xx 错误率是否正常
  • • 响应时间:P99、P95 延迟有没有明显上升
  • • 业务指标:订单量、转化率等核心指标有没有异常
  • • 日志:有没有异常的错误日志

第五步,如果一切正常,逐步扩大流量比例:5% → 10% → 30% → 50% → 100%。每次调整后至少观察 15-30 分钟。

如果发现问题需要回滚,操作也很简单:

代码语言:javascript
复制
# 方式一:把 canary 标记为 down
upstream backend {
    server 127.0.0.1:8080 weight=10;
    server 127.0.0.1:8081 down;
}

# 方式二:直接把 canary 的权重设为 0
split_clients "${remote_addr}" $canary_by_weight {
    0%  1;
    *   0;
}
代码语言:javascript
复制
nginx -t && nginx -s reload

reload 是平滑重载,不会中断现有连接,所以不用担心回滚操作本身会造成影响。

还有几个容易踩坑的地方说一下:

1、Session 一致性问题。如果你的应用是有状态的(比如 Session 存在本地),用户从 stable 切到 canary 的时候可能会丢失登录状态。解决办法是把 Session 放到 Redis 之类的外部存储里,或者用 JWT 这种无状态的认证方式。

2、数据库兼容性。如果新版本涉及数据库表结构变更,一定要确保新老版本都能兼容当前的数据库结构。一般的做法是先做数据库变更(要向后兼容),再发布新版本代码。

3、静态资源的问题。如果前端静态资源也有变更,要注意 CDN 缓存的影响。建议静态资源用带 hash 的文件名,避免缓存导致新老版本的资源混用。

4、WebSocket 连接。如果你的应用用了 WebSocket,金丝雀发布的时候要注意长连接的处理。Nginx reload 不会断开已有的 WebSocket 连接,但新连接会按新的规则分配。

监控配置参考

做金丝雀发布,监控是必不可少的。这里给一个简单的 Nginx 日志配置,方便你区分不同版本的请求:

代码语言:javascript
复制
# 自定义日志格式,加上 backend 标记
log_format canary_log '$remote_addr - $remote_user [$time_local] '
                      '"$request" $status $body_bytes_sent '
                      '"$http_referer" "$http_user_agent" '
                      'backend=$target_backend rt=$request_time';

server {
    # ...
    access_log /var/log/nginx/canary_access.log canary_log;
}

这样在日志里就能看到每个请求走的是哪个后端,配合 ELK 或者 Grafana + Loki 之类的日志分析工具,可以很方便地对比两个版本的各项指标。

用 awk 简单统计一下各版本的请求量和错误率:

代码语言:javascript
复制
# 统计各版本请求量
awk '{for(i=1;i<=NF;i++) if($i ~ /^backend=/) print $i}' /var/log/nginx/canary_access.log | sort | uniq -c

# 统计各版本的 5xx 错误
awk '$9 >= 500 {for(i=1;i<=NF;i++) if($i ~ /^backend=/) print $i}' /var/log/nginx/canary_access.log | sort | uniq -c

写在最后

金丝雀发布说白了就是一个"小心试探"的过程,核心思想就是用最小的代价去验证新版本的稳定性。Nginx 作为实现工具,配置灵活,性能也好,基本上能覆盖大多数场景。

当然,如果你的业务规模比较大,服务数量比较多,可能需要更专业的灰度发布平台,比如结合 Kubernetes 的 Istio、或者用 OpenResty 做更复杂的流量治理。但对于大多数中小团队来说,Nginx 原生的这些能力已经足够了,不需要引入太多额外的复杂度。

我个人的建议是,不管你用什么方案,关键是要把发布流程规范化,每次发布都按照固定的步骤来,观察该观察的指标,该回滚的时候果断回滚。工具只是手段,流程和意识才是核心。

如果这篇文章对你有帮助,欢迎转发给身边的运维和开发同学,大家一起少踩坑。也欢迎关注我,后续会继续分享更多生产实践相关的内容。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-04-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 运维躬行录 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
    • 金丝雀发布到底是个啥
    • 实现原理,其实不复杂
    • 环境准备
    • 方案一:基于权重的流量分配
    • 方案二:基于 Cookie 的精准分流
    • 方案三:基于请求头的分流
    • 方案四:基于 IP 的分流
    • 方案五:组合方案(生产推荐)
    • 发布流程和注意事项
    • 监控配置参考
    • 写在最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档