BodySense Redis 第 12 课:Failure Engineering 与 Capacity
以 Redis Down、Restart、Slow、Memory Full、Connection Exhaustion、AOF Recovery 和丢通知为故障实验,建立按 state ownership 选择 fail closed、fallback 或 best-effort 的最终能力。
[!info] related notes
- 课程 MOC:BodySense Redis 从零到一课程 MOC
- 前置课程:第 11 课
- 对应知识笔记:Redis 运维、观测与故障语义、AI / Agent Fallback Strategy
- BodySense:BodySense 测试策略、BodySense 可观测性设计
BodySense Redis 第 12 课:Failure Engineering 与 Capacity
[!abstract] 最终目标 不看实现代码,仅根据一份 Redis 状态的 ownership、durability、security 和 recovery 要求,就能判断:Redis 挂掉时应该 fail closed、fallback,还是 best-effort continue。
先从零解释:Failure Engineering 是什么
Failure Engineering 可以理解成:
不要只证明系统在“正常情况下能跑”,而要主动定义、制造和验证故障情况下系统应该怎么失败、怎么恢复。
这和普通功能测试不同。
功能测试问:
SET 成功吗?
Login 正常吗?
Refresh 能轮换吗?
Failure Engineering 继续问:
Redis 完全断开呢?
只变慢但没断呢?
命令执行成功但响应丢了呢?
内存满了呢?
重启后只恢复到稍旧状态呢?
1000 个请求同时 retry 呢?
目标不是让系统“永远不失败”。
真实目标是:
failure is expected
↓
behavior is bounded
↓
security/correctness invariants remain explicit
↓
recovery path is known and testable
先建立一个 Fault Model:故障不只有 Down / Up
1. Hard Down / Unavailable
connection refused
service stopped
endpoint unreachable
最容易理解:完全访问不了。
2. Slow / Degraded
GET 本来 2ms
→ 现在 2s
它往往比 hard down 更危险,因为连接和请求会长时间占着资源,造成排队放大。
3. Partial Network Failure / Partition
Client A 能访问
Client B 不能访问
或
request 发到了 Redis
response 回不来
系统不同参与者看到的世界不一致。
4. Resource Exhaustion
memory full
maxclients reached
client pool exhausted
CPU saturated
disk full during persistence
服务进程可能还活着,但无法完成某类操作。
5. Stale Recovery State
例如 AOF everysec 的 durability window:
crash
→ restart
→ Redis 回来了
但最近某些 mutation 没恢复
“Redis 已启动”不等于“业务状态必然和 crash 前完全一样”。
Failure Policy:故障时到底应该怎样
本课程反复出现三种主要策略。
Fail Closed
无法确认安全条件
→ 拒绝继续
适合安全 authority,例如 BodySense Session。
Fallback
优化层失败
→ 使用较慢但正确的 authority path
适合 Cache。
Best-effort Continue + Reconcile
辅助通知失败
→ durable truth 已在别处
→ 继续主流程
→ 后续 reconcile
适合 Pub/Sub wakeup。
所以 Failure Policy 不是 Redis 全局配置,而是由这份状态的 ownership决定。
两个生产概念:Capacity 与 Headroom
Capacity 不是只有“Redis 有多少 GB RAM”。
系统容量至少有多个维度:
memory bytes
key count
ops/sec
CPU time
network bandwidth
client connections
client output buffers
persistence disk throughput
fork / rewrite overhead
hot-key concentration
Headroom 是:
当前正常峰值距离硬上限还剩多少安全余量。
例如:
maxmemory = 8 GB
peak used = 7.9 GB
技术上“平时没 OOM”,但几乎没有 headroom。
当 AOF rewrite、流量 burst、临时复制 buffer 或大 Key 写入发生时,就可能立即越界。
所以生产设计不是:
平均只用 50%
→ 一定安全
而要看:
peak
burst
distribution
failure/recovery overhead
Retry Storm:重试也会制造事故
直觉上:
请求失败了,多重试几次不是更可靠?
但如果 Redis 已经过载:
1000 original requests
× 5 retries
→ 最多 5000 additional attempts
负载越高:
timeout ↑
→ retry ↑
→ load ↑
→ timeout further ↑
形成正反馈。
因此 retry 必须有:
bounded attempts
deadline
backoff
jitter
error classification
idempotency / safe retry semantics
不是所有 Redis 错误都应该自动重试。
Ambiguous Outcome:最危险的不是“失败”,而是“不知道成功没有”
网络系统常见:
Client sends command
↓
Redis executes mutation
↓
response packet lost
↓
Client sees timeout
客户端此时知道的是:
我没收到成功响应
而不是:
Redis 一定没执行
这个状态叫 ambiguous outcome / outcome unknown。
如果操作是:
GET
重试通常问题小。
如果操作是:
consume one-time token
increment billing counter
claim job
execute side effect
盲目重试可能重复副作用。
这就是为什么幂等 identity、compare-and-set、owner token 等机制如此重要。
Blast Radius 与 Failure Domain
Failure Domain 指一组会因为同一个底层故障一起受影响的东西。
例如同一个 Redis instance 里的:
DB 0 → Session
DB 1 → Cache
DB 2 → Notification
虽然 logical DB 编号不同,但仍共享:
同一个 process
同一份 maxmemory
同一套 eviction policy
同一次 restart
同一个 CPU
同一个 endpoint
所以它们仍在同一 Failure Domain。
Blast Radius 就是:
一个故障发生时,最终波及多少业务能力。
把 disposable Cache 和 security-critical Session 混在一起,可能让一个 Cache memory spike 扩大成认证事故。
为什么最后一课不是“更多 Redis 命令”
真实生产问题通常不是:
SET 怎么写?
而是:
Redis 断了会怎样?
慢了会怎样?
内存满了会怎样?
重启恢复后状态会怎样?
连接池耗尽会怎样?
一条通知丢了是否影响正确性?
这决定系统是否真正理解 Redis。
Fault 1:Redis Down——同一个故障,不同业务能力应有不同答案
先不要问:
Redis 挂了怎么办?
先问:
Redis 在这个调用链里扮演什么角色?
Session Authority
GET/EXISTS session:<sid>
→ Redis unavailable
→ cannot determine live/revoked
→ 503 fail closed
这里不能 fallback 到“只验 JWT”,因为那会临时关闭 revocation authority。
Cache
GET workspace:u1
→ Redis unavailable
→ query PostgreSQL/domain source
→ correct but slower
这里如果也直接 503,就把一个本来可选的优化层升级成硬依赖。
Pub/Sub Notifier
cannot receive/publish wakeup
→ durable RuntimeEvent still exists in PostgreSQL
→ periodic reconciliation / reconnect
这里可以 best-effort continue。
Job Wakeup
Redis notify lost
→ PostgreSQL job row remains
→ recovery sweep later claims
正确性应退化为“更慢启动”,而不是“任务消失”。
所以一张 Failure Matrix 比“Redis 故障统一重试三次”更有价值。
Fault 2:Redis Restart
必须问:
是否有 persistence?
哪些 Key 恢复?
TTL 是否仍有正确语义?
刚发生的 rotation/revoke 是否可能落在 durability window?
客户端如何 reconnect?
Pub/Sub subscription 如何恢复?
Restart 不是简单的:
process back online
→ everything fine
Fault 3:Redis Slow——比完全 Down 更容易形成级联故障
Hard down 往往很快返回 connection error。
Slow failure 可能:
request enters
→ waits Redis 8s
→ occupies goroutine/task
→ occupies client connection/pool slot
→ upstream request timeout
→ client retries
→ more load
于是一个 Redis latency 问题扩散成:
HTTP latency
pool exhaustion
goroutine/task growth
reverse proxy queueing
retry storm
Time Budget
生产调用应该有分层 deadline:
HTTP request budget
└─ Redis operation budget
└─ optional retry budget
不能:
每层都各自等 30 秒
否则嵌套等待会超过用户请求的总时间预算。
Slow 和 Down 的 failure policy 一样吗
业务语义可能一样:
Auth
→ 在 deadline 内拿不到 authority
→ 503
但运维诊断不同:
Down → connectivity / process / failover
Slow → command / memory / fork / network / pool / contention
Fault 4:Memory Full——noeviction 把压力转成显式写错误
第 3 课已经区分 Eviction。
这里假设 critical Redis 使用:
maxmemory set
maxmemory-policy noeviction
达到限制后:
已有 Key 不因为内存压力被静默赶走
但新增/扩张内存的写操作可能返回 OOM error
读现有 Key 通常仍可继续。
这不是“Redis 还能正常工作所以不用管”。
它表示故障从:
silent state loss
转换成:
explicit write failure
对安全关键状态通常更容易推理和报警。
Login
密码即使验证成功:
cannot create session:<sid>
→ cannot establish revocation authority
→ do not issue usable login result
→ 503 / authentication unavailable
Refresh
如果 rotation 无法安全写:
不要把新 Refresh Token 交给客户端
否则服务器自己都没有可靠记录这条新 credential 的状态。
Cache Fill
如果:
PostgreSQL read succeeds
Redis SET cache fails with OOM
通常:
return correct DB result
+ metric/log cache write failure
而不是把整个读请求一起变成 500。
为什么必须有 Headroom
达到 maxmemory 才报警已经太晚。
应该根据:
peak memory
growth rate
persistence/replication overhead
largest write
traffic burst
提前保留余量。
Fault 5:Connection Exhaustion——Server 没满,也可能 Client 先排死
“连接耗尽”至少有两层。
Application Client Pool Exhausted
例如:
PoolSize = 20
20 commands/transactions currently occupy connections
request #21
→ waits for a connection
Redis Server CPU 可能只有 10%,但 API 已经因为 pool wait 变慢。
Redis Server maxclients
Redis Server 自己也有限制可接受的同时客户端数量。
到上限时,新连接会被拒绝。
因此排障必须区分:
client-side pool saturation
vs
server-side maxclients / connection rejection
什么会导致 Pool 被占满
slow Redis command
network delay
太长 timeout
大量 blocking command
connection leak / misuse
retry amplification
traffic burst
所以“把 PoolSize 翻 10 倍”不一定修复根因,可能只是把更多并发压力同时推给 Redis。
Fault 6:AOF Recovery
恢复实验需要验证:
Session
Refresh current token
Replay tombstone
Rate Limit
TTL
而不是只验证:
SET hello world
restart
GET hello
因为真正重要的是业务 invariant 是否保持。
Fault 7:Network Timeout / Partition——先识别 Outcome Unknown
最难处理的网络错误不是:
connect failed before request was sent
而是:
request may have reached Redis
Redis may have executed it
response did not reach client
例如:
Client → INCR billing_counter
Redis executes 41 → 42
network drops
Client times out
如果客户端无条件重试:
INCR again
42 → 43
一次业务动作产生两次副作用。
Safe Retry 要看操作语义
读取:
GET
→ generally safe to retry
天然幂等的写:
SET key deterministic-value
→ many cases safe to repeat
非幂等 mutation:
INCR
consume-once
claim
append
必须分析重复执行的后果。
这也是为什么:
idempotency key
expected value / CAS
owner token
request identity
是网络可靠性设计的一部分。
Fault 8:Pub/Sub Lost Notification
Postgres event committed
Redis notification lost
预期:
fallback reconciliation
→ event eventually delivered
如果丢一条 Pub/Sub 消息就永久丢业务事件,说明把 notification plane 错当成 durable authority。
最终 Failure Matrix
| Redis capability | Redis 不可用时的期望 |
|---|---|
| Session authority | 503,fail closed |
| Refresh authority | 503,不签发无法正确记录/撤销的新 credential |
| Auth rate limiter | 当前设计偏 fail closed |
| HealthWorkspace cache | fallback PostgreSQL |
| Knowledge/RAG cache | fallback pgvector / normal retrieval |
| Runtime event notifier | fallback / reconciliation DB polling |
| Job wakeup | fallback periodic DB recovery sweep |
这张表的核心不是背答案,而是理解为什么。
如何从 State Ownership 推导 Failure Policy
先问:
1. Redis 是 Authority 吗?
如果 Redis 中的数据本身决定安全/业务状态:
不能读
→ 无法确认
→ 通常不能假装不存在或假装有效
常见:fail closed。
2. Redis 是可重建 Cache 吗?
Redis error
→ authoritative source still exists
→ fallback
3. Redis 是 Best-effort Notification 吗?
notification lost
→ durable state still exists
→ continue + reconcile
Capacity Planning:不是猜“这台 Redis 能撑多少 QPS”
Capacity Planning 的目标是:
基于真实 workload 和安全余量,找出最先可能成为瓶颈的资源,并验证峰值/故障时仍有边界。
至少考虑:
Memory
├─ average bytes/key
├─ largest keys
├─ key count
├─ TTL distribution
└─ peak / rewrite / replication headroom
Compute
├─ commands/sec
├─ command complexity
├─ Lua runtime
└─ hot key concentration
Network
├─ request bytes/sec
└─ response bytes/sec
Connections
├─ app pool per instance
├─ number of app replicas
└─ Redis maxclients
Persistence
├─ AOF write
├─ fsync
├─ rewrite
└─ disk free / throughput
平均值为什么会骗人
假设:
average memory = 45%
但每天发布后:
15 min peak = 92%
真正决定风险的是峰值和变化速度,而不是全天平均。
同样:
average 10k ops/s
也可能掩盖:
单一 hot key 8k ops/s
导致单 shard/单执行路径热点。
容量验证应使用真实命令分布
一个只跑:
GET tiny-key
的 benchmark 无法代表:
Lua + Sets + AOF + TLS + large payload + mixed read/write
所以 benchmark 要尽量贴近 BodySense 实际 workload,而不是只追求一个漂亮 QPS 数字。
Failure Domain 隔离:为什么 logical DB 不等于独立实例
长期 north star 可以考虑:
redis-critical
├─ session
├─ refresh
├─ revocation
├─ critical rate limit
├─ noeviction
└─ persistence / stronger SLO
redis-fast
├─ disposable cache
├─ Pub/Sub
├─ wakeup
└─ optional queue
这里最重要的不是“必须立刻部署两个 Redis”。
而是理解:
当两类数据的丢失语义、eviction policy、durability 和 SLO 不同时,它们最终可能值得拥有独立 Failure Domain。
为什么 Redis logical DB 0 / 1 / 2 不够
它们仍共享:
- Redis process;
- maxmemory;
- eviction policy;
- CPU;
- AOF/RDB 生命周期;
- restart / crash;
- network endpoint;
- maxclients。
所以:
DB 0
DB 1
DB 2
主要是 namespace 划分,不是资源/故障隔离。
什么时候才值得拆
不要为了“架构看起来高级”提前拆。
信号可能包括:
Cache memory pressure 已开始威胁 Auth
不同 maxmemory-policy 无法同时满足
不同 persistence/SLO
需要独立扩缩容
需要独立维护窗口
security blast radius 要求不同
最终故障实验
Redis Down
Redis Restart
Redis Slow
Redis Memory Full
Connection Exhaustion
AOF Recovery
Network Timeout
Pub/Sub Lost Notification
每个实验记录:
expected behavior
actual behavior
user-visible result
metrics/log evidence
recovery path
protected invariant
Prediction
[!question] 先判断故障语义,再展开答案
- Auth Redis 完全不可用,但客户端 JWT 签名与 exp 都合法,此时受保护 API 应继续放行、返回 401,还是返回 503?
- HealthWorkspace 已经从 PostgreSQL 得到正确结果,但 Redis Cache SET 因 noeviction 内存满而失败,这个读请求应整体失败吗?
- PostgreSQL 已提交 RuntimeEvent,但对应 Pub/Sub wakeup 丢失,会永久丢事件吗?
- noeviction 下 Redis 已满,新登录无法写入 session:
,服务端能不能仍把刚签好的 JWT 返回给客户端? - 客户端向 Redis 发出有副作用的 mutation,Redis 已执行,但响应在网络中丢失。客户端能否安全地假定“没有执行”并无条件重试?
[!success]- Answer / 解析 1. 返回 503,fail closed。 JWT 自身合法只证明 credential 没坏;Redis Session authority 不可读意味着无法确认“当前仍被授权”。返回 401 会把基础设施故障误判成 Session 不存在,直接放行则会绕过撤销系统。
2. 通常不应整体失败。 正确业务结果已经来自 authoritative PostgreSQL;Cache fill 失败只意味着这次优化没有建立成功。应返回正确结果,同时记录 cache write error / memory pressure 指标,保护下游避免持续 stampede。
3. 不会,只要架构保持 durable truth + lossy wakeup。 SSE / API 可以靠 periodic reconciliation、下一次通知或 reconnect + after_seq 从 PostgreSQL runtime_events 补齐。
4. 不能。 如果登录成功的定义包含“服务器已经建立可撤销的在线授权状态”,那么 Session authority 写失败时仍发 JWT,会让客户端拿到一个服务端下一请求无法认可、也无法正确治理的 credential。应让登录失败或返回服务暂不可用。
5. 不能。 这是典型 ambiguous outcome:命令可能执行成功,只是响应丢了。无条件重试可能重复副作用,所以必须依赖幂等 Key、owner/token、唯一 ID、CAS/version 或其他 safe retry 设计。
最终 20 题
[!question] Redis Track 验收
- 为什么 Access Token 是 JWT,仍然要查 Redis Session?
- Session miss 为什么是 401,Redis down 为什么是 503?
- 为什么 Refresh Token 可以用 opaque random credential?
- 为什么 Redis key 存 Refresh Token digest,而不是 raw token?
- 为什么 refresh rotation 要原子化?
- Set、Sorted Set、Stream 分别适合什么访问模式?
- TTL、Persistence、Eviction 有何区别?
- AOF everysec 有什么 durability trade-off?
- Cache Aside 有哪些一致性风险?
- 什么是 cache stampede?
- Pub/Sub 为什么不能替代 runtime_events 表?
- Pub/Sub 丢消息为什么仍可正确恢复?
- Streams 与 Pub/Sub 的 delivery model 有何差别?
- 为什么 JobRuntime 可继续留 PostgreSQL,同时 Redis 改善 worker latency?
- 为什么 noeviction 对当前 Auth Redis 重要?
- Redis 分布式锁释放为什么必须检查 owner/token?
- logical DB 为什么不能替代独立实例做 fault isolation?
- Auth / Cache / SSE / Job 为什么需要不同 failure policy?
- 哪些 BodySense domain state 不应 Redis-only?
- 如何证明 Redis 优化真的改善用户体验,而不是仅仅“用了缓存”?
最终 20 题答案
[!success]- Answer / 解析 1. 为什么 Access Token 是 JWT,仍然要查 Redis Session?
JWT 验签只能证明 Token 是服务器签发且 claims 仍在自身有效期内;Redis Session 决定服务器“此刻”是否仍认可这次登录,从而支持 Logout、管理员撤销、Replay 封禁等即时失效。2. Session miss 为什么是 401,Redis down 为什么是 503?
Miss 是确定的授权事实:Session 不存在;Down 是依赖不可用导致的未知状态。前者属于客户端当前没有有效授权,后者属于服务无法完成安全判断。3. 为什么 Refresh Token 可以用 opaque random credential?
Refresh endpoint 不需要客户端从 Token 中读取 claims;只要它具有足够熵、不可预测,并能映射到服务端 Session / family 状态即可。Opaque credential 还能把生命周期和撤销逻辑集中在服务端。4. 为什么 Redis key 存 Refresh Token digest,而不是 raw token?
Raw Refresh Token 是 bearer credential,一旦 Redis dump、日志或诊断输出泄露,攻击者可直接使用。存 digest 后仍可通过 digest(receivedToken) 查状态,但 Redis 泄露不会直接得到原始 Token。5. 为什么 refresh rotation 要原子化?
check active → consume old → create new → update family/tombstone 若不是单赢家状态转换,并发请求可能同时看到旧 Token active,分叉出多个合法后代,破坏 replay detection 和 rotation 不变量。6. Set、Sorted Set、Stream 分别适合什么访问模式?
Set 适合无序唯一成员和 membership;Sorted Set 适合按 score 排序、排名或时间窗口;Stream 适合保留有 ID 的 entry,并跟踪 Consumer Group、Pending、ACK 等消费进度。7. TTL、Persistence、Eviction 有何区别?
TTL 定义业务生命周期;Persistence 决定 Redis 重启后能否恢复数据;Eviction 是达到内存限制时为了腾空间提前移除 Key。三者分别回答“何时应失效、崩溃后是否回来、内存不够时删谁”。8. AOF everysec 有什么 durability trade-off?
它减少了每次写都同步刷盘的 I/O 成本,通常以大约秒级的最近写入风险换取更好的吞吐和延迟;不是绝对零丢失保证。9. Cache Aside 有哪些一致性风险?
典型包括 mutation 后忘记 invalidation、DB commit 与 cache invalidation 顺序错误、stale fill race、negative cache 未在创建时失效,以及并发 miss 造成 stampede。10. 什么是 cache stampede?
热点 Key miss / 到期后,大量并发请求同时回源重建同一份数据,把瞬时压力集中打到数据库或下游服务。常用 singleflight、锁、stale-while-revalidate、TTL jitter 等控制。11. Pub/Sub 为什么不能替代 runtime_events 表?
Pub/Sub 面向在线订阅者,没有 durable history、ACK 和离线 replay;掉线期间消息会错过。Runtime Event 需要 seq、重连回放、审计和恢复,所以 durable authority 仍应在 PostgreSQL。12. Pub/Sub 丢消息为什么仍可正确恢复?
因为 Pub/Sub 只作为 wakeup。客户端/服务端保存 lastSeq,之后从 PostgreSQL 查询 seq > lastSeq 即可发现遗漏事件;丢通知只影响实时性,不影响事实完整性。13. Streams 与 Pub/Sub 的 delivery model 有何差别?
Pub/Sub 是在线广播;Streams 会保留 entry,并支持 Consumer Group、Pending、ACK/Claim,因此可表达消费进度和重试,但仍不自动保证业务 exactly-once。14. 为什么 JobRuntime 可继续留 PostgreSQL,同时 Redis 改善 Worker latency?
PostgreSQL 已经持有 Job durability、状态机、attempt、idempotency 和 recovery。Redis 只发 job-ready 信号让 Worker 更快醒来;即使信号丢失,周期性 DB sweep 仍能最终 claim 任务。15. 为什么 noeviction 对当前 Auth Redis 重要?
Session / Refresh / security rate-limit 不是可随意重建的 Cache。相比在内存压力下静默删掉现有安全状态,noeviction 把故障显式转换成新写入失败,更容易 fail closed、告警和恢复。16. Redis 分布式锁释放为什么必须检查 owner/token?
Client A 的 Lease 可能过期后被 B 重新获得;如果 A 恢复后直接 DEL lock,会误删 B 的锁。释放必须只允许“仍然持有当前 token 的 owner”删除;高风险副作用还可能需要 fencing token。17. logical DB 为什么不能替代独立实例做 fault isolation?
DB 0/1/2 仍共享同一 Redis process、CPU、memory、maxmemory policy、AOF、重启、网络端点和连接限制。它们只是命名空间隔离,不是资源与故障域隔离。18. Auth / Cache / SSE / Job 为什么需要不同 failure policy?
因为 Redis 在四种场景中的 ownership 不同:Auth 是 authority,Cache 是可重建副本,SSE Pub/Sub 是 best-effort notification,Job wakeup 是延迟优化。Failure policy 必须从“Redis 丢失后真正事实还在哪里”推导。19. 哪些 BodySense domain state 不应 Redis-only?
BodyState、Assessment、Diagnosis、Treatment、Runtime Event durable log、Job ledger 等需要长期事实、关系约束、审计、Replay、事务与恢复的状态,应继续由 PostgreSQL 等 durable system of record 持有。20. 如何证明 Redis 优化真的改善用户体验,而不是仅仅“用了缓存”?
先建立 baseline,再比较 p50/p95 latency、DB query count、Worker wakeup delay、SSE 空轮询、错误率、fallback 次数、cache hit rate 等可观察指标,并做 Redis down / slow / full 等故障实验,确认性能提升没有破坏正确性和恢复能力。
L4 Done Definition
概念解释正确
+
能 trace BodySense 实际代码
+
能预测 failure behavior
+
能用实验/指标验证
+
能解释技术取舍
最终目标不是:
BodySense 尽可能多使用 Redis。
而是:
每一份 Redis 状态都有明确 ownership、lifetime、failure semantics 和 recovery path;Redis 提升速度、实时性和横向扩展能力,但不削弱 durable truth、审计与恢复边界。