247 lines
8.1 KiB
Markdown
247 lines
8.1 KiB
Markdown
|
|
---
|
|||
|
|
tags: [redis, rate-limiting, multi-layer, architecture]
|
|||
|
|
create time: 2026-06-01 10:30
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 多级限流架构实战
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
单个限流算法通常不够用。生产环境中的限流像洋葱一样**层层叠加**——每层保护不同的目标,使用不同的算法和维度。这种设计既防御了大规模攻击,又为正常用户提供了良好的配额管理。
|
|||
|
|
|
|||
|
|
对应薄弱点 F5:多级限流架构各层的设计选择。
|
|||
|
|
|
|||
|
|
## 一、为什么需要多级?
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart LR
|
|||
|
|
subgraph "单一维度的局限"
|
|||
|
|
L1["仅 IP 限流"] --> PROBLEM1["问题:无法区分同一 IP 下的不同用户"]
|
|||
|
|
L2["仅用户限流"] --> PROBLEM2["问题:恶意攻击者可注册大量账号绕过"]
|
|||
|
|
L3["仅接口限流"] --> PROBLEM3["问题:不防刷单/暴力扫描"]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
PROBLEM1 --> MULTI["→ 需要多级组合"]
|
|||
|
|
PROBLEM2 --> MULTI
|
|||
|
|
PROBLEM3 --> MULTI
|
|||
|
|
|
|||
|
|
style PROBLEM1 fill:#ffebee
|
|||
|
|
style PROBLEM2 fill:#ffebee
|
|||
|
|
style PROBLEM3 fill:#ffebee
|
|||
|
|
style MULTI fill:#e8f5e9
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
单层限流的盲区:
|
|||
|
|
- 只看 IP → 一个用户的多个账号共享配额(或反之,一个 IP 下多个用户被误伤)
|
|||
|
|
- 只看用户 → 恶意用户可以创建无数个账号突破限制
|
|||
|
|
- 只看接口 → 对暴力破解、爬虫扫描无能为力
|
|||
|
|
|
|||
|
|
## 二、经典三级限流架构
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD
|
|||
|
|
Client["👤 客户端请求"] --> GW["🌐 API Gateway"]
|
|||
|
|
GW --> L1
|
|||
|
|
|
|||
|
|
subgraph L1Layer["🔴 L1: IP 维度 - 第一道防线"]
|
|||
|
|
direction LR
|
|||
|
|
L1Action["防暴力扫描<br/>防恶意攻击"]
|
|||
|
|
L1Algorithm["短窗口固定计数<br/>例如: 100 req/s/IP"]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
L1 -- pass --> L2
|
|||
|
|
|
|||
|
|
subgraph L2Layer["🟡 L2: 用户维度 - 配额管理"]
|
|||
|
|
direction LR
|
|||
|
|
L2Action["控制每日/每月配额<br/>区分免费/付费等级"]
|
|||
|
|
L2Algorithm["天级滑动窗口日志<br/>例如: 1000 req/day"]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
L2 -- pass --> L3
|
|||
|
|
|
|||
|
|
subgraph L3Layer["🟢 L3: 接口维度 - 保护后端"]
|
|||
|
|
direction LR
|
|||
|
|
L3Action["保护实例不被打满<br/>QPS/内存保护"]
|
|||
|
|
L3Algorithm["令牌桶/漏桶<br/>例如: 5000 req/s/endpoint"]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
L3 -- pass --> Biz["💼 执行业务逻辑"]
|
|||
|
|
|
|||
|
|
L1 -- fail --> R429["HTTP 429 Too Many Requests"]
|
|||
|
|
L2 -- fail --> R429
|
|||
|
|
L3 -- fail --> R429
|
|||
|
|
|
|||
|
|
style L1Layer fill:#ffebee,color:#000
|
|||
|
|
style L2Layer fill:#fff3e0,color:#000
|
|||
|
|
style L3Layer fill:#e8f5e9,color:#000
|
|||
|
|
style R429 fill:#b71c1c,color:#fff
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 层级详解
|
|||
|
|
|
|||
|
|
| 层级 | 限流维度 | 核心目标 | 推荐算法 | 典型参数 | Key 示例 |
|
|||
|
|
|------|---------|---------|---------|---------|---------|
|
|||
|
|
| **L1** | IP / CIDR | 防刷、防攻击、防爬虫 | 短窗口固定计数 | 100 req/s/IP | `rate:ip:{IP}:short` |
|
|||
|
|
| **L2** | 用户 ID / Account | 配额管理(免费/付费) | 天级滑动窗口日志 | 1000 req/day | `rate:user:{UID}:daily` |
|
|||
|
|
| **L3** | Endpoint / Interface | 保护下游资源 | 令牌桶 / 漏桶 | 5000 req/s/path | `rate:api:{path}` |
|
|||
|
|
|
|||
|
|
### L1:IP 层 - 最短视的窗口
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
时间窗口:1 ~ 5 秒
|
|||
|
|
算法:固定窗口计数器
|
|||
|
|
目的:快速拦截自动化攻击
|
|||
|
|
|
|||
|
|
为什么用短窗口?
|
|||
|
|
- 恶意流量的特点是短时间内密集发起
|
|||
|
|
- 短窗口可以及时响应,不留攻击窗口
|
|||
|
|
- 不会因为窗口太长导致误伤正常用户的突发流量
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// L1: IP 限流 - 短窗口固定计数
|
|||
|
|
const shortWindowLua = `
|
|||
|
|
local key = KEYS[1]
|
|||
|
|
local maxCount = tonumber(ARGV[1])
|
|||
|
|
local window = tonumber(ARGV[2])
|
|||
|
|
|
|||
|
|
local count = redis.call('INCR', key)
|
|||
|
|
if count == 1 then
|
|||
|
|
redis.call('EXPIRE', key, window)
|
|||
|
|
end
|
|||
|
|
return count
|
|||
|
|
`
|
|||
|
|
|
|||
|
|
func IPLimitAllow(ctx context.Context, rdb *redis.Client, ip string) (bool, error) {
|
|||
|
|
// 5 秒窗口,最多 100 次请求
|
|||
|
|
const windowSec = 5
|
|||
|
|
const maxReq = 100
|
|||
|
|
|
|||
|
|
key := fmt.Sprintf("rate:ip:{%s}", ip)
|
|||
|
|
result, err := rdb.Eval(ctx, shortWindowLua, []string{key}, maxReq, windowSec).Int()
|
|||
|
|
if err != nil {
|
|||
|
|
return false, err
|
|||
|
|
}
|
|||
|
|
return result <= maxReq, nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### L2:用户层 - 精确的日配额
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
时间窗口:24 小时(按天重置)
|
|||
|
|
算法:Sorted Set 滑动窗口(逐条精确统计)
|
|||
|
|
目的:区分免费用户和付费用户的配额
|
|||
|
|
|
|||
|
|
为什么用 Sorted Set?
|
|||
|
|
- 日配额 QPS 不高(1000/day ≈ 0.01 QPS)
|
|||
|
|
- 需要精确统计,不能有近似误差
|
|||
|
|
- 免费/付费分级,每个用户独立配额
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### L3:接口层 - 保护后端资源
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
时间窗口:持续监控(令牌桶模型)
|
|||
|
|
算法:令牌桶
|
|||
|
|
目的:防止某个接口被打爆,保护数据库/下游服务
|
|||
|
|
|
|||
|
|
为什么用令牌桶?
|
|||
|
|
- O(1) 空间,不随时间增长
|
|||
|
|
- 允许合理突发(用户批量操作)
|
|||
|
|
- 输出速率可控,保护后端稳定
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 三、多级限流的调用链路
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
sequenceDiagram
|
|||
|
|
participant C as 客户端
|
|||
|
|
participant G as Go Service
|
|||
|
|
participant R as Redis
|
|||
|
|
|
|||
|
|
C->>G: HTTP Request
|
|||
|
|
G->>R: L1: IP Check (100/s/5s)
|
|||
|
|
alt Blocked by L1
|
|||
|
|
R-->>G: count > 100
|
|||
|
|
G-->>C: 429 (L1 blocked)
|
|||
|
|
else
|
|||
|
|
G->>R: L2: User Check (1000/day)
|
|||
|
|
alt Blocked by L2
|
|||
|
|
R-->>G: count >= 1000
|
|||
|
|
G-->>C: 429 (L2 blocked)
|
|||
|
|
else
|
|||
|
|
G->>R: L3: Token Bucket (5000/s)
|
|||
|
|
alt Blocked by L3
|
|||
|
|
R-->>G: no tokens
|
|||
|
|
G-->>C: 429 (L3 blocked)
|
|||
|
|
else
|
|||
|
|
R-->>G: allowed
|
|||
|
|
G->>G: Execute Business Logic
|
|||
|
|
G-->>C: 200 OK
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!note]- 性能考量
|
|||
|
|
>
|
|||
|
|
> 多级限流意味着每次请求要发 **3 次 Redis 调用**。优化方向:
|
|||
|
|
> - **本地缓存 + 远程校验**:L1 可以在本地做短期计数(如 sync.Map),定期同步到 Redis
|
|||
|
|
> - **Pipeline 合并**:L1/L2/L3 的三个 Lua 调用可以用 Pipeline 打包
|
|||
|
|
> - **短路策略**:如果 L1 就拒绝了,不需要继续查 L2/L3
|
|||
|
|
|
|||
|
|
## 四、降级与熔断联动
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD
|
|||
|
|
Normal["✅ 正常限流状态"] --> Check{"Redis 可用?"}
|
|||
|
|
|
|||
|
|
Check -- "是" --> RL["正常走多级限流"]
|
|||
|
|
Check -- "否" --> Degradation{"降级策略"}
|
|||
|
|
|
|||
|
|
Degradation -- "短暂断开" --> Local["本地内存限流<br/>降级模式"]
|
|||
|
|
Degradation -- "长时间不可用" --> PermitAll["放行所有请求<br/>宁可超限不可停机"]
|
|||
|
|
|
|||
|
|
Local --> Recovery{"Redis 恢复?"}
|
|||
|
|
Recovery -- "是" --> Normal
|
|||
|
|
Recovery -- "否" --> PermitAll
|
|||
|
|
|
|||
|
|
style Normal fill:#c8e6c9
|
|||
|
|
style PermitAll fill:#fff3e0
|
|||
|
|
style Local fill:#e3f2fd
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| Redis 状态 | L1 IP 层 | L2 用户层 | L3 接口层 |
|
|||
|
|
|-----------|---------|----------|----------|
|
|||
|
|
| **正常** | Redis Lua | Redis Lua | Redis Lua |
|
|||
|
|
| **短暂故障** | 本地 sync.Map 缓存计数 | 跳过(默认放行) | 本地令牌桶 |
|
|||
|
|
| **长时间故障** | 全部放行 | 全部放行 | 全部放行 |
|
|||
|
|
|
|||
|
|
> [!warning]- 降级不是无限制的
|
|||
|
|
>
|
|||
|
|
> 如果 L1(IP 层)也降级放行了,意味着任何 IP 都没有限制——这时应该配合 CDN/WAF 等外部防御层做兜底。
|
|||
|
|
|
|||
|
|
## 五、各层的超时与重试配置
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
多层限流系统的超时设置:
|
|||
|
|
|
|||
|
|
┌─────────┬──────────────┬──────────────────────────┐
|
|||
|
|
│ 层级 │ Redis 超时 │ 客户端超时 │
|
|||
|
|
├─────────┼──────────────┼──────────────────────────┤
|
|||
|
|
│ L1 │ 1ms │ 50ms(快速判断) │
|
|||
|
|
│ L2 │ 5ms │ 200ms(配额查询) │
|
|||
|
|
│ L3 │ 3ms │ 100ms(令牌检查) │
|
|||
|
|
│ 合计 │ 9ms │ — │
|
|||
|
|
└─────────┴──────────────┴──────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
各层超时应该远小于整体业务超时,否则限流模块本身会成为瓶颈。
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[分布式限流]] — 限流的基础算法介绍
|
|||
|
|
- [[Jitter抖动与重试策略]] — 各层的客户端重试策略设计
|
|||
|
|
- [[02-服务治理/06-容错模式]] — 限流在容错体系中的位置
|