最近有个小系统,因为经常加入一些小需求功能,每次开发完成验证后都需要快速上线,而每次上线服务就需要 先杀旧容器,再起新服务。构建加启动的一两分钟里,系统就是"断网"状态,服务每次都需要宕个四五分钟的时间,这体验感也太差了。
想要的效果很朴素:新版本确认能正常服务了,旧版本才退出。这就是滚动发布的思路。
方案选择
Docker Swarm swarm init 即用
Swarm 有个为这个场景量身定做的参数:--update-order start-first。默认的 stop-first 是先停旧再起新(必有中断),改成 start-first 后:起新容器 → 健康检查通过 → 切流量 → 优雅停旧容器。配合 failure_action: rollback,新版本起不来还会自动回滚。
落地:把 docker run 改为 Swarm 服务
流程概览
先看整体的一个构建发布流程,通过jenkins编译服务包,然后推送到远程服务器进行镜像构建,最后进行服务的滚动更新。
Jenkins 构建
│ 1. 传输 test-service.jar 到服务器
▼
服务器 /app/test
│ 2. 生成版本号 V2.0.0.{构建号}_YYYYMMDD
│ 3. docker build 构建新镜像(旧服务照常运行)
│ 4. docker stack deploy 触发滚动更新
▼
Docker Swarm(单机模式,stack 名 zhd)
│ 5. 起新容器 → 健康检查通过 → 切流量 → 优雅停旧容器
│ 6. 观察期 60s 内异常 → 自动回滚
▼
Jenkins 等待收敛结果:成功=绿 / 回滚或超时=红
实操改造
初始化swarm集群,由于本身就是单机部署,所以直接初始化就行
#初始化 Swarm(单机即可)
docker swarm init --advertise-addr $(hostname -I | awk '{print $1}')
swarm集群创建成功会输出一个唯一的token,如果是多节点的情况可以将其他节点通过这个token加入同一个集群中,这里只是单机,所以初始化完成即可。
Stack 文件(deploy_server.yml)
这个yml文件就是部署策略的核心文件,包含了如何更新,如何健康检查,回滚等等,可以理解成docker compose yaml文件的升级版本,支持更多的特性,示例如下:
services:
test-service-v2:
image: test-service:V2.0.0.xxx_YYYYMMDD # 由脚本注入当次版本号
cap_add:
- ALL # 替代 docker run 的 --privileged(Swarm 不支持 --privileged)
ports:
- "9101:9100"
volumes:
- type: bind
source: /app/logs
target: /app/logs
stop_grace_period: 30s # 收到 SIGTERM 后给 30s 处理完存量请求
healthcheck:
# 健康检查在容器内部执行,端口用容器内的 9100(不是对外的 9101)
test: ["CMD", "curl", "-sf", "-o", "/dev/null", "http://127.0.0.1:9100/"]
interval: 5s # 每 5s 检查一次
timeout: 3s
retries: 3 # 连续 3 次失败才判 unhealthy
start_period: 60s # 启动预热期,期间失败不计数(按应用启动耗时调整)
deploy:
replicas: 1
update_config:
parallelism: 1 # 一次滚动 1 个
delay: 10s
order: start-first # ★ 无感核心:先起新容器,healthy 后才停旧容器
failure_action: rollback # 健康检查不过 → 自动回滚
monitor: 60s # 切流量后的观察期,期间异常也算本次更新失败
rollback_config:
parallelism: 1
order: stop-first
restart_policy:
condition: on-failure
max_attempts: 3 # 任务连续失败 3 次后不再重试(需人工 --force)
Jenkins 配置
jenkins的编译过程此处忽略,构建后通过publish 到远程服务实现镜像构建
Publish Over SSH 设置
target/test-service.jartest-serviceAdvanced → Exec timeout (ms) 900000
Exec command
Jenkins的执行命令配置如下部署流程shell脚本,自动写入yaml,然后构建镜像,滚动更新服务
set -e
cd /app/test-service
# ===== 1. 生成唯一版本号(BUILD_NUMBER 由 Jenkins 注入,每次构建都不同)=====
[[ -n "$BUILD_NUMBER" ]] || { echo "==> 错误:未取到 Jenkins BUILD_NUMBER"; exit 1; }
IMAGE_TAG="V2.0.0.${BUILD_NUMBER}_$(date +%Y%m%d)"
SERVICE="test-service-v2"
echo "==> 本次发布版本: test-service:${IMAGE_TAG}"
# ===== 2. 生成 stack 文件(tag 写文件时注入,YAML 随 Jenkins 任务走,服务器不用手工维护)=====
cat > test-service.yml <<EOF
services:
test-service-v2:
image: test-service:${IMAGE_TAG}
cap_add:
- ALL
ports:
- "9101:9100"
volumes:
- type: bind
source: /app/logs
target: /app/logs
stop_grace_period: 30s
healthcheck:
test: ["CMD", "curl", "-sf", "-o", "/dev/null", "http://127.0.0.1:9100/"]
interval: 5s
timeout: 3s
retries: 3
start_period: 60s
deploy:
replicas: 1
update_config:
parallelism: 1
delay: 10s
order: start-first
failure_action: rollback
monitor: 60s
rollback_config:
parallelism: 1
order: stop-first
restart_policy:
condition: on-failure
max_attempts: 3
EOF
# ===== 3. 构建新镜像(构建期间旧服务照常运行,零中断;唯一 tag 不会覆盖旧镜像)=====
docker build --rm -f Dockerfile -t "test-service:${IMAGE_TAG}" .
# ===== 4. 首次迁移:swarm 服务尚不存在时才停旧 docker run 容器 =====
if ! docker service inspect ${SERVICE} >/dev/null 2>&1; then
docker rm -f test-service 2>/dev/null || true
fi
# ===== 5. 部署/滚动更新(唯一 tag 保证每次都触发滚动:起新容器 → healthy 后切流量 → 优雅停旧容器)=====
docker stack deploy -c test-service.yml example
# ===== 6. 等待滚动结果:成功才退出0;自动回滚/超时让 Jenkins 构建标红 =====
OK=0
for i in $(seq 120); do
STATE=$(docker service inspect --format '{{.UpdateStatus.State}}' ${SERVICE} 2>/dev/null || echo none)
case "${STATE}" in
completed)
OK=1; echo "==> 滚动更新成功"; break ;;
rollback_started|rollback_completed)
echo "==> 新版本健康检查未通过,Swarm 已自动回滚"; exit 1 ;;
none|"")
REPLICAS=$(docker service inspect --format '{{.Replicas}}' ${SERVICE} 2>/dev/null || echo 0/0)
if [[ "${REPLICAS}" == "1/1" ]]; then OK=1; echo "==> 服务就绪"; break; fi
;;
esac
sleep 5
done
[[ ${OK} -eq 1 ]] || { echo "==> 等待滚动更新超时(10分钟)"; exit 1; }
# ===== 7. 镜像清理:保留最近 3 个版本(供手动回滚),删除更早的 =====
KEEP=3
docker images test-service --format '{{.Tag}}' | tail -n +$((KEEP+1)) | while read -r t; do
docker rmi "test-service:${t}" 2>/dev/null || true
done
docker image prune -f >/dev/null
echo "==> 部署完成: test-service:${IMAGE_TAG}"
echo "==> 手动回滚命令(如需): docker service rollback ${SERVICE}"
常用运维命令
回滚操作
# 方式 1:一键回滚到上一个版本(镜像本地保留了最近 3 版,秒回)
docker service rollback example_test-service
# 方式 2:回滚到指定历史版本
docker images test-service # 查看保留的版本
docker service update --image test-service:V2.0.0.126_20260915 example_test-service
# 方式 3:Jenkins 重新构建旧代码分支(会生成新版本号,走正常滚动流程)
服务的停止 / 启动 / 删除
# 临时停止(保留服务定义,可随时恢复)
docker service scale example_test-service=0
docker service scale example_test-service=1 # 恢复
# 删除单个服务(容器停止 + 服务定义删除)
docker service rm example_test-service
# 删除整个 stack
docker stack rm exampe
总结
服务的稳定性和可用性在生产中都是重要的指标,会直接影响用户的使用体验,通过以上的方式可以实现最低成本的服务不停服更新,全面保证系统的可用性,不会再出现每次部署都需要等待杀死,然后重新启动完成的这个间隙不可用的问题。
评论