K8s生产环境防雪崩指南:30条集群排障军规与资源预留实践
第一次把生产集群折腾到雪崩起因不是一次大版本发布也不是网络被流量打满而是一个节点重启。节点起来之后几十个Pod全部Pending调度器反复报Insufficient cpu。我盯着云厂商控制台里“8核16G”的规格看了半天实在想不通为什么8核连一个Pod都塞不进去。后来登到节点上翻了一圈才明白系统服务、kubelet、容器运行时、镜像GC阈值、日志文件、内核缓存……这些“不存在于Pod列表里”的资源占用早就超过了一半我以为的8核根本不是业务能用的8核。那次事故之后我开始把每一次线上排障的经验沉淀成一条条能对着检查的规则前后整理了30条。它们不解决“Kubernetes怎么装”它们解决的是“Kubernetes在业务底下怎么不崩”。这篇内容适合刚接手集群的运维、SRE也适合那些已经考过CKA但还没真正扛过生产压力的人。严格说这不是一篇教程更像是我给自己和团队留的一份集群巡检清单。1. 装机规划是最便宜的调优控制平面与工作节点的资源预算很多人调优第一反应是改apiserver参数、开HPA、配优雅停机但往往忽略了一个最基础的问题节点本身的资源预算就没算对。Kubernetes的资源模型很特殊它管的是“Pod能用的部分”而不是“节点上真实存在的部分”。这中间的差价如果一开始没规划好后面所有花哨的调优都是在沙地上盖楼。1.1 控制平面节点怎么给资源etcd的“阈值敏感体质”控制平面的核心组件里kube-apiserver、etcd、kube-scheduler、kube-controller-manager各有各的资源脾气。最常见的错误是按业务Pod的思路给它们配资源觉得apiserver吃内存就盲目给8G觉得scheduler平时很闲就给1核觉得etcd数据量不大就顺手塞在一台高IO业务机器上。先说etcd。它对CPU和内存的敏感度其实一般最致命的是磁盘IO和延迟。etcd每次写入都要做fsync它的健康模型里磁盘是一个“高敏体质”的角色。官方给过一个参考线磁盘的p99延迟需要稳定在10ms以内一旦出现明显抖动Raft心跳就可能超时随之而来的是频繁选主。你在业务Pod里跑一个高IOPS的数据库恰好又和etcd在同一个宿主机的同一个块设备上那等数据库开始刷盘时整个集群的leader可能都在晃。所以生产环境里etcd要么独立三节点用高性能本地盘要么至少要保证那块盘的写入延迟可控。资源占用方面几千个Pod规模的集群给etcd实例2到4GB内存通常够用但你还要给apiserver留出预算。kube-apiserver在集群规模大了以后大量watch连接、缓存和事件聚合会让内存涨得很快。一个几百节点的集群给apiserver预留4GB基础内存并不夸张。如果你把控制平面组件全部挤在一台机器上只按“组件本身多大”来估算内存就一定会出事。控制平面所在的节点我倾向于不跑业务Pod尤其不跑高IOPS的数据类应用。控制平面的“不稳定”波及的是整个集群一台工作节点宕机可能只影响几十个Podetcd节点抖动却会让所有组件的状态同步都停下来。1.2 工作节点规格先算“可调度资源”再算台数工作节点选大规格还是小规格一直是团队里争论不休的话题。我的结论很简单先定单节点Pod数上限再回来定规格。如果你选64核256G的超大节点看起来资源利用率很高但一个节点挂掉就是上百个Pod同时消失kubelet在节点恢复后要重建的容器数也极其惊人。反过来如果你用2C4G的小机器每台只能塞十几个Pod维护几百个节点的成本会先把你拖垮。比较健康的区间是单节点承载50到100个Pod左右CPU在4C到32C之间内存16G到128G具体按业务实际画像调整。这里有个新手最容易忽略的点kubectl describe node里面看到的Allocatable根本不是节点总资源而是扣掉系统预留、kubelet预留、驱逐阈值后剩下的部分。计算公式大致是实际可调度资源 节点总容量 - kube-reserved - system-reserved - eviction-thresholds如果Kubelet启动参数里没有设置--system-reserved和--kube-reserved它就会认为所有资源都能给Pod。等节点上的systemd、sshd、监控agent和Pod抢内存时Kubelet按驱逐阈值开始杀Pod可能业务Pod一批批被杀掉了系统进程反而还在。反直觉吧但这正是生产环境里真实发生的事情。以我常用的16C64G节点为例系统预留和Kubelet预留各给1G内存加0.5核CPU是比较保守的起步值驱逐阈值再留出memory.available1Gi的缓冲。如果节点上的数据库类中间件比较多还要再往上加。预留不是越少越好而是要根据节点上跑的系统组件实际用多少来定。1.3 容易被忽略的隐形占用镜像、日志与临时存储第三类资源坑不在Pod的CPU内存里而在磁盘。容器运行时的数据目录、容器日志目录、emptyDir临时目录这三样东西会在你毫无感知的情况下把节点盘写满。很多人装Kubernetes时直接给节点一块根分区系统、镜像、日志全放在一起。根分区一旦满了系统日志写不进去、kubelet心跳上报异常、sshd可能都连不上节点会直接变成NotReady。比较稳的做法是把/var/lib/containerd这类容器数据目录独立挂载一块盘和系统盘分开。这样即使容器镜像和日志把数据盘塞满你至少还能ssh登录节点做清理而不是眼睁睁看着整台机器失联。镜像的GC阈值由Kubelet控制imageGCHighThresholdPercent默认在大概85%时会尝试删除未被使用的镜像但如果你用了imagePullPolicy: Always旧tag的镜像可能一直残留。日志方面则必须配logrotate轮转尤其是Pod的stdout被kubelet写到宿主机/var/log/pods目录之后如果只收不转日志文件会慢慢吃光一整块盘。这一章压成几条军规军规01etcd所在节点不要和高IO业务混部磁盘延迟是它的命门。军规02控制平面至少三节点并分布在不同故障域单节点控制平面只配用来做实验。军规03工作节点规格不要走极端单节点Pod数控制在50到100之间。军规04必须设置kube-reserved和system-reserved否则节点资源的“纸面富余”会在关键时刻骗你。军规05容器数据目录与系统盘分开挂载给排障留一条后路。2. 从kubelet到containerd运行时调用链路的底层原理有读者问过我一个问题Kubernetes到底是怎么调用containerd的。恰好这也是面试高频题。很多人知道kubelet要和容器运行时通信但中间隔了几层、每一层做什么、故障时该查哪一层说不清楚。这里我按实体调用顺序拆一遍。2.1 CRI是kubelet与containerd之间的翻译官Kubernetes早期用Docker作为运行时kubelet内嵌了dockershim请求先到Docker daemon再由Docker去调containerd。链路长不说Docker还引入了自己的API转换。从Kubernetes 1.24开始dockershim被正式移除containerd自己实现了CRI插件成了最主流的运行时。所谓的CRI全称是Container Runtime Interface它把“创建容器、删除容器、查询状态”这类操作抽象成一组gRPC接口。kubelet并不关心你底层用的是containerd还是其他兼容CRI的运行时它只负责按--container-runtime-endpoint配置的socket地址发请求。以containerd为例kubelet启动参数里通常会有这样一段--container-runtimeremote --container-runtime-endpointunix:///run/containerd/containerd.sock当你要创建Pod时kubelet会先通过CRI接口让containerd创建一个sandbox也就是pause容器这个pause容器负责持有Pod的network namespace。随后kubelet再通知containerd启动业务容器让这些容器加入同一个sandbox。containerd内部收到CRI请求后会把请求转换成它自己的task管理操作再交给下一层执行。实体的调用顺序大致是这样的kubelet - CRI(gRPC) - containerd cri-plugin - containerd task API - containerd-shim - runc - Linux内核(namespaces/cgroups)。理解了这条链你就知道故障发生时应该去哪一层看状态如果kubectl get pods显示ContainerCreating但节点上进程完全没起来问题多半在containerd或runc这一层如果kubelet连不上CRI socketkubelet日志里会先报错Pod会一直卡在Pending。2.2 containerd-shim与runc谁在真正“看管”容器很多初学者以为containerd收到请求后就直接创建容器进程了其实中间还隔着一个containerd-shim。containerd本身是个管理daemon但它并不直接持有容器的主进程。每启动一个容器containerd都会拉起一个对应的containerd-shim进程这个shim有点像“监工”它负责维持容器的标准输入输出、向containerd上报状态、在容器主进程退出后回收资源并传递退出码。shim的存在还有一个很重要的意义当containerd daemon需要重启或升级时已经在运行的容器不会因为daemon挂掉而被杀掉。每个容器“看管人”是shim而不是containerd本身。这也是生产环境敢放心升级containerd的底层原因之一。再往下才是真正“动手”的runc。runc是OCI runtime的一个标准实现它根据容器的配置去创建namespace、设置cgroups限制、切换rootfs然后让业务进程启动。你在宿主机用ps -ef看到的容器进程父进程通常就是shim而不是runc因为runc在完成启动后就会退出把后续监护交给shim。运维排查时有个容易踩的坑crictl和ctr是两套工具。crictl是专门给kubelet/CRI视角用的客户端它会按Pod语义展示容器推荐在Kubernetes节点上排查问题优先使用。ctr是containerd的原生客户端直接操作containerd不经过CRI层用ctr操作Pod关联的容器可能让Kubernetes侧的状态变得不一致。如果某台机器执行crictl ps报connect: no such file多半是crictl默认连的还是Docker的socket需要手动配置crictl config runtime-endpoint unix:///run/containerd/containerd.sock crictl config image-endpoint unix:///run/containerd/containerd.sock2.3 边缘设备与镜像瘦身从源头减少运行时资源占用最近在边缘网关这类资源有限的设备上部署Kubernetes越来越多。边缘场景最现实的约束就是磁盘小、CPU弱、带宽不稳。我自己在类似环境里踩过最深的一个坑是镜像一个基础镜像里装了全套系统工具一层就占了几百MB。节点数量一多升级一次应用就要从镜像仓库拉几百MB慢不说磁盘也很快被撑满。瘦身不是让开发把所有依赖都硬塞进一个distroless镜像。更接地气的做法是基础镜像优先选alpine或distroless按需装工具构建时用小体积的二进制合并层减少层数。镜像体积小了edge节点上的磁盘占用和拉取时间自然就降下来。稍微有条件的团队还可以在节点上开启镜像预拉取把常用镜像在业务发版前就拉到本地避免发布那一刻把仓库带宽打满。配合Kubelet的镜像GC配置比如imageMinimumGCAge和imageGCHighThresholdPercent在边缘节点上把阈值调低一些让旧镜像更及时地被回收。如果你的边缘设备规格真的很低也可以评估K3s这类轻量发行版但在跨入那条路之前先把镜像和清理机制做对往往比换发行版更有效果。这一章的军规军规06使用containerd作为运行时确认kubelet的--container-runtime-endpoint指向containerd.sock不要拿Docker时代的习惯硬套。军规07排查Pod问题优先用crictl不要用ctr直接操作容器。军规08生产镜像禁止使用latest标签要用确定性的版本tag或镜像摘要。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →