Skip to content

🔵 静态资源灰度发布(Canary Release for Static Assets)

静态资源(HTML/CSS/JS/Images 等)没有服务端逻辑,因此灰度发布的核心是:

让部分用户访问新版本静态资源,而其他用户仍使用旧版本。 实现关键:分流 + 版本并存 + 可回滚

下面给你常见可行方案:

⭐ 方案 1:Nginx / CDN 分流(最常用、最稳定)

⛳ 思路

  1. 构建两份静态资源:old 与 new
  2. 按一定规则将部分用户路由到 new 资源目录
  3. 规则可以是:IPCookieHeaderAB Test ID、账户等
nginx
# 默认走旧版本
set $version "v1";

# 如果 Cookie 中带 version=v2 则走灰度版本
if ($http_cookie ~* "version=v2") {
    set $version "v2";
}

location / {
    alias /var/www/$version/;  # 指向对应版本资源目录
    try_files $uri $uri/ /index.html;
}

用法

给灰度用户设置 Cookie

bash
document.cookie = "version=v2; path=/"

优点

✔ 精准控制用户比例 ✔ 版本共存,可随时回滚 ✔ 对前端透明,兼容 SPA

⭐ 方案 2:CDN 边缘规则灰度(企业常用,如阿里云、Cloudflare)

CDN 支持:

  • 按 User-Agent 分流
  • 按地域分流
  • 按 Cookie 分流
  • 按百分比分流(权重)

示例(Cloudflare Worker/PHP Rule)

js
if (Math.random() < 0.1) {
  // 10% 用户访问新版本
  return fetch("https://cdn.xxx.com/app-v2" + request.url);
} else {
  return fetch("https://cdn.xxx.com/app-v1" + request.url);
}

⭐ 方案 3:HTML 层 AB Test(适用于 SPA)

⛳ 思路

给不同用户返回不同版本的 index.html,其里面的 JS/CSS 路径不同。

示例:

  • index.v1.html -> 引用 /static/v1/*
  • index.v2.html -> 引用 /static/v2/*

Nginx/Server 决定给谁返回哪份 HTML。

⭐ 方案 4:单页应用内部版本切换(有限场景)

在 JS 中决定加载哪个版本的资源(比较少用)

js
if (isGrayUser()) {
  loadScript("/static/v2/main.js");
} else {
  loadScript("/static/v1/main.js");
}

⭐ 方案 5:CI/CD 流程灰度

结合脚本自动执行:

  1. 上传 v2 静态资源
  2. CDN / Nginx 设置 x% 灰度分流
  3. 监控指标 OK → 提升百分比 → 全量
  4. 若异常 → 切换回 v1

🔥 关键注意点

1. 必须使用带 Hash 的构建产物

比如:

  • main.55c33.js
  • vendor.9ad01.css

否则缓存会导致用户版本不一致。

2. 旧版本资源必须继续保留

否则灰度用户刷新页面会 404

3. index.html 才是灰度的关键

CDN 缓存策略要保证: 页面(HTML)可灰度,静态资源(JS/CSS)长期缓存。

最常见配置:

  • HTMLno-cache or max-age=0
  • JS/CSScache-control: max-age=31536000, immutable

🟢 给你一个非常简短易记的总结

静态资源灰度 = 两个版本并存 + 控制不同用户返回不同 index.html

核心:HTML 分流,资源多版本共存,缓存版本锁定。

下面给你一个完整、可真实上线的 Vue 项目静态资源灰度发布案例。 包含:构建、多版本部署、Nginx 配置、Cookie 灰度分流、回滚方案。

内容非常实用,面试也能直接讲。

🔵 Vue 项目静态资源灰度发布(完整可落地案例)

📌 场景

你的项目是:

my-app/
  dist/
    v1/     ← 当前线上版本
    v2/     ← 新灰度版本(准备发布)

你想让:

  • 10% 用户访问 v2
  • 90% 用户继续使用 v1
  • 可以随时回滚
  • 旧版本资源继续保留,避免 404

第一步:构建产物做版本隔离

🔧 build 脚本(package.json)加 version 参数

json
{
  "scripts": {
    "build:v1": "vue-cli-service build --dest dist/v1",
    "build:v2": "vue-cli-service build --dest dist/v2"
  }
}

执行:

bash
npm run build:v1   # 线上的旧版本
npm run build:v2   # 即将灰度的新版本

这样 Vue 会分别打包到:

  • /dist/v1/*
  • /dist/v2/*

并且每个 JS/CSS 都带 hash,不冲突。

第二步:服务器结构

将两套版本部署到服务器:

/var/www/my-app/
    v1/
    v2/

index.html 不能缓存,否则灰度分流无效。

第三步:Nginx 灰度配置(核心)

下面是可直接用的灰度分流配置,按 Cookie 判断用户落到 v1 还是 v2

🔧 Nginx 配置(可直接复制)

nginx
# 给灰度用户设置 Cookie 后即可访问 v2
map $http_cookie $version {
    default "v1";
    "~*version=v2" "v2";
}

server {
    listen 80;
    server_name example.com;

    # 根路径
    location / {
        alias /var/www/my-app/$version/;

        # 关闭 index.html 缓存
        add_header Cache-Control "no-cache";

        try_files $uri $uri/ /index.html;
    }

    # 静态资源开启强缓存
    location ~* \.(js|css|png|jpg|jpeg|gif|svg)$ {
        alias /var/www/my-app/$version/;
        add_header Cache-Control "max-age=31536000, immutable";
    }
}
✔ 说明

📌 Cookieversion=v2 → 访问 v2 📌 无 Cookie → 默认访问 v1 📌 JS/CSS 永不过期(带 hash,不会冲突) 📌 HTML 不缓存,以便实时分流

方式一:浏览器控制台测试

js
document.cookie = "version=v2; path=/";

刷新 → 强制进入 v2 版本。

方式二:服务端给部分用户下发 Cookie(真实灰度)

例如:

nginx
# 随机 10% 灰度用户
set $rand "";
set $rand $request_id;

# 取 request_id 的最后一位数字
if ($rand ~ "([0-9])$") {
    set $last_digit $1;
}

# 0~9 的数字,10% 落入 v2
if ($last_digit = 1) {
    add_header Set-Cookie "version=v2; Path=/;";
}

比如末尾是 1 的用户下发灰度 Cookie

第五步:回滚方案(非常重要)

❌ 发现问题?一键回滚:

只需改 Nginx 的分流逻辑即可:

  1. 修改 Nginx 分流:所有用户走 v1

    nginx
    map $http_cookie $version {
        default "v1";
    }
  2. reload Nginx

    bash
    nginx -s reload
  3. 等待所有灰度 Cookie 失效或立即清除 Cookie

  4. 确认没有请求访问 /v2/ 目录(通过 Nginx access log

  5. 最后删除 /v2/ 目录

回滚非常快,因为是纯静态资源。

第六步:CI/CD 自动化(可选)

比如:

  1. Jenkins / GitHub Actions 编译 v2
  2. 上传v2到服务器
  3. 修改 Nginx 分流比例从 1% → 5% → 10%
  4. 观察日志和告警
  5. 全量后删除 v1(或保留 1 个历史版本)

🟢 最终效果

  • 访问同一个域名
  • 不同用户访问到不同版本的 Vue 应用
  • 旧资源保留可随时回滚
  • index.html 动态灰度,JS/CSS 永久缓存
  • Vue 项目完全无侵入

🔵 灰度自动化脚本

脚本以 Nginx + Cookie 分流 为例,可用于 Jenkins / GitHub Actions / 手工执行。

重点特性:

✔ 支持设置灰度比例(如 5%、10%、30%、50%、100%) ✔ 自动写入 Nginxmap 配置 ✔ 自动 reload Nginx ✔ 自动备份配置(防误操作) ✔ 全量后自动关闭灰度逻辑

这是行业常用的一套静态资源灰度控制流程。

🟦 1. Nginx 分流方案(基于随机数灰度)

你需要在 Nginx 中预留一个字段,用于不同脚本来修改:

nginx
# /etc/nginx/conf.d/gray.conf
map $cookie_gray $gray_flag {
    default "off"; # 可由脚本修改为 on/off
}

map $request_id $gray_bucket {
    "~(?<num>[0-9])$" $num;  # 取 request_id 的最后一位数字 (0-9)
}

map "$gray_flag:$gray_bucket" $version {
    # 灰度开关关闭 → 全量旧版本
    "off:0"  "v1";
    "off:1"  "v1";
    "off:2"  "v1";
    "off:3"  "v1";
    "off:4"  "v1";
    "off:5"  "v1";
    "off:6"  "v1";
    "off:7"  "v1";
    "off:8"  "v1";
    "off:9"  "v1";

    # 灰度开关开启 → 实际灰度逻辑由脚本注入
    # (脚本会把 gray.conf 的内容替换成对应灰度比例)
}

脚本只需要修改 灰度比例对应的映射表

🟦 2. 灰度脚本 gray.sh(支持 0–100% 任意比例)

创建文件:

/opt/scripts/gray.sh

内容如下(可直接用):

bash
#!/bin/bash

# -----------------------------
# 参数检查
# -----------------------------
if [ -z "$1" ]; then
    echo "❌ 使用方法: ./gray.sh <比例>"
    echo "   示例: ./gray.sh 10   # 灰度 10%"
    echo "   示例: ./gray.sh 100  # 全量"
    exit 1
fi

PERCENT=$1

if [ "$PERCENT" -lt 0 ] || [ "$PERCENT" -gt 100 ]; then
    echo "❌ 灰度比例必须是 0~100"
    exit 1
fi

# -----------------------------
# 配置文件位置
# -----------------------------
NGINX_GRAY_CONF="/etc/nginx/conf.d/gray.conf"
BACKUP="/etc/nginx/conf.d/gray.conf.bak"

# 备份配置
cp $NGINX_GRAY_CONF $BACKUP

# -----------------------------
# 生成新的灰度映射规则
# -----------------------------
echo "⚙ 正在更新灰度比例为:${PERCENT}% ..."

# 计算有多少 bucket 命中灰度
# bucket 0~9,一共 10
GRAY_BUCKET_COUNT=$((PERCENT / 10))

# 处理小数,如 15% → 灰度 1 个 bucket(10%)
if [ "$PERCENT" -gt 0 ] && [ "$GRAY_BUCKET_COUNT" -eq 0 ]; then
    GRAY_BUCKET_COUNT=1
fi

# -----------------------------
# 写入新版 gray.conf
# -----------------------------
cat > $NGINX_GRAY_CONF <<EOF
# 本文件由 gray.sh 自动生成
map \$request_id \$gray_bucket {
    "~(?<num>[0-9])$" \$num;
}

map \$gray_bucket \$version {
EOF

for i in $(seq 0 9); do
    if [ "$i" -lt "$GRAY_BUCKET_COUNT" ]; then
        echo "    $i  v2;" >> $NGINX_GRAY_CONF
    else
        echo "    $i  v1;" >> $NGINX_GRAY_CONF
    fi
done

echo "}" >> $NGINX_GRAY_CONF

# -----------------------------
# 重载 Nginx
# -----------------------------
nginx -t
if [ $? -ne 0 ]; then
    echo "❌ Nginx 配置错误,正在回滚..."
    cp $BACKUP $NGINX_GRAY_CONF
    exit 1
fi

nginx -s reload
echo "✅ 灰度比例已更新为 ${PERCENT}%"

🟦 3. 使用方式(非常简单)

✔ 灰度 10%
bash
./gray.sh 10

10% 用户访问 v2,90% 进 v1。

✔ 灰度提升到 30%
bash
./gray.sh 30

无需重启,只会让更多用户落到 v2

✔ 灰度 90%
bash
./gray.sh 90
✔ 全量 100%
bash
./gray.sh 100

系统自动把所有 bucket 指向 v2,相当于完全发布。

✔ 关闭灰度(回滚 v1
bash
./gray.sh 0

所有用户全部回到 v1

🟦 4. 日志查看(确认灰度比例)

Nginx access log 中增加:

$version

示例:

log_format main '$remote_addr - $version - [$time_local] "$request" $status';

这样你能看到当前用户落到哪个版本。

🟦 5. 在 CI/CD中使用

Jenkins pipeline 中:

groovy
stage('灰度 10%') {
    sh "/opt/scripts/gray.sh 10"
}

stage('灰度提升 50%') {
    sh "/opt/scripts/gray.sh 50"
}

stage('全量') {
    sh "/opt/scripts/gray.sh 100"
}

🟢 最终效果(你已经具备)

  • ✔ 支持任意灰度比例 0–100%
  • ✔ 自动生成 Nginx 配置
  • ✔ 自动 reload
  • ✔ 支持全量发布
  • ✔ 支持一键回滚
  • ✔ 可以集成 CI/CD
  • ✔ 不需要改前端代码
  • ✔ 不需要改 Nginx 主配置结构

🔵 灰度失败 → 自动降级到稳定版本

真正可在生产使用的「灰度失败 → 自动降级到稳定版本」机制,包括:

  • ✔ 前端自动检测灰度版本加载失败
  • ✔ 自动重新加载稳定版本(v1)
  • ✔ 避免无限循环 + 保证用户最终可正常访问
  • ✔ 适用于 Vue CLI / Vite / SPA
  • ✔ 可同时配合 Nginx 灰度发布使用

🟦 一、核心思路

灰度(v2)用户可能遇到的问题:

  • 新版本 JS 加载失败(网络 / CDN / 打包错误)
  • 白屏(入口文件 load 失败)
  • 静态资源 404
  • 部分文件缓存冲突

当 v2 失败时,应自动降级到 v1,以保证用户不受影响。

机制如下:

  1. 浏览器加载灰度的 index.html(指向 v2/main.js)
  2. 如果任何入口文件(main.js / chunk)加载失败
  3. 自动切换 cookie:version=v1
  4. 强制刷新加载旧版本 v1 的页面
  5. 永不陷入循环:最多尝试 1 次

🟦 二、Nginx 分流(前提)

Nginx 根据 Cookie 分流:

nginx
map $http_cookie $version {
    "~*version=v2"  v2;
    default         v1;
}

灰度用户有 version=v2 → 加载 v2 降级用户 version=v1 → 稳定版 v1

🟦 三、前端自动降级脚本(放在 index.html 顶部)

这是关键! 要放在 index.html<head> 中,优先于所有 JS 加载。

html
<script>
(function () {
  // 降级标记(防无限循环)
  const retryKey = 'gray_retry';

  // 若已经降级过一次,不再重复
  if (localStorage.getItem(retryKey) === 'done') return;

  // 全局资源加载失败监听
  window.addEventListener('error', function (e) {
    const target = e.target;

    // 仅处理 JS/CSS 资源错误
    if (target.tagName === 'SCRIPT' || target.tagName === 'LINK') {
      // 记录已降级
      localStorage.setItem(retryKey, 'done');

      // 切换到稳定版 v1
      document.cookie = "version=v1; path=/; max-age=3600";

      // 刷新页面重新加载 v1 资源
      window.location.reload(true);
    }
  }, true);
})();
</script>

🟦 四、降级的工作原理

假设灰度用户加载:

/v2/js/main.abc123.js

如果:

  • 文件损坏
  • CDN 部署不完整
  • JS 报错
  • 404
  • chunk 丢失(懒加载失败)

浏览器触发 window.error 事件(资源加载异常)。

然后脚本会自动:

1. 设置降级标记,避免无限 reload
js
localStorage.setItem('gray_retry', 'done');
js
document.cookie = "version=v1; path=/";
3. 立即刷新页面 → 加载稳定版本 v1
js
window.location.reload(true);

🟦 五、确保不会无限循环(防止死循环)

只允许触发一次降级:

js
if (localStorage.getItem(retryKey) === 'done') return;

所以逻辑是:

  • 第一次灰度加载失败 → 自动降级
  • 第二次(稳定版 v1)加载一定不会失败 → 不会重复触发

🟦 六、额外增强:捕获 JS Runtime 白屏错误

有时不是资源失败,而是代码 runtime 崩溃(Vue 初始化失败)。

加上:

html
<script>
window.addEventListener("error", function(e) {
  // runtime 错误白屏(灰度版本常见)
  if (e.message && localStorage.getItem("gray_retry") !== "done") {
    localStorage.setItem("gray_retry", "done");
    document.cookie = "version=v1; path=/;";
    location.reload();
  }
});
</script>

🟦 七、额外增强:捕获未捕获 Promise 错误

很多 chunk 加载失败会走这里:

html
<script>
window.addEventListener("unhandledrejection", function(e) {
  if (localStorage.getItem("gray_retry") === "done") return;

  // 如果是动态 chunk 加载失败
  if (e.reason && e.reason.message && e.reason.message.includes('Loading chunk')) {
    localStorage.setItem("gray_retry", "done");
    document.cookie = "version=v1; path=/;";
    location.reload();
  }
});
</script>

🟢 最终你拥有的能力

你现在具备了完整的「灰度失败自动降级」机制:

  • ✔ 自动检测入口 JS / chunk 失败
  • ✔ 自动降级到稳定版本
  • ✔ 自动刷新
  • ✔ 不会重复触发(避免无限循环)
  • ✔ 可结合 Nginx 灰度策略
  • ✔ SPA(Vue)天然支持

最终效果:

灰度版本即使完全坏掉,用户也看不到白屏,而是自动切回稳定版本。

这是企业大规模灰度发布必备的机制。