5.3 KiB
5.3 KiB
09 — 限流
一句话概括:Redis Lua 原子令牌桶 + 双层限流 + Fail-Open 降级,保护系统免受过载。
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 脚本核心逻辑
-- 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 过期 |
执行顺序
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
全局限流在前:先检查系统总体配额,避免单个用户耗尽全局配额。
配置参数
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 模式:
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/1.1 200 OK
X-RateLimit-Remaining: 12
被限流
HTTP/1.1 429 Too Many Requests
Retry-After: 3600
X-RateLimit-Remaining: 0
{
"code": 429,
"message": "请求过于频繁,请稍后再试"
}
| Header | 说明 |
|---|---|
X-RateLimit-Remaining |
剩余令牌数 |
Retry-After |
建议重试等待秒数 |
📊 指标采集
限流中间件自动采集 Prometheus 指标:
metrics.RateLimitRequestsTotal.WithLabelValues(scope, endpoint, result).Inc()
metrics.RateLimitRemainingTokens.WithLabelValues(scope, endpoint).Set(float64(remaining))
scope:user/globalendpoint:prompt/generateresult:allowed/denied
配合告警规则 RateLimitHighDenialRate(拒绝率 > 5%),及时发现异常流量。