ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

K8s资源模型深度剖析:Request、Limit与调度器如何决定Pod命运

K8s资源模型深度剖析:Request、Limit与调度器如何决定Pod命运 写这篇东西的念头源于我在生产环境里排查过一次诡异的节点资源碎片问题。当时集群里明明还有大量剩余CPUPod却怎么都调度不上去kubectl describe node一看才发现是内存资源碎片化导致打分全部偏低。那次排查让我意识到很多人对K8S资源模型的理解还停留在“给Pod写个request和limit”的层面但对调度器究竟怎么消费这些数字、QoS等级怎么影响命运、节点打分背后是什么逻辑其实是一笔糊涂账。这篇文章就把K8S调度体系里最核心的“资源模型”彻底讲透。既适合刚把K8S跑起来、正准备深入理解调度原理的新手也适合那些已经在维护集群、经常被“调度失败”“资源碎片”“QoS被驱逐”折磨的运维老兵。我会从资源模型的基本单位讲起一路拆到调度器的打分机制、Node实际可分配量的计算、以及GPU这类扩展资源怎么接入调度最后附上我踩过的坑和排障命令集。保证你看完能从“会写YAML”进化到“懂调度”。1. 资源模型的底层认知Request、Limit与调度器的“口味”1.1 资源模型到底在解决什么问题K8S本质上是一个“多租户的资源超卖和隔离系统”——这句话你可以先记下来。整个调度器的存在目的就一句话把Pod放到一个能满足其资源需求的节点上。这里的关键词是“需求”。如果你只写一个containers.resources字段K8S根本不知道这个容器要多大的“房子”。所以K8S定义了一套资源描述语言核心就是请求request和限制limit。调度器只看request完全不看limit——这是最容易被误解的一点。1.2 CPU和内存的计量单位比你想象的讲究先看一个基础YAML片段resources: requests: cpu: 500m memory: 128Mi limits: cpu: 1 memory: 256MiCPU这里出现的500m意思不是500毫核而是0.5核。m是milli的简写1个核心1000m。所以1和1000m是等价的但如果你写0.1K8S会直接报错因为规范里CPU的合法单位就是“整数核”或者“m后缀的毫核”。内存的Mi是Mebibyte2的20次方字节而M是Megabyte10的6次方字节两者差别虽然只有4.8%但在大内存Pod的调度计算里这一点误差可能就导致节点打分差出几个百分点进而排到不同的节点。在调度器内部所有这些资源都被统一换算成一个整型数CPU的单位是“毫核”内存的单位是“字节”。节点报告自己有多少资源Pod请求多少资源最终都在这个统一的数值空间里做加减法。1.3 Request进调度器Limit进Kubelet各自管一段这是整篇的思路基石先画个清晰的分工字段谁在消费作用时机实际效果requestsScheduler、Kubelet调度决策、节点资源扣减决定Pod落在哪个节点limitsKubelet运行时cgroupPod运行阶段限制容器的CPU/内存用量上限调度器在决定节点时只看你请求了多少而Pod真正跑起来之后Kubelet会把你写的limit写到cgroup里。所以你会发现一个Pod如果只写了limits没写requests调度器会默认把requestslimits来用——这种做法最安全但资源利用率会偏低因为调度器按上限给你预留资源。1.4 为什么CPU超卖会抖动内存超卖会OOMCPU是可压缩资源limit限制的是CPU时间片。即使CPU超卖容器也只会变慢不会死。内存是绝对不可压缩资源一旦超卖内核的OOM Killer就会出手干活优先杀掉占用高或者优先级低的进程。这带来了一个核心结论生产环境里内存的requests必须是你真正的内存需求尽量不要在内存上做超卖。我见过太多人CPU和内存都写了很低的request指望“反正还能用swap”结果节点一抖Pod全变成OOMKilled。2. 节点资源都去哪了从Allocatable到系统占用2.1 节点资源的总账本调度器不是拿着节点上的所有物理资源来做计算的它使用的是status.allocatable——这是Kubelet向API Server上报的“可分配资源量”。计算公式如下Allocatable Node Capacity - Reserved(kube-reserved system-reserved) - eviction-threshold其中Node Capacity节点真实的物理总资源比如cpu: 8、memory: 32Gikube-reserved给Kubelet、容器运行时containerd等系统组件预留的资源system-reserved给systemd、sshd等宿主机系统进程预留的资源eviction-threshold内存和磁盘的驱逐阈值默认memory压力是100Mi意味着当节点可用内存低于100Mi时开始触发驱逐也就是说调度器的账本比物理资源“小一圈”。如果你没有显式配置kube-reservedKubelet会使用默认值通常是cpu100m, memory1Gi这种级别。有些云厂商的节点比如EKS会用system-reserved和kube-reserved把5%左右的资源默认锁掉所以明明买的是8核32Gkubectl describe node看到的allocatable往往只有7.8核31.5G——这是正常的。2.2 节点上的Pod资源怎么被扣减当Pod被调度并启动后Kubelet会通过status.allocatable减去该Pod的requests得到节点剩余可调度资源。这个扣减是立即生效的——调度器做出判断后Pod对象就进入了节点的status里记录即使容器还没启动资源就已经被占用。这也是为什么你经常看到“Pending Pod卡住不动”但节点看起来还有资源kubectl describe node worker-node-1输出的Allocated resources一节显示的是所有已经绑定到该节点的Pod的requests总和不只是Running状态的Pod。如果节点上有大量状态诡异的Pod比如Terminating卡住或者大量失败的Job它们的requests依然占着这个账本。很多人排查“明明资源够却调度不上去”时第一反应去看Running的Pod数量其实应该看这个聚合值。2.3 查询节点资源账本的实战命令# 查看节点完整资源视图重点看 Allocatable 与 Allocated resources 的差额 kubectl describe node node-1 # 用jsonpath快速计算某个节点的剩余CPU、内存 kubectl get node node-1 -o jsonpath{.status.allocatable.cpu}{\n}{.status.allocatable.memory}{\n} # 更直观地看每个节点Requested资源占比 kubectl describe nodes | grep -A4 Allocated resourcesAllocated resources下面的% of Allocatable就是你判断该节点是否过载的参考。比如CPU Requested是5%内存却是110%这个节点挂掉的概率极高因为内存超卖已经超过物理上限了。3. 调度器怎样利用资源模型选择节点3.1 从Predicate到Priority先筛后排的两段式流水线K8S默认调度器kube-scheduler的工作模式分为两段Feasibility可行性和Scoring打分。Feasibility阶段是过滤条件判断节点是否满足Pod的硬性要求其中最重要的就是PodFitsResources检查节点的剩余可分配资源是否满足Pod的requests。如果剩余资源不满足直接淘汰。Scoring阶段则是排序对通过过滤的节点打分。默认打分插件NodeResourcesFit会根据节点的资源使用情况打分策略有两种风格LeastAllocated默认剩余资源越多的节点得分越高适合“分散”Pod让集群负载均衡MostAllocated已分配资源越多的节点得分越高适合“聚合”Pod把负载打满部分节点再启用新节点K8S 1.30默认的NodeResourcesFit策略是混合模式CPU走LeastAllocated、内存走LeastAllocated你可以通过scheduler-config.yaml自己调整apiVersion: kubescheduler.config.k8s.io/v1 kind: KubeSchedulerConfiguration profiles: - schedulerName: default-scheduler plugins: score: disabled: - name: NodeResourcesFit enabled: - name: NodeResourcesFit weight: 2 pluginConfig: - name: NodeResourcesFit args: scoringStrategy: type: LeastAllocated resources: - name: cpu weight: 1 - name: memory weight: 13.2 分数是怎么算出来的心里要有数LeastAllocated的计分逻辑是score (node_capacity - total_requested) / node_capacity * MaxScore简化理解剩余资源越多得分越高。如果一个节点CPU容量是8核已经requested了6核那它的CPU分数就是(8-6)/8*100 25分。内存同理最后把各项资源的分数按权重加权就是总分。这个加权机制极其重要。默认情况下CPU和内存权重相同这意味着如果一个节点只剩大量内存但CPU几乎耗尽另一个节点CPU充足但内存只剩一点两者的总分可能相近。你会发现Pod的分布在不经意间出现了偏斜。我经历过一个场景集群里全是高CPU低内存的Pod结果内存使用率被推到90%以上而CPU还剩一半——这就是权重不当的典型后果。你可以通过给内存增加权重或者给不同命名空间设置ResourceQuota来约束。3.3 一个真实案例为什么“看起来资源够”却调度失败曾经有个CLB服务Pod一直Pendingkubectl describe pod说0/4 nodes are available。我看节点状态时每个节点都显示剩余CPU和内存都不少比如4核节点还剩1600m CPU、2.5Gi内存而待调度的Pod只要求500m CPU和512Mi内存理论上绰绰有余。结果翻到事件列表才看到真正的原因节点有污点Taint比如node.kubernetes.io/unreachable或者自定义污点。调度器的过滤阶段不只检查资源还会检查TaintToleration。当节点有污点而Pod没有对应容忍时这个节点就被过滤掉了——无论配额多充足。这也提醒你在排查调度问题时别只盯着资源数值先看完整的事件流kubectl describe pod pod-name | grep -A10 Events事件里通常直接写明了被哪个插件过滤、哪个节点不可行这比你自己一个个节点猜要高效得多。4. QoS等级当资源紧张时谁先死谁后死4.1 三档QoS的划分规则K8S给每个Pod分配一个QoSQuality of Service等级这个等级直接决定了节点内存压力时Pod的“牺牲顺序”。三档分别是Guaranteed每个容器都设置了requests和limits且每个资源项的requests limits。这类Pod优先级最高几乎不会因节点内存压力被驱逐。Burstable至少有一个容器设置了requests但不满足Guaranteed。这一类Pod在资源充足时一切正常节点压力上来后可能被驱逐。BestEffort所有容器都没有设置requests和limits。这类Pod一遇到节点压力第一个被清理。值得注意的是当你在容器的resources里只设置了limits没有requests时K8S会自动把requests值复制为limits的值于是这个Pod会被归为Guaranteed。这也是为什么很多人无意中就拥有了高优先级Pod。4.2 OOM Killer与Kubelet Eviction的先后顺序当节点内存真的不够时有两套机制在同时发挥作用Kubelet的Eviction Manager根据evictionThresholds检测节点内存压力按QoS等级从低到高驱逐PodBestEffort先走再Burstable最后才是Guaranteed。内核的OOM Killer如果Kubelet还没来得及动手内核会基于进程的oom_score由QoS等级和三者的记忆体用量计算直接杀进程。这里有个非常经典的坑如果你的Pod是Guaranteed但设置的内存limits物理上超出节点可用内存万一Kubelet驱逐不及OOM Killer会去杀。而OOM Killer并不认QoS的优先顺序它只在同一个cgroup里按分数排队。所以Guaranteed不等于永生——内存limits超过节点物理上限一样会被杀掉。4.3 如何根据QoS给重要业务兜底生产环境的建议很粗暴数据库、核心API等关键服务一律做成Guaranteedrequests和limits写一样的值对跑批任务、离线分析这类能容忍延迟的用Burstablerequests写经验均值limits写峰值永远不要让核心业务变成BestEffort我还有一种做法给那些“不能死”的Pod设置priorityClassName: system-cluster-critical同时配合PriorityClass让调度器在节点压力来临时优先保证它不被驱逐。但记住PriorityClass只在节点驱逐时生效它本身不改变QoS。5. 扩展资源与自定义资源让K8S“认识”GPU5.1 从CPU内存到GPU资源模型的延伸K8S的资源模型不是封闭的。它允许你通过Extend Resources上报任意设备资源典型的就是GPU。NVIDIA的device plugin会把GPU上报成nvidia.com/gpu调度器在过滤节点时会把GPU请求和节点上报的GPU数量做匹配。resources: limits: nvidia.com/gpu: 1这里有个特别容易踩的坑GPU是只支持limits不支持requests。如果你只写requests不写limitsdevice plugin没分配GPUPod会一直Pending而如果你写了limits调度器会把它当作requests来处理。也就是说即便你只写限制系统也会隐式创建一个等值的请求。5.2 自定义资源的注意点像GPU这类扩展资源还有几个行为细节必须先搞清楚扩展资源只能是整数不支持500m这种小数扩展资源不能设置requests和limits不一致也就是说你写了limit值requests就会被强制定为同一个值调度器只看allocatable中的扩展资源量而这个量由device plugin、调度器扩展点一起维护如果你想给自己的硬件设备比如FPGA、RDMA网卡建立一个资源模型基本套路是写一个Device Plugin启动时向Kubelet注册资源名和数量用户Pod声明your-vendor.com/device: 1的limits调度器据此做分配然后Kubelet绑核或透传设备5.3 节点上GPU一旦被占满Pod卡住排查我遇到过GPU Pod一直在Pending的问题describe node显示节点有8张GPU但Allocated resources里GPU的使用量已经超过8。排查后才知道是有一批Terminating状态的残留Pod没有释放GPU资源。解决办法是手动删掉这些僵尸Pod或者把GPU Pod的terminationGracePeriodSeconds调短让它们在节点压力增加时尽快退出。6. 多维度资源限制ResourceQuota与LimitRange6.1 Namespace怎么“分蛋糕”资源模型的另一大块是在Namespace维度上管理总量。ResourceQuota可以为命名空间里的所有Pod设置总量上限apiVersion: v1 kind: ResourceQuota metadata: name: dev-quota namespace: dev spec: hard: requests.cpu: 10 requests.memory: 20Gi limits.cpu: 20 limits.memory: 40Gi count/pods: 50这个配额限制的不只是Pod数量更重要的是requests和limits的总和。当Namespace里的Pod请求总量达到了配额上限后续Pod一律调度失败事件里会出现exceeded quota。这非常有用但要注意如果Namespace没有ResourceQuota调度器是不会主动“限流”的。我见过不少团队在新Namespace里忘了配额结果开发环境被一堆测试Pod打穿节点全部过载。建议任何一个新业务接入集群时先想好配额再放Pod进来。6.2 LimitRange批量设置默认资源LimitRange是用来给“没写资源的Pod”兜底的策略。它可以给Namespace设置默认的requests和limits值apiVersion: v1 kind: LimitRange metadata: name: default-limit-range namespace: dev spec: limits: - default: cpu: 500m memory: 512Mi defaultRequest: cpu: 200m memory: 256Mi max: cpu: 4 memory: 8Gi type: Container设置了LimitRange之后凡是在这个Namespace里创建的Pod如果没有显式声明资源就会被自动注入默认值。这可以保证不会出现BestEffort的Pod——“虽然我没写但平台帮我兜底了”。7. 实战疑难杂症资源碎片的成因与对策7.1 为什么资源够仍然调度不下资源碎片是K8S调度里的老大难问题。当一个集群中每个节点的剩余资源都大于单个Pod的需求但因为每个节点某一维度比如CPU或内存的剩余不足导致所有节点都无法满足Pod的需求就会出现“全局有资源局部无可用”的碎片困境。典型的例子集群10个节点每个节点剩余1.2核CPU、1Gi内存这时来了一个需要2核CPU的Pod调度器找不到任何节点能放下它虽然总体CPU剩余有12核。对策我总结下来比较有效的是把一个节点上的Pod数量压缩减少碎片比如每个节点最多跑30个Pod而不是默认的110使用topologySpreadConstraints把压力均衡分布防止某个节点被掏空对业务做资源画像不要无脑把requests设成峰值的两倍导致资源被白白浪费7.2 排查命令集建议放进你的运维手册# 查看所有节点可分配与已分配的概览 kubectl get nodes -o custom-columnsNAME:.metadata.name,CPU:.status.allocatable.cpu,MEM:.status.allocatable.memory # 查看所有节点的Pod密度对判断碎片很有用 kubectl get pods -A --field-selectorstatus.phaseRunning -o wide | awk {print $8} | sort | uniq -c | sort -rn # 查看某个Pod为何调度失败的核心事件 kubectl get events --field-selector involvedObject.namepod-name -n namespace # 查看当前节点的压力状态 kubectl get node node-name -o jsonpath{range .status.conditions[*]}{.type}{.status}{\n}{end}7.3 节点内存超卖率我建议的合理区间根据我的运维经验可以给出一个资源超卖的安全参考区间生产环境业务类型CPU超卖率内存超卖率备注在线核心API1:11:1必须Guaranteed在线一般业务2:11.2:1内存不要超卖太多离线批处理任务4:11.5:1容忍重试和迟延超卖率 所有Pod requests总量 / 节点容量。CPU可以适度超卖但内存超卖率超过1.5节点局部OOM的概率会呈指数上升。8. 资源模型与HPA、Cluster Autoscaler的联动8.1 HPA只看requests不看真实负载HPAHorizontalPodAutoscaler的扩缩容逻辑基于一个很容易被忽略的事实它计算的是当前Pod的requests量而不是真实资源使用率。打个比方你的Pod CPU requests是500m实际只用了100m但HPA按当前消耗/requests的比值来判断是否扩容。如果requests设置得太大HPA永远不会扩容相反requests设置得小负载一波动就疯狂扩容浪费资源。所以在配置HPA之前先做一轮资源画像把requests调整到合理值再设置HPA阈值才有意义。我见过太多团队先上HPA再回头调requests导致扩缩容震荡不断。8.2 Cluster Autoscaler如何结合资源模型扩容集群层面的自动扩缩容逻辑更直接当出现Pending的Pod且调度器找不到合适节点时Cluster AutoscalerCA会触发节点池扩容。CA判断扩容的依据同样是资源模型——它检查所有Pending Pod的requests计算需要增加多少容量的节点才能装下这些Pod。如果你的Pod requests写得太高比如CPU请求是实际用量的5倍CA扩出来的节点数量会远超真实需求账单起飞。这里有一条经验法则在自动扩容的集群里CPU requests按P99的60%-70%来设内存requests按P99的80%来设。既能保证调度稳定又不至于让CA过度扩容。8.3 一个数据化资源画像的小栗子如果想知道一个服务真实的资源使用率分布可以用Prometheus Grafana做。核心指标如下# 容器的CPU实际使用率相对于request sum(rate(container_cpu_usage_seconds_total{container!}[5m])) by (pod) / sum(kube_pod_container_resource_requests{resourcecpu}) by (pod) # 容器的内存实际使用率相对于request sum(container_memory_working_set_bytes{container!}) by (pod) / sum(kube_pod_container_resource_requests{resourcememory}) by (pod)把一周的数据拉出来看P50、P95、P99。如果P99还不到requests的40%就把requests往下调如果P99接近100%就得往上调或拆副本。这套方法执行一两个迭代后集群的整体资源利用率至少能提升20%-30%。9. 写在最后的几个调度资源建议生产集群折腾到现在我对资源模型最深的三点体会第一requests是排队的依据limits是打架的底线。千万别把limits当成requests用这两个值含义完全不同。所有调度优化、成本控制都是围绕requests做文章。第二宁可牺牲一点资源利用率也要保证内存不超卖。CPU超卖顶多让Pod变慢内存超卖直接把Pod杀掉。这个风险等级完全不同。第三集群里的资源不是越多越好碎片化比资源少更可怕。与其所有节点都跑一半负载不如把一部分节点打满另一部分节点空出来承接大Pod。适当用MostAllocated策略或者配合节点池拆分把“大内存业务”和“高CPU业务”分开节点池部署能有效缓解碎片问题。调度和资源模型这块内容非常深这篇文章先把骨架立起来——从Request/Limit的概念到调度器的打分机制再到QoS、扩展资源和配额管理最后落到排障命令和参数经验。如果你能把这套逻辑串起来再看任何调度问题都会清晰很多。后面如果有时间我准备再写一篇调度器插件的深度剖析把PreFilter、Filter、Score这些扩展点逐个拆开讲敬请期待。
返回列表