4.9 基于 Git 的 CI/CD 持续部署方案

预计阅读时间:16 分钟

📖 目录

学习目标

学完本章后,你将能够:

  • 理解 CI/CD 的核心概念和流水线架构
  • 使用 GitHub Actions 配置自动化测试和部署工作流
  • 使用 Git Hooks 实现自托管服务器的自动化部署
  • 实现 Blue-Green 零停机部署方案
  • 制定回滚策略和部署检查清单

核心知识

  • CI(持续集成)——开发人员频繁将代码合并到主干,每次合并自动触发构建和测试,尽早发现集成问题
  • CD(持续部署/交付)——CI 通过后自动将代码部署到生产环境(持续部署)或准备就绪等待人工确认(持续交付)
  • GitHub Actions——GitHub 内置的 CI/CD 平台,通过 YAML 定义工作流,支持事件触发(push、PR 等)
  • Git Hooks——Git 版本控制事件触发的本地脚本,如 post-receive 在代码推送到远程仓库后执行
  • Blue-Green 部署——维护两套生产环境(蓝=当前在线,绿=新版本),切换流量完成更新,实现零停机
  • CI 流水线(Pipeline)——代码从提交到部署的自动化流程,通常包括:代码检查→单元测试→构建→集成测试→部署
  • 回滚(Rollback)——部署失败时恢复到上一个已知正常版本的能力

知识关联

原理讲解

为什么需要 CI/CD

传统部署流程:开发在本地写完代码 → 压缩 → scp 到服务器 → 停服务 → 替换文件 → 重启。这种方式的问题包括:手动操作容易出错、部署窗口需要停机、回滚困难、缺乏测试验证。CI/CD 将整个过程自动化:代码推到 Git 即触发 CI 流水线(自动运行测试→构建镜像→推送到镜像仓库→自动部署到目标环境),整个过程无人干预,可重复、可审计、可回滚。

流水线架构

完整的 CI/CD 流水线通常包含以下阶段: ① 触发——代码 push 或 PR merge 到主分支; ② CI 阶段——代码检查(lint)、单元测试、构建产物(如 Docker 镜像); ③ 制品管理——将构建产物推送到制品仓库(Docker Registry、Nexus 等); ④ CD 阶段——将制品部署到目标环境,执行健康检查和烟雾测试; ⑤ 完成——发送通知(Slack/邮件),记录部署版本。

Blue-Green 部署原理

Blue-Green 部署维护两套相同的生产环境(Blue 和 Green)。任意时刻只有一套环境服务用户流量。部署新版本时,将新版本部署到非活跃环境(如 Green),在 Green 上运行测试验证,验证通过后将负载均衡器从 Blue 切换到 Green。如果新版本有问题,只需将流量切回 Blue——回滚秒级完成,且零停机。

流水线设计模式

成熟的 CI/CD 流水线普遍遵循几个可复用的设计模式:Build Once, Deploy Many——CI 只构建一次产物(镜像/制品),所有环境部署同一个 sha 的产物,杜绝"每个环境各自构建"导致的版本漂移;Pipeline as Code——流水线定义文件随代码入库、走 Code Review,任何环境都能重建出同样的流水线;环境门禁递进——dev 自动部署 → staging 自动部署+自动冒烟 → production 人工审批(或 GitOps 合并门禁);失败即止——任一步骤失败立即中止后续阶段并通知,避免坏版本继续向后传播;产物不可变——镜像 tag 用 commit sha 而非 latest,保证每个部署可追溯、可回滚。这些模式与具体平台无关,GitHub Actions、GitLab CI、Jenkins 只是不同的语法外壳。

为什么 CI/CD 要分流水线阶段

将流水线拆分为 lint → test → build → deploy 多个阶段,而非写成一个长脚本,核心原因有三:快速反馈——lint 和单元测试在几秒内完成,如果代码风格有问题或测试失败,开发者在 30 秒内就能得到反馈,而不需要等完整构建完成;资源隔离——不同阶段可以运行在不同类型的 Runner 上(lint/test 用轻量级 runner,build 用大内存 GPU runner),避免资源争抢;失败隔离——test 阶段失败不会触发 build 和 deploy,节省计算资源和时间,同时防止有缺陷的代码进入构建产物。阶段之间的依赖关系(needs)形成有向无环图(DAG),支持并行执行无依赖的阶段,进一步缩短流水线总耗时。

Jenkins vs GitLab CI vs GitHub Actions:平台对比

维度JenkinsGitLab CIGitHub Actions
部署自托管(Java 服务)自托管 / SaaSSaaS(GitHub 托管 Runner)
配置Jenkinsfile(Groovy DSL).gitlab-ci.yml(YAML).github/workflows/*.yml(YAML)
生态1800+ 插件内置功能为主Marketplace
维护成本高(需维护 Jenkins 服务+插件兼容性)中(GitLab 升级同步)低(全托管)
自托管 Runner天然支持支持支持(self-hosted runner)
适用场景遗留系统、深度定制已有 GitLab 的团队GitHub 项目首选

选型建议:代码在 GitHub → GitHub Actions;代码在 GitLab → GitLab CI;需要深度定制且团队有运维能力 → Jenkins。

为什么推荐 Declarative Pipeline 而不是 Scripted

Jenkins 提供两种 Pipeline 语法:Declarative(声明式)和 Scripted(脚本式)。官方推荐 Declarative 的原因:结构化——强制 pipeline {} / stages {} / steps {} 三层结构,一目了然;可读性——非 Jenkins 专家也能读懂流水线在做什么;内置功能——agentenvironmentwheninputpost 等声明式指令直接可用,Scripted 需要手动编写 Groovy 代码;Blue Ocean 兼容——Declarative Pipeline 在 Jenkins 的 Blue Ocean 可视化界面中能正确渲染阶段视图。Scripted 语法更灵活(本质是 Groovy 脚本),适合需要复杂条件逻辑的极端场景,但 95% 的流水线用 Declarative 即可覆盖。

示例代码

1. GitHub Actions 完整工作流

# .github/workflows/deploy.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run tests
        run: |
          npm ci
          npm test

  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Push to registry
        run: |
          docker tag myapp:${{ github.sha }} ghcr.io/myorg/myapp:latest
          docker push ghcr.io/myorg/myapp:latest

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to server via SSH
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_KEY }}
          script: |
            cd /var/www/app
            docker compose pull
            docker compose up -d --force-recreate
            sleep 10
            curl -f http://localhost:8080/health

2. 自建 Git Hooks 部署

# 在服务器上创建 bare 仓库并配置 post-receive hook
sudo mkdir -p /var/repo/app.git
cd /var/repo/app.git
sudo git init --bare  # 输出: Initialized empty Git repository in /var/repo/app.git/

# 创建 post-receive hook
sudo tee /var/repo/app.git/hooks/post-receive << 'HOOK'
#!/bin/bash
set -euo pipefail

DEPLOY_DIR="/var/www/app"
GIT_DIR="/var/repo/app.git"

echo "=== 开始部署: $(date) ==="

# 检出代码到工作目录
git --work-tree="$DEPLOY_DIR" --git-dir="$GIT_DIR" checkout -f main

# 构建和重启
cd "$DEPLOY_DIR"
docker compose build
docker compose up -d --force-recreate

# 健康检查
sleep 5
if curl -sf http://localhost:8080/health > /dev/null 2>&1; then
    echo "=== 部署成功 ==="
else
    echo "=== 健康检查失败,触发回滚 ==="
    git --work-tree="$DEPLOY_DIR" --git-dir="$GIT_DIR" checkout -f HEAD~1
    docker compose up -d --build --force-recreate
    exit 1
fi
HOOK

sudo chmod +x /var/repo/app.git/hooks/post-receive

# 本地开发机添加远程仓库
git remote add production ssh://user@server/var/repo/app.git
git push production main  # 输出: To ssh://user@server/var/repo/app.git

3. Blue-Green 部署脚本

#!/bin/bash
# blue-green-deploy.sh
set -euo pipefail

APP_NAME="myapp"
BLUE_PORT="8081"
GREEN_PORT="8082"
NGINX_CONF="/etc/nginx/sites-available/${APP_NAME}"

# 确定当前活跃环境
if curl -sf http://localhost:$BLUE_PORT/health > /dev/null 2>&1; then
    ACTIVE="blue"
    IDLE="green"
    IDLE_PORT=$GREEN_PORT
else
    ACTIVE="green"
    IDLE="blue"
    IDLE_PORT=$BLUE_PORT
fi

echo "当前活跃: $ACTIVE,部署到: $IDLE (端口 $IDLE_PORT)"

# 部署新版本到非活跃环境
docker compose -f docker-compose.$IDLE.yml up -d --build

# 健康检查(重试 10 次)
for i in $(seq 1 10); do
    if curl -sf http://localhost:$IDLE_PORT/health > /dev/null 2>&1; then
        echo "健康检查通过"
        break
    fi
    if [ $i -eq 10 ]; then
        echo "健康检查失败,回滚"
        docker compose -f docker-compose.$IDLE.yml down
        exit 1
    fi
    sleep 3
done

# 切换 Nginx 流量到新环境
sed -i "s/server 127.0.0.1:$BLUE_PORT/server 127.0.0.1:$IDLE_PORT/" $NGINX_CONF
nginx -t && systemctl reload nginx

echo "部署完成,环境已切换: $IDLE 上线"
echo "旧环境 ($ACTIVE) 保留,如需回滚执行切换脚本"

4. 部署检查清单脚本

#!/bin/bash
# deploy-checklist.sh —— 部署前置检查
set -euo pipefail

echo "=== 部署前置检查 ==="

# 1. 代码通过 CI 测试
echo "[ ] CI 测试已通过"

# 2. 备份当前版本
BACKUP_DIR="/var/backups/deploy/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR"
cp -r /var/www/app "$BACKUP_DIR/app"
echo "[✓] 当前版本已备份到 $BACKUP_DIR"

# 3. 数据库迁移检查
echo "[?] 数据库迁移是否需要?(确认 schema 变更)"
# 如需要,先执行备份
# mysqldump --single-transaction mydb > "$BACKUP_DIR/mydb.sql"

# 4. 部署新版本
echo "[?] 开始部署..."

# 5. 健康检查
sleep 5
if curl -sf http://localhost:8080/health; then
    echo "[✓] 健康检查通过"
else
    echo "[✗] 健康检查失败"
    exit 1
fi

# 6. 烟雾测试
if curl -sf http://localhost:8080/api/status; then
    echo "[✓] 烟雾测试通过"
else
    echo "[✗] 烟雾测试失败"
    exit 1
fi

# 7. 监控验证
echo "[?] 查看 Grafana 确认各项指标正常"

# 8. 回滚方案已就绪
echo "[✓] 回滚方案: git revert 或 docker compose down && cp backup"
echo "=== 部署完成 ==="

5. 回滚策略

# 方案 A:Git 回滚(版本回退)
git revert HEAD --no-edit  # 输出: [main abcd123] Revert "previous commit"
git push origin main  # 输出: To github.com:myorg/myapp.git

# 方案 B:Docker 回滚(使用之前的镜像标签)
docker compose down  # 输出: [+] Running 2/2
# 将 compose.yml 中的 image 标签改回上一个版本
docker compose up -d

# 方案 C:Blue-Green 回滚(秒级切换)
# 重新执行 blue-green-deploy.sh 切换回旧环境

# 方案 D:文件级别回滚
cp -r /var/backups/deploy/20260730_143000/app /var/www/app
systemctl restart myapp

6. GitLab CI 完整流水线示例

# .gitlab-ci.yml
stages:                    # 阶段顺序执行,同阶段 job 并行
  - lint
  - test
  - build
  - deploy

variables:
  IMAGE_TAG: $CI_COMMIT_SHORT_SHA        # GitLab 预置变量
  REGISTRY: registry.example.com/myapp

workflow:
  rules:                   # 流水线级别开关
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"   # MR 只跑检查
    - if: $CI_COMMIT_BRANCH == "main"                    # main 全量发布

lint-job:
  stage: lint
  image: node:22-alpine
  script:
    - npm ci
    - npm run lint
  cache:                   # 依赖缓存: key 随 lock 文件变化,命中后秒级还原
    key:
      files:
        - package-lock.json
    paths:
      - node_modules/

test-job:
  stage: test
  image: node:22-alpine
  script:
    - npm ci
    - npm test -- --coverage
  coverage: '/All files[^|]*\|[^|]*\s+([\d.]+)/'   # 解析覆盖率

build-job:
  stage: build
  image: docker:27
  services:
    - docker:27-dind       # Docker-in-Docker 服务
  script:
    - docker build -t $REGISTRY:$IMAGE_TAG .
    - docker push $REGISTRY:$IMAGE_TAG
  only:
    - main

deploy-prod:
  stage: deploy
  image: alpine:3.19
  script:
    - apk add --no-cache openssh-client
    - echo "$DEPLOY_SSH_KEY" | tr -d '\r' | ssh-add -
    - ssh -o StrictHostKeyChecking=no deploy@prod-server \
        "docker pull $REGISTRY:$IMAGE_TAG && docker compose up -d"
  environment:
    name: production
    url: https://app.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual          # 生产部署需人工点击确认

7. GitHub Actions 工作流语法详解

# .github/workflows/ci.yml
name: CI
on:                        # 触发事件
  push:
    branches: [main]
    paths-ignore: ['docs/**']    # docs 目录变更不触发
  pull_request:
    branches: [main]
  schedule:                # 定时触发(夜间全量构建)
    - cron: '0 2 * * *'
  workflow_dispatch:       # 手动触发按钮

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:              # 矩阵构建: 多版本并行
      matrix:
        node: [20, 22, 24]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          cache: npm       # 内置依赖缓存,命中后秒级还原

      - run: npm ci
      - run: npm test

      - uses: actions/upload-artifact@v4    # 制品传递给后续 job
        with:
          name: test-report-${{ matrix.node }}
          path: coverage/

  deploy:
    needs: build           # 依赖: build 全部成功后执行
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment: production     # 环境保护规则(可挂人工审批)
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - run: |
          docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
          docker push ghcr.io/${{ github.repository }}:${{ github.sha }}

# 关键概念: 事件(on) → 作业(jobs) → 步骤(steps) → 动作(uses/run)
# secrets 存于仓库 Settings → Secrets and variables,运行时以 ${{ secrets.X }} 引用

8. Jenkins 声明式 Pipeline 示例

// Jenkinsfile(声明式语法,存于仓库根目录,随代码版本管理)
pipeline {
    agent { docker { image 'node:20-alpine' } }   // 使用容器作为执行环境

    options {
        timeout(time: 30, unit: 'MINUTES')               // 超时保护
        buildDiscarder(logRotator(numToKeepStr: '20'))   // 保留 20 次构建
        disableConcurrentBuilds()                        // 同分支禁止并发
    }

    environment {
        REGISTRY = 'registry.example.com/myapp'
        IMAGE_TAG = "${env.GIT_COMMIT.take(8)}"
    }

    stages {
        stage('Lint & Test') {
            steps {
                sh 'npm ci'
                sh 'npm run lint'
                sh 'npm test'
            }
        }
        stage('Build Image') {
            steps {
                sh 'docker build -t ${REGISTRY}:${IMAGE_TAG} .'
                sh 'docker push ${REGISTRY}:${IMAGE_TAG}'
            }
        }
        stage('Deploy Staging') {
            steps {
                sh 'ansible-playbook -i inventory/staging deploy.yml -e app_image=${REGISTRY}:${IMAGE_TAG}'
            }
        }
        stage('Deploy Production') {
            input {                    // 人工确认门禁
                message '确认部署到生产环境?'
                ok '开始部署'
            }
            steps {
                sh 'ansible-playbook -i inventory/prod deploy.yml -e app_image=${REGISTRY}:${IMAGE_TAG}'
            }
        }
    }

    post {                            // 结果通知
        success { emailext subject: "构建成功: ${env.JOB_NAME}", to: 'dev@example.com' }
        failure { emailext subject: "构建失败: ${env.JOB_NAME}", to: 'dev@example.com' }
    }
}

常见错误

错误表现根因正确做法
CI 流水线通过但生产部署失败CI 环境与生产环境不一致(操作系统版本、依赖差异)使用 Docker 构建确保环境一致性;CI 和生产使用相同的 Dockerfile
Git Hook 部署后网站 502构建耗时过长,健康检查超时增加健康检查等待时间;使用 Blue-Green 部署避免停机
数据库迁移导致数据丢失migration 脚本有 DROP TABLE 或 ALTER COLUMN 破坏性操作迁移前备份数据库;使用向前兼容的迁移(先加列再删列分两步)
GitHub Actions 中 SSH 连接失败部署服务器的 SSH 密钥未添加到 GitHub Secrets,或目标主机 key 变更在 GitHub 仓库 Settings → Secrets 中添加 DEPLOY_KEY;确认 ssh-keyscan 已更新
回滚后数据不一致回滚了代码但数据库迁移不可逆数据库迁移始终向前兼容;回滚代码时不同时回滚数据库

最佳实践

实践原理示例
CI 阶段运行完整的自动化测试在部署前发现回归问题单元测试 + 集成测试 + 代码风格检查(lint)
使用 Docker 确保环境一致性消除"在我机器上能跑"的问题CI 中构建镜像,部署时拉取同一镜像
采用不可变部署(Immutable Deploy)不原地更新,始终替换整个实例每次部署创建新容器而非在旧容器内修改
部署前备份数据库和配置文件回滚时能恢复到精确的状态mysqldump + cp -r config
逐步发布(Canary Release)先让一小部分用户使用新版本,观察指标后再全量Kubernetes 中设置 strategy: rollingUpdate 控制 maxSurge
每次部署打 Git Tag精确定位每个版本的代码状态git tag v1.2.3 && git push --tags
CI 依赖缓存分层策略node_modules/依赖缓存命中可节省 60% 以上构建时间GitHub Actions 用 setup-node cache: npm;GitLab 用 cache: key: files: package-lock.json
Dockerfile 层缓存优先依赖安装层放在 COPY 源码之前,代码变更不触发依赖重装COPY package*.jsonRUN npm ci,最后 COPY 源码
构建与部署分离,产物不可变部署失败可重复拉取同一产物重试,不重新构建build job 产出 tag=sha 的镜像,deploy job 只拉取不构建

练习题

  1. (概念)CI 和 CD 的区别是什么?持续交付和持续部署又有什么区别?
  2. (概念)Blue-Green 部署如何实现零停机?它的回滚为什么比原地部署快?
  3. (实操)在 GitHub 仓库中创建一个 .github/workflows/ci.yml,实现:当 push 到 main 分支时,执行 npm testdocker build。在仓库中随便放一个 Node.js 项目(含 package.json 和 一个测试)测试流水线运行。
  4. (实操)在本地用 Git 仓库模拟自托管部署:创建 bare 仓库、配置 post-receive hook 将代码检出到 /tmp/deploy-test 目录;从另一个目录 push 代码并验证文件自动更新。
  5. (🔍 挑战)设计并实现一个完整的 Blue-Green 部署实验环境:使用 Docker Compose 启动两套应用实例(不同端口)和一个 Nginx 反向代理;编写切换脚本;模拟部署新版本(修改页面内容)并通过切换 Nginx upstream 完成零停机更新。验证在切换过程中 curl 始终返回 200 而非 502。
点击查看答案
  1. CI(持续集成)是频繁合并代码并自动构建测试,尽早发现集成问题。CD 分为持续交付(自动构建+测试,手动确认部署)和持续部署(自动部署到生产,无人干预)。
  2. Blue-Green 维护两套独立环境,流量通过负载均衡器切换。部署新版本到非活跃环境,验证后一键切换流量。回滚只需将流量切回旧环境,秒级完成。
  3. 创建 .github/workflows/ci.ymlon: push: branches: [main]jobs: test: runs-on: ubuntu-latest → steps: checkout → npm ci → npm test; build: needs: test → docker build。在仓库根目录放 package.json + 简单 test 文件验证。
  4. 服务器创建 bare 仓库并配置 post-receive hook 执行 git --work-tree=/tmp/deploy-test --git-dir=/var/repo/app.git checkout -f main。本地 git push 后检查 /tmp/deploy-test 是否更新。
  5. Docker Compose 启动两个 Nginx 实例(不同端口)+ 一个 Nginx 反向代理(upstream 指向活跃环境)。切换脚本修改 upstream 并 reload。用 curl -I http://localhost 持续监测,切换过程中始终返回 200。

学习检查点

学完本章后,请检验自己是否掌握以下内容:

检查项自测问题验证方法
概念理解能用自己的话解释 CI/CD 的核心价值和流水线阶段尝试向他人讲解
命令操作能不查文档完成 CI/CD 工具配置、流水线编写在终端实际执行
原理掌握能说出持续集成、持续交付、持续部署的区别画出流程图
故障排查能独立排查流水线失败、测试不通过、部署回滚问题模拟故障并修复
最佳实践能说明为什么需要为 CI/CD 配置环境隔离和密钥管理对比不同方案

本章总结

CI/CD 是现代软件工程的核心实践,将代码从提交到部署的全流程自动化。GitHub Actions 是云上最便捷的 CI/CD 方案,适合大多数项目起步。自建 Git Hooks 适合有独立服务器且需要精细控制的场景。Blue-Green 部署实现了零停机更新和秒级回滚。无论选择哪种方案,核心原则不变:自动化测试保障质量、Docker 确保环境一致性、部署可回滚、全过程可审计。每次部署都是一个可重复、可验证的标准化流程。

速查表

命令/配置用途
.github/workflows/*.ymlGitHub Actions 工作流定义
git push production main推送到自建服务器触发部署
post-receive hookGit 接收推送后触发的脚本
git revert HEAD撤销最后一次提交(Git 回滚)
docker compose up -d --build构建并启动新版本
curl -f http://localhost/health健康检查
git tag v1.2.3标记部署版本

学习路径建议

  • 学完本章后建议阅读 cicd01 CI/CD 生产实践(多环境、审批门禁、交付流水线)
  • 学完本章后建议阅读 6.7:GitOps 入门 GitOps 入门(以 Git 为单一事实来源的部署模式)
  • 容器编排场景可看 6.2:Kubernetes 入门 Kubernetes 入门(K8s 原生 Rolling Update 和 Canary 部署)
  • 进阶可学习 Jenkins/GitLab CI/CD 等更多 CI/CD 平台

延伸阅读

常见问题

CI/CD 流水线中密钥怎么管理?
永远不要将密钥写入代码仓库。GitHub Actions 用 Secrets(Settings → Secrets and variables → Actions),GitLab CI 用 CI/CD Variables。在流水线中以 ${{ secrets.MY_SECRET }} 引用。大厂也可用 HashiCorp Vault 动态获取密钥。本地密钥用 .env 文件但确保 .gitignore 忽略。
回滚怎么做最安全?
推荐三种策略:① Git revert:回退代码后重新部署(简单但会留下 revert commit);② 保留上一版本的制品(Docker 镜像、jar 包),部署时指定旧版本 tag;③ 蓝绿部署:保留旧版本环境,切换流量。关键服务的回滚步骤必须写在 runbook 中并定期演练。
灰度发布如何实现?
灰度发布(Canary Release)将新版本先发给小部分用户验证。实现方式:① 多实例分组:新版本先部署到 1 台实例,通过负载均衡规则分配 5% 流量;② K8s 用 Service Mesh(Istio)按 header/cookie 路由;③ 蓝绿部署:新旧两套环境,通过 DNS/负载均衡切换比例。观察 15-30 分钟无异常后再全量。
↑ 回到顶部