系统是被打出来的 · 系统设计实战手册
附录 A · 急诊索引

病历卡图鉴
17 种故障的症状形状

事故现场通常不会先报出故障名称,它只留下一条曲线、一组分叉或一片过分安静的大盘。这里把全书病历按症状形状重新归组,让你从看见的证据反查可能的机理;每张卡还标了第 16 章总检用的机制轴——放大 / 一致性 / 单点,作为第二条查找路径。

怎么用这本图鉴

值班时,先按你在大盘上看到的形状找到下面六组,再在组内逐条核对症状文字。形状负责缩小搜索面,机理、易发场景与上下游证据负责确认诊断;不要只凭一根曲线直接开药。每张卡末尾还有一行机制轴标签——放大类 / 一致性类 / 单点类,那是第 16 章总检用的第二轴:形状回答“我看见了什么”,机制回答“该动哪一族处方、游戏日先打哪一类”。

同一个故障可能投下多种影子,同一种形状也可能来自不同机理。先用形状索引,再用业务账、调用链和时间线交叉验证。

同形状怎么排除——鉴别诊断表

形状只负责把嫌疑范围缩小到几个候选,不负责宣判。下表按你正在看的是哪块面板分组:同一组里的候选必须能在同一块面板上长成同一个样子,否则谈不上鉴别——一根时间轴上的尖刺、一张按分片切开的直方图、一笔对不平的业务账,是三块不同的面板,不能混在一组。每个候选配一条下一步证据,写清本卡该看到什么、看到别的该转向谁:证据若排除不了同组里至少一个候选,就还不算证据。本表不新增病历卡编号,只把已有的卡按「怎么排除」重排一遍。

你在看的面板候选诊断下一步证据 · 本卡该看到什么 → 否则转向谁
大盘平坦
聚合 RED 与基础设施曲线全绿,业务侧却有工单或账差
只是「聚合」看起来平坦,局部子集在报错(No.15 灰色故障)把错误率与延迟按实例、分区、可用区、版本、租户切片。本卡:某个子集显著偏离、其余正常,而且这些请求确实失败了。若切片后各子集都干净、请求全部成功只是结果不对 → 不是 No.15,转去做下面两行的对账。
同一实体两处存储的值不同(No.03 双写不一致)抽样同一 key,比较缓存里的值与 DB 里的版本号/更新时间。本卡:同一实体在两处的值不同,差异出现在最近一次写之后,且每一处内部自洽。若缓存与 DB 一致、业务台账却缺一条或多一条 → 转向 No.12。
同一事件被处理了 0 次或 ≥2 次(No.12 消息丢失与重复)用同一幂等键/消息 id 做三方对账:发送记录、broker 投递记录、消费结果。本卡:处理次数 ≠ 1(0 次是丢失,≥2 次是重复),而单条存储内部值是自洽的。若三方次数都是 1、只是缓存与 DB 值不同 → 转向 No.03。
时间轴尖刺
末端 QPS 或错误率的时间曲线,尖起又回落
真实入口尖峰:开售、推送、热搜把入口 QPS 与尖刺时刻叠在同一条时间轴上。本卡:入口与末端同一时刻、同一比例尖起。若入口曲线平稳、只有末端尖起 → 放大发生在系统内部,转向 No.05 / No.08。
一个热点 key 过期后的并发回源(No.05 缓存击穿)查尖刺窗口内的慢查询/回源日志。本卡:请求集中在同一个 key,该 key 的 miss 计数在同一秒跳升并与它的 TTL 到期对齐,尖刺前后这个 key 落到 DB 的量接近 0。若同一 key 在尖刺之外也持续高频落 DB → 那是持续热点,不是击穿,转向 No.10 的下钻;若落到 DB 的是大量不同的、查不到结果的 key → 转向 No.04;若尖刺里是很多不同 key、来自同一批客户端 → 转向 No.08。
一批等待者同刻醒来(No.08 惊群效应)看尖刺是否以固定周期重复,周期是否等于统一超时、固定 backoff 或定时任务间隔。本卡:周期性,每次尖刺内请求分散在很多 key、但发起时间挤在同一毫秒级窗口。若只出现一次、且集中在一个 key → 转向 No.05。
写后立刻读到旧值的毛刺(No.02 主从复制延迟)把错误率毛刺与复制延迟曲线、批量写入时刻叠在一起。本卡:报错的都是刚写完立刻读、且打到副本的请求,DB 上没有并发回源的尖刺,副本追平后自愈。若毛刺伴随 DB QPS 尖起、报错的不限于刚写过的 key → 转向 No.05。
单调爬升
末端负载或队列长度的时间曲线只升不落
真实新流量涌入,容量确实不够了看入口 QPS/DAU 是否与末端指标同步、同比例上涨。本卡:入口先涨、末端跟涨。若入口平稳、只有末端在涨 → 多出来的负载是系统自己造的,转向 No.07 / No.11。
重试放大(No.07 重试风暴)把入口 QPS、末端 QPS 与重试计数放在同一条时间轴。本卡:入口平稳,末端 QPS 与重试计数同步爬升。若末端处理速率已贴顶不动、涨的只是排队长度 → 转向 No.11。
消费速率长期低于生产速率(No.11 消息积压)看 backlog 与 oldest-message-age 是否同向上涨、消费曲线是否长期压在生产曲线之下。本卡:末端消费速率是平的(已到上限),涨的是欠债;暂停生产后 backlog 以消费速率线性下降。若末端速率本身还在往上爬 → 转向 No.07。
切片不对称
按分片 / 可用区 / 实例切开的直方图,一根柱子偏离其余——不是时间轴
一片被打热(No.10 热点分片)偏离的维度是负载:单片 QPS、CPU 或慢查询显著高。算最热 ÷ 平均,第 8 章的判据是大于 3;再下钻到 key,看是否少数 key 或某个场次贡献了大头。若最热 ÷ 平均在 3 以内、或偏离的不是负载而是错误率 → 转向 No.15。
一片坏了但负载正常(No.15 灰色故障)偏离的维度是错误率或延迟,而该子集的负载与其他子集持平甚至更低;把它摘流后用户报错立即消失。若摘流后热度只是转移到另一片 → 转向 No.10。
并发写者
业务台账:同一订单被扣两次、库存为负、同一键两条互相矛盾的记录;大盘往往平静
两个进程同时进入临界区(No.13 分布式锁失效)查锁服务日志与临界区日志。本卡:两个 owner 的持有区间重叠(后者 acquire 早于前者 release),或旧 fencing token 在新 token 之后仍写入成功。只看到 TTL 到期或续租失败不算——那只是嫌疑,没有重叠 owner 就排除不了 No.12。若临界区里始终只有一个 owner、重复来自同一条消息被投递两次 → 转向 No.12。
同一条消息被投递两次(No.12 消息重复)查 broker 的重投计数与消费者 ack 超时日志。本卡:同一 message-id 被投递 ≥2 次,两次执行是同一条消息,中间隔着一次消费超时或重启。若两次执行来自两条不同请求、同时进入临界区 → 转向 No.13。
时间戳裁决出了错误顺序(No.17 时钟漂移)把冲突两条记录的写入时间戳与业务因果顺序对比,并看节点时钟偏差监控。本卡:时间戳顺序与因果顺序矛盾(取消带着比支付更早的时间),偏差超过租约或合并规则的安全裕量。若时间戳顺序自洽、只是执行了两次 → 转向 No.12 / No.13。
两地各自当主(No.14 脑裂)看两地在同一时段是否都有写入成功日志。本卡:同一业务键出现两条互不继承的版本线,各自在自己的机房成功,复制恢复时冲突集中暴露。若只有一个机房在写、冲突发生在单集群内 → 转向 No.13。

第二轴:按机制归组

形状告诉你“看见了什么”,机制告诉你“该动哪一族处方”。第 16 章的收官总检把全书病历归成三种体质,这里把它接到图鉴上:每张卡末尾的「机制轴」一行标着它属于哪一类,本表是反向索引——从体质出发,找到同类的全部卡。同一张卡只在下面六个形状组里出现一次,这里只放指针。

体质共同机理处方族卡片
放大类小触发在系统内部长成比自身大得多的负载:并发回源、逐层重试、同步唤醒、沿依赖传播、按时间累积欠债、触发消失后仍自我供能。切断放大边:互斥回源、单一重试责任层与预算、jitter、舱壁与熔断、入口限流与慢放量。No.04 缓存穿透 · No.05 缓存击穿 · No.06 缓存雪崩 · No.07 重试风暴 · No.08 惊群效应 · No.09 级联故障 · No.11 消息积压 · No.16 亚稳态故障
一致性类同一份事实在时间或故障中被复制成两个版本:主与从、缓存与 DB、生产与消费、两个持有者、两个主、两只时钟。让真相可仲裁:写后读主与确认中语义、补偿与对账、幂等键与死信、fencing token 与持有者校验、多数派与单调版本。No.02 主从复制延迟 · No.03 缓存与 DB 双写不一致 · No.12 消息丢失与重复 · No.13 分布式锁失效 · No.14 脑裂 · No.17 时钟漂移
单点类一处共享闸门或一个被钉住的子集先耗尽,其余节点的余量无法接力;灰色故障是它的局部型——坏的不是负载,是被路由钉住的那个子集。打散与隔离:限并发与获取超时、按下游容量定池、热点隔离与二次打散、按切片观测并摘流。No.01 连接池耗尽 · No.10 热点分片 · No.15 灰色故障

这三类在第 16 章各对应一种游戏日实验与一种“证明没留后遗症”的方式:第 16 章 · 3 病历卡总检 →

01 · 断崖·阶跃型

指标垂直跳变,一眼可见:资源突然贴顶、保护层突然失效,或调用链从局部异常快速塌成全局不可用。

连接池耗尽

病历卡 No.01
Connection Pool Exhaustion · 有限槽位被占满,等待者持续堆积
机理
请求到达速度接近或超过连接归还速度,活跃连接占满池上限;新请求停在获取连接的等待队列里,等待又占住应用线程与内存,最终由超时把失败向上游传播。
症状
▁▂▅███ 阶跃打满。active 连接快速顶到上限后贴线不回,waiters 单调增加;应用 P99 和 502 飙升,但 DB 单条查询仍可能很快。
易发场景
突发流量;慢 SQL 或锁等待拉长连接持有时间;事务开启后夹杂远程调用;连接泄漏;应用实例扩容后总连接数超过数据库承载边界。
处方
① 限并发与快速失败:把到达量压回可验证容量;② 设置获取连接超时:不让等待队列无界增长;③ 缩短持有时间:优化慢查询,事务内不做远程等待;④ 按下游容量定总池大小:实例数增加时同步核算总连接。
案例
「秒抢」第 2 个月被本地媒体报道,入口从约 72 QPS 涨到 486 QPS。50 个连接槽打满、等待者升到 1,842,全站 502 达 61.8%;入口限并发后,DB 无需重启便恢复周转。
机制轴
单点类 · 有限连接槽是全部请求的共同闸门:应用实例再多,也只是在同一道门前排更长的队。机制轴索引 ↑

展开章节:第 2 章 · 第一次宕机——连接池耗尽之夜

缓存雪崩

病历卡 No.06
Cache Avalanche · 大面积同时失效
机理
击穿是一个 key 的事故,雪崩是一大片 key 同时失效。两种触发方式:同批 TTL 让大量 key 在同一时刻过期;缓存集群宕机或重启则让命中率断崖式归零,DB 直面全量流量。
症状
███▁▁▁▁ 断崖塌方。命中率垂直下坠;DB QPS 阶跃式打满并持续;线程池排队、上游超时,故障沿调用链向上蔓延成级联故障(病历卡 No.09)。
易发场景
定时任务统一写缓存;缓存集群升级、重启、故障切换;容器整批调度重建——任何让「很多 key 的生死绑在同一时刻」的操作。
处方
① TTL 加随机抖动,把过期洪峰摊开;② 集群高可用,用主从、持久化与分片缩小失效面;③ 熔断限流兜底,保护 DB;④ 预热与慢放量,避免冷缓存再次压垮存储。
案例
「秒抢」02:47 完成 Redis 集群滚动升级后,命中率从平时 98% 跌到 2.1%,DB 查询升到 31,400 QPS,订单线程池排队 2.3 万;团队先限流,再预热头部 key 并逐级放量。
机制轴
放大类 · 大片 key 同刻失效,本该被缓存吸收的总读量整体转嫁给 DB,冷缓存与过载 DB 还可能互相锁死。机制轴索引 ↑

展开章节:第 5 章 · 缓存三兄弟——穿透、击穿、雪崩

级联故障

病历卡 No.09
Cascading Failure · 一个依赖过载拖走整条调用链
机理
下游变慢占住上游线程、连接和队列;上游无法服务其他请求,又让更上游等待与重试。共享资源把局部故障传播成多服务不可用,即使各进程仍能通过探活。
症状
███▇▅▃▁ 逐层熄灭。调用链靠近故障点的服务先出现长尾,随后上游可用率按依赖顺序下降;探活可能仍绿,业务吞吐却接近归零。
易发场景
服务共用线程池或连接池;下游无超时和熔断;同步调用链过长;重试风暴放大流量;健康检查不覆盖真实业务依赖。
处方
① 熔断:失败越线后停止调用;② 舱壁隔离:每个依赖拥有独立资源上限;③ 限流降级:在传播前减量;④ 端到端超时:上游放弃后取消下游工作。
案例
「秒抢」支付渠道只是变慢,支付、订单、网关却依次耗尽工作线程;用户资料等无关接口也因共享池排队。没有组件宕机,平台仍在五分钟内从局部慢退化为全站超时。
机制轴
放大类 · 共享线程池与连接池把一处变慢传播成整条调用链的等待,扩散半径随依赖深度增长。机制轴索引 ↑

展开章节:第 7 章 · 重试风暴与级联故障——好心办坏事

02 · 尖刺型

瞬时脉冲,回落快:故障证据只在短窗口里出现,必须把尖刺与 TTL、写高峰、重试周期、进程停顿或故障切换对齐。

缓存击穿

病历卡 No.05
Hot Key Breakdown · 一个热点 key 的过期瞬间
机理
某个热点 key到达 TTL 过期。过期后的短窗口里,大量并发请求同时 miss、同时查询 DB、同时试图回源,DB 在一瞬间收到本该由一次回源解决的全部压力。
症状
▁▁▁█▁▁▁ 单点尖刺。DB QPS 出现毫秒级垂直尖刺后迅速回落;慢查询集中在同一条 SQL、同一个 key;时间点与该 key 的 TTL 对齐。
易发场景
开票页、热搜条目、首页配置——任何「读极多、key 极少」的数据。流量越大,过期瞬间的并发 miss 越密集,尖刺越致命。
处方
① 互斥回源(singleflight),同一 key 只放一个请求查 DB;② 逻辑过期,先返回旧值并异步刷新;③ 热点不设物理过期,由写路径主动更新。
案例
「秒抢」顶流场次详情 key 在开票高峰到期,同一时刻的大量 miss 集中查询一条 SQL,DB QPS 垂直冒出一根尖刺;加入 singleflight 后,每个实例只保留一个回源执行者。
机制轴
放大类 · 一个 key 的一次过期,被并发请求放大成同一瞬间的 N 次回源。机制轴索引 ↑

展开章节:第 5 章 · 缓存三兄弟——穿透、击穿、雪崩

惊群效应

病历卡 No.08
Thundering Herd · 大量等待者在同一时刻醒来
机理
大量请求同时超时,使用相同固定退避后又在同一时刻醒来;它们争抢刚恢复的少量资源,再次打满下游,形成周期性同步冲击。
症状
▁▁█▁▁█▁ 周期同步尖刺。QPS 和连接建立数按固定间隔出现尖峰,峰与统一超时、缓存 TTL、定时任务或固定 backoff 周期对齐。
易发场景
客户端使用相同固定重试间隔;服务恢复时积压请求同时放行;锁释放后大量等待线程竞争;同批 TTL 或定时任务在整点触发。
处方
① 随机抖动:在退避时间上加入扰动;② 慢放量:恢复后逐步提高并发;③ 请求合并:相同工作只让一个执行者完成;④ 错峰:TTL 与定时任务加入随机分布。
案例
「秒抢」第一版客户端统一等待 1 秒再试,渠道 QPS 每秒冒出一根尖刺;加入指数 backoff 后尖峰摊开,再加入 ±50% jitter,客户端不再在同一个刻度集体醒来。
机制轴
放大类 · 同步唤醒把分散在时间轴上的请求压进同一毫秒:平均负载没变,峰值成倍。机制轴索引 ↑

展开章节:第 7 章 · 重试风暴与级联故障——好心办坏事

主从复制延迟

病历卡 No.02
Replication Lag · 写入已提交,副本尚未重放
机理
主库提交与副本应用日志之间存在时间差。读路由把刚写完的查询送到尚未追平的副本,于是返回旧行、旧状态或空结果;写越密、重放越慢,暴露窗口越大。
症状
▁▁▃▁▁█▁ 随写高峰的毛刺。主库提交成功,副本查询却短暂未命中;业务错误率与复制延迟、批量写入在同一分钟抬升,待副本追平后自行回落。
易发场景
开票和支付后的立即查询;大事务或批任务与在线写共用复制通道;应用端有效并行度不够、追不上主库并发写;网络抖动后副本集中追赶。
处方
① 给关键写后读划主库通道;② 会话粘性,在安全窗口内读主;③ 缩短与监控重放,拆小大事务并区分接收差与应用差;④ 修正产品语义,复制窗口内显示处理中。
案例
「秒抢」本次开票中,主库提交成功率 99.98%,副本延迟从约 35ms 毛刺到 2.8s;刚支付的详情查询有 18.4% 返回未命中。强制写后读主并暂停大事务后,错误率随复制位点追平而回落。
机制轴
一致性类 · 主库与副本是同一份事实的两个版本,延迟窗口内“已提交”与“可读到”分叉。机制轴索引 ↑

展开章节:第 3 章 · 读写分离——以及复制延迟挖的坑

分布式锁失效

病历卡 No.13
Distributed Lock Failure · 持有锁不等于仍有写入资格
机理
锁没有过期导致遗留死锁;租约短于业务时间导致并发持有;释放时不校验 owner 误删新锁;主从切换丢失尚未复制的锁记录。应用仍按「拿过锁」继续写,临界区出现多个执行者。
症状
▁▁█▁▁ 低频但每次致命。平时吞吐与锁成功率正常,只在 GC 停顿、长尾任务或故障切换附近出现一次重复扣减、状态倒退或库存分叉,随后曲线恢复平静。
易发场景
业务时长接近锁 TTL;进程停顿或网络分区;释放使用无条件 DEL;锁服务异步复制后故障切换;旧持有者恢复后继续写下游。
处方
① 租约有界且可续;② owner 校验释放,比较与删除放在原子脚本;③ 业务幂等兜底;④ fencing token,让下游拒绝旧持有者写入。
案例
「秒抢」一次票权归还任务耗时从平时 400ms 抖到 6s,3s 锁已过期;第二个实例接手后,旧实例恢复并无条件删除新锁,第三个实例也进入临界区,最终同一张票被重复释放。
机制轴
一致性类 · 租期与持有者身份脱节,两个进程各自相信自己独占同一段临界区。机制轴索引 ↑

展开章节:第 11 章 · 幂等与分布式锁——用户狂点了八次购买

03 · 爬升不回落型

单调恶化,不自愈:先找到让斜率持续为正的反馈环或净流入,再谈扩容、重启和恢复时间。

重试风暴

病历卡 No.07
Retry Storm · 失败请求产生更多失败请求
机理
下游变慢或超时后,上游立即重试;多层独立重试按乘法放大末端请求。新增尝试继续占住线程与连接,造成更多超时,又生成下一轮重试。
症状
▁▂▄█████ 自持续爬升。原始入口流量稳定,末端 QPS、待发重试和超时率却持续上升;触发方恢复后,积压也可能让高位维持一段时间。
易发场景
调用链多层配置默认重试;超时后无 backoff;非幂等请求也重试;客户端、网关和服务各自只看局部成功率,没有端到端预算。
处方
① 指定重试责任层;② 重试预算,限制额外尝试占比;③ backoff + jitter,降低同步峰;④ 熔断,下游持续失败时快速停止调用。
案例
「秒抢」入口支付约 2,010 QPS,三层各尝试 3 次后,渠道端出现约 54,200 QPS;待发重试升到 18.6 万,原始流量没变,系统却被自己产生的尝试压满。
机制轴
放大类 · 每一层重试都把上游的一次失败变成下游的多次尝试,层数是指数的底。机制轴索引 ↑

展开章节:第 7 章 · 重试风暴与级联故障——好心办坏事

消息积压

病历卡 No.11
Message Backlog · 消费长期追不上生产
机理
生产速率持续高于消费速率,差值逐秒累积成待处理消息;消费变慢、分区并行度不足、下游限速或毒消息反复重试,都可能让出水口变窄。
症状
▁▂▃▄▅▆▇ 单调爬升不回落。backlog 与 oldest-message-age 同向上涨;生产和消费都不为零,但消费曲线长期压在生产曲线下方。
易发场景
开票洪峰;消费者发布后性能退化;分区数限制并行度;下游数据库限流;补算任务与在线消息争用同一消费集群。
处方
① 先改斜率:限制非核心生产并恢复消费性能;② 按瓶颈扩容:分区、消费者和下游容量一起核算;③ 算恢复窗口:用净消费速率估算追平时间;④ 隔离毒消息:有限重试后转死信。
案例
「秒抢」本次事故中,画像补算把消费吞吐压到 7,000/s,而生产维持 12,000/s;队列以 5,000/s 的净速率增长,积压达到 482 万条,最老消息等待超过 11 分钟。
机制轴
放大类 · 生产快于消费的差额按时间累积成欠债,一段小差额变成恢复期要一次偿还的大笔负载。机制轴索引 ↑

展开章节:第 9 章 · 消息队列——削峰填谷

亚稳态故障

病历卡 No.16
Metastable Failure · 触发离开,系统仍被反馈环锁在坏状态
机理
短暂触发先把系统推过临界点,随后超时、重试、缓存失效、积压或资源争用形成自我维持的反馈环。原始触发条件已经消失,系统内部产生的负载仍足以让它停在坏状态。
症状
▁█████ 触发消失后不回落。外部流量或最初故障已经恢复,错误率、负载或排队却维持高位;单纯重启某个组件后很快再次恶化。
易发场景
冷缓存与过载 DB 相互锁死;多层重试持续制造额外流量;队列积压与超时补偿争抢在线资源;恢复时一次性放回全部流量。
处方
先识别并切断反馈边:入口限流、暂停重试或隔离积压;再让核心依赖在受控负载下恢复,预热必要状态,最后分级慢放量。恢复标准要验证曲线能回到稳态,而不是只看进程重启成功。
案例
Facebook 在 2010 年的一次著名故障中,因配置校验逻辑的错误,所有客户端将缓存值判为无效并同时涌向数据库集群,最终不得不整站下线才恢复,全程约两个半小时(官方复盘公开)。官方来源:Facebook Engineering · More Details on Today's Outage
机制轴
放大类 · 触发撤走后,超时、重试与冷缓存组成的反馈环继续自我供能,负载已经不再需要外部原因。机制轴索引 ↑

展开章节:附录新增 · 互链 第 5 章死亡螺旋 / 第 7 章重试风暴

04 · 渗血·慢性型

缓慢劣化,容易被忽视:日常大盘可能只比昨天差一点,直到一次流量上升把长期累积的余量吃完。

缓存穿透

病历卡 No.04
Cache Penetration · 查询「根本不存在」的数据
机理
请求查询一个 DB 里根本不存在的 key。缓存里自然没有,于是每次都放行到 DB;DB 查不到,也就没有东西可以回填缓存。缓存对这类请求持续失效——它被打穿了。
症状
▁▂▂▃▃▄▄ 缓慢渗血。命中率持续偏低或缓慢阴跌;DB 慢查询日志里出现大量返回空结果的同构查询,key 往往是无规律的 ID。
易发场景
恶意脚本或爬虫枚举 ID(订单号、用户 ID 逐个试);上游 bug 传入非法参数;被删除数据的历史链接仍被外部引用。
处方
① 缓存空值,查无此数据也缓存短 TTL 标记;② 布隆过滤器,把全量合法 key 装入过滤器;③ 入口层参数校验与风控限速。
案例
「秒抢」上线三个月时遭遇过爬虫逐号枚举订单接口:命中率一周内从 98% 阴跌到 91%,没人在意——直到一次促销把 DB 推过了临界点。穿透的可怕在于它平时只是慢性病。
机制轴
放大类 · 缓存对无效 key 失去衰减作用,这部分请求以 100% 比例落到 DB——放大的不是倍数,是被绕过的那一层保护。机制轴索引 ↑

展开章节:第 5 章 · 缓存三兄弟——穿透、击穿、雪崩

05 · 无症状型

监控看不见,只有工单或对账能发现:基础设施曲线平稳,不代表业务结果正确;这组故障要靠主动比对制造可观测证据。

缓存与 DB 双写不一致

病历卡 No.03
Cache / DB Dual-write Inconsistency · 两份值在失败窗口里分叉
机理
一次业务更新要触碰 DB 与缓存两个独立系统,中间没有共同事务。某一步失败、超时或被并发读穿插后,一边保存新值,另一边继续提供旧值;高命中率会让错误绕开权威 DB。
症状
▁▁▁▁▁ 无症状(只有工单)。接口成功率、缓存命中率和 DB 延迟都可能正常;对账、抽样比对或用户投诉才暴露同一 key 的值分叉。
易发场景
cache-aside 写 DB 后漏删缓存;更新缓存成功但 DB 回滚;删除缓存与提交 DB 被并发读穿插;消息失效通知丢失;超长 TTL 让脏值长期存活。
处方
① 先定义权威源;② 缩短错误窗口,写 DB 后删除缓存并对失败重试;③ 可观测对账,抽样比较版本号和值;④ 幂等失效事件,让漏掉的删除能够补发。
案例
「秒抢」运营将场次价格从 ¥580 改为 ¥680,后台写 DB 成功却漏删缓存;热点 key 以 5,000 QPS 持续命中旧值,监控全绿,直到 327 张客服工单把双写裂缝照出来。
机制轴
一致性类 · 缓存与 DB 各持一份值,第二步失败后两份真相各自成立、互不知情。机制轴索引 ↑

展开章节:第 4 章 · 上缓存——以及你签下的一致性契约

消息丢失与重复

病历卡 No.12
Lost or Duplicate Delivery · 传输成功不等于业务只执行一次
机理
确认边界与业务提交不在同一个原子事务里:确认太早,处理失败后消息丢失;确认太晚,处理成功但确认丢失后消息会再次投递。可靠系统通常要明确选择偏向哪一侧,再由业务补齐另一侧。
症状
▁▁▁▁▁ 无症状(只有对账)。队列吞吐、延迟和错误率都可能正常,业务侧却出现订单缺口、重复出票或同一幂等键多条结果。
易发场景
消费者在业务提交与消息确认之间崩溃;网络超时让生产者不确定发送结果;人工重放死信;跨系统补偿任务重复执行。
处方
① 偏向 at-least-once,处理成功后再确认;② 业务幂等,请求方生成稳定幂等键并用数据库 UNIQUE 约束兜底;③ 端到端对账;④ 可控重放,保留原键与追踪链。
案例
「秒抢」曾在消费者滚动发布时出现确认回包丢失:队列再次投递了已完成的出票消息。队列大盘全绿,直到订单与票权日终对账出现重复记录,团队才发现防重只存在进程内存里。
机制轴
一致性类 · 发送方记录、broker 与消费者对“这条消息处理了几次”各有一份答案。机制轴索引 ↑

展开章节:第 9 章 · 消息队列——削峰填谷

灰色故障

病历卡 No.15
Gray Failure · 部分请求坏了,聚合健康信号仍然正常
机理
部分实例、分区、网络路径或请求组合持续失败,但粗粒度探活只验证进程存活,聚合指标又把少数异常摊薄。系统不是全好或全坏,而是对某些用户稳定地坏。
症状
▇▇▇▇ 探活全绿但用户报错。部分坏但探活正常。健康检查全绿、用户在报错;按实例、分区、版本或请求路径切片后,异常才从平均值里浮出来。
易发场景
滚动发布后只有一批实例异常;单分片或单可用区退化;某条网络路径丢包;依赖只对特定参数、租户或数据组合失败;探活未覆盖真实业务。
处方
把健康检查升级为业务合成探测;按实例、分区、版本、租户和路径切片 RED 指标;保留慢请求 trace 与错误日志;隔离异常子集并回滚,避免只盯集群平均。
案例
「秒抢」订单集群探活持续全绿,聚合错误率也低于告警线,但使用某个场次分区的用户稳定收到失败;团队按分区切片后定位异常副本,将它摘流并回滚。
机制轴
单点类 · 局部型:坏掉的是被路由钉住的一个子集——某批实例、某个分区或某条路径;健康节点的余量帮不上被钉在坏子集上的用户,探活又把它摊薄。机制轴索引 ↑

展开章节:附录新增 · 互链 第 13 章微服务拆分 / 第 14 章可观测性

06 · 分叉·不对称型

不是变差,是变得不一致:集群平均值可能健康,真正的证据藏在分片差、双线版本或互相矛盾的时间轴里。

热点分片

病历卡 No.10
Hot Shard · 业务热点被稳定映射到单个分片
机理
分片键分布或访问频率不均,一个高频 key、租户或时间段集中落到同一分片。集群总容量仍有余量,但最热分片先耗尽 CPU、连接或 I/O,其他节点无法自动分担。
症状
▁▁▁█▁▁ 单柱独高。集群平均 CPU 与 P99 看似可控,单片指标却显著高于其余分片;最热/平均超过热点判据,错误集中在特定 key 或租户。
易发场景
按 show_id 分片遇到顶流场次;按时间分片导致最新分区承接全部写入;大租户独占一个 key;递增 ID 的错误范围切分;虚拟节点过少造成环区间不均。
处方
① 热点隔离,使用独立通道与资源池;② 二次打散,在业务 key 后追加 bucket;③ 虚拟节点,平滑一致性哈希区间;④ 按分片观测,告警最热/平均。
案例
「秒抢」8 个订单分片总计 80,000 QPS,顶流 show_id=8842 占 62%,shard-03 独自承接 49,600 QPS;最热/平均达 4.96,其余七片仍有余量。
机制轴
单点类 · 分片键把最热的 key 钉在一台机器上,其余分片的余量无法替它服务。机制轴索引 ↑

展开章节:第 8 章 · 分库分表——以及热点分片

脑裂

病历卡 No.14
Split Brain · 两边同时认为自己有写入资格
机理
网络分区、错误成员变更或失效围栏让两个节点组同时自认主节点,并接受同一业务空间的写入;复制恢复后出现无法自动判定权威的冲突事实。
症状
██‖██ 双线分叉。两地写入吞吐都不为零、各自成功率正常,同一业务键的版本号或状态却沿两条线并行增长;复制恢复时冲突集中暴露。
易发场景
跨机房链路中断;两地独立自动提升;分区时紧急重配成员;DNS 只切流量但旧主仍可写;旧 token 未被下游拒绝。
处方
① 固定多数派,成员变更也走共识;② 双层围栏,入口停流加存储 token 拒绝;③ 单元化配额,减少跨城共享写;④ 业务仲裁,按支付、库存与状态机证据合并。
案例
「秒抢」容灾演练误触发两套本地提升逻辑,上海与杭州同时写入共享订单空间;冲突键以约 160/s 增长。团队围栏杭州写入、保全双边日志,并按合法多数派与业务账本逐笔收敛。
机制轴
一致性类 · 两个多数派各自接受写入,同一业务键长出两条互不继承的版本线。机制轴索引 ↑

展开章节:第 15 章 · 异地多活——以及脑裂

时钟漂移

病历卡 No.17
Clock Drift · 节点各自报时,事件顺序因此互相矛盾
机理
不同节点的物理时钟存在偏差,校时还可能让本地时间跳变。若系统用墙上时间判断写入新旧、租约先后或超时边界,同一组事件会在不同节点得到相反顺序。
症状
▁▁↕▁▁ 时间轴自相矛盾。分布式系统里现在几点是危险问题:日志先后、版本时间戳和业务状态互相冲突,较晚到达的事件可能带着更早的时间。
易发场景
跨机房节点各自校时;用本地时间生成全局版本;依赖 last-write-wins 合并订单状态;租约与锁只比较墙上时间;故障恢复后集中回放旧事件。
处方
业务顺序使用单调版本、共识日志位置或 fencing token;墙上时间只作观察线索,不单独承担写入资格;监控时钟偏差并为租约留安全裕量;冲突合并依赖业务状态机证据。
案例
「秒抢」两地恢复复制后,同一订单的取消事件晚到却带着更早时间,按 last-write-wins 合并会保留已失效的支付状态;团队改用单调业务版本与状态机规则仲裁。
机制轴
一致性类 · 每个节点各持一份“现在几点”,用墙上时间裁决先后,等于让多份真相互相仲裁。机制轴索引 ↑

展开章节:附录新增 · 互链 第 15 章异地多活与脑裂