🛠️ 第12章 场景四:PR、Review 与 CI/CD 自动化
📄 一句话概括

无人值守不是「不用人」,是「没人的时候它也不会走错」。 四个前提:可重复的干净环境、权限刚好、失败能收尸、有超时。 四个里最便宜的是超时——它只是一行参数;最容易漏的也是它。

📝 本章和前面几章的关系

前三个场景都是「有人在旁边看着」。这一章把人也拿掉。 一旦没人看,之前所有「反正我会发现」的东西都会失效。 为什么四个前提里没有「模型要够强」? 因为它不是前提,它是变量。 模型会越来越好,但环境脏、权限大、失败不收尸、没超时这四件事,换多强的模型都不会自己变好。 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 秒实验

  1. 找一个你的定时任务,连跑两次,看输出一样不一样。 不一样的话,找到它记住了什么。
  2. 查一下你的自动化脚本能写哪些目录。 列出「它能写但不需要写」的那些,收掉。
  3. 给你的每一个自动化任务加一个 timeout。 加完再想一遍:卡住的时候,谁能发现。

常见误区

  1. 「无人值守就是把人的操作写成脚本。」 只写了一半。 人的另一半作用是「出问题时补救」,那一半也得写进去。
  2. 「权限给足,省得中途报错。」 中途报错你能看见,悄悄改错源码你看不见。 有人值守可以给足,无人值守必须给刚够。
  3. 「超时时间调大一点更安全。」 调大只是把「卡住」的发现时间往后推。 超时的作用是给「没有尽头」设一个上限,不是让它跑得更久。
自测三问(点开对答案) 1. 四个前提里为什么没有「模型够强」? —— **因为它是变量不是前提。** **模型变强不会让环境变干净、权限变小、失败能自愈。** 2. 收尸的两件事是什么?哪件更容易漏? —— **失败时清中间状态、启动时检查残留。** **第二件更容易漏,而它是唯一能自愈的那件。** 3. 超时拦的到底是什么? —— **拦的是「没有尽头」,不是「慢」。** **没有上限的任务在无人值守里就是个黑洞。**

下一篇具体到 CI:构建失败之后,哪些能自动修,哪些必须交给人。

相关笔记