加了代理,限额也跟着变大
一句话总结
全局速率限制把计数的地方放在代理之外的同一处。Envoy 会为每个请求生成描述符(descriptor),通过 gRPC 询问速率限制服务,服务则增加 Redis 中的计数器,回答放行或超限。因此无论代理有多少台,限额都只有一个。
为什么需要它
本地速率限制(令牌桶)速度快,没有依赖。但它的桶在一台 Envoy 之内。官方文档也说明,本地限制的桶默认作用于“一个 Envoy 进程”。入口代理从两台增加到六台的那一刻,“每位客户每小时 1,000 次”的约定就变成了 6,000 次。再加上自动扩缩容,限额会随流量自己增长——想要阻止的流量激增来得越猛,门就开得越宽。
像 API 销售这样存在全局约定的场景,计数的地方必须只有一个。这就是全局速率限制服务,而 Envoy 一侧有向该服务询问的 HTTP 过滤器(envoy.filters.http.ratelimit)。服务一侧的参考实现是 envoyproxy/ratelimit。
工作原理
1. 路由生成描述符。过滤器自己并不知道要统计什么。由路由(或虚拟主机)的 rate_limits 从请求中提取值,生成描述符。
rate_limits:
- actions:
- request_headers: { header_name: x-plan, descriptor_key: plan }
# x-plan: free 인 요청 → 설명자 [("plan", "free")]
如果没有这个头,就不会生成描述符,该请求不受限制。写在虚拟主机上的 rate_limits 会作用于该主机的所有路由,只有当路由自己有 rate_limits 时,才会改用路由的。
2. 服务与规则匹配。服务配置是一个域下的一组描述符规则。
| 配置 | 含义 |
|---|---|
domain: edge |
必须与 Envoy 过滤器的 domain 相同,才会使用这条规则 |
key: plan、value: free |
描述符恰好是这一对时 |
unit: hour、requests_per_unit: 3 |
每个时间窗口 3 次 |
3. 由 Redis 计数。服务在每个窗口用 INCRBY 增加一个键(edge_plan_free_<창 시작 시각>,占位符为窗口起始时间),并按窗口长度设置 TTL。服务本身没有状态,所以可以启动多台——限额汇集到一个 Redis 键上。
4. 结果体现在响应中。超限时 Envoy 返回 429。开启 enable_x_ratelimit_headers 后,会附加 x-ratelimit-limit、x-ratelimit-remaining、x-ratelimit-reset 头,客户端可以据此知道还剩多少。
服务挂掉时由 failure_mode_deny 决定。默认值 false 表示放行。速率限制是一种保护机制,这个选择的含义是:它出了故障,也不要让服务停下来。作为代价,那一刻只会记录在统计 cluster.<업스트림>.ratelimit.error、failure_mode_allowed(占位符为上游集群名称)中。
两者一起使用。全局限制每个请求都要多一次网络往返,而且该服务会成为新的瓶颈。所以把本地限制放在前面先过滤流量激增,全局限制则用来遵守约定。官方文档建议的组合也是这个。
在现场相遇的样子
“增加代理之后,客户的限额变多了。”这是只使用本地限制的情况。把限额按台数除开来写的办法,在自动扩缩容面前会失效。
“改了限额配置,却没有生效。”描述符只要与规则有一个字符不同,规则就不会命中,而未命中的请求会不受限制地通过。要分别确认:服务的 /rlconfig 是否读取了规则,以及 Envoy 发送的是什么描述符。
“Redis 故障期间,没有任何人被拦住。”因为默认值是放行。如果这是有意为之,就给统计设置告警;如果不是,就改为 failure_mode_deny: true,但这样一来,Redis 就掌握了所有请求的可用性,这一点也必须一并接受。
官方文档:Global rate limiting · Rate limit filter · Local rate limit filter · envoyproxy/ratelimit
下一项实验要做什么
在 Pod 内启动 Redis 和参考实现的速率限制服务,并搭建两台调用同一个服务的 Envoy。并排统计:交替发往两台的请求共用一个限额,以及同样的请求改用本地令牌桶时,每台各自单独计数。最后把服务杀掉,通过响应和统计确认默认值是放行。