无停服更新技术方案

基于反向代理、蓝绿槽位、健康检查、原子切流和快速回滚的无停服发布教程,适合 1Panel、Docker Compose、Caddy / Nginx 部署场景。

方案目标

无停服更新的目标不是“永远不会出错”,而是让发布过程具备可验证、可切换、可回滚的工程结构。 用户请求始终由一个健康版本承接,新版本只有在通过检查后才进入生产流量。

目标做法结果
不中断访问旧版本继续服务,新版本在空闲槽位启动用户请求不会打到未启动完成的实例
降低发布风险先内部健康检查,再公网 smoke test问题在切流前暴露
快速回滚旧槽位不立刻停止新版本异常时可以切回旧 upstream
便于审计记录镜像 tag、commit、active 槽位、验证结果出问题时能定位是哪次发布引入

核心思路

core-ideatext
无停服更新不是“发布速度快”,而是把风险拆成 4 个可控动作:

1. 老版本继续对外服务
2. 新版本先部署到空闲环境
3. 新版本健康检查通过后,再切换流量
4. 老版本保留一段时间,发现问题可以快速切回

传统发布经常是“停旧服务、替换文件、启动新服务”。这会带来窗口期: 新服务启动慢、健康检查失败、静态资源不完整、数据库迁移异常,都会直接影响用户。 蓝绿发布把这些动作移到空闲槽位完成,最后只做一次反向代理切换。

推荐架构

architecturetext
用户请求
  ↓
域名 / CDN / DNS
  ↓
反向代理层(Caddy / Nginx / Traefik)
  ↓
当前 active upstream
  ├─ app-blue   当前生产版本
  └─ app-green  空闲发布槽位

发布时:
1. 如果 blue 正在对外服务,就把新版本部署到 green
2. green 健康检查通过后,把反向代理 upstream 指向 green
3. blue 暂时保留,作为回滚版本
组件职责关键要求
反向代理接收公网请求,并转发到 active 槽位支持 reload,不应重启导致断流
blue 槽位一个完整应用实例能独立提供服务
green 槽位另一个完整应用实例与 blue 共享同一套外部依赖,但端口或容器名隔离
健康检查判断实例是否可接流量不能只检查进程存在,要检查依赖是否可用
回滚入口把 active upstream 指回旧槽位必须简单、明确、可重复执行

发布前检查

prechecktext
发布前检查:

1. 确认当前分支、commit、镜像 tag 或构建产物版本
2. 确认本地没有未提交的关键变更
3. 确认私钥、ssh.md、.env、证书等敏感文件没有进入 Git
4. 确认数据库迁移是否向后兼容
5. 确认新版本有 /health 或 /ready 健康检查
6. 确认反向代理可以无中断 reload
7. 确认有明确回滚路径

标准发布流程

release-flowtext
标准蓝绿发布流程:

1. 识别当前 active 槽位
2. 选择 idle 槽位作为新版本部署目标
3. 拉取或构建新镜像
4. 启动 idle 槽位容器
5. 等待 idle 槽位 /ready 返回 200
6. 对 idle 槽位做内部 smoke test
7. 更新反向代理 upstream 到 idle 槽位
8. reload 反向代理
9. 通过公网域名验证 /ready、/health 和核心页面
10. 保留旧槽位一段时间,用于快速回滚

判断发布是否成功,不能只看容器是否 running。必须从公网域名验证真实路径, 因为用户访问链路还包括 DNS、CDN、TLS、反向代理、静态资源和后端接口。

Docker Compose 示例

下面是简化示例。真实生产环境还会包含网络、卷、环境变量、日志、资源限制和反向代理配置。

docker-compose.bluegreen.ymlyaml
services:
  app-blue:
    image: ghcr.io/example/app:2026-05-27-001
    container_name: app-blue
    environment:
      - APP_SLOT=blue
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/ready"]
      interval: 10s
      timeout: 3s
      retries: 30

  app-green:
    image: ghcr.io/example/app:2026-05-27-002
    container_name: app-green
    environment:
      - APP_SLOT=green
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/ready"]
      interval: 10s
      timeout: 3s
      retries: 30

反向代理切流

反向代理层只维护一个 active upstream 文件。切流时替换这个文件并 reload 代理, 不直接停止旧容器。

blue-active.confcaddyfile
# caddy-upstreams/app-active.conf
reverse_proxy app-blue:3000
green-active.confcaddyfile
# caddy-upstreams/app-active.conf
reverse_proxy app-green:3000
release-commands.shbash
# 1. 查看当前运行状态
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"

# 2. 查看当前 active upstream
cat caddy-upstreams/app-active.conf

# 3. 启动空闲槽位
docker compose up -d app-green

# 4. 等待健康检查
curl -fsS http://app-green:3000/ready

# 5. 切换 upstream 后 reload 反向代理
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile

# 6. 从公网域名验证
curl -fsS https://example.com/ready
curl -fsS https://example.com/health

前端静态资源热更新

如果只是改文案、样式、前端页面或 i18n 文案,不一定需要重建后端镜像。 可以把前端产物作为静态资源覆盖目录发布,再通过新槽位加载新资源。

frontend-only-hotfixtext
前端静态资源热更新策略:

1. 本地构建前端 dist
2. 打包 dist,避免带入 macOS ._* 文件
3. 上传到服务器临时目录
4. 解压到 data/public.new-时间戳
5. 校验文件完整性和权限
6. 把旧 data/public 原子移动为备份目录
7. 把 public.new 原子切换为 data/public
8. 让新槽位读取静态资源目录
9. 健康检查通过后切换流量
10. 保留旧 public 目录用于回滚

健康检查设计

health-checkstext
建议至少准备 3 层检查:

1. /health
   进程是否存活,依赖是否能初始化

2. /ready
   当前实例是否已经准备好接收真实流量

3. smoke test
   用公网域名访问核心页面或核心 API,确认反向代理、静态资源、鉴权和数据库都正常

/health 可以很轻,表示进程活着;/ready 应该更严格, 表示实例可以接流量。发布切流必须以 ready 为准。

回滚方案

rollbacktext
回滚流程:

1. 如果新槽位还没切流量:停止新槽位,继续保留旧版本
2. 如果已经切流量:把 upstream 改回旧槽位
3. reload 反向代理
4. 通过公网域名验证 /ready、/health 和核心业务
5. 如果是前端静态资源问题,把 data/public.bak 恢复为 data/public
6. 记录失败原因,禁止在未定位原因前重复发布同一版本

回滚方案要在发布前就存在,而不是线上出问题后临时想。最实用的做法是: 新版本切流后不要立刻停止旧槽位,至少保留到观察窗口结束。

高风险场景

danger-listtext
这些情况不适合直接蓝绿切换:

1. 数据库迁移不向后兼容
2. 新旧版本不能同时连接同一份数据
3. 消息队列消费者会重复消费或抢锁
4. 文件存储目录结构被破坏性变更
5. 新版本启动成功,但核心业务没有 smoke test
6. 反向代理 reload 会中断连接,且没有验证过

发布后观察

post-releasetext
发布后观察:

1. 公网 /ready 和 /health 状态码
2. 反向代理日志里的 4xx / 5xx
3. 新槽位容器最近 5-10 分钟错误日志
4. 核心接口延迟和首包时间
5. 前端 index.html 是否引用新 chunk
6. 控制台是否出现静态资源 404
7. 用户侧是否有登录、支付、API 调用等关键链路异常

建议每次发布都记录:发布时间、commit、镜像 tag、切换前 active 槽位、切换后 active 槽位、 健康检查结果、smoke test 结果和是否保留旧槽位。

下一步