面试知识库

M3 · Runbook 冷启动与自增长闭环#

简历 Bullet Point: 初期导入通用 Runbook 实现冷启动;后续 Tier 3 调查经人工确认后,由 LLM 自动从证据链中提炼前置条件与处置步骤,生成结构化 Runbook 经审核入库,持续扩充 Tier 1 覆盖率。


开场钩子#

场景#

V4 的三级递进调查引擎上线后,第一个月的统计数据很尴尬:Tier 1 的命中率是 0%——所有告警一路落到 Tier 3。

原因很简单:Tier 1 的本质是”已知故障的确定性处置”,但系统刚上线时什么故障都是”未知”的,Runbook 库是空的。这就像一个新员工入职第一天——SOP 手册再完美也得先有 SOP。于是我们面临两个问题:第一,冷启动——怎么快速填充初始 Runbook 让系统不至于一直在 Tier 3 跑满 LLM;第二,可持续增长——不能永远靠人手写 Runbook,系统要能从自己的调查经验中学习。

更有意思的是一个设计约束:Runbook 的 action_steps 里没有 command 字段——你写不出 shell 命令,只能引用资源域 REGISTRY 里登记的结构化操作。这意味着 LLM 自动生成的 Runbook 草稿,如果发明了一个 REGISTRY 里没有的操作(比如 disk.clean),会被 validate_draft 直接拒收。飞轮的油门和刹车是同一套设计。

面试官切入#

“你提到了 Runbook 自增长——LLM 自动生成的 Runbook 靠谱吗?怎么防止它生成危险操作?“


一、模块运作流程#

1.1 一句话定位#

Runbook 是 Tier 1 确定性处置的执行单元,解决”已知故障零 LLM 自愈”的问题。冷启动用种子 Runbook 覆盖高频场景,自增长飞轮让 Tier 3 的调查经验沉淀为新 Runbook,持续前移调查深度(Tier 3 → Tier 1)。

1.2 全景流程图(文字版)#

1.3 分步详解#

Step 1: Runbook 数据模型

Runbook 是一个完整的”检测-校验-处置-验证-回滚”流水线,对标 Robusta playbooks / StackStorm sensor→rule→action 的 schema:

  • detectmatch_conditions——alertname 正则(fullmatch,前缀不算)+ service 正则 + severity 白名单 + label 精确匹配,四项 AND 语义。宁漏不错——一项不匹配就不命中。
  • diagnoseprecondition_checks——只读探针,校验目标资源的当前状态是否符合执行前提(容器是不是真的停了?磁盘是不是真的满了?)。探测不到判不通过,宁可递进 Tier 2 也不瞎跑处置。
  • actionaction_steps——结构化动作序列。action_type 必须在 REGISTRY 中登记(如 container.start),resource_pattern 用占位符渲染(container://{service})。没有 command 字段——从 schema 层面写不出 shell 命令。
  • verifyverify_steps——三级门禁声明(只允许 command_success / service_health / metric_recovery)。
  • rollback:回滚步骤。为空则 reversible=False,自动降级人工审批。
  • 元数据source(seed/auto/manual)、status(active/disabled/pending_review)、cooldown_sechit_count/success_count

Step 2: 匹配逻辑

matcher.py::find_matches 从 registry 取所有 active 状态 Runbook,逐个尝试四项 AND 匹配。冷却期双实现:Redis TTL key(生产)+ 内存 dict(降级)。fail-open 设计:冷却查询失败时允许命中——后面还有 CAS + 门禁兜底。

匹配后 Tier 1 逐候选试:一条”磁盘满”告警可能同时匹配三份 Runbook(悬空镜像/停止容器/构建缓存),前置条件不过就试下一个。第一个探测到有东西可回收的候选被选中。

Step 3: 自增长三条件

三个条件的顺序不确定——人工确认可能在门禁之前也可能之后。两个触发点(accept-rca API 和 remediation_workerVERIFY_PASSED)各调一次 maybe_generate_runbook(),各自检查三条件是否全中,靠 investigations.generated_runbook_id 做幂等。

Step 4: validate_draft 校验链

LLM 通过 structured output 产出 RunbookDraft(比 Runbook 窄,不让 LLM 决定 id/source/status)。校验链:

  1. name 非空、match_conditions 非空、action_steps 非空、verify_steps 非空
  2. 每个 action_step 的 action_type 必须在 REGISTRY 中且非只读
  3. precondition_checks 必须是只读操作
  4. 通过后 draft_to_runbook 强制设 source="auto", status="pending_review"

Step 5: 运行时刹车

Tier 1 的处置提案携带 runbook_idapproval_requests.runbook_id 列)一路流到 remediation_worker。门禁失败时自动 flag_for_review() 把 Runbook 打回 pending_review——一份会把事情做错的剧本,在人工看它之前不该再命中第二次。

1.4 数据流 trace#

场景:一条 DiskUsageHigh 告警经过 Tier 1 匹配

1.5 技术选型决策表#

选了什么备选方案为什么选它什么情况下换
YAML 种子 RunbookDB 种子 / 硬编码 Python可 diff、可 code review、非开发人员可维护种子数量超过 50 个且需要动态启停时迁入 DB
正则 fullmatch通配符 / 精确匹配fullmatch 严格(前缀不算),通配符太松,精确匹配太紧如果告警命名约定高度规范,精确匹配即可
Redis TTL 做冷却期DB row 记录冷却自然过期、无需清理、查询 O(1)Redis 不可用时自动降级内存 dict
LLM structured output 做草稿自由文本 + 正则提取Pydantic 强校验,字段缺失直接 validation errorN/A(structured output 是硬约束)

1.6 口述脚本#

开场(30s):“Tier 1 的核心问题是——Runbook 从哪来?系统刚上线时什么故障都是未知的,Runbook 库是空的。”

冷启动(20s):“我们导入了 8 个种子 Runbook 覆盖容器重启、磁盘清理、服务启动等高频场景,用 YAML 管理方便 code review。”

自增长(30s):“更关键的是自增长飞轮——Tier 3 查明一个未知故障后,如果人工确认了 RCA、门禁也全过了,LLM 自动从证据链提炼出 Runbook 草稿入审核队列。审核通过后下次同类告警在 Tier 1 就能命中。”

留钩子(10s):“但飞轮的难点不是转起来,而是踩刹车——怎么防止 LLM 生成危险操作或错误剧本。”

面试官大概率追问刹车机制 → 接 validate_draft + flag_for_review。


二、踩坑实录#

坑 1:docker JSON 的三个静默错误#

  • 现象:种子 Runbook 对 build_cache 的回收量恒为 0,Tier 1 永远因前置条件失败而不命中——但不报错。
  • 根因:三个 docker JSON 的语义陷阱:
    1. docker system df -v 把布尔序列化成字符串 'false'not x.get('InUse') 恒为 False → build_cache 可回收量恒 0
    2. 时间串有三种格式(带时区秒 / 纳秒 UTC / 无时区),解析错让年龄过滤失效
    3. docker builder prune --filter until= 过滤的是未使用时长LastUsedAt),不是创建时间。用 CreatedAt 会让”上周建、今早用过”的缓存算进可回收量,而 prune 不删它 → verify 永远假失败
  • 修法:字符串布尔做显式 str(x).lower() == 'true' 检查、多格式时间解析、统一用 LastUsedAt 与 prune 过滤条件对齐。全部钉进 tests/test_imagestore_reclaim.py
  • 教训可回收量的口径必须与 prune 的过滤条件一一对应。看起来应该对的 API 字段语义,在不同 docker 命令之间不一致——先做 data profiling 再写逻辑。

坑 2:docker system df 顶层 Reclaimable 与实际 prune 结果差 14 倍#

  • 现象:顶层 docker system dfImages.Reclaimable = 13.04GB,但种子 Runbook 探测到的 dangling 可回收量只有 0.907GB,差 14 倍。
  • 根因:顶层 Reclaimable 含有 tag 但未被容器使用的镜像,而 docker image prune(不带 -a)只删 dangling(无 tag)镜像。两个数字的定义域不同。
  • 修法:放弃顶层汇总数据,改用 docker image ls --filter dangling=true 做逐条累加——与 prune 的实际删除范围严格一致。
  • 教训汇总 API 的语义和细粒度 API 的语义不一定一致。不要用概览接口的数字去预测细粒度操作的效果。

坑 3:Tier 1 候选只试第一个就放弃#

  • 现象:一条”磁盘满”告警匹配到三份 Runbook(悬空镜像/停止容器/构建缓存),第一份前置条件失败后整个 Tier 1 判 miss → 递进 Tier 3 耗 LLM。但实际第三份有 3.86GB 可回收。
  • 根因:最初实现是”匹配到第一个候选 → 前置条件不过 → Tier 1 miss”,没有尝试后续候选。
  • 修法:改为逐候选试——遍历所有匹配到的 Runbook,第一个前置条件通过的被选中。
  • 教训匹配 ≠ 命中。告警能匹配多份 Runbook 是常态(同类告警不同修复手段),前置条件是区分”哪个手段当前可行”的关键。

坑 4:飞轮装好了但没人踩第一脚#

  • 现象generate_runbook() 有单测覆盖但零生产调用方。rca_accepted / verification_passed 两个参数在 app/ 里除了函数签名再无第二处出现。/runbooks/pending 永远返回空。
  • 根因:自增长流水线的代码写好了,但没有任何代码路径会触发它——Tier 3 调查完成后的回调链断了。
  • 修法
    1. 新表 investigations 持久化调查结果(假设树 + 证据链 + 根因),图跑完这些信息才能被提炼
    2. 两个触发点:accept-rca API + remediation_worker VERIFY_PASSED 之后
    3. 幂等闸 generated_runbook_id 防重复生成
  • 教训单元测试通过不代表功能可达。代码存在 + 测试通过 ≠ 有调用方 → 功能实际上不存在。应该用集成测试覆盖”从入口到出口”的完整路径。

三、量化评估#

3.1 评估数据集构建#

  • 种子 Runbook 静态校验:8 个种子 Runbook 全部通过 static_check(REGISTRY 校验 + verify_steps 存在性),数据来源 tests/test_runbooks.py::test_seed_static_check
  • 匹配回归:手工构造 12 条告警覆盖正向匹配 + severity 不匹配 + label 不匹配 + disabled 不匹配 + 冷却期阻止 + 正则前缀不算。
  • 自增长三条件:8 个测试用例覆盖全中/缺一/幂等/异常吞掉,数据来源 tests/test_runbook_growth.py
  • 真机验证:本地 Postgres + Redis,连推 5 条同源告警 → L2 降噪 → Tier 1 逐候选匹配 → 前两份因 clean 前置失败,第三份 build_cache 3.86GB 通过。

3.2 评估方法#

  • 脚本pytest tests/test_runbooks.py tests/test_runbook_growth.py tests/test_imagestore_reclaim.py(共 48 个测试)
  • 对照组:无 Runbook(所有告警走 Tier 3)vs 有种子 Runbook(高频场景在 Tier 1 消化)
  • 可复现pytest 一键跑,DB 无关(全 mock)

3.3 结果与解读#

指标来源
种子 Runbook 数量8 个,覆盖 4 个域data/runbooks/*.yaml 文件计数
种子静态校验通过率100%(8/8)test_seed_static_check
validate_draft 拒收率LLM 发明的操作 100% 被拒test_draft_unknown_action_rejected
真机 Tier 1 命中前两份 miss、第三份 hit(前置条件区分可行性)P0 实施报告真机验证
自增长幂等两个触发点顺序不确定场景均正确(8/8 测试通过)test_runbook_growth.py

局限性:Tier 1 的生产命中率尚未实测——需要 MCP 工具连接的环境 + 告警风暴回放。种子 Runbook 只覆盖 4 个域,真实运维场景的覆盖率取决于自增长飞轮的效果。


四、面试问答#

基础题(必问级)#

Q1: Runbook 和 Skill 是什么关系?为什么要两套并存?#

答: Skill 是给 LLM 看的”菜单卡”——自由 Markdown 格式的排障剧本,Tier 3 假设生成时注入提供排查方向。Runbook 是确定性处置单元——结构化的动作序列,Tier 1 零 LLM 直接执行。两者解决不同问题:Skill 解决”LLM 应该往哪个方向想”,Runbook 解决”已知故障不需要想,直接做”。

追问 1: 那能不能把 Skill 也结构化,统一成 Runbook? 答: 不能,因为 Skill 的价值恰恰在于”自由”。排障剧本包含”先看 A 指标,如果正常就看 B 日志,如果 B 也正常考虑 C 原因”这种条件分支和经验判断,这些很难完全结构化为确定性动作序列。只有那些能被 100% 结构化的处置步骤(“容器停了就重启、磁盘满了就清理”)才适合做成 Runbook。Skill 是经验,Runbook 是确定性——两者的适用场景不重叠。

追问 2: 自增长飞轮会不会让 Skill 越来越没用? 答: 恰恰相反。自增长的输入之一就是 Skill——Tier 3 假设生成时 _select_playbook 注入 Skill 里的排障经验,这些经验引导 LLM 沿正确方向调查。调查成功后才有机会提炼成 Runbook。Skill 是飞轮的燃料,不是被替代的对象。

Q2: validate_draft 校验了什么?为什么需要它?#

答: 校验链四步:name/match/action/verify 非空 → action_type 在 REGISTRY 中且非只读 → precondition 必须是只读操作 → verify 门禁类型合法。需要它的原因:LLM 通过 structured output 生成草稿,格式是对的但语义可能是危险的——比如发明一个 disk.clean 操作(REGISTRY 里没有)或把写操作放进前置条件(前置条件应该是只读探测)。validate_draft 是飞轮的刹车:格式合规 ≠ 语义安全

追问: 如果 LLM 很聪明,只用 REGISTRY 里有的操作,但搭配了错误的参数呢? 答: 参数校验分两层。第一层是 REGISTRY 的 preconditions 检查——Tier 1 运行时会验证目标资源的当前状态是否在该操作的前置条件集合内。第二层是 CAS 漂移检测——执行前快照比对确认状态未漂移。即使 Runbook 的参数从语义上说”把 A 容器重启”但实际上 A 容器已经在 running,CAS 会检出”预期 stopped 实际 running”的漂移,零变更中止。所以参数错误不会导致错误执行——最多导致 Tier 1 不命中,递进 Tier 2。

进阶题(区分度)#

Q3: 自增长三条件的到达顺序不确定——你怎么保证不漏不重?#

答: 两个触发点(accept-rca API 和 remediation_worker VERIFY_PASSED)各调一次 maybe_generate_runbook()。每次调用都检查三条件是否全中——investigations 表查 rca_accepted(有没有人工确认)+ Incident 状态是否 RESOLVED(门禁过了吗)+ 置信度是否 ≥ 0.8。谁先到发现条件不全,什么也不做。谁后到发现条件齐了,触发提炼。幂等靠 investigations.generated_runbook_id 字段——非空就跳过。即使两者同时到达(理论上极低概率的 race),DB 级别的 UPDATE … WHERE generated_runbook_id IS NULL 保证只有一个赢。

追问: GenerationRefused 被吞掉不外抛——万一是真 bug 呢? 答: GenerationRefused 的语义是”这次提炼的条件不满足”,比如 REGISTRY 拒收了 LLM 发明的操作。这是正常路径——飞轮转不动不应该影响处置执行或 HTTP 响应。区分 bug 的方式是日志分级:GenerationRefused 打 WARNING 带原因(unknown_action_type: disk.clean),RuntimeError(真 bug)也被吞但打 ERROR。监控 ERROR 率即可。

Q4: 种子 Runbook 为什么按域拆成 8 个而不是做一个通用的?#

答: 以磁盘域为例:三份 Runbook(悬空镜像/停止容器/构建缓存)拆开是因为 Tier 1 前置条件取 AND——如果合成一份,任一范围 clean 就整体 miss。拆开后 Tier 1 逐候选试,“悬空镜像没得清”不代表”构建缓存也没得清”。实测中前两份因 clean 失败,第三份 build_cache 3.86GB 通过——如果合成一份就全丢了。

追问: 那候选的顺序重要吗?按什么排? 答: 按安全优先级排——悬空镜像 → 停止容器 → 构建缓存。悬空镜像是最安全的回收对象(无 tag、不被任何容器引用),构建缓存可能影响下次构建速度。先试最安全的,安全的不够再试次安全的。排序是种子 YAML 里的声明顺序,find_matches 返回的是有序列表。

压力题(面试官挑战设计决策)#

Q5: 你说刻意不做通用 disk.clean——但这不是让系统在磁盘问题上覆盖率很低吗?只能清 docker 垃圾,“某个业务日志涨爆了”就做不了。#

答: 是的,这是有意为之的取舍。通用的 disk.clean(path) 一旦存在,LLM 就能把任意路径塞进参数——路径穿越、符号链接跟随、删除系统文件,每一个都是安全隐患。imagestore 的闭集 URI 校验(只允许 dangling_images/stopped_containers/build_cache)消除了这些风险。业务日志涨爆的场景一路落到 Tier 3 由 LLM 诊断,但处置建议进审批由人工执行。

如果要扩展覆盖,正确做法是为特定目录注册特定资源类型(applog://service-name/logs),闭集校验路径,不接受任意 path 参数。每多一个资源类型都是安全审视 + code review 的过程,不是开放一个通道让 LLM 任意使用。

追问: 宁可覆盖率低也不退回自由命令——这个立场在生产环境下站得住吗? 答: 站得住。AIOps Agent 的风险在于 LLM 的输出最终变成对生产环境的真实变更。覆盖率低的代价是人工处置多一些,自动化程度低一些;退回自由命令的代价是一次 LLM 幻觉就可能删掉生产数据。Google SRE 的原则是”先建议后自动、用错误预算约束放量”——覆盖率应该随信任积累逐步扩大,不应该为了一步到位的覆盖率放弃安全边界。

Q6: 飞轮从第一次命中到审核通过平均要多久?有没有可能永远转不起来?#

答: 飞轮有三个瓶颈点:

  1. Tier 3 调查需要产出高置信度根因——冷启动阶段 Tier 3 的质量取决于 LLM 能力 + 工具覆盖 + 知识库质量。如果告警类型超出系统能力(比如底层硬件问题),置信度达不到 0.8,不会触发提炼。
  2. 人工确认 RCA——需要运维人员实际审核诊断报告并点”采纳”。如果没人看报告,rca_accepted 永远不会变 true。
  3. 审核队列清理——草稿进了 pending 没人审,就不会变 active。

永远转不起来的场景确实存在——如果运维团队不参与(不看报告、不审核),飞轮就是死的。这是产品问题不是技术问题:需要在流程上确保”Tier 3 诊断报告必须有人看”。飞书通知会推送到 oncall 群,但”推送了 ≠ 有人看”。


五、前沿概念与延伸#

5.1 Runbook-as-Code / Automated Runbook Generation#

是什么:把运维剧本从 Wiki 文档变成版本化、可执行、可测试的代码制品(YAML/DSL/代码)。自动化 Runbook 生成进一步让系统从自身的调查经验中提炼出新剧本。

为什么要这么做:传统 Runbook 存在 Wiki 里,和实际执行脱节——文档说”重启服务”但没说用什么命令、什么顺序、怎么验证。Runbook-as-Code 让剧本可执行(有明确的动作序列)、可测试(可以回放验证)、可 review(diff 可见)。

这么做的理由:相比自由文本 Wiki,结构化 Runbook 有三个优势:确定性执行(不依赖人的理解)、版本化管理(可 diff、可回滚)、可自动验证(新 Runbook 上线前可以用历史告警回放检查匹配率)。

举例说明:本项目的实现——种子 Runbook 用 YAML 管理(可 code review),action_type 只引用 REGISTRY 登记的操作(可执行),validate_draft 做语义校验(可测试),auto_regression 用历史告警回放检查匹配率(可自动验证)。StackStorm 的 pack 生态和 Robusta 的 playbook 是同一思路的开源实现。

延伸问答

面试官:“Runbook-as-Code 和 Infrastructure-as-Code 是什么关系?” 答:两者是同一理念在不同域的应用——IaC 管的是”基础设施长什么样”(声明式),Runbook-as-Code 管的是”出了问题怎么修”(命令式)。两者互补:Terraform 管的是稳态,Runbook 管的是异常态。在成熟的 SRE 实践中两者共存——Terraform plan-apply 做变更,Runbook 做故障响应。

5.2 Knowledge Flywheel(知识飞轮)#

是什么:系统从自身运行中积累知识、反哺决策、持续改进的闭环机制。每一次成功的故障处理都让系统在下一次同类故障中表现更好。

为什么要这么做:传统运维的知识沉淀靠人(写 Postmortem、更新 Wiki),存在显著的时间滞后和遗忘风险。知识飞轮让沉淀自动化——系统自己从经验中学习,减少对人的依赖。

这么做的理由:相比纯人工沉淀,自动化飞轮有两个优势:时效性(Tier 3 调查完成后立即触发提炼,不需要等人写 Postmortem)和结构化(提炼产物是可执行的 Runbook,不是自由文本)。但飞轮的刹车同样重要——只有验证通过 + 人工确认的经验才允许入库,未验证的不进入飞轮,防止”越学越错”。

举例说明:本项目的 Tier 3 → Runbook 自增长:调查经验(假设树 + 证据链 + 根因)经 LLM 提炼为结构化 Runbook 草稿 → validate_draft 拒收非法操作 → 审核队列人工或自动回归验证 → 入库后下次同类告警在 Tier 1 命中。Google 的 SRE Postmortem 文化是同一理念的非自动化版本;Microsoft RCACopilot 的”检索增强 RCA”是只做 Tier 2(复用历史根因)不做 Tier 1(生成新剧本)的半飞轮。

延伸问答

面试官:“飞轮有没有可能越转越歪?比如一个错误的 Runbook 被人审核通过了。” 答:有可能,这就是为什么有运行时刹车——门禁失败的 Runbook 自动打回 pending_review。即使人审通过了一个错误的 Runbook,它在实际执行后如果验证门禁不通过(命令成功但服务没恢复),会被 flag_for_review 打回去。这是运行时纠错机制——审核是入口闸,门禁是出口闸,双重保险。但如果门禁本身有 bug(比如 metric_recovery 误判为通过),那确实会让错误 Runbook 逃逸。这是门禁质量的问题,不是飞轮机制的问题。


六、诚实边界#

  1. Tier 1 生产命中率未实测。种子 Runbook 的真机验证在本地 Postgres + Redis 上跑通,但生产级的命中率分布(Tier 1 消化多少 / Tier 2 消化多少 / Tier 3 兜底多少)需要告警风暴回放数据集,这个数据集尚未构建。
  2. 自增长飞轮从未在生产中完成一圈。触发链路(Tier 3 → investigations 落库 → accept-rca + VERIFY_PASSED → generate → pending → review → active → Tier 1 命中)的每个环节都有单测或真机验证,但端到端完整跑过一圈的只有测试环境。
  3. 审核队列没有前端/runbooks/pending 只有 API,没有 dashboard 让运维人员方便地查看和审核。CLI 审核(POST /{id}/review)对非开发者不友好。
  4. 覆盖率低是设计选择,但也是能力边界。不做通用 disk.clean 是安全决策,但结果是 Tier 1 只能处置”docker 垃圾把盘吃满”,处置不了”业务日志涨爆”。后者一路落到 Tier 3 由人来看。
  5. auto_regression 只做静态检查 + 匹配率,不实际执行处置。真正验证一个 Runbook 对不对,需要在真实或仿真环境里执行一遍——这涉及混沌工程的基础设施,目前没有。