从零实现一个简易的蓝绿部署
前几天半夜给服务发新版本,systemctl restart 敲下去的瞬间,监控上直接一条 502,紧接着群里就有人问“网站是不是挂了”。其实就断了十来秒,但这种事发生一次就够糟心的了。之前一直觉得蓝绿部署是 K8s 那些大厂玩具,跟我这台 2核4G 的小机器没什么关系,后来查了一下才发现,这玩意儿的核心思路简单得离谱:跑两套一模一样的环境,一套对外服务,另一套用来发新版,发完把流量切过去,出问题再切回来。那为什么不直接找个现成的轻量方案呢?可能是我不愿意去找吧...
下面是我在自己机器上实测跑通的做法,只需要 docker、nginx 和一个几十行的 shell 脚本。
先说思路
机器上同时存在 blue 和 green 两个容器,nginx 的 upstream 指向其中之一:
用户 --> nginx --> blue(9001) 对外服务
\-> green(9002) 待命,用来发新版
发版的时候永远往“不对外”的那套上部署,健康检查通过了就把 nginx 的 upstream 改一下、reload,旧的那套先留着不删,观察一会儿没问题再停掉。下次发版就反过来。整个过程理论上零中断,回滚也只是把 upstream 改回去再 reload 一下,秒级的事。
把两套环境跑起来
先是两份 docker-compose 文件,内容几乎一样,就是端口不同,镜像 tag 都用 myapp:latest,反正同一时间只有一套在跑:
# docker-compose.blue.yml
services:
app:
image: myapp:latest
restart: unless-stopped
ports:
- "127.0.0.1:9001:8080" # 只绑本机,别暴露到公网
green 那份把 9001 换成 9002 即可,没准儿你机器上 9001 已经被别的服务占了,那换个端口就是了。这里端口一定要绑在 127.0.0.1 上,不然两套环境等于同时暴露在公网,流量就绕过 nginx 直连了,这通操作等于白忙活。
然后把当前对外的这套先启动,并记下颜色:
docker compose -f docker-compose.blue.yml up -d
echo blue > .current
nginx 配置里对应的部分长这样:
upstream app_backend {
server 127.0.0.1:9001;
}
server {
listen 80;
location / {
proxy_pass http://app_backend;
}
}
写个脚本把流程串起来
手动发版其实就是固定顺序的几步:构建新镜像、起备用环境、健康检查、切流量、停旧环境。手敲两回就烦了,索性写个 deploy.sh:
#!/bin/bash
set -e
CURRENT=$(cat .current) # 当前对外的颜色
if [ "$CURRENT" = "blue" ]; then
TARGET=green; PORT=9002
else
TARGET=blue; PORT=9001
fi
docker build -t myapp:latest .
echo "启动 $TARGET ..."
docker compose -f docker-compose.$TARGET.yml up -d
echo "等健康检查..."
ok=""
for i in $(seq 1 30); do
if curl -sf http://127.0.0.1:$PORT/healthz > /dev/null; then
ok=1
break
fi
sleep 2
done
if [ -z "$ok" ]; then
echo "健康检查一直没过,先别切流量,看看日志吧"
exit 1
fi
echo "切换 nginx 到 $TARGET ..."
sed -i "s/127.0.0.1:900[12]/127.0.0.1:$PORT/" /etc/nginx/conf.d/app.conf
nginx -t && nginx -s reload
sleep 5 # 留个后悔窗口,这几秒内 ctrl+c 的话旧环境不会被停
docker compose -f docker-compose.$CURRENT.yml stop
echo "$TARGET" > .current
echo "部署完成,当前线上是 $TARGET"
几个细节说一下。sed 那行是直接把配置里的端口替换掉,因为两个端口只差最后一位,所以正则写成 900[12],替换完先 nginx -t 校验一下再 reload,免得手滑改坏了配置直接把 nginx 搞挂。nginx -s reload 是平滑重载,老连接会处理完才断,新连接直接走新的 upstream,所以不会出现 502。不出问题的话,脚本跑完 .current 里写的就是新颜色,下次发版自动就往另一套部署了。
回滚才是蓝绿的意义
脚本最后停旧环境用的是 stop 而不是 down,容器是留着不删的。所以回滚根本不用重新构建镜像,只要把 nginx 的 upstream 改回旧端口再 nginx -s reload,用户那边就是无感的,或者更狠一点,直接 docker start 把旧容器拉起来。这也是蓝绿最大的好处:回滚就是一次反向切换,而不是一次重新发布。代价就是磁盘上会一直躺着一份旧镜像加一个停止的容器,对现在的机器来说这点空间不算什么。
踩过的坑
- 数据库变更要兼容两个版本。切换和回滚的瞬间,新旧两个版本的代码是先后在跑的,所以表结构不能一步到位地改。新增字段尽量给默认值或者允许为空,删字段更要忍一忍,等新版稳定跑几天了再清理。
- 登录态别放本地。我第一版把 session 存在进程内存里,切完流量所有用户集体掉线,还以为是新版把认证搞挂了,排查半天。后来老老实实挪到 redis 里,两个环境共用,切换就真的无感了。
- 定时任务只能有一份。两套环境都起的话,那个每分钟跑一次的统计脚本就会跑两遍,数据直接翻倍。所以 cron 类的东西我单独拆了一个容器,不跟着蓝绿走。
- 磁盘要记得清。每发一版就多一个镜像,小机器磁盘本来就紧张,cron 里挂一条
docker image prune -f,不然哪天磁盘写满又是新的故事了。
写在最后
没有 K8s,没有 Jenkins,没有 Argo Rollouts,就是 docker + nginx 加几十行 shell,在这台小机器上稳定跑了几个月,每次发版群里再也没人喊“网站挂了”了。当然,服务规模再大一点这套就不够看了,但对一台 2核4G 的 VPS 来说,刚刚好。下一步打算给脚本加个发版成功后推 Telegram 通知的功能,免得半夜发完版还惦记着要不要爬起来看一眼日志。
评论
还没有评论。
发表评论
提交后评论将经过自动审核,审核通过后公开展示。