跳到内容

第 0 卷 / 第 8 章 / 第 3 课

8.3 本地发布与回滚实验 ​

前置知识:本章前两页

预计时间:30 分钟

练习环境:继续使用 $release_lab_dir;容器小节使用 Docker Engine 28+ 的默认 bridge/NAT 网络及 Bash、curl 7.71+

完成标志:用同一 JAR 构建两个可识别版本,按已知良好 image ID 实际回滚,并验证结果

自动化结构已经审过,但远程 registry 和生产环境不属于这个练习的范围。本实验留在本机,用同一个 JAR 完成版本切换、健康检查、证据记录、实际回滚和清理。

bash
cd "$release_lab_dir"

热身:验证制品 ​

bash
mvn --batch-mode verify
sha256sum target/release-lab-1.0.0.jar
jar tf target/release-lab-1.0.0.jar | head

macOS 可用 shasum -a 256。临时项目默认没有 Git 历史,此时记录源码文件和 JAR 的哈希;只有源码确实来自提交时才记录 commit SHA。保留构建日志;不要把 -DskipTests 写进默认发布路径。

启动 JAR 后,从另一个终端请求 /health 与 /,再用普通终止信号结束你刚启动的 PID。不要用 kill -9 作为首选,它不给应用清理机会。

挑战:容器候选版本 ​

这里先验证已知良好 v1,再运行 v2,最后主动发起一次回滚演练,按记录的 v1 image ID 启动并重新核对 /health、/version 和容器实际镜像。两份镜像复用同一个 JAR,仅公开版本标识不同;这验证版本切换,不证明业务兼容性或零停机。临时端口会随容器改变,因此也不模拟生产流量入口。

bash
(
set -e
lab_tag="workshop-ch08-release:${RANDOM}-${RANDOM}"
good_tag="${lab_tag}-good"
candidate_tag="${lab_tag}-candidate"
active_id=
cleanup_release() {
  if [ -n "$active_id" ]; then
    docker stop "$active_id" >/dev/null 2>&1 || true
  fi
  docker image rm "$candidate_tag" "$good_tag" >/dev/null 2>&1 || true
}
trap cleanup_release EXIT
trap 'exit 130' INT
trap 'exit 143' TERM

start_and_check() {
  active_id=$(docker run --detach --rm --env HOST=0.0.0.0 \
    --publish 127.0.0.1::8080 "$1")
  published=$(docker port "$active_id" 8080/tcp)
  host_port=${published##*:}
  curl --fail --silent --show-error --retry 10 --retry-all-errors \
    --retry-delay 1 --max-time 2 "http://127.0.0.1:$host_port/health"
  observed=$(curl --fail --silent --show-error --max-time 2 \
    "http://127.0.0.1:$host_port/version")
  test "$observed" = "$2"
  printf 'version=%s image=%s\n' "$observed" "$1"
}

docker build --build-arg APP_VERSION=v1 -t "$good_tag" .
good_image=$(docker image inspect "$good_tag" --format '{{.Id}}')
start_and_check "$good_image" v1
docker stop "$active_id"
active_id=

docker build --build-arg APP_VERSION=v2 -t "$candidate_tag" .
candidate_image=$(docker image inspect "$candidate_tag" --format '{{.Id}}')
test "$good_image" != "$candidate_image"
start_and_check "$candidate_image" v2
docker logs "$active_id"
docker image inspect "$candidate_tag" --format '{{json .RepoDigests}}'

docker stop "$active_id"
active_id=
start_and_check "$good_image" v1
test "$(docker inspect "$active_id" --format '{{.Image}}')" = "$good_image"
printf 'rollback verified\n'
)

预期依次看到 version=v1、version=v2、version=v1 和 rollback verified,子 Shell 退出时清理本次容器与标签。本地镜像通常没有 registry RepoDigests,但有内容寻址的 image ID;推送后才记录 registry digest。本页没有执行推送。

写发布与回滚判据 ​

text
候选 commit / artifact hash / image digest:
发布前通过的测试:
数据库迁移是否向后兼容:
健康检查路径与超时:
观测窗口:
停止推广条件:
已知良好 digest:
回滚命令与负责人:
回滚后验证:

回滚不是“再拉一次 latest”。它应把流量或部署声明恢复到已知良好的不可变版本。若数据库迁移不向后兼容,单纯换回镜像可能仍会失败,必须提前设计前向兼容或恢复方案。

排障 ​

现象处理
端口冲突查看监听者,优先换随机回环端口,不杀未知进程
线上仍是旧代码对比部署 digest、容器 .Image、响应版本字段和缓存层
no main manifest attribute检查 JAR manifest 与打包插件;不要假定普通 JAR 可执行
health 返回 200 但业务失败健康检查过浅;增加关键依赖就绪检查和外部合成请求
容器刚启动就退出docker logs、退出码、入口命令、配置和权限

实验结束后确认临时项目再删除:

bash
cd
ls -ld -- "$workdir"
rm -r -- "$workdir"
unset release_lab_dir

本机实验通过后,你已经有了版本切换和回滚的可核对记录。第一卷接着学习程序本身的语法与结构。

→ 前往第一卷:编程基础部分

Built with VitePress | Software Systems Atlas