共计 3210 个字符,预计需要花费 9 分钟才能阅读完成。
文章目录 [显示]
每次上线都像拆炸弹?改完代码心里没底,点下部署按钮手都在抖?别慌,今天我们就来聊聊怎么让生产部署变得像换灯泡一样简单——关掉旧的,拧上新的,灯还不能灭。
本文你将学到
- 蓝绿部署的核心原理和实操方法
- 金丝雀发布如何让风险降到最低
- Docker 环境下实现零停机更新的完整方案
- 出问题时秒级回滚的保命技巧
阅读时间 : 约 12 分钟
实操时间 : 约 40 分钟
难度等级 : 中高级(需要 Day 1-26 的基础)
为什么要聊部署策略
先说个真实场景:你辛辛苦苦写了一周的新功能,测试环境跑得好好的,信心满满地部署到生产环境。结果——白屏了。用户炸锅了,老板电话打过来了,你手忙脚乱地回滚,发现回滚脚本上次改完忘记测了。
这种场景,但凡做过几年开发的人都经历过。问题的根源不是代码写得差,而是部署策略太粗暴。就像搬家,你不能把所有东西一股脑扔到新房子里,得有计划地搬,搬错了还能搬回来。
蓝绿部署:两套房子轮着住
原理
蓝绿部署的思路特别简单:准备两套完全一样的环境,一套跑老版本(蓝),一套部署新版本(绿)。新版本验证没问题后,把流量从蓝切到绿。出问题?秒切回蓝。
用户请求
|
v
+----------+
| Nginx |
| 负载均衡 |
+----------+
/ \
当前流量 待切换
/ \
+---------+ +---------+
| 蓝环境 | | 绿环境 |
| (v1.0) | | (v1.1) |
| 运行中 | | 已就绪 |
+---------+ +---------+
| |
+---------+ +---------+
| 数据库 | | 数据库 |
| (共享) | | (共享) |
+---------+ +---------+
这就像你有两套房子,一套在住,另一套装修好了。搬过去住两天,发现马桶漏水?直接搬回老房子,一点不耽误。
实操:Docker Compose 实现蓝绿部署
先准备目录结构:
mkdir -p ~/docker-blue-green && cd ~/docker-blue-green
创建一个简单的应用来演示。先写 app/server.js:
mkdir -p app
cat > app/server.js << 'EOF'
const http = require('http');
const version = process.env.APP_VERSION || 'unknown';
const color = process.env.APP_COLOR || 'unknown';
const server = http.createServer((req, res) => {if (req.url === '/health') {res.writeHead(200, { 'Content-Type': 'application/json'});
res.end(JSON.stringify({ status: 'healthy', version, color}));
return;
}
res.writeHead(200, { 'Content-Type': 'text/plain'});
res.end(`Hello from ${color} environment, version ${version}\n`);
});
server.listen(3000, () => {console.log(`${color} server v${version} running on port 3000`);
});
EOF
写 Dockerfile:
cat > app/Dockerfile << 'EOF'
FROM node:20-alpine
WORKDIR /app
COPY server.js .
EXPOSE 3000
CMD ["node", "server.js"]
EOF
创建蓝环境的 compose 文件 docker-compose.blue.yml:
services:
app-blue:
build: ./app
environment:
- APP_VERSION=1.0.0
- APP_COLOR=blue
networks:
- web
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 5s
timeout: 3s
retries: 3
networks:
web:
external: true
创建绿环境的 compose 文件 docker-compose.green.yml:
services:
app-green:
build: ./app
environment:
- APP_VERSION=1.1.0
- APP_COLOR=green
networks:
- web
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 5s
timeout: 3s
retries: 3
networks:
web:
external: true
Nginx 配置 nginx.conf:
upstream backend {server app-blue:3000;}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /health {proxy_pass http://backend;}
}
创建 Nginx 的 compose 文件 docker-compose.nginx.yml:
services:
nginx:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
networks:
- web
networks:
web:
external: true
启动蓝环境:
# 创建共享网络
docker network create web
# 启动蓝环境
docker compose -f docker-compose.blue.yml up -d --build
# 启动 Nginx
docker compose -f docker-compose.nginx.yml up -d
验证蓝环境正常工作:
curl http://localhost:8080
# 预期输出: Hello from blue environment, version 1.0.0
现在部署绿环境:
# 启动绿环境(此时蓝环境仍在服务)docker compose -f docker-compose.green.yml up -d --build
# 验证绿环境健康
docker compose -f docker-compose.green.yml ps
切换流量到绿环境——修改 nginx.conf 中的 upstream:
sed -i 's/app-blue:3000/app-green:3000/' nginx.conf
# 重新加载 Nginx 配置(零停机)docker compose -f docker-compose.nginx.yml exec nginx nginx -s reload
验证切换成功:
curl http://localhost:8080
# 预期输出: Hello from green environment, version 1.1.0
回滚
发现问题?一条命令切回去:
sed -i 's/app-green:3000/app-blue:3000/' nginx.conf
docker compose -f docker-compose.nginx.yml exec nginx nginx -s reload
就这么简单。蓝环境一直在那儿等着你,随时可以切回来。
金丝雀发布:先派侦察兵
原理
蓝绿部署虽然好,但有个问题——切换是全量的,要么全走新版本,要么全走老版本。金丝雀发布更精细,先让一小部分流量(比如 5%)走新版本,观察一段时间没问题,再逐步放大比例。
用户请求 (100%)
|
v
+-----------+
| Nginx |
| 权重分配 |
+-----------+
/ \
95% 流量 5% 流量
/ \
+-----------+ +-----------+
| 稳定版本 | | 金丝雀版本 |
| (v1.0) | | (v1.1) |
| 3 个实例 | | 1 个实例 |
+-----------+ +-----------+
第一阶段: 5% → 观察 10 分钟
第二阶段: 25% → 观察 10 分钟
第三阶段: 50% → 观察 10 分钟
第四阶段: 100% → 全量发布完成
名字的来历很有意思:早年矿工下矿井前,会先放一只金丝雀进去。金丝雀对有毒气体特别敏感,如果金丝雀没事,矿工再下去。我们的做法也一样——先让少量用户当 " 金丝雀 ",没问题再全量放开。
实操:Nginx 权重实现金丝雀
修改 nginx.conf 支持权重分配:
upstream backend {
# 稳定版本 - 权重 95
server app-blue:3000 weight=95;
# 金丝雀版本 - 权重 5
server app-green:3000 weight=5;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
确保两个环境都在运行,然后重载 Nginx:
docker compose -f docker-compose.nginx.yml exec nginx nginx -s reload
验证流量分配:
# 发送 20 个请求,观察分布
for i in $(seq 1 20); do
curl -s http://localhost:8080
done
预期输出大致是 19 个 blue、1 个 green(5% 的比例不是精确的,但趋势是对的)。
逐步调大金丝雀比例:
# 第二阶段: 25%
sed -i 's/weight=95/weight=75/' nginx.conf
sed -i 's/weight=5/weight=25/' nginx.conf
docker compose -f docker-compose.nginx.yml exec nginx nginx -s reload
# 第三阶段: 50%
sed -i 's/weight=75/weight=50/' nginx.conf
sed -i 's/weight=25/weight=50/' nginx.conf
docker compose -f docker-compose.nginx.yml exec nginx nginx -s reload
# 第四阶段: 全量切换
sed -i 's/weight=50/weight=0/' nginx.conf # 蓝环境权重设为 0
sed -i 's/weight=50/weight=100/' nginx.conf # 绿环境全量
docker compose -f docker-compose.nginx.yml exec nginx nginx -s reload
Docker 零停机滚动更新
如果你用的是 Docker Swarm 或者单纯的 Docker Compose,还有一种更简单的方式——滚动更新。它的原理是逐个替换容器,始终保证有容器在服务。
Docker Compose 滚动更新
创建 docker-compose.rolling.yml:
services:
app:
build: ./app
environment:
- APP_VERSION=1.1.0
- APP_COLOR=rolling
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
order: start-first
restart_policy:
condition: on-failure
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 5s
timeout: 3s
retries: 3
networks:
- web
networks:
web:
external: true
关键配置解读:
parallelism: 1:每次只更新 1 个容器delay: 10s:两次更新之间等待 10 秒order: start-first:先启动新容器,再停掉旧容器(保证零停机)
滚动更新过程:
时间轴 →
┌─────────┐
实例 1 │ v1.0 │ ──停止──→ ┌─────────┐
└─────────┘ │ v1.1 │
└─────────┘
┌─────────┐ ┌─────────┐
实例 2 │ v1.0 │ ─────── 停止 ───────→│ v1.1 │
└─────────┘ └─────────┘
┌─────────┐ ┌─────────┐
实例 3 │ v1.0 │ ──────────── 停止 ───────────────→│ v1.1 │
└─────────┘ └─────────┘
始终有至少 2 个实例在服务,用户无感知
完整的回滚方案
部署策略再好,也得有后手。回滚方案就是你的保险丝。
镜像版本化管理
# 构建时打上版本标签
docker build -t myapp:1.0.0 -t myapp:latest ./app
# 同时保留多个版本
docker tag myapp:1.0.0 myapp:rollback
docker tag myapp:1.1.0 myapp:latest
自动化回滚脚本
创建 rollback.sh:
#!/bin/bash
set -e
PREVIOUS_VERSION=${1:?"用法: ./rollback.sh < 版本号>"}
CURRENT_ENV=$(cat .current-env 2>/dev/null || echo "blue")
echo "当前环境: $CURRENT_ENV"
echo "回滚到版本: $PREVIOUS_VERSION"
# 确定回滚目标环境
if ["$CURRENT_ENV" = "blue"]; then
ROLLBACK_ENV="green"
ROLLBACK_COMPOSE="docker-compose.green.yml"
else
ROLLBACK_ENV="blue"
ROLLBACK_COMPOSE="docker-compose.blue.yml"
fi
# 检查回滚版本的镜像是否存在
if ! docker image inspect "myapp:$PREVIOUS_VERSION" > /dev/null 2>&1; then
echo "错误: 镜像 myapp:$PREVIOUS_VERSION 不存在"
exit 1
fi
# 切换 Nginx 上游
sed -i "s/app-$CURRENT_ENV:3000/app-$ROLLBACK_ENV:3000/" nginx.conf
docker compose -f docker-compose.nginx.yml exec nginx nginx -s reload
# 更新当前环境记录
echo "$ROLLBACK_ENV" > .current-env
echo "已回滚到 $ROLLBACK_ENV 环境 (版本 $PREVIOUS_VERSION)"
echo "验证: curl http://localhost:8080/health"
chmod +x rollback.sh
健康检查驱动的自动回滚
更高级的玩法是让系统自己判断要不要回滚:
#!/bin/bash
# auto-rollback-check.sh
HEALTH_URL="http://localhost:8080/health"
MAX_FAILURES=3
CHECK_INTERVAL=10
failure_count=0
echo "开始健康检查监控..."
while true; do
response=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_URL" 2>/dev/null)
if ["$response" != "200"]; then
failure_count=$((failure_count + 1))
echo "健康检查失败 ($failure_count/$MAX_FAILURES), HTTP 状态: $response"
if ["$failure_count" -ge "$MAX_FAILURES"]; then
echo "连续失败 $MAX_FAILURES 次,触发自动回滚!"
./rollback.sh "$(cat .previous-version)"
exit 1
fi
else
failure_count=0
fi
sleep "$CHECK_INTERVAL"
done
部署策略对比:选哪个
| 特性 | 蓝绿部署 | 金丝雀发布 | 滚动更新 |
|---|---|---|---|
| 资源占用 | 双倍 | 少量额外 | 几乎不变 |
| 回滚速度 | 秒级 | 秒级 | 分钟级 |
| 风险控制 | 全量切换 | 渐进式 | 渐进式 |
| 复杂度 | 中等 | 较高 | 较低 |
| 适用场景 | 重大版本更新 | 功能灰度测试 | 常规迭代 |
选择建议:
- 日常小版本迭代 → 滚动更新 ,简单够用
- 大版本上线、数据库迁移 → 蓝绿部署 ,回滚无忧
- 新功能灰度、A/B 测试 → 金丝雀发布 ,精细控制
常见问题 Q&A
Q1: 蓝绿部署需要双倍的服务器资源,成本太高怎么办?
A: 不一定需要常驻双倍资源。平时只跑一套环境,部署时临时拉起另一套,切换完成后可以把旧环境缩容。用云服务器的弹性伸缩功能,按需付费,成本可以控制在增加 10%-20% 左右。
Q2: 金丝雀发布时,如果新版本往数据库写了脏数据怎么办?
A: 这是个好问题。核心原则是数据库变更要向前兼容。新版本的数据库迁移脚本必须保证:老版本读得了新数据,新版本也读得了老数据。具体做法是 " 先加后删 "——先加新字段,双版本都能跑了,再在下个版本删旧字段。
Q3: 小团队没精力搞这么复杂的部署流程,有简化方案吗?
A: 从滚动更新开始就够了。Docker Compose 配合健康检查,加一个简单的回滚脚本,覆盖 80% 的场景。等团队规模大了、服务多了,再逐步引入蓝绿或金丝雀。千万别一上来就搞全套,过度工程比没有工程更可怕。
小结
今天我们学了三种核心部署策略:
- 蓝绿部署 :两套环境轮着来,切换快、回滚也快,适合大版本上线
- 金丝雀发布 :先放一小波流量试水,没问题再全量,适合灰度测试
- 滚动更新 :逐个替换容器,简单实用,适合日常迭代
核心思想就一句话: 永远给自己留退路 。部署不是赌博,是工程。有了这些策略,你再也不用上线时拆炸弹了。
记住几个关键点:
- 一定要有健康检查,让系统自己告诉你它是不是好的
- 一定要有回滚方案,而且回滚方案要提前测过
- 数据库变更要向前兼容,这是所有部署策略的基础
明天是 Day 28 容器故障排查 ,我们来聊聊容器出问题时怎么快速定位和修复。毕竟部署只是开始,运维才是持久战。