脚本幂等性:跑两次不应该出问题

./deploy.sh 第一次成功,第二次把目录删了,第三次开始报权限错误。这种脚本不是自动化,只是把手工事故改成了批量事故。

幂等的意思是:同一个脚本重复执行,结果应该稳定,不该越跑越歪。对脚本来说,这不是理论问题,而是“失败后能不能安全重跑”。

很多自动化脚本看起来能用,问题往往出在这里:能跑一次不难,能安全重跑才难。尤其是部署、初始化、同步、清理这类脚本,最怕半途失败后不知道该不该重试。

先看什么叫“不幂等”

最常见的几类:

1. 重复追加

1
echo "export PATH=/opt/bin:$PATH" >> ~/.bashrc

跑一次还行,跑十次就多十行。

更稳一点的写法是先检查再写:

1
2
grep -qxF 'export PATH=/opt/bin:$PATH' ~/.bashrc || \
echo 'export PATH=/opt/bin:$PATH' >> ~/.bashrc

有就别加,没有再加。

2. 重复创建

1
mkdir /data/app

目录已经存在就报错。通常应该写成:

1
mkdir -p /data/app

-p 的意思是:存在也算成功。

3. 重复删除后依赖失效

1
2
rm -rf /data/cache
rm /data/cache/index

第一行已经删完了,第二行自然报错。更麻烦的是,脚本作者以为是“第二步失败”,实际是“第一步做得太狠”。

幂等脚本的一个核心习惯:先判断状态,再决定动作

别假设环境是干净的,也别假设上次一定成功。脚本要面对的是未知中间态。

比如安装二进制文件,不要每次都覆盖:

1
2
3
if [ ! -x /usr/local/bin/mytool ]; then
install -m 755 ./mytool /usr/local/bin/mytool
fi

如果确实要更新,就比较版本或校验值,而不是直接复制。

1
2
3
if ! cmp -s ./mytool /usr/local/bin/mytool; then
install -m 755 ./mytool /usr/local/bin/mytool
fi

这里的重点是:脚本不是“执行动作列表”,而是“把系统推到目标状态”。

比起“做了什么”,更该写“最后应当是什么样”

这两段看起来很像,但可靠性差很多。

面向动作

1
2
3
4
useradd app
mkdir /srv/app
cp config.yml /srv/app/
systemctl start app

面向状态

1
id app >/dev/null 2>&1 || useradd -r -s /usr/sbin/nologin app

mkdir -p /srv/app

if ! cmp -s ./config.yml /srv/app/config.yml 2>/dev/null; then
install -m 644 ./config.yml /srv/app/config.yml
fi

systemctl enable –now app

后者的意思是:用户存在、目录存在、配置一致、服务已启用并运行。脚本跑第二次,不会因为“已经做过”而失败。

有副作用的步骤,要么可检测,要么可回滚

幂等最怕两种东西:远程调用和数据库修改。因为它们往往不像本地文件那样容易判断状态。

例如建表,别直接执行可能失败的 SQL,优先用“如果不存在则创建”这一类语句。不同数据库写法不同,但原则一样:把目标状态写进命令里。

如果脚本要调用 HTTP 接口,最好带上可重复请求的标识。有些系统支持请求去重键,避免重试时创建两份资源。若接口本身是否支持未验证,这一步应标记为“未验证”或改为人工确认,不要默认它天然安全。

失败处理不要只靠 set -e

很多人喜欢开头先写:

1
set -euo pipefail

这没问题,但它解决的是“尽早停”,不是“可安全重跑”。

  • set -e:命令失败就退出
  • -u:未定义变量直接报错
  • pipefail:管道里任何一步失败都算失败

它们能减少沉默出错,但不能自动让脚本幂等。下面这些问题仍然要自己处理:

  • 文件已存在怎么办
  • 服务已启动怎么办
  • 中途失败后留下一半结果怎么办
  • 第二次执行时如何识别“做到哪一步了”

一个实用写法:给步骤加“完成标记”

遇到耗时操作,或者无法轻易判断状态时,可以用标记文件。

1
2
STEP_DIR=/var/lib/myjob
mkdir -p "$STEP_DIR"

if [ ! -f “$STEP_DIR/extract.done” ]; then
tar -xf app.tar.gz -C /opt/app
touch “$STEP_DIR/extract.done”
fi

if [ ! -f “$STEP_DIR/init.done” ]; then
/opt/app/bin/init.sh
touch “$STEP_DIR/init.done”
fi

这不是万能方案,但很适合初始化脚本。重点是:标记只能在步骤成功后写入,别提前 touch

输出也要幂等:让人一眼看懂是否真的变更了

不要一律打印“success”。更好的做法是区分:

  • created
  • updated
  • skipped
  • already exists

比如:

1
2
3
4
5
6
if [ -d /data/app ]; then
echo "skipped: /data/app already exists"
else
mkdir -p /data/app
echo "created: /data/app"
fi

这样排查时,能知道脚本是“真的改了东西”,还是“只是确认状态”。

检查一个脚本是否幂等

不要只跑一次,至少做这三步:

  1. 在干净环境跑一次
  2. 原地再跑一次
  3. 故意让中间一步失败,再重跑

第三步最容易暴露问题。现实里的脚本,很少死在第一行,更常见的是跑到中途失败,然后你开始犹豫:要不要再执行一遍?

如果一段脚本需要你每次重跑前先手工清现场,那它大概率还没写完。


脚本幂等性:跑两次不应该出问题
https://ghost.kasumi.live/2026/04/12/脚本幂等性:跑两次不应该出问题/
作者
Amadeus
发布于
2026年4月12日
许可协议