在这里插入图片描述

基于Nginx权重实现灰度发布原理

1. 灰度发布讲解

灰度发布(Gray Release)是一种渐进式的软件发布策略,它允许将新版本服务逐步推向生产环境,而不是一次性替换整个系统。这种发布方式的核心目标是降低发布风险、保证系统稳定性,同时能够实时监控新版本的运行状态。

优点:

  • 风险控制:将新版本可能带来的故障影响范围控制在最小
  • 用户体验:避免全量发布导致的整体服务中断或性能下降
  • 实时反馈:通过小流量测试收集真实用户反馈和性能数据
  • 快速回滚:发现问题时可立即切回稳定版本,减少损失

2. Nginx权重分流原理

Nginx通过upstream模块实现负载均衡,这是灰度发布的基石。其核心配置如下:

vim /etc/nginx/conf.d/gray-release.conf 
# 定义后端服务集群 - 增加健康检查参数
upstream backend_servers {
    server 127.0.0.1:8080 weight=50 max_fails=2 fail_timeout=30s;  # Version1 - 增加容错
    server 127.0.0.1:8081 weight=50 max_fails=2 fail_timeout=30s;  # Version2 - 增加容错
}

工作原理:

  • Nginx根据权重比例计算每个服务器的概率分布

  • 对于每个新请求,Nginx按照权重概率选择后端服务器

  • 权重算法基于平滑加权轮询,确保流量分布均匀且符合比例

  • max_fails=2:最多失败2次后标记服务器不可用

  • fail_timeout=30s:失败后30秒内不再向该服务器转发请求

  • 30秒后会自动重试,如成功则重新加入负载均衡池

3. 精细化流量控制策略

基于Cookie的精准路由:

# 优化基于Cookie的灰度发布配置 - 使用更精确的正则匹配
map $cookie_gray_release $backend_group {
    default "backend_servers";   # 默认使用权重分配
    "~*v1"  "version1_server";  # 使用正则确保匹配包含v1的cookie值
    "~*v2"  "version2_server";  # 使用正则确保匹配包含v2的cookie值
}
  • 内部测试:测试人员通过设置Cookie值强制访问特定版本
  • 用户体验:特定用户群体优先体验新功能
  • 问题排查:重现用户问题时保持环境一致性

基于IP段的灰度发布:

geo $gray_ip_group {
    default         "backend_servers";
    192.168.1.0/24  "version2_server";  # 内部测试网络访问新版本
    10.0.0.100      "version2_server";  # 特定测试机访问新版本
}
  • 内部办公网络优先体验新版本
  • 特定机房或地域逐步放开

监控模块

通过Nginx status模块实时监控:

	location /nginx_status {
        stub_status on;      # 开启状态模块
        access_log off;      # 可选:关闭此位置的访问日志
        allow 127.0.0.1;     # 允许访问的IP地址,例如本地
        allow 192.168.16.0/24; # 允许的内网网段,按需设置
        deny all;            # 拒绝其他所有IP访问,重要!
    }
    
# 状态信息:
Active connections: 3 
server accepts handled requests
 307 307 487 
Reading: 0 Writing: 1 Waiting: 2
  • Reading:正在读取请求头的连接数(待处理请求)
  • Writing:正在发送响应的连接数(处理中请求)
  • Waiting:保持连接但空闲的连接数
  • accepts/handled/requests:总接受/处理/请求数,用于计算成功率

4. 灰度发布流程图

在这里插入图片描述

流量入口 (客户端 → Nginx)

  • 动作:所有外部客户端的请求首先统一到达部署好的 Nginx 网关服务器(监听8082端口)。
  • 角色:Nginx 在这里扮演了 “流量调度员” 和 “策略执行者” 的核心角色。它是整个灰度系统的大脑。

流量调度 (Nginx 权重分发)

  • 动作:Nginx 根据预先配置的权重规则(upstream模块),将接收到的请求按比例分发到后端的两个服务实例。 90% 的请求被转发到运行 旧版本 V1.0 的服务端(A服务器,监听8080端口)。 10% 的请求被转发到运行 新版本 V1.1 的服务端(B服务器,监听8081端口)。
  • 特点:这个过程对用户是完全透明的,用户无需感知也控制不了自己访问的是哪个版本,分流是随机的。

业务处理 (服务端 → 数据层)

  • 动作:两个版本的服务实例接收到请求后,各自独立处理业务逻辑。它们都需要读写同一个数据库/Redis。
  • 核心要求:这里存在一个灰度发布的关键前提,即版本V1.1对数据的任何修改(数据库表结构、缓存格式等)都必须是向后兼容的,不能影响V1.0的正常读写,否则会导致数据错乱。

基于Nginx权重实现灰度发布实验

注意:安装Nginx版本要≥1.18.0,若公网访问,请放行所需端口。

1. 创建Version1服务

sudo mkdir -p /var/www/html/{v1,v2,static}
vim /var/www/html/v1/index.html

Version1服务:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>服务版本 1.0</title>
    <style>
        body { 
            font-family: Arial, sans-serif; 
            background-color: #f0f8ff; 
            text-align: center; 
            padding: 50px;
        }
        .version { 
            color: #ff6b6b; 
            font-size: 2.5em; 
            margin-bottom: 20px;
        }
        .features { 
            background: white; 
            padding: 20px; 
            border-radius: 10px; 
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            max-width: 600px;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    <div class="version">Version 1.0 - 稳定版</div>
    <div class="features">
        <h2>当前功能特性</h2>
        <ul style="text-align: left; display: inline-block;">
            <li>基础用户管理功能</li>
            <li>标准报告生成</li>
            <li>基础数据分析</li>
            <li>传统界面设计</li>
        </ul>
        <p><strong>服务器端口: 8080</strong></p>
        <p>更新时间: 2024-01-01</p>
    </div>
</body>
</html>

在这里插入图片描述

2. 创建Version2服务

vim /var/www/html/v2/index.html 

Version2服务:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>服务版本 2.0 - 新特性</title>
    <style>
        body { 
            font-family: Arial, sans-serif; 
            background-color: #fff0f5; 
            text-align: center; 
            padding: 50px;
        }
        .version { 
            color: #4ecdc4; 
            font-size: 2.5em; 
            margin-bottom: 20px;
        }
        .features { 
            background: white; 
            padding: 20px; 
            border-radius: 10px; 
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            max-width: 600px;
            margin: 0 auto;
        }
        .new-badge { 
            background: #4ecdc4; 
            color: white; 
            padding: 2px 8px; 
            border-radius: 10px; 
            font-size: 0.8em; 
            margin-left: 5px;
        }
    </style>
</head>
<body>
    <div class="version">Version 2.0 - 新特性版 <span class="new-badge">NEW</span></div>
    <div class="features">
        <h2>全新功能特性</h2>
        <ul style="text-align: left; display: inline-block;">
            <li>智能用户推荐系统 <span class="new-badge">新</span></li>
            <li>AI报告自动生成 <span class="new-badge">新</span></li>
            <li>实时数据分析看板</li>
            <li>现代化界面设计 <span class="new-badge">新</span></li>
            <li>移动端优化支持</li>
        </ul>
        <p><strong>服务器端口: 8081</strong></p>
        <p>更新时间: 2025-10-09</p>
    </div>
</body>
</html>

在这里插入图片描述

3. 编写Nginx配置文件

vim /etc/nginx/conf.d/gray-release.conf 
# 定义后端服务集群 - 增加健康检查参数
upstream backend_servers {
    server 127.0.0.1:8080 weight=50 max_fails=2 fail_timeout=30s;  # Version1 - 增加容错
    server 127.0.0.1:8081 weight=50 max_fails=2 fail_timeout=30s;  # Version2 - 增加容错
}

# 基于Cookie的灰度发布配置 - 使用更精确的正则匹配
map $cookie_gray_release $backend_group {
    default "backend_servers";   # 默认使用权重分配
    "~*v1"  "version1_server";  # 使用正则确保匹配包含v1的cookie值
    "~*v2"  "version2_server";  # 使用正则确保匹配包含v2的cookie值
}

# 基于IP的灰度配置示例(可选策略)
geo $gray_ip_group {
    default         "backend_servers";
    192.168.1.0/24  "version2_server";  # 特定IP段访问新版本
    10.0.0.100      "version2_server";  # 特定IP访问新版本
}

upstream version1_server {
    server 127.0.0.1:8080 max_fails=2 fail_timeout=30s;
}

upstream version2_server {
    server 127.0.0.1:8081 max_fails=2 fail_timeout=30s;
}

# 主服务器配置
server {
    listen 8083;
    server_name localhost;
    
    # 设置全局访问日志格式,便于区分流量
    access_log /var/log/nginx/gray_access.log ;
    error_log /var/log/nginx/gray_error.log;
    
    # 静态资源服务
    location /static/ {
        root /var/www/html;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
    
    location /nginx_status {
        stub_status on;      # 开启状态模块
        access_log off;      # 可选:关闭此位置的访问日志
        allow 127.0.0.1;     # 允许访问的IP地址,例如本地
        allow 192.168.16.0/24; # 允许的内网网段,按需设置
        deny all;            # 拒绝其他所有IP访问,重要!
    }
    
    # 主要请求处理 - 增强的灰度路由
    location / {
        # 可根据需要选择使用Cookie或IP策略:
        # 当前使用Cookie策略,如需切换可注释下一行并使用IP策略
        proxy_pass http://$backend_group;
        
        # 如需使用IP策略,取消下面这行的注释:
        # proxy_pass http://$gray_ip_group;
        
        # 设置代理头信息
        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_set_header X-Gray-Version $backend_group;  # 新增:向后端传递灰度版本信息
        
        # 超时设置
        proxy_connect_timeout 30s;
        proxy_send_timeout 30s;
        proxy_read_timeout 30s;
        
        # 重试设置(谨慎使用)
        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
        proxy_next_upstream_tries 2;
    }
}

# Version1 服务配置 (8080端口) - 保持稳定
server {
    listen 8080;
    server_name localhost;
    
    location / {
        root /var/www/html/v1;
        index index.html;
        
        # 记录访问日志用于监控
        access_log /var/log/nginx/version1.access.log;
    }
    
    # 健康检查端点
    location /health {
        access_log off;
        return 200 "Version1 Healthy\n";
        add_header Content-Type text/plain;
    }
}

# Version2 服务配置 (8081端口) - 保持稳定
server {
    listen 8081;
    server_name localhost;
    
    location / {
        root /var/www/html/v2;
        index index.html;
        
        # 记录访问日志用于监控
        access_log /var/log/nginx/version2.access.log;
    }
    
    # 健康检查端点
    location /health {
        access_log off;
        return 200 "Version2 Healthy\n";
        add_header Content-Type text/plain;
    }
}

启动Nginx:

# 检查Nginx的语法
nginx -t 

# 启动nginx
nginx -s reload 
# 可不重启Nginx达到动态更新的效果

4. 访问测试

访问8083端口,查看是否与我们定义的配置文件内定义的比例是否相同:(目前是50:50,每两次必定刷新新旧两个版本。)

在这里插入图片描述
在这里插入图片描述

后续测试和动态平滑更新均可以使用该字段进行更新。

在这里插入图片描述

如果我们想下线Version1,完成大的版本迭代,我们可以修改配置文件:

# 定义后端服务集群 - 增加健康检查参数
upstream backend_servers {
    server 127.0.0.1:8080 weight=10 max_fails=2 fail_timeout=30s down;  # 使用down标记服务下线完成更新
    server 127.0.0.1:8081 weight=90 max_fails=2 fail_timeout=30s; 
}

加上 down标记后,Nginx 在负载均衡时将完全忽略该服务器,所有新请求都会发给健康的服务器。

不过,在彻底下线旧版本的服务之前,我们要进行例行的检查,这里提供一个shell脚本进行检查:

#!/bin/bash

# 服务下线预检查脚本
# 功能:检查服务状态,判断是否可以安全下线,不执行任何下线操作。

set -e  # 遇到错误立即退出,确保检查的严谨性

# 颜色定义,用于清晰显示结果
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # 恢复默认颜色

# 检查结果汇总
CAN_DOWN=true
WARNING_MESSAGES=()
ERROR_MESSAGES=()

SERVICE_ADDR="127.0.0.1:8080"  # 待下线的服务地址(Version1)
SERVICE_NAME="Version1@${SERVICE_ADDR}"

echo -e "🔍 开始对 ${YELLOW}${SERVICE_NAME}${NC} 进行下线前健康检查...\n"
echo "=========================================="

# 1. 检查服务进程是否存在
echo -e "1. 检查服务进程..."
PID=$(lsof -ti :8080)
if [ -n "$PID" ]; then
    echo -e "   ${GREEN}✓ 服务进程存在 (PID: $PID)${NC}"
else
    echo -e "   ${RED}✗ 服务进程不存在${NC}"
    CAN_DOWN=false
    ERROR_MESSAGES+=("服务进程已不存在,无需执行下线操作,但请排查为何进程消失。")
fi

# 2. 检查服务健康接口
echo -e "2. 检查服务健康接口 (HTTP)..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://${SERVICE_ADDR}/health" --connect-timeout 3 --max-time 5 || echo "000")
if [ "$HTTP_CODE" -eq 200 ]; then
    echo -e "   ${GREEN}✓ 健康接口返回 200${NC}"
elif [ "$HTTP_CODE" -eq 000 ]; then
    echo -e "   ${RED}✗ 无法连接到健康接口 (服务可能已完全不可用)${NC}"
    WARNING_MESSAGES+=("服务健康接口无法连接,下线操作风险较低,但需确认是否为预期情况。")
else
    echo -e "   ${YELLOW}⚠ 健康接口返回非200状态码: ${HTTP_CODE}${NC}"
    WARNING_MESSAGES+=("服务健康接口异常 (HTTP ${HTTP_CODE}),建议先排查此问题再下线。")
    CAN_DOWN=false # 若非200状态码认为不健康,可根据您的策略调整
fi

# 3. 检查Nginx upstream中该服务的当前状态
echo -e "3. 检查Nginx配置中该服务的状态..."
NGINX_CONF="/etc/nginx/conf.d/gray-release.conf" # 请修改为您的实际配置文件路径
if grep -q "server ${SERVICE_ADDR}" "$NGINX_CONF"; then
    if grep -q "server ${SERVICE_ADDR}.*down" "$NGINX_CONF"; then
        echo -e "   ${YELLOW}⚠ 服务在Nginx配置中已被标记为 'down'${NC}"
        WARNING_MESSAGES+=("该服务在Nginx配置中已处于 'down' 状态,请确认是否已执行过下线操作。")
    else
        echo -e "   ${GREEN}✓ 服务在Nginx配置中处于活跃状态${NC}"
    fi
else
    echo -e "   ${RED}✗ 在Nginx配置中未找到该服务${NC}"
    ERROR_MESSAGES+=("在Nginx配置文件中未找到目标服务,请检查配置文件路径和服务地址是否正确。")
    CAN_DOWN=false
fi


# 4. 检查服务在负载均衡中的活跃请求(核心修改部分)
echo -e "4. 检查服务在负载均衡中的活跃请求..."
STATUS_URL="http://127.0.0.1:8083/nginx_status"

# 使用curl获取状态页内容,并提取Reading和Writing的数值
STATUS_OUTPUT=$(curl -s $STATUS_URL)
READING=$(echo "$STATUS_OUTPUT" | awk '/Reading/ {print $2}')
WRITING=$(echo "$STATUS_OUTPUT" | awk '/Writing/ {print $4}')

# 显式打印出关键指标
echo "   当前连接状态: Reading=$READING, Writing=$WRITING"

# 核心判断逻辑:只有当读写操作均为0时,才认为可以安全下线
if [ "$READING" -eq 0 ] && [ "$WRITING" -eq 0 ]; then
    echo -e "   ${GREEN}✓ 当前无活跃请求(Reading + Writing = 0)${NC}"
    CAN_DOWN=true
else
    echo -e "   ${RED}✗ 当前仍有活跃请求(Reading: $READING, Writing: $WRITING)${NC}"
    ERROR_MESSAGES+=("服务仍有 [$READING] 个读取请求和 [$WRITING] 个写入请求,请等待请求处理完毕后再下线。")
    CAN_DOWN=false
fi

# 5. 检查系统资源(可选,判断是否存在其他潜在问题)
echo -e "5. 快速系统资源检查..."
LOAD_AVG=$(uptime | awk -F 'load average:' '{print $2}' | awk '{print $1}' | sed 's/,//')
MEM_USAGE=$(free | grep Mem | awk '{printf "%.1f", $3/$2 * 100.0}')
echo -e "   系统平均负载: $LOAD_AVG"
echo -e "   内存使用率: $MEM_USAGE%"
# 简单阈值判断,您可以根据服务器情况调整
if (( $(echo "$LOAD_AVG > 5" | bc -l 2>/dev/null) )); then
    echo -e "   ${YELLOW}⚠ 系统负载较高${NC}"
    WARNING_MESSAGES+=("当前系统负载较高 [${LOAD_AVG}],请评估下线服务对整体性能的影响。")
fi

echo ""
echo "=========================================="
echo -e "📋 ${YELLOW}检查结果汇总:${NC}"

# 输出警告和信息消息
if [ ${#WARNING_MESSAGES[@]} -gt 0 ]; then
    echo -e "\n${YELLOW}⚠ 警告信息:${NC}"
    for msg in "${WARNING_MESSAGES[@]}"; do
        echo "  - $msg"
    done
fi

if [ ${#ERROR_MESSAGES[@]} -gt 0 ]; then
    echo -e "\n${RED}❌ 错误信息:${NC}"
    for msg in "${ERROR_MESSAGES[@]}"; do
        echo "  - $msg"
    done
fi

# 最终决策
echo ""
if [ "$CAN_DOWN" = true ]; then
    echo -e "${GREEN}✅【结论:可以下线】${NC}"
    echo -e "所有关键检查已通过。当前服务状态允许执行下线操作。请您手动执行下线命令。"
else
    echo -e "${RED}❌【结论:不建议下线】${NC}"
    echo -e "存在关键问题,当前下线服务可能存在风险。请先解决上述错误信息中提到的问题。"
fi

echo ""
echo "=========================================="
echo "检查完成于: $(date)"

检查项检查内容为何重要
服务进程服务进程是否仍在运行。确认操作对象存在,避免对已停止的服务进行无效操作。
健康接口服务的 /health端点是否返回200。这是判断服务内部状态是否健康的直接证据,比进程是否存在更重要。
Nginx配置状态服务在Nginx配置中是否已被标记为 down。防止重复操作,提醒您当前配置状态。
活跃连接(建议)服务是否还有未处理完的请求。最关键的一环。如果仍有活跃连接,强制下线会中断用户请求。
系统资源当前系统负载和内存使用情况。评估下线单个服务对整体系统稳定性的潜在影响。

执行该脚本后,会简单的为我们分析当前环境是否可以完全去除旧版本的服务:(注意配置文件路径和服务暴露端口等)

🔍 开始对 Version1@127.0.0.1:8080 进行下线前健康检查...

==========================================
1. 检查服务进程...
   ✓ 服务进程存在 (PID: 1740674
1740675
1740676
1740677
1740678)
2. 检查服务健康接口 (HTTP)...
   ✓ 健康接口返回 200
3. 检查Nginx配置中该服务的状态...
   ✓ 服务在Nginx配置中处于活跃状态
4. 检查服务在负载均衡中的活跃请求...
   当前连接状态: Reading=0, Writing=1
   ✗ 当前仍有活跃请求(Reading: 0, Writing: 1)
5. 快速系统资源检查...
   系统平均负载: 0.02
   内存使用率: 9.1%

==========================================
📋 检查结果汇总:

❌ 错误信息:
  - 服务仍有 [0] 个读取请求和 [1] 个写入请求,请等待请求处理完毕后再下线。

❌【结论:不建议下线】
存在关键问题,当前下线服务可能存在风险。请先解决上述错误信息中提到的问题。

==========================================
检查完成于: Thu Oct  9 14:21:18 CST 2025

当Read和Write均为0后即可下线服务,完成灰度发布:

curl http://127.0.0.1:8083/nginx_status
Active connections: 1 
server accepts handled requests
 5 5 5 
Reading: 0 Writing: 1 Waiting: 0 

5. 基于特定Cookies进行访问

Cookie 名称: gray_release(由 $cookie_gray_release变量定义)
在这里插入图片描述

打开测试页面:在浏览器(Chrome/Firefox/Edge)中访问您的网站 http://您的域名或IP:8083。

打开开发者工具:按 F12键打开开发者工具。

找到 Application/C存储 面板:Chrome/Edge:切换到 Application标签页 -> 左侧找到 Cookies并展开 -> 选择您的网站。Firefox:切换到 存储标签页 -> 左侧找到 Cookie并展开 -> 选择网站。

添加 Cookie:点击 +或右键点击空白处选择 Add。

填写以下信息:

Name: gray_releaseValue: v2(如果想测试新版) 或 v1(如果想测试旧版)

Domain: 域名或IP(如 localhost、IP,通常会自动填充)

Path: /(确保对所有路径生效)

刷新页面:添加完成后,刷新页面,您的所有请求就会被定向到指定的版本,不会受到灰度发布策略所影响。

在这里插入图片描述

问题思考

1. 公共服务连接影响

问题:如果新版本(V2)服务所需的数据库表结构与旧版本(V1)不一致,如何操作?是让V1和V2连接同一个数据库,还是各自连接不同的数据库?

回答:绝对必须使用同一个数据库,并且必须保证数据结构的向后兼容。 让V1和V2服务连接不同的数据库是一个非常危险且错误的选择,会导致严重的数据不一致和业务逻辑混乱。

2. 服务连续性问题

问题:一个用户第一次访问被灰度策略分配到了V2服务,他的后续请求是会持续访问V2,还是可能被随机跳转到V1服务?这个服务链路是否是连续、一致的?

回答:默认的基于权重的灰度策略下,用户的后续请求是随机的,可能会在V1和V2之间跳跃。 但可以通过会话保持技术来解决,强制同一用户的请求始终落在同一个版本上。Nginx默认的权重分流策略是无状态的。对每一个新到来的请求都会根据权重比例重新“掷一次骰子”,独立决定将其转发到V1还是V2。它不关心这个请求来自谁,也不关心这个用户之前的请求去了哪里。

解决方案:

基于Cookie的会话保持

  • Nginx可以提供sticky模块或使用hash算法,主动给用户浏览器设置一个Cookie,里面包含版本信息。
  • 流程:用户第一次访问,Nginx按权重分配并为其设置一个Cookie(如route_version=v2)。该用户后续所有请求都带着这个Cookie,Nginx根据Cookie值直接将其路由到V2,不再重新计算权重。

正是这样的思考,就有了文章接下来的部分:

基于Cookie的会话保持

1. 修改Nginx配置文件

修改Nginx配置文件,本次的文件为生产级Nginx实现灰度发布文件:

# 1) Seed selection: prefer logged-in user id cookie (COOKIE_USER_ID), otherwise use client IP.
#    Replace COOKIE_USER_ID below with your actual login cookie name or leave as empty string "" to always use remote_addr.
# If your login cookie is "user_id", change the first map line to map $cookie_user_id ...
map $cookie_user_id $seed {
    "" $remote_addr;
    default $cookie_user_id;
}

# 2) Percent-based stable assignment based on seed (decided in request phase)
#    Adjust 50%/50% as needed. Uses the seed so same seed maps consistently.
split_clients "$seed" $percent_choice {
    10%     version1_server;   # change 10% to your desired percentage for V1
    *       version2_server;
}

# 3) Cookie mapping (if client already has gray cookie)
map $cookie_gray_version $cookie_group {
    default "";
    "v1" "version1_server";
    "v2" "version2_server";
}

# 4) Final selection: cookie takes precedence, otherwise percent_choice
map "$cookie_group:$percent_choice" $final_group {
    "~^version1_server:" "version1_server";
    "~^version2_server:" "version2_server";
    default $percent_choice;
}

# 5) Compose Set-Cookie string depending on whether cookie already exists AND scheme
#    If cookie is empty and final_group==versionX, generate cookie.
#    Use Secure+SameSite=None when over HTTPS; otherwise omit Secure and use SameSite=Lax.
map "$cookie_gray_version:$final_group:$scheme" $set_gray_cookie {
    "~^:version1_server:https$" "gray_version=v1; Path=/; Max-Age=14400; HttpOnly; Secure; SameSite=None";
    "~^:version2_server:https$" "gray_version=v2; Path=/; Max-Age=14400; HttpOnly; Secure; SameSite=None";
    "~^:version1_server:http$"  "gray_version=v1; Path=/; Max-Age=14400; HttpOnly; SameSite=Lax";
    "~^:version2_server:http$"  "gray_version=v2; Path=/; Max-Age=14400; HttpOnly; SameSite=Lax";
    default "";
}

# Optionally, if you want to set Domain=.yourdomain.com (for sharing across subdomains),
# you can create a map that appends "; Domain=.yourdomain.com" to the cookie strings for https/http cases.
# Example (uncomment and adjust your domain if needed):
# map $set_gray_cookie $set_gray_cookie_domain {
#     default "$set_gray_cookie; Domain=.yourdomain.com";
# }
# Then use add_header Set-Cookie $set_gray_cookie_domain always; (instead of $set_gray_cookie)

# 6) Upstream definitions
upstream version1_server {
    server 127.0.0.1:8080 max_fails=3 fail_timeout=10s;
    keepalive 32;
}

upstream version2_server {
    server 127.0.0.1:8081 max_fails=3 fail_timeout=10s;
    keepalive 32;
}

# 7) Logs (include decision & cookie for observability)
log_format grayformat '$remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_sent "$http_referer" "$http_user_agent" '
    'upstream=$upstream_addr final_group=$final_group percent="$percent_choice" '
    'cookie_gray="$cookie_gray_version" setcookie="$set_gray_cookie"';

# 8) Main server block
server {
    listen 8083;
    server_name YOUR_DOMAIN;  # replace with IP or domain as appropriate

    access_log /var/log/nginx/gray_access.log grayformat;
    error_log  /var/log/nginx/gray_error.log warn;

    # Health/status and static can be added as needed
    location /nginx_status {
        stub_status on;
        access_log off;
        allow 127.0.0.1;
        deny all;
    }

    location / {
        # Expose decision headers (debugging/observability)
        add_header X-Gray-Decision $final_group always;
        add_header X-Gray-Percent $percent_choice always;

        # Add Set-Cookie only when map produced a non-empty cookie string
        # If you need to avoid emitting empty Set-Cookie header when $set_gray_cookie is empty,
        # consider installing the headers_more module and conditionally set headers there.
        add_header Set-Cookie $set_gray_cookie always;

        # Proxy to selected upstream
        proxy_pass http://$final_group;
        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_set_header X-Gray-Version $final_group;

        # Timeouts & buffering tuning (adjust to your app)
        proxy_connect_timeout 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
        proxy_buffering on;

        # Failover behavior: be careful — automatic retry may mask new-version issues.
        # Consider removing proxy_next_upstream or limit its use in strict gray tests.
        proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
        proxy_next_upstream_tries 1;
    }
}

# 9) Backend test servers (example) - replace with real upstream or remove

# Version1 服务配置 (8080端口) - 保持稳定
server {
    listen 8080;
    server_name localhost;
    
    location / {
        root /var/www/html/v1;
        index index.html;
        
        # 记录访问日志用于监控
        access_log /var/log/nginx/version1.access.log;
    }
    
    # 健康检查端点
    location /health {
        access_log off;
        return 200 "Version1 Healthy\n";
        add_header Content-Type text/plain;
    }
}

# Version2 服务配置 (8081端口) - 保持稳定
server {
    listen 8081;
    server_name localhost;
    
    location / {
        root /var/www/html/v2;
        index index.html;
        
        # 记录访问日志用于监控
        access_log /var/log/nginx/version2.access.log;
    }
    
    # 健康检查端点
    location /health {
        access_log off;
        return 200 "Version2 Healthy\n";
        add_header Content-Type text/plain;
    }
}

2. 服务验证

执行该配置文件后进行查看:

在这里插入图片描述

3. 配置文件解读

智能种子选择机制

map $cookie_user_id $seed {
    "" $remote_addr;
    default $cookie_user_id;
}
  • 创新点:优先使用已登录用户的ID作为种子,如果没有则回退到客户端IP
  • 优势:为登录用户提供更稳定的版本分配(同一用户始终访问同一版本),未登录用户按IP分配

稳定的百分比分配

split_clients "$seed" $percent_choice {
    10%     version1_server;
    *       version2_server;
}
  • 优势:基于种子值的哈希分配,确保同一用户/IP的分配结果一致
  • 可调参数:您可以轻松调整百分比(如改为5%/95%)

Cookie处理

map "$cookie_gray_version:$final_group:$scheme" $set_gray_cookie {
    "~^:version1_server:https$" "gray_version=v1; Path=/; Max-Age=14400; HttpOnly; Secure; SameSite=None";
    "~^:version2_server:https$" "gray_version=v2; Path=/; Max-Age=14400; HttpOnly; Secure; SameSite=None";
    "~^:version1_server:http$"  "gray_version=v1; Path=/; Max-Age=14400; HttpOnly; SameSite=Lax";
    "~^:version2_server:http$"  "gray_version=v2; Path=/; Max-Age=14400; HttpOnly; SameSite=Lax";
    default "";
}
  • 安全特性:HTTPS环境下:使用Secure和SameSite=None,适合跨站场景HTTP环境下:使用SameSite=Lax,提供基本安全保护所有Cookie都设置HttpOnly。

优先级决策逻辑

map "$cookie_group:$percent_choice" $final_group {
    "~^version1_server:" "version1_server";
    "~^version2_server:" "version2_server";
    default $percent_choice;
}
  • 明确优先级:现有Cookie > 百分比分配
  • 正则匹配:使用正则表达式确保精确匹配

用户体验优化

  • 会话一致性:登录用户在整个用户会话中保持同一版本
  • 跨设备支持:基于用户ID而非IP,用户在不同设备上也能获得一致体验

安全性与合规性

  • 安全Cookie设置:区分HTTP/HTTPS环境,符合现代浏览器安全要求
  • SameSite策略:防止CSRF攻击,同时支持跨站使用
  • HttpOnly标志:保护Cookie不被JavaScript访问

可观测性与调试

log_format grayformat '$remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_sent "$http_referer" "$http_user_agent" '
    'upstream=$upstream_addr final_group=$final_group percent="$percent_choice" '
    'cookie_gray="$cookie_gray_version" setcookie="$set_gray_cookie"';
  • 详细日志:记录所有关键决策变量,便于故障排查和数据分析
  • 响应头调试:添加X-Gray-Decision和X-Gray-Percent头,便于实时调试

灵活性和可扩展性

  • 易于调整:只需修改split_clients百分比即可调整流量比例
  • 多环境支持:自动适应HTTP和HTTPS环境
  • 域名配置:提供Domain配置示例,支持跨子域名共享Cookie

至此,本篇文章就结束啦,拜拜。

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐