你的 'Atomic' 部署可能并非原子性的

发布: (2026年1月13日 GMT+8 06:13)
5 分钟阅读
原文: Dev.to

Source: Dev.to

封面图:您的“原子”部署可能并非原子

如果你使用符号链接交换来实现零停机部署,可能会写出类似下面的代码:

ln -sfn releases/20260112 current

看起来是原子的。其实并不是。

The Bug

Run that command through strace:

symlink("releases/20260112", "current") = -1 EEXIST
unlink("current") = 0                    
symlink("releases/20260112", "current") = 0

unlink 与第二个 symlink 之间,current 符号链接并不存在。负载较高时,一些请求会在这段空窗期触发,返回 ENOENT。你的“零停机时间”部署实际上导致了停机。

Linux 修复

文档完善的解决方案是:

ln -s releases/20260112 .tmp/current.$$
mv -T .tmp/current.$$ current

创建一个临时符号链接,然后使用 mv -T 原子地替换目标。-T 选项强制 mv 调用 rename(2),在 POSIX 文件系统上是原子的。

macOS 问题

BSD mv 缺少 -T 参数,并且对符号链接的处理方式不同——如果 current 指向一个目录,mv .tmp/current.$$ current 会把临时符号链接 into 那个目录中,而不是替换它。Capistrano 社区早已知晓此问题,并通过 Ruby 进行规避,但大多数工具要么在 macOS 上接受这种竞争条件,要么要求在 Linux 上进行开发。

不同的方法

一种可移植的解决方案通过使用 Python 的 os.replace() 在 Linux 和 macOS 上均可工作,该函数在所有 POSIX 系统上直接调用 rename(2)

detect_platform() {
    if mv --version 2>/dev/null | grep -q 'GNU'; then
        printf 'linux'
    else
        printf 'bsd'
    fi
}

activate_release() {
    local tmp_link=".tmp/current.$$"
    ln -s "releases/$release_id" "$tmp_link"

    if [[ "$(detect_platform)" == "linux" ]]; then
        mv -T "$tmp_link" "current"
    else
        python3 -c "import os; os.replace('$tmp_link', 'current')"
    fi
}

平台检测通过检查 GNU coreutils (mv --version) 而不是依赖 uname,从而处理诸如 macOS 上 Homebrew GNU coreutils 等边缘情况。

证明竞争条件

#!/bin/bash
mkdir -p releases/v1 releases/v2
echo "v1" > releases/v1/version
echo "v2" > releases/v2/version
ln -s releases/v1 current

# Reader loop in background
(
  for i in {1..10000}; do
    cat current/version 2>/dev/null || echo "ENOENT"
  done
) > reads.log &

reader_pid=$!

# Writer loop – rapidly swap symlink
for i in {1..1000}; do
  ln -sfn releases/v1 current
  ln -sfn releases/v2 current
done

wait $reader_pid

errors=$(grep -c ENOENT reads.log || true)
echo "Errors: $errors / 10000 reads"

在普通系统上,每次运行大约会出现 10–50 条 ENOENT 错误。使用原子方式可以消除这些错误。

The Full Script

作者将逻辑封装到一个单文件部署脚本中,提供了以下功能:

  • 原子符号链接交换 在 Linux 和 macOS 上
  • 基于目录的锁定,并具备过期 PID 检测
  • 自动回滚,在 SIGINT/SIGTERM 时触发
  • 状态机清理,能够判断是回滚还是仅删除临时文件

GitHub:

它不做的事

  • 共享目录 – 没有 Capistrano‑style 共享文件夹符号链接
  • 远程部署 – 请自行使用 ssh/rsync 包装
  • 发布修剪 – 如有需要可添加定时任务
  • 服务重启 – 使用你的 init 系统

一件事,做好了。

FAQ

为什么不直接使用 Capistrano/Deployer?
它们在 Ruby/PHP 环境中非常好用。这个脚本可以直接放入任何 CI 流水线。

为什么不使用容器?
并非所有人都在运行 Kubernetes。虚拟机、裸金属和边缘设备仍然存在。

Python 是依赖项。
Python 3 随 macOS 以及几乎所有 Linux 发行版一起提供。它和 Bash 一样无处不在。

renameat2()RENAME_EXCHANGE 怎么样?
那是 Linux 3.15+ 配合 glibc 2.28+ 的特性。它提供真正的原子交换,但不可移植。

这在 NFS 上能工作吗?
不能。rename(2) 的原子性保证在网络文件系统上不成立。

进一步阅读

Back to Blog

相关文章

阅读更多 »

你好,我是新人。

嗨!我又回到 STEM 的领域了。我也喜欢学习能源系统、科学、技术、工程和数学。其中一个项目是…

解构 TikTok 直播购物 UX

引言 在竞争激烈的市场中,基于用户生成内容(UGC)创建一个可行的平台本身就是一项困难的任务,但加入 live eCommerce……