Skip to content

前端灰度发布落地方案

发布于:

nginx ingress

Table of contents

展开目录

原理

基于 Nginx Ingress Controller

使用 canary-* 注解完成前端基线与灰度的流量转发:

  • 一部分用户继续使用老版本的服务,
  • 将一部分用户的流量切换到新版本,
  • 如果新版本运行稳定,则逐步将所有用户迁移到新版本。

nginx.ingress.kubernetes.io/canary-by-cookie

nginx.ingress.kubernetes.io/canary-by-cookie

容器化

前端需要 dockerized,这样才能变成服务,而后让 ingress 将流量打入前端的 Service

ingress

额外小心

前端容器化后,每次发布新版本会变成全量发布,而不是以往的增量发布

现在的 SPA 项目,打包构建后,

为了加快首屏的展示速度,都会将路由组件打包成数个 module.chunk.js

由统一的主入口 bundle.chunk.js 来控制加载当前路由的 chunk.js

上次发版的构建产物

dist
 ┣ bundle.old.js
 ┣ index.html
 ┗ module.old.js
index.html
<script src="bundle.old.js" />

本次发版的构建产物

dist
 ┣ bundle.new.js
 ┣ index.html
 ┗ module.new.js
index.html
<script src="bundle.new.js" />

由于index.html文件名没有发生变化,发版成功后,

客户端访问时,会使用缓存,即上个版本缓存的 index.html,此时仍然会加载 bundle.old.js

路由匹配后,bundle.old.js 会动态添加 <script src="module.old.js" />

这也是某些场景,前端发布后新功能没有生效的原因。

当容器化发布后,旧的文件会被全部移除,

module.old.js 已经不存在,但又要加载它,浏览器会白屏,控制台会报错。

module not found

缓存的处理

为了避免上述 SPA 应用增量发布的问题,只需要做一件事情:HTML 文件不缓存,缓存其他静态文件

html 不缓存

html no-cache

静态资源文件 缓存 30 天

static cache30days

HTML 文件非常小,通常只有 1~2kb,每次请求并吞吐最新的文件也不会太消耗带宽。

<meta /> 标签控制缓存

index.html
<meta http-equiv="cache-control" content="no-cache" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="expires" content="0" />

仅仅通过 <meta />标签仍然是不够的,还需要完善 nginx.conf

nginx.conf
server {
  listen 80;
  server_name localhost;
  root /usr/share/app;
  index index.php index.html index.htm;
 
  location / {
    try_files $uri $uri/ /index.html;
  }
 
  # 静态资源文件 缓存30天;
  location ~ \.(css|js|gif|jpg|jpeg|png|bmp|swf|ttf|woff|otf|ttc|pfa)$ {
    expires 30d;
  }
 
  # `html` 不缓存
  location ~ \.(html|htm)$ {
    add_header Cache-Control "no-store, no-cache, must-relalidate";
  }
}

Dockerfile

选用 *-alpine 版本的镜像,可以极大缩小镜像体积

Dockerfile
# build stage
FROM node:lts-alpine as build-stage
 
WORKDIR /usr/share/app
 
COPY package*.json ./
 
RUN npm install --registry https://registry.npmmirror.com
 
COPY . .
 
RUN npm run build
 
# production stage
FROM nginx:stable-alpine as production-stage
 
COPY --from=build-stage /usr/share/app/nginx.conf /etc/nginx/nginx.conf
 
COPY --from=build-stage /usr/share/app/dist /usr/share/app
 
EXPOSE 80
 
CMD ["nginx", "-g", "daemon off;"]

本地调试,创建容器并运行项目

docker build -t app:latest .
 
# 访问 http://localhost:8080
docker run -itd -p 8080:80 app:latest

基线与灰度

工作流

环境切换

一部分用户继续使用老版本的服务(基线),一部分用户的流量切换到新版本(灰度)

灰度要精确到用户级别,那么最好的方式便是提供一个 API 让前端调用,

静态资源层 通过 cookie: env-gray=always ,由 ingress 将流量转发至 prodprod-gray

示例代码
const loadEnv = async () => {
  const COOKIE_GRAY_KEY = "env-gray";
  const COOKIE_GRAY_VALUE = "always";
 
  const reload = () => {
    // 设置 cookie: env-gray=always 后,重新加载页面
    // 下次请求携带 always, ingress 便会将流量转发至 prod-gray
    window.location.reload();
  };
 
  try {
    const { data } = await request("user/env");
 
    const currentEnvIsGray = getCookie(COOKIE_GRAY_KEY) === COOKIE_GRAY_VALUE;
 
    const env = data?.env;
 
    switch (env) {
      case "gray":
        if (!currentEnvIsGray) {
          setCookie(COOKIE_GRAY_KEY, COOKIE_GRAY_VALUE);
          reload();
        }
        break;
 
      default:
        removeCookie(COOKIE_GRAY_KEY);
 
        if (currentEnvIsGray) {
          reload();
        }
        break;
    }
  } catch (e) {
    removeCookie(COOKIE_GRAY_KEY);
  }
};

cookie 不存在灰度标识,流量转发至基线:

基线

cookie 存在灰度标识,流量转发至灰度:

灰度

API 层 通过请求头增加灰度标识,来告知 API 按 prod 还是 prod-gray 处理。

后端打通基线与灰度,比前端复杂更多,阿波罗配置、字典表、权限等等一系列的配置或业务需要做处理,

除了在请求头中增加 API 的灰度标识,也可以结合自身项目实际场景替换。