无人值守不是「不用人」,是「没人的时候它也不会走错」。 四个前提:可重复的干净环境、权限刚好、失败能收尸、有超时。 四个里最便宜的是超时——它只是一行参数;最容易漏的也是它。
前三个场景都是「有人在旁边看着」。这一章把人也拿掉。 一旦没人看,之前所有「反正我会发现」的东西都会失效。 为什么四个前提里没有「模型要够强」? 因为它不是前提,它是变量。 模型会越来越好,但环境脏、权限大、失败不收尸、没超时这四件事,换多强的模型都不会自己变好。 Demo 把四个前提各关掉一次,看后果:同一任务跑两次结果不同、源码被悄悄改掉、 失败一次之后永远失败、任务挂住不回。 四个后果都是真跑出来的。
一、无人值守的四个前提
| 前提 | 不满足的症状 | 为什么无人时更致命 |
|---|---|---|
| 干净环境 | 同一任务跑两次结果不同 | 没人看得出第二次不对 |
| 权限刚好 | 源码被悄悄改过 | 没人拦 |
| 能收尸 | 失败一次之后永远失败 | 没人重启 |
| 有超时 | 任务挂住不回 | 没人发现它卡了 |
四行的第三列是同一个句式:没人。 有人值守时,这四个问题都会被人顺手解决—— 他看见结果不对会重跑,看见改动会问一句,看见卡住会 kill 掉。 所以「无人值守」不是把人的操作自动化了,是把人的这四种补救也自动化了。 漏掉后面这半句,自动化就只是「没人看着的手动操作」。
二、前提一:可重复的干净环境
Demo 里那个任务很简单:读一个计数器文件,加一,写回去。 同一个目录连跑两次,输出从 1 变成 2。 每次换新目录跑两次,两次都是 1。 这不是 bug,是「它记得上一次」。 而无人值守最怕的就是这一点:它不报错,只是每次跑出不同的数。 干净环境的实现方式不止一种,各有各的代价。
| 做法 | 代价 |
|---|---|
| 每次开新目录 | 要清理,磁盘会涨 |
| 每次重建容器 | 启动慢,但最彻底 |
| 跑前跑后各清一次 | 要维护一份「哪些是产出」的清单 |
第三行最实用,但它的前提是「你知道哪些是产出」。 不知道的话,清理就会误删输入,比不清还糟。
三、前提二:权限要给到刚好
Demo 里那个任务本该只写 work/,但拿到了全仓写权限。
于是它顺手把 src/app.py 里的版本号从 1 改成了 2。
一行,不报错,事后也没人知道是谁改的。
这条原则在有人值守和无人值守时是相反的。
有人在场时,「给够了好办事」——他改错了你当场能看见;
没人在场时,「给多了没人拦」——它改错了要到下次才知道。
「给到刚好」的具体做法:按目录分,不按操作系统用户分。
因为按用户分的话,最后还是给了一个「什么都能干」的账号。
而按目录分,你能一句话说清「它能写哪几处」。
顺带一个反直觉的点:只读权限比可写权限更有用。
任务要理解代码,读全仓是对的;要改代码,只改一处就够了。
这两件事分开授权,能力不减,风险砍掉一大半。
四、前提三和四:收尸与超时
Demo 里那个任务在中途抛异常,留下一个 job.lock。
第二次启动时它看见锁就退出,退出码 3;第三次、第四次都一样。
一次失败,把自己永久锁死了。
收尸要做两件事,第二件常被忘。
失败时清掉自己的中间状态——这一件大家都会想到;
下次启动时先检查有没有残留——这一件是唯一能自愈的。
超时那一段更有意思。
Demo 里的脚本要睡四秒,设一秒超时之后,一秒左右就被掐断,返回超时标记。
不设的话它就老老实实等四秒。
四秒不长——但如果某个版本里它变成了死循环,就是四小时。
所以超时的意义不是「防止慢」,是「让卡住这件事有个上限」。
没有上限的任务,在无人值守里等于一个黑洞——
它不产出,也不释放资源,而且日志停在最后一行,看不出发生了什么。
四个前提里,超时的成本最低,收益最直接。
如果只能先补一个,先补它。
五、动手 Demo:无人值守的四个前提:关掉一个就看后果
四个小任务,每个只违反一个前提,各跑一遍看后果。
计数任务验环境,写文件任务验权限,抛异常任务验收尸,睡四秒任务验超时。
四个都是真跑:真子进程、真文件、真超时。超时那一项用的是一秒的 timeout 参数。
# 前提一:同一个任务,连跑两次
c1, o1 = run(root, [PY, "job.py"]) # run 1
c2, o2 = run(root, [PY, "job.py"]) # run 2 <- 结果变了
# 前提四:设一秒超时
t0 = time.perf_counter()
try:
subprocess.run([PY, "slow.py"], timeout=1)
except subprocess.TimeoutExpired:
print("掐断,用时 %.0f 毫秒" % ((time.perf_counter() - t0) * 1000))
实跑输出:
--------------------------------------------------------------------
无人值守的四个前提:关掉一个就看后果
--------------------------------------------------------------------
无人值守不是「不用人」,是「没人的时候它也不会走错」。四个前提,缺一个就出问题。
① 前提一:可重复的干净环境
--------------------------------------------------------------------
不清场连跑两次:run 1 / run 2。
| 跑法 | 第一次 | 第二次 | 可重复吗 |
|---|---|---|---|
| 同目录连跑 | run 1 | run 2 | 不 |
| 每次清场 | run 1 | run 1 | 是 |
! 「第二次结果不一样」是无人值守最典型的坏味道。它不报错,只是每次跑出不同的数,而你不在场。
② 前提二:权限给到刚好
--------------------------------------------------------------------
任务跑完:src/app.py 从 'VERSION = 1' 变成 'VERSION = 2'。越权了
| 权限 | 能做什么 | 最坏结果 |
|---|---|---|
| 全仓可写 | 什么都能改 | 悄悄改掉源码 |
| 只写 work/ | 只产出自己的东西 | 改不动别的 |
| 只读 + 特定目录 | 看全部,写一处 | 最坏也只是产出错 |
无人值守的权限原则跟有人值守相反:有人在场时「给够了好办事」,没人在场时「给多了没人拦」。
③ 前提三:失败要能自己收尸
--------------------------------------------------------------------
| 第几次 | 退出码 | 输出 |
|---|---|---|
| 1 | 1 | RuntimeError: 中途炸了 |
| 2 | 3 | 上次没跑完,锁还在,退出 |
| 3 | 3 | 上次没跑完,锁还在,退出 |
第二次开始一直退出码 3——一次失败把它自己锁死了。
! 收尸要做的事只有两件:失败时清掉自己的中间状态,下次启动时先检查有没有残留。
第二件常被忘,而它是唯一能自愈的那件。
④ 前提四:必须有超时
--------------------------------------------------------------------
设一秒超时:1019 毫秒后被掐断,返回我约定的超时标记(-9)。
没超时的话它会跑满四秒。四秒不长,但无人值守的任务里,一次卡死就是一个通宵。
| 设了超时 | 没设超时 |
|---|---|
| 一秒后拿到退出码 | 一直等 |
| 能重试 | 不知道该不该重试 |
| 日志里有记录 | 日志停在最后一行 |
! 四个前提里,超时最便宜也最容易漏——它只是一行参数。
⑤ 四个前提的自查
--------------------------------------------------------------------
| 前提 | 不满足的症状 | 最小的补法 |
|---|---|---|
| 干净环境 | 同一任务跑两次结果不同 | 每次开新目录 |
| 权限刚好 | 源码被悄悄改过 | 只给产出目录写权限 |
| 能收尸 | 失败一次之后永远失败 | 启动先清残留 |
| 有超时 | 任务挂住不回 | 加一个 timeout 参数 |
四行的「最小的补法」都是一两行代码。
它们的成本低得不成比例,而漏掉的代价是「你的自动化不能真的无人」。
第一个场景那张表两行就够了:同目录连跑是 1 和 2,每次清场都是 1。
差别不在输出,在「你能不能说清第二次为什么是 2」。
说不清,就是环境不干净。
权限那一段的输出是一行:VERSION = 1 变成 VERSION = 2。
这行输出比后面所有表格都重要——
它演示的是「一次无人值守的任务,悄悄改了源码,而没有任何东西报错」。
收尸那三行退出码要连起来看:1、3、3。
第一次是真失败,后两次是「因为上次失败所以不能跑」。
这种状态最麻烦:它看起来像「任务正常退出」,因为你设的告警多半只盯退出码 1。
超时那一项量的是「一秒左右被掐断」。
注意这里没有说「任务失败」——它被掐断,但任务本身没写错任何东西。
超时拦的不是错误,是「没有尽头」。
30 秒实验
- 找一个你的定时任务,连跑两次,看输出一样不一样。 不一样的话,找到它记住了什么。
- 查一下你的自动化脚本能写哪些目录。 列出「它能写但不需要写」的那些,收掉。
- 给你的每一个自动化任务加一个 timeout。 加完再想一遍:卡住的时候,谁能发现。
常见误区
- 「无人值守就是把人的操作写成脚本。」 只写了一半。 人的另一半作用是「出问题时补救」,那一半也得写进去。
- 「权限给足,省得中途报错。」 中途报错你能看见,悄悄改错源码你看不见。 有人值守可以给足,无人值守必须给刚够。
- 「超时时间调大一点更安全。」 调大只是把「卡住」的发现时间往后推。 超时的作用是给「没有尽头」设一个上限,不是让它跑得更久。
自测三问(点开对答案)
1. 四个前提里为什么没有「模型够强」? —— **因为它是变量不是前提。** **模型变强不会让环境变干净、权限变小、失败能自愈。** 2. 收尸的两件事是什么?哪件更容易漏? —— **失败时清中间状态、启动时检查残留。** **第二件更容易漏,而它是唯一能自愈的那件。** 3. 超时拦的到底是什么? —— **拦的是「没有尽头」,不是「慢」。** **没有上限的任务在无人值守里就是个黑洞。**下一篇具体到 CI:构建失败之后,哪些能自动修,哪些必须交给人。
- ☐ (本篇完,下一篇 12.2 CI 自修复:从构建失败到自动提交 PR)