直接把新文件复制到活动的静态目录上,会造成一段访问者可能从一个版本拿到 HTML、从另一个版本拿到资源的时期。更安全的模式是:在活动树旁预置一棵完整的树,验证它,再改变目录名。
把所有发布目录放在同一文件系统上
把活动树、预置树与回滚树放在同一文件系统上。以下片段假设使用同一个 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;仅保留一个不对外服务的回滚目录并不能做到这一点。文件系统重命名也不自动保证崩溃后的持久性,这需要平台相应的持久化步骤。
来源
- POSIX
rename(),名字替换语义,已核验 2026-06-01。 - Linux
rename(2),文件系统与替换行为,已核验 2026-10-15。 - GNU tar manual,归档创建与解包,已核验 2026-06-01。
- GNU Coreutils: 目标目录,
-T与目录嵌套风险,已核验 2026-10-15。 - GNU Coreutils: stat,文件系统设备号检查,已核验 2026-10-15。