线上灰度发布翻车后,我用Nginx金丝雀发布救了一命
客户差点因为一次全量发布把整个独立站系统搞崩了。那天晚上加班到凌晨两点,冷汗都出来了。找到我们做架构优化,我们团队花了一周时间研究了Nginx的金丝雀发布方案,现在分享给大家,希望你们别踩我踩过的坑。
说实话,金丝雀发布这个名字听起来挺高大上的,其实原理很简单。就像以前矿工下井前会带只金丝雀,如果有毒气金丝雀先挂了,矿工就知道危险了。我们发布新版本也是这个道理,先让一小部分用户试用新版本,有问题马上回滚,不至于全军覆没。
为什么要用金丝雀发布
我之前在一家创业公司待过,那时候发布新版本就是简单粗暴,直接全量上线。有一次产品经理非要在周五下午发布一个"小功能",结果周末两天我都在公司处理线上问题,女朋友差点跟我分手。
传统的发布方式问题太多了。你想啊,一个新功能开发完,测试环境跑得好好的,但生产环境的数据量、并发量、网络环境完全不一样。等你发现问题的时候,可能已经有几万个用户在骂娘了。
金丝雀发布就不一样了,它可以让你:
- 先给5%的用户试用新版本,观察一段时间
- 如果没问题,再逐步放量到20%、50%、100%
- 一旦发现问题,立刻切回旧版本
- 整个过程用户基本无感知
我现在的公司用了金丝雀发布之后,线上事故率直接降了70%,这个数据不是吹的。
Nginx实现金丝雀发布的几种姿势
Nginx实现金丝雀发布其实有好几种方法,我都试过,每种都有自己的适用场景。
基于权重的流量分配
这是最简单的一种方式,适合你刚开始尝试金丝雀发布的时候用。原理就是通过upstream的权重参数,把流量按比例分配到不同版本的服务器上。
upstream backend {
server 192.168.1.10:8080 weight=95; # 旧版本
server 192.168.1.11:8080 weight=5; # 新版本
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}这个配置的意思是,100个请求里面,95个会打到旧版本服务器,5个会打到新版本。你可以根据实际情况调整这个比例。
不过这种方式有个问题,就是同一个用户的请求可能一会儿打到旧版本,一会儿打到新版本,体验不太好。我之前就遇到过用户投诉说页面样式一会儿变一会儿不变的情况,后来才发现是这个原因。
基于Cookie的灰度发布
为了解决上面那个问题,我们可以用Cookie来做用户粘性。简单说就是,一个用户第一次访问的时候,我们给他打个标记,后续的请求都根据这个标记来决定走哪个版本。
upstream backend_v1 {
server 192.168.1.10:8080;
}
upstream backend_v2 {
server 192.168.1.11:8080;
}
server {
listen 80;
server_name api.example.com;
location / {
set $backend "backend_v1";
# 如果Cookie中有canary标记,走新版本
if ($http_cookie ~* "canary=true") {
set $backend "backend_v2";
}
# 随机给5%的新用户打上canary标记
set $random_canary "";
if ($http_cookie !~* "canary") {
set $random_canary "${random_canary}A";
}
# 生成1-100的随机数
set $rand_num $request_id;
if ($random_canary = "A") {
# 这里简化处理,实际可以用Lua脚本
add_header Set-Cookie "canary=false; Path=/; Max-Age=86400";
}
proxy_pass http://$backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}这个配置看起来有点复杂,但逻辑很清楚。用户第一次访问的时候,我们随机决定他是不是金丝雀用户,然后种个Cookie。后续请求都会带着这个Cookie,保证同一个用户始终访问同一个版本。
但是这个方案还是有点粗糙,因为Nginx原生不支持生成随机数,我们需要借助一些技巧或者第三方模块。
基于Header的灰度发布
有时候我们想更精确地控制哪些用户走新版本,比如内部员工、测试账号、特定地区的用户等等。这时候可以用Header来做判断。
upstream backend_v1 {
server 192.168.1.10:8080;
}
upstream backend_v2 {
server 192.168.1.11:8080;
}
server {
listen 80;
server_name api.example.com;
location / {
set $backend "backend_v1";
# 如果请求头中有特定标记,走新版本
if ($http_x_canary_version = "v2") {
set $backend "backend_v2";
}
# 内部员工走新版本
if ($http_x_employee_id != "") {
set $backend "backend_v2";
}
proxy_pass http://$backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}这种方式特别适合做内部测试。我们可以在客户端(比如App或者前端页面)加个开关,员工登录后自动在请求头里加上特定标记,这样就能体验新版本了。
基于IP地址的灰度发布
还有一种场景,比如你想先在某个地区试点新功能,或者只给公司内网用户开放新版本。这时候可以用IP地址来做判断。
geo $canary_user {
default 0;
10.0.0.0/8 1; # 公司内网
192.168.1.0/24 1; # 特定网段
123.45.67.89 1; # 特定IP
}
upstream backend_v1 {
server 192.168.1.10:8080;
}
upstream backend_v2 {
server 192.168.1.11:8080;
}
server {
listen 80;
server_name api.example.com;
location / {
set $backend "backend_v1";
if ($canary_user = 1) {
set $backend "backend_v2";
}
proxy_pass http://$backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}geo模块是Nginx内置的,性能很好。我之前用这个方案做过一次大版本升级,先在北京地区试点了一周,没问题后再全国推广,效果不错。
进阶玩法:结合Lua实现动态灰度
上面那些方案都有个问题,就是每次调整灰度比例都要修改配置文件,然后reload Nginx。这在生产环境其实挺麻烦的,万一配置写错了,reload失败,那就尴尬了。
我现在用的方案是结合OpenResty(Nginx + Lua),把灰度规则存在Redis里,这样就可以动态调整了。
upstream backend_v1 {
server 192.168.1.10:8080;
}
upstream backend_v2 {
server 192.168.1.11:8080;
}
server {
listen 80;
server_name api.example.com;
location / {
set $backend "backend_v1";
access_by_lua_block {
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
ngx.log(ngx.ERR, "failed to connect redis: ", err)
return
end
-- 从Redis获取灰度比例
local canary_percent, err = red:get("canary:percent")
if not canary_percent or canary_percent == ngx.null then
canary_percent = 0
end
-- 生成随机数判断是否走新版本
math.randomseed(ngx.now())
local rand = math.random(100)
if rand <= tonumber(canary_percent) then
ngx.var.backend = "backend_v2"
end
red:close()
}
proxy_pass http://$backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}这样的话,我们只需要在Redis里修改canary:percent的值,就可以实时调整灰度比例了,不需要reload Nginx。
# 设置灰度比例为10%
redis-cli set canary:percent 10
# 逐步放量
redis-cli set canary:percent 30
redis-cli set canary:percent 50
redis-cli set canary:percent 100
# 紧急回滚
redis-cli set canary:percent 0我还做了个简单的管理后台,可以可视化地调整灰度比例,监控新旧版本的错误率、响应时间等指标。一旦发现新版本有问题,点一下按钮就能回滚,特别方便。
实战中的一些坑
说了这么多理论,我再分享几个实战中遇到的坑,这些都是血泪教训。
Session一致性问题
有一次我们做灰度发布,结果用户反馈说登录状态老是丢失。排查了半天才发现,是因为Session存在服务器本地,用户的请求一会儿打到v1一会儿打到v2,Session当然就丢了。
解决办法有两个:
- 把Session存到Redis这种共享存储里
- 用Cookie或者Header做用户粘性,保证同一个用户的请求打到同一个版本
我们最后选了第一种方案,顺便把整个Session机制重构了一遍,现在扩展性好多了。
数据库兼容性问题
还有一次更坑,新版本改了数据库表结构,结果灰度发布的时候,新旧版本同时在跑,旧版本写入的数据新版本读不了,新版本写入的数据旧版本也读不了,整个系统乱套了。
后来我们定了个规矩,涉及数据库表结构变更的发布,必须分两步走:
- 先发布一个兼容版本,既能处理旧数据格式,也能处理新数据格式
- 等兼容版本全量发布后,再发布纯新版本
虽然麻烦点,但安全多了。
监控和告警
金丝雀发布不是配置完就完事了,监控和告警特别重要。我们现在的做法是,在Nginx日志里加上版本标记,然后用ELK收集日志,实时对比新旧版本的各项指标。
log_format canary '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'backend=$backend rt=$request_time';
access_log /var/log/nginx/access.log canary;这样在Kibana里就能看到每个版本的请求量、错误率、响应时间等指标,一目了然。
我们还配置了告警规则,如果新版本的错误率比旧版本高出10%,或者响应时间慢了50%,就会自动发送告警,甚至可以自动回滚。
一些优化建议
用了这么久金丝雀发布,我总结了一些优化建议。
灰度策略要灵活
不要一上来就5%、10%、50%、100%这样机械地放量。要根据实际情况灵活调整。
比如一个小功能,可能5%观察半小时没问题,就直接100%了。但如果是核心功能的大改动,可能要5%观察一天,10%观察一天,慢慢来。
我们现在的策略是,先在内部员工里测试,没问题后再给1%的真实用户,然后5%、10%、30%、50%、100%,每个阶段都要观察一段时间。
做好回滚预案
金丝雀发布虽然降低了风险,但不代表没有风险。一定要做好回滚预案。
我们的做法是,每次发布前都要演练一遍回滚流程,确保出问题的时候能在5分钟内回滚。而且回滚不能只是把流量切回去,还要考虑数据一致性、缓存清理等问题。
有一次我们发现新版本有问题,回滚后发现缓存里还是新版本的数据,导致页面显示错乱。后来我们在回滚脚本里加了清理缓存的步骤,就没再出过这种问题。
用户体验要考虑
虽然金丝雀发布对用户来说应该是无感知的,但有些细节还是要注意。
比如不要在用户操作的过程中切换版本,这样可能导致数据丢失或者页面错乱。我们的做法是,用Cookie做用户粘性,保证同一个用户在一段时间内(比如24小时)始终访问同一个版本。
还有就是,如果新旧版本的UI差异比较大,最好在客户端做个平滑过渡,不要让用户觉得突兀。
工具推荐
最后推荐几个我常用的工具。
Nginx Plus
如果预算充足,可以考虑Nginx Plus,它自带了很多企业级功能,包括动态配置、健康检查、高级负载均衡等。金丝雀发布用起来会方便很多。
不过Nginx Plus是商业版,要花钱的。我们推荐用的是开源版本加OpenResty,也够用了。
OpenResty
前面提到过,OpenResty是Nginx加上Lua脚本引擎,可以实现很多复杂的逻辑。我现在基本上所有的Nginx配置都是基于OpenResty的。
学习曲线可能有点陡,但学会了之后真的很香。你可以用Lua实现各种复杂的灰度策略,甚至可以对接公司的配置中心、监控系统等。
Prometheus + Grafana
监控方面,我强烈推荐Prometheus + Grafana这套组合。Nginx可以通过nginx-module-vts或者nginx-prometheus-exporter暴露指标,然后用Prometheus收集,Grafana做可视化。
我们现在的监控大盘可以实时看到每个版本的QPS、错误率、P99延迟等指标,还能设置告警规则,非常好用。
Consul / Etcd
如果你的服务是动态扩缩容的,可以考虑用Consul或者Etcd做服务发现。Nginx可以通过consul-template或者confd动态更新upstream配置,不需要手动维护服务器列表。
我们现在用的是Consul,配合Kubernetes使用,服务上下线都是自动的,省了很多运维工作。
写在最后
金丝雀发布说起来简单,但真正用好需要很多配套的东西,包括监控、告警、回滚机制、数据兼容性等等。不要指望一步到位,可以先从简单的方案开始,逐步优化。
我从最开始的全量发布,到现在的金丝雀发布,花了差不多一年时间。期间踩了无数的坑,但现在回头看,这些都是值得的。线上事故少了,晚上睡觉也踏实了,周末也不用老是待命了。
如果你们公司还在用传统的发布方式,真的建议试试金丝雀发布。刚开始可能会觉得麻烦,但用习惯了之后,你会发现这是保证系统稳定性的利器。
对了,金丝雀发布也不是万能的,它只能降低风险,不能完全消除风险。该做的测试还是要做,该有的监控还是要有,该写的文档还是要写。技术只是手段,最终还是要靠人来保证质量。
好了,今天就分享到这里。如果你在实施金丝雀发布的过程中遇到什么问题,欢迎留言讨论。我虽然不是什么大神,但踩过的坑还是挺多的,说不定能帮上忙。
如果这篇文章对你有帮助,欢迎点赞、转发,让更多的人看到。也欢迎关注@运维躬行录,我会持续分享运维实战经验,咱们一起成长,一起避坑。
运维这条路不好走,但有你们陪伴,就不孤单。我们下期见!
公众号:运维躬行录
个人博客:躬行笔记