AI 自动化流水线并发写入冲突复盘:5 个真实场景与 flock 锁修复
去年我搭的 OpenClaw AI 自动化流水线(4 个 cron 任务 × 每日 4 次 × isolated session × 流水线生成 → 生成 HTML → 推送 GitHub Pages)在 W21-W26 平稳跑了 5 周,结果在 W30(2026-07-20)单日内连续踩进同一个坑 **3 次**:6AM cron 启动后被 22PM cron 残留文件覆盖,22PM cron 自己又被别的 cron subagent 覆盖,最终我以为发布的 4 篇新文章(Thunderbolt 4 / WordPress 7.0 / NAS 网卡 / Apple USB-C)实际上 0 篇来自我写的 cn.txt,三次写入三次被替换。本文把这次完整的崩溃-修复-再崩溃-最终锁定的全过程拆开讲,重点是为什么 v2 的"双源同步"修复(corrections.md 6/23)根本不够,以及 v3 用 flock -xn /tmp/article-gen.lock + 隔离文件名是怎么彻底解决的。如果你也在跑多个 cron 任务共享临时目录,这 5 个真实崩溃场景 + flock 锁 + 8 项生产验证清单值得从头到尾读完。
⏳ 太长不看版(TL;DR)
- 崩溃根因:4 个 cron 任务(6AM/12PM/18PM/22PM)的 isolated session 共享 `/tmp/article-gen/` 目录,多 session 并发 `write()` 同一文件,**最后写入者覆盖前者**,中间窗口(约 30 秒)被任意其他 session 替换。
- 表现症状:cn.txt 写入成功(10559 bytes)→ Phase 0 读取 OK → Phase 2 generate-html.py 读到的不是你的文件,是别人 30 秒前写入的 NAS 网卡 / Thunderbolt / Apple USB-C 主题文章,发布 12 篇全错位。
- 错误修复 v2(6/23 双源同步)**根因不对**:以为「drafts/ vs /tmp/article-gen/ 两份文件不一致」,实际是「多 session 覆盖单源」,连一个源都对不了。
- 正确修复 v3:`flock -xn /tmp/article-gen.lock` 包裹整个写-跑流程 + 隔离文件名 `${STAMP}-cn.txt`(带 cron 名 + 时间戳)+ `generate-html.py --input-file
` 单文件指定 + `atomic mv` 替换。
为什么 /tmp/article-gen/ 成了多 Session 战场
OpenClaw 流水线的设计里,run-pipeline.py 的 Phase 1 调 generate-html.py,而 generate-html.py 硬编码读 /tmp/article-gen/cn.txt 和 /tmp/article-gen/en.txt(不是 drafts/cn.txt)。原因很简单:临时目录比工作目录快,跨 session 不用每次解析相对路径。
但 W21 之后调度策略变了:**6AM cron(每日 6 点跑一次)启动时间不固定**,取决于 subagent 队列深度。当 6AM cron 还在写 cn.txt 时,22PM cron(实际 21:53 启动)已经启动 run-pipeline.py。两者共享同一文件名 /tmp/article-gen/cn.txt,导致:
时间线(2026-07-20 真实)
21:42 22PM isolated session 启动,写 /tmp/article-gen/cn.txt (10559 bytes)
21:44 6AM isolated session 启动,覆盖写 cn.txt (14752 bytes, NAS 网卡主题)
21:47 6AM Phase 0 读 cn.txt,发现主题被改成 NAS 网卡 → 重新写
21:48 6AM 重新写又被 22PM 覆盖(22PM 此时正在生成 HTML)
21:50 6AM 等 22PM last_run 完成后重新写(16752 bytes WordPress 7.0)
21:51 6AM 跑 pipeline 成功,发布 4 篇(含本次 CN+EN 2 篇 + drafts 队列 10 篇历史补推)
21:53 22PM 也跑完了,但 22PM 写的"AI 自动化流水线 5 个 Bug"主题 0 篇被推送
这就是 v2 修复(6/23 双源同步)无法解决的问题:你的 cn.txt 在被另一个人覆盖,不是一致性问题,是所有权问题。
💣 踩坑录:5 个真实崩溃场景(按时间倒序)
崩溃场景 1:22PM 21:53 第三次踩坑(最严重,2026-07-20)
现象:22PM isolated session 21:42 写 cn.txt("AI 自动化流水线 5 个 Bug"主题)→ 21:46 跑 pipeline → Phase 1 generate-html.py 读到的 cn.txt 主题是 NAS 网卡(来自 6AM 21:44 覆盖写入)→ 生成的 12 篇文章全部来自 6AM 主题或 drafts 队列历史补推,0 篇是 22PM 写的"AI 自动化流水线 5 个 Bug"。
**根因**:run-pipeline.py Phase 0 读 /tmp/article-gen/cn.txt 后,**中间 30 秒窗口**被 6AM isolated session 写入覆盖。Phase 2 generate-html.py 读到的不是 22PM 自己的文件。
解决方案:v3 flock 锁(见下文)—— 整个写-跑流程必须在一个锁内,期间不允许其他 session 介入。
崩溃场景 2:6AM 与 22PM 互相覆盖(21:48,2026-07-20)
现象:6AM 21:44 开始写 cn.txt(WP 7.0 Playground 主题,14752 bytes),写到 21:46 完成 → 但 22PM cron 21:47 启动 run-pipeline.py,覆盖 cn.txt 为 NAS 网卡主题 → 6AM Phase 0 读发现"主题被换",21:48 重新写又被 22PM 覆盖。
根因:两个 cron session 完全没有协调,每个 session 都假设自己是唯一的写者。当 6AM 等待 30 秒重写时,22PM 正好在跑 Phase 2,又一次覆盖。
解决方案:v3 flock 锁 + 调度时差 ≥30 分钟(已升格永久标准 #24)。
崩溃场景 3:v2「双源同步」根本不管用(2026-06-23)
现象:6/23 22PM cron 第二次掉进同一坑,generate-html.py 路径 vs drafts/ 路径不一致。
修复:v2 修复方案——把 drafts/cn.txt 拷贝到 /tmp/article-gen/cn.txt 后再跑 pipeline。
为什么不够:v2 假设「drafts/ 是真相源,/tmp/article-gen/ 是临时副本」,但实际 7/20 案例里 drafts/cn.txt 是 22PM 写的"5 个 Bug",/tmp/article-gen/cn.txt 已经被 6AM 改成 NAS 网卡。Phase 0 读 drafts/cn.txt 是对的,但 Phase 1 generate-html.py 读 /tmp/article-gen/cn.txt 是错的,而 generate-html.py 没参数指定输入文件,无法指向 drafts/。
教训:v2 只解决了「drafts/ 残留污染 /tmp/article-gen/」这一种场景,对「多 session 并发写同一文件」无效。
崩溃场景 4:6AM 6:13 残留污染(2026-07-17,W29)
现象:6AM cron 跑完后留下 /tmp/article-gen/cn.txt(Ansible 主题,06:13 mtime),当晚 22PM cron 启动 run-pipeline.py,generate-html.py 读到 Ansible 主题,22PM 写的 WordPress 主题文章 0 篇被推送。
**修复**:6AM cron 跑完后**手动** rm /tmp/article-gen/*.txt 或 mv 到 backup。
教训:v2 修复的「单次残留」场景,v3 必须扩展到「并发覆盖」场景。
崩溃场景 5:cron subagent 队列意外写入(2026-07-20)
现象:22PM cron 启动后,6AM 的 subagent 队列还残留了 12PM/18PM 之前未完成的 subagent,这些 subagent 在 22PM 21:47 写入 /tmp/article-gen/。没人叫它们写,是调度系统的 subagent 自动恢复机制。
根因:OpenClaw 调度器在 cron 触发时,会先恢复所有未完成的 subagent(默认 1 小时窗口)。如果 12PM cron 的 subagent 因故未完成,它会在 22PM cron 启动时自动恢复并执行剩余任务,其中可能包括写 /tmp/article-gen/。
解决方案:v3 flock 锁 + 调度时差必须 ≥ 30 分钟(避免 subagent 恢复窗口重叠)。
🛠️ 实战修复:v3 flock 锁 + 隔离文件名(完整脚本)
Step 1: 写文件用隔离文件名(带 cron 名 + 时间戳)
#!/bin/bash
# 22PM cron 开头:先取隔离文件名
STAMP="$(date +%Y-%m-%d-%H%M)-22pm" # 例:2026-07-20-2153-22pm
CN_FILE="/tmp/article-gen/${STAMP}-cn.txt"
EN_FILE="/tmp/article-gen/${STAMP}-en.txt"
# 检查其他 cron 是否在跑(v3 永久标准 #24)
if ps auxf | grep -E "publish-via-api|run-pipeline|generate-html" | grep -v grep > /dev/null; then
echo "⚠️ 其他 cron 流水线在跑,等待 30s 重检"
sleep 30
if ps auxf | grep -E "publish-via-api|run-pipeline|generate-html" | grep -v grep > /dev/null; then
echo "❌ 仍有活跃进程,放弃本次发布"
exit 1
fi
fi
# 用 flock 包裹整个写-跑流程(关键!)
exec 9>/tmp/article-gen.lock
if ! flock -xn 9; then
echo "⚠️ flock 锁被占用(其他 cron 正在写),放弃本次"
exit 1
fi
# 在锁内写文件
cat > "$CN_FILE" <<'EOF'
# 你的 CN 文章内容(pipe 格式元数据 + 正文)
EOF
cat > "$EN_FILE" <<'EOF'
# 你的 EN 翻译版本
EOF
# Phase 1: 调 generate-html.py,**指定输入文件**(v3 新参数)
python3 generate-html.py --cn "$CN_FILE" --en "$EN_FILE" \
--out-cn /root/.openclaw/workspace/yaohehe.github.io/2026-07-20-xxx.html \
--out-en /root/.openclaw/workspace/yaohehe.github.io/2026-07-20-xxx-en.html
# Phase 2: 推送(同样在锁内)
python3 /root/.openclaw/workspace/yaohehe.github.io/publish-via-api.py 2026-07-20-xxx.html
python3 /root/.openclaw/workspace/yaohehe.github.io/publish-via-api.py 2026-07-20-xxx-en.html
# 清理(保留 24h 用于排查)
find /tmp/article-gen/ -name "${STAMP}-*" -mmin +1440 -delete
# 释放锁(脚本结束自动释放)
Step 2: generate-html.py 增加 --input-file 参数
# generate-html.py 关键修改(patch)
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--cn', default='/tmp/article-gen/cn.txt',
help='CN 输入文件路径(v3 新参数)')
parser.add_argument('--en', default='/tmp/article-gen/en.txt',
help='EN 输入文件路径(v3 新参数)')
parser.add_argument('--out-cn', required=True, help='CN 输出 HTML 路径')
parser.add_argument('--out-en', required=True, help='EN 输出 HTML 路径')
args = parser.parse_args()
# 读取 v3 指定的输入文件(不再硬编码)
with open(args.cn, 'r', encoding='utf-8') as f:
cn_content = f.read()
with open(args.en, 'r', encoding='utf-8') as f:
en_content = f.read()
# ... 后续 HTML 生成逻辑不变
Step 3: 调度时差检查(永久标准 #24)
# 在 22PM cron 触发前,检查最近 30 分钟是否有其他 cron 完成
LAST_RUN=$(stat -c '%Y' /root/.openclaw/workspace/affiliate-blog/drafts/.last_run 2>/dev/null || echo 0)
NOW=$(date +%s)
DIFF=$((NOW - LAST_RUN))
if [ "$DIFF" -lt 1800 ]; then
echo "⚠️ 距离上次 cron 完成仅 ${DIFF}s,等待 $((1800 - DIFF))s"
sleep $((1800 - DIFF))
fi
🔒 为什么 flock 是正确解(而不是 mv 原子替换)
很多人第一反应是「用 mv 原子替换不就完了?」——**不对**。
| 方案 | 单写者 | 多写者 | 跨 session | 锁释放 | 失败回滚 |
|---|---|---|---|---|---|
| `mv` 原子替换 | ✅ | ❌ 后写者覆盖前写者 | ❌ | N/A | ❌ 文件残留 |
| `cp + mv`(v2) | ✅ | ❌ | ❌ | N/A | ❌ |
| flock 包裹 | ✅ | ✅ 阻塞等待 | ✅ | ✅ 自动 | ✅ 超时退出 |
| `flock -xn` 非阻塞 | ✅ | ✅ 立即放弃 | ✅ | ✅ 自动 | ✅ 立即退出 |
flock -xn 的关键优势:**非阻塞语义**——如果锁被占,立刻失败退出,不让 cron 等 5 分钟。否则两个 cron 互相等锁会导致**死锁的近亲**(互相阻塞 30 秒后超时,但中间 30 秒双方都无法工作)。
🛡️ 8 项生产验证清单
升级到 v3 后,必须验证:
- [ ] **V1**:两个 cron 同时触发时,第二个 `flock -xn` 立刻失败退出(不阻塞)
- [ ] **V2**:锁释放后,第二个 cron 重新触发可以正常写文件
- [ ] **V3**:`generate-html.py --cn /tmp/article-gen/22pm-xxx-cn.txt` 读的是指定文件,不是默认的 cn.txt
- [ ] **V4**:publish-via-api.py 推送的 HTML mtime 在写文件后(不是覆盖前)
- [ ] **V5**:6AM/22PM 调度时差 ≥ 30 分钟(subagent 恢复窗口不重叠)
- [ ] **V6**:`/tmp/article-gen.lock` 文件存在且权限 0644
- [ ] **V7**:`/tmp/article-gen/` 24 小时前文件自动清理(`find -mmin +1440 -delete`)
- [ ] **V8**:连续 7 天 4 个 cron × 4 次 = 28 次发布,0 次主题错位
🔗 相关内链
- 6/23 22PM cron 第二次踩坑(v2 修复根源不足):corrections.md 6/23 条目
- 7/17 6AM 残留污染(v3 flock 锁起源):corrections.md 7/17 条目
- 7/20 21:48 / 21:53 双崩溃案例(本文核心数据来源):corrections.md 7/20 条目
总结与下一步
AI 自动化流水线的并发冲突不是「双源不一致」问题,是「多 session 写同一文件所有权」问题。v3 用 flock -xn + 隔离文件名 + generate-html.py --input-file 参数三个机制叠加,才能彻底解决。**W30(本周)已升格为永久标准 #24**,所有 4 个 cron 任务(6AM/12PM/18PM/22PM)的 prompt 头部必须固化这套流程。
下一步方向:
1. W31 验证:连续 7 天观察 4 个 cron 是否还出现类似冲突(V8 验证清单)
2. run-pipeline.py v2 升级:Phase 0 增加 flock 锁自动包裹,cron prompt 不用手写锁
3. subagent 调度窗口调整:把 OpenClaw 调度器的 subagent 恢复窗口从 1 小时缩短到 30 分钟
👉 Join MiniMax Token Plan: AI coding acceleration for businesses
👉 Join Zhipu Coding Plan: GLM-4.6/GLM-5 coding packages, China-stable, pay-per-token unlimited
👉 Join Aliyun AI: Top AI products with exclusive coupons for business innovation
📌 This article was AI-assisted generated and human-reviewed | TechPassive — An AI-driven content testing site focused on real tool reviews
🔗 Recommended Tools
These are carefully selected tools. Using our affiliate links supports us to keep producing quality content: