通常一个新功能开发完,测试环境可能跑得好好的,但生产环境因为数据量、并发量、网络环境完全不一样,可能会产生各种意料之外的错误。等发现问题的时候,可能已经造成了不小的影响。
因此,新功能的发布推荐使用灰度发布的方式,先给少量用户试用新版本,确认无问题后再逐渐放量到全部用户。
- 先给少量(如5%)的用户试用新版本,观察一段时间
- 如果没问题,再逐步放量到20%、50%、100%
- 一旦发现问题,立刻切回旧版本
- 整个过程用户基本无感知
值得注意的是,进行灰度发布时,监控和告警特别重要 。
推荐做法:在Nginx日志里加上版本标记,然后用ELK收集日志,实时对比新旧版本的各项指标。这样在Kibana里就能看到每个版本的请求量、错误率、响应时间等指标,一目了然。
此外还应配置告警规则,如果新版本的错误率比旧版本高出10%,或者响应时间慢了50%,就会自动发送告警,甚至可以自动回滚。
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;优化建议
灰度策略要灵活
不要一上来就5%、10%、50%、100%这样机械地放量。要根据实际情况灵活调整。
比如一个小功能,可能5%观察半小时没问题,就直接100%了。但如果是核心功能的大改动,可能要5%观察一天,10%观察一天,慢慢来。
最好是先在内部员工里测试,没问题后再给1%的真实用户,然后5%、10%、30%、50%、100%,每个阶段都要观察一段时间。
做好回滚预案
灰度发布虽然降低了风险,但不代表没有风险。一定要做好回滚预案。
推荐做法是,每次发布前都要演练一遍回滚流程,确保出问题的时候能在5分钟内回滚。而且回滚不能只是把流量切回去,还要考虑数据一致性、缓存清理等问题。
用户体验要考虑
虽然灰度发布对用户来说应该是无感知的,但有些细节还是要注意。
比如不要在用户操作的过程中切换版本,这样可能导致数据丢失或者页面错乱。我们的做法是,用Cookie做用户粘性,保证同一个用户在一段时间内(比如24小时)始终访问同一个版本。
还有就是,如果新旧版本的UI差异比较大,最好在客户端做个平滑过渡,不要让用户觉得突兀。
使用nginx可以通过多种 不同的方式来进行灰度发布,分别介绍如下:
基于权重的流量分配
这是最简单的一种方式,原理就是通过upstream的权重参数,把流量按比例分配到不同版本的服务器上。
下面这个配置的意思是,100个请求里面,95个会打到旧版本服务器,5个会打到新版本。
不过这种方式有个问题,就是同一个用户的请求可能一会儿打到旧版本,一会儿打到新版本,体验不太好。因此最好设置一个ip_hash。
- 会话粘性:确保来自同一客户端 IP 的请求总是被转发到同一台后端服务器
- 解决状态问题:当后端应用服务器没有共享会话状态时,避免用户会话丢失
- 提升缓存效率:同一客户端的请求路由到同一服务器,可提高本地缓存命中率
- 简化架构:避免部署复杂的分布式会话存储系统
upstream backend {
ip_hash;
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;
}
}基于Cookie的灰度发布
一个用户第一次访问的时候,我们给他打个标记,后续的请求都根据这个标记来决定走哪个版本。
实现方式:用户第一次访问的时候,随机决定他是不是可以访问新版本,然后种个Cookie。后续请求都会带着这个Cookie,保证同一个用户始终访问同一个版本。
但是这个方案还是有点粗糙,因为Nginx原生不支持生成随机数,我们需要借助一些技巧或者第三方模块。
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;
}
}基于Header的灰度发布
可以更精确地控制哪些用户走新版本,比如内部员工、测试账号、特定地区的用户等等。这时候可以用Header来做判断。
这种方式特别适合做内部测试。
使用方式:在客户端(比如App或者前端页面)加个开关,员工登录后自动在请求头里加上特定标记,这样就能体验新版本了。
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;
}
}基于IP地址的灰度发布
还有一种场景,比如计划先在某个地区试点新功能,或者只给公司内网用户开放新版本。这时候可以用IP地址来做判断。
geo模块是一个Nginx内置核心模块,用于根据客户端 IP 地址创建变量,实现基于地理位置或 IP 段的条件处理。它允许服务器对不同来源的请求执行不同的操作,无需额外安装,随 Nginx 一起编译。
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;
}
}灰度发布中的一些常见问题
Session一致性问题
如果Session存在服务器本地,用户的请求可能一会儿打到v1,一会儿打到v2,那么就会造成Session的丢失。于是就会造成用户登录状态老是丢失的问题。
解决办法有两个:
- 把Session存到Redis这种共享存储里
- 用Cookie或者Header做用户粘性,保证同一个用户的请求打到同一个版本
数据库兼容性问题
比如新版本改了数据库表结构,结果灰度发布的时候,新旧版本同时在跑,旧版本写入的数据新版本读不了,新版本写入的数据旧版本也读不了,整个系统乱套了。
解决方案
- 先发布一个兼容版本,既能处理旧数据格式,也能处理新数据格式
- 等兼容版本全量发布后,再发布纯新版本
这样虽然麻烦点,但安全多了。
评论 (0)