第 5 章

资源限制

·约 16 分钟

不设限制的容器,能把整台节点的 CPU 和内存吃光,节点上的其他 Pod 跟着遭殃。这一章给容器划定用量:先给单个 Deployment 设上限,再给整个 namespace 设默认值。

实验用一个两节点集群:控制平面 cp、worker 一台。压测程序是 vish/stress 镜像,按参数占用指定的 CPU 和内存。


requests 和 limits

每个容器可以设两组数:

requestslimits
谁看它调度器节点内核(cgroup)
意思我至少要这么多,节点要预留出来我最多只能用这么多
不满足会怎样没有节点预留得出来,Pod 停在 PendingCPU 超了限速,进程不死;内存超了 OOMKilled

CPU 和内存的处理不一样:CPU 可以少分几个时间片,进程只是变慢;内存分出去就收不回来,只能杀进程。

单位:

写法意思
cpu: "1"1 个核
cpu: "0.5" = 500m半个核,m 是千分之一核
memory: 500Mi500 × 1024 × 1024 字节
memory: 500M500 × 1000 × 1000 字节

单位区分大小写,500mi 不是合法写法。


给 Deployment 设上限

先看不设限的样子

kubectl create deployment hog --image vish/stress
kubectl get deployment hog -o yaml
    spec:
      containers:
      - image: vish/stress
        imagePullPolicy: Always
        name: stress
        resources: {}

resources: {}:什么都没设。这样的 Pod 能用光节点的资源,调度器也不替它预留任何东西。

导出 YAML 的两种方法

要改 resources,先把 YAML 拿到手。

kubectl get deploy hog -o yamlkubectl create deploy hog --image vish/stress --dry-run=client -o yaml
内容线上对象的全部只有你给的参数
要清理要删 status、uid、resourceVersion、creationTimestamp不用
适合修改一个已经存在、不清楚当初怎么建的资源新建资源,生成模板

diff 两份文件,get 多出来的分三类:

类别字段谁加的
身份和版本uid、resourceVersion、creationTimestamp、generation、namespaceapiserver 创建对象时生成
默认值strategy.rollingUpdate: 25%、revisionHistoryLimit: 10、imagePullPolicy: Always、restartPolicy: Always、dnsPolicy: ClusterFirst、terminationGracePeriodSeconds: 30……没写的字段,apiserver 按默认值补全
状态status: 一整段controller 运行时写入

dry-run 的文件是你的意图,get 出来的是集群眼里的完整对象。

几个默认值值得记住:

  • imagePullPolicy: Always:镜像没写 tag 就是 :latest,每次启动都去拉;写了具体 tag,默认变成 IfNotPresent
  • maxSurge: 25% / maxUnavailable: 25%:滚动更新时最多多出 25%、最多少掉 25%
  • generation / observedGeneration:改了几次 spec / controller 处理到第几次,相等说明 controller 跟上了

踩坑:kubectl get deployment -o yaml 不带名字,导出来的是 kind: List,Deployment 被包在 items: 里多缩进一层。按名字导出才是单个对象。

加上 requests 和 limits

        resources:
          limits:
            memory: "4Gi"
          requests:
            memory: "2500Mi"
kubectl replace -f hog.yaml

改了 Pod 模板,Deployment 滚动更新,建了一个新 Pod:

$ kubectl get po
NAME                   READY   STATUS    RESTARTS   AGE
hog-6c95d866cb-j8w5g   1/1     Running   0          10m
hog-8696ccdcc4-x555t   0/1     Pending   0          21s

新 Pod 一直 Pending。在 worker 上看一眼内存:

MiB Mem :   1899.5 total,    376.3 free,    660.0 used,   1019.1 buff/cache

整台 worker 只有 1.9G,requests 要 2500Mi,没有节点预留得出来。cp 有控制平面污点,普通 Pod 也上不去。kubectl describe pod 最底下的 Events 会写明原因。

调度器比的是 requests 的总和,不是实际用了多少。节点再闲,预留额度不够也调度不上去。

kubectl describe node worker | grep -A 8 "Allocated resources"

旧 Pod 没被删,是滚动更新在起作用:1 个副本的 maxUnavailable: 25% 向下取整是 0,一个都不能少;maxSurge: 25% 向上取整是 1,可以多建一个。新 Pod 起不来,旧 Pod 就一直留着,服务不中断。

让 stress 动起来

把 requests 降下来,再加上 CPU 的限制和 stress 的参数:

      containers:
      - image: vish/stress
        name: stress
        resources:
          limits:
            cpu: "1"
            memory: "4Gi"
          requests:
            cpu: "0.5"
            memory: "500Mi"
        args:
        - -cpus
        - "2"
        - -mem-total
        - "950Mi"
        - -mem-alloc-size
        - "100Mi"
        - -mem-alloc-sleep
        - "1s"
  • args 和 resources 都是容器的字段,缩进对齐
  • 数字加引号:args 是字符串列表
  • stress 要 2 个核,limit 只给 1 个,故意制造冲突

踩坑:第一次只改了 resources,忘了加 args。Pod 是 Running,top 里却找不到 stress:

$ kubectl logs hog-66d5b8bcff-8vd26
I1009 22:41:57.016561       1 main.go:26] Allocating "0" memory, in "4Ki" chunks, with a 1ms sleep between allocations
I1009 22:41:57.016621       1 main.go:29] Allocated "0" memory

Running 只说明容器进程活着,不说明它在干活。 看日志才知道它在做什么。参数写错了也是这样:150mi 这种非法单位会让 stress 直接 panic,Pod 变成 Error。

在 top 里看限制生效

worker 上:

MiB Mem :   1899.5 total,     81.7 free,   1574.0 used,    397.5 buff/cache
MiB Swap:      0.0 total,      0.0 free,      0.0 used.    325.4 avail Mem

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
   3450 root      20   0  958796 955248   3180 S  99.7  49.1   0:39.52 stress
看哪值说明
%CPU99.7stress 开了 2 个线程,被 limits.cpu: "1" 压在一个核,到不了 200%
RES955248 KB ≈ 933Mi就是 -mem-total 950Mi,没超 4Gi,不会被杀
%MEM49.1一个容器占了半台机器
avail Mem325.4内核把文件缓存挤掉腾给 stress,buff/cache 从 1019 降到 397

这台 worker 已经很紧张了。没有 limit 的话,一个失控的程序能把整台节点吃光,cilium、coredns 这些跟着出问题。

用 K8s 的视角看,在 cp 上就能查,不用登录节点(要装了 metrics-server):

kubectl top pod
kubectl top node

小结:给 Deployment 设上限

#步骤说明
1导出 YAML新建用 dry-run,改现有对象用 get 再清理
2加 requests调度器按它找节点,太大就 Pending
3加 limitsCPU 限速、内存超限被杀
4加 args让 stress 真的去占资源
5top / kubectl topCPU 卡在 limit,内存涨到目标值停住

给 namespace 设默认值

LimitRange

上面的限制写在 Deployment 里。也可以给整个 namespace 设:

kubectl create namespace low-usage-limit
apiVersion: v1
kind: LimitRange
metadata:
  name: low-resource-range
spec:
  limits:
  - default:
      cpu: 1
      memory: 500Mi
    defaultRequest:
      cpu: 0.5
      memory: 100Mi
    type: Container
kubectl create -f low-resource-range.yaml -n low-usage-limit
字段作用
default容器没写 limits 时,填这个
defaultRequest容器没写 requests 时,填这个
type: Container按每个容器算

文件里没写 namespace,所以用 -n 指定。不带 -n 查,什么也查不到:

$ kubectl get limitrange
No resources found in default namespace.

每条命令只看当前 namespace。 用 describe 看更直观:

kubectl -n low-usage-limit describe limitrange

默认值是什么时候填上的

在这个 namespace 里建一个不写 resources 的 Deployment:

kubectl -n low-usage-limit create deployment limited-hog --image vish/stress

Pod 里有了 resources:

    resources:
      limits:
        cpu: "1"
        memory: 500Mi
      requests:
        cpu: 500m
        memory: 100Mi

Deployment 里却还是 resources: {}。LimitRange 不改 Deployment,而是 Pod 创建那一刻由 apiserver 填进去(准入控制,admission)。所以:

  • LimitRange 建立之前就存在的 Pod,不会被补上
  • 改了 LimitRange,要等 Pod 重建才生效

管不住自己写了 resources 的容器

把 hog.yaml 复制一份,metadata 里加上 namespace: low-usage-limit,也建进去。现在有三个 Deployment:

$ kubectl get po -A -o wide
NAMESPACE         NAME                           READY   STATUS    ...   NODE
default           hog-68d9f599b4-rkt9h           1/1     Running   ...   worker
low-usage-limit   hog-68d9f599b4-ffzzm           1/1     Running   ...   cp
low-usage-limit   limited-hog-5876fb44bf-bb5hp   1/1     Running   ...   worker

两台节点的 top(这次节点是 8G 内存):

# cp
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
   3461 root      20   0  958796 955328   3180 S 100.0  11.8   3:47.97 stress

# worker
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
   2613 root      20   0  958796 955188   3180 R  99.7  11.8  14:21.39 stress

两个 hog,一个在受限的 namespace,一个不在,用量一模一样。

Deployment自己写了 resources?实际 limit受谁约束
default/hog✅1 核 / 4Gi自己
low-usage-limit/hog✅1 核 / 4Gi自己,LimitRange 没管它
limited-hog❌1 核 / 500MiLimitRange

关键证据是内存:low-usage-limit 里的 hog 用到了 933Mi 还活着。如果它拿的是 LimitRange 的 500Mi,早就 OOMKilled 了。default 只补空,容器自己写了就不管。

limited-hog 确实被限住了,但它没有 args,stress 空转,看不出效果。给它加上 args、不加 resources,涨到 500Mi 就会被杀。

hog 被调度到了 cp:这个集群的 cp 没有污点,worker 上已经有一个 stress 在跑,调度器挑了更空的那台。

真正管得住:max 和 ResourceQuota

手段管什么超了怎样
LimitRange default / defaultRequest没写 resources 的容器不存在超不超,只是补上默认值
LimitRange max / min每个容器的上下限Pod 被拒绝创建
ResourceQuota整个 namespace 的总量新 Pod 被拒绝创建

LimitRange 加上 max.memory: 1Gi 后,hog 申请 4Gi 就建不出来。这时 Deployment 显示 created,READY 却一直是 0/1,get pods 一个都没有:Pod 在准入阶段就被拒了。原因在 ReplicaSet 的 Events 里:

kubectl -n low-usage-limit describe rs

Deployment 0/1、一个 Pod 都看不到,就去查 ReplicaSet 的 Events。

小结:给 namespace 设默认值

#步骤说明
1建 namespace 和 LimitRange用 -n 指定 namespace
2建不写 resources 的 DeploymentPod 创建时被填上默认值,Deployment 不变
3建自己写了 resources 的 Deployment默认值不生效,照样用到 4Gi 的上限
4要真正限制用 max 或 ResourceQuota

改资源的几种方法

create、replace、apply

create -freplace -fapply -f
对象不存在创建❌ NotFound创建
对象已存在❌ AlreadyExists用文件整个替换只改有差异的字段
风格命令式:去创建命令式:换成这个声明式:最终应该是这样

apply 会把这次的文件内容存进注解 kubectl.kubernetes.io/last-applied-configuration,下次 apply 对比三份:

来源用来
这次的文件想要的状态
上次 apply 的内容(注解)判断哪些字段是你删掉的
线上对象当前状态

好处是:文件里删掉的字段,线上也会删掉;别人用 kubectl scale 改过的副本数,你的文件里没写就不动它。replace 是整个覆盖,做不到这两点。

用 create 建的对象第一次 apply,会提示缺少这个注解,并自动补上。create 时加 --save-config 可以提前存好。

uid 和 resourceVersion 冲突

用 get -o yaml 导出的文件没清理,就会碰到这两个报错:

$ kubectl replace -f hog.yaml
Error from server (Conflict): ... Precondition failed: UID in precondition: 89f1a30c-d9b3-4b93-ba9f-db9ca8c3ee0a, UID in object meta: 96ffd206-5390-42d5-b76c-acd4b771ec4b

文件里的 uid 是第一次建的 hog 的。之后删了重建过,新对象换了 uid。uid 是对象的身份证,名字一样、uid 不同,就是另一个对象,replace 拒绝覆盖。

$ kubectl apply -f hog.yaml
Warning: resource deployments/hog is missing the kubectl.kubernetes.io/last-applied-configuration annotation ...
Error from server (Conflict): ... the object has been modified; please apply your changes to the latest version and try again

文件里还有 resourceVersion: "1568"。这是乐观锁:提交修改时带上版本号,apiserver 发现线上早已不是这个版本,就拒绝,防止覆盖别人的修改。

两个报错都指向一件事:从 get 导出的文件,要删掉 uid、resourceVersion、creationTimestamp、status 再用,或者干脆用 dry-run 重新生成。

grep -nE 'uid|resourceVersion|creationTimestamp|generation|^status' hog.yaml

改之前先看会改什么:

kubectl diff -f hog.yaml

它让 apiserver 先模拟补全默认值再比较,只显示你真正改了的地方。

只改资源,不写文件

kubectl set resources deployment hog --limits=cpu=1,memory=4Gi --requests=cpu=500m,memory=500Mi

直接改线上对象,触发滚动更新。加 args 这种改动 set resources 做不了,要改 YAML 或者 kubectl edit。

删了重建(kubectl replace --force -f)最省事,但期间服务中断,只在实验里用。

字段写法记不住

kubectl explain pod.spec.containers.resources
kubectl set resources --help

文档:


命令速查表

命令用途
kubectl create deploy <名> --image <镜像> --dry-run=client -o yaml > x.yaml生成干净的 YAML 模板
kubectl get deploy <名> -o yaml导出线上对象(用前要清理)
kubectl diff -f x.yaml看文件和线上的真实差异
kubectl create -f / replace -f / apply -f新建 / 整个替换 / 声明式更新
kubectl replace --force -f x.yaml删了重建
kubectl set resources deploy <名> --limits=... --requests=...直接改资源设置
kubectl logs deploy/<名>看 Deployment 下某个 Pod 的日志,不用复制 Pod 名
kubectl top pod / kubectl top node实际用量(要 metrics-server)
kubectl describe node <节点> | grep -A 8 "Allocated resources"节点上 requests 已经预留了多少
kubectl -n <ns> describe limitrange看 namespace 的默认值和上下限
kubectl -n <ns> describe rsDeployment 起不来又没有 Pod 时,看 Events
kubectl explain pod.spec.containers.resources查字段写法