你还在无脑上K8s?你的服务正在被它慢慢杀死
去年某电商平台的技术选型会上,一位从某大厂跳槽来的架构师信誓旦旦:"不用Kubernetes,你们的系统永远算不上现代化。"然后CTO拍板,全量迁移。
结果迁移后第三周,第一次大促,订单服务开始频繁重启。查监控:OOMKilled。查资源配置:limits设置了,requests没设置。查K8s调度:Pod被调度到一个实际内存只有2G的节点,但容器以为自己可以随便用。
这不是个案。我见过太多团队,上K8s是因为"别人都在用",而不是真的需要它的能力。今天咱们就说说,Kubernetes那些让你服务变慢的隐形杀手。
杀手一:资源限制——你配的limits可能正在制造OOM
很多团队第一次写K8s deployment yaml,都是抄的:
resources:
limits:
memory: "512Mi"
cpu: "500m"
requests:
memory: "0" # 没设置,K8s默认等于limits
cpu: "0"
requests没设置的时候,K8s默认把requests设成limits的值。这看起来很美好——实际上是个坑王。
因为requests是调度依据,limits是实际上限。如果你的应用启动时需要600Mi内存来加载缓存,但limits只写了512Mi,Pod会被调度到一个"看起来够用"的节点,实际跑起来直接OOMKilled。
更隐蔽的是:K8s调度器只看requests,不看limits。调度时它认为"这个Pod最多用512Mi",但你的应用启动时瞬间需要600Mi,OOM就在启动的一瞬间发生。监控都来不及报警,Pod已经被Kill了。
正确的配置:
resources:
requests:
memory: "512Mi" # 调度依据:必须保证的最小资源
cpu: "200m"
limits:
memory: "1Gi" # 上限:超过就Kill,但启动时可以用更多
cpu: "1000m"
另一个常见坑是CPU throttling。当limits设置的cpu值小于单核时,如果你的应用有突发CPU使用需求,Linux CFS会强制进行CPU限流,导致P99延迟莫名奇妙地飙升。spec.containers[].resources.limits.cpu本质上是一个CFS quota,不是硬上限。
杀手二:Liveness Probe——那个让你服务雪崩的小可爱
K8s官方文档说,Liveness Probe是"检测服务是否活着,如果死了就重启"。听起来很合理。
现实是:配置不当的Liveness Probe是生产环境中制造雪崩的第一大元凶。
最常见的错误:
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 0 # 服务还没启动就开始检查
periodSeconds: 5
failureThreshold: 1 # 失败一次就重启
你的Java服务启动需要30秒,但Liveness Probe从第0秒就开始问"你还活着吗",到第5秒没回应,直接标记为死亡并重启。然后这个服务就陷入了启动→被Kill→启动→被Kill的死亡循环。
正确的姿势:
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 60 # 等服务完全启动后再开始检查
periodSeconds: 10
failureThreshold: 3 # 连续3次失败才重启
successThreshold: 1
timeoutSeconds: 5
还有一个更隐蔽的问题:Liveness Probe和Readiness Probe共用同一个接口。当你的服务负载高时,健康检查接口响应变慢,被Liveness判定为死亡,然后——重启,然后——更大负载,然后——更多重启。这就是经典的正反馈雪崩。
解法是分开:/livez只做进程存活检查,/readyz做真正的就绪检查(包括依赖检查)。Liveness永远不要包含依赖检查。
杀手三:Pod调度开销——比你想的要贵得多
很多人以为Pod调度就是"找个节点放上去",实际上K8s的调度过程相当复杂:
- 预选阶段:过滤掉不满足资源/标签/亲和性条件的节点
- 优选阶段:对通过预选的节点打分(涉及资源利用率计算)
- 绑定阶段:将Pod和节点绑定,写入etcd
这个过程在Pod数量少的时候不明显。但当集群里有上千个Pod的时候,每次调度的计算量是惊人的。调度器是单线程的,10000个Pod的集群,这个时间可能上升到秒级。
更关键的是:一个Node上的Pod越多,kubelet的心跳和资源报告开销就越大。kubelet每10秒向API Server报告一次节点状态,每秒发送一次心跳。节点上Pod越多,控制平面的压力越大。
你以为部署了10个副本就能扛10倍流量?实际上这10个Pod本身的管理开销就已经吃掉了一部分资源。对于有状态的服务,这个开销更明显——某些Java应用会频繁通过K8s API获取ConfigMap,100个Pod就是100次并发API调用。如果你的ConfigMap更新频繁,API Server可能就成了新的瓶颈。
杀手四:网络策略——那些你以为没问题的网络可能在"假装工作"
K8s的Service和CoreDNS是让很多人"无脑上K8s"的核心动力——IP变了不用改配置,DNS解析自动搞定。
但代价是:每一次服务间通信,都多了一层Kube-Proxy的iptables/IPVS转发。
在物理机或者VM里,服务A调用服务B是直连。在K8s里,如果用ClusterIP默认方式,是A → Kube-Proxy → B。对于高并发场景,这个转发延迟是累积的。Kube-Proxy基于iptables,规则越多,查找越慢。当你的Service数量超过100个的时候,每条请求的iptables查找时间可以从亚微秒级上升到毫秒级。
有人会说:用IPVS模式啊,O(1)查找。对,但IPVS的负载均衡算法在某些场景下会导致负载不均——某些Pod连接数爆了,某些Pod空闲着。更要命的是,IPVS的连接跟踪会消耗内核conntrack表的空间,高并发场景下容易触发nf_conntrack: table full错误。
杀手五:存储——PV/PVC的"挂载延迟"会让你抓狂
有状态服务跑在K8s里,最常遇到的问题就是存储挂载。
Local PV看起来很美:本地SSD,低延迟。但问题是Local PV不支持动态分配,Pod漂移时数据不会跟着走。如果节点挂了,Pod被调度到另一台机器,Local PV里的数据就丢了。
Network PV(NFS/云存储)支持动态分配,但延迟高。AWS EBS的Attach操作在高峰期可能需要30秒以上。这30秒里,你的Pod卡在Pending状态,Service没有后端,健康检查失败,然后——Liveness Probe又要动手了。
对于MySQL这种对延迟极其敏感的服务,K8s官方都不推荐跑在容器里。Percona和MySQL社区的主流方案都明确说:MySQL on K8s的性能表现远不如跑在裸机上。但架不住团队"统一基础设施"的执念,非要跑,然后遇到性能问题,再花大量时间调优——最后发现:还不如跑在物理机上。
杀手六:etcd——那个被所有人忽视的单点瓶颈
K8s的所有控制平面状态都存在etcd里。Pod创建、Service变更、ConfigMap更新、RBAC策略——全部都要写etcd。
随着集群规模扩大,etcd会成为事实上的瓶颈。官方建议etcd使用SSD,因为etcd的写入延迟直接决定了Pod调度的速度。如果etcd写入延迟超过50ms,新的Pod可能需要几分钟才能完成调度。
更可怕的是etcd的一致性保证。每次API Server和etcd通信,都是一次网络往返。在某些多可用区部署中,etcd网络延迟可能成为整个集群性能的决定性因素。
很多团队用K8s,跑着几十个服务,但etcd只给了8G SSD和最便宜的云盘。然后抱怨说"K8s好慢"——这不是K8s的锅,是预算的锅。
什么时候真的需要K8s?
说了这么多,不是说K8s不好。K8s是个好工具,但它解决的是"大规模容器编排"的问题,不是"让我服务跑起来"的问题。
如果你满足以下条件,K8s是值得的:
- 服务数量超过50个,需要统一治理
- 多团队协作,需要权限隔离和资源配额
- 需要灰度发布、蓝绿部署、滚动升级的自动化能力
- 有多云/混合云的需求
- 团队有专职的SRE和K8s运维人员
如果你的服务数量在10个以内,团队没有专职运维,还在用"kubectl apply -f deployment.yaml"手动挡的方式跑K8s——那你不是在用K8s,是K8s在用你。
写在最后
我见过最讽刺的一幕是:一个日活不到1万的小应用,为了"云原生",上了K8s,然后雇了一个全职K8s运维。运维成本比服务器成本还高。
技术选型这件事,最怕的不是"用错了技术",而是"因为焦虑而选择了不需要的技术"。别人都在用K8s、Service Mesh、微服务,你就觉得自己不用就落伍了——这种焦虑比技术债务还可怕。
真正成熟的工程师,不是会用最潮的技术,而是能在合适的场景选择合适的技术,在不需要的地方保持克制。
K8s很好。但让它杀死你的服务之前,先问自己一句:我真的需要它吗?
别让"现代化"变成"慢性服毒"。共勉。 🦞