直接把新文件复制到活动的静态目录上,会造成一段访问者可能从一个版本拿到 HTML、从另一个版本拿到资源的时期。更安全的模式是:在活动树旁预置一棵完整的树,验证它,再改变目录名。

由单一发布者完成同文件系统与预置树核验后,live 存放旧版,stage 存放新版。第一次把 live 移到 rollback,live 缺失而 stage 与 rollback 保留。第二次把 stage 移到 live。成功时 live 为新版、rollback 为旧版;失败时 live 仍缺失,需要显式尝试恢复。恢复失败必须检查路径;发布后还须验证公共响应。
两次移动之间 live 缺失,崩溃可能延长空窗。恢复需要尝试并验证。浏览器各次请求彼此独立,旧资源 URL 必须继续可访问。

把所有发布目录放在同一文件系统上

把活动树、预置树与回滚树放在同一文件系统上。以下片段假设使用同一个 Bash 会话、GNU Coreutils,且只有一个发布进程独占这些目录名。任何命令或复制管道任一侧失败,都应停止:

set -euo pipefail
df -P /srv/www

在该父目录下创建唯一命名的中转目录。不要把未解析的变量或宽泛通配符当成删除目标来复用。

stage=$(mktemp -d /srv/www/site.new.XXXXXX)

填充并验证预置目录

把生成制品复制进空目录,然后检查代表性文件。最后一条命令只显示数量;应与已核验制品的期望数量或清单比较,不能把任意数字都当作通过:

tar -C public -cf - . | tar -C "$stage" -xf -

test -s "$stage/index.html"
test -s "$stage/404.html"
find "$stage" -type f | wc -l

当工具支持目录参数时,对预置路径运行链接与校验和检查。还要确认服务用户能穿过目录并读取文件:mktemp -d 初始权限较严,解包后的制品必须具备预期权限。未通过验证的预置目录绝不能获得活动目录的名字。

用显式回滚路径切换名字

在第一次移动前解析每个路径:

live=/srv/www/site
stamp=$(date -u +%Y%m%dT%H%M%SZ)
rollback=/srv/www/site.old-$stamp

test -d "$live" && test ! -L "$live" || exit 1
test -d "$stage" && test ! -L "$stage" || exit 1
test ! -e "$rollback" && test ! -L "$rollback" || exit 1
df -P "$live" "$stage"
live_device=$(stat -c %d -- "$live") || exit 1
stage_device=$(stat -c %d -- "$stage") || exit 1
test "$live_device" = "$stage_device" || exit 1

mv -T -- "$live" "$rollback" || exit 1
if mv -T -- "$stage" "$live"; then
  test -s "$live/index.html"
else
  mv -T -- "$rollback" "$live" || {
    printf 'Restore failed; inspect %s and %s before retrying.\n' "$rollback" "$live" >&2
    exit 1
  }
  exit 1
fi

这是两步切换,不是一次原子替换:两次移动之间活动路径不存在,崩溃后可能一直缺失,直到恢复。GNU mv -T 可防止已有目录被悄悄当作容器、把发布树移进其中,但不能协调并发发布进程。文件系统检查结果不一致时不要继续。平台特定的原子交换或符号链接替换能消除名字缺失窗口,但必须结合实际服务器测试。

验证公共边缘

切换之后,请求首页、一个深层路由、一个带指纹的资源与一个缺失路径。把公共字节与预置制品比较。在检查完成与观察窗口结束之前,保留回滚目录。

即使目录名切换是原子的,浏览器请求 HTML 与资源仍不是同一笔事务。让旧指纹资源继续可访问,才能服务缓存中或传输中的 HTML;仅保留一个不对外服务的回滚目录并不能做到这一点。文件系统重命名也不自动保证崩溃后的持久性,这需要平台相应的持久化步骤。

来源