发版流程实战:从代码提交到生产上线的完整链路
作者:林 | 系列:DevOps 实战 | 适合读者:后端/运维/架构师
一、为什么需要一套规范的发版流程
很多团队的发版流程是"人工操作 + 口头确认":开发本地打包、运维手动部署、群里喊一声"发了"。这种方式在团队小、服务少的时候还能凑合,但一旦服务超过 5 个、团队超过 10 人,各种问题就来了:
- 发错版本:分支没合并干净,把半成品发到生产
- 回滚慢:出问题后找不到上一个稳定版本,或者回滚步骤不一致
- 环境不一致:测试环境和生产环境配置不同,测试通过了生产挂了
- 发版窗口长:从代码合并到上线要半天,错过业务窗口
一套规范的发版流程并非为了"流程化而流程化",重点是为了让发版可重复、可预测、可回滚。
二、整体架构一览
一套完整的发版链路,通常包含以下环节:
每个环境的职责不同:
| 环境 | 用途 | 部署触发方式 | 数据 |
|---|---|---|---|
| dev | 开发自测 | 手动 / push 触发 | mock 数据 |
| test | 功能测试、集成测试 | 合并到 develop 触发 | 测试数据 |
| staging | 预发布验证,跟生产一致 | 合并到 main / 手动 | 生产数据脱敏副本 |
| prod | 线上服务 | 打 tag / 手动审批 | 真实数据 |
三、分支策略
3.1 Git Flow(经典方案)
适合有明确版本发布节奏的项目:
main ────────────────────────────────────────── (生产)
│
├── release/v1.2 ──────────────────────────── (发布分支)
│ │
│ ├── hotfix/fix-xxx ──────────────────── (热修复)
│ │
│ └── merge back to main + develop
│
└── develop ───────────────────────────────── (开发主线)
│
├── feature/user-login ──────────────── (功能分支)
├── feature/payment ─────────────────── (功能分支)
└── bugfix/fix-null-pointer ─────────── (修复分支)分支命名约定:
| 前缀 | 用途 | 示例 |
|---|---|---|
feature/ | 新功能 | feature/user-login |
bugfix/ | Bug 修复 | bugfix/fix-null-pointer |
hotfix/ | 生产热修复 | hotfix/fix-payment-timeout |
release/ | 发布分支 | release/v1.2.0 |
工作流程:
- 从
develop切feature/xxx分支 - 开发完成,提 MR 合并回
develop develop自动部署到测试环境- 测试通过,从
develop切release/vx.x.x release分支部署到 staging,验证通过后合并到mainmain打 tag,自动部署到生产
3.2 Trunk-Based(主干开发)
适合持续部署、快速迭代的团队:
main ────────────────────────────────────────── (主干)
│
├── feature/short-lived-branch (1-2天) ───── (短生命周期分支)
│ └── merge back quickly
│
└── release/v1.2 (从 main 切出) ──────────── (发布 tag)核心原则:
- 分支生命周期不超过 2 天
- 用 feature flag 控制未完成功能的可见性
- 每次合并都触发 CI/CD
选择建议:
| 场景 | 推荐 |
|---|---|
| 团队 < 10 人,日发多次 | Trunk-Based |
| 团队 > 10 人,有固定发版节奏 | Git Flow |
| 有多个版本并行维护 | Git Flow |
| 微服务架构,独立部署 | Trunk-Based + Feature Flag |
四、CI/CD 流水线设计
4.1 CI(持续集成)阶段
代码合并后自动触发,核心目标是快速反馈。下面是一个生产级的 CI 工作流示例,包含构建、测试、代码扫描、镜像推送全流程:
# .github/workflows/ci.yml
# 完整的 CI 流水线:构建 → 测试 → 代码扫描 → 镜像构建与推送
name: CI Pipeline
on:
push:
branches: [develop, main]
pull_request:
branches: [develop, main]
# 同一分支的新 push 自动取消之前的运行,避免资源浪费
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# ──────────────────────────────────────────────
# Job 1: 代码编译与单元测试
# ──────────────────────────────────────────────
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'corretto'
cache: 'maven' # 内置缓存支持,无需手动配置 actions/cache
- name: Build & Unit Test
run: mvn clean verify -B -DskipITs # 跳过集成测试,单独跑
- name: Generate JaCoCo Report
if: success()
run: mvn jacoco:report
# 上传测试报告和覆盖率报告,方便在 PR 中查看
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
target/surefire-reports/
target/site/jacoco/
retention-days: 7
# ──────────────────────────────────────────────
# Job 2: 代码质量与安全扫描
# ──────────────────────────────────────────────
code-scan:
runs-on: ubuntu-latest
needs: build-and-test # 构建通过后再扫描
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # SonarQube 需要完整 git 历史来做 blame 分析
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'corretto'
cache: 'maven'
# SonarQube 静态代码分析
- name: SonarQube Scan
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
run: mvn sonar:sonar -Dsonar.projectKey=myapp
# Trivy 文件系统漏洞扫描(扫描依赖包)
- name: Trivy FS Scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
severity: 'CRITICAL,HIGH'
exit-code: '1' # 发现高危漏洞时阻断流水线
# ──────────────────────────────────────────────
# Job 3: 构建并推送 Docker 镜像
# ──────────────────────────────────────────────
build-image:
runs-on: ubuntu-latest
needs: [build-and-test, code-scan] # 测试和扫描都通过后才构建镜像
# 仅在 push 到主分支时推送镜像,PR 只做验证
if: github.event_name == 'push'
permissions:
contents: read
packages: write # 推送到 GitHub Container Registry
outputs:
image-tag: ${{ steps.meta.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
# 使用 Docker Buildx 获得更好的构建缓存
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# 登录 GitHub Container Registry
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# 自动生成镜像 tag:分支名-sha、语义化版本(如果是 tag)
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
# 构建并推送,使用 GitHub Actions 缓存加速二次构建
- name: Build & Push Docker Image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# 镜像构建后立即扫描镜像漏洞
- name: Trivy Image Scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
severity: 'CRITICAL,HIGH'
exit-code: '1'CI 阶段的关键检查点:
| 检查项 | 工具 | 失败策略 |
|---|---|---|
| 编译通过 | Maven / Gradle | 阻断 |
| 单元测试通过 | JUnit 5 / TestNG | 阻断 |
| 代码覆盖率 ≥ 80% | JaCoCo | 告警(可配置为阻断) |
| 代码规范检查 | Checkstyle / SpotBugs | 告警 |
| 静态代码分析 | SonarQube | 质量门禁阻断 |
| 依赖漏洞扫描 | Trivy / Snyk | 高危阻断 |
| 镜像漏洞扫描 | Trivy | 高危阻断 |
| 镜像构建与推送 | Docker Buildx + GHCR | 阻断 |
4.2 CD(持续部署)阶段
CI 通过后,进入部署环节。核心是分环境逐步推进。下面是一个完整的 CD 工作流,包含 staging 部署、审批、canary 灰度、生产全量发布:
# .github/workflows/deploy.yml
# CD 流水线:staging 部署 → 审批 → canary 灰度 → 生产全量
name: CD Pipeline
on:
# 通过 workflow_call 被 CI 流水线调用,或手动触发
workflow_call:
inputs:
image-tag:
required: true
type: string
description: '要部署的镜像 tag'
workflow_dispatch:
inputs:
image-tag:
required: true
type: string
description: '要部署的镜像 tag(Git SHA)'
skip-canary:
required: false
type: boolean
default: false
description: '是否跳过 canary 灰度阶段'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# ──────────────────────────────────────────────
# Job 1: 部署到 Staging 环境
# ──────────────────────────────────────────────
deploy-staging:
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- name: Checkout
uses: actions/checkout@v4
# 配置 kubectl 连接 staging 集群
- name: Set up kubeconfig
uses: azure/k8s-set-context@v4
with:
kubeconfig: ${{ secrets.STAGING_KUBECONFIG }}
# 更新 Deployment 镜像
- name: Deploy to Staging
run: |
kubectl set image deployment/myapp \
myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ inputs.image-tag }} \
-n staging
echo "等待 rollout 完成..."
kubectl rollout status deployment/myapp -n staging --timeout=300s
# 冒烟测试:验证核心接口
- name: Smoke Test
run: |
for i in $(seq 1 5); do
if curl -sf https://staging.example.com/health; then
echo "\n✅ Staging 健康检查通过"
exit 0
fi
echo "等待服务就绪... ($i/5)"
sleep 10
done
echo "❌ Staging 健康检查失败"
exit 1
# E2E 测试(可选)
- name: E2E Tests
run: |
# 在 staging 环境运行端到端测试
# mvn verify -Pstaging -Dbase.url=https://staging.example.com
echo "E2E 测试通过"
# ──────────────────────────────────────────────
# Job 2: Canary 灰度部署(10% 流量)
# ──────────────────────────────────────────────
# 通过 GitHub Environment 的审批门禁
deploy-canary:
needs: deploy-staging
if: ${{ !inputs.skip-canary }}
runs-on: ubuntu-latest
environment:
name: production-canary
url: https://example.com
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up kubeconfig
uses: azure/k8s-set-context@v4
with:
kubeconfig: ${{ secrets.PROD_KUBECONFIG }}
# 部署 canary 版本(副本数 1,承载约 10% 流量)
- name: Deploy Canary
run: |
kubectl set image deployment/myapp-canary \
myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ inputs.image-tag }} \
-n production
kubectl rollout status deployment/myapp-canary -n production --timeout=300s
# 灰度观察期:监控核心指标
- name: Monitor Canary (5 min)
run: |
echo "🔍 开始灰度观察,持续 5 分钟..."
for i in $(seq 1 10); do
# 检查错误率(从 Prometheus 拉取)
ERROR_RATE=$(curl -s "http://prometheus:9090/api/v1/query?query=rate(http_server_requests_seconds_count{status=~'5..',application='myapp',version='canary'}[1m])/rate(http_server_requests_seconds_count{application='myapp',version='canary'}[1m])*100" | jq -r '.data.result[0].value[1] // "0"')
echo "[$((i*30))s] 错误率: ${ERROR_RATE}%"
if (( $(echo "$ERROR_RATE > 1" | bc -l) )); then
echo "❌ 错误率超过阈值 1%,触发回滚"
kubectl rollout undo deployment/myapp-canary -n production
exit 1
fi
sleep 30
done
echo "✅ 灰度观察通过"
# ──────────────────────────────────────────────
# Job 3: 生产环境全量发布
# ──────────────────────────────────────────────
deploy-production:
needs: [deploy-staging, deploy-canary]
# 如果跳过 canary,只要有 staging 就行;否则需要 canary 通过
if: always() && needs.deploy-staging.result == 'success' && (needs.deploy-canary.result == 'success' || needs.deploy-canary.result == 'skipped')
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up kubeconfig
uses: azure/k8s-set-context@v4
with:
kubeconfig: ${{ secrets.PROD_KUBECONFIG }}
# 全量更新生产 Deployment
- name: Full Rollout
run: |
kubectl set image deployment/myapp \
myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ inputs.image-tag }} \
-n production
echo "等待全量 rollout 完成..."
kubectl rollout status deployment/myapp -n production --timeout=600s
# 清理 canary 副本
- name: Scale Down Canary
if: ${{ !inputs.skip-canary }}
run: |
kubectl scale deployment/myapp-canary --replicas=0 -n production
echo "✅ Canary 副本已缩容"
# 发版后验证
- name: Post-deploy Verification
run: |
echo "🔍 生产环境健康检查..."
for i in $(seq 1 5); do
if curl -sf https://example.com/health; then
echo "\n✅ 生产环境健康检查通过"
exit 0
fi
sleep 10
done
echo "❌ 生产环境健康检查失败,考虑回滚"
exit 1
# 发送发版通知
- name: Notify
if: always()
run: |
STATUS=${{ job.status }}
echo "发版 ${STATUS}: ${{ inputs.image-tag }}"
# 可接入企业微信/钉钉/Slack webhook
# curl -X POST "$WEBHOOK_URL" -H 'Content-Type: application/json' -d '{"text":"发版完成: ${{ inputs.image-tag }}"}'4.3 Monorepo 多服务构建优化
在微服务 monorepo 场景下,每次提交可能只改了某个子服务,但全量构建所有服务既浪费时间又浪费资源。GitHub 提供了 dorny/paths-filter 和 tj-actions/changed-files 两个 Action 来检测变更路径,只构建有变化的服务:
# .github/workflows/monorepo-ci.yml
# Monorepo CI:只构建变更的服务
name: Monorepo CI
on:
push:
branches: [develop, main]
pull_request:
branches: [develop, main]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
# ──────────────────────────────────────────────
# Job 1: 检测哪些服务发生了变更
# ──────────────────────────────────────────────
detect-changes:
runs-on: ubuntu-latest
outputs:
user-service: ${{ steps.filter.outputs.user-service }}
order-service: ${{ steps.filter.outputs.order-service }}
gateway: ${{ steps.filter.outputs.gateway }}
steps:
- uses: actions/checkout@v4
- name: Detect changed paths
id: filter
uses: dorny/paths-filter@v3
with:
filters: |
user-service:
- 'services/user-service/**'
- 'libs/common/**' # 公共库变更也要重新构建
order-service:
- 'services/order-service/**'
- 'libs/common/**'
gateway:
- 'services/gateway/**'
- 'libs/common/**'
# ──────────────────────────────────────────────
# Job 2: 使用 matrix 并行构建变更的服务
# ──────────────────────────────────────────────
build-services:
needs: detect-changes
# 仅在有服务变更时运行
if: needs.detect-changes.outputs.user-service == 'true' || needs.detect-changes.outputs.order-service == 'true' || needs.detect-changes.outputs.gateway == 'true'
runs-on: ubuntu-latest
strategy:
# 动态 matrix:只包含有变更的服务
matrix:
include:
- service: user-service
enabled: ${{ needs.detect-changes.outputs.user-service }}
module: services/user-service
port: 8081
- service: order-service
enabled: ${{ needs.detect-changes.outputs.order-service }}
module: services/order-service
port: 8082
- service: gateway
enabled: ${{ needs.detect-changes.outputs.gateway }}
module: services/gateway
port: 8080
# 某个服务构建失败不影响其他服务
fail-fast: false
steps:
- name: Skip if not changed
if: matrix.enabled != 'true'
run: echo "⏭️ ${{ matrix.service }} 未变更,跳过构建"
- name: Checkout
if: matrix.enabled == 'true'
uses: actions/checkout@v4
- name: Set up JDK 17
if: matrix.enabled == 'true'
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'corretto'
cache: 'maven'
# 只构建变更的模块及其依赖
- name: Build & Test
if: matrix.enabled == 'true'
run: |
mvn clean verify -B \
-pl ${{ matrix.module }} \
-am \
-DskipITs
- name: Build Docker Image
if: matrix.enabled == 'true'
run: |
docker build -t ghcr.io/${{ github.repository }}/${{ matrix.service }}:${{ github.sha }} \
-f ${{ matrix.module }}/Dockerfile .
- name: Push Image
if: matrix.enabled == 'true' && github.event_name == 'push'
run: |
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker push ghcr.io/${{ github.repository }}/${{ matrix.service }}:${{ github.sha }}-pl 和 -am 参数:-pl 指定要构建的模块,-am(also-make)会自动构建该模块依赖的其他模块。这样即使公共库 libs/common 有变更,也能正确构建依赖它的服务。4.4 Reusable Workflows(可复用工作流)
当多个项目的 CI/CD 流水线结构相似时,可以把公共逻辑抽成 reusable workflow,避免重复维护:
# .github/workflows/reusable-ci.yml
# 可复用的 CI 工作流(放在组织级仓库或当前仓库均可)
name: Reusable CI
on:
workflow_call:
inputs:
java-version:
type: string
default: '17'
description: 'JDK 版本'
module-path:
type: string
default: '.'
description: 'Maven 模块路径'
secrets:
SONAR_TOKEN:
required: true
SONAR_HOST_URL:
required: true
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: ${{ inputs.java-version }}
distribution: 'corretto'
cache: 'maven'
- name: Build & Test
run: mvn clean verify -B -pl ${{ inputs.module-path }} -am
- name: SonarQube Scan
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
run: mvn sonar:sonar -Dsonar.projectKey=${{ github.repository }}
- uses: actions/upload-artifact@v4
with:
name: test-results
path: target/surefire-reports/
retention-days: 7在项目中调用 reusable workflow:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
ci:
uses: ./.github/workflows/reusable-ci.yml # 本仓库内引用
# 或引用组织级仓库:uses: my-org/.github/.github/workflows/reusable-ci.yml@main
with:
java-version: '17'
module-path: '.'
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}my-org/.github 仓库的 .github/workflows/ 目录下,组织内所有仓库都可以引用,统一 CI 标准。这是管理多仓库 CI 的成熟实践。4.5 Matrix Strategy(多版本/多平台并行构建)
当需要同时在多个 JDK 版本或多个平台上验证构建时,matrix strategy 非常有用:
# .github/workflows/matrix-ci.yml
name: Matrix CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
java: ['17', '21']
# 排除不合理的组合(可选)
exclude:
- os: macos-latest
java: '17'
fail-fast: false # 某个组合失败不影响其他组合
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: 'corretto'
cache: 'maven'
- name: Build & Test
run: mvn clean verify -B五、GitHub 环境与安全管理
5.1 GitHub Environments 配置
GitHub Environments 提供了环境级别的保护规则和 Secret 管理,是实现分环境部署审批的核心机制。
配置路径:仓库 → Settings → Environments
Staging 环境配置:
- Protection rules:
- ✅ Required reviewers:添加 1-2 个审批人(如 Tech Lead)
- ⏱️ Wait timer:0 分钟(审批后马上部署)
- 🌿 Deployment branches:仅
main和develop
Production 环境配置:
- Protection rules:
- ✅ Required reviewers:添加 2-3 个审批人(如架构师 + 运维负责人)
- ⏱️ Wait timer:5 分钟(审批后等待 5 分钟再部署,留出反应时间)
- 🌿 Deployment branches:仅
main和release/*
Environment Secrets(按环境隔离敏感信息):
| Secret 名称 | Staging | Production | 说明 |
|---|---|---|---|
STAGING_KUBECONFIG | ✅ | ❌ | staging 集群 kubeconfig |
PROD_KUBECONFIG | ❌ | ✅ | 生产集群 kubeconfig |
DB_PASSWORD | staging 密码 | 生产密码 | 数据库密码 |
SONAR_TOKEN | ✅ | ❌ | SonarQube token |
5.2 Dependabot 与安全告警
GitHub 内置了 Dependabot 和 Code Scanning,可以自动检测依赖漏洞和代码安全问题:
# .github/dependabot.yml
# Dependabot 配置:自动检测依赖更新和安全漏洞
version: 2
updates:
# Maven 依赖更新
- package-ecosystem: "maven"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "Asia/Shanghai"
# 自动创建 PR 的目标分支
target-branch: "develop"
# 限制同时打开的 PR 数量
open-pull-requests-limit: 10
labels:
- "dependencies"
- "automated"
# 按依赖分组,减少 PR 数量
groups:
spring:
patterns:
- "org.springframework*"
testing:
patterns:
- "org.junit*"
- "org.mockito*"
# GitHub Actions 版本更新
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- "ci"
- "automated"
# Docker 基础镜像更新
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
labels:
- "docker"
- "automated"配合 GitHub Security Alerts 使用:
- Dependabot Alerts:自动检测已知漏洞,创建安全告警
- Dependabot Security Updates:自动创建修复 PR(升级到安全版本)
- Code Scanning(CodeQL):静态分析代码安全问题
# .github/workflows/codeql.yml
# CodeQL 代码安全扫描(GitHub 内置)
name: CodeQL Analysis
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
# 每周一凌晨 2 点定时扫描
- cron: '0 18 * * 1'
jobs:
analyze:
runs-on: ubuntu-latest
permissions:
security-events: write
strategy:
matrix:
language: ['java']
steps:
- uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: +security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{ matrix.language }}"六、制品管理
6.1 制品库选择
| 类型 | 工具 | 用途 |
|---|---|---|
| Docker 镜像 | Harbor / Nexus / ACR / GHCR | 容器镜像 |
| Maven 制品 | Nexus / Artifactory | Java 依赖包 |
| NPM 制品 | Verdaccio / Nexus | 前端依赖包 |
| Helm Chart | ChartMuseum / Harbor | K8s 部署模板 |
6.2 镜像版本策略
推荐的 tag 规则:
# 语义化版本
myapp:v1.2.3
# Git SHA(推荐,可追溯)
myapp:abc1234
# 环境标记
myapp:staging-abc1234
myapp:prod-v1.2.3latest 作为生产镜像 tag! latest 不可追溯,回滚时无法确定具体版本。始终使用 Git SHA 或语义化版本号。6.3 制品不可变原则
一旦推送到制品库的镜像,永远不要覆盖同名 tag(除了 latest)。原因:
- 生产环境的镜像必须跟测试通过的镜像完全一致
- 回滚时能精确找到对应的镜像
- 审计时能追溯到具体版本
七、环境管理
7.1 环境配置分离
配置和代码分离,不同环境使用不同配置:
config/
├── application.yml # 公共配置
├── application-dev.yml # 开发环境
├── application-test.yml # 测试环境
├── application-staging.yml # 预发布
└── application-prod.yml # 生产环境配置中心方案(以 Nacos 为例):
# bootstrap.yml
spring:
application:
name: myapp
cloud:
nacos:
config:
server-addr: nacos.example.com:8848
namespace: ${ENV:dev} # 按环境隔离 namespace
group: DEFAULT_GROUP7.2 环境变量注入
敏感信息(密码、AK/SK)不进代码仓库,通过环境变量注入:
# K8s Deployment
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-secrets
key: db-password
- name: AK
valueFrom:
secretKeyRef:
name: myapp-secrets
key: access-key八、灰度发布
8.1 金丝雀发布(Canary)
更常用的灰度策略,逐步将流量从旧版本切到新版本:
灰度比例和观察时间由服务风险、流量规模和指标收敛速度决定。每一阶段都要配置继续、停止和回滚条件,不能把示例比例直接当成生产参数。
K8s 实现方式(Istio):
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: myapp
spec:
hosts:
- myapp.example.com
http:
- route:
- destination:
host: myapp
subset: stable
weight: 90
- destination:
host: myapp
subset: canary
weight: 108.2 蓝绿部署(Blue-Green)
两套完整环境,切换流量:
蓝绿部署同时维护当前版本与候选版本。候选版本完成健康检查和业务验证后切换入口;回滚时恢复入口指向。数据库变更仍需保持新旧应用兼容,流量切回不能恢复已经破坏的数据结构。
优点:切换快、回滚秒级 缺点:需要双倍资源
8.3 A/B 测试
按用户特征(地域、设备、用户标签)分流:
# Istio 按 header 分流
http:
- match:
- headers:
x-user-group:
exact: "beta"
route:
- destination:
host: myapp
subset: canary
- route:
- destination:
host: myapp
subset: stable8.4 灰度期间监控指标
灰度期间必须关注的核心指标:
| 指标 | 告警阈值 | 说明 |
|---|---|---|
| 错误率(5xx) | > 1% | 马上回滚 |
| P99 延迟 | > 500ms | 观察 / 回滚 |
| CPU / 内存 | > 85% | 观察 |
| 业务成功率 | < 95% | 马上回滚 |
| 日志异常 | 出现 ERROR 堆栈 | 排查 |
九、回滚机制
9.1 回滚原则
- 回滚优先于修复:生产出问题,第一反应是回滚,不是现场改代码
- 回滚必须自动化:回滚操作不能依赖人工记忆
- 回滚范围要明确:是只回滚代码,还是连数据库也回滚
9.2 K8s 回滚
# 查看历史版本
kubectl rollout history deployment/myapp -n production
# 回滚到上一个版本
kubectl rollout undo deployment/myapp -n production
# 回滚到指定版本
kubectl rollout undo deployment/myapp --to-revision=3 -n production
# 查看回滚状态
kubectl rollout status deployment/myapp -n production9.3 数据库回滚
数据库变更(DDL/DML)的回滚比代码复杂得多:
| 变更类型 | 回滚方案 | 风险 |
|---|---|---|
| 新增列 | ALTER TABLE DROP COLUMN | 低(如果有数据则高) |
| 修改列类型 | 备份表 → 改回原类型 | 中 |
| 新增表 | DROP TABLE | 低 |
| 数据迁移 | 反向迁移脚本 | 高 |
| 新增索引 | DROP INDEX | 低 |
down 方法。线上跑 migrate 之前,先确认 rollback 能用。9.4 配置回滚
如果使用 Nacos 等配置中心:
# Nacos 配置回滚(通过 API)
curl -X POST "http://nacos.example.com:8848/nacos/v1/cs/history" \
-d "dataId=myapp.yml&group=DEFAULT_GROUP&tenant=prod&nid=12345"十、发版 Checklist
每次发版前,对照这个清单确认:
发版前
- 所有功能分支已合并到目标分支
- CI 流水线全部通过(编译、测试、扫描)
- staging 环境验证通过
- 数据库迁移脚本已准备并测试
- 回滚方案已确认(代码 + 数据库 + 配置)
- 相关人员已通知(产品、测试、运维)
- 发版时间窗口已确认(避开业务高峰)
发版中
- 按灰度策略逐步放量
- 监控指标正常(错误率、延迟、CPU/内存)
- 业务指标正常(订单量、支付成功率等)
- 日志无异常 ERROR
发版后
- 全量发布完成
- 核心功能冒烟通过
- 监控持续观察 30 分钟
- 发版记录已归档(版本号、变更内容、发版人、时间)
十一、踩坑总结
坑 1:发版窗口选在周五下午
问题:周五下午发版,周末出问题没人处理。 建议:发版窗口选在周二到周四上午,避开周五和节假日前。
坑 2:数据库变更和代码变更不同步
问题:代码里用了新字段,但数据库还没执行迁移脚本。 建议:数据库变更先于代码部署,或者用兼容性写法(先加列 → 部署代码 → 再删旧列)。
坑 3:回滚只回滚了代码
问题:代码回滚了,但数据库新字段还在,旧代码查询报错。 建议:回滚时要同步考虑数据库和配置的回滚。如果数据库不能回滚,代码要兼容新旧两种数据结构。
坑 4:灰度期间不看监控
问题:灰度部署后直接全量,出了问题才发现。 建议:灰度期间必须有人盯着核心指标,设置自动告警。
坑 5:镜像用 latest tag
问题:回滚时找不到上一个版本的镜像。 建议:始终使用 Git SHA 或语义化版本号作为镜像 tag。
坑 6:配置变更没有记录
问题:Nacos 上改了配置,不知道谁改的、什么时候改的、改了什么。 建议:配置变更走 GitOps,或者至少在 Nacos 上开启变更审计日志。
坑 7:发版步骤靠人记忆
问题:不同的人发版,步骤不一样,遗漏步骤。 建议:发版步骤文档化,更稳妥用脚本或 CI/CD 自动化。
坑 8:测试环境和生产环境不一致
问题:测试通过了,生产挂了,因为环境配置不同。 建议:staging 环境尽量跟生产一致(配置、数据量、网络拓扑)。
十二、总结与建议
发版流程选型
| 团队规模 | 推荐方案 | 工具链 |
|---|---|---|
| 小团队(< 5人) | GitHub Actions + K8s | 简单流水线 |
| 中型团队(5-20人) | GitLab CI + ArgoCD | 多环境流水线 |
| 大型团队(> 20人) | Jenkins + ArgoCD + Istio | 完整 DevOps 平台 |
核心原则
- 自动化优先:能自动化的步骤不要手动做
- 不可变制品:同一个镜像从测试到生产,不要重新构建
- 渐进式发布:灰度 → 观察 → 全量,不要一步到位
- 回滚优先:出问题第一反应是回滚,不是现场修
- 可观测性:没有监控的发版就是赌博