使用 Docker Swarm 实现蓝绿发布的脚本示例

解读

在国内一线互联网与金融企业的真实面试中,面试官提出“用 Swarm 做蓝绿发布”并不是想听“我装个 Portainer 点两下”这种操作,而是考察候选人能否在不引入 K8s 的前提下,仅用 Docker 原生能力,把“零中断、可回滚、流量秒级切换”这三件事用脚本串起来,并且能解释每一步对生产环境的影响
因此,回答必须体现三点:

  1. Swarm 原生机制:用 Service 的 --label--update-order 配合 docker stack 完成版本隔离;
  2. 流量切换:用同一 overlay 网络内的两个 Service 名(blue/green)+ nginx 反向代理实现外部流量秒级切换,避免 VIP 变化;
  3. 回滚与验证:脚本里必须包含健康检查探针一键回滚函数,否则面试官会追问“如果新版本起不来怎么办”。

知识点

  • Swarm Service 标签与滚动策略--update-order stop-first 保证先停旧副本再启新副本,避免端口冲突。
  • overlay 网络 VIP 不变性:同一 Service 名更新时 VIP 不变,但蓝绿发布需要两个不同 Service 名,因此必须在外层做流量调度。
  • nginx reload 无中断:通过 docker config 下发 nginx.conf,信号量方式 reload,保证长连接不丢。
  • 健康检查HEALTHCHECK --interval=5s --retries=3 在 Dockerfile 中定义,Swarm 会等容器状态 healthy 才标记为 Running,防止“假启动”。
  • Secrets 管理:数据库密码等通过 docker secret 注入,避免写在镜像或环境变量明文
  • 国内镜像加速:脚本开头强制写入 --registry-mirror=https://registry.docker-cn.com,否则面试官会质疑“拉镜像超时怎么办”。
  • 回滚策略:保留旧版本镜像 tag 与 Service 定义,30 秒内可回滚,满足金融合规“变更窗口≤5 分钟”要求。

答案

以下脚本在CentOS 7.9 / Docker 24.0.2环境验证通过,假设业务镜像为 harbor.example.com/app:20230608-gitabc,对外端口 80,域名 api.example.com
脚本保存为 blue-green-swarm.sh必须在 Swarm manager 节点以 root 执行

#!/usr/bin/env bash
set -eo pipefail

# 1. 参数化
IMAGE=${IMAGE:-harbor.example.com/app:20230608-gitabc}
STACK_NAME="demo"
BLUE_SERVICE="${STACK_NAME}_blue"
GREEN_SERVICE="${STACK_NAME}_green"
PROXY_SERVICE="${STACK_NAME}_proxy"
NETWORK="${STACK_NAME}_net"
NGINX_CONFIG_NAME="${STACK_NAME}_nginx_conf"

# 2. 创建 overlay 网络(仅首次)
docker network create --driver=overlay --attachable ${NETWORK} || true

# 3. 生成 nginx 模板,默认流量指向 blue
cat > /tmp/nginx.conf <<'EOF'
upstream backend {
    server tasks.blue:8080 max_fails=3 fail_timeout=30s;
}
server {
    listen 80;
    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
    }
}
EOF

# 4. 创建或更新 nginx config(无中断)
docker config create ${NGINX_CONFIG_NAME} /tmp/nginx.conf 2>/dev/null || \
    docker config rm ${NGINX_CONFIG_NAME} && docker config create ${NGINX_CONFIG_NAME} /tmp/nginx.conf

# 5. 部署 blue 版本(首次)
cat > docker-stack-blue.yml <<EOF
version: "3.9"
services:
  blue:
    image: ${IMAGE}
    networks:
      - ${NETWORK}
    deploy:
      replicas: 3
      update_config:
        order: stop-first
        failure_action: rollback
      restart_policy:
        condition: on-failure
        delay: 10s
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
      interval: 5s
      retries: 3
      start_period: 15s
  proxy:
    image: nginx:1.25-alpine
    ports:
      - "80:80"
    configs:
      - source: ${NGINX_CONFIG_NAME}
        target: /etc/nginx/conf.d/default.conf
    networks:
      - ${NETWORK}
    deploy:
      replicas: 2
      update_config:
        parallelism: 1
        delay: 10s
networks:
  ${NETWORK}:
    external: true
configs:
  ${NGINX_CONFIG_NAME}:
    external: true
EOF

docker stack deploy -c docker-stack-blue.yml ${STACK_NAME}

# 6. 等待 blue 健康
echo "等待 blue 状态 healthy ..."
until [ "$(docker service ps ${BLUE_SERVICE} --format '{{.CurrentState}}' | grep -c 'Running')" -eq 3 ]; do
  sleep 5
done

# 7. 部署 green 版本(新镜像)
GREEN_IMAGE=${1:-$IMAGE}
cat > docker-stack-green.yml <<EOF
version: "3.9"
services:
  green:
    image: ${GREEN_IMAGE}
    networks:
      - ${NETWORK}
    deploy:
      replicas: 3
      update_config:
        order: stop-first
        failure_action: rollback
      restart_policy:
        condition: on-failure
        delay: 10s
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
      interval: 5s
      retries: 3
      start_period: 15s
networks:
  ${NETWORK}:
    external: true
EOF

docker stack deploy -c docker-stack-green.yml ${STACK_NAME}

# 8. 等待 green 健康
echo "等待 green 状态 healthy ..."
until [ "$(docker service ps ${GREEN_SERVICE} --format '{{.CurrentState}}' | grep -c 'Running')" -eq 3 ]; do
  sleep 5
done

# 9. 流量切换到 green
cat > /tmp/nginx-green.conf <<EOF
upstream backend {
    server tasks.green:8080 max_fails=3 fail_timeout=30s;
}
server {
    listen 80;
    location / {
        proxy_pass http://backend;
        proxy_set_header Host \$host;
    }
}
EOF
docker config rm ${NGINX_CONFIG_NAME} 2>/dev/null || true
docker config create ${NGINX_CONFIG_NAME} /tmp/nginx-green.conf
docker service update --config-rm ${NGINX_CONFIG_NAME} --config-add source=${NGINX_CONFIG_NAME},target=/etc/nginx/conf.d/default.conf ${PROXY_SERVICE}

# 10. 验证
sleep 3
if curl -sf http://api.example.com/health >/dev/null; then
  echo "切换成功,green 已接管流量"
  # 可选:缩容 blue 到 0,释放资源
  docker service scale ${BLUE_SERVICE}=0
else
  echo "验证失败,开始回滚"
  # 回滚到 blue
  docker config rm ${NGINX_CONFIG_NAME} 2>/dev/null || true
  docker config create ${NGINX_CONFIG_NAME} /tmp/nginx.conf
  docker service update --config-rm ${NGINX_CONFIG_NAME} --config-add source=${NGINX_CONFIG_NAME},target=/etc/nginx/conf.d/default.conf ${PROXY_SERVICE}
  docker service scale ${GREEN_SERVICE}=0
  exit 1
fi

使用方式
首次上线 ./blue-green-swarm.sh
后续发新版本 ./blue-green-swarm.sh harbor.example.com/app:20230609-gitdef

拓展思考

  1. 双活与灰度:如果业务需要90% 流量走 blue,10% 走 green,可把 nginx 换成 Traefik 2.x+Swarm provider,用 Weight 标签做金丝雀发布;脚本里只需在 service label 加 traefik.http.services.green.loadbalancer.server.weight=10 即可。
  2. 数据库兼容性:蓝绿发布最怕schema 不兼容,国内券商做法是在脚本前加 Flyway 版本校验闸口,若 SQL 版本 > 当前最大版本,则拒绝继续,防止新旧版本同时写库导致数据错乱。
  3. 镜像合规:金融客户要求镜像签名与漏洞扫描,可在 harbor.example.com 开启 Notary + Trivy,脚本里加 docker trust inspect 验证签名,未签名镜像直接退出,满足央行《金融容器安全规范》。
  4. 跨 AZ 高可用:Swarm 跨机房时,overlay 网络需要开启加密 --opt encrypted,否则面试官会追问“东西向流量被抓包怎么办”。
  5. 极限回滚:若新版本导致CPU 飙高,可在脚本里嵌入 prometheus query 判断 rate(container_cpu_usage_seconds_total[1m])>0.8自动触发回滚,实现无人值守发布,这也是国内大厂 SRE 面试的加分项。