设置了熔断器却不起作用的原因
一句话总结
DestinationRule 的 trafficPolicy 会被转换为 Envoy 集群的字段。loadBalancer 对应 lb_policy,connectionPool 对应 circuit_breakers.thresholds,outlierDetection 对应 outlier_detection。转换后的数值会原样生效——超出限额的请求根本到不了上游,直接变成 503;连续失败的端点则是在经历失败之后才被移出。
为什么需要它
“明明设置了断路器,却没有效果”“明明开启了异常点检测,错误却一直在发生”,这类问题层出不穷。大多数情况下,并不是配置写错了,而是期望错了。断路器的限额不是按整个服务统计的,而是由发起调用的每一个 sidecar,按各自的集群分别统计。异常点检测是事后措施,而不是预防措施,所以在达到设定的次数之前,用户会先遭遇失败。此外,如果给子集单独指定策略,父策略会整块被覆盖,而不是逐个字段覆盖,没有写出的限额会悄悄消失。
这些问题,无论把 YAML 看多少遍都看不出来。必须查看它被转换成 Envoy 集群之后的样子,并真正让它溢出、让它失败,才能看到。
工作原理
| DestinationRule | Envoy 集群 |
|---|---|
loadBalancer.simple |
lb_policy |
connectionPool.tcp.maxConnections |
circuit_breakers.thresholds[0].max_connections |
connectionPool.http.http1MaxPendingRequests |
…max_pending_requests |
connectionPool.http.http2MaxRequests |
…max_requests |
outlierDetection.consecutive5xxErrors |
outlier_detection.consecutive_5xx |
outlierDetection.baseEjectionTime · maxEjectionPercent |
base_ejection_time · max_ejection_percent |
没有写出的限额,不是 Envoy 的默认值 1024,而是 Istio 填入的 4294967295(实际上不设上限)。子集拥有各自独立的集群,所以策略也是分别应用的;至于如何合并,官方文档只有一句话——子集的策略会覆盖对应的设置——仅此而已。覆盖的单位是 connectionPool、loadBalancer、outlierDetection、tls 这样的整块。
断路器的计数很简单。如果已经占用了 max_connections 个连接,新请求就会进入等待队列;如果队列超过 max_pending_requests,Envoy 会立即返回 503 和 x-envoy-overloaded: true。异常点检测会针对每个端点统计连续失败次数,达到 consecutive_5xx 之后,就在 base_ejection_time 期间把它移出负载均衡。不过,max_ejection_percent 限制了一次最多可以移出的比例——因为如果全部移出,就没有可以发送的地方了。
在现场相遇的样子
压力测试中,断路器没有打开。限额是由发起调用的每个 sidecar 分别统计的。如果发起调用的 Pod 有十个,服务实际收到的并发连接数就是限额的十倍。反过来,只有一个调用 Pod 的批处理作业,会立刻撞上很小的限额。
只有金丝雀子集偶尔会涌出大量 503。要么是在子集中只写了一个 tcp.maxConnections,结果父级的 http1MaxPendingRequests 消失了;要么相反,是在子集中写了较小的 http 限额之后忘了。可以用 proxy-config cluster 把子集集群的限额与父级并排放在一起查看。
开启了异常点检测,仪表板上的 5xx 却降不到 0。会一直看到被驱逐之前的失败,以及驱逐时间结束、端点回归之后的失败。异常点检测不是消除错误的装置,而是防止拖得太久的装置。要与重试配合使用,用户看到的错误才会减少。
官方文档:DestinationRule · Circuit breaking · Envoy cluster
下一项实验要做什么
编写带有流量策略的 DestinationRule,做出字段对照表,按照对照表建立集群,再用 config_dump 读回。混入一个始终失败的端点,数一数异常点检测在被打中几次之后才将其移出;用 /clusters 确认子集策略会整块覆盖父级;再向慢速上游同时涌入请求,统计断路器返回的 503。