Envoy Composite Cluster 详解:基于重试次数的确定性上游集群选择机制
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
Envoy 的 Composite Cluster 扩展把"重试演进"固化为一种集群类型:子集群列表的顺序就是重试尝试的次序,第 N 次尝试确定性地路由到第 N 个子集群,而不再由健康状态比例决定流量走向。读完本文,你会掌握它的配置方法、attempt count 到集群下标的源码映射链路、无主机时的回退(failover)边界行为,以及与路由重试策略的配套关系。
痛点:重试打到哪里,你说了不算
Envoy 路由的重试机制解决的是"要不要再试一次",但没回答"这一次试谁"。默认情况下重试仍然落在原集群,由原集群的负载均衡算法(round robin、maglev 等)在现有主机里挑一个——对多提供商、多成本档位的上游架构来说,这远远不够:你希望第一次请求走首选供应商,第一次重试切到备用供应商,第二次重试落到最便宜的兜底服务,并且每次尝试的落点可预期、可复现。
Aggregate Cluster 看起来接近,但它的顶层负载均衡按各子集群的健康状态分流,结果取决于健康度快照,天然带不确定性。Composite Cluster 的选路依据只有一个:当前请求是第几次尝试。两者定位不同:
| 维度 | Aggregate Cluster | Composite Cluster |
|---|---|---|
| 选择依据 | 子集群健康状态 | 重试尝试次数 |
| 适用目标 | 按健康比例容灾 | 按次序确定性降级 |
| 超出容量 | 取决于健康状态 | 请求直接失败 |
Composite Cluster 配置方法:列表顺序即尝试次序
配置消息是 ClusterConfig,扩展注册名envoy.clusters.composite。它只有一个字段clusters(repeated ClusterEntry,校验min_items: 1),每个ClusterEntry只有一个name(校验min_len: 1)。被引用的子集群必须在配置的其他位置独立定义——Composite Cluster 自身不承载 endpoint、负载均衡算法或健康检查。
name: composite_cluster connect_timeout: 0.25s lb_policy: CLUSTER_PROVIDED cluster_type: name: envoy.clusters.composite typed_config: "@type": type.googleapis.com/envoy.extensions.clusters.composite.v3.ClusterConfig clusters: - name: primary_cluster - name: secondary_cluster - name: fallback_cluster关键字段到运行时行为的对应关系:
lb_policy: CLUSTER_PROVIDED→ 主机选择完全委托给被选中的子集群,Composite 自身无 host;clusters[0].name→ attempt 1(初始请求)的目标;clusters[1].name→ attempt 2(第一次重试)的目标;clusters[2].name→ attempt 3(第二次重试)的目标;- attempt 4 及以后 → 下标越界,请求以 no host available 失败。
源码机制:attempt count 到 cluster index 的三步链路
选择逻辑全部发生在 worker 线程本地的负载均衡器里,实现集中在 cluster.cc。入口chooseHost()(cluster.cc#L156-L170)的链路是"提取 → 映射 → 委托":
第一步,提取尝试次数。getAttemptCount()(cluster.cc#L45-L58)从LoadBalancerContext取requestStreamInfo(),读其中的attemptCount();上下文或 StreamInfo 为空时回退为 0。
第二步,1 基转 0 基。mapAttemptToClusterIndex()(cluster.cc#L60-L77)做纯算术映射:
if (attempt_count == 0) { ENVOY_LOG(warn, "invalid attempt count 0 ..."); return std::nullopt; } const size_t cluster_index = attempt_count - 1; // attempt 1 → index 0 if (cluster_index < clusters_->size()) { return cluster_index; } return std::nullopt; // 越界:请求失败第三步,委托子集群选主机。拿到下标后selectHostWithFailover()通过getClusterByIndex()经cluster_manager_.getThreadLocalCluster()取到子集群的ThreadLocalCluster,用 lb_context.h 中的CompositeLoadBalancerContext包装原始上下文后,调用cluster->loadBalancer().chooseHost()由子集群自己的负载均衡算法完成最终主机选择。peekAnotherHost与selectExistingConnection(cluster.cc#L172-L208)走同样的"先映射下标、再委托"路径,但只针对映射到的单个集群,不做 failover 遍历。
线程模型上,cluster.h 中Cluster继承自ClusterImplBase,initializePhase()返回Secondary(等待被引用的子集群先初始化完成);CompositeLoadBalancerFactory在每个 worker 线程内各自创建一个CompositeClusterLoadBalancer,后者构造时通过addThreadLocalClusterUpdateCallbacks注册回调整步子集群的增删,选择路径本身无跨线程开销。
边界行为:越界尝试与同一尝试内的 failover
两组边界行为值得逐条确认。
越界尝试。attempt_count == 0属异常状态,打 warn 日志并返回nullopt;重试次数超过子集群数时(例如配置 3 个子集群却有 attempt 4)同样返回nullopt,chooseHost返回空主机,请求以 no host available 失败。这正是"子集群数量应与重试总尝试次数对齐"的底层原因。
同一尝试内 failover。映射到的子集群若拿不出任何主机(DNS 解析返回空 endpoint 列表、或被 outlier detection 全部逐出),selectHostWithFailover()(cluster.cc#L112-L154)会从该下标起向后遍历,直到某个集群能返回主机;全部失败才以no_healthy_upstream结束:
for (size_t cluster_index = start_index; cluster_index < clusters_->size(); ++cluster_index) { auto* cluster = getClusterByIndex(cluster_index); if (cluster != nullptr) { CompositeLoadBalancerContext composite_context(context, cluster_index); response = cluster->loadBalancer().chooseHost(&composite_context); if (response.host != nullptr || response.cancelable != nullptr) { return response; // 选到主机或异步选择进行中,立即返回 } } if (!skip_clusters_without_hosts) { break; // 开关关闭时,第一个无主机的集群即终止 } }两个细节:
- 若子集群返回的
cancelable非空(异步主机选择进行中),循环立即返回,不再尝试后续集群——异步流程拥有选择过程的全部后续; - 该行为受 runtime 开关
envoy.reloadable_features.composite_cluster_skip_clusters_without_hosts保护(默认开启)。设为false回退旧行为:映射到的子集群无主机则本次尝试立即失败,不做向后 failover。对应修复见 composite_cluster__skip-clusters-without-hosts.rst,解决的正是"无主机时误报 503 no_healthy_upstream"的问题。
还有一个容易踩的点:failover 不移动后续尝试的映射。映射由尝试次数驱动,配置[primary, secondary, fallback]且primary为空时,attempt 1 因 primary 无主机落到 secondary,attempt 2 依然映射 secondary(而非 fallback)。设计重试规模时要按这个语义预留余量。
重试策略配套:让次数与集群数对齐
Composite Cluster 依赖路由级重试策略提供"尝试次数"这个输入信号:
retry_policy: retry_on: "5xx,gateway-error,connect-failure,refused-stream" num_retries: 2 # 共 3 次尝试:attempt 1、2、3num_retries: 2意味着 1 次初始 + 2 次重试 = 3 次尝试,恰好覆盖上文三个子集群。配大了,超出的尝试越界失败;配小了,尾部的兜底集群用不上。集成测试 cluster_integration_test.cc 按 3 个静态子集群 + 1 个 composite 集群构造场景,路由设置retry_on: "5xx",并借助include_request_attempt_count/include_attempt_count_in_response回显每次尝试实际命中的集群;单元测试 cluster_test.cc 则覆盖 null 上下文、无 StreamInfo、越界值等getAttemptCount边界,以及关闭 runtime 开关后的旧行为回退。
何时用、注意什么
Composite Cluster 适合"按次序降级"而非"按健康分流"的架构:AI Gateway 多提供商故障切换(首选模型 → 备用供应商)、成本分级路由(先打昂贵高性能服务、失败回退便宜服务)、以及任何需要重试落点完全可预测的场景。
使用时把握三条纪律:📌lb_policy必须为CLUSTER_PROVIDED,子集群数量与num_retries + 1严格对齐;⚠️ 每个子集群独立维护健康检查、负载均衡与 outlier detection,Composite 只负责"选哪个子集群";⚠️ 同一尝试内的 failover 只影响当前尝试,不会推移后续尝试的映射,重试策略的规模要按此语义设计。
完整语义说明可对照架构文档 composite_cluster.rst,与 aggregate 集群的对比章节在其中。
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考