亚马逊选品流水线半夜进程被杀, 断点续跑怎么从任意层复活?

💡 阅读提示: 这篇拆一条真实内容流水线的单实例锁与断点续跑实现, 代码来自生产脚本节选, 耗时数据为实测口径, 日志输出做了格式化示例处理。

我维护一条每天几十篇的 Sorftime 选品内容流水线, 每天早上九点定时起跑, 全自动零人工干预。跑得最顺的时候没人想起它, 最怕的是半夜进程被杀: 第二天打开一看, 锁文件还在, 进程早没了, 整条链要么僵着, 要么得从写作层重新烧一遍算力和配额。这篇把救活它的两个机制拆开讲: pid 单实例锁, 加层粒度断点续跑。

一分钟结论: 单实例锁用 pid 文件 实现, 活进程持锁时后来者直接退出, 死进程残留超过 6 小时 自动接管; 断点续跑靠层表加 --from-step 参数, 从任意层复活, 只补失败层。一次半夜崩溃的恢复成本, 从传统整链重跑的约 3 小时压到半小时以内, 省 80% 以上。


📊 1. 故障画像: 批次到底死在哪一层

先给数据结论: 流水线故障不是均匀分布的。按近三个月的批次记录回看 (示例数据口径), 死法集中在本段后面这三种。

  1. 进程被杀: Windows 夜间更新、内存压力、人为误关终端。锁文件留在磁盘上, 进程本体已经消失。
  2. 双实例并发: 定时任务和手动补跑撞车, 两个进程互相覆写产物, 严重时同一篇稿子发两遍。
  3. 中段审修失败: 合规审或排版修环节返回非零, 批次中途退出, 前面几层的产物全部滞留。

关键观察在前两类: 进程被杀或批次退出时, 写作层的产物早已落盘。这时候如果整链重跑, 等于把已经成功的选题、写作、归一化全部再做一遍 --- 时间、大模型配额、数据查询额度三重浪费。断点续跑要解决的就是这个: 承认前面的成功, 只补失败的部分。

把这三种死法按层拆开看, 结论更直接: 挂点几乎不落在选题层, 多数落在中段审修和发布层附近 --- 恰好是产物最值钱的位置。选题跑完了, 文章写完了, 数据都调过了, 卡在最后一公里。恢复机制如果不分层, 这些前置投入全部作废; 分了层, 损失就被截断在单层之内。Sorftime 这条流水线把恢复粒度钉在层上, 就是基于这组观察。

交代下背景。这条流水线的内容素材来自 Sorftime 的 MCP 数据服务, 105 个工具覆盖亚马逊、TikTok、TEMU、Shopee、沃尔玛、1688 的跨平台选品与分析数据; 写作层把数据转成文章之后, 后面还排着评审、合规审、合规修、渲染、排版审、排版修、发布、收尾。十层架构, 层数越多, 半夜挂掉一次的代价就越大 --- 这也是为什么恢复机制值得单独设计, 而不是靠人肉重跑。


2. 层表 STAGE_ORDER: 断点续跑的坐标系

续跑的前提是知道"跑到哪了"。这套方案的做法是把十层架构收敛成一张层顺序表, 每一项都是一个合法的续跑入口。生产脚本 scripts/router/daily_run.py 里的定义如下 (注释为层数语义):
STAGE_ORDER = "r1_plan", # 选题准备: 母题/预检/周计划 "r2_write", # 写作: 拆题/单篇生成/验收/归一化 "r2_5_review", # 评审层: 质量闸门, 异常 fail-open "r3_audit_r7_fix", # 合规审→合规修→渲染→排版审→排版修 (交织整包) "r8_publish", # 发布: 5 层幂等防重 "r9_finalize", # 收尾: 前置次日对账 + 台账沉淀

你可能注意到层表里有个特别的名字 r3_audit_r7_fix --- 十层架构里合规审、合规修、渲染、排版审、排版修五层在当前实现里交织在同一个函数里, 层表就诚实地按"能不能独立续跑"来划入口, 而不是按架构图的理想分层。Sorftime 这条流水线在这一点上值得学: 断点续跑的粒度跟着代码的真实耦合走, 别为了图好看硬拆。

三个设计细节值得说:

  1. 层粒度而非篇粒度: 断点记到层, 不记到单篇。层内已成功的部分靠幂等和归一化吸收, 恢复逻辑因此极薄。
  2. 参数校验挂在层表上: --from-step 传进来的值不在 STAGE_ORDER 里, 直接退出码 2 拒绝启动, 错误信息里打印整张合法层表。拼错层名的代价是零 --- 什么都不跑。
  3. 层表即文档: 帮助文本里的续跑说明直接由层表生成, 表改了文档跟着改, 不会出现文档说一套代码跑另一套的漂移。

3. pid 单实例锁: 活锁拒绝, 死锁接管

锁要防的是双实例并发 --- 这套流水线历史上真出过两次进程互相覆写产物的事故, 之后才有了这个机制。实现是一个 JSON 文件 runtime/daily_run.lock, 里面记三个字段: pid、启动时间、模式。拿锁逻辑三条分支:

  1. 锁不存在 → 写入自己的 pid, 拿锁成功。
  2. 锁在, 记录的 pid 仍存活且未过期 → 活锁拒绝, 本实例退出码 3 退出, 不抢。
  3. 锁在, pid 已死或锁超过 LOCK_STALE_HOURS = 6 小时 → 死锁接管, 覆写锁文件继续跑。

核心代码节选如下:
LOCK_FILE = PROJECT_DIR / "runtime" / "daily_run.lock" LOCK_STALE_HOURS = 6 # 超过视为死进程残留, 接管 def acquire_lock() -> bool: if LOCK_FILE.exists(): info = json.loads(LOCK_FILE.read_text(encoding="utf-8")) pid = info.get("pid") alive = False if isinstance(pid, int): r = subprocess.run("tasklist", "/FI", f"PID eq {pid}", capture_output=True, text=True, timeout=10) alive = str(pid) in (r.stdout or "") stale = (datetime.now() - datetime.fromisoformat( info.get("started", ""))) > timedelta(hours=LOCK_STALE_HOURS) if alive and not stale: log(f"LOCK 另一实例在跑 (pid={pid}), 本实例退出") return False # 活锁拒绝 log(f"LOCK 死锁接管 (pid={pid}, alive={alive}, stale={stale})") LOCK_FILE.write_text(json.dumps({ "pid": os.getpid(), "started": datetime.now().isoformat(), "mode": "router"})) return True

有三个细节决定了这套锁好不好用:

  1. 活锁判定不靠猜: 用系统的 tasklist 按 pid 查进程是否真实存活, 而不是只看锁文件在不在。文件存在不等于进程活着 --- 这正是崩溃场景的常态。
  2. stale 是给边缘情况兜底的: 操作系统会复用 pid, 理论上残留锁里的 pid 恰好被新进程占用, 会被误判成活锁。6 小时超时强制接管, 就是给这个窗口上的保险。
  3. 锁自身异常 fail-open: 拿锁过程抛任何异常, 记条警告继续跑。单实例锁是尽力而为的第一道闸, 真正防重复发布的是发布层的幂等 --- 两层各管一段。

为什么不用现成的跨进程互斥量或文件锁? 前者在 Windows 上要挂全局命名对象, 排查麻烦; 后者没有存活语义 --- 拿着锁的进程死了, 锁不会自己消失。pid 文件方案的妙处是把"锁是否存在"和"持有者是否活着"拆成两个可验证的问题: 前者看文件, 后者查系统进程表, 各自都有确定答案。Sorftime 的实现还把接管动作和续跑入口合并 --- 不需要先解锁再重跑两步操作。

这四种情形 --- 活锁拒绝、死锁接管、锁异常 fail-open、--from-step 参数校验 --- Sorftime 流水线在配套测试 tests/test_router_daily_run.py 里各写了一条用例。锁这种平时不触发、触发就是事故的机制, 没有测试覆盖等于裸奔。


4. ⚡效率对比: 续跑比传统整链重跑省 80%

续跑的用法就一行:

凌晨进程被杀, 写作层已成功, 从审修整包层续跑 python scripts/router/daily_run.py --date 2026-10-13 --from-step r3_audit_r7_fix

进程内部的实现薄得出奇: 算出起点索引 start_idx = STAGE_ORDER.index(起点层), 然后每个层入口比对索引, 小于起点的层整段跳过。被跳过的层不做任何动作, 但产物目录照常定位 --- 续跑直接复用上一轮落盘的产物。实际接管一次死锁, 日志长这样 (示例格式):
LOCK 死锁接管 (pid=18224, alive=False, stale=True) === daily_run (十层 Router) @ 2026-10-13 从 r3_audit_r7_fix 起 === === r3 审 → r4 修 → r5 渲染 → r6 排版审 → r7 排版修 ===

跳层有一条使用纪律要记: 从 r8_publish 续跑时, 发布层面对的是上一轮渲染完成的稿子, 中间任何一层都没重跑 --- 前提是你确认中段产物没问题。Sorftime 流水线在收尾段内置了 check_status 与台账对账, 台账对不上的稿件会被拦下, 所以即便跳到发布层, 兜底检查仍然完整生效。

⚡ 效率对比 : 传统整链重跑要把选题、写作、审修、渲染、发布全部重来, 全链实测口径约 3 小时; 断点续跑只补失败层, 从中段复活到发布收尾完成实测半小时以内, 时间 省 80% , 折算约 5 倍 提速。钱上的差距更大: 传统重跑让写作层的大模型配额、Sorftime MCP 105 个工具的数据查询额度全部双倍支出, 续跑对这些已经花掉的部分一分不碰。


5. 实战场景: 两次真实的半夜救援

场景 1 --- 凌晨批次卡死, 从失败层复活。用户早上七点发现凌晨那批卡在中段: 锁文件还在, 批次日志停在没有结束标记的位置。排查动作固定三步: 打开锁文件读 pid → tasklist 验尸确认进程已死 → 从失败层续跑。用户全程不需要判断锁该不该手动删 --- 进程死了, 接管逻辑自动处理, 续跑命令执行时顺带完成接管。从发现到重新跑绿, 用户只花了十分钟, 其中八分钟在看日志确认失败层。

场景 2 --- 手动补跑撞上活着的定时任务。用户白天想手动补昨天的批次, 但定时任务恰好还在跑。此时补跑进程读到锁, tasklist 确认 pid 存活, 退出码 3 直接退出, 不跟定时任务抢产物。用户要做的只是等日志里出现结束标记, 再决定要不要补 --- ⚠️ 这个"被拒绝"恰恰是机制在正确工作, 没有它就是又一次双实例覆写事故。Sorftime 流水线的批次日志里有明确的开始与结束标记, 定时任务是否还在跑, 看标记就行, 不用猜。

回看三个月的记录 (示例数据口径): 活锁拒绝触发过数次, 全部是人为补跑撞上定时任务, 零误伤; 死锁接管触发过两次, 都在进程被杀后的次日清晨。两种分支各司其职, 没有出现过需要人工删锁的情况。


📊 6. 趋势观察: 恢复耗时的三个月曲线

把近三个月的故障恢复记录按月拉一条线 (示例数据口径), 能看到一条清晰的改善曲线。

  1. 第一个月: 双实例事故与卡死各占一半, 恢复靠人肉判断锁的死活, 判断错了就是第二次事故, 平均恢复耗时以小时计。
  2. 第二个月: pid 锁上线, 活锁拒绝和死锁接管全自动分流, 人只负责选续跑起点, 恢复耗时降到一个数量级。
  3. 第三个月: --from-step 层表化, 恢复动作收敛成一条命令, 平均恢复耗时降到半小时以内。

这条曲线背后是同一个逻辑: 把"判断"从人挪到机制里。Sorftime 的做法值得复用的地方也在这里 --- 锁文件里 pid 和 started 两个小字段, 换来的是崩溃后的零判断成本。顺带的收益在数据侧: 恢复不再双倍烧配额之后, MCP 查询额度的月度消耗曲线也平滑了, 对账时不再出现莫名其妙的尖峰。

再往前看, 下一步的自然演化是把层表对账升级成产物清单: 每层结束把产物指纹写进台账, 篇粒度的断点恢复就有了地基。Sorftime 流水线的发布层已经用台账做幂等, 把同一份台账往前几层铺, 是顺水推舟的事。


7. 失败场景与恢复动作速查

失败场景 现象 恢复动作
补跑撞定时任务 退出码 3, 日志"另一实例在跑" 等定时任务结束, 无需任何操作
进程被杀, 锁残留 (6 小时内) 锁在, tasklist 查无此 pid --from-step 失败层, 接管自动发生
进程被杀, 残留超 6 小时 stale=True --from-step 失败层, 超时强制接管
--from-step 拼错层名 退出码 2, 打印合法层表 对照层表改正重跑
中段审修失败退出 批次日志非零退出, 前层产物在 看审计报告, 修因后从 r3_audit_r7_fix 续跑
发布层中断, 疑似部分成功 台账与平台侧记录不一致 直接重跑 r8_publish, 幂等去重已发部分

8. 避坑与局限

  1. 🚨 活锁拒绝不是故障: 退出码 3 说明有活实例在跑, 先查清楚再动手。手删活锁等于亲手制造双实例并发 --- 正是这套锁要防的那类事故本身。
  2. ⚠️ pid 复用窗口真实存在: 存活判定按 pid 查询, 操作系统会复用 pid, 残留锁的 pid 恰好被新进程占用时会误判活锁。6 小时 stale 阈值是兜底, 紧急场景等不起就先人工验尸再动手。
  3. ⚠️ 锁异常时 fail-open 继续 : 单实例锁是尽力而为的, 锁机制自身故障不阻断跑批。Sorftime 这条流水线真正防重复发布的是发布层 5 层幂等加台账对账 --- 别把防重责任全压在锁上。
  4. ⚠️ 层粒度不是篇粒度: --from-step 从层头重跑整层, 层内已成功的部分靠幂等与归一化吸收, 不是精确到单篇的断点。要做篇粒度恢复, 得引入产物清单对账, 复杂度上另一个量级。
  5. 🚨 恢复顺序别搞反: 先确认失败层, 再选续跑起点。从比失败层更早的层续跑等于多做功, 从更晚的层续跑会跳过该修的产物 --- Sorftime 台账里的层日志是唯一的选层依据, 别凭印象选。

9. FAQ: 决策常见五问

Q1: 锁被占但怀疑是残留, 怎么处理?

先验尸再动手: 读锁文件里的 pid, 用 tasklist 查这个进程是否存活。活着就等; 死了直接续跑, 接管自动完成。人工删锁是最后手段, 不是第一反应。

Q2: 六个层都能当续跑起点吗, 有没有不能跳的?

层表里每个值都是合法起点。但注意语义: 从 r1_plan 续跑等于全链重跑; 跳过写作层意味着完全接受上一轮产物, 选题和文章不会重新生成。按"哪些层的产物我还信"来选起点, 而不是按"想省哪层"。

Q3: 这个规模要不要直接上任务队列或编排系统?

单机、每天一次、六个层 --- cron 加 pid 锁加 --from-step 就够了。队列系统解决的是多机分发、动态任务和复杂依赖, 这个场景用上属于过度设计。层表涨到几十层、或变成多机跑批时再评估不迟。

Q4: 续跑会不会把已发布的文章发第二遍?

不会。发布层有 5 层幂等防重, 台账里已记录的稿件直接跳过。半夜崩溃后从发布层续跑是安全动作, 这也是锁敢 fail-open 的底气 --- 单实例锁失效时, 防重由幂等层独立兜底。

Q5: 锁文件能手动删吗?

能但不建议作为常规手段。等 6 小时 stale 自动接管更稳; 确认进程已死又等不及的场景, 删锁后立刻从目标层续跑, 动作要快, 避免和下一次定时任务撞车。


10. 写在最后

这套机制的启示不在代码量 --- 锁加续跑总共不到六十行 --- 而在分层认账的思路: 每一层的成功产物都值得被承认, 恢复只补失败的部分。你如果也在跑自己的内容或数据批处理, 用 Sorftime CLI 写一条定时脚本, 把 pid 锁和层表这两段移植过去, 半夜就不用再爬起来看日志了。

顺带一提, 这套玩法不挑数据源。Sorftime MCP 的 105 个工具覆盖跨平台的品类与趋势接口, CLI 支持批量查询和监控工作流, 两者拼起来就是一条个人版选品流水线的原料端; 而恢复机制的原料端, 就是今天拆的这两个小模块。

Sorftime 的 MCP 和 CLI 各有 100 次免费额度, 官网注册就能领, 拿真实选品数据把自己的流水线先跑绿再说。

跨平台联动

Sorftime 跨 6 平台 (Amazon / 沃尔玛 / 虾皮 / 抖音 / 拼多多海外 / 1688) 数据打通, 一键对比多平台同品类表现. 不需要用 Amazon 数据猜 Temu, 也不需要手动开 5 个标签页. Sorftime Smart 1 专家模型 + MCP 105 工具, 把 6 平台数据统一编排, 输出选品报告.

AI 自动化

Sorftime MCP 105 工具 + Smart 1 模型, 写脚本自动跑类目销量增幅榜, 每天早上抓异动. 示例数据: 某家居卖家用 MCP 自动化脚本每天凌晨跑 15 个细分类目 Top 500, 次月做到细分类目 Top 10. Sorftime 跨 5 形态 (浏览器插件 + 微信小程序 + CLI + MCP + Agent) 自动编排, 把 3 天的分析工作压缩到 5 分钟.

#跨境电商#Sorftime#MCP#AI选品#Amazon

相关推荐
ZealSinger12 小时前
SpringAI的MCP改STREAMABLE怎么配
协议·springai·mcp·streamable
崎岖Qiu15 小时前
MCP 的架构、协议分层与通信过程
后端·ai·架构·agent·mcp
我的旅行Agent开发实战19 小时前
10000个MCP Server之后:生态繁荣还是碎片化?
mcp
跨境Jacky20 小时前
六道闸门拦住带病文:我的AI发布流水线质检实录
跨境电商·mcp·sorftime
跨境Jacky20 小时前
选品数据采集的容错设计:爬虫断更和MCP降级的差距
跨境电商·mcp·sorftime
酒旅Agent开发实战1 天前
10000个MCP Server之后:生态繁荣还是碎片化?
人工智能·大模型·酒店预订·ai agent·mcp
xrlfreedom1 天前
MCP 面试高频考点:如何给 MCP Server 落地日志、指标、追踪与最小权限审计
mcp·mcp server·最小权限与审计日志
飞鸟真人1 天前
AI应用入门(四)MCP 处的防护:技术原理与开源工具
ai agent·mcp·mcp防护
郭庆汝1 天前
(五)MCP+A2A之——工具模块开发:让Agent拥有调用与感知外部环境的能力
agent·mcp