运维知识管理:Wiki + Runbook 体系怎么真正落地 原创
本文是 SRE 实战系列第 18 篇,讲一件所有团队都说自己在做、但几乎没人做对的事:运维知识管理。
第 6 篇讲告警治理时,我把"没有 Runbook 的告警等于没有告警"当成一条铁律扔了出去。之后被问得最多的一句话是——那 Runbook 到底该写在哪、谁来写、怎么保证它三个月后还活着?
这篇就是那个问题的完整回答。约 9000 字,从一次真实事故开头,拆出 Wiki 与 Runbook 的分工、目录结构、Runbook 的十四段骨架、可执行 Runbook 的做法、三层新鲜度机制、AI 助手接入的边界,最后给一份四阶段落地路线图和一套"知识债"指标。
一句话概括本文的立场:运维知识不该是"写给人看的文档",而应该是"系统的一部分"——能被告警引用、能被 CI 校验、能被机器执行、能被时间淘汰。
一、一个 12 分钟的夜晚
先说场景。生产事故,容器镜像仓库的凭据过期了。
这事本身不难。技术方案有现成的:用 Vault 存凭据,或者做镜像同步,或者干脆换成临时凭据。团队里至少有三个人知道怎么办。
问题在 22:40 那一刻——知道怎么办的三个人都不在线。
on-call 是个刚轮进来第二周的同学,他打开公司的 Confluence,输入关键词。搜出来 214 条结果。排第一的是一份 2019 年的《XXX 容器化改造总结》,正文里的截图还是 Jenkins 的旧界面;排第七的是一份 2021 年的 SOP,链接指向的一个内网 IP,那台机器已经下架了;他往下翻了六页,终于看到一份 2023 年的文档,作者写得很认真,但缺了最关键的一句:切换之后必须重启下游的 image-pull-proxy,否则缓存里还是旧的凭据。
他照着文档做完了,没有任何报错。故障没有恢复。
22:52,他找到我。我让他先重启 proxy,再确认两个镜像仓库之间的 tag 是否对齐。23:04,业务恢复。
恢复耗时 24 分钟,其中 12 分钟花在找对的那份文档上,0 分钟花在真正的操作上。
这个比例,是我见过的大多数中大型团队的常态。后面我们会一次次回到这个夜晚,因为它精确地暴露了运维知识管理的四个失效点:
- 知识是散的——Wiki、语雀、飞书文档、GitHub README、钉钉群公告、某个人脑子里的记忆,六个地方各有一版,且都不知道哪一版是对的
- 知识是陈的——写下来的那天是对的,之后环境变了三次,文档一次没变
- 知识是死的——文档只描述"应该是什么",不提供"现在该怎么办",on-call 拿到它还得自己翻译成命令
- 知识没有归属——没人对"这份文档是否还正确"负责。写得认真的人调岗了,文档就变成了历史遗迹
本文要解决的,就是这四件事。而不是"再买一个更好的 Wiki 工具"——工具从来不是瓶颈。
二、先纠正一个普遍误解:Wiki 和 Runbook 不是一件事
大多数团队的知识管理失败,第一个原因就是把两类知识混在了一个空间里。
它们需要的东西正好相反:
| Wiki(知识库) | Runbook(处置手册) | |
|---|---|---|
| 回答的问题 | 为什么是这样、系统怎么运转 | 现在出事了,我下一步敲什么 |
| 读者状态 | 有空、想理解、通常在白天 | 没空、只想动作、通常在半夜 |
| 结构 | 网状,可以自由跳转、可以长 | 线性,必须能在 30 秒内定位到第 3 步 |
| 有效期 | 半年到两年,随架构演进 | 随配置和版本,通常 1~3 个月 |
| 判断标准 | 新人读完能不能讲清楚原理 | 陌生人能不能不思考地照做 |
| 变更方式 | 自由编辑、逐步积累 | 只能走 PR,必须评审 |
| 失效后果 | 学不到东西,但不至于出事 | 把人带进坑,比没有更糟 |
混在一起会怎样? 典型症状是:Runbook 被写成了一篇讲原理的长文,前三屏是背景介绍,操作步骤在第五屏;或者反过来,Wiki 里塞满了 kubectl 命令,没人知道这些命令为什么这么敲,环境一变全废。
所以第二条原则(第一条是"工具不是瓶颈"):
Wiki 用来"培养能判断的人",Runbook 用来"兜住不能判断的时刻"。前者追求理解的深度,后者追求动作的确定性。
一个团队只需要 30~60 篇高质量的 Wiki,但可能需要 300+ 份 Runbook——因为 Runbook 是跟着告警走的,而告警是按(服务 × 故障模式)展开的。
三、分工:三个仓库,三套规矩
落地时我的建议是三个物理隔离的仓库,不要试图用目录区分。
3.1 三个仓库
# 1. 知识库:讲为什么,给人读
kb.internal/ # 建议用 Git 仓库 + 静态站点(VitePress / MkDocs)
├── 01.架构/
│ ├── 订单系统为什么这样拆.md
│ └── 为什么我们用双镜像仓库.md
├── 02.环境/
│ ├── 生产环境拓扑与网络分区.md
│ └── 账号权限申请与回收流程.md
├── 03.规范/
│ ├── 发布规范.md
│ └── 命名与标签规范.md
├── 04.复盘/
│ └── 2026-10-18 镜像源故障复盘.md
└── 99.归档/ # 过期的不要删,挪到归档并标注失效日期
# 2. 处置手册:讲怎么做,给人和机器读
runbooks/ # Git 仓库,按服务分目录
├── payment-api/
│ ├── high-error-rate.md
│ ├── db-connection-pool-exhausted.md
│ └── rollback.md
├── image-registry/
│ └── pull-credential-expired.md
└── _templates/
└── runbook-template.md
# 3. 自动化:Runbook 里的动作,能自动化的一律变成命令
ops-cli/ # 内部运维 CLI,Runbook 直接调用它
├── cmd/
└── probes/ # 只读探针,供 Runbook 第 2 段使用为什么 Runbook 必须独立于 Wiki? 三个硬理由:
- 引用关系不同:Runbook 会被告警的
runbook_url直接引用,URL 必须稳定,不能因为有人重命名了目录就断链 - 生命周期不同:Wiki 可以慢慢改,Runbook 必须随配置和版本同步改,评审节奏完全不同
- 机器可读:Runbook 要被 CI 校验、被渲染成
kubectl建议命令、被 AI 助手检索。自由编辑的 Wiki 做不到
3.2 唯一入口原则
物理隔离之后,最难的不是技术,是人的习惯。必须有一条硬规矩:
任何知识,只有一个权威入口。其他地方出现的同类内容,一律只能是一行摘要 + 一个链接。
这条规矩的价值在第 1 节那个夜晚非常直观:如果那条破规矩被执行了,on-call 的同学根本不会看到那 214 条搜索结果——他只会看到一份索引,索引里每个服务一行,指向唯一的权威文档。
3.3 索引是谁维护的
落地中最常见的腐化不是文档写错,而是索引没人维护。新服务上线,忘了往索引里加一行,三个月后 index 缺了 20 个服务。
我的做法是把索引变成代码生成的,而不是手写的:
# services.yaml —— 服务清单的唯一事实源(Source of Truth)
services:
- name: payment-api
display: "支付核心 API"
owner: team-payment # 唯一 owner,不允许写两个人
tier: critical # critical / standard / internal
slo: "99.95% @ 30d"
dashboards:
l1: https://grafana.internal/d/payment-api-l1
runbooks:
- alert: PaymentApiHighErrorRate
url: https://runbook.internal/payment-api/high-error-rate
- alert: PaymentApiRollback
url: https://runbook.internal/payment-api/rollback这份 services.yaml 提交到 Git,CI 做三件事:生成 Wiki 的服务索引页、校验每个 url 指向的文件确实存在、检查每个 alert 名在 Prometheus 规则里真实存在。
索引从"要求大家记得更新"变成了"忘了更新就红",腐化问题解决 80%。
四、Wiki 侧:只写四类内容
Wiki 最容易膨胀成垃圾场,因为它没有准入标准。我的规矩是只允许四类页面,其余一律拒收:
4.1 四类准入
① 架构决策记录(ADR)——回答"为什么这样设计"
重点不是描述现状,而是记录当时放弃了什么。一份好的 ADR 里,最值钱的是"被否决的方案及原因"。因为半年后一定有人提议"我们为什么不改成 XXX",答案往往就躺在那一行里。
# ADR-013:镜像仓库使用双源 + 同步,而不是单一主源
- 状态:已采纳(2026-03-11)
- 决策人:@xxx
- 关联:INCIDENT-2026-0311
## 背景
...
## 候选方案
1. 单一镜像源(部署简单,但单点)
2. 每个集群独立构建(隔离好,但构建资源 ×3,且版本漂移)
3. **双源 + 主动同步(采纳)**
## 为什么选 3
...
## 放弃 2 的真实原因
构建资源其实是次要的,真正的问题是"同一 commit 在不同集群构建出不同镜像",
灰度时无法保证行为一致——这个坑我们在 2025 年踩过一次。
## 这个决策什么时候该被推翻
当同步延迟成为瓶颈,或镜像体积增长导致同步成本超过独立构建时。最后一行"什么时候该被推翻",是我见过的 Wiki 里最少见、也最有用的字段。
② 环境与权限说明——回答"这东西现在长什么样"
拓扑、网络分区、账号体系、审批链路。这类内容变化快,唯一要求是每个页面顶部写明"最后核实时间"和"核实方式"(比如"2026-10-20 通过脚本 ops-cli env probe 核实")。
③ 规范与门禁——回答"我们怎么做事"
发布规范、命名规范、值班规范、变更分级。这类内容要和 CI 联动,规范里写的规则必须能在流水线里找到对应的检查步骤,否则就是一张废纸。
④ 复盘报告——回答"我们踩过什么坑"
第 9 篇讲过无责复盘。这里只强调一点:复盘报告的"改进项"必须落到具体仓库的具体文件上——要么是一个 PR,要么是一份新建的 Runbook,要么是一条新的 CI 检查。写成"加强意识"、"持续关注"的一律打回。
4.2 拒收清单
以下五类内容不进 Wiki,它们是 Wiki 变垃圾的主要来源:
- 临时通知("今晚 22:00 发布")→ 走群或公告系统
- 工单/操作流水("3 月 5 日扩容了 2 台")→ 走工单系统
- 讨论过程("关于是否上 Istio 的讨论")→ 结论进 ADR,过程留在聊天记录
- 工具使用手册(PromQL 语法、
kubectl用法)→ 官方文档链接即可,别抄 - 没有 owner 的页面 → 拒收,谁写谁挂名
4.3 每个页面必须有的三个字段
---
owner: team-payment # 必须是一个团队,不是"张三"
last_verified: 2026-10-20 # 最后核实时间,不是最后编辑时间
expires_in: 180d # 到期后自动标记为「待核实」
---owner 必须是团队而不是个人——个人会调岗,团队不会(至少不会一夜消失)。
last_verified ≠ 最后编辑时间。这两者混用是知识库腐化的隐形杀手:有人改了个错别字,文件的 mtime 变新了,编辑器里看起来"很活跃",实际内容三年没核实过。核实时间只能通过一个动作产生——有人真的去验证了一遍,然后更新这个字段。
五、Runbook 侧:十四段骨架
这是本文的核心。下面这份模板使用了两百多份,被验证过在 caffeinated(咖啡因上头)和 half-asleep(半睡半醒)两种状态下都能用。
5.1 骨架总览
---
title: payment-api 5xx 错误率超过 1%
service: payment-api
owner: team-payment
alerts:
- PaymentApiHighErrorRate
last_verified: 2026-10-20
review_interval: 90d
automation: partial # none / partial / full
---
## 0. 一句话
## 1. 确认影响(60 秒内回答"用户受影响了吗")
## 2. 三十秒判断方向
## 3. 应急操作(按顺序,每步都有验证)
## 4. 常见根因与对应动作
## 5. 不要做这些
## 6. 什么时候必须升级
## 7. 止血之后
## 8. 相关链接看起来 8 段,实际展开是 14 个必填项(含 H1 下的元信息)。逐段讲。
5.2 第 0 段:一句话
## 0. 一句话
payment-api 的 5xx 比例超过 1% 并持续 5 分钟,最常见的两个原因是
①最近一次发布引入了兼容性问题 ②下游 order-db 连接池耗尽。这三十秒能省掉 on-call 五分钟的阅读时间。 很多 Runbook 的第一句是"本文介绍 payment-api 错误率告警的处理流程"——这是废话,信息量为零。第一句就应该给出先验概率:这个告警 80% 是因为什么。
这个数字不是拍脑袋。它来自复盘:统计过去一年的同类告警,前两大原因占了 76%。Runbook 应该写成统计结论,不是操作教程。
5.3 第 1 段:确认影响
这一段的唯一目标:60 秒内回答"用户是不是正在受影响"。
## 1. 确认影响(60 秒)
只读,不做任何变更。
### 1.1 用户侧受影响了吗
ops-cli probe sli payment-api --window 15m
期望输出:
availability: 99.62% (SLO 99.95%) ← 跌破即用户受影响
err_rate_5xx: 1.83%
p99_latency: 412ms
### 1.2 影响面有多大
ops-cli probe blast payment-api
判断标准:
- 全量 / 单地域 / 单租户 → 见下方定级表
- 是否还在恶化:对比 15m 与 1h 曲线斜率
### 1.3 快速定级(对接第 13 篇的五级框架)
| 条件 | 级别 | 动作 |
| --- | --- | --- |
| 可用性 < 99.5% 且仍恶化 | P1 | 电话叫醒 owner,同时执行第 3 段 |
| 可用性 99.5%~99.95% | P2 | IM 通知 owner,工作时间内处理 |
| 错误率上升但可用性未破 SLO | P3 | 记录 + 观察 |注意 1.1 的命令是只读的,且在模板里明确写了这一段的性质是"不做任何变更"。 人在紧张时会先动手后观察,Runbook 必须在结构上把这两个阶段分开,并且让"观察"这一步短到没人会跳过它。
注意 1.3 与第 13 篇的定级框架直接对接。 Runbook 不重新发明级别,它只是把级别判断变成可执行的三行条件。这是知识体系之间最重要的耦合方式:规范和手册用同一套词汇。
5.4 第 2 段:三十秒判断方向
这是很多人漏掉的一段,但它决定了效率。
## 2. 三十秒判断方向
按顺序问,命中即停:
1. 最近 30 分钟有发布吗?
kubectl -n production rollout history deploy/payment-api | tail -3
命中 → 跳到 3.1(回滚)
2. 下游 order-db 连接数是否饱和?
ops-cli probe db order-db --metric connections
> 90% → 跳到 3.2(限流 + 扩容连接池)
3. 某个下游依赖的延迟是否异常?
ops-cli probe deps payment-api
命中 → 跳到 3.3(降级开关)
4. 上面都不命中 → 跳到 4(常见根因),同时开始计时,10 分钟未定位进入 6(升级)这一段是整个 Runbook 的中枢。 它把"排查"这件开放式的事,变成了一棵有边界的决策树。四条问题,最坏情况 30 秒走完。
设计这一段有个技巧:每个判断都必须能一条命令得出,不允许"综合判断"。"综合判断"在半夜等于"不知道"。
5.5 第 3 段:应急操作
## 3. 应急操作
每一步执行后必须验证,验证不过不要进入下一步。
### 3.1 回滚(最近 30 分钟有发布时首选)
kubectl -n production rollout undo deploy/payment-api
kubectl -n production rollout status deploy/payment-api --timeout=180s
验证:
ops-cli probe sli payment-api --window 5m
期望 5xx < 0.5%
失败处理:回滚卡住 > 5 分钟 → 跳到 6(升级),不要反复 undo。
### 3.2 连接池(order-db 连接饱和)
先限流,再扩池,顺序不能反:
ops-cli traffic limit payment-api --ratio 0.5 --duration 10m
ops-cli db pool order-db --max-connections 400
验证:
ops-cli probe db order-db --metric connections
期望 < 70%
注意:扩池前必须确认 order-db 剩余连接配额,超过 500 会挤掉报表任务(见 4.2)。
### 3.3 降级开关
ops-cli toggle set payment-api.recommendation.disable=true
验证:
ops-cli probe deps payment-api
期望推荐依赖调用量归零,主链路 5xx < 0.5%三条硬性写作规则:
- 命令必须是能直接复制执行的完整命令,不允许出现
<your-pod-name>这种要人填的占位符(要用-l选择器或 CLI 封装) - 每个动作后面必须有验证命令和期望结果。没有期望结果的验证等于没有验证
- 每个动作必须有失败处理。当步骤失败时,人才是最容易乱来的状态
5.6 第 4 段:常见根因
这一段和第 0 段呼应,把"先验概率"展开成可操作的分支。
## 4. 常见根因
### 4.1 发布引入兼容性问题(约占 45%)
现象:错误率与发布时间强相关,错误集中在某几个接口。
判断:kubectl -n production logs deploy/payment-api --since=10m | grep -c "unknown field"
动作:3.1 回滚 → 通知发布人 → 排查版本差异
### 4.2 order-db 连接池耗尽(约占 31%)
现象:错误类型以 connection timeout 为主,连接数接近上限。
**注意**:不要直接把 max-connections 拉到 1000,order-db 上有报表任务,
超过 500 会把报表挤掉,导致次日 09:00 数据不出。详见 KB:数据库连接预算。
### 4.3 ..."不要做这些"藏在这一段里是有效的,因为人在看根因时最容易冲动。第 4.2 条那句加粗提醒,就把一次"看起来合理的扩容"和"第二天早上报表挂掉"隔开了。
5.7 第 5~8 段
## 5. 不要做这些
- 不要同时执行回滚和扩容:两个变量一起动,之后无法归因
- 不要重启 order-db:本服务的连接池残留会让它在 3 分钟内再次打满
- 不要在未确认影响面时直接全量降级:会掩盖真实错误率,误导后续定级
## 6. 什么时候必须升级
任一条件命中立即升级,不要自己扛:
- 15 分钟未找到方向
- 已执行 2 个应急动作仍无改善
- 涉及数据不一致、订单状态错乱
- 需要变更数据库或网络配置
升级链路:@team-payment owner → #sre-oncall → 值班经理
## 7. 止血之后(24 小时内做完,否则不算闭环)
- [ ] 在 services.yaml 里补/更新该故障模式
- [ ] 如果本次有一步是"文档里没写的",补进本文
- [ ] 如果本次有一步是"文档里写错了的",立即改并记录下来
- [ ] 判断能否自动化:只读探针 / 一键动作 / 自动止血,三选一
- [ ] 发起复盘(第 9 篇模板)
## 8. 相关链接
- L1 面板:https://grafana.internal/d/payment-api-l1
- 依赖拓扑:https://kb.internal/架构/订单系统为什么这样拆
- 连接预算:https://kb.internal/规范/数据库连接预算
- 上次同类故障:INCIDENT-2026-0712第 7 段是整份模板里最容易被跳过、也最值钱的一段。 它把"一次故障"变成了"一次知识更新"。没有这一段,Runbook 就只是被消费,不会被进化。
第 6 段的升级条件是防神风特攻。on-call 最常见的失败模式不是不会做,而是不敢升级——觉得"再试一下应该就好了"。给出明确的量化条件(15 分钟、2 个动作),能把"要不要叫人"从性格问题变成规则问题。
六、从文字到可执行:Runbook 的三级成熟度
到目前为止讲的都是 Markdown。但 Markdown 有个天花板:它只能给建议,不能替人执行。
我按"人能省多少力"把 Runbook 分三级,团队应该持续往右推:
| 级别 | 形态 | 谁执行 | 典型代表 | 半夜恢复耗时 |
|---|---|---|---|---|
| L1 文档型 | Markdown + 可复制命令 | 人逐个复制粘贴 | 本文第五节的模板 | 10~30 分钟 |
| L2 探针型 | 只读检查全部封装成 CLI | 人判断,机器查 | ops-cli probe ... | 3~10 分钟 |
| L3 可执行型 | 动作封装成幂等 CLI/Job,人工触发 | 机器执行,人确认 | ops-cli remedy rollback ... | 1~3 分钟 |
6.1 L2:先把"看"自动化
这是投入产出比最高的一步,因为排查的大部分时间花在收集信息,而不是做决策。
#!/usr/bin/env bash
# ops-cli 的 probe 子命令示意:所有 Runbook 第 1、2 段共用同一批探针
# 只读,绝不修改任何状态
set -euo pipefail
probe_sli() {
local svc="$1" window="${2:-15m}"
# 直接查 SLO 数据源,而不是让人自己算比例
curl -sG "${SLO_API}/query" \
--data-urlencode "service=${svc}" \
--data-urlencode "window=${window}" |
jq -r '"availability: \(.availability)\nerr_rate: \(.err_rate)\np99: \(.p99_latency)"'
}
probe_db() {
local db="$1" metric="${2:-connections}"
# 输出必须自带判断,而不是给一堆裸数字
local cur max ratio
read -r cur max < <(psql_ro "$db" -tAc \
"select count(*), setting::int from pg_stat_activity, pg_settings
where name='max_connections' group by setting")
ratio=$(awk -v c="$cur" -v m="$max" 'BEGIN{printf "%.1f", c*100/m}')
echo "connections: ${cur}/${max} (${ratio}%)"
awk -v r="$ratio" 'BEGIN{ exit (r>90)?1:0 }' ||
echo "WARN: 连接数超过 90%,按 runbook 3.2 处理"
}
case "$1" in
sli) probe_sli "$2" "${3:-15m}" ;;
db) probe_db "$2" "${3:-connections}" ;;
*) echo "usage: ops-cli probe {sli|db|deps|blast} ..." >&2; exit 2 ;;
esac关键设计:探针输出里带判断。connections: 468/500 (93.6%) 后面跟一句 WARN: 超过 90%,比给一个裸数字有用得多——因为看的人可能不知道 90% 是阈值。
6.2 L3:把"做"变成幂等动作
# ops-cli remedy rollback payment-api
# 设计约束(四条,缺一条就不许上线):
# 1. 幂等:重复执行不产生额外副作用
# 2. 可观测:每一步都打到审计日志,带 operator 身份
# 3. 自带验证:执行完自动跑 probe,结果写进审计
# 4. 有回退:动作本身能被反向执行
remedy_rollback() {
local svc="$1" ns="${2:-production}"
local rev
rev=$(kubectl -n "$ns" rollout history "deploy/$svc" |
awk '/REVISION/{last=$2} END{print last}')
audit "rollback.start service=$svc from_revision=$rev operator=$OPERATOR"
kubectl -n "$ns" rollout undo "deploy/$svc"
kubectl -n "$ns" rollout status "deploy/$svc" --timeout=180s
local result
result=$(probe_sli "$svc" 5m)
audit "rollback.done service=$svc result=$(echo "$result" | tr '\n' ' ')"
echo "$result"
}三个细节值得单独说:
audit带 operator:这是复盘时唯一能还原"谁在什么时候动了什么"的依据- 执行完自动跑 probe:把"验证"从人的责任转移到工具的责任,这一步消除了最常见的漏动作
- 不进全自动闭环:L3 依然是"人触发、机器执行"。第 8 篇讲过自愈,但自愈的前提是已验证。Runbook 的动作在变成自愈策略之前,必须先以 L3 形态被人执行过至少 20 次,这是第 16 篇红线七的同一种思路——新能力先用人工确认跑够样本,再谈自动。
6.3 一条分界线
有些团队会问:"那干脆全自动算了?"分界线我建议划在这里:
- 只读(看数据、算比例、查历史)→ 无条件自动化,L2 起步
- 可逆且幂等(回滚、限流、切开关)→ L3,人触发
- 不可逆(删数据、改 schema、动网络策略)→ 永远留在文档里,人不许一键执行。这类动作的 Runbook 第 3 段应该只有"升级"两个字,没有命令
第三条是硬线。能被一条命令执行的不可逆操作,总有一天会被误执行。 这不是对人的不信任,是对凌晨三点的生理状态的不信任。
七、让知识活着:三层新鲜度机制
写到这里,我们解决了"知识该放哪、该长什么样、该怎么用"。还剩最后一个、也是最难的问题:三个月后它还在不在?
我用三层机制,从上到下依次兜住:
7.1 第一层:时间驱动(自动过期)
# 每份 Runbook 的 frontmatter 都带 review_interval
# CI 每天跑一次,把过期清单推到值班群和 owner 的周报
- cron: "0 9 * * 1"
script: |
python scripts/kb-staleness.py --root runbooks/ --report /tmp/stale.md
# 输出三档:
# overdue > 30d → 红色,本周必须处理
# overdue 0~30d → 黄色,下周排期
# automation=none 且 90d 未动 → 直接升级为红色# scripts/kb-staleness.py(核心逻辑,约 60 行)
import sys, yaml, datetime, pathlib
TODAY = datetime.date.today()
def scan(root):
rows = []
for p in pathlib.Path(root).rglob("*.md"):
meta = yaml.safe_load(p.read_text().split("---")[1])
if meta.get("title") is None: # _templates 跳过
continue
verified = datetime.date.fromisoformat(str(meta["last_verified"]))
interval = int(str(meta.get("review_interval", "90d")).rstrip("d"))
overdue = (TODAY - verified).days - interval
rows.append((p, meta, overdue))
return rows
def level(meta, overdue):
if overdue > 30:
return "red"
if overdue > 0:
return "yellow"
# 没走 L2 也没自动化的手册,最危险:从来没被执行过
if meta.get("automation", "none") == "none" and overdue > -60:
return "red"
return "green"注意最后那个判断:automation: none 且接近过期的手册一律判红。因为它从来没被封装成探针,意味着没人真正验证过它能不能用。这类手册的腐化速度比其他快得多。
7.2 第二层:事件驱动(用完即核实)
比时间更准的信号是"这份文档刚刚被用过"。
# 在 ops-cli 里,所有从 Runbook 出发的动作都带上 runbook 标识
ops-cli remedy rollback payment-api --runbook image-registry/pull-credential-expiredCLI 收到 --runbook 后做两件事:
- 记录一次
used事件(谁、什么时候、执行了第几步) - 在审计日志末尾回写一行:
本次是否命中第 2 段的判断树?Y/N
如果 30 天内被用过 3 次以上,说明这份文档在实战频次上很重要——把它推进自动化优先级队列。 如果 180 天一次都没被用过,那多半是告警被删了或者服务下线了——该退役了。
时间驱动解决"没人管",事件驱动解决"不知道哪份重要"。两者结合,知识库的维护就变成了一个有优先级的队列,而不是一个靠自觉的负担。
7.3 第三层:结构驱动(改配置必须改文档)
最硬的一层:把文档和配置绑在一起,改一个不改另一个就合不进去。
# .cnb.yml 片段:配置类变更同时校验对应 Runbook
- name: 🔍 文档一致性检查
script: |
set -euo pipefail
changed=$(git diff --name-only origin/$CNB_DEFAULT_BRANCH...HEAD)
# 规则一:改了告警规则,必须同步 Runbook 或说明为什么不需要
if echo "$changed" | grep -q '^alerts/'; then
if ! echo "$changed" | grep -qE '^runbooks/|^docs/'; then
echo "❌ 本次改了 alerts/,但没有更新对应 Runbook"
echo " 如确实不需要更新,请在 PR 描述中写明 [no-runbook]"
exit 1
fi
fi
# 规则二:Runbook 里的每个 runbook_url 都必须指向真实文件
python scripts/check-runbook-links.py runbooks/ alerts/
# 规则三:runbooks 目录下新增文件必须来自模板(含 14 个必填段)
python scripts/check-runbook-shape.py runbooks/ _templates/runbook-template.mdcheck-runbook-shape.py 的检查清单(也是交接时的验收清单):
| 必填项 | 检查方式 |
|---|---|
| frontmatter 六字段齐全 | title / service / owner / alerts / last_verified / review_interval |
| 8 个二级标题齐全且顺序正确 | 按模板的 H2 列表做前缀匹配 |
| 每个操作段落含验证命令 | 段落内出现 验证: 与 期望 |
| 无占位符残留 | 正则匹配 <[a-z-]+>、xxx、TODO、待补充 |
每个 alerts 名在规则里存在 | 与 alerts/*.yaml 的 alert 字段交叉校验 |
这套门禁上线后,我们的模板遵循率从 34% 涨到 100%——不是因为大家变认真了,而是不合规的进不来。
八、真实改造:一年,从 214 条搜索结果到一份索引
下面是我们 2025 年到 2026 年的一次完整改造记录。规模:3 个 K8s 集群、180+ 微服务、6 个运维,原本知识分散在 Confluence(约 1400 页)、语雀(约 300 页)、GitHub README、群公告四处。
8.1 改造前的量化基线
| 指标 | 改造前(2025-09) | 说明 |
|---|---|---|
| 知识条目总数 | 约 1900 页 | 四处分散,无索引 |
| 有明确 owner 的页面 | 约 20% | 大部分是"个人空间" |
| Runbook 覆盖率(关键告警) | 31% | 关键告警 214 条,有手册的 66 条 |
| Runbook 平均陈旧度 | 11 个月 | 以 last_verified 无法确定的按创建时间估 |
| 故障平均恢复耗时(MTTR) | 47 分钟 | 全年重大事故均值 |
| 一次故障中"找文档"耗时 | 未度量 | 改造后补的埋点 |
"找文档耗时未度量"这件事本身,就是改造的第一个发现。 我们连自己浪费了多少时间都不知道。
8.2 四次动作
动作 1:先做减法,两周(2025-09)
按"是否被访问过、是否有 owner、内容是否还对"三问过滤 1900 页:
- 不删任何东西,先统一搬进
99.归档/,页面顶部加一条⚠️ 本文可能已过期,最后编辑于 2021-03 - 访问统计来自语雀/Confluence 的页面访问日志,一年 0 访问的直接归档
- 大约 62% 进了归档区(1180 页),主目录从 1900 页降到 720 页
这一步的产出不是"文档变多了",而是"搜索终于能用了"。 214 条搜索结果里 130 条是归档区噪音,加个 archive 标签就能过滤掉。
动作 2:214 条关键告警的 Runbook 攻坚,六个月(2025-10 ~ 2026-03)
不是一次全做完,而是按季度分三批,每批 70 条左右:
- 每条 Runbook 由"最了解这个服务的人"写(通常是业务研发,不一定是 SRE)
- SRE 负责 Review 结构合规性(14 段齐全)+ 命令可执行性(当场跑一遍)
- 每批的最后两周做"新人演练":让一个从没接触过该服务的同学,只看 Runbook 在预发环境跑一遍,卡住的地方改到不卡为止
演练这一步是关键。 我们统计过:被演练过的 Runbook,实战中被反馈"看不懂/走不通"的比例是 8%;没演练过的是 41%。
动作 3:探针统一 + CLI 化,四个月(并行)
把第 1、2 段里所有"查一下"的操作统一到 ops-cli probe,10 个探针覆盖了约 80% 的排查动作。这是纯工程活,写代码即可,不需要组织协调,所以放在并行轨道上推进。
动作 4:三层门禁 + 事件埋点,两个月(2026-02 ~ 2026-03)
第七节讲的那套机制在这个阶段上线。上线的第一个月,过期清单上平均每周有 23 条红色项;三个月后稳定在 5~8 条。
8.3 改造后的结果
| 指标 | 改造前 | 改造后(2026-09) | 变化 |
|---|---|---|---|
| 知识条目总数(非归档) | 约 1900 | 640 | 减少 66% |
| 有明确 owner 的页面 | 20% | 100% | 强制字段 |
| Runbook 覆盖率(关键告警) | 31% | 100% | 214/214 |
| Runbook 平均陈旧度 | 11 个月 | 34 天 | last_verified 机制 |
| 走 L2 以上(探针化)的 Runbook | 0% | 76% | 161/214 |
| 走 L3(可执行)的 Runbook | 0% | 22% | 47 条,集中在高频动作 |
| 故障平均恢复耗时(MTTR) | 47 分钟 | 22 分钟 | 降低 53% |
| 其中"定位 + 找资料"耗时 | 约 18 分钟(事后回填估算) | 约 4 分钟 | 埋点度量 |
| 新人独立值班达成时间 | 约 3 个月 | 约 6 周 | 演练 + 手册 |
必须诚实说明的部分:MTTR 降低 53% 里,不能全归功于文档。同期我们还做了一次集群迁移和告警治理(第 6 篇),三者共同作用。但"定位 + 找资料"这一段从 18 分钟降到 4 分钟,是可以明确归因的,因为它有独立的埋点。
我认为最值钱的一项不是 MTTR,是"新人独立值班从 3 个月到 6 周"。 它意味着团队的扩张成本降了一半,而这件事在预算表上是看不见的。
8.4 三次撞过的坑
坑一:一开始想用"更好的工具"解决,浪费了两个月。 我们花两个月做了一次 Confluence → 语雀的迁移选型,做完发现搜索依然难用。因为问题不在搜索算法,在于根本没人给页面打标签、没人写 owner。垃圾进,垃圾出。
坑二:Runbook 写得太"正确",反而没人用。 第一批 70 条,SRE 写得很规范,四平八稳,每条都从"背景介绍"开始。第三周我随机抽查了一次实战使用,发现 on-call 都在翻到第三屏后放弃了。改成"一句话 + 30 秒判断树"之后,使用率才上去。规范的目标是"可执行",不是"好看"。
坑三:门禁上线第一周,PR 被卡了 14 次,团队开始抱怨。 原因是我们的 check-runbook-shape.py 太严——连标点符号和标题层级的中英文括号都校验。后来放宽到只校验"段落在不在、验证命令在不在",抱怨立刻消失。门禁要卡的是"能不能用",不是"写得漂不漂亮"。
九、AI 助手接入:边界在哪里
2026 年绕不开这个话题。我们的实践结论一句话:AI 让"找到知识"变简单了,让"错误知识被大规模采信"也变简单了。
9.1 值得做的三件事
① Runbook 优先检索,Wiki 次之
把 runbooks/ 作为第一检索层,而且只在 frontmatter 的 last_verified 在 90 天内的文档里检索。过期的直接排除,而不是"参考一下"。
② 把告警上下文喂给它
告警触发时自动拼一个上下文包(告警名、当前 SLI 数值、最近发布记录、相关面板链接),再让模型基于 Runbook 生成"当前应该看哪一节"。这样它的输出是定位到段落,而不是复述整篇。
③ 让 AI 写草稿,人做终审
故障之后,把时间线、命令历史、聊天记录丢给它,生成 Runbook 草稿或增量更新建议。这一步效率提升非常明显——从零写一份 Runbook 要 2 小时,改草稿要 20 分钟。
9.2 三条硬边界
边界一:AI 不执行不可逆动作。
第 6.3 节的分界线在 AI 这里同样适用,而且更严格。让它生成命令供人确认,可以;让它直接执行,不行。不可逆操作的执行权必须绑定到一个具体的人。
边界二:找不到权威文档时,必须回答"没有",不许拼凑。
这是检索增强最容易出事的地方。模型在检索不到时会倾向于用训练数据"补全",而运维场景下,一段看起来合理的 kubectl patch 可能刚好把你的 PDB 删掉。
我们的做法:检索结果为空或相关性低于阈值,强制返回 未找到对应 Runbook,请升级到 @sre-oncall。
边界三:AI 产出的每一份文档,必须有人的 owner 签字。
"AI 写的"不能成为免责理由。文档的 owner 字段永远是人或团队,出错的追责也落在这个字段上。这条规则听起来形式主义,实际上它决定了 AI 是提高知识密度的工具,还是制造知识垃圾的机器。
9.3 一个具体的提示词骨架
你是运维助手,只允许基于下面提供的 Runbook 内容回答。
告警:{{ alertname }}
当前指标:{{ sli_snapshot }}
最近发布:{{ recent_deploys }}
Runbook 内容(last_verified: {{ last_verified }}):
{{ runbook_full_text }}
请输出:
1. 对应 Runbook 的哪一段(只给段落标题 + 一句话摘要)
2. 该段要求执行的下一步命令(原文照抄,不要改写)
3. 该段的验证期望值(原文照抄)
禁止:编造 Runbook 中不存在的命令;如果内容不足以回答,只输出
"未找到对应步骤,请按第 6 段升级"。"原文照抄,不要改写"这句是核心。 命令一旦被模型"顺手优化"过,就不再是 Runbook 里被验证过的那条命令了。
十、四阶段落地路线图
如果从零开始,我建议按这个顺序,不要跳步。
阶段一(第 1~4 周):减法与索引
- 统计所有知识存放位置与条目数,算出一个"散乱指数"(几处存放 × 多少页)
- 按"一年 0 访问 / 无 owner / 内容已错"三条归档,目标砍掉 50% 以上
- 建立
services.yaml,先只填 name / owner / tier 三个字段——这是后面所有自动化的地基 - CI 生成服务索引页,替换掉"人工维护的目录"
验收:一个新人问"XX 服务的文档在哪",答案只有一个 URL。
阶段二(第 2~6 月):Runbook 攻坚
- 列出全部 page 级别告警,按调用频次排序,取前 30% 开始写
- 用本文第八节的 14 段模板,强制
check-runbook-shape.py校验 - 每批写完做新人演练,卡住就改
- 告警规则强制
runbook_url,CI 校验链接真实存在
验收:page 级告警 100% 有 Runbook,且每份都被一个"没写它的人"成功执行过一次。
阶段三(第 4~8 月):探针化
- 抽出第 1、2 段的所有"查一下",做成
probe子命令 - 探针输出自带判断(阈值 + WARN)
- Runbook 里所有排查段改成调用探针
- 统计探针复用率,低于 3 次的探针考虑合并
验收:Runbook 里裸命令(非 CLI 封装)比例低于 20%。
阶段四(第 6~12 月):门禁与埋点
- 三层新鲜度机制上线(时间 / 事件 / 结构)
ops-cli支持--runbook埋点- 高频动作推 L3(幂等 + 审计 + 自带验证)
- 把"知识债"纳入季度工程汇报
验收:过期红色项稳定在个位数,且每周都在被处理。
十一、六个常见误区
误区一:"先上一个好工具,知识自然就管起来了。"
工具只能承载知识,不能生产知识。我们换代工具两次,腐化速度没变。真正起作用的是 owner 字段、模板和门禁——这三样与工具无关。
误区二:"Runbook 应该 SRE 统一写,保证风格一致。"
SRE 不了解每个服务的内部细节,写出来的东西"规范但不对"。正确分工是:业务研发写(他们知道真相),SRE 审(保证结构与可执行性)。让写的人写,让懂标准的人卡标准。
误区三:"文档写详细一点总没错。"
错。在半夜三点,"详细"等于"找不到"。Runbook 的长度上限是一屏半内必须出现第一个可执行动作。细节放在 Wiki 里,通过链接引出。
误区四:"过期文档留着吧,万一有用。"
过期文档比没有文档更危险,因为它会被人采信。我们的做法是归档而不删除:挪到 99.归档/,顶部加警示条,搜索默认过滤。保留历史,剥夺权威。
误区五:"Runbook 写完就自动化,一步到位。"
跳过 L2 直接做 L3 的团队,通常在三个月后把自动化关掉。因为没有经历过 L1/L2 阶段,你不知道哪些动作高频、哪些边缘情况会让自动化出错。先被人手动执行 20 次,再谈自动化(第 16 篇红线七的同一个道理)。
误区六:"有 AI 了,不用写文档了,问它就行。"
AI 的知识来自你的文档。没有权威文档,它给你的就是训练数据里的"平均答案"——而那正是最危险的一种答案。AI 放大的是已有知识的质量,不是替代它。
十二、给不同角色的一句话建议
| 角色 | 建议 |
|---|---|
| 一线 SRE | 下次值班,凡是你在文档里"卡住超过 1 分钟"的位置,截图存下来。一个月后把它们全改掉,你会得到一份真正好用的手册。 |
| 平台工程师 | 你的产出不该是"一个更好的 Wiki 平台",而是 probe 命令和 check-*.py 脚本——让知识可执行,比让知识可存放重要十倍。 |
| 后端开发 | 你服务对应的 Runbook 应该由你写。不是你欠 SRE 的,是你欠凌晨两点那个替你值班的人的。 |
| 技术决策者 | 评估知识管理水平别问"我们有多少篇文档",问"新人多久能独立值班"和"上周有几条过期红色项"。前者是存量,后者才是活性。 |
十三、和前面几篇的关系
- 第 6 篇(告警治理):那一篇扔出了"没有 Runbook 的告警等于没有告警",本篇回答"Runbook 从哪来、怎么保活"。两者是同一条流水线的两端:告警负责叫人,Runbook 负责让人做成事
- 第 8 篇(on-call 生存指南):本篇第五节的 14 段模板,是第 8 篇那个简化版模板的完整形态。区别在于:第 8 篇给的是"告警到达之后",本篇给的是"告警到达之前的建设"
- 第 13 篇(告警分级):Runbook 第 1.3 节的定级表必须与五级框架使用同一套词汇,否则 on-call 要在两套语言之间翻译,那是最容易出错的地方
- 第 17 篇(面板设计):面板负责"给判断",Runbook 负责"给动作"。一篇 Runbook 的第 8 段必须链到它对应的 L1 面板,一个面板的每个分区也该反向链回 Runbook
- 第 9 篇(从运维到 SRE):本篇是"消除 Toil"这条线的一次具体实践——把重复的排查、重复的救火、重复的"问一下老张",从人的记忆搬到系统里
- 第 15 篇(团队搭建):新人独立值班时间从 3 个月缩到 6 周,靠的不是更好的培训,而是可执行的手册。团队规模化的真正瓶颈常常是知识传递,不是招聘
- 第 16 篇(成本优化):红线七要求降本验收必须同时看钱和 SLO,本篇的"知识债"指标是同一个思路——知识管理的验收必须同时看"写没写"和"用不用得上"
写在最后
这篇文章如果只能带走一个观念,是这个:
运维知识不是文档,是系统的一部分。
一旦你这么看它,很多做法会自动改变:它会像代码一样有版本、有评审、有测试、有 owner;它会像监控指标一样有水位、有告警、会过期;它会像接口一样被上游(告警)引用、被下游(CLI)消费。
而如果只能带走一个动作,是这个:
挑一条你自己服务上的 page 级告警,现在就去写它的 Runbook,用第五节的 14 段模板,写给一个你从没见过的、凌晨三点被叫醒的人看。
写完你会发现两件事:第一,你比自己以为的更需要这份文档;第二,写它的过程中你会想起三件"只有你才知道"的小事——那三件事,就是团队真正的知识债。
那个夜晚的 12 分钟,我不想让团队里任何一个人再花第二次。
本文是 SRE 实战系列第 18 篇。它接的是第 6、8、13 篇在告警与 on-call 这条线上留下的尾巴:告警治理解决了"别叫醒不该叫的人",本篇解决"被叫醒的人怎么在 5 分钟内做成事"。
下一篇预告:故障演练与混沌工程——不是"演练给别人看",而是"发现 Runbook 里哪一步会卡住"。