← [ BACK_TO_NOTES ]

BodySense Redis 第 12 课:Failure Engineering 与 Capacity

以 Redis Down、Restart、Slow、Memory Full、Connection Exhaustion、AOF Recovery 和丢通知为故障实验,建立按 state ownership 选择 fail closed、fallback 或 best-effort 的最终能力。

#course #growing #backend #ops #architecture #redis #bodysense

[!info] related notes

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 capabilityRedis 不可用时的期望
Session authority503,fail closed
Refresh authority503,不签发无法正确记录/撤销的新 credential
Auth rate limiter当前设计偏 fail closed
HealthWorkspace cachefallback PostgreSQL
Knowledge/RAG cachefallback pgvector / normal retrieval
Runtime event notifierfallback / reconciliation DB polling
Job wakeupfallback 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] 先判断故障语义,再展开答案

  1. Auth Redis 完全不可用,但客户端 JWT 签名与 exp 都合法,此时受保护 API 应继续放行、返回 401,还是返回 503?
  2. HealthWorkspace 已经从 PostgreSQL 得到正确结果,但 Redis Cache SET 因 noeviction 内存满而失败,这个读请求应整体失败吗?
  3. PostgreSQL 已提交 RuntimeEvent,但对应 Pub/Sub wakeup 丢失,会永久丢事件吗?
  4. noeviction 下 Redis 已满,新登录无法写入 session:,服务端能不能仍把刚签好的 JWT 返回给客户端?
  5. 客户端向 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 验收

  1. 为什么 Access Token 是 JWT,仍然要查 Redis Session?
  2. Session miss 为什么是 401,Redis down 为什么是 503?
  3. 为什么 Refresh Token 可以用 opaque random credential?
  4. 为什么 Redis key 存 Refresh Token digest,而不是 raw token?
  5. 为什么 refresh rotation 要原子化?
  6. Set、Sorted Set、Stream 分别适合什么访问模式?
  7. TTL、Persistence、Eviction 有何区别?
  8. AOF everysec 有什么 durability trade-off?
  9. Cache Aside 有哪些一致性风险?
  10. 什么是 cache stampede?
  11. Pub/Sub 为什么不能替代 runtime_events 表?
  12. Pub/Sub 丢消息为什么仍可正确恢复?
  13. Streams 与 Pub/Sub 的 delivery model 有何差别?
  14. 为什么 JobRuntime 可继续留 PostgreSQL,同时 Redis 改善 worker latency?
  15. 为什么 noeviction 对当前 Auth Redis 重要?
  16. Redis 分布式锁释放为什么必须检查 owner/token?
  17. logical DB 为什么不能替代独立实例做 fault isolation?
  18. Auth / Cache / SSE / Job 为什么需要不同 failure policy?
  19. 哪些 BodySense domain state 不应 Redis-only?
  20. 如何证明 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、审计与恢复边界。

创建于 2026/9/18 更新于 2026/9/18
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本