【K8s 调度实战全景】从 Pod 诞生到 HPA 弹性伸缩:一文吃透 Kubernetes 调度与资源管控
摘要 :你有没有好奇过:一条 kubectl apply 敲下去,集群内部到底发生了什么?Pod 是怎么被"挑中"某个节点的?为什么有时候 Pod 一直 Pending 就是起不来?又怎么让服务在流量洪峰自动扩容、多团队共享集群时不互相"抢食"?这篇文章用一套环境 + 20 多个可复现实验,把 Kubernetes 调度体系从「Pod 诞生过程」到「调度算法」再到「资源配额管控」讲个通透。看完动手练一遍,调度这块基本就稳了。
一、环境准备
全文实验都在这个命名空间下操作,方便做完后一次性清理:
bash
root@master30:~# kubectl create ns scheduler
root@master30:~# kubectl config set-context --current --namespace scheduler
二、调度是什么?先认识 kube-scheduler
调度 ,就是把 Pod放置到合适的节点上的过程。kube-scheduler是集群的默认调度器。它通过 Watch 监听机制发现集群中未被调度到节点上的 Pod,然后把这些"无家可归"的 Pod 安排到一个合适的节点上运行。
一句话记住它的边界 :kube-scheduler 只负责选节点,不负责启动容器。启动容器是目标节点 kubelet + 容器运行时的活。
三、Pod 调度全流程拆解
3.1 直接创建 Pod 的完整链路
bash
# 客户端发起请求 → apiserver 认证授权准入 → 写入 etcd → scheduler 监听并绑定 → kubelet 启动容器
七个核心组件各司其职:
| 组件 | 职责 |
|---|---|
| 客户端 (kubectl) | 组装请求,与 API Server 交互 |
| kube-apiserver | 集群唯一入口,认证/授权/准入校验,读写 etcd |
| etcd | 键值数据库,持久化集群所有资源状态 |
| kube-scheduler | 监听 Pending Pod,过滤+打分选最优节点,发起绑定 |
| kubelet | 节点代理,监听绑定到本机的 Pod,调用 CNI/CSI/CRI 启动容器 |
| 容器运行时 | containerd/docker,拉镜像、创建容器、运行业务进程 |
精简版:7 步记住全流程
- 客户端发送创建 Pod 请求至 kube-apiserver;
- apiserver 完成认证、授权、准入校验,合法则把 Pod 存入 etcd,Pod 为 Pending;
- kube-scheduler 监听到未调度 Pod,执行过滤、打分选出最优节点,向 apiserver 发起绑定;
- apiserver 更新 Pod,写入节点名称;
- 对应节点 kubelet 感知 Pod 绑定到本机,调用 CNI 准备网络、CSI 准备存储;
- kubelet 调用容器运行时拉取镜像、启动容器;
- kubelet 检测容器正常运行,通知 apiserver 将 Pod 状态更新为 Running。
3.2 通过控制器(Deployment)创建 Pod 的链路
bash
Client → kube-apiserver → etcd → kube-controller-manager → kube-scheduler → kubelet → 容器运行时
关键区别:Deployment 控制器只负责"创建/删除 Pod",不参与调度、不启动容器。它通过对比期望副本数与实际 Pod 数量,缺失时调用 apiserver 创建新的 Pending Pod。
精简版流程
- 用户通过kubectl提交Deployment创建请求至apiserver;
- apiserver完成认证授权准入校验后将Deployment存入etcd;
- kube-controller-manager内的Deployment控制器监听到Deployment,对比期望副本数与现有Pod数量,缺失则调用apiserver创建Pending状态Pod存入etcd;
- kube-scheduler监听到未调度Pod,筛选选出最优节点并发起绑定;
- apiserver更新Pod绑定节点名称;
- 对应节点kubelet感知Pod调度到本机,准备存储与网络,调用容器运行时拉取镜像启动容器;
- kubelet检测容器正常运行后更新Pod状态为Running。
3.3 调度器内部两步走
第一步:过滤(Filter / Predicates)
把所有满足 Pod 调度需求的节点选出来,得出一个"可调度节点列表"。通常这个列表不止一个节点;如果列表为空,这个 Pod 就不可调度(会一直 Pending)。
| Predicate | 作用 |
|---|---|
PodFitsHostPorts |
节点是否有空闲端口供 Pod 请求使用 |
PodFitsHost |
Pod 是否通过主机名指定了特定节点 |
PodFitsResources |
节点的空闲资源(CPU/内存)能否满足 Pod 的 request |
MatchNodeSelector |
节点的标签是否匹配 Pod 的 nodeSelector |
NoVolumeZoneConflict |
考虑存储的故障区域限制,Pod 请求的卷在该节点是否可用 |
NoDiskConflict |
Pod 请求的卷与节点上已挂载的卷是否冲突 |
MaxCSIVolumeCount |
CSI 卷挂载数量是否超过配置的限制 |
PodToleratesNodeTaints |
Pod 的 toleration 能否容忍节点的 taint |
CheckVolumeBinding |
Pod 请求的 PVC(已绑定/未绑定)能否适配该节点 |
第二步:打分(Score / Priorities)
根据当前启用的打分规则给每个可调度节点算分,得分最高者胜出 。如果有多个并列第一,kube-scheduler 从中随机选取一个。
| Priority | 作用 |
|---|---|
SelectorSpreadPriority |
把属于同一 Service / StatefulSet / ReplicaSet 的 Pod 打散到不同主机 |
InterPodAffinityPriority |
实现 Pod 间亲和性与反亲和性的软策略 |
LeastRequestedPriority |
倾向资源请求较少的节点(节点上 Pod 越多、用得越多,得分越低) |
MostRequestedPriority |
倾向资源请求最多的节点(让工作负载集中在尽可能少的节点上) |
RequestedToCapacityRatioPriority |
基于 requests/capacity 比率的资源分配评分 |
BalancedResourceAllocation |
倾向 CPU 与内存使用比例均衡的节点 |
NodePreferAvoidPodsPriority |
根据节点注解 scheduler.alpha.kubernetes.io/preferAvoidPods 调整优先级 |
NodeAffinityPriority |
根据 nodeAffinity 中 preferred 规则的首选项打分 |
TaintTolerationPriority |
按节点上不可容忍污点的数量调整节点排名 |
ImageLocalityPriority |
优先选择已缓存了该容器镜像的节点(加快启动) |
ServiceSpreadingPriority |
确保同一 Service 的 Pod 分布在不同节点,提升容灾能力 |
EqualPriority |
给所有节点相同权重 |
EvenPodsSpreadPriority |
实现 Pod 拓扑分布约束的首选策略 |
实用技巧 :Pod 一直 Pending 时,先
kubectl describe pod <pod名>看 Events。过滤失败的事件信息非常直白(如0/3 nodes are available: 1 node(s) had untolerated taint...),对照上面的 Predicate 表就能快速定位根因。
四、手动干预:控制 Pod 的运行位置
通常不需要干预------调度器自己会做合理放置(把 Pod 分散、避开资源不足的节点)。但有些场景必须干预:大量磁盘 I/O 的 Pod 想跑在 SSD 节点上;需要 GPU 的 Pod 必须落在 GPU 节点上。
可用的四种方式:
- 与节点名匹配的
nodeName - 与节点标签匹配的
nodeSelector - 亲和性与反亲和性(nodeAffinity / podAffinity / podAntiAffinity)
- Pod 拓扑分布约束(topologySpreadConstraints)
4.1 nodeName:最简单、也最不推荐
nodeName 是 PodSpec 的一个字段,值是节点名称。它是优先级最高的节点选择方式,通常不使用。
使用限制:
- 指定的节点不存在 → Pod 不会运行,甚至会被自动删除
- 指定节点资源不够 → 调度失败,原因明确(如
OutOfmemory、OutOfcpu) - 云环境中节点名称并非总是可预测或稳定的
bash
root@master30:~# kubectl create deployment webapp --image nginx --replicas 2 -o yaml --dry-run=client > deploy-with-nodeName.yaml
root@master30:~# vim deploy-with-nodeName.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: webapp
name: webapp
spec:
replicas: 2
selector:
matchLabels:
app: webapp
strategy: {}
template:
metadata:
labels:
app: webapp
spec:
# 添加nodeName配置
nodeName: worker32.laoma.cloud
containers:
- image: hub.laoma.cloud/library/nginx
name: nginx
root@master30:~# kubectl apply -f deploy-with-nodeName.yaml
root@master30 ~# kubectl get pods -o wide | awk '{print $1,$3,$7}'
NAME STATUS NODE
webapp-55c6b896bc-ns9m5 Running worker32.laoma.cloud
webapp-55c6b896bc-qr892 Running worker32.laoma.cloud
# 清理资源
root@master30:~# kubectl delete deployments.apps webapp
两个副本被死死钉在同一个节点上------零容错、零弹性。
4.2 nodeSelector:推荐的简单做法
nodeSelector 包含键值对的映射。节点的标签中必须包含这里的每个键值对(也可以有额外标签),Pod 才能调度上去。
提示:nodeSelector 是节点选择约束的推荐形式。
bash
# 查看node标签
root@master30:~# kubectl get node --show-labels
NAME STATUS ROLES AGE VERSION LABELS
master30.laoma.cloud Ready control-plane 7d8h v1.30.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=master30.laoma.cloud,kubernetes.io/os=linux,node-role.kubernetes.io/control-plane=,node.kubernetes.io/exclude-from-external-load-balancers=
worker31.laoma.cloud Ready <none> 7d8h v1.30.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=worker31.laoma.cloud,kubernetes.io/os=linux
worker32.laoma.cloud Ready <none> 7d8h v1.30.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=worker32.laoma.cloud,kubernetes.io/os=linux
# 标注worker31 disktype是ssd
root@master30:~# kubectl label node worker31.laoma.cloud disktype=ssd
root@master30:~# kubectl get node -L disktype
NAME STATUS ROLES AGE VERSION DISKTYPE
master30.laoma.cloud Ready control-plane 11d v1.30.2
worker31.laoma.cloud Ready <none> 11d v1.30.2 ssd
worker32.laoma.cloud Ready <none> 11d v1.30.2
在 Pod 模板的 spec 里指定跑在 SSD 节点上:
bash
root@master30:~# vim deploy-with-nodeSelector.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: webapp
name: webapp
spec:
replicas: 2
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
# 添加 nodeSelector
nodeSelector:
disktype: ssd
containers:
- image: hub.laoma.cloud/library/nginx
name: nginx
root@master30:~# kubectl apply -f deploy-with-nodeSelector.yaml
root@master30:~# kubectl get pods -o wide | awk '{print $1,$3,$7}'
NAME NODE
webapp-7d675f9d9c-rz9hx Running worker31.laoma.cloud
webapp-7d675f9d9c-zrj97 Running worker31.laoma.cloud
⚠️ 一个极易踩坑的细节
删除节点标签,不会触发重新部署------已经在跑的 Pod 会继续在原地跑:
bash
# 删除worker31标签
root@master30:~# kubectl label node worker31.laoma.cloud disktype-
root@master30:~# kubectl get node -L disktype
NAME STATUS ROLES AGE VERSION DISKTYPE
master30.laoma.cloud Ready control-plane 11d v1.30.2
worker31.laoma.cloud Ready <none> 11d v1.30.2
worker32.laoma.cloud Ready <none> 11d v1.30.2
# 此时不会触发重新部署deployment
而删除 Deployment 的 nodeSelector 会自动触发重新部署:
bash
# 删除deployment的nodeSelector
root@master30:~# kubectl patch deployment webapp --type json \
-p '[{"op":"remove","path":"/spec/template/spec/nodeSelector"}]'
# 自动触发重新部署
root@master30:~# kubectl get pods -o wide | awk '{print $1,$3,$7}'
NAME NODE
webapp-7fb6dc88f9-hj6m6 Running worker31.laoma.cloud
webapp-7fb6dc88f9-qcvtr Running worker32.laoma.cloud
# 清理资源
root@master30:~# kubectl delete deployments.apps webapp
核心结论 :选择/亲和性规则只在调度那一刻生效。节点标签后续怎么变,都不会驱逐已运行的 Pod。
4.3 nodeAffinity:nodeSelector 的超集升级版
支持两种强度:
requiredDuringSchedulingIgnoredDuringExecution:必须满足(硬约束,类似 nodeSelector)preferredDuringSchedulingIgnoredDuringExecution:优先满足(软偏好,不强制)
注意:如果节点标签在运行时发生变更,Pod 仍继续在该节点上运行。
配置位置:.spec.affinity.nodeAffinity
bash
root@master30:~# vim deploy-with-nodeAffinity.yaml
apiVersion: apps/v1 # 资源API版本,Deployment属于apps/v1
kind: Deployment # 资源类型:Deployment控制器
metadata:
labels:
app: web # Deployment自身标签
name: web # Deployment名称
spec:
replicas: 2 # 期望Pod副本数量:2个
selector:
matchLabels:
app: web # 匹配Pod标签,用来管理Pod
template: # Pod模板,定义创建出来的Pod配置
metadata:
labels:
app: web # Pod的标签,和selector对应
spec:
affinity: # 亲和性配置
nodeAffinity: # 节点亲和性
requiredDuringSchedulingIgnoredDuringExecution: # 硬亲和,调度必须满足
nodeSelectorTerms:
- matchExpressions:
- key: CPU # 节点标签key
operator: In # 运算符:包含
values:
- L1
- L2 # 节点标签CPU值必须是L1或L2,否则Pod Pending
preferredDuringSchedulingIgnoredDuringExecution: # 软亲和,优先满足,不满足也能调度
- weight: 1 # 权重1,多个偏好时用于打分排序(范围1~100)
preference:
matchExpressions:
- key: MEM # 节点标签key
operator: In # 运算符:包含
values:
- L1
- L2 # 优先选MEM=L1/L2的节点,没有也可以调度到其他符合硬亲和的节点
containers:
- image: hub.laoma.cloud/library/nginx # 容器镜像地址
name: nginx # 容器名称
规则解读 :Pod 只能放在带 CPU=L1 或 CPU=L2 标签的节点上;在满足条件的节点中,优先选带 MEM=L1/L2 标签的。
三组必备规则(面试常考)
| 场景 | 结果 |
|---|---|
同时指定 nodeSelector 和 nodeAffinity |
两者都必须满足 |
多个 nodeSelectorTerms |
任意一个满足即可(OR) |
同一个 term 里的多个 matchExpressions |
必须全部满足(AND) |
operator 支持:In、NotIn、Exists、DoesNotExist、Gt、Lt。用 NotIn / DoesNotExist 可以实现节点反亲和效果。
实验 1:节点无标签 → 全部 Pending
bash
root@master30:~# kubectl apply -f deploy-with-nodeAffinity.yaml
root@master30:~# kubectl get pods
NAME READY STATUS RESTARTS AGE
web-854cd4986-598lb 0/1 Pending 0 2s
web-854cd4986-mp266 0/1 Pending 0 2s
实验 2:给节点打 CPU 标签
bash
root@master30:~# kubectl label nodes worker31.laoma.cloud CPU=L1
root@master30:~# kubectl label nodes worker32.laoma.cloud CPU=L2
# 再次查看pod调度情况
root@master30:~# kubectl get pods -o wide --no-headers |awk '{print $1,$3,$7}'
web-854cd4986-598lb Running worker31.laoma.cloud
web-854cd4986-mp266 Running worker31.laoma.cloud
# worker31打上标签后,满足pod调度条件,此时所有pod调度到worker31节点。
# 重新部署
root@master30:~# kubectl rollout restart deployment web
root@master30:~# kubectl get pods -o wide --no-headers |awk '{print $1,$3,$7}'
web-85c8ffff8-q2j7r Running worker31.laoma.cloud
web-85c8ffff8-rkq7s Running worker32.laoma.cloud
# worker31和worker32都打上标签后,满足pod调度条件,此时pod均衡调度到worker31和worker32节点。
实验 3:给 worker32 打 MEM 标签 → 软偏好立即生效
bash
root@master30:~# kubectl label nodes worker32.laoma.cloud MEM=L2
# 重新部署
root@master30:~# kubectl rollout restart deployment web
# 只在worker32上运行
root@master30:~# kubectl get pods -o wide --no-headers |awk '{print $1,$3,$7}'
web-6c45779498-glxbg Running worker32.laoma.cloud
web-6c45779498-sn996 Running worker32.laoma.cloud
权重 weight:1~100 的打分机制
每条 preferred 规则的 weight 取值范围 1~100 。调度器遍历节点满足的所有偏好规则,把匹配的权重求和 ,再叠加到该节点的其他优先级评分上,总分最高者胜出。
bash
apiVersion: v1 # 资源版本,Pod 属于 v1
kind: Pod # 资源类型:Pod
metadata:
name: with-affinity-anti-affinity # Pod名称
spec:
affinity: # 亲和性配置
nodeAffinity: # 节点亲和性
requiredDuringSchedulingIgnoredDuringExecution: # 节点硬亲和(强制条件)
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/os # 标签key:操作系统
operator: In # 运算符:In(属于)
values:
- linux # 节点标签 kubernetes.io/os 的值必须是 linux,不满足则Pod Pending
preferredDuringSchedulingIgnoredDuringExecution: # 节点软亲和(偏好打分)
- weight: 1 # 权重1,分值低,优先级低
preference:
matchExpressions:
- key: label-1
operator: In
values:
- key-1 # 匹配 label-1=key-1,命中 +1分
- weight: 50 # 权重50,分值高,优先级更高
preference:
matchExpressions:
- key: label-2
operator: In
values:
- key-2 # 匹配 label-2=key-2,命中 +50分
containers:
- name: with-node-affinity # 容器名称
image: hub.laoma.cloud/library/nginx # 使用的nginx镜像
上例中,带 label-2: key-2 的节点(weight=50)会比带 label-1: key-1(weight=1)的节点获得高得多的加分。
bash
# 清理环境
root@master30:~# kubectl delete deployments.apps web
root@master30:~# kubectl label nodes worker31.laoma.cloud CPU-
root@master30:~# kubectl label nodes worker32.laoma.cloud CPU-
root@master30:~# kubectl label nodes worker32.laoma.cloud MEM-
4.4 Pod 间亲和性与反亲和性
前面的规则全部基于节点标签 ;Pod 间亲和/反亲和则是基于已在节点上运行的 Pod 的标签:
如果 X 拓扑域上已运行了一个或多个满足规则 Y 的 Pod:
- 亲和性:本 Pod 应该运行在 X
- 反亲和性:本 Pod 不应该运行在 X
- X :节点 / 机架 / 可用区 / 地理区域等拓扑域,由
topologyKey(节点标签的键)表达 - Y:标签选择算符表达的匹配规则
配置位置:.affinity.podAffinity、.affinity.podAntiAffinity
⚠️ 两条注意事项:
- Pod 间亲和/反亲和会消耗大量计算资源 ,显著拖慢大规模集群调度,不建议在数百节点以上的集群使用
- 使用反亲和时,集群中每个节点都必须有匹配
topologyKey的标签,否则会出现意外行为
语法模板
bash
apiVersion: v1 # Pod资源API版本
kind: Pod # 资源类型:Pod
metadata:
name: pod-with-podaffinity # Pod名称
spec:
affinity: # 亲和性配置
podAffinity: # Pod亲和性(和别的Pod放一块)
requiredDuringSchedulingIgnoredDuringExecution: # Pod硬亲和,强制规则
- labelSelector:
matchExpressions:
- key: security # 匹配Pod标签key
operator: In
values:
- S1 # 寻找标签 security=S1 的Pod
topologyKey: topology.kubernetes.io/zone # 拓扑域:可用区zone
podAntiAffinity: # Pod反亲和(尽量不和某些Pod放一起)
preferredDuringSchedulingIgnoredDuringExecution: # Pod软反亲和,偏好规则
- weight: 100 # 权重100,优先级最高
podAffinityTerm: # ✅ 软的这里要包一层 podAffinityTerm
labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S2 # 匹配标签 security=S2 的Pod
topologyKey: topology.kubernetes.io/zone # 拓扑域:可用区zone
containers:
- name: pod-with-podaffinity # 容器名称
image: hub.laoma.cloud/library/nginx # nginx镜像
- 亲和规则 :只有当某 zone 内已有
security=S1的 Pod,本 Pod 才能调度到该 zone 内的任意节点 - 反亲和规则 :若某 zone 内有
security=S2的 Pod,尽量避免把本 Pod 调度到该 zone
合法操作符:In、NotIn、Exists、DoesNotExist。topologyKey 不允许为空。
实验 1:同 zone 聚拢(亲和性)
bash
# node节点打标签
root@master30:~# kubectl label node worker31.laoma.cloud \
topology.kubernetes.io/zone=v
root@master30:~# kubectl label node worker32.laoma.cloud \
topology.kubernetes.io/zone=v
# 创建两个pod
root@master30:~# vim deploy-with-podaffinity.yaml
bash
---
apiVersion: apps/v1 # Deployment API版本
kind: Deployment # 资源类型:Deployment
metadata:
name: web1 # Deployment名称web1
spec:
selector:
matchLabels:
app: web1 # 管理标签app=web1的Pod
replicas: 1 # 副本数1
template:
metadata:
labels:
app: web1 # Pod标签 app=web1
spec:
nodeSelector: # 节点选择器(简单硬匹配)
topology.kubernetes.io/zone: v # 只能调度到zone=v这个可用区
containers:
- name: web-server # 容器名
image: hub.laoma.cloud/library/nginx # nginx镜像
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web2 # Deployment名称web2
spec:
selector:
matchLabels:
app: web2 # 管理标签app=web2的Pod
replicas: 1 # 副本数1
template:
metadata:
labels:
app: web2 # Pod标签 app=web2
spec:
affinity:
podAffinity: # Pod亲和性
requiredDuringSchedulingIgnoredDuringExecution: # Pod硬亲和【强制】
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web1 # 寻找标签 app=web1 的Pod
topologyKey: topology.kubernetes.io/zone # 拓扑域:可用区zone
containers:
- name: web-server # 容器名
image: hub.laoma.cloud/library/nginx # nginx镜像
bash
root@master30:~# kubectl apply -f deploy-with-podaffinity.yaml
root@master30 ~# kubectl get pods -o wide --no-headers |awk '{print $1,$3,$7}'
web1-849b694c75-t2spf Running worker31.laoma.cloud
web2-84fdf8f999-vn5l9 Running worker32.laoma.cloud
思考题:把 worker32 的 zone 标签删掉,重启 web2 后会去哪?
bash
root@master30:~# kubectl label nodes worker32.laoma.cloud \
topology.kubernetes.io/zone-
root@master30 ~# kubectl rollout restart deployment web2
# 调度到worker31节点
root@master30 ~# kubectl get pods -o wide --no-headers |awk '{print $1,$3,$7}'
web1-849b694c75-t2spf Running worker31.laoma.cloud
web2-7947994f89-pwnkv Running worker31.laoma.cloud
结论:Pod 亲和性必须同时满足两个条件
- node 必须具有相应标签(把节点定义成同一机架、同一机房等物理区域)
- 该拓扑域内至少运行着一个带相应标签的 Pod
bash
# 清理环境
root@master30:~# kubectl delete -f deploy-with-podaffinity.yaml
root@master30:~# kubectl label nodes worker31.laoma.cloud \
topology.kubernetes.io/zone-
实验 2:每节点一副本(hostname 反亲和)------高可用标配
bash
# 准备deployment
root@master30:~# vim deploy-with-podAntiAffinity.yaml
apiVersion: apps/v1 # Deployment API版本
kind: Deployment # 资源类型:Deployment
metadata:
name: store # Deployment名称 store
spec:
selector:
matchLabels:
app: store # 筛选管理标签 app=store 的Pod
replicas: 3 # 期望副本数:3个
template:
metadata:
labels:
app: store # Pod自身标签 app=store
spec:
affinity:
podAntiAffinity: # Pod反亲和
requiredDuringSchedulingIgnoredDuringExecution: # 硬反亲和(强制约束)
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- store # 匹配 app=store 的Pod
topologyKey: "kubernetes.io/hostname" # 拓扑域:单台节点(hostname)
containers:
- name: web-server # 容器名称
image: hub.laoma.cloud/library/nginx # nginx镜像
# 表示同一台节点上,不允许同时存在 2 个 app=store 的 Pod
bash
# 查看pod调度情况
root@master30:~# kubectl get pods -o wide --no-headers |awk '{print $1,$3,$7}'
store-5596cf4c84-9z5ch Pending <none>
store-5596cf4c84-dswkq Running worker32.laoma.cloud
store-5596cf4c84-ww2l4 Running worker31.laoma.cloud
结果 :每个节点的 hostname 唯一,所以每个节点只能跑一个副本,第三个副本找不到可用节点 → Pending。
生产建议:
replicas别超过可用节点数,否则副本会一直 Pending。
实验 3:亲和 + 反亲和的组合冲突
bash
# 缩容 deployment
root@master30:~# kubectl scale deployment store --replicas 1
root@master30:~# kubectl get pods -o wide --no-headers |awk '{print $1,$3,$7}'
store-5596cf4c84-dswkq Running worker32.laoma.cloud
# 准备新deployment
root@master30:~# vim deploy-with-multi-Affinity.yaml
apiVersion: apps/v1 # Deployment API版本
kind: Deployment # 资源类型:Deployment
metadata:
name: web-store # Deployment名称 web-store
spec:
selector:
matchLabels:
app: web-store # 管理标签 app=web-store 的Pod
replicas: 1 # 副本数:1
template:
metadata:
labels:
app: web-store # Pod自身标签 app=web-store
spec:
affinity:
podAntiAffinity: # Pod硬反亲和
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web-store # 匹配 app=web-store 的Pod
topologyKey: "kubernetes.io/hostname" # 拓扑域:单节点hostname
podAffinity: # Pod硬亲和
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- store # 匹配 app=store 的Pod
topologyKey: "kubernetes.io/hostname" # 拓扑域:单节点hostname
containers:
- name: web-app # 容器名称
image: hub.laoma.cloud/library/nginx # nginx镜像
# web-store Pod 必须跑到一台【有 store Pod、同时又没有 web-store Pod】的节点上
bash
root@master30:~# kubectl apply -f deploy-with-multi-Affinity.yaml
root@master30:~# kubectl get pods -o wide --no-headers |awk '{print $1,$3,$7}'
store-5596cf4c84-f4dfb Running worker32.laoma.cloud
web-store-7b4779c958-vpl58 Running worker32.laoma.cloud
# 扩容:验证新副本位置
root@master30:~# kubectl scale deployment web-store --replicas 2
root@master30:~# kubectl get pods -o wide --no-headers |awk '{print $1,$3,$7}'
store-5596cf4c84-f4dfb Running worker32.laoma.cloud
web-store-7b4779c958-bswws Pending <none>
web-store-7b4779c958-vpl58 Running worker32.laoma.cloud
为什么第二个副本挂起?
- 亲和性:必须和 store 同节点 → 只有 worker32 合格
- 反亲和性:不能和已有 web-store 副本同节点 → worker32 被排除
- 交集为空 → Pending
这就是设计 YAML 时最容易掉进去的坑:亲和 + 反亲和取交集,别让结果变成空集。
五、Taint 与 Toleration:反过来的"节点选 Pod"
前面都是 Pod 挑节点 ,污点/容忍则是节点挑 Pod:
- Taint(污点):打在节点上,让"不对味"的 Pod 进不来
- Toleration(容忍度):配在 Pod 上,让自己"够资格"进这些节点
🤔 nodeAffinity 和 Taint 到底啥区别?
| 机制 | 本质 |
|---|---|
nodeAffinity |
Pod 选 Node 的必要条件 |
Taint |
Node 选 Pod 的必要条件 |
5.1 管理节点污点
bash
# 给节点增加污点:键名 CPU,键值 L1,效果 NoSchedule
root@master30:~# kubectl taint nodes worker31.laoma.cloud \
CPU=L1:NoSchedule
# 移除污点(键名:effect-)
root@master30:~# kubectl taint nodes worker31.laoma.cloud CPU:NoSchedule-
5.2 Pod 侧配置容忍度
bash
tolerations:
- key: "CPU"
operator: "Equal" # 默认值:键值对完全相同才匹配
value: "L1"
effect: "NoSchedule"
# 只匹配 key,不关心 value 时:
tolerations:
- key: "CPU"
operator: "Exists"
effect: "NoSchedule"
effect 三种效果:
| effect | 行为 |
|---|---|
NoSchedule |
不能容忍的新 Pod 不会被调度上来;已在运行的 Pod 不会被驱逐 |
PreferNoSchedule |
软性版本,尽量避免调度过来,但不保证 |
NoExecute |
无法容忍的已运行 Pod 立刻被驱逐 ;能容忍但未指定 tolerationSeconds 的一直运行;指定了则运行超时后被驱逐 |
⚠️ 高危配置 :容忍度的
key为空且operator为Exists,表示容忍任意 taint,慎用。
5.3 实验:单污点 vs 单容忍度
bash
root@master30:~# kubectl taint nodes worker31.laoma.cloud CPU=L1:NoSchedule
root@master30:~# kubectl taint nodes worker32.laoma.cloud CPU=L2:NoSchedule
第一次尝试 :故意用不匹配的容忍度(CPU=L3)
bash
apiVersion: apps/v1 # Deployment资源API版本
kind: Deployment # 资源类型:Deployment控制器
metadata:
labels:
app: web # Deployment自身标签
name: web # Deployment名称
spec:
replicas: 2 # 期望Pod副本数量:2个
selector:
matchLabels:
app: web # 匹配Pod标签,管理Pod
template:
metadata:
labels:
app: web # Pod标签
spec:
tolerations: # 容忍(污点容忍)配置
- key: "CPU" # 污点key
operator: "Equal" # 运算符:Equal(等于)
value: "L3" # 污点value
effect: "NoSchedule" # 污点效果:不允许调度
containers:
- name: nginx # 容器名称
image: hub.laoma.cloud/library/nginx # 镜像地址
imagePullPolicy: IfNotPresent # 拉取策略:本地有镜像就不拉取
bash
root@master30:~# kubectl apply -f deploy-with-tolerations.yaml
root@master30:~# kubectl get pods
NAME READY STATUS RESTARTS AGE
web-5bf7b7b98f-kh7hv 0/1 Pending 0 2s
web-5bf7b7b98f-km7w5 0/1 Pending 0 2s
# 事件表明3个节点上的污点与pod不匹配,所以无法创建pod
root@master30:~# kubectl describe pod web-5bf7b7b98f-kh7hv
......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 29s default-scheduler 0/3 nodes are available: 1 node(s) had untolerated taint {CPU: L1}, 1 node(s) had untolerated taint {CPU: L2}, 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }. preemption: 0/3 nodes are available: 3 Preemption is not helpful for scheduling.
顺带解释了:为什么普通 Pod 永远跑不到 master 上------control-plane 节点天生带
node-role.kubernetes.io/control-plane污点。
第二次尝试 :改成匹配的 CPU=L1
bash
root@master30:~# kubectl delete deployments.apps web
root@master30:~# vim deploy-with-tolerations.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: web
name: web
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
tolerations:
- key: "CPU"
operator: "Equal"
value: "L1"
effect: "NoSchedule"
containers:
- name: nginx
image: hub.laoma.cloud/library/nginx
imagePullPolicy: IfNotPresent
bash
root@master30:~# kubectl apply -f deploy-with-tolerations.yaml
root@master30:~# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web-7c99d767-w9rzt 1/1 Running 0 5s 10.224.162.142 worker31.laoma.cloud <none> <none>
web-7c99d767-wwjhk 1/1 Running 0 5s 10.224.19.13 worker31.laoma.cloud <none> <none>
# Pod调度成功,而且分配到了worker31
# 清理环境
root@master30:~# kubectl taint node worker31.laoma.cloud CPU:NoSchedule-
root@master30:~# kubectl taint node worker32.laoma.cloud CPU:NoSchedule-
root@master30:~# kubectl delete deployments.apps web
关键认知 :容忍度只是"准入许可证",不是定向指令 。它能上 worker31,同时也保留了上其他节点的可能性。想"只跑在某类节点",必须污点 + 节点亲和性配合使用。
5.4 多污点 + 多容忍度的过滤器逻辑
处理过程就像一个过滤器:遍历节点所有污点,过滤掉被 Pod 容忍的,剩下的决定结果:
- 剩余污点含
NoSchedule→ 不分配 - 无
NoSchedule但含PreferNoSchedule→ 尽量不分配 - 剩余污点含
NoExecute→ 不分配(未运行)或驱逐(已运行)
实验:worker31 打三个污点,Pod 只容忍前两个
bash
root@master30:~# kubectl taint nodes worker31.laoma.cloud CPU=L1:NoSchedule
root@master30:~# kubectl taint nodes worker31.laoma.cloud CPU=L1:NoExecute
root@master30:~# kubectl taint nodes worker31.laoma.cloud MEM=L2:NoSchedule
root@master30:~# kubectl taint nodes worker32.laoma.cloud CPU=L2:NoSchedule
bash
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: web
name: web
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
tolerations:
- key: "CPU"
operator: "Equal"
value: "L1"
effect: "NoSchedule"
- key: "CPU"
operator: "Equal"
value: "L1"
effect: "NoExecute"
containers:
- name: nginx
image: hub.laoma.cloud/library/nginx
imagePullPolicy: IfNotPresent
bash
root@master30:~# kubectl apply -f deploy-with-tolerations.yaml
root@master30:~# kubectl get pods
NAME READY STATUS RESTARTS AGE
web-6844d698c4-b8cz7 0/1 Pending 0 4s
web-6844d698c4-wrpkj 0/1 Pending 0 4s
# 事件表明节点上的3个污点与pod不匹配,所以无法创建pod
root@master30:~# kubectl describe pod web-6844d698c4-b8cz7
......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 50s default-scheduler 0/3 nodes are available: 1 node(s) had untolerated taint {CPU: L2}, 1 node(s) had untolerated taint {MEM: L2}, 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }. preemption: 0/3 nodes are available: 3 Preemption is not helpful for scheduling.
补上第三条容忍度后成功:
bash
root@master30:~# kubectl delete deployments.apps web
root@master30:~# vim deploy-with-tolerations.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: web
name: web
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
tolerations:
- key: "CPU"
operator: "Equal"
value: "L1"
effect: "NoSchedule"
- key: "CPU"
operator: "Equal"
value: "L1"
effect: "NoExecute"
- key: "MEM"
operator: "Equal"
value: "L2"
effect: "NoSchedule"
containers:
- name: nginx
image: hub.laoma.cloud/library/nginx
imagePullPolicy: IfNotPresent
bash
root@master30:~# kubectl apply -f deploy-with-tolerations.yaml
root@master30:~# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web-77d5464ccf-656fd 1/1 Running 0 5s 10.224.19.18 worker31.laoma.cloud <none> <none>
web-77d5464ccf-wjk6m 1/1 Running 0 5s 10.224.19.27 worker31.laoma.cloud <none> <none>
# 清理环境
root@master30:~# kubectl taint nodes worker31.laoma.cloud CPU=L1:NoSchedule-
root@master30:~# kubectl taint nodes worker31.laoma.cloud CPU=L1:NoExecute-
root@master30:~# kubectl taint nodes worker31.laoma.cloud MEM=L2:NoSchedule-
root@master30:~# kubectl taint nodes worker32.laoma.cloud CPU=L2:NoSchedule-
root@master30:~# kubectl delete deployments.apps web
5.5 生产用例:专用节点与 GPU 节点
用户专用节点
bash
# 给专用节点打污点
kubectl taint nodes <node> dedicated=groupName:NoSchedule
然后给这组用户的 Pod 加对应 toleration。注意 :这样配置的 Pod 既能上专用节点,也能上集群其他节点。
若要求 Pod 只能上专用节点,必须再叠加:
bash
# 1. 同时给专用节点加标签
kubectl label nodes <node> dedicated=groupName
# 2. 给 Pod 加节点亲和性,限定只能调度到带该标签的节点
特殊硬件节点(GPU)
bash
# 给 GPU 节点打污点,避免不需要 GPU 的 Pod 占用资源
kubectl taint nodes <gpu-node> special=true:NoSchedule
# 再给真正需要 GPU 的任务加匹配的 toleration
5.6 基于污点的驱逐:K8s 自动加了哪些?
节点异常时,节点控制器会自动给节点加污点:
| 内置污点 | 含义 |
|---|---|
node.kubernetes.io/not-ready |
节点未就绪(Ready=False) |
node.kubernetes.io/unreachable |
节点控制器访问不到节点(Ready=Unknown) |
node.kubernetes.io/memory-pressure |
内存压力 |
node.kubernetes.io/disk-pressure |
磁盘压力 |
node.kubernetes.io/pid-pressure |
PID 压力 |
node.kubernetes.io/network-unavailable |
网络不可用 |
node.kubernetes.io/unschedulable |
节点不可调度 |
node.cloudprovider.kubernetes.io/uninitialized |
外部云平台驱动未初始化 |
为保证节点问题引发的驱逐速率可控,系统以限定速率添加污点,避免主控与 worker 通信中断时 Pod 被大量驱逐。
利用 tolerationSeconds 争取缓冲时间:
bash
tolerations:
- key: "node.kubernetes.io/unreachable"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 6000
✨ 两个必须知道的内置行为:
- K8s 会自动给所有 Pod 注入
not-ready和unreachable容忍度(tolerationSeconds=300)→ 节点异常时 Pod 默认还能坚持 5 分钟 - DaemonSet 的 Pod 对这两个污点的容忍不设 tolerationSeconds → 保证节点级 Agent(如 CNI、kube-proxy)永不被驱逐
bash
# 环境清理
root@master30:~# kubectl delete ns scheduler
六、节点维护三件套:cordon / drain / uncordon
6.1 cordon:标记为不可调度
bash
root@master30:~# kubectl cordon -h
Mark node as unschedulable.
Usage:
kubectl cordon NODE [options]
root@master30:~# kubectl get nodes
NAME STATUS ROLES AGE VERSION
master30.laoma.cloud Ready control-plane 9d v1.30.2
worker31.laoma.cloud Ready <none> 9d v1.30.2
worker32.laoma.cloud Ready <none> 9d v1.30.2
# 标记worker31.laoma.cloud为SchedulingDisabled
root@master30:~# kubectl cordon worker31.laoma.cloud
root@master30:~# kubectl get nodes
NAME STATUS ROLES AGE VERSION
master30.laoma.cloud Ready control-plane 9d v1.30.2
worker31.laoma.cloud Ready,SchedulingDisabled <none> 9d v1.30.2
worker32.laoma.cloud Ready <none> 9d v1.30.2
# 创建一个deployment
root@master30:~# kubectl create deployment web --image=hub.laoma.cloud/library/nginx --replicas=2
root@master30:~# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web-59b9bb7664-flhvx 1/1 Running 0 10s 10.224.225.69 worker32.laoma.cloud <none> <none>
web-59b9bb7664-ss586 1/1 Running 0 10s 10.224.225.68 worker32.laoma.cloud <none> <none>
注意:cordon 只影响新 Pod,已经在 worker31 上跑的 Pod 不会被赶走。
6.2 uncordon:恢复调度
bash
root@master30:~# kubectl uncordon worker31.laoma.cloud
root@master30:~# kubectl get nodes
NAME STATUS ROLES AGE VERSION
master30.laoma.cloud Ready control-plane 36d v1.30.2
worker31.laoma.cloud Ready <none> 36d v1.30.2
worker32.laoma.cloud Ready <none> 36d v1.30.2
root@master30:~# kubectl scale deployment web --replicas=4
root@master30:~# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web-59b9bb7664-flhvx 1/1 Running 0 2m6s 10.224.225.69 worker32.laoma.cloud <none> <none>
web-59b9bb7664-jkb5q 1/1 Running 0 5s 10.224.51.133 worker31.laoma.cloud <none> <none>
web-59b9bb7664-ss586 1/1 Running 0 2m6s 10.224.225.68 worker32.laoma.cloud <none> <none>
web-59b9bb7664-w6mbs 1/1 Running 0 5s 10.224.51.132 worker31.laoma.cloud <none> <none>
6.3 drain:安全地清空节点(维护前必做)
内核升级、硬件维护之前,用 kubectl drain 把节点上的 Pod 安全地驱逐走。驱逐过程允许 Pod 容器体面终止,并确保满足 PodDisruptionBudget。
前置条件:
- 节点清空期间,确保应用具有高可用性
- 已为需要的应用配置了 PodDisruptionBudget(建议设置
AlwaysAllow不健康 Pod 驱逐策略)
bash
root@master30:~# kubectl drain -h
Drain node in preparation for maintenance.
The given node will be marked unschedulable to prevent new pods from arriving.
'drain' evicts the pods if the APIServer supports.
Usage:
kubectl drain NODE [options]
# 默认不驱逐 DaemonSet 控制的pod
root@master30:~# kubectl drain worker31.laoma.cloud
node/worker31.laoma.cloud cordoned
error: unable to drain node "worker31.laoma.cloud", aborting command...
There are pending nodes to be drained:
worker31.laoma.cloud
error: cannot delete DaemonSet-managed Pods (use --ignore-daemonsets to ignore): kube-system/calico-node-jwdzx, kube-system/kube-proxy-98qkl
# 使用--ignore-daemonsets驱逐DaemonSet控制的pod
root@master30:~# kubectl drain worker31.laoma.cloud --ignore-daemonsets
实用选项:
bash
# 只驱逐特定标签的 Pod
--pod-selector='': Label selector to filter pods on the node
-l, --selector='': Selector (label query) to filter on,supports '=', '==', and '!='.(e.g. -l CPU=L1,MEM=L2). Matching objects must satisfy all of the specified label constraints.
root@master30:~# kubectl get node
NAME STATUS ROLES AGE VERSION
master30.laoma.cloud Ready control-plane 36d v1.30.2
worker31.laoma.cloud Ready,SchedulingDisabled <none> 36d v1.30.2
worker32.laoma.cloud Ready <none> 36d v1.30.2
# 删除worker31上SchedulingDisabled标记
root@master30:~# kubectl uncordon worker31.laoma.cloud
cordon vs drain 一句话区别:cordon = 只关门(不让新人进);drain = 关门 + 清人(把现有 Pod 都赶走)。
七、Metrics-Server:让 kubectl top 和 HPA 有数据可用
7.1 它解决什么问题
- 如何监控 node 计算资源使用情况?
- 如何监控 pod 计算资源使用情况?
- 如何根据 pod 计算资源使用情况自动扩展?
Metrics Server 通过 Kubelet 收集资源指标,再通过 Metrics API 公开在 Kubernetes apiserver 中,供 HPA 和 VPA 使用。
7.2 环境准备
bash
root@master30:~# kubectl create ns metric
root@master30:~# kubectl config set-context --current --namespace metric
7.3 部署
bash
# 下载 Metrics-Server
root@master30:~# wget -O metrics-server-components.yaml https://laoma.cloud/class/course-materials/softwares/stage03/metrics-server-components-v0.7.1.yaml
# 修改 Metrics-Server,不校验tls(实验环境自签证书必需)
root@master30:~# sed -i '/metric-resolution/a\ - --kubelet-insecure-tls' metrics-server-components.yaml
# 修改镜像为私有仓库
root@master30:~# grep image: metrics-server-components.yaml
image: registry.k8s.io/metrics-server/metrics-server:v0.7.1
root@master30:~# sed -i 's/registry.k8s.io/hub.laoma.cloud/g' metrics-server-components.yaml
# 部署
root@master30:~# kubectl apply -f metrics-server-components.yaml
serviceaccount/metrics-server created
clusterrole.rbac.authorization.k8s.io/system:aggregated-metrics-reader created
clusterrole.rbac.authorization.k8s.io/system:metrics-server created
rolebinding.rbac.authorization.k8s.io/metrics-server-auth-reader created
clusterrolebinding.rbac.authorization.k8s.io/metrics-server:system:auth-delegator created
clusterrolebinding.rbac.authorization.k8s.io/system:metrics-server created
service/metrics-server created
deployment.apps/metrics-server created
apiservice.apiregistration.k8s.io/v1beta1.metrics.k8s.io created
# 查看 Metrics-Server 状态
root@master30:~# kubectl get pods -n kube-system |grep metrics
metrics-server-6556f8cb6c-78mhq 1/1 Running 0 45s
7.4 使用
bash
root@master30:~# kubectl top node
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
master30.laoma.cloud 97m 4% 1328Mi 35%
worker31.laoma.cloud 49m 2% 912Mi 24%
worker32.laoma.cloud 45m 2% 926Mi 24%
root@master30:~# kubectl top pods -n kube-system
NAME CPU(cores) MEMORY(bytes)
calico-kube-controllers-6dfcd885bf-8rhg2 2m 11Mi
calico-node-crfr7 27m 57Mi
calico-node-sbmjt 22m 57Mi
calico-node-z9dvd 24m 56Mi
coredns-6d56c8448f-nhjpg 3m 12Mi
coredns-6d56c8448f-w2f48 2m 10Mi
etcd-master30.laoma.cloud 16m 54Mi
kube-apiserver-master30.laoma.cloud 46m 355Mi
kube-controller-manager-master30.laoma.cloud 11m 50Mi
kube-proxy-hx566 1m 18Mi
kube-proxy-t2r4x 1m 18Mi
kube-proxy-x6k8h 1m 18Mi
kube-scheduler-master30.laoma.cloud 4m 22Mi
metrics-server-6556f8cb6c-78mhq 1m 11Mi
单位换算 :
1 CPU = 1000m
八、HPA(Horizontal Pod Autoscaler):Pod 水平自动扩缩容
控制器从 metrics.k8s.io、custom.metrics.k8s.io、external.metrics.k8s.io 获取度量值(前者通常由 Metrics Server 提供),根据定义的指标动态伸缩 Pod 数量,支持 Deployment、ReplicationController、ReplicaSet。
适用场景:
- 流量波动大的无状态服务(nginx、api、网关、微服务)
- 需要提高并发、削峰填谷
核心特点:
- 不修改 Pod 配置,只改副本数
- 实时、秒级扩缩
- 最常用、最稳定
支持指标:CPU 使用率、内存使用率、自定义指标(对接 Prometheus)、QPS/延迟等
8.1 三个核心调参(实验环境建议调小)
Pod 数量扩缩容由 kube-controller-manager 管理:
| 参数 | 默认值 | 作用 |
|---|---|---|
--horizontal-pod-autoscaler-sync-period |
15s | 采样周期:拉取指标 → 计算期望副本 → 判断是否扩容 |
--horizontal-pod-autoscaler-initial-readiness-delay |
30s | Pod 启动后这段时间内视为"启动中",不参与 HPA 计算 |
--horizontal-pod-autoscaler-downscale-stabilization |
300s(5 分钟) | 缩容冷却时间 |
bash
root@master30:~# vim /etc/kubernetes/manifests/kube-controller-manager.yaml
spec:
containers:
- command:
- kube-controller-manager
- --allocate-node-cidrs=true
......
# 增加下面三行参数
- --horizontal-pod-autoscaler-sync-period=10s
- --horizontal-pod-autoscaler-initial-readiness-delay=20s
- --horizontal-pod-autoscaler-downscale-stabilization=30s
保存退出,静态 Pod 会自动重启生效。
实际扩容 cooling 时间范围约 20s~30s:最短 20s(Pod 启动刚好赶上采指立刻扩),最长 30s(启动后还要等一个采样周期)。
8.2 实验:基于 CPU 使用率伸缩
准备资源
bash
root@master30:~# kubectl create deployment web --image=hub.laoma.cloud/library/nginx
root@master30:~# kubectl get pods
NAME READY STATUS RESTARTS AGE
web-68b95c775c-xksf4 1/1 Running 0 4s
创建 HPA
bash
Usage: kubectl autoscale (-f FILENAME | TYPE NAME | TYPE/NAME) [--min=MINPODS]--max=MAXPODS [--cpu-percent=CPU] [options]
# 示例
root@master30:~# kubectl autoscale deployment web --max=5 --min=2 --cpu-percent=80 -o yaml
# 省略部分不重要配置
# 注意这里版本是v1
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: web
namespace: metric
spec:
maxReplicas: 5
minReplicas: 2
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web
targetCPUUtilizationPercentage: 80
bash
# 稍等片刻,hpa会自动创建一个新的pod(补齐 minReplicas)
root@master30:~# kubectl get pods
NAME READY STATUS RESTARTS AGE
web-68b95c775c-48ngs 1/1 Running 0 13s
web-68b95c775c-xksf4 1/1 Running 0 41s
root@master30:~# kubectl get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
web Deployment/web cpu: <unknown>/80% 2 5 2 34s
# CPU 目标为unknown
⚠️ TARGETS 显示
<unknown>的排查清单 (必看):必须同时满足两个条件 ------① 已安装 Metrics Server;② Pod 设置了资源限制 limits(没有 limit 就没有百分比的分母)。
bash
root@master30:~# kubectl edit deployments.apps web
# 修改spec.template.spec.containers.[N].resources属性,添加limit属性,如下:
resources:
limits:
cpu: 100m
memory: 200Mi
# 再次查看hpa
root@master30:~# kubectl get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
web Deployment/web cpu: 0%/80% 2 5 2 83s
root@master30:~# kubectl expose deployment web --port=80 --target-port=80
root@master30:~# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web ClusterIP 10.100.255.92 <none> 80/TCP 6s
压力测试
bash
# 打开一个监控窗口
root@master30:~# \
while true
do
clear;
date;echo
kubectl get hpa;echo
kubectl get pod;echo
kubectl top pods
sleep 1
done | tee hpa.log
# 上 CPU 压力
root@master30:~# apt install -y apache2-utils
root@master30:~# while true ;do ab -n 300000 -c 100 http://10.100.255.92/;done
# -n 300000,总请求数
# -c 100,每次并发数
扩容过程实录(每轮约 20 秒新增一个副本):
bash
Every 2.0s: kubectl get hpa;echo;kubectl top pods master30.laoma.cloud: Sun Apr 19 09:57:22 2026
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
web Deployment/web cpu: 100%/80% 2 5 2 8m32s
NAME CPU(cores) MEMORY(bytes)
web-6dcdc45c94-6gwd2 101m 3Mi
web-6dcdc45c94-dvd2m 100m 3Mi
bash
Every 2.0s: kubectl get hpa;echo;kubectl top pods master30.laoma.cloud: Sun Apr 19 09:58:04 2026
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
web Deployment/web cpu: 95%/80% 2 5 3 9m14s
NAME CPU(cores) MEMORY(bytes)
web-6dcdc45c94-2zbmb 93m 3Mi
web-6dcdc45c94-6gwd2 100m 3Mi
web-6dcdc45c94-dvd2m 90m 3Mi
bash
Every 2.0s: kubectl get hpa;echo;kubectl top pods master30.laoma.cloud: Sun Apr 19 09:58:59 2026
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
web Deployment/web cpu: 99%/80% 2 5 4 11m
NAME CPU(cores) MEMORY(bytes)
web-6dcdc45c94-2zbmb 100m 3Mi
web-6dcdc45c94-6gwd2 100m 3Mi
web-6dcdc45c94-dvd2m 97m 3Mi
web-6dcdc45c94-tsjkr 99m 3Mi
bash
Every 2.0s: kubectl get hpa;echo;kubectl top pods master30.laoma.cloud: Sun Apr 19 09:59:59 2026
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
web Deployment/web cpu: 100%/80% 2 5 5 11m
NAME CPU(cores) MEMORY(bytes)
web-6dcdc45c94-2zbmb 101m 3Mi
web-6dcdc45c94-6gwd2 100m 3Mi
web-6dcdc45c94-dvd2m 100m 3Mi
web-6dcdc45c94-hpg45 98m 3Mi
web-6dcdc45c94-tsjkr 101m 3Mi
观察结论:
- 随着 Pod CPU 使用率上升,自动扩展 Pod 数量
- 正常情况下约 20 秒创建一个新 Pod
- 即使 CPU 超过阈值,Pod 数量不会超过 maxReplicas(5)
- 停止压力测试,负载降低后,Pod 数量会缩回 minReplicas(2)
bash
# 清理资源
root@master30:~# kubectl delete hpa web
root@master30:~# kubectl delete deployment web
# 保留 svc,后续使用
8.3 实验:基于 Memory 使用率伸缩
想让 Nginx 内存涨,最简单方法是让它返回一个超大响应体。
准备资源
bash
# worker节点创建big.img
[root@worker31 ~]# mkdir /www
[root@worker31 ~]# dd if=/dev/zero of=/www/big.img bs=1M count=200
[root@worker32 ~]# mkdir /www
[root@worker32 ~]# dd if=/dev/zero of=/www/big.img bs=1M count=200
root@master30:~# vim deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 1
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: big-file
mountPath: /usr/share/nginx/html
resources:
limits:
cpu: 100m
memory: 200Mi
volumes:
- name: big-file
hostPath:
path: /www
root@master30:~# kubectl apply -f deployment.yaml
创建 HPA(注意这里是 v2 版本)
bash
root@master30:~# vim hpa-mem.yaml
# 注意这里版本为v2
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web
spec:
minReplicas: 2
maxReplicas: 5
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web
metrics:
- resource:
name: memory
target:
type: Utilization
# 使用20% 效果比较明显,这里实用60%
averageUtilization: 60
type: Resource
root@master30:~# kubectl apply -f hpa-mem.yaml
# 稍等片刻:目标值中内存才会正常显示
root@master30:~# kubectl get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
web Deployment/web memory: 2%/60% 2 5 1 13m
# 稍等片刻,创建一个新的pod
root@master30:~# kubectl get pods
NAME READY STATUS RESTARTS AGE
web-6dcdc45c94-2zj8s 1/1 Running 0 13m
web-6dcdc45c94-w62pr 1/1 Running 0 13s
压力测试
bash
# 打开一个监控窗口
root@master30:~# \
while true
do
clear;
date;echo
kubectl get hpa;echo
kubectl get pod;echo
kubectl top pods
sleep 1
done | tee hpa.log
# 上 MEM 压力
root@master30:~# while true ;do ab -n 300000 -c 100 http://10.99.129.137/big.img;sleep 1;done
排雷提示 :本机作为压测客户端向外大量发起 HTTP 请求会产生海量 TCP 收发包。若 master30 硬件配置低,会导致自身 CPU 性能瓶颈------用
top看会发现 si(软中断)指标偏高,特定 CPU 核心接近 100%,这是软中断全堆在单核处理导致的。
bash
Every 4.0s: kubectl get hpa;echo;kubectl top pods master30.laoma.cloud: Sun Apr 19 11:13:29 2026
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
web Deployment/web memory: 86%/60% 2 5 2 16m
NAME CPU(cores) MEMORY(bytes)
web-88cff78bb-2b7vh 15m 174Mi
web-88cff78bb-76pg2 15m 171Mi
bash
Every 4.0s: kubectl get hpa;echo;kubectl top pods master30.laoma.cloud: Sun Apr 19 11:13:46 2026
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
web Deployment/web memory: 58%/60% 2 5 3 16m
NAME CPU(cores) MEMORY(bytes)
web-88cff78bb-2b7vh 15m 174Mi
web-88cff78bb-4hcm5 5m 3Mi
web-88cff78bb-76pg2 15m 171Mi
bash
Every 4.0s: kubectl get hpa;echo;kubectl top pods master30.laoma.cloud: Sun Apr 19 11:15:46 2026
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
web Deployment/web memory: 58%/60% 2 5 4 16m
NAME CPU(cores) MEMORY(bytes)
web-88cff78bb-2b7vh 15m 174Mi
web-88cff78bb-4hcm5 5m 3Mi
web-88cff78bb-76pg2 15m 171Mi
web-88cff78bb-ws6t4 1m 3Mi
bash
Every 4.0s: kubectl get hpa;echo;kubectl top pods master30.laoma.cloud: Sun Apr 19 11:17:46 2026
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
web Deployment/web memory: 58%/60% 2 5 5 16m
NAME CPU(cores) MEMORY(bytes)
web-88cff78bb-2b7vh 15m 174Mi
web-88cff78bb-4hcm5 5m 3Mi
web-88cff78bb-76pg2 15m 171Mi
web-88cff78bb-ws6t4 1m 3Mi
web-88cff78bb-t4q2p 100m 3Mi
观察结论:
- 随着 Pod 内存使用率上升,自动扩展 Pod 数量
- 正常情况下约 20 秒创建一个新 Pod
- 即使内存超过阈值,Pod 数量不会超过 5
- 停止压测后,副本数缩回 2
- 太高的负载可能导致 Pod 出现 OOMKilled
bash
# 清理资源
root@master30:~# kubectl delete hpa web
root@master30:~# kubectl delete deployment web
root@master30:~# kubectl delete svc web
8.4 HPA 精细管控:behavior(K8s 1.18+)
扩容 scaleUp
bash
scaleUp:
# 扩容稳定窗口:负载突增后等待多久再扩容
stabilizationWindowSeconds: 0
# 扩容限速策略:控制每个周期最多新增多少Pod
policies:
- type: Percent
value: 数值
periodSeconds: 周期时长
- type: Pods
value: 数值
periodSeconds: 周期时长
# 多策略选择逻辑
selectPolicy: Max / Min / Disabled
① stabilizationWindowSeconds :用于过滤瞬时突刺流量,避免瞬间飙升立刻扩容
- 取值逻辑:窗口内取指标计算的最小副本数作为目标,只有持续高负载才会扩容
- 默认值:0(无等待,负载一超标立刻尝试扩容)
② policies:每条策略三要素
type:Percent(周期内最多新增当前副本百分比)/Pods(周期内最多新增固定数量 Pod)value:阈值periodSeconds:策略生效周期(最大 1800 秒)
③ selectPolicy
Max:取新增 Pod 更多的策略(激进,默认)Min:取新增 Pod 更少的策略(保守,生产推荐)Disabled:完全禁止
默认扩容策略
bash
scaleUp: # HPA 扩容(加Pod)行为配置
stabilizationWindowSeconds: 0 # 稳定窗口:0秒,指标一满足立刻扩容,不等待防抖
policies: # 扩容限制策略(两条同时生效,按规则选一条)
- type: Percent # 策略1:百分比限制
value: 100 # 每15s,最多新增 当前副本数的100%(翻倍)
periodSeconds: 15 # 周期:15秒
- type: Pods # 策略2:固定Pod数量限制
value: 4 # 每15s,最多新增 4个Pod
periodSeconds: 15 # 周期:15秒
selectPolicy: Max # 策略选择规则:两个策略算出来,取【扩容数量更大】的那个
解读:无等待先格子满分为参考;两条 15s 滑动窗口策略------每 15s 最多翻倍(Percent 100%)或最多新增 4 个(Pods 4),取允许扩更多的那条作为本轮上限。
⚠️ policy 是上限,最终扩容数量由指标计算公式决定,不是强制每次扩这么多。
扩容执行流程
- Metrics Server 15s 采集一次指标
- HPA 计算目标副本数
- 读取稳定窗口内历史推荐值,取最小值作为候选副本
- 判断需要扩容 → 用限速策略计算本周期允许新增 Pod 上限
- 按限速分步扩容,不会一次性拉满 maxReplicas
- 下个周期重新采集指标,逐步扩容
三个典型配置
bash
# 案例1:禁止扩容
scaleUp:
policies:
- type: Percent
value: 0
periodSeconds: 3600
selectPolicy: Max
# 案例2:激进扩容(无等待,每 15s 扩 50%)
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 50
periodSeconds: 15
selectPolicy: Max
# 案例3:平缓扩容(等 60s 确认持续高负载;每 30s 最多扩 20% 或 4 个,取更保守的)
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 20
periodSeconds: 30
- type: Pods
value: 4
periodSeconds: 30
selectPolicy: Min
缩容 scaleDown
bash
scaleDown:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 数值
periodSeconds: 周期时长
- type: Pods
value: 数值
periodSeconds: 周期时长
selectPolicy: Max / Min / Disabled
① stabilizationWindowSeconds
- 取值逻辑:窗口内取最大期望副本作为目标副本;只有持续低负载超过窗口时长才真正缩容,防止瞬时流量下跌删 Pod 导致反弹
- 默认值:300(5 分钟),取值范围 0~3600(1 小时)
- 举例:当前 10 副本,指标瞬时建议缩到 6,但 5 分钟窗口内历史最大值仍是 10 → 不缩;5 分钟后高值过期才按更低副本缩
默认缩容策略
bash
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 100
periodSeconds: 15
selectPolicy: Max
# 只有单条策略,selectPolicy字段存在但不产生取舍效果
缩容执行流程
- Metrics Server 每 15s 采集 CPU/内存指标
- HPA 计算理论期望副本
- 查询稳定窗口历史推荐值,取最大值作为候选目标
- 对比当前副本,判断需要缩容
- 执行 policies 限速规则,计算本周期允许删除的最大 Pod 数
- 按限速值缩减副本,不会一次性缩到最低
- 下个周期重复计算,分步缓慢下降
三个典型配置
bash
# 案例1:禁止缩容
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 0
periodSeconds: 3600
selectPolicy: Max
# 案例2:激进缩容
scaleDown:
stabilizationWindowSeconds: 30
policies:
- type: Percent
value: 100
periodSeconds: 15
selectPolicy: Max
# 案例3:平缓缩容(等 10 分钟确认低负载;每分钟最多缩 10% 或删 2 个,取删得更少的)
scaleDown:
stabilizationWindowSeconds: 600
policies:
- type: Percent
value: 10
periodSeconds: 60
- type: Pods
value: 2
periodSeconds: 60
selectPolicy: Min
扩容 vs 缩容完整对比
| 配置项 | scaleUp(扩容) | scaleDown(缩容) |
|---|---|---|
| stabilizationWindowSeconds 默认值 | 0 | 300(5 分钟) |
| 稳定窗口取值逻辑 | 窗口内取最小期望副本 | 窗口内取最大期望副本 |
| 默认 policies 条数 | 2 条(Percent + Pods) | 1 条(仅 Percent) |
| 默认策略内容 | 15s 最多扩容 100% / 最多新增 4 Pod | 15s 最多删除 100% 副本 |
| 默认 selectPolicy | Max | Max(单策略无实际效果) |
| selectPolicy 支持选项 | Max / Min / Disabled | Max / Min / Disabled |
| 触发阈值 | 利用率 > target × 1.1 | 利用率 < target × 0.9 |
| 设计思想 | 优先快速扩容保障业务 | 谨慎缩容,避免流量反弹 |
8.5 VPA:纵向扩容,了解一下就行
根据 VPA 中定义的指标动态调整 Pod 的 requests 和 limits(不改数量)。
适用场景 :资源配置不合理的服务、长期运行且流量稳定的服务、Java/Go 等内存占用固定的应用、不适合频繁扩缩的服务。
核心特点 :不改变 Pod 数量;自动给 Pod 推荐/设置 CPU/内存;生效需要重启 Pod(默认);有侵入性,比 HPA 用得少。
| 项目 | HPA 横向扩缩容 | VPA 纵向扩缩容 |
|---|---|---|
| 全称 | Horizontal | Vertical |
| 扩缩方式 | 增加/减少 Pod 数量 | 调整 CPU/内存 request/limit |
| 是否重启 Pod | ❌ 不重启 | ✅ 一般需要重启 |
| 适合负载 | 流量波动大 | 资源配置不合理 |
| 生效速度 | 快 | 慢 |
| 稳定性 | 高 | 中 |
| 生产使用 | 非常普遍 | 较少 |
| 能否一起开 | 不能同时开(会冲突) | --- |
| 支持资源 | CPU、内存、自定义 | CPU、内存 |
bash
# 环境清理
root@master30:~# kubectl delete ns metric
九、ResourceQuota:命名空间级的总量硬限制
问题 :多用户/团队共享集群时,有人会用超过公平份额的资源。
解决:用资源配额限制每个 Namespace 能用的资源总量。
工作方式:
- 不同团队在不同 namespace 下工作(可通过 RBAC 强制)
- 集群管理员为每个 namespace 创建 ResourceQuota 对象
- 创建资源时配额系统实时跟踪用量,确保不超过硬限制
- 违反约束的请求会报 HTTP 403 FORBIDDEN,并明确告知违反了什么
若 namespace 启用了计算资源(cpu/memory)配额,用户必须 为 Pod 设定 request 和 limit,否则拒绝创建。可用 LimitRanger 准入控制器自动补默认值。
9.1 环境准备
bash
root@master30:~# kubectl create ns quota
root@master30:~# kubectl config set-context --current --namespace quota
(需提前部署好 Metric Server)
9.2 两种配额类型
① 对象数量配额------提高稳定性,避免 etcd 无限增长、避免占用节点 IP 等资源
bash
persistentvolumeclaims / services / secrets / configmaps / replicationcontrollers
deployments.apps / replicasets.apps / statefulsets.apps / jobs.batch / cronjobs.batch
② 计算资源配额------避免单个 namespace 吃光集群资源
| 资源名称 | 描述 |
|---|---|
limits.cpu |
所有非终止状态 Pod 的 CPU 限制总和上限 |
limits.memory |
所有非终止状态 Pod 的内存限制总和上限 |
requests.cpu |
所有非终止状态 Pod 的 CPU 请求总和上限 |
requests.memory |
所有非终止状态 Pod 的内存请求总和上限 |
requests.storage |
所有 PVC 的存储请求总和上限 |
cpu |
等同于 requests.cpu |
memory |
等同于 requests.memory |
单位说明:
- CPU :
1 cpu = 1000m,默认单位是 CPU 核心数 - memory :支持 1024 进制(
Ki|Mi|Gi|Ti|Pi|Ei)和 1000 进制(k|M|G|T|P|E),默认单位是 G(如1.5= 1500M)
9.3 实验:对象数量配额如何拦违规行为
bash
root@master30:~# kubectl create quota myquota --hard=pods=2,services=3,secrets=5,persistentvolumeclaims=10
root@master30:~# kubectl get resourcequotas
NAME AGE REQUEST LIMIT
my-quota 10s persistentvolumeclaims: 0/10, pods: 0/2, secrets: 1/5, services: 0/3
root@master30:~# kubectl describe quota myquota
Name: myquota
Namespace: quota
Resource Used Hard
-------- ---- ----
persistentvolumeclaims 0 10
pods 0 2
secrets 1 5
services 0 3
等价 YAML:
bash
apiVersion: v1
kind: ResourceQuota
metadata:
name: myquota
spec:
hard:
persistentvolumeclaims: "10"
pods: "2"
secrets: "5"
services: "3"
创建 3 副本 Deployment 试试:
bash
root@master30:~# kubectl create deployment web --image=hub.laoma.cloud/library/nginx --replicas=3
root@master30:~# kubectl get all
NAME READY STATUS RESTARTS AGE
pod/web-96d5df5c8-dl7qk 1/1 Running 0 40s
pod/web-96d5df5c8-j7fhh 1/1 Running 0 40s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/web 2/3 2 2 40s
NAME DESIRED CURRENT READY AGE
replicaset.apps/web-96d5df5c8 3 2 2 40s
root@master30:~# kubectl describe rs web-96d5df5c8
LAST SEEN TYPE REASON OBJECT MESSAGE
......
33s Warning FailedCreate replicaset/web-96d5df5c8 Error creating: pods "web-96d5df5c8-xg6c9" is forbidden: exceeded quota: myquota, requested: pods=1, used: pods=2, limited: pods=2
2s Warning FailedCreate replicaset/web-96d5df5c8 (combined from similar events): Error creating: pods "web-96d5df5c8-89p7j" is forbidden: exceeded quota: myquota, requested: pods=1, used: pods=2, limited: pods=2
25s Normal SuccessfulCreate replicaset/web-96d5df5c8 Created pod: web-96d5df5c8-bkzmd
34s Normal ScalingReplicaSet deployment/web Scaled up replica set web-96d5df5c8 to 3
# 超过配额,创建失败
# 修改配额 pod数量为10
root@master30:~# kubectl patch resourcequotas myquota -p '{"spec":{"hard":{"pods":10}}}'
# 此时重新扩展rs
root@master30:~# kubectl scale rs web-96d5df5c8 --replicas 3
# 再次验证pod数量
root@master30:~# kubectl get pods
NAME READY STATUS RESTARTS AGE
pod/web-96d5df5c8-ajcz2 1/1 Running 0 2s
pod/web-96d5df5c8-dl7qk 1/1 Running 0 60s
pod/web-96d5df5c8-j7fhh 1/1 Running 0 60s
# 清理环境
root@master30:~# kubectl delete deployments.apps web
root@master30:~# kubectl delete resourcequotas myquota
思考题:如果一个用户可以管理多个 namespace,能否限定该用户的总配额?(提示:ResourceQuota 只作用于单个 namespace,跨 ns 需要自建计费/管控体系。)
十、Request 与 Limits:Pod 的资源契约
bash
pod.containers.resources
├── requests:Pod 运行需要的**最少**计算资源,调度器据此查找资源充足的节点
└── limits :Pod 能获得的**最多**计算资源,节点用 Linux cgroup 强制限制
10.1 实验:启用了计算资源配额却不写 resources
bash
root@master30:~# vim resourcequota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: myquota
spec:
hard:
persistentvolumeclaims: "10"
pods: "2"
secrets: "5"
services: "3"
requests.cpu: 1000m
requests.memory: "2048Mi"
limits.cpu: 1000m
limits.memory: "2048Mi"
root@master30:~# kubectl apply -f resourcequota.yaml
创建不带 resources 的 Pod:
bash
root@master30:~# vim pod-without-quota.yaml
apiVersion: v1
kind: Pod
metadata:
name: web
labels:
app: web
spec:
containers:
- name: web
image: httpd
imagePullPolicy: IfNotPresent
ports:
- name: web
containerPort: 80
protocol: TCP
root@master30:~# kubectl apply -f pod-without-quota.yaml
Error from server (Forbidden): error when creating "pod-without-quota.yaml": pods "web" is forbidden: failed quota: myquota: must specify limits.cpu for: web; limits.memory for: web; requests.cpu for: web; requests.memory for: web
# 清理环境
root@master30:~# kubectl delete resourcequotas myquota
10.2 实验:requests 超上限 / 未超上限
bash
root@master30:~# vim resourcequota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: myquota
spec:
hard:
requests.cpu: 1000m
requests.memory: "2048Mi"
root@master30:~# kubectl apply -f resourcequota.yaml
示例 1:超上限 → 拒绝
bash
root@master30:~# vim pod-request-1.yaml
apiVersion: v1
kind: Pod
metadata:
name: web
labels:
app: web
spec:
containers:
- name: web
image: hub.laoma.cloud/library/httpd
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 2000m
memory: 4096Mi
ports:
- name: web
containerPort: 80
protocol: TCP
root@master30:~# kubectl apply -f pod-request-1.yaml
Error from server (Forbidden): error when creating "pod-request-1.yaml": pods "web" is forbidden: exceeded quota: myquota, requested: requests.cpu=2,requests.memory=4Gi, used: requests.cpu=0,requests.memory=0, limited: requests.cpu=1,requests.memory=2Gi
示例 2:未超上限 → 成功,且能看到实时用量
bash
root@master30:~# vim pod-request-2.yaml
apiVersion: v1
kind: Pod
metadata:
name: web
labels:
app: web
spec:
containers:
- name: web
image: hub.laoma.cloud/library/httpd
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 200m
memory: 1024Mi
ports:
- name: web
containerPort: 80
protocol: TCP
root@master30:~# kubectl apply -f pod-request-2.yaml
root@master30:~# kubectl get pod
NAME READY STATUS RESTARTS AGE
web 1/1 Running 0 43s
# 验证使用情况
root@master30:~# kubectl describe resourcequotas myquota
Name: myquota
Namespace: quota
Resource Used Hard
-------- ---- ----
requests.cpu 200m 1
requests.memory 1Gi 2Gi
# 删除pod和quota
root@master30:~# kubectl delete pod web
root@master30:~# kubectl delete resourcequotas myquota
10.3 压测工具:progrium/stress
bash
Usage: stress [OPTION [ARG]] ...
-?, --help show this help statement
--version show version statement
-v, --verbose be verbose
-q, --quiet be quiet
-n, --dry-run show what would have been done
-t, --timeout N timeout after N seconds
--backoff N wait factor of N microseconds before work starts
-c, --cpu N spawn N workers spinning on sqrt()
-i, --io N spawn N workers spinning on sync()
-m, --vm N spawn N workers spinning on malloc()/free()
--vm-bytes B malloc B bytes per vm worker (default is 256MB)
--vm-stride B touch a byte every B bytes (default is 4096)
--vm-hang N sleep N secs before free (default none, 0 is inf)
--vm-keep redirty memory instead of freeing and reallocating
-d, --hdd N spawn N workers spinning on write()/unlink()
--hdd-bytes B write B bytes per hdd worker (default is 1GB)
Example: stress --cpu 8 --io 4 --vm 2 --vm-bytes 128M --timeout 10s
Note: Numbers may be suffixed with s,m,h,d,y (time) or B,K,M,G (size).
bash
# 压力测试内存
$ docker run --name stress hub.laoma.cloud/progrium/stress -m 1 --vm-bytes 512M
# 压力测试CPU
$ docker run --name stress hub.laoma.cloud/progrium/stress -c 1
# 压力测试IO
$ docker run --name stress hub.laoma.cloud/progrium/stress --d 1 --hdd-bytes 3G
Pod 中用法:
bash
apiVersion: v1
kind: Pod
metadata:
name: stress
spec:
containers:
- name: stress
image: hub.laoma.cloud/progrium/stress
imagePullPolicy: IfNotPresent
command: ['sh','-c','sleep 3600']
# 或者不用command,而是使用args作为参数传递给镜像的Entrypoint。
#args: ['-m','1','--vm-bytes','512M']
#args: ['-c','1']
#args: ['-d','1','--hdd-bytes','3G']
10.4 实验:CPU limit → 限流不死
bash
root@master30:~# vim resourcequota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: myquota
spec:
hard:
limits.cpu: 1000m
limits.memory: "2048Mi"
root@master30:~# kubectl apply -f resourcequota.yaml
# pod 示例
root@master30:~# vim pod-limit-cpu.yaml
apiVersion: v1
kind: Pod
metadata:
name: stress
spec:
containers:
- name: stress
image: hub.laoma.cloud/progrium/stress
imagePullPolicy: IfNotPresent
args: ['-c','1']
resources:
limits:
cpu: 200m
memory: "256Mi"
root@master30:~# kubectl apply -f pod-limit-cpu.yaml
另一个终端监控(需提前部署 Metrics-Server):
bash
root@master30:~# kubectl top pods
NAME CPU(cores) MEMORY(bytes)
stress 201m 0Mi
结论 :CPU 使用率稳定维持在 200m 左右------被限住但没死。
bash
# 删除 pod
root@master30:~# kubectl delete pod stress --force
10.5 实验:Memory limit → 直接 OOMKilled
bash
root@master30:~# vim pod-limit-memory.yaml
apiVersion: v1
kind: Pod
metadata:
name: stress
spec:
containers:
- name: stress
image: hub.laoma.cloud/progrium/stress
imagePullPolicy: IfNotPresent
args: ['-m','1','--vm-bytes','512M']
resources:
limits:
cpu: 200m
memory: "256Mi"
root@master30:~# kubectl apply -f pod-limit-memory.yaml
bash
root@master30:~# kubectl get pods -w
NAME READY STATUS RESTARTS AGE
stress 0/1 OOMKilled 1 (2s ago) 3s
stress 0/1 CrashLoopBackOff 1 (2s ago) 4s
结论 :申请 512M 但 limit 只有 256M → 状态 OOMKilled 并不断重启。
bash
# 删除 pod
root@master30:~# kubectl delete pod stress --force
root@master30:~# kubectl delete resourcequotas myquota
10.6 经典面试题:limits 总和会超过节点资源吗?
答案:可能会。
假设节点可用内存 1G,每个 Pod requests=256M / limit=512M。节点按 requests 算大约能放 4 个 Pod,而此时 limits 总和已经是 2G。
那 K8s 怎么处理资源耗尽?
- CPU :K8s 认为 CPU 是可压缩资源 ,容器达到 limit 时只减少其调度时间(变慢),不会杀死应用
- Memory :K8s 认为内存是不可压缩资源 ,超限时会杀死占用超过 request 的应用(1.9 版本之后)。驱逐顺序:先杀没指定 request 的容器 → 再杀超出 request 更多的容器 → 同等条件下优先级更低的先死
十一、LimitRange:给"忘写 resources"的 Pod 兜底
K8s 创建 Pod 时默认不指定资源请求和限制;但 namespace 一旦设了配额,不带 resources 的 Pod 又创建不了。LimitRange (别名 limits)就是来解决这个矛盾的:为单个 Pod 定义资源请求/限制的 default、min、max 值。
LimitRange 作用于特定 namespace。
规则:
- 请求的 Pod 未提供计算资源 → 用 default 值创建
- 请求的资源小于 min → 无法创建
- 请求的资源大于 max → 无法创建
11.1 定义 LimitRange
bash
root@master30:~# vim limits.yaml
apiVersion: v1
kind: LimitRange
metadata:
name: mylimit
spec:
limits:
- type: Container
max:
memory: 1024Mi
cpu: 1
min:
memory: 128Mi
cpu: 100m
default:
memory: 512Mi
cpu: 500m
defaultRequest:
memory: 256Mi
cpu: 200m
字段说明:
name:只能用小写字母、数字、-和.,且必须以数字或字母开头结尾default:配置 resourceQuota 时,创建 container 的默认 limitdefaultRequest:创建 container 的默认 requestmax:该 namespace 下 container 的资源最大值min:该 namespace 下 container 的资源最小值
约束关系 :
min <= defaultRequest <= default <= max
bash
root@master30:~# kubectl apply -f limits.yaml
root@master30:~# kubectl get limitranges
NAME CREATED AT
mylimit 2021-09-09T04:09:15Z
root@master30:~# kubectl describe limitranges mylimit
Name: mylimit
Namespace: quota
Type Resource Min Max Default Request Default Limit Max Limit/Request Ratio
---- -------- --- --- --------------- ------------- -----------------------
Container cpu 100m 1 200m 500m -
Container memory 128Mi 1Gi 256Mi 512Mi -
11.2 情况一:完全不写 resources → 自动补默认值
bash
root@master30:~# vim pod-without-limits.yaml
apiVersion: v1
kind: Pod
metadata:
name: stress
spec:
containers:
- name: stress
image: hub.laoma.cloud/progrium/stress
imagePullPolicy: IfNotPresent
args: ['-c','1']
root@master30:~# kubectl apply -f pod-without-limits.yaml
root@master30:~# kubectl top pods
NAME CPU(cores) MEMORY(bytes)
stress 501m 0Mi
root@master30:~# kubectl get pod stress -o yaml
......
spec:
containers:
image: hub.laoma.cloud/progrium/stress
imagePullPolicy: IfNotPresent
name: stress
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 200m
memory: 256Mi
......
# 清理资源
root@master30:~# kubectl delete limitranges mylimit
root@master30:~# kubectl delete pod web --force
11.3 情况二:只指定 limit
示例 2-1:limit > max → 拒绝
bash
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: web
image: hub.laoma.cloud/library/nginx
resources:
limits:
cpu: 1.1
memory: 1100Mi
root@master30:~# kubectl apply -f limit.yml
Error from server (Forbidden): error when creating "limit.yml": pods "web" is forbidden: [maximum cpu usage per Container is 1, but limit is 1100m, maximum memory usage per Container is 1Gi, but limit is 1181116006400m]
示例 2-2:limit < min → 拒绝
bash
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: web
image: hub.laoma.cloud/library/nginx
resources:
limits:
cpu: 60m
memory: 60Mi
root@master30:~# kubectl apply -f limit.yml
Error from server (Forbidden): error when creating "limit.yml": pods "web" is forbidden: [minimum cpu usage per Container is 100m, but request is 60m, minimum memory usage per Container is 128Mi, but request is 60Mi]
示例 2-3:min < limit < max → 成功,但有个大坑
bash
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: web
image: hub.laoma.cloud/library/nginx
resources:
limits:
cpu: 600m
memory: 600Mi
root@master30:~# kubectl get pod web -o yaml
......
spec:
containers:
- image: hub.laoma.cloud/library/nginx
imagePullPolicy: Always
name: web
resources:
limits:
cpu: 600m
memory: 600Mi
requests:
cpu: 600m
memory: 600Mi
......
✍️ 结论① :创建的容器 limits 必须满足
min < limit < max✍️ 结论② :只指定 limits 时,requests 会自动等于 limits(而不是取 defaultRequest!)
11.4 情况三:只指定 requests
示例 3-1:requests > max → 校验直接失败
bash
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: web
image: hub.laoma.cloud/library/nginx
resources:
requests:
cpu: 1600m
memory: 600Mi
root@master30:~# kubectl apply -f limit4.yml
The Pod "web" is invalid:
* spec.containers[0].resources.requests: Invalid value: "1600m": must be less than or equal to cpu limit
* spec.containers[0].resources.requests: Invalid value: "1600Mi": must be less than or equal to memory limit
示例 3-2:requests < min → 拒绝
bash
root@master30:~# kubectl apply -f limit.yml
Error from server (Forbidden): error when creating "limit.yml": pods "web" is forbidden: [minimum cpu usage per Container is 100m, but request is 60m, minimum memory usage per Container is 128Mi, but request is 60Mi]
示例 3-3:min < requests < max → 成功
bash
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: web
image: hub.laoma.cloud/library/nginx
resources:
requests:
cpu: 400m
memory: 400Mi
root@master30:~# kubectl get pod web -o yaml
......
spec:
containers:
- image: hub.laoma.cloud/library/nginx
imagePullPolicy: Always
name: web
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 400m
memory: 400Mi
......
✍️ 结论① :requests 必须满足
min < requests < limits✍️ 结论② :只指定 requests 时,limits 取 default 值
11.5 一张表记住 LimitRange 的填充规则
| 你写的内容 | 系统自动补的部分 |
|---|---|
| 什么都不写 | limits = default,requests = defaultRequest |
| 只写 limits | requests = limits(⚠️ 不是 defaultRequest) |
| 只写 requests | limits = default |
11.6 LimitRange 能限定哪些资源
| Type | 资源 | 说明 |
|---|---|---|
| container | cpu、memory | 限定单个容器 |
| Pod | cpu、memory | 限定 Pod 中所有容器的总和 |
| PVC | storage | 限定 PVC 申请的存储空间大小 |
bash
apiVersion: v1
kind: LimitRange
metadata:
name: storagelimits
spec:
limits:
- type: PersistentVolumeClaim
max:
storage: 2Gi
min:
storage: 1Gi
bash
# 环境清理
root@master30:~# kubectl delete ns quota
十二、全文总结:一张图 + 六条经验
┌──────────────── Pod 是怎么跑起来的 ────────────────┐
│ Client → apiserver(认证/授权/准入) → etcd │
│ → controller-manager(控制器补齐 Pod 数量) │
│ → scheduler(过滤 → 抢占 → 打分 → 绑定) │
│ → kubelet(CNI 网络 / CSI 存储) │
│ → containerd(拉镜像 / 起容器) │
│ → 状态回写 Running │
└───────────────────────────────────────────────────┘
┌───────────── 调度约束(Pod 选节点 vs 节点选 Pod)─────────────┐
│ Pod 视角:nodeName / nodeSelector / nodeAffinity │
│ podAffinity / podAntiAffinity(topologyKey 拓扑域) │
│ Node 视角:Taint 污点 + Toleration 容忍(专用节点/GPU 节点) │
│ 节点维护:cordon(关门)/ drain(关门+清人)/ uncordon │
└───────────────────────────────────────────────────────────────┘
┌───────────── 资源管控(限总量 / 兜底)─────────────┐
│ ResourceQuota:namespace 级总量硬限制(对象数 + 计算资源) │
│ LimitRange:单容器默认值 + 上下限兜底 │
│ requests:调度依据 limits:cgroup 硬限制 │
│ CPU 可压缩(限流) 内存不可压缩(OOMKilled) │
└────────────────────────────────────────────────────┘
┌───────────── 弹性伸缩(动态容量)─────────────────┐
│ Metrics-Server:指标采集(kubectl top 的数据源) │
│ HPA:横向扩缩副本数(CPU/内存/自定义指标) │
│ behavior.scaleUp / scaleDown:扩缩节奏精细控制 │
│ VPA:纵向调 requests/limits(需重启,较少用) │
└───────────────────────────────────────────────────┘
六条生产经验:
- Pod 一直 Pending → 先
kubectl describe pod看 Events,对照「过滤 Predicate」表定位(资源不足 / 污点未容忍 / 亲和规则无解 / 节点被 cordon) - 高可用服务标配 :
podAntiAffinity+topologyKey: kubernetes.io/hostname(每节点一副本),且副本数别超过节点数 - 亲和 + 反亲和配置时要取交集,别让结果变成空集(实验 3 的 Pending 就是这么来的)
- "只跑在某类节点"必须污点 + 节点亲和性双管齐下,单靠 toleration 只拿到准入资格
- HPA 生效前提 :部署 Metrics-Server 且 Pod 必须设置 limits,否则 TARGETS 永远
<unknown> - 多租户集群 :
ResourceQuota(管总量) +LimitRange(给默认值)组合使用最省心