凌晨三点,生产环境炸了
周五晚上 11 点,你刚准备关电脑下班。
"应该没事吧?就改了一行代码。"
你点击了部署按钮。
三分钟后,运维群炸了:
"网站打不开了!" "数据库连接失败!" "用户疯狂投诉!" "老板在问怎么回事!"
你的手开始发抖,冷汗直冒。
这不是电影情节,这是每个开发者都可能经历的噩梦。
今天,我们就来聊聊那些年我们在部署时踩过的坑,以及如何避免它们。
错误 1:直接在生产环境改代码
灾难现场
# SSH 连接到生产服务器
ssh root@production-server
# 直接修改代码
vim /var/www/app/index.js
# 改完重启服务
pm2 restart app
# 结果:网站挂了,而且不知道改了什么 💥
为什么这样做?
- "就改一行,很快的"
- "测试环境太慢了"
- "紧急修复,来不及走流程"
为什么会翻车?
- 没有版本控制:改了什么?谁改的?什么时候改的?全都不知道
- 无法回滚:出问题了想恢复?对不起,没有备份
- 没有测试:直接在生产环境测试,用户就是你的测试员
- 团队不知情:其他人不知道你改了什么,后续维护一脸懵逼
正确姿势:永远不要直接改生产环境
# ✅ 正确的流程
# 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
数据库迁移黄金法则:
- 向后兼容:新迁移不能破坏旧代码
- 分步进行:添加 → 部署代码 → 删除
- 可回滚:每个迁移都要有回滚脚本
- 小步快跑:一次迁移只做一件事
// ✅ 使用迁移工具(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
环境变量管理最佳实践:
- 永远不要提交密钥到 Git
- 使用密钥管理服务(AWS Secrets Manager、HashiCorp Vault)
- 不同环境使用不同的密钥
- 定期轮换密钥
- 最小权限原则(每个服务只能访问它需要的密钥)
错误 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 分钟
写在最后:部署不是结束,而是开始
很多人觉得代码写完、部署上线就完事了。
但实际上,部署只是开始。
一个成熟的部署流程应该包括:
- 自动化测试:确保代码质量
- 自动化部署:减少人为错误
- 灰度发布:降低风险
- 健康检查:及时发现问题
- 监控告警:快速响应
- 快速回滚:止损及时
- 事后复盘:持续改进
记住这些黄金法则:
- ✅ 永远不要直接在生产环境改代码
- ✅ 永远要有回滚方案
- ✅ 数据库迁移要向后兼容
- ✅ 环境变量不要提交到 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
现在,去建立一个让人放心的部署流程吧!🚀