研究与学习 · 功能讲解
birchline/api 里的限流是怎么工作的
一句话 — 每个请求都会过一遍
rateLimit() 中间件:先把调用方解析为一个
桶 key,再从 Redis 取出对应的令牌桶,要么消耗一个令牌,要么返回 429。
每条路由的限额写在 config/limits.yaml 里;没配置的路由会继承
default 档(每个 API key 100 次/分钟)。
一次请求会经过哪些步骤
展开每一步可以看它具体跑了什么、代码在哪。整条路径只有约 40 行,对每个请求大约多增加 0.4 ms 的 p50 延迟。
1 · 识别调用方 middleware/ratelimit.ts:21
中间件首先把请求归约成一个 bucketKey:如果带了 Authorization 头就用 API key,
否则用客户端 IP(沿着 x-forwarded-for 链取,并且只信任我们自己的 LB)。
匿名 IP 流量会落到一个明显更低的默认档。
2 · 查桶 lib/tokenBucket.ts:9
路由名加桶 key 映射到一个 Redis 哈希(rl:{route}:{key}),里面存
tokens 和 updatedAt。如果 key 不存在,会以满容量懒加载创建——没有热身阶段。
3 · 补充并消费令牌 lib/tokenBucket.ts:31
根据距离上次更新的时间算出补充量(rate × Δt,上限是 burst),然后扣掉一个令牌。
整段「读—改—写」放在一个 Lua 脚本里跑,并发请求不会重复消耗。
4 · 桶空了就拒绝 middleware/ratelimit.ts:48
如果脚本返回 tokens < 0,中间件就短路返回
429 Too Many Requests,Retry-After 设为下一个令牌补满所需的秒数。
成功响应总是带上 X-RateLimit-Remaining。
给你的路由配限流
你不用改中间件本身。在 config/limits.yaml 里以路由名为 key 加一项,
然后(可选地)在路由上打个标,让中间件能找到它。
# config/limits.yaml default: rate: 100/min burst: 120 search.query: rate: 20/min burst: 40 key: api_key # 也可以填: ip
// routes/search.ts router.post( "/search", rateLimit("search.query"), handler, );
HTTP/1.1 429 Too Many Requests
Retry-After: 17
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 0
{ "error": "rate_limited", "retry_after": 17 }
★
如果你只需要默认档,连 YAML 条目都不用加——直接用
rateLimit()(不带参数)包一下 handler 就行,
路由名会从路径推断出来。几个值得留意的坑
- 本地开发时限额是按进程算的。没设
REDIS_URL时 Redis 客户端会回退到内存 map, 所以本地测出的行为不能反映真实集群里的情况。 - Burst ≠ rate。
burst是桶容量;一个闲置一分钟的调用方 即便rate很低,也能瞬间打出burst个请求。 - 流式响应只算一次。令牌是在请求开始时扣的;一段 30 秒的 SSE 流也只占一个令牌。
常见问题
- 怎么放过内部流量?
- 调用方设置
x-birchline-internal: 1;中间件会拿它和 mTLS 对端名字校验, 通过的话直接跳过整个桶。 - 怎么看谁正在被限流?
- 每次
429都会打出一个ratelimit.rejected指标,按路由和 key 类型打 tag。 Grafana 上有个面板,在 API → Health 下面。 - 可以给某个用户单独放宽限额吗?
- 可以——在 YAML 的
overrides:下加上他的 API key。覆盖项支持热加载,不用走发布。