小Cの已经记不起来的博客

从零实现一个简易的蓝绿部署

前几天半夜给服务发新版本,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 把旧容器拉起来。这也是蓝绿最大的好处:回滚就是一次反向切换,而不是一次重新发布。代价就是磁盘上会一直躺着一份旧镜像加一个停止的容器,对现在的机器来说这点空间不算什么。

踩过的坑

写在最后

没有 K8s,没有 Jenkins,没有 Argo Rollouts,就是 docker + nginx 加几十行 shell,在这台小机器上稳定跑了几个月,每次发版群里再也没人喊“网站挂了”了。当然,服务规模再大一点这套就不够看了,但对一台 2核4G 的 VPS 来说,刚刚好。下一步打算给脚本加个发版成功后推 Telegram 通知的功能,免得半夜发完版还惦记着要不要爬起来看一眼日志。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放