服务网格 E06:本地限流为什么不等于全局配额
两个代理都说“还剩一个名额”
前端的两个副本各有代理,两个 orders → inventory-v1 的只读 GET /quote?sku=paper 同时到达。若每个代理各维护“每秒 5 个”的局部桶,它们没有共享计数器:在同一计数窗口的理想示意中,两个满桶最多可各准入 5 个,合起来 10 个,而不是整个库存服务的 5 个。这里的 5、10 都是说明模型的假设值,绝非实测:窗口起点、token refill、路由偏斜、并发和实例动态扩缩会改变实际数量。
图分为两种模型:本地模式的两个桶各自连接库存;全局模式的两个代理向同一 RLS 查询。若要为同一租户设置全局配额,两代理须使用一致的 descriptor 向外部限额服务请求,由限额服务返回允许或超限。Envoy 的 global rate limit filter 发送请求并执行响应,但整个系统能否严格保持限额取决于限额服务实现、缓存、故障配置和存储,而不是 filter 的名字。单代理可能有多个 worker,不同配置的 token bucket 范围也不同,需要逐项核对。
突发、排队与故障策略
以纯理论、非测量的同时到达例子对比两种范围:
1 | |
| 限制方式 | 决策范围 | 配额服务失联时 |
|---|---|---|
| 两个独立本地桶 | 各代理各自计数 | 本地桶不依赖远端配额 RPC |
| 共享描述符的全局服务 | 服务端计数实现决定真实约束 | 按明确配置选择放行或拒绝 |
局部 token bucket 的容量决定短时间突发,补充速率决定长期平均;全局服务可能为降低网络往返预分配 token,允许短时超发。若在 orders 和 inventory 两层都限流,匹配维度不一致就会形成意外的双重 429;如果先前应用已执行 POST /reserve,事后被另一层拒绝也不能回滚账本。此处只用只读报价作限流主实验,合成副作用仅用于说明拒绝时点,不能写成订单事务。
设计两种基线:固定并发、同样的请求 ID 集合,分别对一代理和两代理施加相同的局部桶配置,统计每代理 200/429 与库存实际处理次数;然后在同样输入下使用全局限额服务和共同 descriptor。故障对照只停止隔离实验自己的配额服务,显式选择 failure mode:放行可用性更高但可能突破限额;拒绝控制支出但让正常业务失败。记录失败时间窗、限额 RPC 的超时/错误、客户端退出码、每个代理日志和后端响应,绝不能以配额服务的 ACK 当实际拦截数量。请求重试会改变计数,须冻结或单独记录(见 14 重试);共享代理资源和租户描述符范围见 31 多租户。
工程已经提供双代理驱动与配额服务;运行状态以输出目录 status.txt 为准,未执行的准入/超发/故障路径保持 NOT_RUN。复跑先冻结 Envoy 版本和两代理路由,运行限定时间的并发生成器,收原始请求和桶/RPC 统计,按实际观测给出超发范围,不将例子里的 10 当测量值。
两个代理与一个真实 RLS
examples/service-mesh/electives/E06/ 提供 Go 实现的 Envoy v3 Rate Limit Service,以及真实双 Envoy 驱动。服务使用累计工程已有的官方 protobuf 与 gRPC 依赖,不手写 HTTP 接口冒充 RLS。ShouldRateLimit 接收 domain=inventory、tenant=lab 描述符;两个代理必须发送相同的组合,才会操作同一份计数。
这里的共享范围是一个 RLS 进程,计数在 mutex 内完成,五次准入后返回 OVER_LIMIT。窗口从第一次有效调用开始,持续一小时,避免短实验中 refill 干扰比较。进程重启丢失计数,且没有持久存储或副本同步,因此不适合每日计费限额。它足以验证“两个代理共享决策点”这个机制,不证明多副本 RLS 的一致性。
本地桶同样使用初始容量 5、一小时补充 5。实验在一小时内完成时,两代理合计应允许 10 次;全局服务合计应允许 5 次。这里把 refill 时间拉长是为了隔离变量,生产参数应按业务突发和平均容量重新计算。
1 | |
启用比例与强制比例都设为 100,避免只计数不拦截。每个 Envoy 显式启动一个 worker,实验不把 worker 范围混入进程范围。默认本地桶不是跨两个 Envoy 进程共享;增加第二个进程也就增加了第二份初始容量。
全局模式把该过滤器换成 envoy.filters.http.ratelimit,在 route 中配置描述符动作:
1 | |
这个固定键只适合单租户实验。生产中若从请求头取租户,必须先认证并清除可伪造输入,否则攻击者换键即可获得新额度。RLS cluster 必须使用 HTTP/2,因为协议是 gRPC;仅把地址指向一个普通 HTTP 服务无法完成检查。
全局过滤器设置 domain: inventory、timeout: 0.1s 与 transport_api_version: V3。failure_mode_deny: true 在 RLS 调用错误时拒绝,正常 OVER_LIMIT 返回 429;故障拒绝默认是 500。这里的参数名与 E05 的 failure_mode_allow 方向相反,复制配置时很容易写错。
并发、故障和后端到达对照
1 | |
第一条只验证服务端共享计数:20 个 goroutine 并发请求同一描述符,仅 5 个得到允许,未知描述符拒绝。第二条才启动真实 Envoy,使用 20 个并发请求、每个代理 10 个请求的输入,依次运行本地、全局、RLS 停止且故障拒绝、RLS 停止且故障放行四组场景。脚本在每组结束后终止该组代理,避免后续实验复用已经消耗的本地桶。
| 场景 | 20 次请求预期允许 | 其余响应 | 库存实际处理 |
|---|---|---|---|
| 两个独立本地桶 | 10 | 429 | 与 200 数量一致 |
| 共享 RLS | 5 | 429 | 与 200 数量一致 |
| RLS 停止、故障拒绝 | 0 | 500 | 0 |
| RLS 停止、故障放行 | 20 | 无拒绝 | 20 |
表格定义可执行断言,本轮真实结果见下节。实际响应和请求 ID 保存为 requests.json;库存接收列表、各代理统计和 RLS 原始日志分别保存。客户端 200 而后端没有同一 ID,或者后端收到某个被拒请求,都会让场景失败。驱动没有配置重试,避免一次业务请求消耗多次 quota。
恢复分两类:代理与 RLS 的网络恢复之后,新 RPC 可以重新得到决策;RLS 进程重启还会重置内存计数。后者造成的新增准入不应归为 Envoy 超发。需要保持配额跨进程重启时,应把计数移到有明确原子操作、窗口与故障语义的持久共享存储,再复跑多 RLS 副本与存储故障实验。
| 需求关键词 | 可迁移原则 | 本例边界 |
|---|---|---|
| 扩容后总量变大 | 先确定状态共享范围 | 每进程本地桶独立 |
| 全局额度不增加 | 在共同决策点原子更新 | 单 RLS mutex,未验证分布式存储 |
| 依赖不可用 | 把故障策略计入容量承诺 | 故障放行会突破额度 |
本轮真实代理结果
Linux/arm64、Envoy 1.37.0 的完整驱动已通过,原始目录 examples/service-mesh/evidence/E06/20261003T091345Z-1/ 包含版本、配置验证、请求、后端 ID、代理日志、计数器和 status.txt。使用的 Envoy 镜像 digest 为 sha256:f9e63fffcc6e831547cbd1690d50c85f864d40348a5223ceee0af96037c4d506;Go 服务编译环境为 Go 1.25.1。E06 每组发起 20 个请求,本地允许 10、共享额度允许 5、故障拒绝允许 0、故障放行允许 20,重启内存 RLS 后允许 5;后端记录与允许数一致。临时 RLS 二进制在临时目录编译并清理,不进入证据目录。
这些结果限定于本例单机环回网络、固定过滤链和合成输入。它们不证明多机高可用、持久额度、生产身份认证或吞吐上限。
练习与参考解答
- 画出第三个代理加入时哪一层计数增加。参考解答:局部模式增加第三个独立桶,理论总准入容量随副本数变化;全局模式若三个代理使用同一 descriptor、后端实现原子计数,意图是维持相同总配额,但仍需测试缓存与突发边界。
- 将全局配额服务配置为故障放行后还可以声称严格限制每天 100 次吗?参考解答:不能。故障窗口内未受限的请求会消耗实际下游容量;需记录放行数并制定账单/补偿策略;调整为故障拒绝又会牺牲可用性。
资料与系列导航
Envoy 1.37 local rate limit、global rate limit、RLS API;源码入口 Envoy 固定提交限流过滤器。研究记录 writing-plans/service-mesh/research/E06.md。
系列导航:E05 外部授权 → E06 全局配额(本文) → E07 Ambient 多集群。
