Kubernetes 熔断 配置避坑指南
前阵子我们一个订单服务半夜炸了,排查下来是下游库存服务的数据库慢查询,接口从 50ms 拖到 8 秒,订单服务的线程池被一点点堆满,最后整条链路陪着一起挂。事后复盘,大家一致觉得该上个熔断。Kubernetes 环境嘛,最顺手的方案就是 Istio 的 DestinationRule,配置看起来也就十几行,我寻思十分钟收工。
结果这十几行的配置前前后后折腾了快两天,踩的坑比配置本身还多,记录一下,免得下次再踩一遍。
先把话说清楚:Kubernetes 本身没有熔断
先把预期管理好:Kubernetes 原生没有熔断这个概念,Service 只是个负载均衡,下游慢成什么样它照样往里送请求。想熔断,要么在应用里做(Resilience4j、Sentinel 这些),要么上服务网格让 Envoy 替你做。我们当时为了不动代码选了后者,下面这些坑基本都是围绕 DestinationRule 来的,用应用层方案的朋友可以直接跳到最后一段。
坑一:host 没写对,整份配置等于白配
这是我踩的第一个坑,也是最低级的一个。DestinationRule 里的 host 必须和你请求的目标对得上,我一开始图省事写了短名 stock-service,而应用请求的目标是 stock-service.prod.svc.cluster.local,两边对不上,这份配置就静静躺在那里谁也不理。为什么这种关键信息官方文档不写在最显眼的地方呢?可能是我瞎没找到吧...
host 直接写 FQDN 最稳。apply 完过后可以用这个命令确认 Envoy 有没有真的吃到配置:
kubectl exec -n prod deploy/order-service -c istio-proxy -- \
curl -s localhost:15000/config_dump | grep stock-service
grep 出东西才算下发成功,grep 不出来就别往下折腾了,先解决配置本身的问题。
坑二:官方 demo 别原样抄上生产
官方文档的熔断示例里有一段:
connectionPool:
tcp:
maxConnections: 1
http:
http1MaxPendingRequests: 1
maxRequestsPerConnection: 1
这个 maxRequestsPerConnection: 1 是为了演示效果硬造出来的,每个请求都新建一条连接,阈值随便压随便爆。你要原样抄进生产,等于把连接复用整个关掉,每来一个请求都重新建连,再叠加 mTLS 握手,QPS 高一点 CPU 直接起飞。生产环境这个值要么不设,要么设个大点的数,demo 里写 1 只是让你用 fortio 轻松把现象压出来而已。
坑三:拿 curl 测两下全是 200,以为熔断没生效
配完我下意识进容器 curl 了几下,全是 200,一度以为没生效。后来才反应过来:熔断是被动统计出来的,你一条一条串行发请求,连接池和错误计数根本堆不起来,它凭什么弹开?
要测就得并发压,我用的 fortio:
kubectl run -it --rm fortio --image=fortio/fortio -n prod -- \
load -c 50 -qps 0 -t 20s http://stock-service:8080/ping
拉镜像报错的可以自己想办法,没准儿你网络比我好。跑完看输出里的 503,然后到 sidecar 里确认是不是熔断触发的:
kubectl exec -n prod deploy/order-service -c istio-proxy -- \
curl -s localhost:15000/stats | grep pending_overflow
这个计数在涨才是熔断在干活;如果看到的是 UH(no healthy upstream),说明是 Pod 被弹开或者压根没找到后端,那是另一回事了。
坑四:Pod 被弹开后,kubectl 里看不出任何变化
这个坑属于“知道之前很懵,知道之后很无语”。Envoy 的 outlier detection 是纯被动的,而且是每个客户端 sidecar 各自维护的本地状态,Pod 被弹开之后 kubectl get pods 里它照样 Running、照样 Ready,探针和重启次数全都不动。所以别去 Pod 状态里找证据,也别指望一个 sidecar 弹开了全网格都会知道。
弹开时长是 baseEjectionTime 乘以被弹开的次数,下限是 interval,默认一次 30 秒。测试时为了快点看到效果可以把 interval 设成 1s,但记得改回来,我就干过忘改的事,一个 Pod 被连弹两轮,直接三分钟没流量,容量本来就不富裕,雪上加霜。
坑五:副本少的时候,盯紧 maxEjectionPercent 和 panic 模式
maxEjectionPercent 默认 10%,副本多无所谓,副本少的时候这个默认值可能让你想弹都不敢弹,官方 demo 里直接写 100 不是拍脑袋,是有原因的。还有个更反直觉的:Envoy 有 panic 模式,健康副本比例低于阈值(minHealthPercent)的时候,它会干脆放弃弹开,把流量打到所有节点上,死马当活马医。这是故意的——反正服务已经快不行了,不如都试一试。但你要是不知道这个机制,排查时会以为熔断坏了。具体默认值版本之间有些差异,用之前对着你这版 Istio 的文档看一眼,别背博客(包括我这篇)。
应用层熔断也有坑,阈值要按 Pod 算
如果最后你选了在应用里做熔断(Resilience4j 这类),注意阈值是每个 Pod 各算各的。你按整个服务 1000 QPS 评估出来的阈值,摊到 10 个副本,每个 Pod 手里其实只有 100 QPS,滑动窗口可能半天攒不够触发条件。滚动更新时更明显,新 Pod 的指标是冷的,刚上线那会儿熔断基本是睁眼瞎。另外 retry 别和熔断瞎组合,重试会“洗白”失败计数,熔断器可能一直打不开,这两个谁包谁、什么顺序,得想清楚再上。
最后给一份实测能用的配置
把上面的坑都绕开之后,我们生产在用的差不多长这样,改改名字就能抄:
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: stock-service
namespace: prod
spec:
host: stock-service.prod.svc.cluster.local # <--- 用 FQDN,写短名容易匹配不上
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
http:
http1MaxPendingRequests: 50
http2MaxRequests: 200
maxRequestsPerConnection: 0 # <--- demo 里的 1 千万别抄,0 才是不限
outlierDetection:
consecutive5xxErrors: 5
interval: 10s
baseEjectionTime: 30s
maxEjectionPercent: 50
500 这种错误归 consecutive5xxErrors 管,502/503/504 这些网关错误另有一个 consecutiveGatewayErrors,想分开对待就都写上,别只设了一个然后纳闷为什么 500 不触发。
最后还是那句话,熔断不是配完就完事了,上面这些数字都是拿 fortio 压出来的结果,你抄走之前,最好也拿自己的真实流量压一遍。
评论
还没有评论。
发表评论
提交后评论将经过自动审核,审核通过后公开展示。