面试 · 原理追问型
目标:考察对核心机制的深层理解——不是"是什么",而是"为什么这样设计""不这样会怎样"。 答题框架:是什么 → 为什么 → 不这样会怎样 → 如何验证(视题目类型选用适用步骤)。
1. 为什么 SLO 不能设为 100%?从工程经济学角度解释。#
参考答案:
为什么不能 100%:
成本维度:每增加一个"九",成本呈指数增长。99.9% → 99.99% 通常需要多 AZ 冗余、更严格的变更流程、更多的 on-call 覆盖,成本 3-5 倍。99.99% → 99.999% 又是 3-5 倍。
边际收益维度:用户感知不出来 99.99% 和 99.999% 的区别(每年只差 47 分钟可用)。额外投入的工程资源放在新功能开发上,对用户价值的边际收益可能远大于追求更高的可用性。
创新维度:100% 意味着任何变更都有风险 → 不敢发布 → 系统变成化石。SRE 理论认为:允许一定比例的失败是实现快速迭代的前提。没有 Error Budget 就没有发布自由。
可行性维度:分布式系统的依赖链上(DNS、网络、硬件、云服务商)任何一个环节都不在你的控制范围内。你不可能承诺自己控制不了的东西。
不这样会怎样:追求 100% 的团队反而出更多事故——发布慢导致每次改动大,回归测试覆盖不住。Google 内部观察:SLO 设过高的团队,事故率比 SLO 适中的团队高。
如何验证:季度回顾 SLO 达标率和成本投入。如果 SLO 长期 100% 达标但团队没有创新产出,说明 SLO 设低了(应该提高目标或增加功能开发);如果 SLO 频繁违规且团队疲于救火,说明 SLO 设高了。
2. 错误预算机制为什么能平衡"发布速度"和"系统稳定性"?#
参考答案:
为什么能平衡:
传统模式是两个极端的博弈:
- 开发想快速发版 → SRE 凭感觉拦"最近不太稳" → 立场之争
- 或者 SRE 放弃抵抗 → 系统不稳定,没人负责
Error Budget 引入了一个中立的量化裁判:
- 稳定性好(Budget 充足)→ 自动绿灯,开发有发布自主权
- 稳定性差(Budget 耗尽)→ 自动冻结,不是 SRE 拦你,是数据在说话
双向激励效应:
- 开发有动力提高稳定性 → 稳定性好 → Budget 多 → 可以多发版
- SRE 有动力降低告警噪音 → 误报少 → Budget 不被误消耗 → 开发不抱怨
本质是把速度和稳定放在同一把尺子上量,用数据替代主观判断。
不这样会怎样:没有 Error Budget,发布决策变成"谁嗓门大"——开发强势就乱发版出事故,SRE 强势就拦发布拖慢业务。两种极端都是组织失败。
如何验证:Error Budget 机制生效的标志——发布决策有明确数据依据("Budget 还剩 X%,可以发"),不再有"SRE 拦不拦"的争论。
3. 多窗口燃烧率告警的原理是什么?为什么短窗口和长窗口必须用 AND 而不是 OR?#
参考答案:
原理:用一个短窗口确认"现在正在出问题"(高召回率),用一个长窗口确认"问题在持续而非毛刺"(高精确率),两者同时满足才告警。
为什么必须 AND:
- AND:短窗 TRUE 且 长窗 TRUE → 触发。真实持续性故障会在两个窗口都超标。
- OR:短窗 TRUE 或 长窗 TRUE → 触发。短窗口因为瞬时毛刺频繁触发(误报),长窗口因为历史故障的"尾巴"持续触发(信号陈旧)。
AND 的实际效果:瞬时尖刺会被长窗口自动过滤掉;持续 3 天但烧得很慢的故障会被短窗口(设置较高的燃烧率阈值)过滤掉——因为烧得慢的故障短窗口值很低。
不这样会怎样:
- 用 OR:告警噪音爆炸,工程师开始脱敏
- 只用短窗口:毛刺频繁误报
- 只用长窗口:响应太慢,故障已经持续一天才发现
如何验证:上线多窗口告警后,对比告警数量和真阳率。健康的指标:告警数下降 50%+,真阳率提升到 70%+。
4. 熔断器的三种状态(CLOSED/OPEN/HALF-OPEN)是如何流转的?为什么需要 HALF-OPEN 状态?#
参考答案:
状态流转:
CLOSED → 失败次数达阈值 → OPEN → 冷却时间到 → HALF-OPEN → 探测成功 → CLOSED
→ 探测失败 → OPEN(回到熔断)为什么需要 HALF-OPEN:
如果直接从 OPEN 回到 CLOSED,有两个问题:
- 设了 60s 熔断时长,时间到了不确定下游是否恢复
- 直接放全量流量过去 → 如果下游还没恢复 → 上游再次被拖垮 → 又需要一次故障积累才熔断
HALF-OPEN 的微妙之处:只放行极少量探测请求(1-3 个),用最小代价测试下游是否恢复。成功了再逐步放开;失败了继续熔断,而不需要重新积累失败次数。
不这样会怎样:没有 HALF-OPEN 的熔断器在恢复阶段会产生"震荡"——OPEN → CLOSED → 又被错误打回 OPEN → 反复横跳,用户体验极差。
如何验证:监控熔断器状态变化次数。HALF-OPEN 成功率低说明下游还没真正恢复,需要调长冷却时间。
5. 为什么重试必须用"指数退避 + Jitter"?如果不加 Jitter 会怎样?#
参考答案:
指数退避的必要性:
- 线性退避(1s, 2s, 3s)在持续故障时总重试时间仍然较短,下游压力减缓不够
- 指数退避(100ms, 200ms, 400ms, 800ms...)越长间隔越大,给下游更多恢复时间
Jitter(随机抖动)的必要性:
无 Jitter 时:100 个调用方同时发现下游故障,同时开始重试,同时用固定间隔 1s → 在秒级粒度上,100 个重试请求几乎同时到达下游 → 下游刚恢复就被瞬间打死(惊群效应)。
有 Jitter 时:100 个调用方的重试时间散布在 1s-2s 的窗口内,下游的请求是平滑的,有喘息空间。
不这样会怎样:不加 Jitter 的重试 = 定时炸弹。某次大规模故障时,所有客户端同时重试 → 下游永远恢复不了 → 重试风暴持续放大 → 雪崩加速。
如何验证:压测时模拟下游故障,观察恢复瞬间的请求量。有 Jitter 的应该是平滑曲线,无 Jitter 的会有明显尖刺。
6. 为什么 HPA 不适合常驻内存型服务?底层原理是什么?#
参考答案:
为什么不适合:
HPA 基于内存使用率判定的逻辑是:内存用得多 → 需要更多副本分担 → 扩 Pod。
但常驻内存型服务(文件代理、Node.js 堆、挂载缓冲区)的内存使用与业务量脱钩:
- 不管 QPS 多少,单 Pod 的内存基线都是 2Gi(因为加载了文件索引/缓存/堆)
- 加 10 个 Pod → 每个 Pod 还是 2Gi → 总计 20Gi → 无意义
死循环:内存使用率超标 → HPA 扩副本 → 单 Pod 内存不变 → 使用率还是超标 → HPA 继续扩。
不这样会怎样:A 旗舰项目生产环境真实踩过的坑——13 个 Pod 伺候 0.09 核的活。内存 HPA 把副本从 4 扩到 13,每个 Pod 内存还是 2Gi,完全资源浪费,还撞满专用节点池导致发版超时。
如何验证:观察 HPA 扩容前后单 Pod 内存变化。如果扩容后单 Pod 内存不变(或只降一点点),说明内存与业务量脱钩,不适合内存 HPA。
改进方案:对常驻内存型服务只用 CPU 或自定义 QPS 扩容指标,禁用内存 HPA。
7. 为什么 postmortem 需要"运气成分(Where we got lucky)"这一栏?#
参考答案:
为什么需要:这一栏是对人性弱点的刻意纠正。事故恢复后人类天然倾向"还好没事"的如释重负感,会忽略"这次没死纯属运气"的事实。
识别运气因素的价值:
- 破除侥幸心理。如果事故发生在高峰时段可能严重 10 倍——现在就修,不要等高峰。
- 暴露未验证的保护机制。上一次 image 还在 registry 所以 rollback 快——如果 registry 也被清了呢?
- 推动预防性改进。把依赖运气的事情变成系统保证的事情。
不这样会怎样:没有"运气"栏的复盘容易变成"我们处理得很好"的自我表扬。真正危险的隐患被"这次没事"掩盖,下次在更坏条件下爆发。
如何验证:回顾历史 postmortem 的"运气"栏,看里面识别的隐患是否都被修复了。如果同样的"运气"反复出现,说明复盘没有真正落地。
本质上,这一栏是把"我们逃过一劫"转化为"我们发现了哪些该修没修的东西"。
8. 混沌工程的"假设驱动"为什么这么重要?不做假设直接随机注入的后果是什么?#
参考答案:
为什么重要:
假设驱动的价值:
- 把演练从"撞大运"变成"验证已知风险"——每个实验验证一个具体的工程信念
- 有明确的成功/失败标准——假设成立 = 通过,不成立 = 有 Action Item
- 复盘时有对照——不是"系统好像还行",而是"假设 3 不成立,错误率超了 0.7%"
没有假设的演练:
- 随机 kill pod,然后说"系统没挂"——结论空洞,没有可行动的发现
- 不知道在测试什么,也就不知道观测什么
- 复盘没有标尺
不这样会怎样:没有假设的"混沌工程"是骚扰,不是工程。写不清楚"我验证的是什么",就不该做这个实验。Google 和 Netflix 都强调"混沌工程是实验,不是破坏"。
如何验证:每次演练前 review 假设文档。如果假设写不清楚("看看系统会不会挂"不算假设),打回重写。演练后对照假设逐条验证。
9. 为什么"一个缺陷往往是一类"?从真实故障案例说明。#
参考答案:
为什么是一类:这个规律来自生产经验的归纳——同一类配置错误几乎不可能只出现在一个组件上。团队的配置模板、部署脚本、架构约定都是批量应用的。如果 A 组件的配置有缺陷,使用同一套模板/约定的 B、C 组件大概率也有同样的缺陷。
真实案例(A 旗舰 passport 故障):
- 系统池节点带 taint,istio gateway 缺 toleration → Pod Pending → passport 全挂
- 修复后复盘发现:之前修 cert-manager 时已经发现 system-pool 缺 toleration
- 但只修了 cert-manager,没排查同款的 istio gateway
- 结果同一个缺陷、同一个集群、同一类组件,修了一个漏了一个 → 出 P0
不这样会怎样:只修单点的后果——同样的故障换个组件再发生一次。passport 故障就是"修了一个漏了一个"的直接代价。
如何验证:发现任何配置缺陷后,必须 grep 全集群排查同款。验证方法——列出所有使用相同 nodeSelector/taint/HPA 配置的组件,逐一检查。
排查心法:发现任何配置缺陷(taint、HPA 误配、IAM 兜底),立即 grep 全集群:"还有谁是同款?"
10. 死机开关(Dead Man's Switch)的反直觉设计原理是什么?为什么它值得信赖?#
参考答案:
反直觉之处:正常情况下,死机开关一直在响。传统告警的逻辑是"不出事不响",而死机开关是"不出事反而应该持续收到它的心跳"。
为什么值得信赖:
- 它检测的是告警链路整体沉默——告警系统全挂了、消息通道断了,不会有任何告警。
- 如果正常运作,Watchdog 持续触发 → healthchecks.io 规律收到心跳 → 无动作。
- 如果 Watchdog 失联(Prometheus 挂了 / Alertmanager 挂了 / 网络断了)→ 健康检查服务在 N 分钟无心跳后独立触发反向告警。
不这样会怎样:没有死机开关,"没有告警"可能意味着"一切正常",也可能意味着"告警系统挂了"——你无法区分。监控系统自己死了没人知道是最危险的盲区。
如何验证:主动 kill 一个 Alertmanager pod,2 分钟后应该收到反向告警。如果没收到,说明死机开关本身有问题。
关键设计:反向告警通道必须独立于主告警通道(不同群、不同 webhook、最好不同云 region)。否则主通道挂了反向告警也发不出去。
11. Kubernetes 中 PodDisruptionBudget(PDB)为什么在混沌工程中被验证为重要但常被遗漏?#
参考答案:
为什么常被遗漏:
- PDB 不影响非自愿驱逐(节点宕机、OOM),平时看不出效果
- 只有在滚动更新、节点维护驱逐时才生效——这些操作本身不频繁
- 一般配置 Deployment 时不会默认生成 PDB
- K8s 文档里 PDB 不在"必配"清单
为什么重要:PDB 限制了一次自愿性驱逐中同时不可用的 Pod 数量。没有 PDB,一次节点维护可能同时驱逐多个 Pod,导致服务短暂不可用。
混沌工程暴露 PDB 缺失:一次简单的 kubectl delete pod 演练发现整个 namespace 都没配 PDB——虽然单 kill 没触发,但节点维护时如果同时驱逐所有 Pod 服务就挂了。
不这样会怎样:没有 PDB 的服务在节点维护时可能全挂。K8s 的 drain 操作会驱逐节点上所有 Pod,没有 PDB 保护就一次性全下线。
如何验证:kubectl get pdb -A 检查所有 namespace。每个有副本的服务都应该有对应的 PDB。新服务上线准入清单里必须有 PDB。
12. 为什么"先止损后找根因"是事故响应的核心原则?先找根因再止损的风险是什么?#
参考答案:
为什么先止损:这个原则基于一个残酷事实——找根因的时间远大于止损的时间,且根因不修好业务已经在挂了。
先找根因的风险:
- 花 1 小时定位根因 → 业务也已经挂了 1 小时 → 用户流失了
- 在查根因的过程中可能又触发了新的雪崩(排查操作本身有时也危险)
- 根因分析需要日志、trace、snapshot——系统挂的时候这些信号可能也在丢
先止损的优势:
- 止损往往有明确路径(回滚、切流、重启)→ 几分钟能恢复
- 系统恢复后面临压力消失 → 有更冷静的环境来查根因
- 用户影响最小化——这是事故响应的第一目标
不这样会怎样:先查根因的团队典型表现——事故群里 30 个人在猜原因,业务挂了 1 小时还没人执行回滚。
如何验证:每次事故后复盘"从事故发生到止损执行用了多长时间"。健康指标——止损决策 < 15 分钟,根因分析可以在系统恢复后慢慢做。
简单判断:如果有个操作能立刻让业务恢复(回滚到上个版本),先做。这个版本的问题可以在 staging 环境慢慢复现分析。
13. 为什么 Prometheus 的 Pull 模型比 Push 模型更适合长驻服务监控?#
参考答案:
为什么 Pull 更适合:
天然存活检测:Pull 模型下,监控系统周期性抓 target。抓不到 = target 死了。这个"抓不到即判死"的免费存活检测是 Pull 最被低估的优点。Push 模型需要额外的心跳机制判断 target 是否存活。
监控端掌控频率:Pull 的抓取频率由监控系统决定,不会因为被监控方配置错误而打爆收集器。Push 模型下,被监控方可能因为 bug 疯狂推数据,收集器扛不住。
target 无状态:Pull 模型下,target 只需要暴露
/metrics端点,不需要知道监控系统的存在。Push 模型下,target 需要配置收集器地址,增加耦合。服务发现:Pull 模型配合服务发现(K8s Service Discovery)可以自动发现新 target,不需要 target 主动注册。
不这样会怎样:长驻服务用 Push 的问题——每个服务都要配置 push 地址、管理 push 频率、处理 push 失败。运维复杂度高,且 target 死了不会自动被发现。
如何验证:用 Pull 模型的 Prometheus,配 up 指标告警。up == 0 持续 1 分钟 = target 不可达。这是最简单可靠的存活检测。
Push 的适用场景:短命任务(批处理、Serverless)不适合 Pull——任务跑完就退出,监控系统抓不到。这时用 Push Gateway 兜底。
14. 为什么 Alertmanager 的 silence 必须用负匹配排除 Watchdog?用 .+ 会怎样?#
参考答案:
为什么必须排除 Watchdog:
Watchdog 是"永远在 firing"的死机开关告警。如果 silence 用 alertname=~".+"(匹配所有告警名),会把 Watchdog 也 silence 掉 → healthchecks.io 收不到心跳 → 反向告警机制失效。
用 .+ 会怎样(真实事故):
某团队上线日临时 silence 所有告警,写了 alertname=~".+",把 Watchdog 心跳一并屏蔽。反向告警机制失效几小时无人发现。等监控大屏出现长时间空白才反应过来,但当时所有人都以为"上线日就是会一阵静默"。
正确做法:所有"全部 silence"必须显式排除 Watchdog:
amtool silence add alertname!=Watchdog --duration=2h --comment="上线窗口"或调 v2 API 用 isEqual: false:
{"matchers": [{"name":"alertname","value":"Watchdog","isRegex":false,"isEqual":false}]}如何验证:silence 创建后,检查 Watchdog 是否被命中:
curl -s "$AM_URL/api/v2/alerts?filter=alertname=Watchdog" | jq '.[].status.silencedBy'应该返回空数组。如果非空,说明 Watchdog 被 silence 了,立即修复。
15. 为什么 Recording Rules 的 interval 必须 ≤ 告警 for 时间的一半?#
参考答案:
为什么:
Recording Rule 的 interval 决定了预计算指标的刷新频率。如果 interval 5m,告警 for: 2m,会出现:
T+0: Recording Rule 刷新,指标值 = 10
T+2m: 告警条件满足,但指标还是 T+0 的值(5m 没到)
T+5m: Recording Rule 刷新,指标值 = 15
T+7m: 告警才真正触发(用了 5m 前的数据)结果是告警延迟严重,且行为异常——明明 for: 2m 但实际等了 7 分钟才触发。
不这样会怎样:
- 告警延迟不可预测
- 告警行为和配置不符(
for: 2m实际等更久) - 调试困难——看告警规则配置没问题,但行为就是不对
如何验证:对比 Recording Rule 的实际刷新时间和告警触发时间。如果告警总是在 Recording Rule 刷新后立即触发,说明 interval 合理;如果告警延迟严重,说明 interval 太长。
经验值:Recording Rule interval 30s,适合大多数告警 for: 2m-15m。如果告警 for: 1m,Recording Rule interval 应该 ≤ 30s。
16. 为什么 blameless 文化不是"不追责"?它和"无责任"有什么区别?#
参考答案:
为什么不是"不追责":
Blameless 是把"人为什么会犯错"当成系统问题来分析,而不是"谁做错了"。同样是"改错了生产配置":
- 有指责的追问:"谁改的?"
- Blameless 追问:"为什么这个配置是人手改的而不是 GitOps 管的?为什么没有 peer review?为什么没有 dry-run?"
结论永远指向系统改进,而不是"以后小心点"。"以后小心点"是无效的——人在疲劳、压力下必然会犯错,系统设计必须假设人会犯错。
和"无责任"的区别:
Blameless 不等于"什么都能原谅"。以下行为不属于 blameless 保护范围:
- 恶意破坏:故意删数据、注入后门
- 重复犯错:同样的错误第三次,是态度问题
- 违反明确规则:明令禁止 prod 直接操作却违反
- 隐瞒事故:出了事不报,私下处理
这些需要单独处理,不在 postmortem 范围内。
不这样会怎样:没有 blameless 的复盘 = 追责会。工程师不敢说真话,根因永远是"操作失误"。真正的系统性缺陷被掩盖,同类事故反复发生。
如何验证:观察复盘会议的氛围。如果工程师敢于说"我改了这个配置,以为没影响,结果...",说明 blameless 文化到位;如果大家都在推卸责任、隐藏细节,说明文化有问题。
blameless 的核心信念:人是系统的产物。改变系统比改变人更可靠。追责会让人隐藏错误,blameless 会让人暴露错误——只有暴露才能改进。
17. 为什么给正在级联失败(cascading failure)的系统"加机器"往往救不了?必须先做什么?#
参考答案:
级联失败的机制:一个节点过载/挂掉 → 它的负载转移到剩余节点 → 剩余节点也过载 → 逐个倒下 → 正反馈雪崩。队列与线程池的耗尽是典型正反馈:请求堆积 → 队列满 → 线程全阻塞在等待 → 新请求继续进队列 → 更满,光加线程/加机器停不下来。
为什么加机器救不了(正在级联时):
- 来不及:新机器启动要时间(拉镜像、预热、建连接池、填缓存),级联以秒级推进,扩容以分钟级到位——赶不上。
- 一上线就被打死:新实例还没预热就被同样的过量负载打满,甚至加剧问题。
- 被判不健康摘除:过载态下健康检查也在超时 → 新实例可能刚起来就被判死摘掉 → 白扩。
- 启动本身耗资源:JIT、缓存回源、连接建立,正是系统最缺资源时雪上加霜。
必须先做的(打破正反馈):
- 先丢负载:load shedding / 限流,把入口流量压到剩余容量之下,让系统喘息。
- 减少启动期工作:lazy 加载、跳过预热、临时关非核心,降低新实例上线成本。
- 切断正反馈源:关掉会放大的重试(或加大退避 + Jitter)、有界队列拒绝而非排队、熔断下游。
- 等回到稳态再扩容:只有当负载 < 容量、系统稳住,扩容才有意义。
不这样会怎样:过载态直接扩容 = 往火里加柴,典型表现是"越扩越挂"。
如何验证:混沌演练模拟过载,对比"直接扩容" vs "先丢负载再扩容"的恢复曲线——前者常常拉不回来,后者能在丢负载后迅速收敛。
18. 什么是尾延迟放大(tail-at-scale)?为什么 fan-out 越大 P99 越糟?#
参考答案:
现象:一个请求扇出(fan-out)到 N 个后端,必须等全部返回才能响应 → 整体延迟取决于最慢的那个 → N 越大,"至少有一个慢"的概率越大 → 整体 P99 远差于单后端 P99。
为什么 fan-out 放大尾延迟(数字直觉):假设单后端 99% 的请求都快(只有 1% 慢)。扇出到 100 个后端,一个请求要"全部都不慢"的概率 = 0.99¹⁰⁰ ≈ 37% → 意味着 约 63% 的请求会命中至少一个慢后端。单后端罕见的尾部事件(1%),被扇出放大成了整体请求的"过半数会慢"。
为什么重要:搜索、推荐、聚合类高扇出架构里,优化平均延迟几乎没用——用户体验由尾部主导,"平均很好但总有人卡"。
治理手段(Google《The Tail at Scale》):
- 对冲请求(hedged requests):等一小会儿还没回,就向副本再发一个,谁先回用谁。
- 绑定请求(tied requests):同时发给两个副本,谁开始处理就取消另一个。
- 慢节点探测与摘除 + 分位数感知的负载均衡 + 微分区。
- 缩小扇出:合并请求、能异步的别同步等。
不这样会怎样:只盯平均延迟的高扇出系统,P99/P999 会被扇出无声放大,SLO 长期不达标却找不到"哪台机器慢"——因为不是某一台慢,是概率在放大。
如何验证:对比单后端 P99 和聚合请求 P99,若聚合远差于单点,且随扇出数上升而恶化,就是尾延迟放大;上线对冲/绑定请求后观察聚合 P99 是否回落。
答题技巧:原理追问题的关键是深挖"为什么"。不要停留在"是什么",要解释设计背后的原理、不这样做的后果、如何验证设计有效。框架:是什么 → 为什么 → 不这样会怎样 → 如何验证。