第 5 章
资源限制
不设限制的容器,能把整台节点的 CPU 和内存吃光,节点上的其他 Pod 跟着遭殃。这一章给容器划定用量:先给单个 Deployment 设上限,再给整个 namespace 设默认值。
实验用一个两节点集群:控制平面 cp、worker 一台。压测程序是 vish/stress 镜像,按参数占用指定的 CPU 和内存。
requests 和 limits
每个容器可以设两组数:
| requests | limits | |
|---|---|---|
| 谁看它 | 调度器 | 节点内核(cgroup) |
| 意思 | 我至少要这么多,节点要预留出来 | 我最多只能用这么多 |
| 不满足会怎样 | 没有节点预留得出来,Pod 停在 Pending | CPU 超了限速,进程不死;内存超了 OOMKilled |
CPU 和内存的处理不一样:CPU 可以少分几个时间片,进程只是变慢;内存分出去就收不回来,只能杀进程。
单位:
| 写法 | 意思 |
|---|---|
cpu: "1" | 1 个核 |
cpu: "0.5" = 500m | 半个核,m 是千分之一核 |
memory: 500Mi | 500 × 1024 × 1024 字节 |
memory: 500M | 500 × 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 yaml | kubectl create deploy hog --image vish/stress --dry-run=client -o yaml | |
|---|---|---|
| 内容 | 线上对象的全部 | 只有你给的参数 |
| 要清理 | 要删 status、uid、resourceVersion、creationTimestamp | 不用 |
| 适合 | 修改一个已经存在、不清楚当初怎么建的资源 | 新建资源,生成模板 |
diff 两份文件,get 多出来的分三类:
| 类别 | 字段 | 谁加的 |
|---|---|---|
| 身份和版本 | uid、resourceVersion、creationTimestamp、generation、namespace | apiserver 创建对象时生成 |
| 默认值 | strategy.rollingUpdate: 25%、revisionHistoryLimit: 10、imagePullPolicy: Always、restartPolicy: Always、dnsPolicy: ClusterFirst、terminationGracePeriodSeconds: 30…… | 没写的字段,apiserver 按默认值补全 |
| 状态 | status: 一整段 | controller 运行时写入 |
dry-run 的文件是你的意图,get 出来的是集群眼里的完整对象。
几个默认值值得记住:
imagePullPolicy: Always:镜像没写 tag 就是:latest,每次启动都去拉;写了具体 tag,默认变成IfNotPresentmaxSurge: 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" memoryRunning 只说明容器进程活着,不说明它在干活。 看日志才知道它在做什么。参数写错了也是这样:
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
| 看哪 | 值 | 说明 |
|---|---|---|
| %CPU | 99.7 | stress 开了 2 个线程,被 limits.cpu: "1" 压在一个核,到不了 200% |
| RES | 955248 KB ≈ 933Mi | 就是 -mem-total 950Mi,没超 4Gi,不会被杀 |
| %MEM | 49.1 | 一个容器占了半台机器 |
| avail Mem | 325.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 | 加 limits | CPU 限速、内存超限被杀 |
| 4 | 加 args | 让 stress 真的去占资源 |
| 5 | top / kubectl top | CPU 卡在 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 核 / 500Mi | LimitRange |
关键证据是内存: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 的 Deployment | Pod 创建时被填上默认值,Deployment 不变 |
| 3 | 建自己写了 resources 的 Deployment | 默认值不生效,照样用到 4Gi 的上限 |
| 4 | 要真正限制 | 用 max 或 ResourceQuota |
改资源的几种方法
create、replace、apply
create -f | replace -f | apply -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
文档:
- Resource Management for Pods and Containers:原理和单位
- Assign Memory Resources to Containers and Pods:带 OOMKilled 的演示
- Assign CPU Resources to Containers and Pods
- Limit Ranges
命令速查表
| 命令 | 用途 |
|---|---|
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 rs | Deployment 起不来又没有 Pod 时,看 Events |
kubectl explain pod.spec.containers.resources | 查字段写法 |