部署翻车现场:那些年我们踩过的坑

103 阅读12分钟

凌晨三点,生产环境炸了

周五晚上 11 点,你刚准备关电脑下班。

"应该没事吧?就改了一行代码。"

你点击了部署按钮。

三分钟后,运维群炸了:

"网站打不开了!" "数据库连接失败!" "用户疯狂投诉!" "老板在问怎么回事!"

你的手开始发抖,冷汗直冒。

这不是电影情节,这是每个开发者都可能经历的噩梦。

今天,我们就来聊聊那些年我们在部署时踩过的坑,以及如何避免它们。


错误 1:直接在生产环境改代码

灾难现场

# SSH 连接到生产服务器
ssh root@production-server

# 直接修改代码
vim /var/www/app/index.js

# 改完重启服务
pm2 restart app

# 结果:网站挂了,而且不知道改了什么 💥

为什么这样做?

  • "就改一行,很快的"
  • "测试环境太慢了"
  • "紧急修复,来不及走流程"

为什么会翻车?

  1. 没有版本控制:改了什么?谁改的?什么时候改的?全都不知道
  2. 无法回滚:出问题了想恢复?对不起,没有备份
  3. 没有测试:直接在生产环境测试,用户就是你的测试员
  4. 团队不知情:其他人不知道你改了什么,后续维护一脸懵逼

正确姿势:永远不要直接改生产环境

# ✅ 正确的流程

# 1. 在本地开发分支修改
git checkout -b hotfix/urgent-bug
# 修改代码...
git add .
git commit -m "fix: 修复紧急 bug"

# 2. 推送到远程仓库
git push origin hotfix/urgent-bug

# 3. 创建 Pull Request,代码审查
# 4. 合并到主分支
# 5. 触发 CI/CD 自动部署

# 或者使用 Git Flow
git flow hotfix start urgent-bug
# 修改代码...
git flow hotfix finish urgent-bug

黄金法则:

  • ✅ 所有代码修改都要经过版本控制
  • ✅ 所有部署都要经过 CI/CD 流程
  • ✅ 生产环境只读,不可写
  • ✅ 紧急修复也要走流程(可以简化,但不能跳过)

错误 2:没有回滚计划,出问题就慌了

灾难现场

# 部署新版本
./deploy.sh v2.0.0

# 5 分钟后发现问题
# 想回滚,但是...

# ❌ 没有保留旧版本
# ❌ 数据库已经迁移,无法回退
# ❌ 配置文件被覆盖
# ❌ 不知道怎么回滚

# 结果:网站挂了 2 小时,损失惨重

真实案例:

某电商网站在双十一当天部署新版本,结果支付功能出现 bug。因为没有回滚方案,工程师花了 3 小时才修复,损失订单数千万。

正确姿势:部署前先准备好回滚方案

# ✅ 蓝绿部署(Blue-Green Deployment)

# 1. 保留旧版本(蓝色环境)
# 2. 部署新版本到新环境(绿色环境)
# 3. 切换流量到绿色环境
# 4. 观察一段时间
# 5. 如果有问题,立即切回蓝色环境

# Nginx 配置示例
upstream backend {
    server blue.example.com:3000;   # 旧版本
    # server green.example.com:3000;  # 新版本(注释掉)
}

# 部署新版本后,只需要修改配置
upstream backend {
    # server blue.example.com:3000;   # 旧版本(注释掉)
    server green.example.com:3000;  # 新版本
}

# 重载 Nginx(不中断服务)
nginx -s reload

# 如果出问题,立即切回
upstream backend {
    server blue.example.com:3000;   # 切回旧版本
}
nginx -s reload
# ✅ Kubernetes 滚动更新 + 快速回滚
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # 最多多出 1 个 Pod
      maxUnavailable: 0  # 最多 0 个 Pod 不可用
  template:
    spec:
      containers:
      - name: myapp
        image: myapp:v2.0.0

# 部署
kubectl apply -f deployment.yaml

# 查看部署状态
kubectl rollout status deployment/myapp

# 如果出问题,一键回滚
kubectl rollout undo deployment/myapp

# 回滚到指定版本
kubectl rollout undo deployment/myapp --to-revision=2
// ✅ 使用 PM2 的部署和回滚
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'myapp',
    script: './app.js',
    instances: 4,
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production'
    }
  }],
  deploy: {
    production: {
      user: 'deploy',
      host: 'production-server',
      ref: 'origin/main',
      repo: 'git@github.com:user/repo.git',
      path: '/var/www/myapp',
      'post-deploy': 'npm install && pm2 reload ecosystem.config.js'
    }
  }
}

// 部署
pm2 deploy production

// 回滚到上一个版本
pm2 deploy production revert 1

回滚检查清单:

  • 代码可以快速回滚(保留旧版本)
  • 数据库迁移可逆(或者分两步部署)
  • 配置文件有备份
  • 静态资源有版本控制
  • 有回滚演练(定期测试回滚流程)
  • 回滚时间 < 5 分钟

错误 3:数据库迁移和代码部署一起搞

灾难现场

# ❌ 错误的做法:同时部署代码和数据库迁移

# deploy.sh
git pull
npm install
npm run db:migrate  # 数据库迁移
pm2 restart app     # 重启应用

# 结果:
# 1. 迁移过程中,旧代码访问新表结构 → 报错
# 2. 迁移失败,但代码已经部署 → 不兼容
# 3. 想回滚,但数据库已经改了 → 数据丢失风险

真实案例:

某社交平台在部署时,同时执行了"删除旧字段"的数据库迁移和"使用新字段"的代码部署。结果在部署过程中,部分服务器还在使用旧代码访问已删除的字段,导致大量报错。

正确姿势:数据库迁移分三步走

# ✅ 正确的做法:向后兼容的数据库迁移

# === 第一步:添加新字段(不删除旧字段)===
# 迁移脚本 001_add_new_column.sql
ALTER TABLE users ADD COLUMN email_verified BOOLEAN DEFAULT FALSE;

# 部署迁移
npm run db:migrate

# 此时:旧代码和新代码都能正常运行

# === 第二步:部署新代码(同时支持新旧字段)===
# 新代码
class User {
  async save() {
    // 同时写入新旧字段
    await db.query(`
      UPDATE users
      SET
        email_verified = $1,
        is_verified = $1  -- 旧字段也更新
      WHERE id = $2
    `, [this.emailVerified, this.id])
  }

  async load() {
    // 优先读取新字段,fallback 到旧字段
    const user = await db.query('SELECT * FROM users WHERE id = $1', [this.id])
    this.emailVerified = user.email_verified ?? user.is_verified
  }
}

# 部署新代码
git pull
npm install
pm2 restart app

# 此时:新旧代码都能正常运行

# === 第三步:观察一段时间后,删除旧字段 ===
# 等待 1-2 周,确认没问题后
# 迁移脚本 002_remove_old_column.sql
ALTER TABLE users DROP COLUMN is_verified;

# 部署迁移
npm run db:migrate

数据库迁移黄金法则:

  1. 向后兼容:新迁移不能破坏旧代码
  2. 分步进行:添加 → 部署代码 → 删除
  3. 可回滚:每个迁移都要有回滚脚本
  4. 小步快跑:一次迁移只做一件事
// ✅ 使用迁移工具(Knex.js 示例)
// migrations/20250117_add_email_verified.js
exports.up = function(knex) {
  return knex.schema.table('users', function(table) {
    table.boolean('email_verified').defaultTo(false)
  })
}

exports.down = function(knex) {
  return knex.schema.table('users', function(table) {
    table.dropColumn('email_verified')
  })
}

// 执行迁移
knex migrate:latest

// 回滚迁移
knex migrate:rollback

错误 4:环境变量管理混乱

灾难现场

// ❌ 硬编码配置
const config = {
  database: {
    host: 'localhost',
    port: 5432,
    username: 'admin',
    password: 'admin123'  // 密码写死在代码里!
  },
  apiKey: 'sk-1234567890abcdef',  // API 密钥也写死!
  debug: true  // 生产环境还开着 debug!
}

// 提交到 Git
git add config.js
git commit -m "add config"
git push

// 结果:
// 1. 密码泄露到 GitHub
// 2. 所有环境用同一个配置
// 3. 改配置要重新部署代码

正确姿势:使用环境变量和密钥管理

# ✅ 使用 .env 文件(不提交到 Git)

# .env.example(提交到 Git,作为模板)
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=your_username
DATABASE_PASSWORD=your_password
API_KEY=your_api_key
NODE_ENV=development

# .env(不提交到 Git,包含真实密钥)
DATABASE_HOST=prod-db.example.com
DATABASE_PORT=5432
DATABASE_USER=prod_user
DATABASE_PASSWORD=super_secret_password_123
API_KEY=sk-real-api-key-here
NODE_ENV=production

# .gitignore
.env
.env.local
.env.*.local
// ✅ 代码中使用环境变量
require("dotenv").config()

const config = {
  database: {
    host: process.env.DATABASE_HOST,
    port: parseInt(process.env.DATABASE_PORT),
    username: process.env.DATABASE_USER,
    password: process.env.DATABASE_PASSWORD,
  },
  apiKey: process.env.API_KEY,
  debug: process.env.NODE_ENV !== "production",
}

// 验证必需的环境变量
const requiredEnvVars = [
  "DATABASE_HOST",
  "DATABASE_USER",
  "DATABASE_PASSWORD",
  "API_KEY",
]

for (const envVar of requiredEnvVars) {
  if (!process.env[envVar]) {
    throw new Error(`Missing required environment variable: ${envVar}`)
  }
}
# ✅ Docker Compose 管理环境变量
version: "3.8"
services:
  app:
    image: myapp:latest
    environment:
      - NODE_ENV=production
      - DATABASE_HOST=${DATABASE_HOST}
      - DATABASE_PASSWORD=${DATABASE_PASSWORD}
    env_file:
      - .env.production
    secrets:
      - db_password
      - api_key

secrets:
  db_password:
    external: true
  api_key:
    external: true
# ✅ Kubernetes Secrets
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
data:
  database-password: c3VwZXJfc2VjcmV0X3Bhc3N3b3Jk # base64 编码
  api-key: c2stcmVhbC1hcGkta2V5LWhlcmU=

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    spec:
      containers:
        - name: myapp
          image: myapp:latest
          env:
            - name: DATABASE_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: database-password
            - name: API_KEY
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: api-key

环境变量管理最佳实践:

  1. 永远不要提交密钥到 Git
  2. 使用密钥管理服务(AWS Secrets Manager、HashiCorp Vault)
  3. 不同环境使用不同的密钥
  4. 定期轮换密钥
  5. 最小权限原则(每个服务只能访问它需要的密钥)

错误 5:没有健康检查,部署完就不管了

灾难现场

# ❌ 简单粗暴的部署脚本
#!/bin/bash
git pull
npm install
pm2 restart app

echo "部署完成!"
# 然后就不管了...

# 实际情况:
# - 应用启动失败了,但 pm2 显示 "online"
# - 数据库连接不上,但没人知道
# - 内存泄漏,1 小时后服务挂了
# - 用户疯狂投诉,但监控没报警

正确姿势:完善的健康检查和监控

// ✅ 实现健康检查端点
const express = require("express")
const app = express()

// 简单的健康检查
app.get("/health", (req, res) => {
  res.status(200).json({ status: "ok" })
})

// 详细的健康检查
app.get("/health/detailed", async (req, res) => {
  const health = {
    status: "ok",
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    checks: {},
  }

  // 检查数据库连接
  try {
    await db.query("SELECT 1")
    health.checks.database = { status: "ok" }
  } catch (error) {
    health.checks.database = {
      status: "error",
      message: error.message,
    }
    health.status = "error"
  }

  // 检查 Redis 连接
  try {
    await redis.ping()
    health.checks.redis = { status: "ok" }
  } catch (error) {
    health.checks.redis = {
      status: "error",
      message: error.message,
    }
    health.status = "error"
  }

  // 检查磁盘空间
  const diskUsage = await checkDiskUsage()
  if (diskUsage > 90) {
    health.checks.disk = {
      status: "warning",
      usage: `${diskUsage}%`,
    }
    health.status = "warning"
  } else {
    health.checks.disk = {
      status: "ok",
      usage: `${diskUsage}%`,
    }
  }

  // 检查内存使用
  const memUsage = process.memoryUsage()
  health.checks.memory = {
    status: "ok",
    heapUsed: `${(memUsage.heapUsed / 1024 / 1024).toFixed(2)} MB`,
    heapTotal: `${(memUsage.heapTotal / 1024 / 1024).toFixed(2)} MB`,
  }

  const statusCode = health.status === "ok" ? 200 : 503
  res.status(statusCode).json(health)
})

// 就绪检查(Readiness Probe)
app.get("/ready", async (req, res) => {
  // 检查应用是否准备好接收流量
  const isReady = await checkIfReady()
  if (isReady) {
    res.status(200).json({ ready: true })
  } else {
    res.status(503).json({ ready: false })
  }
})

// 存活检查(Liveness Probe)
app.get("/alive", (req, res) => {
  // 简单检查进程是否还活着
  res.status(200).json({ alive: true })
})
# ✅ Kubernetes 健康检查配置
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    spec:
      containers:
        - name: myapp
          image: myapp:latest
          ports:
            - containerPort: 3000

          # 存活探针:检查容器是否还活着
          livenessProbe:
            httpGet:
              path: /alive
              port: 3000
            initialDelaySeconds: 30 # 启动后 30 秒开始检查
            periodSeconds: 10 # 每 10 秒检查一次
            timeoutSeconds: 5 # 超时时间 5 秒
            failureThreshold: 3 # 失败 3 次后重启容器

          # 就绪探针:检查容器是否准备好接收流量
          readinessProbe:
            httpGet:
              path: /ready
              port: 3000
            initialDelaySeconds: 10
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3

          # 启动探针:检查容器是否启动成功
          startupProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 0
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 30 # 最多等待 150 秒(30 * 5)
# ✅ 部署后自动验证脚本
#!/bin/bash

echo "开始部署..."

# 1. 部署新版本
kubectl apply -f deployment.yaml

# 2. 等待部署完成
echo "等待部署完成..."
kubectl rollout status deployment/myapp --timeout=5m

# 3. 检查健康状态
echo "检查健康状态..."
for i in {1..30}; do
  HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://myapp.example.com/health)

  if [ "$HTTP_CODE" == "200" ]; then
    echo "✅ 健康检查通过"
    break
  else
    echo "⏳ 等待服务就绪... ($i/30)"
    sleep 10
  fi

  if [ $i -eq 30 ]; then
    echo "❌ 健康检查失败,开始回滚"
    kubectl rollout undo deployment/myapp
    exit 1
  fi
done

# 4. 冒烟测试
echo "执行冒烟测试..."
SMOKE_TEST_RESULT=$(curl -s http://myapp.example.com/api/test)

if [[ $SMOKE_TEST_RESULT == *"success"* ]]; then
  echo "✅ 冒烟测试通过"
else
  echo "❌ 冒烟测试失败,开始回滚"
  kubectl rollout undo deployment/myapp
  exit 1
fi

# 5. 监控关键指标
echo "监控关键指标 5 分钟..."
sleep 300

ERROR_RATE=$(curl -s "http://prometheus.example.com/api/v1/query?query=rate(http_requests_total{status=~'5..'}[5m])")

if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then
  echo "❌ 错误率过高,开始回滚"
  kubectl rollout undo deployment/myapp
  exit 1
fi

echo "🎉 部署成功!"

错误 6:周五下午部署,周末背锅

灾难现场

周五下午 5:30
开发:就一个小改动,部署一下吧
运维:行吧,反正马上下班了

周五晚上 8:00
用户:网站怎么打不开了?

周五晚上 8:30
老板:@全体成员 紧急会议

周六凌晨 3:00
团队:终于修好了...

周一早上
老板:以后周五不准部署!

真实数据:

  • 70% 的生产事故发生在周五下午到周日
  • 周五部署的回滚率是平时的 3 倍
  • 周末修复问题的平均时间是工作日的 2 倍

正确姿势:制定部署时间窗口

# ✅ 部署时间规则

允许部署的时间:
- 周一到周四:10:00 - 16:00
- 周五:10:00 - 14:00(留出时间观察)
- 周末和节假日:禁止部署(除非紧急情况)

禁止部署的时间:
- 业务高峰期(如电商的晚上 8-10 点)
- 重大活动前后(如双十一、春节)
- 下班前 2 小时
- 周五下午

紧急部署流程:
- 需要技术负责人批准
- 需要完整的回滚方案
- 需要相关人员待命
- 需要详细的风险评估
// ✅ 自动检查部署时间
// deploy-time-check.js
const moment = require("moment")

function canDeploy() {
  const now = moment()
  const hour = now.hour()
  const day = now.day() // 0 = 周日, 6 = 周六

  // 周末禁止部署
  if (day === 0 || day === 6) {
    return {
      allowed: false,
      reason: "周末禁止部署",
    }
  }

  // 周五下午 2 点后禁止部署
  if (day === 5 && hour >= 14) {
    return {
      allowed: false,
      reason: "周五下午 2 点后禁止部署",
    }
  }

  // 工作日只允许 10:00 - 16:00 部署
  if (hour < 10 || hour >= 16) {
    return {
      allowed: false,
      reason: "只允许在 10:00 - 16:00 部署",
    }
  }

  // 检查是否在业务高峰期
  if (hour >= 20 && hour <= 22) {
    return {
      allowed: false,
      reason: "业务高峰期禁止部署",
    }
  }

  return {
    allowed: true,
    reason: "可以部署",
  }
}

// 在 CI/CD 流程中使用
const deployCheck = canDeploy()
if (!deployCheck.allowed) {
  console.error(`❌ 部署被阻止: ${deployCheck.reason}`)
  process.exit(1)
}

console.log(`✅ ${deployCheck.reason}`)
# ✅ GitHub Actions 部署时间限制
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  check-deploy-time:
    runs-on: ubuntu-latest
    steps:
      - name: Check if deployment is allowed
        run: |
          HOUR=$(date +%H)
          DAY=$(date +%u)  # 1-7 (周一到周日)

          # 周末禁止部署
          if [ $DAY -eq 6 ] || [ $DAY -eq 7 ]; then
            echo "❌ 周末禁止部署"
            exit 1
          fi

          # 周五下午 2 点后禁止部署
          if [ $DAY -eq 5 ] && [ $HOUR -ge 14 ]; then
            echo "❌ 周五下午 2 点后禁止部署"
            exit 1
          fi

          # 只允许 10:00 - 16:00 部署
          if [ $HOUR -lt 10 ] || [ $HOUR -ge 16 ]; then
            echo "❌ 只允许在 10:00 - 16:00 部署"
            exit 1
          fi

          echo "✅ 可以部署"

  deploy:
    needs: check-deploy-time
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        run: |
          # 部署逻辑...

错误 7:没有灰度发布,一上线就全量

灾难现场

# ❌ 直接全量部署
kubectl set image deployment/myapp myapp=myapp:v2.0.0

# 结果:
# - 新版本有 bug,影响所有用户
# - 发现问题时已经来不及了
# - 回滚需要时间,损失已经造成

真实案例:

某视频网站直接全量部署新版本,结果新版本的视频播放器在某些浏览器上无法正常工作。等发现问题时,已经有数百万用户受影响,投诉电话打爆了客服。

正确姿势:灰度发布(金丝雀部署)

# ✅ Kubernetes 金丝雀部署

# 1. 保留旧版本(90% 流量)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-stable
spec:
  replicas: 9 # 90% 的 Pod
  template:
    metadata:
      labels:
        app: myapp
        version: v1.0.0
    spec:
      containers:
        - name: myapp
          image: myapp:v1.0.0

---
# 2. 部署新版本(10% 流量)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-canary
spec:
  replicas: 1 # 10% 的 Pod
  template:
    metadata:
      labels:
        app: myapp
        version: v2.0.0
    spec:
      containers:
        - name: myapp
          image: myapp:v2.0.0

---
# 3. Service 同时指向两个版本
apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector:
    app: myapp # 不区分版本,流量自动分配
  ports:
    - port: 80
      targetPort: 3000
// ✅ 使用 Nginx 实现灰度发布
// nginx.conf
upstream backend_stable {
    server stable-1.example.com:3000;
    server stable-2.example.com:3000;
    server stable-3.example.com:3000;
}

upstream backend_canary {
    server canary-1.example.com:3000;
}

server {
    listen 80;

    location / {
        # 10% 流量到金丝雀版本
        if ($request_id ~* "^[0-9a-f]$") {
            proxy_pass http://backend_canary;
            break;
        }

        # 90% 流量到稳定版本
        proxy_pass http://backend_stable;
    }
}
// ✅ 基于用户的灰度发布
// feature-flag.js
class FeatureFlag {
  constructor() {
    this.canaryUsers = new Set([
      "user123", // 内部测试用户
      "user456",
      // ...
    ])
    this.canaryPercentage = 10 // 10% 的用户
  }

  shouldUseCanary(userId) {
    // 1. 白名单用户始终使用金丝雀版本
    if (this.canaryUsers.has(userId)) {
      return true
    }

    // 2. 根据用户 ID 哈希决定
    const hash = this.hashUserId(userId)
    return hash % 100 < this.canaryPercentage
  }

  hashUserId(userId) {
    let hash = 0
    for (let i = 0; i < userId.length; i++) {
      hash = (hash << 5) - hash + userId.charCodeAt(i)
      hash = hash & hash
    }
    return Math.abs(hash)
  }
}

// 使用
const featureFlag = new FeatureFlag()

app.get("/api/data", async (req, res) => {
  const userId = req.user.id

  if (featureFlag.shouldUseCanary(userId)) {
    // 使用新版本逻辑
    return res.json(await getDataV2())
  } else {
    // 使用旧版本逻辑
    return res.json(await getDataV1())
  }
})

灰度发布流程:

1. 部署金丝雀版本(5% 流量)
   ↓
2. 观察 30 分钟
   - 错误率正常?
   - 响应时间正常?
   - 用户投诉?
   ↓
3. 如果正常,扩大到 25% 流量
   ↓
4. 观察 1 小时
   ↓
5. 如果正常,扩大到 50% 流量
   ↓
6. 观察 2 小时
   ↓
7. 如果正常,全量发布
   ↓
8. 观察 24 小时后,下线旧版本

部署最佳实践总结

1. 自动化一切

# ✅ 完整的 CI/CD 流程
name: CI/CD Pipeline

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Run tests
        run: npm test
      - name: Run linter
        run: npm run lint
      - name: Security scan
        run: npm audit

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .
      - name: Push to registry
        run: docker push myapp:${{ github.sha }}

  deploy-canary:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Deploy canary (10%)
        run: |
          kubectl set image deployment/myapp-canary \
            myapp=myapp:${{ github.sha }}

      - name: Wait and monitor
        run: |
          sleep 300  # 等待 5 分钟
          ./scripts/check-metrics.sh

      - name: Rollback if needed
        if: failure()
        run: kubectl rollout undo deployment/myapp-canary

  deploy-production:
    needs: deploy-canary
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to production
        run: |
          kubectl set image deployment/myapp \
            myapp=myapp:${{ github.sha }}

      - name: Monitor deployment
        run: |
          kubectl rollout status deployment/myapp
          ./scripts/smoke-test.sh

2. 监控和告警

// ✅ 部署后自动监控关键指标
const metrics = {
  // 错误率
  errorRate: {
    threshold: 0.01, // 1%
    query: 'rate(http_requests_total{status=~"5.."}[5m])',
  },

  // 响应时间
  responseTime: {
    threshold: 1000, // 1 秒
    query: "histogram_quantile(0.95, http_request_duration_seconds)",
  },

  // CPU 使用率
  cpuUsage: {
    threshold: 80, // 80%
    query: "rate(process_cpu_seconds_total[5m]) * 100",
  },

  // 内存使用率
  memoryUsage: {
    threshold: 85, // 85%
    query: "(process_resident_memory_bytes / node_memory_MemTotal_bytes) * 100",
  },
}

async function monitorDeployment(duration = 300000) {
  const startTime = Date.now()

  while (Date.now() - startTime < duration) {
    for (const [name, metric] of Object.entries(metrics)) {
      const value = await queryPrometheus(metric.query)

      if (value > metric.threshold) {
        console.error(`❌ ${name} 超过阈值: ${value} > ${metric.threshold}`)
        await rollback()
        return false
      }
    }

    await sleep(30000) // 每 30 秒检查一次
  }

  console.log("✅ 监控通过")
  return true
}

3. 部署检查清单

## 部署前检查

- [ ] 代码已经过 Code Review
- [ ] 所有测试都通过(单元测试、集成测试、E2E 测试)
- [ ] 已在测试环境验证
- [ ] 数据库迁移脚本已准备好(且可回滚)
- [ ] 配置文件已更新
- [ ] 依赖项已更新
- [ ] 文档已更新
- [ ] 回滚方案已准备好
- [ ] 相关人员已通知
- [ ] 在允许的部署时间窗口内

## 部署中检查

- [ ] 健康检查通过
- [ ] 日志没有异常
- [ ] 关键指标正常(错误率、响应时间、CPU、内存)
- [ ] 数据库连接正常
- [ ] 外部服务连接正常
- [ ] 缓存正常工作

## 部署后检查

- [ ] 冒烟测试通过
- [ ] 关键业务流程正常(登录、支付、下单等)
- [ ] 监控指标正常
- [ ] 没有用户投诉
- [ ] 日志没有异常
- [ ] 观察至少 30 分钟

写在最后:部署不是结束,而是开始

很多人觉得代码写完、部署上线就完事了。

但实际上,部署只是开始。

一个成熟的部署流程应该包括:

  1. 自动化测试:确保代码质量
  2. 自动化部署:减少人为错误
  3. 灰度发布:降低风险
  4. 健康检查:及时发现问题
  5. 监控告警:快速响应
  6. 快速回滚:止损及时
  7. 事后复盘:持续改进

记住这些黄金法则:

  • ✅ 永远不要直接在生产环境改代码
  • ✅ 永远要有回滚方案
  • ✅ 数据库迁移要向后兼容
  • ✅ 环境变量不要提交到 Git
  • ✅ 部署后要监控关键指标
  • ✅ 周五下午不要部署
  • ✅ 先灰度,再全量

2026 年了,别再让部署成为噩梦。

下次部署前,先问自己:

  • 如果出问题,我能在 5 分钟内回滚吗?
  • 我有监控和告警吗?
  • 我在合适的时间部署吗?
  • 我有灰度发布吗?

如果答案都是"是",那就放心部署吧。

如果有任何一个是"否",那就先完善流程,再部署。

记住:部署不是赌博,而是工程。


彩蛋:一键部署脚本模板

#!/bin/bash
# deploy.sh - 生产级部署脚本

set -e  # 遇到错误立即退出

# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# 配置
APP_NAME="myapp"
ENVIRONMENT="production"
HEALTH_CHECK_URL="https://myapp.example.com/health"
ROLLBACK_ON_FAILURE=true

# 日志函数
log_info() {
    echo -e "${GREEN}[INFO]${NC} $1"
}

log_warn() {
    echo -e "${YELLOW}[WARN]${NC} $1"
}

log_error() {
    echo -e "${RED}[ERROR]${NC} $1"
}

# 检查部署时间
check_deploy_time() {
    HOUR=$(date +%H)
    DAY=$(date +%u)

    if [ $DAY -eq 6 ] || [ $DAY -eq 7 ]; then
        log_error "周末禁止部署"
        exit 1
    fi

    if [ $DAY -eq 5 ] && [ $HOUR -ge 14 ]; then
        log_error "周五下午 2 点后禁止部署"
        exit 1
    fi

    if [ $HOUR -lt 10 ] || [ $HOUR -ge 16 ]; then
        log_error "只允许在 10:00 - 16:00 部署"
        exit 1
    fi

    log_info "部署时间检查通过"
}

# 健康检查
health_check() {
    log_info "执行健康检查..."

    for i in {1..30}; do
        HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" $HEALTH_CHECK_URL)

        if [ "$HTTP_CODE" == "200" ]; then
            log_info "健康检查通过"
            return 0
        fi

        log_warn "等待服务就绪... ($i/30)"
        sleep 10
    done

    log_error "健康检查失败"
    return 1
}

# 回滚
rollback() {
    log_error "部署失败,开始回滚..."
    kubectl rollout undo deployment/$APP_NAME
    log_info "回滚完成"
}

# 主流程
main() {
    log_info "开始部署 $APP_NAME$ENVIRONMENT"

    # 1. 检查部署时间
    check_deploy_time

    # 2. 拉取最新代码
    log_info "拉取最新代码..."
    git pull origin main

    # 3. 运行测试
    log_info "运行测试..."
    npm test || {
        log_error "测试失败"
        exit 1
    }

    # 4. 构建镜像
    log_info "构建 Docker 镜像..."
    VERSION=$(git rev-parse --short HEAD)
    docker build -t $APP_NAME:$VERSION .

    # 5. 推送镜像
    log_info "推送镜像到仓库..."
    docker push $APP_NAME:$VERSION

    # 6. 部署
    log_info "部署到 Kubernetes..."
    kubectl set image deployment/$APP_NAME $APP_NAME=$APP_NAME:$VERSION

    # 7. 等待部署完成
    log_info "等待部署完成..."
    kubectl rollout status deployment/$APP_NAME --timeout=5m || {
        if [ "$ROLLBACK_ON_FAILURE" = true ]; then
            rollback
        fi
        exit 1
    }

    # 8. 健康检查
    health_check || {
        if [ "$ROLLBACK_ON_FAILURE" = true ]; then
            rollback
        fi
        exit 1
    }

    # 9. 冒烟测试
    log_info "执行冒烟测试..."
    npm run smoke-test || {
        if [ "$ROLLBACK_ON_FAILURE" = true ]; then
            rollback
        fi
        exit 1
    }

    # 10. 监控
    log_info "监控关键指标 5 分钟..."
    sleep 300

    log_info "🎉 部署成功!"
    log_info "版本: $VERSION"
    log_info "时间: $(date)"
}

# 执行
main

现在,去建立一个让人放心的部署流程吧!🚀