研究与学习 · 功能讲解

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}),里面存 tokensupdatedAt。如果 key 不存在,会以满容量懒加载创建——没有热身阶段。

3 · 补充并消费令牌 lib/tokenBucket.ts:31

根据距离上次更新的时间算出补充量(rate × Δt,上限是 burst),然后扣掉一个令牌。 整段「读—改—写」放在一个 Lua 脚本里跑,并发请求不会重复消耗。

4 · 桶空了就拒绝 middleware/ratelimit.ts:48

如果脚本返回 tokens < 0,中间件就短路返回 429 Too Many RequestsRetry-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 就行, 路由名会从路径推断出来。

几个值得留意的坑

常见问题

怎么放过内部流量?
调用方设置 x-birchline-internal: 1;中间件会拿它和 mTLS 对端名字校验, 通过的话直接跳过整个桶。
怎么看谁正在被限流?
每次 429 都会打出一个 ratelimit.rejected 指标,按路由和 key 类型打 tag。 Grafana 上有个面板,在 API → Health 下面。
可以给某个用户单独放宽限额吗?
可以——在 YAML 的 overrides: 下加上他的 API key。覆盖项支持热加载,不用走发布。