K8s 1.28 滚动发布:readiness 瞬时抖动(GC STW / 连接池未建好)导致 Pod 被反复摘除-加回 Endpoints,failureThreshold / periodSeconds 怎么定才能容忍抖动而不放大 5xx?
该问题的正文、回答与讨论。
背景
15 年云原生,最近在做 K8s 1.28.9(containerd 1.7.13,cgroup v2,内核 5.15,x86_64)集群里一条 5 副本 Java 服务(Spring Boot 3.2.4,JDK 17.0.8,G1 GC)的滚动发布加固。探针配置:readinessProbe 是 HTTP GET /actuator/health/readiness,periodSeconds=10、failureThreshold=3、timeoutSeconds=1;livenessProbe 同路径但 failureThreshold=6。滚动策略 maxSurge=1、maxUnavailable=0。上游 3 个 gRPC 客户端(Netty 4.1.100,maxConnectionsPerHost=50)走 ClusterIP Service,CoreDNS TTL=30s。
现象
某次滚动发布,新 Pod 在 readiness 连续通过 2 次后,第 3 次瞬时返回 503——根因是 G1 GC 长 STW(GC 日志显示 Pause Full 2.8s,连接池在 STW 期间还没建好,/actuator/health/readiness 里的 db 依赖检查超时)。failureThreshold=3 下,这次 503 直接让 kubelet 把 Pod 摘除 Endpoints;约 15s 后 readiness 恢复,Pod 又被加回。这 15s 窗口内,CoreDNS 缓存(TTL=30s)+ 上游 gRPC 客户端的连接池仍持有旧 Pod IP,而旧 Pod 正在 Terminating(preStop 还在 sleep 5),一波 5xx(约 200+ 笔,P99 从 25ms 飙到 480ms)。
已尝试的排查
- 把
failureThreshold从 3 调到 5、periodSeconds从 10 调到 15:抖动窗口从 15s 缩到约 8s,但没消除——因为failureThreshold只控制"连续失败 N 次才摘除",不控制"摘除后多久加回",而加回速度取决于periodSeconds,两者耦合,调参空间有限。 - 在
preStop里加sleep 10,让旧 Pod 在 Terminating 期间继续接流量:5xx 减少但没消除,因为新 Pod 被摘除 Endpoints 期间,流量还是打到旧 Pod(旧 Pod 还在 Terminating),只是窗口延长了。 - 查了 CoreDNS 缓存(
TTL=30s)和 gRPC 客户端连接池(Netty 4.1.100),确认是 DNS 缓存 + 连接复用叠加导致的"流量仍打到旧 Pod"——这部分参考了社区里优雅下线的讨论,但我的卡点不在旧 Pod 侧,而在新 Pod 被反复摘除-加回 Endpoints 这个"抖动"本身。
期望得到的答案形态
failureThreshold/periodSeconds/timeoutSeconds的组合到底怎么定,才能让 readiness 瞬时抖动(GC STW、连接池未建好)不至于造成 Pod 被反复摘除-加回 Endpoints?有没有"软摘除"的原生方案(比如readinessGates+ 自定义控制器,先标记"降级"而不是直接摘除 Endpoints)?- 如果原生方案不够,社区里有没有经过验证的"readiness 抖动容忍"实践?比如 readiness 探针里加一个"连续 N 次失败才降级"的中间态,或者用
PodReadinessGate+ 自定义控制器做"软摘除"(Pod 仍留在 Endpoints 里但权重降为 0,等 readiness 稳定后再恢复权重)? - 与 id=1351(readiness 二值 vs 渐进就绪,JIT 预热)和 id=201(GC STW 撞 liveness)的关系:我的场景是 readiness 瞬时失败导致 Endpoints 抖动(Pod 被摘除-加回),不是 readiness 通过后的渐进放量,也不是 liveness 被 STW 撞穿,但根因都跟"探针二值 + 应用渐进就绪"有关,想听听社区有没有更系统的解法。