Envoy 上游熔断机制深度解析:五种限流器、ResourceManager 源码实现与配置实践
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
在分布式系统中,熔断(Circuit Breaking)是防止故障放大的核心组件:与其让请求无限堆积,不如尽早失败(fail quickly)并把背压(back pressure)施加到下游。Envoy 的独特之处在于它把熔断限制强制执行在网络层,而不是要求每个应用独立编码和配置——这意味着即使后端服务本身不具备限流能力,流量在到达它之前就已经被 Envoy 拦截住了。读完后你将掌握:Envoy 五种熔断器的语义与触发路径、对应的 overflow 统计计数器、基于集群与优先级的阈值配置方式、通过 runtime 动态调整阈值的方法,以及ResourceManagerImpl源码层面的计数、gauge 与 retry budget 实现。
一、Envoy 的熔断体系:五种“去中心化”熔断器
Envoy 支持多种完全分布式(各实例之间不协调,not coordinated)的熔断器,均作用于**上游集群(upstream cluster)**层面。理解每种熔断器对应的统计计数器(位于config_cluster_manager_cluster_stats),是排查限流问题的第一入口。
1. Cluster maximum connections(集群最大连接数)
限制 Envoy 到一个上游集群中所有主机建立的连接总数。溢出时,该集群的upstream_cx_overflow计数器递增。
几个容易忽视的关键细节:
- 所有连接都计数:无论连接是 active 还是 draining,都计入该限制。
- 保底语义:即使熔断器已溢出,Envoy 仍会保证由集群负载均衡选中的某个 host至少有一条连接。由此推导出一个上界公式:
upstream_cx_active 的上界 = cluster maximum connections + (集群中 endpoint 数量) × (该集群的连接池数量)这个上界适用于所有 worker 线程的连接总和。集群有多少个连接池,取决于连接池化策略(参见 connection pooling)。
2. Cluster maximum pending requests(集群最大挂起请求数)
限制“正在等待一个就绪连接池连接”的排队请求数。每当没有足够的上游连接可以立即派发请求时,新请求就会被加入挂起请求列表。
对HTTP/2有一个重要特例:如果未配置Http2ProtocolOptions.max_concurrent_streams和Cluster.max_requests_per_connection,所有请求都会多路复用在同一条连接上——因此这个熔断器只有在尚无任何已建立连接时才会被触及。对HTTP/3,与 HTTP/2 的max_concurrent_streams对应的字段是QuicProtocolOptions.max_concurrent_streams。溢出时upstream_rq_pending_overflow计数器递增。
3. Cluster maximum requests(集群最大在途请求数)
限制任意时刻对集群中所有主机**在途(outstanding)**的请求总数。溢出时upstream_rq_active_overflow计数器递增。
注意一个行为变更:默认情况下,这条路径不再递增传统的upstream_rq_pending_overflow计数器。如果你的仪表盘或告警还依赖旧行为,可以把 runtime 标志envoy.reloadable_features.skip_pending_overflow_count_on_active_rq设为false,以恢复同时递增upstream_rq_pending_overflow的向后兼容行为。
4. Cluster maximum active retries(集群最大活动重试数)
限制任意时刻对集群中所有主机在途的重试总数。官方建议优先使用 retry budgets(见下文配置示例),而不是静态的max_retries;但若坚持静态熔断,则应设置得比较激进——目的是:允许偶发失败产生少量重试,但阻止重试总量指数膨胀并引发大规模级联故障(cascading failure)。溢出时upstream_rq_retry_overflow计数器递增。
5. Cluster maximum concurrent connection pools(集群最大并发连接池数)
限制可以同时实例化的连接池数量。某些功能(如Original Src Listener Filter)可能创建无界数量的连接池,这一熔断器就是为它们兜底的。
与“最大连接数”熔断器的关键区别在于生命周期:
| 维度 | 连接(connections) | 连接池(connection pools) |
|---|---|---|
| 超时回收 | 通常会有 | 从不过期 |
| 自动清理 | 是 | 否,需主动回收 |
当一个集群耗尽了并发连接池配额,它会先尝试回收一个空闲连接池;若回收失败,熔断器才溢出,upstream_cx_pool_overflow计数器递增。由于连接池正常工作至少需要一条上游连接,实践中该值不应大于Cluster maximum connections。
二、配置:按集群 × 优先级的阈值体系
每一个熔断限制都通过circuit_breakers配置段调整,并且按上游集群、按流量优先级(priority)分别跟踪。这允许分布式系统中不同组件(比如 DEFAULT 优先级业务流量与 HIGH 优先级旁路流量)独立调优、使用不同阈值。
一个典型的配置示例(来自 cluster_circuit_breakers 配置文档),其中retry_budget展示了百分比预算的推荐用法:
circuit_breakers: thresholds: - priority: "DEFAULT" max_requests: 75 max_pending_requests: 35 retry_budget: budget_percent: value: 25.0 min_retry_concurrency: 10上述示例中,retry_budget的语义是:允许的重试并发数不超过在途请求数的 25%,但至少允许 10 个并发重试。完整的 v3 API 字段(envoy.config.cluster.v3.CircuitBreakers及其Thresholds子消息)可在 Circuit Breakers v3 API 文档 中查阅。
运行时动态调整(Runtime)
所有熔断配置项都支持 runtime 覆盖,命名方案为:
circuit_breakers.<cluster_name>.<priority>.<setting>其中cluster_name是集群配置中的name字段。runtime 设置会覆盖静态配置文件中的值,这使得调参无需重新下发配置。
三、多 worker 线程下的共享语义与最终一致性
熔断限制在所有 worker 线程间共享。例如:活跃连接阈值为 500,若 worker 1 已占 498 条,worker 2 最多只能再分配 2 条。
需要理解实现上的取舍:该实现是最终一致的,线程之间的竞态可能导致限制被短暂超过。这一点在源码中被明确承认——ResourceManagerImpl 的注释写道:
/** * Implementation of ResourceManager. * NOTE: This implementation makes some assumptions which favor simplicity over correctness. * 1) Primarily, it assumes that traffic will be mostly balanced over all the worker threads since * no attempt is made to balance resources between them. It is possible that starvation can * occur during high contention. * 2) Though atomics are used, it is possible for resources to temporarily go above the supplied * maximums. This should not effect overall behavior. */也就是说,实现明确“以简单性换正确性”:不在线程间做资源再平衡(高争用下可能出现饥饿),原子操作也允许资源短暂超过上限。这与第一节中“upstream_cx_active可超过阈值”的保底语义是一致的工程取舍。
四、源码深潜:ResourceManagerImpl 如何计数与暴露状态
熔断核心实现在 source/common/upstream/resource_manager_impl.h,每个集群的每种资源都由ManagedResourceImpl封装(继承自BasicResourceLimitImpl,支持 runtime key 覆盖阈值)。
1. 打开态与剩余量的 gauge
每次inc()/decBy()后都会同步更新两个统计 gauge(见 ManagedResourceImpl):
open_gauge_:熔断器是否已打开(0/1);remaining_:距离熔断器打开还剩多少资源。
注意updateRemaining()的一个防御性细节:因为多线程下current_可能瞬时大于max,而两者都是无符号数,直接相减会下溢,所以用显式比较而不是std::max:
void updateRemaining() { const uint64_t current_copy = current_; remaining_.set(max() > current_copy ? max() - current_copy : 0); }这些统计正是运维面板上circuit_breakers相关 gauge(cx_open、remaining_cx等)的数据来源。
2. Retry Budget:比静态 max_retries 更聪明的重试熔断
ResourceManagerImpl内部嵌套的RetryBudgetImpl(源码位置)实现了动态重试预算,其行为完全可以通过源码确认:
- 动态上限公式(
max()方法):
// budget_percent 默认 20.0,min_retry_concurrency 默认 3 return std::max<uint64_t>(budget_percent / 100.0 * requests_for_budget, min_retry_concurrency);即:允许的重试并发 =max(budget_percent × 在途请求数, min_retry_concurrency)。当未配置budget_interval时,requests_for_budget取“在途请求数 + 挂起请求数”。
固定窗口模式:若配置了
budget_interval,实现切换为按固定窗口统计窗口内请求总量,灵感来自 tower.rs 的 TPS 预算(源码注释中明确引用)。定时器每budget_interval / NumSlots(NumSlots = 10)触发一次expireRequests(),用环形 slot 队列(expire_amountsdeque)把离开窗口的请求从计数中扣减。与 max_requests 熔断器的联动:配置了 retry budget 后,
requests_(在途请求)的on_inc_cb_会被挂上retries_.writeRequest(),让预算感知到新的在途请求。源码注释说明刻意只计在途请求、不重复计挂起请求:
// Count active requests when retry budget is configured. // Pending requests are not counted to avoid counting them twice. if (budget_percent.has_value() || min_retry_concurrency.has_value()) { requests_.on_inc_cb_ = [this]() { retries_.writeRequest(); }; }- 一个值得注意的运维细节:使用 retry budget 时,
remaining_retries_gauge 失去意义(它依赖于会随时变化的其他资源),因此clearRemainingGauge()会将其重置为 0。如果你的看板盯着 remaining retries 指标,需要注意该语义。
3. 阈值如何被 runtime 覆盖
每个ManagedResourceImpl都携带一个 runtime key(如<prefix>max_connections),阈值解析走Runtime::Loader——这解释了第二节中“所有设置均可按circuit_breakers.<cluster>.<priority>.<setting>在 runtime 覆盖”的能力。
五、启用与禁用:默认值、上限与 x-envoy-overloaded 头
默认行为:熔断器默认开启,且带有一些“保守”的默认值——例如每集群 1024 连接。
如何“禁用”:Envoy没有提供关闭熔断的开关,正确做法是把阈值拉到允许的最大值。官方 FAQ(Is there a way to disable circuit breaking?)给出的示例是把所有阈值设为1000000000:
circuit_breakers: thresholds: - priority: DEFAULT max_connections: 1000000000 max_pending_requests: 1000000000 max_requests: 1000000000 max_retries: 1000000000 - priority: HIGH max_connections: 1000000000 max_pending_requests: 1000000000 max_requests: 1000000000 max_retries: 1000000000该 FAQ 同时提醒:Envoy 支持路由级优先级路由,禁用时也要按优先级分别调整阈值。性能基准测试文档(how_to_benchmark_envoy)也把“关闭熔断”列为消除测量干扰的手段之一——因为熔断本身会改变系统在高负载下的行为。
HTTP 层的可观测信号:当 HTTP 请求因熔断被拒时,router filter 会设置x-envoy-overloaded响应头。上游调用方可以据此区分“业务错误”与“代理过载拒绝”,实现更合理的客户端侧退避。
六、排查清单:从计数器到配置项
结合前文,实际排障时可以按下面路径走:
- 看计数器定位类型:
upstream_cx_overflow(连接)、upstream_rq_pending_overflow(挂起)、upstream_rq_active_overflow(在途)、upstream_rq_retry_overflow(重试)、upstream_cx_pool_overflow(连接池)。 - 看 open/remaining gauge 判断裕量:熔断器的实时状态(剩余多少资源才触发)通过统计暴露,可接入告警。
- 核对 HTTP 协议特性:HTTP/2 全复用在一条连接上时,
max_pending_requests基本不会被触及,瓶颈更多体现在max_requests。 - 检查是否配置了 retry budget:若已配置,
remaining_retries_恒为 0,不能据此判断重试熔断状态;重试上限是动态的max(百分比 × 在途请求, min_retry_concurrency)。 - 注意多 worker 的最终一致性:短时超限属预期行为,不要据此怀疑实现 bug。
- 需要禁用时:没有总开关,用超大阈值近似禁用(如 FAQ 中的
1000000000示例),并理解这会移除一层网络层保护。
小结
Envoy 的熔断不是应用代码,而是内建于代理网络层的一组分布式资源限制器:连接数、挂起请求数、在途请求数、重试数、并发连接池数五个维度,按集群与优先级独立配置,通过 runtime 热调整,并以 overflow 计数器和 open/remaining gauge 提供完整可观测性。其实现(ResourceManagerImpl)在“简单性优先”的多线程最终一致性假设之上,还内置了比静态阈值更优雅的动态 retry budget。理解这套机制的语义边界(draining 连接也计数、worker 间短暂超限、retry budget 下 remaining gauge 失效等),是把 Envoy 用作服务网格数据面、并构建可靠限流与告警体系的前提。
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考