切换前,current 指向 releases/v1;pending 表示已核验的 current.next 符号链接,指向 releases/v2。在同一文件系统上用 os.replace(pending, current) 替换 current 链接。切换后新打开读取 v2,已打开的文件描述符仍读取 v1。回滚需创建指向已记录 v1 目标的新链接,再执行替换。
同一文件系统上的符号链接替换选择完整发布树。旧资源 URL 必须继续可访问。本地示例不证明生产迁移、浏览器跨请求事务、自动回滚或断电持久性。

Context

将变更文件直接覆盖在线上树中,会暴露部分发布,也让上一版难以命名。完整暂存解决了复制中途的问题,但“把新目录重命名覆盖旧目录”并不是足够的发布过程。mv new existing-directory 通常把 new 移入目的目录。普通 POSIX rename 不能替换已有的非空目录。先移走旧目录、再把新目录移入,则在两次操作之间留下空窗。

Decision

保留不可变、带版本名的发布目录,用 current 符号链接选择其中一版。让文件服务器服务这个稳定路径。校验新树后,在 current 旁创建临时符号链接,再通过同一文件系统上的 rename 原子替换符号链接本身。旧发布目录以原有版本名保留,不受修改。

这是完整树决策的实现模式,不是生产主机已经迁移到此布局的证据。若当前服务根是真实目录,需要单独规划迁移,不能对那个路径直接执行示例并假设符号链接会安全替换它。

在本地演练切换与回滚

下列 Python 3 练习只创建一次性本地文件,在 POSIX 文件系统上展示指针操作,不启动 Caddy,也不改变部署。

from pathlib import Path
from tempfile import TemporaryDirectory
import os

with TemporaryDirectory() as tmp:
    root = Path(tmp)
    for version in ("v1", "v2"):
        release = root / "releases" / version
        release.mkdir(parents=True)
        (release / "index.html").write_text(version, encoding="utf-8")
    current = root / "current"
    current.symlink_to("releases/v1", target_is_directory=True)
    previous = os.readlink(current)
    with (current / "index.html").open() as in_flight:
        pending = root / "current.next"
        pending.symlink_to("releases/v2", target_is_directory=True)
        os.replace(pending, current)
        print("new open:", (current / "index.html").read_text())
        print("already open:", in_flight.read())
    pending.symlink_to(previous, target_is_directory=True)
    os.replace(pending, current)
    print("rollback:", (current / "index.html").read_text())

预期输出为 new open: v2、already open: v1、rollback: v1。成功的 rename 使目录项替换具有原子性,却不会改变已经打开旧文件的描述符,也没有测试断电持久性或 HTTP 路由。

后果

  • 线上没有复制到一半的树: 选中前每个发布都已完整。不要修改已选中或留存的发布目录。
  • 可命名的回滚: 创建指向已记录上一版目标的新临时链接,再替换 current。回滚需要执行和验证,冒烟测试失败不会自动完成它。
  • 切换前证据: 检查暂存清单、所有权、可读权限与剩余空间。清单放在被哈希树之外。
  • 发布串行化: 同时只允许一个发布者。固定临时链接名适用于此隔离练习;生产环境需要协调与防碰撞的临时名称。
  • 存储取舍: 为回滚与进行中的客户端保留旧版,经过留存窗口和检查后才清理。

原子指针替换不是覆盖一个浏览器页面及其所有资源请求的事务。浏览器可能在切换前取得旧 HTML、切换后才请求资源。带内容指纹的资源 URL 只有在新服务树或独立留存资源库仍能通过旧 URL 提供旧字节时才有用。只是把旧发布目录留在磁盘别处并不够。发布和回滚后,缓存也可能继续持有旧响应。

临时链接与 current 必须位于同一文件系统,共用父目录是方便的保证方式。还须检查文件系统及服务器真实的符号链接行为。rename 的原子性描述并发可见性,不等于突然断电后的持久性;更强的持久性需要与文件系统匹配的文件和目录同步。服务根不是沙箱,应拒绝发布内容中意外出现的符号链接。

为什么它胜过就地同步

发布可以命名、在暴露前验证、通过一次指针操作选中,并从已知目标恢复。这些保证有实际价值,无须承诺全球瞬时更新、跨请求快照隔离,或每次失败后的自动恢复。

来源

  • Linux man-pages:rename(2),原子替换、非空目录、符号链接与已打开描述符,已核验 2026-10-15。
  • Python os.replace,成功替换的原子性及非空目录、跨文件系统限制,已核验 2026-10-15。
  • Caddy file_server,根解析与符号链接安全边界,已核验 2026-10-15。