ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Kubernetes资源模型:requests与limits决定Pod调度与QoS

Kubernetes资源模型:requests与limits决定Pod调度与QoS 1. 两个数字把Pod的一生写得明明白白刚接手集群的那段时间我最常被问的问题是为什么我的Pod一直Pending。每次看过去几乎一半的情况都是资源模型写错了要么requests写得太高没人敢接要么根本忘写requests只写了limits要么把CPU当成内存算。K8S的调度机制其实很单纯——它只根据你写在Pod里那几个字段来做预判也就是资源模型中的requests和limits。这个模型理解了集群调度也就理解了七成。1.1 request是入场券limit是紧箍咒先看一个典型Pod配置apiVersion: v1 kind: Pod metadata: name: demo-pod spec: containers: - name: app image: nginx:1.25 resources: requests: cpu: 500m memory: 256Mi limits: cpu: 1 memory: 512Mi让我把这两个对象讲透。requests是Pod要进节点需要向调度器报名时出示的入场券。调度器在预选阶段会逐个节点算账节点剩余可用资源Allocatable减去已经在运行Pod的requests是否大于等于当前Pod的requests小于就淘汰。注意这一步完全不看limits。limits是运行期的紧箍咒。CPU的limit决定这个容器最多能用多少个CPU时间片超过就被内核限流内存的limit决定容器能用到多少内存超过就开始遭殃轻则卡顿重则被杀。所以一句话requests决定你人能不能进去limits决定你进去后能多嚣张。这两个字段完全没有默认关联。很多刚入门的朋友以为写了requests就会自动带上对应limit或者反过来。真的不是。K8S里Pod如果没有指定resources默认按BestEffort处理如果用LimitRange之类的机制做默认值那是另一回事。我之前遇到过一个项目只有requests没写limits业务高峰期容器内存悄悄涨到远超requests因为它根本就没有上限节点被打到swap整宿都在追查谁写的小请求。1.2 CPU是可压缩资源内存是不可压缩资源资源模型背后最关键的一个原理就是CPU和内存对待超限的态度完全不一样。CPU是可压缩资源。意思是CPU超限后容器不会被杀死只是被限流行为主要表现为CPU使用率被拉平、请求变慢、延迟升高。K8S通过Linux CFSCompletely Fair Scheduler的quota来实现容器设置了cpu limit代表它在每个时间窗口内最多能使用的CPU时间超了就只能等下一个窗口。内存是不可压缩资源。进程申请内存失败或者触底限额时内核不可能让进程等着内存明天就有了。它要做的是立刻回收或者杀掉进程。容器内存超过limit后会被内核OOM直接Kill掉。因此内存超限对业务的冲击远比CPU超限要狠往往伴随重启、丢状态、雪崩。这里有个核心类比CPU就像一条拥堵的高速公路车多的时候大家都慢下来但没人会被丢下车内存则像一个仓库货已经摆满了还继续往里面搬要么把旧的货扔掉要么搬货的人直接被赶出去。K8S的资源模型就是把这套机制抽象成了两个字段。一般docker run里面对应的是-c和-m参数K8S就是把这套cgroup参数翻译成了更工程化的声明requests对应你要保证的资源limit对应你最多能用多少。所以介绍K8S和Docker区别时资源模型往往是最先被拿出来讲的差异点因为Docker只负责给你资源而K8S还要负责怎么给、给多少、不够怎么办。2. 调度器只认requests但候选节点之间的差距全靠算知道requests的重要性之后我们再往调度器内部走一步。K8S默认的调度器kube-scheduler把一次调度拆成预选和优选两个阶段。资源模型在预选阶段扮演看门人在优选阶段又会参与打分两个阶段的玩法完全不同。2.1 预选节点能不能装下这个Pod其实就是一道减法题调度器评估每个节点时会维护一组Node资源账本总容量Capacity、可分配量Allocatable、已分配量被现有Pod的requests占用的量。剩余可分配 Allocatable - 已requests总和。如果候选节点的剩余可分配不足新Pod的requests直接过滤掉。一个特别常见的误区是节点的使用量到底看什么。这里看的不是节点当前实时CPU/内存使用量而是所有Pod声明的requests之和。换句话说只要Pod声明要2C哪怕它实际只用0.1C调度器也认为那2C已经被占用了。这就是为什么有时候你会看到节点负载很闲但Pod调不上去——因为账面上已经被申请光了。如果你经历过集群明明CPU五成都没到却调度失败多半就是这个原因。在这个阶段还有一个必须警惕的问题节点上的DaemonSet、已驱逐但没清理的Ending Pod都会继续占住资源账本。尤其DaemonSet里的日志采集、监控agent数量一多requests累计起来非常可观。所以排障时别光看业务Pod要看节点上每一类Pod的requests总和否则你永远想不明白节点为什么空着却进不去。2.2 优选不只看装得下还看装得多不浪费预选通过后调度器会给节点打分。近几年的K8S版本里核心策略是NodeResourcesFit里面有LeastAllocated和MostAllocated两种模式。默认的LeastAllocated倾向于把Pod调度到当前资源占用率最低的节点这样可以让负载更均匀而MostAllocated倾向于把Pod塞到最满的节点也就是人们常说的binpack装箱能压缩机器数量。在实际优化集群成本时我非常推荐试一下MostAllocated。比如一个三节点的Redis集群如果都是低延迟、小内存的Pod用binpack策略把Pod尽量压到少数节点上剩下的节点就可以进入休眠或下线省下实实在在的机器钱。代价是热点风险更高需要靠反亲和性来兜底。这也就是热词里总提到负载调度器、集群调度的原因——调度器本质上是在资源利用率和稳定性之间做平衡。不过我给大多数生产集群的建议是先保持默认的LeastAllocated因为它的行为最可预期。Binpack优化放到后置位等你对业务画像、节点池结构都摸清楚了再通过修改KubeSchedulerConfiguration或者引入调度插件来做也不迟。2.3 调度完成后资源账本怎么算Pod调度成功后调度器会锁定该节点上的资源把这部分requests记到节点的已分配头上节点剩余资源相应减少。Pod被驱逐或删除后资源账本会逐步归还。这里存在一个时间差节点资源账本是异步更新的有些情况下Pod已经没了但节点剩余资源仍然显示偏低需要等缓存刷新。遇到这种节点看着还有资源但调度还是失败的问题我一般会直接看调度器缓存触发重新算账。还有一个细节节点上系统预留的问题。默认情况下K8S会预留一部分资源给系统守护进程体现在Allocatable Capacity。这个差值由NodeStatus里的SystemReserved字段控制。大家做集群巡检时不要拿Capacity去算可用资源一定要以Allocatable为准否则你会高估集群容量。我曾见过容量规划报告里把Capacity全算进去结果按规划扩容了一半就疯狂Pending一查就是没算系统预留。3. QoS分级同样超内存被杀死的往往不是同一个Pod资源模型不光是告诉调度器你要多少它还能决定一个Pod在节点压力下的生存优先级。这就是K8S的QoSQuality of Service分级。很多人真到节点OOM时才知道这三个字母的含金量。3.1 三类QoS从resources字段的组合就能一眼认出资源模型通过requests和limits的组合把Pod自动归入三档QoS级别判定条件生存预期Guaranteed每个容器都设置了requests和limits且各自相等最高Burstable至少一个容器设置了requests或limits但不满足Guaranteed中间BestEffort所有容器都没有设置requests和limits最低这表格不是让你背的而是设计自检用例时直接可以照着写。比如一个容器requests是100m/128Milimits是200m/256Mi那就是Burstable。如果你希望关键服务进入Guaranteed就要让requests和limits相等。这里有个很容易踩的坑多容器Pod的QoS是取所有容器里的最低档不是按主容器或第一个容器看。比如Pod里主容器设置了完整requests/limits但有个sidecar没设置整个Pod就会降级到Burstable甚至BestEffort。我见过不少项目因为这个原因明明按Guaranteed设计得好好的结果一个日志agent容器没配资源整个Pod在节点压力大的时候率先成了牺牲品。排查时不要只看主容器要两个kubectl describe全部看一遍。3.2 节点内存告急Kubelet和内核谁先动手当节点内存紧张Kubelet会先尝试驱逐Eviction驱逐的优先级正是按QoS先BestEffort再Burstable最后Guaranteed。同时它还会综合看Pod超量使用的情况优先拉起那些实际用得多、声明又低的Pod。如果驱逐仍然控制不住内存Linux内核的OOM Killer会介入。Kubelet会给容器设置OOM Score Adj让Guaranteed容器获得极低的被杀概率BestEffort容器获得最高被杀概率。所以实际操作中你会发现每次都先死的往往是那些没写资源的Pod而不是Guaranteed。但是——这不是绝对保护。这里要特别说明一下Kubelet驱逐和内核OOM是两套机制。Kubelet驱逐还有阈值比如可配置memory.available低于多少开始驱逐而内核OOM是瞬时行为。在节点内存被打穿的一瞬间内核可能没等到Kubelet优雅驱逐就自己动手了。你在系统日志里看到Out of memory: Kill process那就是内核OOM而不是Kubelet的eviction。3.3 为什么我Guaranteed的Pod还是被杀了经常有同事抓到我问我Pod是Guaranteed啊为什么节点OOM后还是被杀了原因一就是上面说的整台机器瞬间内存被某个进程打穿内核会立刻释放内存Kubelet可能还没机会优雅驱逐在极端状态下OOM Killer挑选目标时会看重实际内存占用即使Guaranteed容器声明了很小的内存但如果它实际吃掉了大量内存被选中的概率也会很高。原因二是很多朋友把Guaranteed理解成了内存绝对安全。它不是。Guaranteed只保证调度器给你预留了requests大小的资源不保证你的limit不会被打穿更不保证系统级OOM百分百绕过。想真正降低风险还得靠合理评估内存上限、监控告警前置、节点水位控制、以及根据业务做Pod反亲和。对于Redis这类工作负载更要小心。Redis本身内存操作极快limit设得保守会导致内存被打到上限然后OOM完重启缓存穿透式的雪崩紧接着就来了。真正生产里的K8S Redis集群除了资源模型还要配合调度到独立节点池并设置好maxmemory、lazyfree这些Redis内部参数否则K8S这一层救不了你。4. 从CPU内存到GPU异构资源如何进入资源模型聊到这里资源模型还只是CPU和内存。可今天的生产环境早就不是这两个维度了GPU、FPGA、智能加速卡、RDMA网卡全是可调度资源。K8S把这类资源统一抽象为Device Plugin Extended Resource沿用requests/limits的大框架。4.1 设备插件自定义资源是怎么进入调度器的K8S本身并不认识GPU是NVIDIA等厂商的Device Plugin通过gRPC上报给Kubelet我这台机器有8张卡可以按nvidia.com/gpu这个资源单位对外出租。Kubelet收到消息后把节点可用资源更新到Node对象。之后你在Pod里写resources: requests: nvidia.com/gpu: 1 limits: nvidia.com/gpu: 1调度器就能像调度CPU一样在预选阶段按数量判断节点够不够。有人问为什么GPU的requests和limits要写成一样因为GPU这种Extended Resource的语义是不可压缩、独占、不共享K8S要求Extended Resource的requests必须等于limits。同理K8S调度器还可以调度内存带宽、加密卡等扩展资源。抓到这个模式后你会发现K8S的资源模型是插件式的把什么资源挂进NodeStatus调度器就能调度什么资源它就是干资源记账和分配这点事的。4.2 异构资源调度真正的坑在分数与配额之外Extended Resource看起来简单真正跑起来却有三个坑。第一GPU不能像内存一样超卖调度器按数量分配后容器里的进程还要通过环境变量拿到设备路径。环境变量如果生产流程没打通卡启动后就找不到驱动。第二调度的数量不等于性能。两张GPU的型号可能完全不同但K8S默认只认数量。调度到两张不同代际的卡上训练性能会非常差。这种情况下需要给节点打标签再用节点亲和性和资源模型做分层调度。第三资源和拓扑关系没建模。设备有NUMA亲和性、PCIe位置K8S内核不感知就需要拓扑管理器、设备插件自研之类的补丁。热词里出现的CPU智能核心调度和时序调度本质上是这些问题在不同领域的体现——资源怎么分配只是第一层资源分配后的物理位置和数据处理延迟才是更难的那层。你要调度一张GPU到某个容器优先级固然重要但那张GPU落在哪个NUMA节点、和哪个CPU核心通信延迟低对训练和推理任务影响巨大光靠资源模型是回答不了的。4.3 再进一步NUMA感知和CPU Manager对于追求极致性能的场景资源模型只是起点。以HPC和高性能数据库为例Pod申请了4个CPUK8S默认不会关心这4个CPU是不是同一个NUMA节点上的核心。跨NUMA访问内存会造成明显延迟。这时打开CPU Manager的static策略配合Topology Manager让Guaranteed且CPU请求为整数的容器绑定到指定CPU集合并在同一个NUMA节点上分配内存和设备。这个机制依赖几个条件CPU Manager要设staticPod的requests里的cpu必须是整数Pod的QoS要是Guaranteed。满足这些容器进程会被绑到一组固定的CPU上。以前做大数据计算时同样一个计算Pod开启CPU绑核后任务耗时可以下降两位数百分比。代价是这些CPU核心被留着不用换其他人也用不了集群有效资源会有明显折损——性能和资源利用效率从来都是二选一。CPU Manager和Topology Manager之间的关系我用一句话总结CPU Manager负责把CPU切好Topology Manager负责确保你切出来的CPU和其他资源在同一个物理分布范围内两者必须配合才能形成真正的拓扑感知调度。5. 资源模型在真实业务里最容易翻车的三个小动作前面讲的都是机制最后这部分是我自己实际踩过的坑也算给运维和平台工程同学的一点现场还原。这三个细节每次都能让新人在故障排查时至少白折腾半天。5.1 写了个1别忘了CPU时间片不等于一个物理核第一个坑是对CPU的1有误解。容器申请cpu1确实会获得与一个核等量的CPU时间配额在静态CPU Manager绑核时甚至会被绑到一个核上。但在默认调度里这个1和物理核不是严格绑定CFS会按比例分时间片业务进程仍然可能在不同核之间迁移。更隐蔽的坑是limit1并不表示它能抢到整个核。极限场景中如果同节点的其他容器也在大量使用CPUCFS会按权重去分Guaranteed容器只能保证自己的配额不被抢走不保证绝对无竞争。所以在做压测评估时不要因为容器limit是1就以为单容器就能跑满物理机的单核性能。对于Java服务我给的通用建议是CPU request可以用0.5到2之间的某个值和实际压测基线对齐limit不要只设1很多JVM内部线程池是以可用CPU数做参数推导的设置得太小容易直接把并行度卡死。宁可把limit设得宽松也不要在容器里出现JVM以为有32核、实际只能跑2核这种荒诞配置。5.2 内存的limit不是只算堆内内存第二个坑是给Java、Redis这类服务定内存时只算了业务主内存忘了堆外、线程栈、元空间、压缩类空间、NIO buffer和文件缓存。这些都会被算进cgroup内存计数达到容器内存limit后OOM比你想象中来得更快。我记得有一次排查一个Java服务频繁OOM。业务方说-Xmx只用了2g节点内存明明20glimit设了4g怎么会超结果一查堆外线程和Compressed Class Space让整个Committed memory超过4g再叠加page cache容器内存直接撞顶。后来我们把-Xmx和容器limit之间留出了1.5到2倍的余量尤其是堆外占比高的服务。Redis虽然没有JVM的麻烦但持久化时RDB写盘的内存开销、Cluster Bus自己的内存也一样会进cgroup账本。不要拿Estimated Used去填limit要拿全量内存画像来填。以Redis为例如果你估算业务数据量是3g我建议requests填3.5glimits填4.5g留出写盘重写和碎片整理余量。配合K8S的QoS把Redis设成Guaranteed并在节点水位超过85%时提前驱逐低优Pod会顺很多。这个习惯不是空想是很多Redis容器OOM事故总结出来的。5.3 requests定高了HPA和调度都会跟着误判第三个坑是把requests当摆设随便填。很多团队要么全都写0.1C/128Mi要么全都按照上限写满。前者会导致HPA无法正确扩容因为HPA计算Pod CPU使用率时分母是requests分子是实际使用如果requests设置过低哪怕容器已经跑得满头大汗使用率依然在达标线以下HPA永远不触发扩容。后者则会导致集群账本被大量虚占几十个Pod声明了20C实际只用了2C后续Pod只能到处Pending。我个人的填写习惯是这样的CPU requests按正常峰值的1.2倍设limits按正常峰值的2倍设内存requests按正常使用稳定值上浮30%-50%设limits按上浮100%设。当然每类业务不一样但至少这个原则能让HPA的分母比较合理也能让调度器账本不至于太离谱。定好request后用三个指标来监督HPA是否能在压测中触发、节点已分配占比是否与实际利用率偏差在可接受范围、业务是否有未被捕获的内存尖峰。如果requests长期低于实际使用说明工作负载在裸奔一旦节点被其他Pod挤满这类Pod会优先进入驱逐候选名单。线上服务被莫名牺牲时真的别怪调度器要怪就怪自己在资源模型里埋下的伏笔。生产上每次看到资源相关的故障我都会习惯性把那个Pod的yaml重新看一遍把requests和limits在脑海里彻底换算一遍。这个习惯帮我挡掉过很多不必要的告警和半夜电话。K8S看起来是个大系统但真正决定调度命运的地方常常就是资源模型这十几行配置。配置写得好调度器就是你的稳定器配置写得随意调度器就会成为故障放大器。希望这篇分享能让你在部署下一个业务时多花三分钟把这两个数字想清楚。
返回列表