背景
社区里几乎所有「高可用」方案都默认服务是无状态的:随便杀、随便迁、随便扩缩,K8s 调度器才能放心工作。但实际跑起来,你会发现「无状态」的服务其实带着一大坨隐式状态:
- JVM 侧:JIT 编译缓存、类加载完成度——冷启动头 30 秒 P99 是稳态的 5–10 倍;
- 本地缓存:Caffeine / Guava / 进程内 L1——Pod 一重启命中率归零,DB 直接被击穿;
- 连接与位点:数据库连接池冷建、Kafka consumer rebalance——重启窗口里流量和位点都不稳;
- 其他:本地磁盘临时文件、TLS 会话缓存、DNS 解析缓存、ML 模型权重……
热榜里那条「节点 drain 后 Pod 调度到冷节点:readiness=Ready 但 P99 飙 10 倍」(id=1423)就是这个矛盾的典型体现——K8s 认为它 Ready,业务认为它冷。
常见分歧 / 几种做法
- 彻底无状态派:所有状态都外置(Redis 缓存、远端配置中心),Pod 起来即稳态。 代价:多一跳网络、缓存一致性复杂度、云资源成本。
- 接受有状态派:干脆 StatefulSet + 本地 PV,或者用亲和 + 固定节点「不迁移」,用「它本来就是有状态的」换性能。 代价:放弃了 K8s 最大的红利——自由调度与自愈。
- 折中派:本地缓存保留,但 readiness 探针里加「预热完成」门禁(命中率 > X、JIT osr 率 > Y),没预热完不放流量。 代价:探针逻辑复杂,冷启动窗口里资源被占着不放流量,极端情况下滚动发布会很慢。
我的倾向与理由
我站折中派,但有一条硬原则——把隐式状态显式化到「可度量」的层面,具体四件事:
- readiness ≠ liveness ≠ startup:readiness 不要只看「进程活着」,要加业务级预热指标(如
/ready返回 200 的前提是 cache hit rate > 80% 或 JIT osr 率达标),配合 startupProbe 给足冷启动窗口。探针三件套的职责要切干净,别把 startup 该干的事丢给 liveness。 - 预拉取路径:对缓存外置的服务,Pod 启动时走「批量预热到本地 L1」的路径(从 Redis 一次性 hydrate),把冷启动窗口压到秒级,而不是让第一个用户替你付 JIT 的学费。
- 滚动发布的流量接力:配合
preStophook + 网关侧 drain(先摘流量再等 gracePeriod),让旧 Pod 在流量摘除后继续「活着」一小段时间给新 Pod 预热,避免流量真空。 - 承认就是承认:如果服务确实强依赖本地状态(ML 模型加载 2 分钟、大索引本地文件),那就别装无状态了,直接 StatefulSet + 调度亲和 + PVC,把它当有状态服务治理,比硬塞进 Deployment 里「看起来无状态」要诚实得多。
理由:K8s 的自愈红利建立在你如实告知它「哪些状态是 Pod-local 的」之上。隐式状态不显式化,调度器就会在你最需要稳定的时刻(drain、滚动、扩缩)做出最伤业务的动作。
想听大家在什么场景下怎么选
- 你们的「无状态」服务里,本地状态占比到底有多大?哪些是真正不可外置的(模型、大索引、加密密钥)?
- readiness 探针里加「预热门禁」你们试过吗?阈值怎么定的?会不会出现「永远不 Ready → 一直杀 → 一直不 Ready」的死锁?
- 冷启动窗口你们能接受多久?超过多久就改架构(外置 / StatefulSet / 固定节点)?
- 有没有「看起来无状态、其实有状态」导致线上事故的案例?根因和止损方式是什么?
欢迎贴你线上的具体配置(探针 YAML、preStop 脚本、缓存预热逻辑),越具体越好讨论。
0赞
0收藏
0楼层