vault backup: 2026-05-17 21:40:31

This commit is contained in:
hhs
2026-05-17 21:40:31 +08:00
parent bdeb889ee4
commit df6489e370
14 changed files with 3173 additions and 295 deletions
+120 -29
View File
@@ -1,6 +1,6 @@
---
tags: [microservice, circuit-breaker, retry, rate-limiting, bulkhead, health-check]
create time: 2026-05-05
tags: [microservice, circuit-breaker, retry, rate-limiting, bulkhead, health-check, fallback, chaos-engineering]
create time: 2026-05-05 10:30
---
# 容错模式
@@ -21,6 +21,52 @@ graph TB
> [!warning] 核心认知
> 不要假设任何网络调用会成功。每个远程调用的代码都应该考虑失败路径。
> [!question]- 💡 思考题
> **如果一个系统从未出现过故障,还需要容错设计吗?**
>
> 答案是肯定的。Netflix 的混沌工程理念指出:**"故障不是是否发生的问题,而是何时发生。"**
> 没有容错设计的系统在第一次外部依赖超时后就会暴露出级联崩溃的风险。
---
## 正文
### 各模式的组合使用
这 7 种容错模式通常不会单独使用,而是层层叠加形成纵深防御体系:
```mermaid
flowchart LR
subgraph Layer1["第一层:快速失败"]
T["⏱ 超时控制<br/>最先触发,避免线程等待"]
end
subgraph Layer2["第二层:安全重试"]
R["⏱ 指数退避重试<br/>给故障恢复的时间窗口"]
end
subgraph Layer3["第三层:自我保护"]
CB["🔘 熔断器<br/>高频失败时直接短路"]
RL["🔀 限流器<br/>保护下游不被打满"]
BH["🧱 舱壁隔离<br/>故障不蔓延"]
end
subgraph Layer4["第四层:兜底策略"]
D["💤 降级响应<br/>返回缓存/默认值"]
HC["💚 健康检查<br/>自动剔除故障节点"]
end
Layer1 --> Layer2 --> Layer3 --> Layer4
style Layer1 fill:#e3f2fd
style Layer2 fill:#fff3e0
style Layer3 fill:#fce4ec
style Layer4 fill:#e8f5e9
```
> [!tip] 使用顺序的重要性
> 超时是所有机制的前提。一个没有超时的重试只会让请求无限期挂起。
## 1. 超时控制
这是所有容错机制的**前提**——没有超时的系统迟早会被拖垮。
@@ -96,6 +142,15 @@ func retryWithBackoff(ctx context.Context, maxRetries int, fn func() error) erro
| 502/503/504 Gateway Error | 业务错误(如余额不足) |
| 并发冲突(乐观锁失败) | 403 Forbidden |
> [!example]- ❌ 常见反模式:重试非幂等操作
> ```
> // 危险!每次重试都会创建一个新的订单
> resp, err := httpClient.Post("/api/orders", jsonBody)
> if err != nil { retry() } // ← 重复下单!
> ```
>
> **安全做法:** 为每个写操作生成全局唯一的 `requestId`,下游基于 requestId 做幂等校验。
## 3. 熔断器 (Circuit Breaker)
当下游服务频繁失败时,快速失败避免线程堆积雪崩。
@@ -155,7 +210,18 @@ if result.Err != nil {
}
```
## 4. 限流 (Rate Limiting)
### 熔断器 vs 限流器
> [!note]- 两者的区别(常被混淆)
>
> | 维度 | 熔断器 (Circuit Breaker) | 限流器 (Rate Limiter) |
> |------|------------------------|---------------------|
> | **触发条件** | 下游故障时才打开 | 始终生效,不依赖故障状态 |
> | **决策依据** | 成功率、失败次数 | 请求速率超过阈值 |
> | **目的** | 保护自身不被慢速下游拖垮 | 保护下游不被过多请求打满 |
> | **状态性** | 有状态(Closed/Open/Half-Open) | 无状态(持续监控流速) |
>
> 两者配合效果最佳:**限流在前挡流量洪峰,熔断在后兜底保护。**
保护下游服务不被过量请求压垮。
@@ -190,8 +256,8 @@ flowchart LR
```mermaid
flowchart LR
subgraph "🪣 令牌桶内部"
BUCKET[令牌桶<br/>容量 = maxBurst]
REFILL[⚡ 固定速率 r/s 添加令牌<br/>最多填到 maxBurst]
BUCKET["令牌桶<br/>容量 = maxBurst"]
REFILL["⚡ 固定速率 r/s 添加令牌<br/>最多填到 maxBurst"]
end
REQ[请求到达] --> CHECK{桶中有令牌?}
@@ -214,16 +280,14 @@ flowchart LR
假设 `rate = 100/s`, `maxBurst = 200`:
```
时间线(每秒) 桶中令牌数 行为
───────────────── ─────────── ─────────────
t=0s 200 初始满桶
t=1s 300→200 理论上应该增加到300,但桶满了,上限200
t=2s ~ t=9s 100 稳定在 100(每秒消耗 ≈ 每秒生成)
t=10s 200 系统空闲,桶重新蓄满
t=11s 0 ⚡ 瞬间涌入 200 个请求全部通过(突发!)
t=12s 0 后续只有 100 个能通过(恢复限速率)
```
| 时间 | 桶中令牌数 | 行为 |
|------|-----------|------|
| `t=0s` | 200 | 初始满桶 |
| `t=1s` | 300 → 200 | 理论应增到300,但桶已满,上限200 |
| `t=2s~9s` | 100 | 稳定状态(每秒消耗 ≈ 每秒生成) |
| `t=10s` | 200 | 系统空闲,桶重新蓄满 |
| `t=11s` | 0 | ⚡ 瞬间涌入200个请求全部通过(突发!) |
| `t=12s` | 100 | 后续仅100个能通过(恢复限速率) |
这就是为什么令牌桶适合 API 网关——用户可能积攒了多个请求一口气发过来,直接全部拒绝体验很差;允许合理范围内的突发,用户体验更好。
@@ -301,7 +365,7 @@ func (tb *TokenBucket) Wait(ctx context.Context) error {
```mermaid
flowchart TD
REQ["💧 请求源源不断流入"] --> QUEUE[🪣 漏桶<br/>队列缓冲区]
REQ["💧 请求源源不断流入"] --> QUEUE["🪣 漏桶<br/>队列缓冲区"]
QUEUE --> PROCESS["🚰 以固定速率 drainer 处理<br/>不管上游多快,只匀速处理"]
QUEUE --> FULL{"桶满了?"}
@@ -322,19 +386,14 @@ flowchart TD
**关键特性对比令牌桶:**
```
场景:突发流量 500qps 涌入,限流配置 rate=100/s, capacity=200
令牌桶视角: 漏桶视角:
┌──────────────┐ ┌──────────────┐
│ ✅前200个通过 │ ← 桶里够令牌 │ ❌桶满了拒绝 │ ← 已经满了
│ ⏱后面100个/秒 │ ← 恢复限速率 │ ──────────── │
│ ❌多余的被拒 │ │ ✅匀速处理100/s│ ← 底部漏水
└──────────────┘ └──────────────┘
↑
无论上游来多少,
下游只看到匀速的100/s
```
> 场景:突发流量 500qps 涌入,限流配置 rate=100/s, capacity=200
>
> | 维度 | 令牌桶行为 | 漏桶行为 |
> |------|-----------|---------|
> | **前 200 个请求** | ✅ 通过(桶里有足够令牌) | ❌ 拒绝(桶已满了) |
> | **第 201~300 个** | ⏱ 按恢复速率放行(100/s) | ──────── |
> | **后续请求** | ❌ 多余的被拒绝 | ✅ 匀速处理 100/s |
> | **下游视角** | 有突发性 | 始终匀速的 100/s |
| 维度 | 令牌桶 | 漏桶 |
|------|--------|------|
@@ -475,6 +534,12 @@ graph TB
Pool1 -.-> note
```
> [!question]- 💡 什么时候需要舱壁隔离?
> - 单一客户端同时调用多个不稳定下游时
> - 共享进程内存/连接池,某服务线程阻塞会占用全部资源时
>
> **不需要的情况:** 每个下游已经部署在独立进程中(此时故障天然隔离);下游数量极少,连接池开销可以接受。
```go
// poolgroup 示例:为不同服务隔离连接池
orderPool := pool.New(10, 100) // 活跃10个,上限100个
@@ -487,6 +552,12 @@ payPool := pool.New(8, 80)
当系统部分不可用时,通过降级保证核心功能可用。
> [!note]- 降级的触发时机
> - **主动降级:** 管理员手动关闭非核心功能(大促期间关闭推荐、评论)
> - **被动降级:** 熔断器打开后自动切换到兜底响应
>
> 生产环境中,**主动降级通常是第一选择**——在系统还可控时牺牲局部保全整体,比等雪崩发生后再被动的处理代价更小。
```mermaid
flowchart TD
Req["用户请求"]
@@ -534,6 +605,26 @@ sequenceDiagram
- **就绪探针 (Readiness)**:判断"是否能接收流量",未就绪则剔除负载均衡池
- **Readiness 比 Liveness 更重要**——一个进程活着但数据库连接耗尽时,应该停止接收流量而不是重启
> [!tip] 生产环境最佳实践
> 健康检查端点 `/healthz` 不应只做 HTTP 200,还应验证关键依赖(DB、Redis)的连通性。一个数据库连接耗尽的服务返回 200 是虚假的健康信号。
---
## 总览:模式对比速查
| 模式 | 保护谁 | 何时生效 | 复杂度 | 推荐优先级 |
|------|--------|---------|--------|-----------|
| ⏱ **超时控制** | 自身线程/连接 | 每次远程调用 | ⭐ | 🔴 必须实现 |
| ⏱ **重试机制** | 瞬时故障 | 失败后自动触发 | ⭐⭐ | 🔴 幂等操作必做 |
| 🔘 **熔断器** | 自身系统 | 高频失败时 | ⭐⭐ | 🟠 外部依赖必配 |
| 🔀 **限流器** | 下游服务 | 始终生效 | ⭐⭐ | 🟠 网关层必配 |
| 🧱 **舱壁隔离** | 其他服务调用 | 某下游异常时 | ⭐⭐⭐ | 🟡 多下游场景 |
| 💤 **降级策略** | 用户核心体验 | 非核心不可用时 | ⭐⭐ | 🟠 面向 C 端必做 |
| 💚 **健康检查** | 负载均衡/调度 | 实例异常时 | ⭐ | 🔴 K8s 必配 |
> [!success]- 一句话总结
> **超时保底、重试修复瞬时故障、熔断防雪崩、限流护下游、舱壁保隔离、降级保核心。**
## 关联笔记
- [[02-服务治理/01-API网关]] — 网关层的限流和熔断插件