Files
cs-note/hzh/Gen2D/09-限流.md
T

208 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 09 — 限流
> **一句话概括**:Redis Lua 原子令牌桶 + 双层限流 + Fail-Open 降级,保护系统免受过载。
---
```mermaid
flowchart LR
A["🌐 Request"] --> B["🌍 Global<br/>Limiter"]
B -->|pass| C["👤 User<br/>Limiter"]
B -->|deny| F["❌ 429"]
B -->|redis-fail| C
C -->|pass| D["✅ Handler"]
C -->|deny| F
C -->|redis-fail| D
style A fill:#e3f2fd,stroke:#1976d2
style B fill:#fff3e0,stroke:#f57c00
style C fill:#e8f5e9,stroke:#388e3c
style D fill:#c8e6c9,stroke:#2e7d32
style F fill:#ffcdd2,stroke:#c62828
```
---
## ⚙️ 令牌桶算法
Gen2D 使用 **Redis + Lua 脚本** 实现分布式令牌桶限流,保证原子性和一致性。
### Lua 脚本核心逻辑
```lua
-- KEYS[1] = 限流 key
-- ARGV[1] = rate(每秒令牌数)
-- ARGV[2] = burst(桶容量)
-- ARGV[3] = now(当前时间戳,毫秒)
-- ARGV[4] = expiration(key 过期时间)
-- 1. 获取当前桶状态
local data = redis.call('HMGET', key, 'tokens', 'ts')
-- 2. 首次访问,初始化满桶
if tokens == nil then
tokens = burst
last_ts = now
end
-- 3. 计算时间差,补充令牌
local delta = now - last_ts
if delta > 0 and rate > 0 then
local refill = (delta * rate) / 1000
tokens = math.min(burst, tokens + refill)
end
-- 4. 判断是否允许
if tokens >= 1 then
tokens = tokens - 1
allowed = 1
else
retry_after = math.ceil((deficit * 1000) / rate)
end
-- 5. 更新 Redis
redis.call('HSET', key, 'tokens', tokens, 'ts', last_ts)
redis.call('PEXPIRE', key, expiration)
return {allowed, tokens, retry_after}
```
### 固定窗口变体
当 `Rate = 0` 时,令牌桶退化为固定窗口模式:
- 桶初始化为满(Burst 个令牌)
- 用完后不补充(`rate = 0` 时跳过 refill)
- 等待 key 过期后重置(`Expiration` 控制窗口大小)
> 💡 **适用场景**:24 小时维度的配额控制,如"每天 30 次提示词优化"。
---
## 🔀 双层限流配置
Gen2D 对核心接口实施**全局限流 + 用户限流**双重保护:
| 接口 | 全局限流 | 用户限流 | 窗口 |
|------|---------|---------|------|
| `/api/v1/prompt/optimize` | 1000 次/24h | 30 次/24h | 25h 过期 |
| `/api/v1/generate` | 500 次/24h | 15 次/24h | 25h 过期 |
### 执行顺序
```mermaid
flowchart TB
A["请求进入"] --> B["全局限流检查"]
B -->|通过| C["用户限流检查"]
B -->|拒绝| D["429 Too Many Requests"]
C -->|通过| E["执行 Handler"]
C -->|拒绝| D
style B fill:#fff3e0,stroke:#f57c00
style C fill:#e8f5e9,stroke:#388e3c
style D fill:#ffcdd2,stroke:#c62828
style E fill:#c8e6c9,stroke:#2e7d32
```
**全局限流在前**:先检查系统总体配额,避免单个用户耗尽全局配额。
### 配置参数
```go
type Config struct {
Rate int // 每秒令牌数;0 = 固定窗口
Burst int // 桶容量(窗口内总量上限)
KeyPrefix string // Redis key 前缀
Expiration time.Duration // key 过期时间
}
```
| 配置项 | Prompt User | Prompt Global | Generate User | Generate Global |
|--------|:-----------:|:-------------:|:-------------:|:---------------:|
| Rate | 0 | 0 | 0 | 0 |
| Burst | 30 | 1000 | 15 | 500 |
| KeyPrefix | `ratelimit:prompt:user:` | `ratelimit:prompt:global:` | `ratelimit:generate:user:` | `ratelimit:generate:global:` |
| Expiration | 25h | 25h | 25h | 25h |
---
## 🛡️ Fail-Open 降级
当 Redis 不可用时限流器自动降级为 **Fail-Open** 模式:
```go
func (l *TokenBucketLimiter) Allow(ctx context.Context, key string) (bool, int, time.Duration) {
result, err := l.script.Run(ctx, l.client, ...).Int64Slice()
if err != nil {
// Redis 不可用时 fail-open,放行请求
return true, l.config.Burst, 0
}
// ...
}
```
**设计权衡**:
| 策略 | 优点 | 缺点 |
|------|------|------|
| **Fail-Open** ✅ | 保证可用性,用户体验不受影响 | 可能短暂失去限流保护 |
| Fail-Close | 严格限流保护 | Redis 故障导致全站不可用 |
> 🛡️ **选择 Fail-Open**:在"偶尔超限"和"完全不可用"之间,优先保证服务可用性。
---
## 📡 中间件响应
限流中间件返回标准化的 HTTP 响应:
### 允许通过
```http
HTTP/1.1 200 OK
X-RateLimit-Remaining: 12
```
### 被限流
```http
HTTP/1.1 429 Too Many Requests
Retry-After: 3600
X-RateLimit-Remaining: 0
{
"code": 429,
"message": "请求过于频繁,请稍后再试"
}
```
| Header | 说明 |
|--------|------|
| `X-RateLimit-Remaining` | 剩余令牌数 |
| `Retry-After` | 建议重试等待秒数 |
---
## 📊 指标采集
限流中间件自动采集 Prometheus 指标:
```go
metrics.RateLimitRequestsTotal.WithLabelValues(scope, endpoint, result).Inc()
metrics.RateLimitRemainingTokens.WithLabelValues(scope, endpoint).Set(float64(remaining))
```
- `scope`:`user` / `global`
- `endpoint`:`prompt` / `generate`
- `result`:`allowed` / `denied`
配合告警规则 `RateLimitHighDenialRate`(拒绝率 > 5%),及时发现异常流量。
---
## 🔗 关联文档
- [← 返回索引](00-index.md)
- [10 — 中间件链](10-middleware-chain.md) — 限流中间件在链中的位置
- [07 — 可观测性](07-observability.md) — 限流指标和告警规则