小Cの已经记不起来的博客

React 项目中 蓝绿部署 的设计取舍

React 项目做蓝绿部署,一开始我以为把 Nginx 的 root 指到新目录就完事了,结果被浏览器缓存和 index.html 教育了一顿。前端蓝绿不像后端那样只要两个实例、切个 Service 就完事,真正麻烦的是静态资源、入口文件和 API 兼容窗口。下面是我们踩过一圈之后留下的方案,以及每一步为什么要这么取舍。

先说结论:别把蓝绿部署想成流量开关

React 打包出来就是一堆静态文件,理论上复制两份目录,一份 blue 一份 green,Nginx 切过去就完事。但实际切的时候你会发现:用户浏览器里可能还缓存着旧 index.html,旧 index.html 又引用了旧 JS;你切到新目录后旧资源被删了,页面直接白屏。所以前端蓝绿的核心不是“切流量”,而是“让入口原子切换,让资源可以共存”。

我们最后没上 K8s 那套双 Deployment,而是用了版本目录 + 软链切换。不是 K8s 不好,是小项目两套 Pod 的成本和复杂度不划算,静态文件又不需要常驻进程。

静态资源缓存是第一个坑

React 构建产物里,带 hash 的 JS/CSS 可以放心长缓存,但 index.html 绝对不能。我们一开始 Nginx 配了全站 expires 1d,结果切完软链半小时,还有用户访问旧入口。后来改成:

root /var/www/react-app/current;

location = /index.html {
    add_header Cache-Control "no-store";
}

location /assets/ {
    add_header Cache-Control "public, max-age=31536000, immutable";
    try_files $uri =404;
}

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

/assets/ 是 Vite 的默认目录,CRA 项目对应 /static/。关键是资源文件带 hash,内容变了文件名就变,所以可以一年缓存;入口文件不缓存,切换后用户下次请求就能拿到新版本。代价是每次都要回源拿 HTML,但这点流量对前端项目真不算什么。

入口文件必须原子切换

软链切换看着简单,但 ln -sfn 写得不对会有短暂空窗。我们用的是先建临时链接再 mv -Tf:

#!/usr/bin/env bash
set -euo pipefail

NEW_RELEASE="/var/www/react-app/releases/20250601-abc123"

ln -sfn "$NEW_RELEASE" /var/www/react-app/current.new
mv -Tf /var/www/react-app/current.new /var/www/react-app/current

这样 current 指向新版本的动作是原子的,Nginx 不会读到半个链接。代价是旧版本目录不能马上删,至少保留最近 5 个 release。因为用户可能还开着旧页面,旧 index.html 引用的 /assets/xxx.js 必须还能访问。磁盘成本很小,别在这上面省。

软链、Nginx 和 K8s 怎么选

如果项目已经容器化,用两个 Deployment + Ingress 切流量也没问题,回滚就是切回旧 Service。但 React 静态文件做蓝绿,K8s 方案多出来的是一套镜像、一组 Pod 和更长的发布链路。小团队用 Nginx + 软链,发布脚本十几行,回滚也是十几行,出问题时心里有数。

取舍点在于:你需要多强的原子性和多快的回滚。Nginx 方案足够应付大多数中后台和官网项目;如果你们已经在用 Argo Rollouts,那就别为了蓝绿再造一套目录切换,跟着现有发布体系走。

API 兼容窗口比切换动作重要

前端蓝绿切过去之后,旧页面可能还在跑。旧前端调的 API 不能立刻删,字段不能立刻改语义。我们吃过一次亏:后端跟着前端一起发,把旧接口下线了,结果没刷新的用户页面开始报 404。后来定了个规矩:前端蓝绿切换后,旧 API 至少保留一个发布周期,数据库迁移先扩后缩,别在同一次发布里做破坏性变更。

如果构建时把环境变量打进包里,blue 和 green 的环境不同就得分别构建,镜像复用性会差一些。我们后来把后端地址放到运行时 config.js 里,静态包不绑环境,蓝绿目录可以直接复制。代价是多一次请求,但换来的是发布时不用重新打包。

回滚不是把软链切回去那么简单

前端回滚本身很快,软链切回旧 release 就行。但如果新版本已经调用了新接口、写入了新格式数据,回滚就没那么干净。所以我们的发布顺序是:先兼容 API,再发前端;先内部账号切绿,观察 Sentry 和接口错误率,再放 10%,最后全量。监控里一定带版本号,Sentry 的 release 和页面上的 window.__APP_VERSION__ 对得上,出问题能马上定位是哪一版。

我们最后留下的方案

构建产物按 releases/日期-commit 放,current 软链原子切换;index.html 不缓存,带 hash 的资源长缓存;保留最近 5 个版本;API 向后兼容一个发布周期;发布脚本先切内部账号,再按比例放量。说不上多高级,但每次发布不用提心吊胆,回滚也不用翻聊天记录找旧包。蓝绿部署在 React 项目里,真正要设计的不是“怎么切”,而是“切完之后旧东西还能活多久”。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放