Envoy 全局限流详解:gRPC Rate Limit 与配额(RLQS)两种架构的完整实践
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
本文基于 Envoy 官方文档《Global rate limiting》架构篇展开,系统讲解 Envoy 的两种全局限流实现:基于 gRPC Rate Limit 服务的逐连接/逐请求限流,以及基于配额(Quota/RLQS)的公平共享限流。读完本文,你将理解每种方案的适用场景,掌握 network/HTTP 两个层级限流过滤器的配置方法、限流动作(Rate Limit Action)的组合规则、统计指标与运行时开关,并能参考仓库中的示例配置在真实部署中落地全局限流。
一、为什么需要全局限流:熔断的失效场景
Envoy 的分布式熔断(circuit breaking)在大多数场景下能有效控制分布式系统的吞吐量,但文档明确指出一个典型失效场景:
当大量下游主机转发到少量上游主机、且平均请求延迟很低时(例如对数据库服务器的连接/请求),如果目标主机开始积压,所有下游主机会同时把流量灌入该上游集群,形成级联失败(cascading failure)。此时极难为每台下游主机配置一个"足够紧"的熔断阈值——既要保证正常流量模式下的系统正常运行,又要在系统开始故障时阻止级联失败。
问题的根源在于:熔断是每个 Envoy 实例独立决策的,N 台下游各自持有配额,聚合起来的上游压力 = N × 单机阈值,无法表达"整个集群加起来只允许 X QPS"这一全局约束。全局限流(Global rate limiting)正是为这种场景设计的:限流决策由一个集中的限流服务做出,所有 Envoy 实例共享同一份配额视图。
Envoy 提供两种全局限流实现:
- 逐连接或逐 HTTP 请求的限流检查(Per connection or per HTTP request rate limit check):每个新连接或新请求都向限流服务发起一次 gRPC 查询;
- 基于配额的限流(Quota based):通过周期性负载报告,让多个 Envoy 实例公平共享一个全局配额。适合大规模、高 QPS 且流量在各实例间分布不均的 Envoy 部署。
二、方案一:基于 gRPC Rate Limit 服务的限流
2.1 集成方式与参考实现
Envoy 直接集成一个全局gRPC 限流服务:只要服务实现了 Envoy 定义的 RPC/IDL 协议,就可以接入。官方生态中有一个用 Go 编写、以 Redis 为后端的参考实现(ratelimit 项目),部署该服务后,Envoy 的限流过滤器即可以通过 gRPC 与之通信。
Envoy 侧对这一集成提供了两个层级的过滤器:
- 网络层限流过滤器(network level rate limit filter):对监听器上安装的每一个新连接调用一次限流服务。配置中指定一个 domain 和一组 descriptors,最终效果是限制经过该监听器的连接建立速率(connections per second)。配置参考见 network rate limit filter 文档。
- HTTP 层限流过滤器(HTTP level rate limit filter):对监听器上的每一个新请求调用限流服务,但前提是路由表中指定了"该请求需要调用全局限流服务"。所有发往目标上游集群的请求、以及所有从源集群到目标集群的请求都可以被限流。配置参考见 HTTP rate limit filter 文档。
限流服务本身的配置见 Rate limit service 配置。
2.2 与本地限流的组合:两阶段限流
文档特别强调,Envoy 还支持本地限流(local rate limiting),它可以与全局限流叠加使用来降低全局限流服务的压力:
例如,一个本地 token bucket 限流器可以吸收非常大的突发流量,避免这些流量冲击全局限流服务。这样限流就变成两阶段:先由 token bucket 做粗粒度的初步限流,再由细粒度的全局限流完成最终的配额控制。
这一架构在数据库代理等低延迟、高连接速率场景中尤其重要——否则限流服务本身会成为新的瓶颈。
2.3 gRPC IDL 协议
Envoy 期望限流服务支持定义在 rls.proto 中的 gRPC IDL(RateLimitService.Check),限流服务集群配置则遵循 config/ratelimit/v3/rls.proto。如果未配置限流服务,Envoy 会使用一个"null"服务,调用时永远返回 OK。
2.4 网络层过滤器:统计、运行时开关与示例
网络层过滤器使用 type URLenvoy.extensions.filters.network.ratelimit.v3.RateLimit配置。每个已配置的过滤器都会在ratelimit.<stat_prefix>.*下输出统计:
| 名称 | 类型 | 说明 |
|---|---|---|
| total | Counter | 发给限流服务的请求总数 |
| error | Counter | 联系限流服务出错总数 |
| over_limit | Counter | 限流服务返回 over limit 的总数 |
| ok | Counter | 限流服务返回 under limit 的总数 |
| cx_closed | Counter | 因 over limit 响应而关闭的连接总数 |
| active | Gauge | 发往限流服务的进行中请求总数 |
| failure_mode_allowed | Counter | 出错但因failure_mode_deny为 false 而被放行的请求总数 |
这些计数器在源码 source/extensions/filters/network/ratelimit/ratelimit.cc 中逐处递增:发起检查时total_自增(约 L84)、服务返回超限后over_limit_自增(约 L129)、因超限关闭连接时cx_closed_自增(约 L135),并与连接关闭原因ratelimit_close_over_limit一起记录到连接统计中。
运行时开关:
ratelimit.tcp_filter_enabled:调用限流服务的连接百分比,默认 100;ratelimit.tcp_filter_enforcing:调用限流服务并执行决策的连接百分比,默认 100。可以先以 0 的 enforcing 值做"影子模式"观测,再逐步放量执行。
网络层过滤器的 descriptor 值支持基于 stream info 的替换格式化(如访问日志格式变量)。示例:
name: envoy.filters.network.ratelimit domain: foo descriptors: - entries: - key: remote_address value: "%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%" - key: foo value: bar stat_prefix: name该过滤器还会在 gRPC 限流服务的CheckResponse中填充dynamic_metadata字段时,将其作为不透明的google.protobuf.Struct发出到动态元数据。
2.5 HTTP 层过滤器:触发条件、429 语义与故障模式
HTTP 层过滤器使用 type URLenvoy.extensions.filters.http.ratelimit.v3.RateLimit配置,其工作语义(来自 HTTP rate limit filter 文档):
- 触发条件:当请求所匹配的路由或虚拟主机带有一个或多个与过滤器阶段匹配的
rate_limits配置时,过滤器才会调用限流服务;路由可以通过include_vh_rate_limits额外继承虚拟主机上的限流配置。一个请求可同时命中多份配置,每份配置都会生成一个 descriptor 发给限流服务。 - 限流响应:任一 descriptor 返回 over limit,即返回429(状态码可通过
rate_limited_status定制),并默认添加x-envoy-ratelimited响应头(可用disable_x_envoy_ratelimited_header关闭)。 - 故障模式(failure mode):调用限流服务出错、或限流服务返回错误时,
failure_mode_deny为 true 则返回 500,为 false 则放行请求。源码 source/extensions/filters/http/ratelimit/ratelimit.h 中可见该逻辑:构造函数保存failure_mode_deny_,还支持failure_mode_deny_percent以运行时百分比做灰度切换。 - Retry-After 头:启用
enable_retry_after_header后,当过滤器实际下发 429 时,响应会携带Retry-After头(delay-seconds形式)。取值为限流服务返回的所有 over-limit 状态中最大的duration_until_reset,且钳制为至少 1 秒,保证所有命中规则都能等到重置。如果限流服务自己返回了Retry-After头,过滤器不会覆盖。该选项默认关闭,且在上游自己生成 429、未强制执行限流、配置了非 429 状态码或服务未返回 over-limit 状态时均不会发出。
domain: foo enable_retry_after_header: true rate_limit_service: transport_api_version: V3 grpc_service: envoy_grpc: cluster_name: rate_limit_service统计指标输出在cluster.<route target cluster>.ratelimit.<optional stat prefix>.命名空间下,包括ok、error、over_limit、failure_mode_allowed四个计数器;429 或rate_limited_status配置的响应会计入该集群的常规动态 HTTP 统计。运行时开关为ratelimit.<route_key>.http_filter_enabled(按路由上限流配置的 route_key 指定调用限流服务的请求百分比,默认 100)。动态元数据默认存放在envoy.filters.http.ratelimit命名空间下——这一点在源码中可以得到印证:ratelimit.h 中当metadata_namespace为空串时回退到默认值"envoy.filters.http.ratelimit",命名空间可通过过滤器配置中的metadata_namespace字段更改。
仓库中的现网示例也可供参考:envoy_front_proxy.template.yaml 与 envoy_service_to_service.template.yaml 都演示了将envoy.filters.http.ratelimit插入 HTTP 过滤器链、并将限流服务集群指向ratelimit的完整配置形态。
2.6 Rate Limit Action 组合:构造复杂 descriptor
每个路由/虚拟主机上的rate_limits中的 action 都会填充一个 descriptor 条目,条目向量按配置顺序组成一个完整 descriptor。仓库自带的 rate-limit-routes.yaml 展示了五种典型路由,可以直接复用到自己的 bootstrap 中(以下截取其中前两条路由的rate_limits片段):
- match: prefix: "/route0" route: host_rewrite_literal: upstream.com cluster: upstream_com rate_limits: - actions: - source_cluster: {} - generic_key: descriptor_value: some_value0示例 1(组合):/route0的 action 列表是source_cluster+generic_key,生成的 descriptor 为:
("generic_key", "some_value0") ("source_cluster", "from_cluster")示例 2(条件性 action):/route1的 action 列表为source_cluster+remote_address+generic_key。如果请求没有设置x-forwarded-for,remote_addressaction 不产生条目,整个 descriptor 不生成、不会调用限流服务;如果请求设置了x-forwarded-for,则生成:
("generic_key", "some_value1") ("remote_address", "<trusted address from x-forwarded-for>") ("source_cluster", "from_cluster")2.7 Limit Override:用动态元数据覆盖服务端的静态限额
rate limit action可以携带一个limitoverride:其值会被追加到 descriptor 中发给限流服务,覆盖服务端的静态配置。override 的值可以从动态元数据中按指定metadata_key查找;取值失败或键不存在时,override 配置被忽略。
/route2的配置(摘自 rate-limit-routes.yaml):
rate_limits: - actions: - generic_key: descriptor_value: some_value2 limit: dynamic_metadata: metadata_key: key: test.filter.key path: - key: test被查找的动态元数据值必须是一个包含整数字段requests_per_unit和字符串字段unit(可解析为RateLimitUnit枚举)的结构体,例如:
test.filter.key: test: requests_per_unit: 42 unit: HOUR此时 descriptor 中会追加"42 请求/小时"的限额覆盖。
2.8 Descriptor 扩展:用任意请求属性作为描述符值
限流 descriptor 支持通过扩展机制扩展。envoy.extensions.rate_limit_descriptors.expr.v3.Descriptor扩展允许使用任何请求属性 作为描述符值:
- actions: - extension: name: custom typed_config: "@type": type.googleapis.com/envoy.extensions.rate_limit_descriptors.expr.v3.Descriptor descriptor_key: my_descriptor_name text: request.method此外,HTTP 匹配输入函数(matching input functions)也可以作为 descriptor 生产者,例如:
- actions: - extension: name: custom typed_config: "@type": type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput header_name: x-header-name上述配置产生 key 为custom、值取自请求头x-header-name的条目。若头不存在,则不产生该条目、不生成 descriptor;若头存在但为空串,则生成 descriptor 但不添加该条目。
三、方案二:基于配额(Quota)的限流
3.1 协议与当前可用服务
配额式全局限流只能作用于 HTTP 请求。Envoy 会对请求进行分桶(bucketize),并通过 HTTP Rate Limit Quota 过滤器配置,向限流配额服务请求配额分配。配额服务需实现 rlqs.proto 中定义的 gRPC IDL(RateLimitQuotaService)。
需要注意文档中明确标注的前提:该配额式限流服务的开源参考实现目前不可用,当前可与 Google Cloud Rate Limit Service 配合使用(文档中的 TODO 注明待参考实现与 GCP 文档可用后再补充链接)。选择该方案时需先确认自己的限流服务端可用性。
3.2 工作原理:分配、上报与再平衡
配额式限流与方案一"每请求查询"的根本区别在于周期性负载上报 + 服务端主动推送:
- RLQS 向每个连接的 Envoy 实例分配配额(quota assignment);
- 过滤器按配置的
reporting_interval周期性上报每个桶的请求速率,RLQS 据此再平衡各 Envoy 实例间的配额分配——这正是"公平共享"的实现机制,适合流量在 Envoy 实例间分布不均的大规模部署; - 配额分配变化时,RLQS主动推送新分配到 Envoy,无需 Envoy 轮询。
关键生命周期语义:
- 初始状态:所有 Envoy 的配额分配初始为空。当请求第一次匹配到某个桶时,过滤器才向 RLQS 请求配额。等待初始分配期间的行为由
no_assignment_behavior决定:可以立即放行所有请求,也可以拒绝直到收到配额分配。 - 分配过期:配额分配可以带 TTL(
assignment_time_to_live),RLQS 预期会在 TTL 到期前更新。若 TTL 过期仍未收到更新,过滤器可配置为继续使用最后一次分配,或回退到expired_assignment_behavior中预定义的值。 - 未匹配兜底:请求若不匹配任何 matcher,则应用
bucket_matchers中on_no_match字段配置的"catch all"桶;如果未配置on_no_match,所有未匹配请求不限流(fail-open)。 - 桶定义覆盖:桶定义可以被虚拟主机或路由配置覆盖,更具体的定义完全覆盖较不具体的定义(
RateLimitQuotaOverride)。 - 故障模式:与 RLQS 的连接失败时,若尚未收到配额则回退到
no_assignment_behavior,若已有配额但到期前无法重连则回退到expired_assignment_behavior。若某个桶在预定时间内始终未收到初始分配(时间由过滤器实现决定),该桶最终会被从内存中清除,后续请求将重新初始化该桶并重启上报。
3.3 完整配置示例:三个桶与不同的初始行为
仓库自带的 rate-limit-quota-filter-configuration.yaml 是一个可直接参考的完整示例,启用 3 个桶:
- 桶
name: prod-rate-limit-quota:匹配deployment: prod头的请求;等待配额分配期间放行所有请求(ALLOW_ALL); - 桶
name: staging-rate-limit-quota:匹配deployment: staging头的请求;等待配额分配期间拒绝所有请求(DENY_ALL); - 桶
name: default-rate-limit-quota:兜底桶;分配过期后回退到1000 RPS的固定限额。
- name: "envoy.filters.http.rate_limit_quota" typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaFilterConfig rlqs_server: envoy_grpc: cluster_name: rate_limit_quota_service domain: "acme-services" bucket_matchers: matcher_list: matchers: - predicate: single_predicate: input: name: request-headers typed_config: "@type": type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput header_name: deployment value_match: exact: prod on_match: action: name: prod-bucket typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings bucket_id_builder: bucket_id_builder: "name": string_value: "prod-rate-limit-quota" reporting_interval: 60s no_assignment_behavior: fallback_rate_limit: blanket_rule: ALLOW_ALL - predicate: single_predicate: input: name: request-headers typed_config: "@type": type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput header_name: deployment value_match: exact: staging on_match: action: name: staging-bucket typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings bucket_id_builder: bucket_id_builder: "name": string_value: "staging-rate-limit-quota" reporting_interval: 60s no_assignment_behavior: fallback_rate_limit: blanket_rule: DENY_ALL # The "catch all" bucket settings on_no_match: action: name: default-bucket typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings bucket_id_builder: bucket_id_builder: "name": string_value: "default-rate-limit-quota" reporting_interval: 60s deny_response_settings: http_status: code: 429 no_assignment_behavior: fallback_rate_limit: blanket_rule: ALLOW_ALL expired_assignment_behavior: fallback_rate_limit: requests_per_time_unit: requests_per_time_unit: 1000 time_unit: SECOND要点:bucket_id_builder支持按请求属性(如请求头值)动态生成桶 ID,也支持基于配置的静态生成;reporting_interval控制负载上报周期(示例为 60s)。
3.4 自定义拒绝响应:HTTP 与 gRPC 状态
配额过滤器支持通过deny_response_settings自定义超配请求的拒绝响应:
- HTTP 请求:通过
http_status配置状态码(默认 429); - gRPC 请求:可选设置
grpc_status精确指定 gRPC 状态码和消息;不设置时 Envoy 会从 HTTP 状态码推导。
默认行为(gRPC 状态由 HTTP 状态推导):
deny_response_settings: http_status: code: 429显式指定 gRPC 状态:
deny_response_settings: http_status: code: 429 grpc_status: code: 8 # RESOURCE_EXHAUSTED message: "Quota exhausted"四、两种方案选型与落地要点
| 维度 | gRPC Rate Limit(方案一) | Rate Limit Quota / RLQS(方案二) |
|---|---|---|
| 协议 | rls.proto(RateLimitService.Check) | rlqs.proto(RateLimitQuotaService) |
| 作用层级 | 网络层(逐连接)+ HTTP 层(逐请求) | 仅 HTTP 请求 |
| 决策模型 | 每次连接/请求同步查询 | 服务端分配配额,周期性上报负载并再平衡 |
| 对限流服务压力 | 与连接/请求速率成正比(可叠加本地限流缓解) | 上报频率由reporting_interval决定,压力与 QPS 解耦 |
| 适用场景 | 通用场景;有现成的 ratelimit 参考服务 | 大规模、高 QPS、实例间负载不均的部署 |
| 开源服务端 | 官方 Go + Redis 参考实现可用 | 目前需配合 Google Cloud Rate Limit Service |
落地时的通用建议(均源自上述文档):
- 限流服务本身也要有兜底:两个方案都定义了明确的故障回退(
failure_mode_deny/no_assignment_behavior/expired_assignment_behavior),上线前应明确"限流服务不可用时是放行还是拒绝"; - 先影子后执行:利用
ratelimit.tcp_filter_enforcing、ratelimit.<route_key>.http_filter_enabled等运行时百分比开关灰度放量; - 用统计验证:方案一检查
ratelimit.<stat_prefix>.*(网络层)与cluster.<cluster>.ratelimit.*(HTTP 层)计数器,方案二结合桶级上报确认各 Envoy 实例的负载分布。
本文所有结论均基于当前仓库中的全局限流架构文档、限流服务配置文档及网络层/HTTP 层过滤器文档、rls.proto、rlqs.proto 与 source/extensions/filters/http/ratelimit、source/extensions/filters/network/ratelimit 下的源码实现;适用前提为使用 v3 API 的 Envoy。
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考