九、存储管理:数据持久化的艺术
1. 为什么Kubernetes需要存储管理
1.1 容器数据的”短命”问题
容器的本质特性是临时的,删除后数据全部丢失。这对于有状态应用(如数据库)是致命的。
演示问题:
# 启动MySQL,写入数据kubectl run mysql --image=mysql:8.0 --env="MYSQL_ROOT_PASSWORD=pass"kubectl exec mysql -- mysql -uroot -ppass -e "CREATE DATABASE testdb;"
# 删除Podkubectl delete pod mysql
# 重新创建,数据消失kubectl run mysql --image=mysql:8.0 --env="MYSQL_ROOT_PASSWORD=pass"kubectl exec mysql -- mysql -uroot -ppass -e "USE testdb;"# 报错:Database doesn't existKubernetes的存储挑战:
- Pod漂移:Pod可能在不同节点重启
- 多副本共享:多个Pod需访问同一数据
- 生命周期管理:存储生命周期独立于Pod
1.2 存储架构总览
Kubernetes通过四层架构解决存储问题:
四大组件:
| 组件 | 角色 | 说明 |
|---|---|---|
| Volume | 最基础 | Pod内定义,生命周期绑定Pod |
| PV | 存储资源 | 集群级,代表真实存储空间 |
| PVC | 存储申请 | 用户申请,自动匹配PV |
| StorageClass | 自动化 | 动态创建PV,存储分类 |
静态 vs 动态供给:
静态供给:管理员手动创建PV → 用户创建PVC → 自动绑定
动态供给:用户创建PVC(指定StorageClass)→ 自动创建PV → 自动绑定2. Volume:最基础的存储
2.1 emptyDir:临时共享存储
emptyDir 是最简单的Volume,Pod创建时自动创建,Pod删除时数据丢失。
使用场景:
- 容器间数据共享
- 临时缓存
- 计算中间结果
示例:
apiVersion: v1kind: Podmetadata: name: emptydir-demospec: containers: - name: writer image: busybox command: ['sh', '-c', 'echo "Hello" > /data/msg.txt; sleep 3600'] volumeMounts: - name: shared-data mountPath: /data
- name: reader image: busybox command: ['sh', '-c', 'while true; do cat /data/msg.txt 2>/dev/null; sleep 5; done'] volumeMounts: - name: shared-data mountPath: /data
volumes: - name: shared-data emptyDir: {} # 使用磁盘 # emptyDir: # medium: Memory # 使用内存(更快)2.2 hostPath:挂载宿主机目录
hostPath 将宿主机目录挂载到Pod。
⚠️ 注意:
- 不同节点路径可能不同
- 有安全风险
- 不推荐生产环境
示例:
apiVersion: v1kind: Podmetadata: name: hostpath-demospec: containers: - name: nginx image: nginx:1.20 volumeMounts: - name: host-data mountPath: /usr/share/nginx/html
volumes: - name: host-data hostPath: path: /data/nginx type: DirectoryOrCreatehostPath类型:
| type | 说明 |
|---|---|
| DirectoryOrCreate | 目录不存在则创建 |
| Directory | 必须存在的目录 |
| File | 必须存在的文件 |
3. PersistentVolume(PV):持久化存储资源
3.1 PV核心概念
PV 是集群级别的存储资源,由管理员创建或StorageClass动态创建。
特点:
- 独立于Pod生命周期
- 集群级资源(无namespace)
- 代表真实存储空间
PV生命周期:
Available(可用)→ Bound(已绑定)→ Released(已释放)→ Failed(失败)3.2 PV配置详解
apiVersion: v1kind: PersistentVolumemetadata: name: pv-nfs-001spec: capacity: storage: 10Gi
accessModes: - ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: nfs-storage
nfs: server: 192.168.100.14 path: /data/nfs/pv-0013.3 访问模式(AccessModes)
| 模式 | 缩写 | 说明 | 适用场景 |
|---|---|---|---|
| ReadWriteOnce | RWO | 单节点读写 | 数据库 |
| ReadOnlyMany | ROX | 多节点只读 | 静态资源 |
| ReadWriteMany | RWX | 多节点读写 | 共享文件系统 |
不同存储支持的模式:
| 存储类型 | RWO | ROX | RWX |
|---|---|---|---|
| hostPath | ✅ | ❌ | ❌ |
| NFS | ✅ | ✅ | ✅ |
| Ceph RBD | ✅ | ✅ | ❌ |
| 云盘 | ✅ | ❌ | ❌ |
3.4 回收策略(ReclaimPolicy)
| 策略 | 说明 | 适用场景 |
|---|---|---|
| Retain | 保留数据,需手动清理 | 生产环境 |
| Delete | 自动删除PV和数据 | 动态供给 |
| Recycle | 删除数据(已废弃) | 不推荐 |
4. PersistentVolumeClaim(PVC):存储申请
4.1 PVC核心概念
PVC 是用户对存储的申请,类似于Pod对CPU的申请。
特点:
- 命名空间级资源
- 用户无需知道底层存储细节
- K8s自动匹配PV
4.2 PVC配置
apiVersion: v1kind: PersistentVolumeClaimmetadata: name: mysql-pvc namespace: defaultspec: accessModes: - ReadWriteOnce
resources: requests: storage: 10Gi
storageClassName: nfs-storage4.3 PVC绑定规则
K8s如何选择PV?
匹配条件(全部满足):1. accessModes 匹配2. storage大小满足(PV >= PVC)3. storageClassName 匹配
优先级:1. 大小精确匹配2. 最小满足(PV略大于PVC)4.4 Pod使用PVC
apiVersion: v1kind: Podmetadata: name: mysql-podspec: containers: - name: mysql image: mysql:8.0 volumeMounts: - name: mysql-data mountPath: /var/lib/mysql
volumes: - name: mysql-data persistentVolumeClaim: claimName: mysql-pvc5. StorageClass:自动化存储供应
5.1 StorageClass是什么
StorageClass 是存储的”自动售货机”,用户申请存储时自动创建PV。
功能:
- 定义存储类型和参数
- 自动创建PV(动态供给)
- 不同性能等级(SSD/HDD)
5.2 StorageClass配置
apiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: nfs-storageprovisioner: nfs-provisionerparameters: archiveOnDelete: "false"reclaimPolicy: DeletevolumeBindingMode: Immediate关键字段:
| 字段 | 说明 |
|---|---|
| provisioner | 存储供应器(如nfs-provisioner) |
| parameters | 供应器特定参数 |
| reclaimPolicy | PV回收策略 |
| volumeBindingMode | 绑定模式(Immediate/WaitForFirstConsumer) |
5.3 设置默认StorageClass
apiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: nfs-storage annotations: storageclass.kubernetes.io/is-default-class: "true"provisioner: nfs-provisioner# 查看默认StorageClasskubectl get storageclass# NAME PROVISIONER AGE# nfs-storage (default) nfs-provisioner 1d6. 主流后端存储对比
后端存储是PV真正存储数据的地方,选择合适的存储方案至关重要。
6.1 存储方案对比
| 存储类型 | 性能 | 可靠性 | 复杂度 | 成本 | 访问模式 | 适用场景 |
|---|---|---|---|---|---|---|
| hostPath | ⭐⭐⭐ | ⭐ | ⭐ | 免费 | RWO | 开发测试 |
| NFS | ⭐⭐ | ⭐⭐ | ⭐⭐ | 低 | RWO/ROX/RWX | 开发环境、小规模生产 |
| Ceph/Rook | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | 中 | RWO/ROX/RWX | 企业级生产环境 |
| 云盘(EBS) | ⭐⭐⭐ | ⭐⭐⭐ | ⭐ | 高 | RWO | 云环境生产 |
| 对象存储(S3) | ⭐⭐ | ⭐⭐⭐ | ⭐ | 中 | ROX | 静态资源、备份 |
6.2 详细分析
1. hostPath(宿主机目录)
优点:✅ 简单直接,无需额外配置✅ 性能最好(本地磁盘)✅ 零成本
缺点:❌ 数据绑定节点,Pod漂移会丢失数据❌ 无法多节点共享❌ 安全风险高
适用场景:- 开发测试- 单节点集群- DaemonSet日志收集2. NFS(网络文件系统)
优点:✅ 支持RWX多节点读写✅ 配置简单✅ 成本低
缺点:❌ 性能一般(网络IO)❌ 单点故障风险(NFS Server挂了全挂)❌ 不适合高并发
适用场景:- 开发环境- 小规模生产(非核心业务)- 共享配置文件3. Ceph/Rook(分布式存储)
优点:✅ 高可用(数据多副本)✅ 可扩展(横向扩展)✅ 性能好✅ 支持RWX
缺点:❌ 部署复杂❌ 需要专业运维❌ 资源消耗大(至少3节点)
适用场景:- 企业级生产环境- 大规模集群- 对数据可靠性要求高4. 云盘(EBS/GCE Disk)
优点:✅ 高可用(云厂商保障)✅ 易用(云平台集成)✅ 性能可选(SSD/HDD)✅ 快照备份方便
缺点:❌ 只支持RWO❌ 成本较高❌ 绑定云厂商
适用场景:- 云环境生产- 数据库存储- 单实例应用5. 对象存储(S3/OSS)
优点:✅ 无限容量✅ 高可用✅ 成本低(按量付费)
缺点:❌ 不支持POSIX文件系统❌ 只适合读多写少场景
适用场景:- 静态资源(图片、视频)- 备份归档- 大数据存储6.3 选择建议
⚠️ 生产环境建议:
1. 能用云存储就用云存储(省心)2. 自建存储需要专业运维团队3. 数据库等核心应用使用高可用存储4. 定期备份,备份,备份!7. 实战1:使用hostPath实现持久化
7.1 实战目标
使用hostPath验证数据持久化,理解存储的基本原理。
场景: 创建Nginx Pod,使用hostPath存储网页,验证Pod删除后数据是否保留。
7.2 准备工作
# 在node1节点创建目录ssh root@192.168.100.21mkdir -p /data/nginx-htmlecho "<h1>Hello from hostPath - v1</h1>" > /data/nginx-html/index.html7.3 创建使用hostPath的Pod
hostpath-nginx.yaml:
apiVersion: v1kind: Podmetadata: name: nginx-hostpath labels: app: nginxspec: # 指定调度到node1(hostPath绑定节点) nodeSelector: kubernetes.io/hostname: k8s-node1
containers: - name: nginx image: nginx:1.20 ports: - containerPort: 80 volumeMounts: - name: html-data mountPath: /usr/share/nginx/html
volumes: - name: html-data hostPath: path: /data/nginx-html type: Directory---apiVersion: v1kind: Servicemetadata: name: nginx-hostpathspec: selector: app: nginx ports: - port: 80 nodePort: 30090 type: NodePortkubectl apply -f hostpath-nginx.yaml7.4 测试数据持久化
# 1. 访问Nginxcurl http://192.168.100.21:30090# 输出:<h1>Hello from hostPath - v1</h1>
# 2. 在容器内修改文件kubectl exec nginx-hostpath -- sh -c 'echo "<h1>Modified in container - v2</h1>" > /usr/share/nginx/html/index.html'
# 3. 再次访问,看到更新curl http://192.168.100.21:30090# 输出:<h1>Modified in container - v2</h1>
# 4. 删除Podkubectl delete pod nginx-hostpath
# 5. 重新创建Podkubectl apply -f hostpath-nginx.yaml
# 6. 数据还在!curl http://192.168.100.21:30090# 输出:<h1>Modified in container - v2</h1>
# 7. 在宿主机上验证ssh root@192.168.100.21 "cat /data/nginx-html/index.html"# 输出:<h1>Modified in container - v2</h1>⚠️ 测试Pod漂移问题:
# 删除nodeSelector,让Pod可以调度到任意节点kubectl delete -f hostpath-nginx.yaml
# 修改YAML,去掉nodeSelector# 重新创建
# 如果Pod调度到node2,会发现访问失败# 因为node2的/data/nginx-html目录不存在或为空结论:
- ✅ 数据持久化成功
- ❌ 但数据绑定节点,不适合生产环境
8. 实战2:使用NFS实现动态供给
8.1 实战目标
部署NFS服务器和NFS Provisioner,实现PVC自动创建PV的动态供给。
8.2 部署NFS服务器
在harbor机器(192.168.100.14)上安装NFS:
# 安装NFS服务yum install -y nfs-utils rpcbind
# 创建共享目录mkdir -p /data/nfs-storagechmod 777 /data/nfs-storage
# 配置NFS共享cat >> /etc/exports << EOF/data/nfs-storage *(rw,sync,no_root_squash,no_all_squash)EOF
# 启动NFS服务systemctl start rpcbindsystemctl start nfs-serversystemctl enable rpcbindsystemctl enable nfs-server
# 刷新NFS配置exportfs -r
# 查看共享目录showmount -e localhost# 输出:/data/nfs-storage *在所有K8s节点安装NFS客户端:
# 在master、node1、node2上执行yum install -y nfs-utils
# 测试挂载mount -t nfs 192.168.100.14:/data/nfs-storage /mntls /mntumount /mnt8.3 部署NFS Provisioner
创建nfs-provisioner.yaml:
# RBAC权限apiVersion: v1kind: ServiceAccountmetadata: name: nfs-provisioner namespace: kube-system---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: name: nfs-provisionerrules:- apiGroups: [""] resources: ["persistentvolumes"] verbs: ["get", "list", "watch", "create", "delete"]- apiGroups: [""] resources: ["persistentvolumeclaims"] verbs: ["get", "list", "watch", "update"]- apiGroups: ["storage.k8s.io"] resources: ["storageclasses"] verbs: ["get", "list", "watch"]- apiGroups: [""] resources: ["events"] verbs: ["create", "update", "patch"]---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: nfs-provisionerroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: nfs-provisionersubjects:- kind: ServiceAccount name: nfs-provisioner namespace: kube-system---# DeploymentapiVersion: apps/v1kind: Deploymentmetadata: name: nfs-provisioner namespace: kube-systemspec: replicas: 1 selector: matchLabels: app: nfs-provisioner template: metadata: labels: app: nfs-provisioner spec: serviceAccountName: nfs-provisioner containers: - name: nfs-provisioner image: registry.cn-hangzhou.aliyuncs.com/open-ali/nfs-client-provisioner:latest volumeMounts: - name: nfs-client-root mountPath: /persistentvolumes env: - name: PROVISIONER_NAME value: nfs-provisioner # Provisioner名称 - name: NFS_SERVER value: 192.168.100.14 # NFS服务器地址 - name: NFS_PATH value: /data/nfs-storage # NFS共享路径 volumes: - name: nfs-client-root nfs: server: 192.168.100.14 path: /data/nfs-storage---# StorageClassapiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: nfs-storage annotations: storageclass.kubernetes.io/is-default-class: "true" # 设为默认provisioner: nfs-provisionerparameters: archiveOnDelete: "false" # 删除PVC时不归档数据reclaimPolicy: DeletevolumeBindingMode: Immediatekubectl apply -f nfs-provisioner.yaml
# 查看Provisioner状态kubectl get pods -n kube-system -l app=nfs-provisioner
# 查看StorageClasskubectl get storageclass# NAME PROVISIONER RECLAIMPOLICY# nfs-storage (default) nfs-provisioner Delete8.4 测试动态供给
创建test-pvc.yaml:
apiVersion: v1kind: PersistentVolumeClaimmetadata: name: test-pvcspec: accessModes: - ReadWriteMany resources: requests: storage: 1Gi storageClassName: nfs-storagekubectl apply -f test-pvc.yaml
# 查看PVC(自动绑定)kubectl get pvc test-pvc# NAME STATUS VOLUME CAPACITY# test-pvc Bound pvc-abc123-... 1Gi
# 查看自动创建的PVkubectl get pv# NAME CAPACITY ACCESS MODES STATUS# pvc-abc123-... 1Gi RWX Bound
# 在NFS服务器上查看自动创建的目录ssh root@192.168.100.14 "ls -l /data/nfs-storage/"# drwxrwxrwx 2 root root 6 Jan 15 10:30 default-test-pvc-pvc-abc123...创建Pod使用PVC:
apiVersion: v1kind: Podmetadata: name: test-nfs-podspec: containers: - name: app image: nginx:1.20 volumeMounts: - name: data mountPath: /usr/share/nginx/html volumes: - name: data persistentVolumeClaim: claimName: test-pvckubectl apply -f test-nfs-pod.yaml
# 写入测试数据kubectl exec test-nfs-pod -- sh -c 'echo "<h1>NFS Dynamic Provisioning Works!</h1>" > /usr/share/nginx/html/index.html'
# 在NFS服务器验证ssh root@192.168.100.14 "cat /data/nfs-storage/default-test-pvc-*/index.html"# 输出:<h1>NFS Dynamic Provisioning Works!</h1>
# 删除Pod,数据保留kubectl delete pod test-nfs-pod
# 重新创建,数据还在kubectl apply -f test-nfs-pod.yamlkubectl exec test-nfs-pod -- cat /usr/share/nginx/html/index.html8.5 测试回收策略
# 删除PVCkubectl delete pvc test-pvc
# PV自动删除(reclaimPolicy: Delete)kubectl get pv# 无输出(PV已删除)
# NFS服务器上的数据也被删除ssh root@192.168.100.14 "ls /data/nfs-storage/"# 空目录(数据已删除)9. 实战3:StatefulSet持久化MySQL
9.1 实战目标
使用StatefulSet部署MySQL集群,每个实例拥有独立的PVC,验证扩缩容时数据独立性。
9.2 创建StatefulSet MySQL
mysql-statefulset.yaml:
apiVersion: v1kind: Servicemetadata: name: mysql-headlessspec: clusterIP: None # Headless Service selector: app: mysql ports: - port: 3306---apiVersion: apps/v1kind: StatefulSetmetadata: name: mysqlspec: serviceName: mysql-headless replicas: 3 selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: containers: - name: mysql image: mysql:8.0 ports: - containerPort: 3306 env: - name: MYSQL_ROOT_PASSWORD value: "MyPass123" volumeMounts: - name: data mountPath: /var/lib/mysql
# VolumeClaimTemplate(每个Pod独立PVC) volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] storageClassName: nfs-storage resources: requests: storage: 5Gikubectl apply -f mysql-statefulset.yaml9.3 验证独立存储
# 查看Podkubectl get pods -l app=mysql# NAME READY STATUS RESTARTS AGE# mysql-0 1/1 Running 0 1m# mysql-1 1/1 Running 0 50s# mysql-2 1/1 Running 0 40s
# 查看PVC(每个Pod独立PVC)kubectl get pvc# NAME STATUS VOLUME CAPACITY# data-mysql-0 Bound pvc-abc... 5Gi# data-mysql-1 Bound pvc-def... 5Gi# data-mysql-2 Bound pvc-ghi... 5Gi
# 在mysql-0中创建数据库kubectl exec mysql-0 -- mysql -uroot -pMyPass123 -e "CREATE DATABASE db0;"
# 在mysql-1中创建不同的数据库kubectl exec mysql-1 -- mysql -uroot -pMyPass123 -e "CREATE DATABASE db1;"
# 验证数据独立kubectl exec mysql-0 -- mysql -uroot -pMyPass123 -e "SHOW DATABASES;" | grep db# db0
kubectl exec mysql-1 -- mysql -uroot -pMyPass123 -e "SHOW DATABASES;" | grep db# db1
# 数据完全独立!9.4 测试扩缩容
# 缩容到1个副本kubectl scale statefulset mysql --replicas=1
# 查看Pod(mysql-1和mysql-2被删除)kubectl get pods -l app=mysql# NAME READY STATUS RESTARTS AGE# mysql-0 1/1 Running 0 5m
# PVC不会被删除(数据保护)kubectl get pvc# NAME STATUS VOLUME CAPACITY# data-mysql-0 Bound pvc-abc... 5Gi# data-mysql-1 Bound pvc-def... 5Gi <- 保留# data-mysql-2 Bound pvc-ghi... 5Gi <- 保留
# 扩容回3个副本kubectl scale statefulset mysql --replicas=3
# 新Pod自动绑定原来的PVC,数据恢复!kubectl exec mysql-1 -- mysql -uroot -pMyPass123 -e "SHOW DATABASES;" | grep db# db1 <- 数据还在!总结
本章学习了:
-
存储架构
- Volume/PV/PVC/StorageClass关系
- 静态供给 vs 动态供给
-
存储类型
- Volume:emptyDir、hostPath
- PV:访问模式、回收策略
- PVC:绑定规则
- StorageClass:自动化供给
-
后端存储选择
- hostPath:开发测试
- NFS:小规模生产
- Ceph:企业级
- 云盘:云环境
-
实战经验
- hostPath持久化(单节点)
- NFS动态供给(多节点共享)
- StatefulSet独立存储
生产建议:
⚠️ 存储是有状态应用的生命线
1. 能用云存储就用云存储(省心)2. 自建存储需要专业团队运维3. 生产环境必须: - 定期备份 - 测试恢复流程 - 监控存储容量和性能4. 核心数据使用高可用存储(Ceph/云盘)5. 非核心数据可用NFS风险提示:
⚠️ 自建存储意味着对数据负全责⚠️ 存储故障 = 数据丢失 = 业务灾难⚠️ 优先考虑云存储或托管存储服务十、K8s调度:让Pod去该去的地方
1. 调度基础概念
1.1 什么是K8s调度
调度(Scheduling) 是Kubernetes的核心功能之一,决定Pod应该运行在哪个节点上。
用生活化的比喻来理解:
想象一个物流调度中心:
Pod = 货物(需要运送到某个仓库)Node = 仓库(存放货物的地方)Scheduler = 调度员(决定货物放到哪个仓库)
调度员需要考虑:- 仓库剩余空间(节点资源)- 货物特殊要求(需要冷藏?易碎?)- 距离和效率(网络延迟、亲和性)- 仓库限制(某些仓库不收危险品)调度的重要性:
为什么调度如此重要?
1. 资源利用率 - 合理分配Pod,避免某些节点过载 - 提高集群整体资源使用效率
2. 高可用性 - 将Pod分散到不同节点/机房 - 避免单点故障
3. 性能优化 - 将相关Pod调度到一起(减少网络延迟) - 将Pod调度到合适的硬件(GPU、SSD)
4. 合规要求 - 某些数据必须存储在特定区域 - 敏感服务只能运行在特定节点1.2 调度器工作原理
Scheduler的调度流程:
详细流程解析:
┌─────────────────────────────────────────────────────────────┐│ 用户创建Pod:kubectl apply -f pod.yaml │└─────────────────────────────────────────────────────────────┘ ↓┌─────────────────────────────────────────────────────────────┐│ API Server接收请求 ││ - Pod状态:Pending ││ - nodeName:空(未分配节点) │└─────────────────────────────────────────────────────────────┘ ↓┌─────────────────────────────────────────────────────────────┐│ Scheduler监听到新Pod ││ 开始调度流程 │└─────────────────────────────────────────────────────────────┘ ↓┌─────────────────────────────────────────────────────────────┐│ 预选阶段(Filtering) ││ ││ 检查每个节点是否满足Pod的硬性要求: ││ ✓ 资源充足?(CPU、内存) ││ ✓ 端口可用? ││ ✓ 节点选择器匹配?(nodeSelector) ││ ✓ 节点亲和性满足?(nodeAffinity required) ││ ✓ 污点能容忍?(Tolerations) ││ ✓ 其他约束满足? ││ ││ 结果:从10个节点中筛选出5个候选节点 │└─────────────────────────────────────────────────────────────┘ ↓┌─────────────────────────────────────────────────────────────┐│ 优选阶段(Scoring) ││ ││ 对候选节点打分(0-100分): ││ - 资源均衡度(LeastRequestedPriority) ││ - 节点亲和性偏好(NodeAffinityPriority) ││ - Pod亲和性偏好(InterPodAffinityPriority) ││ - 镜像已存在(ImageLocalityPriority) ││ - ... ││ ││ Node1: 85分 | Node2: 92分 | Node3: 78分 | ... │└─────────────────────────────────────────────────────────────┘ ↓┌─────────────────────────────────────────────────────────────┐│ 选择最高分节点:Node2(92分) ││ 绑定Pod到Node2 │└─────────────────────────────────────────────────────────────┘ ↓┌─────────────────────────────────────────────────────────────┐│ Node2上的Kubelet ││ - 监听到分配给自己的Pod ││ - 拉取镜像 ││ - 创建容器 ││ - Pod状态:Running │└─────────────────────────────────────────────────────────────┘1.3 调度策略全景图
K8s提供了多种调度策略,按照约束强度可以分为:
┌─────────────────────────────────────────────────────────────┐│ K8s调度策略全景图 │├─────────────────────────────────────────────────────────────┤│ ││ 【硬性约束】必须满足,否则Pod无法调度 ││ ┌─────────────────────────────────────────────────────┐ ││ │ • nodeSelector - 节点标签选择器 │ ││ │ • nodeName - 指定节点名称 │ ││ │ • nodeAffinity.required - 节点亲和性(硬性) │ ││ │ • podAffinity.required - Pod亲和性(硬性) │ ││ │ • Taints & Tolerations - 污点与容忍 │ ││ │ • 资源请求 - CPU/内存必须满足 │ ││ └─────────────────────────────────────────────────────┘ ││ ││ 【软性偏好】尽量满足,不满足也能调度 ││ ┌─────────────────────────────────────────────────────┐ ││ │ • nodeAffinity.preferred - 节点亲和性(软性) │ ││ │ • podAffinity.preferred - Pod亲和性(软性) │ ││ │ • podAntiAffinity - Pod反亲和性 │ ││ └─────────────────────────────────────────────────────┘ ││ ││ 【优先级调度】资源不足时的抢占机制 ││ ┌─────────────────────────────────────────────────────┐ ││ │ • PriorityClass - Pod优先级 │ ││ │ • Preemption - 抢占低优先级Pod │ ││ └─────────────────────────────────────────────────────┘ ││ │└─────────────────────────────────────────────────────────────┘策略优先级关系:
调度决策顺序(从高到低):
1. nodeName(最高优先级) ↓ 直接指定节点,绕过Scheduler2. Taints & Tolerations ↓ 节点排斥机制3. nodeSelector / nodeAffinity.required ↓ 硬性节点选择4. podAffinity/podAntiAffinity.required ↓ 硬性Pod亲和/反亲和5. nodeAffinity.preferred ↓ 软性节点偏好6. podAffinity/podAntiAffinity.preferred ↓ 软性Pod亲和/反亲和7. 资源均衡、镜像本地化等其他因素2. 节点选择机制
2.1 nodeSelector:最简单的节点选择
nodeSelector 是最简单的节点选择方式,通过标签匹配将Pod调度到特定节点。
使用场景:
场景1:将Pod调度到SSD节点 节点标签:disk-type=ssd Pod配置:nodeSelector: disk-type: ssd
场景2:将Pod调度到GPU节点 节点标签:gpu=nvidia Pod配置:nodeSelector: gpu: nvidia
场景3:将Pod调度到特定机房 节点标签:zone=beijing Pod配置:nodeSelector: zone: beijing配置示例:
apiVersion: v1kind: Podmetadata: name: nginx-ssdspec: nodeSelector: # 节点选择器 disk-type: ssd # 只调度到标签为disk-type=ssd的节点 containers: - name: nginx image: nginx:1.20节点标签管理:
# 查看节点标签kubectl get nodes --show-labels
# 为节点添加标签kubectl label nodes k8s-node1 disk-type=ssd
# 修改节点标签kubectl label nodes k8s-node1 disk-type=hdd --overwrite
# 删除节点标签kubectl label nodes k8s-node1 disk-type-
# 根据标签筛选节点kubectl get nodes -l disk-type=ssdnodeSelector的局限性:
✗ 只支持精确匹配(key=value)✗ 不支持"或"逻辑(disk-type=ssd 或 disk-type=nvme)✗ 不支持"非"逻辑(不要disk-type=hdd)✗ 不支持软性偏好(尽量选择,但不强制)
解决方案:使用nodeAffinity(节点亲和性)2.2 nodeAffinity:高级节点亲和性
nodeAffinity 是nodeSelector的增强版,提供更灵活的节点选择能力。
两种亲和性类型:
| 类型 | 说明 | 效果 |
|---|---|---|
| requiredDuringSchedulingIgnoredDuringExecution | 硬性要求 | 必须满足,否则不调度 |
| preferredDuringSchedulingIgnoredDuringExecution | 软性偏好 | 尽量满足,不满足也可调度 |
名称解析:
requiredDuringScheduling = 调度时必须满足preferred DuringScheduling = 调度时尽量满足IgnoredDuringExecution = 运行时忽略(Pod已运行后,即使条件不满足也不驱逐)支持的操作符:
| 操作符 | 说明 | 示例 |
|---|---|---|
| In | 值在列表中 | key In [v1, v2] |
| NotIn | 值不在列表中 | key NotIn [v1, v2] |
| Exists | 标签存在 | key Exists |
| DoesNotExist | 标签不存在 | key DoesNotExist |
| Gt | 大于(数值) | key Gt 5 |
| Lt | 小于(数值) | key Lt 10 |
配置示例1:硬性要求
apiVersion: v1kind: Podmetadata: name: nginx-affinity-requiredspec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: # 硬性要求 nodeSelectorTerms: - matchExpressions: - key: disk-type operator: In values: - ssd - nvme # disk-type=ssd 或 disk-type=nvme - key: zone operator: In values: - beijing # 同时 zone=beijing containers: - name: nginx image: nginx:1.20配置示例2:软性偏好
apiVersion: v1kind: Podmetadata: name: nginx-affinity-preferredspec: affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: # 软性偏好 - weight: 80 # 权重1-100,越高越优先 preference: matchExpressions: - key: disk-type operator: In values: - ssd - weight: 20 # 权重较低 preference: matchExpressions: - key: zone operator: In values: - beijing containers: - name: nginx image: nginx:1.20配置示例3:硬性+软性组合
apiVersion: v1kind: Podmetadata: name: nginx-affinity-combospec: affinity: nodeAffinity: # 硬性要求:必须在beijing或shanghai requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: zone operator: In values: - beijing - shanghai # 软性偏好:尽量选择SSD节点 preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: disk-type operator: In values: - ssd containers: - name: nginx image: nginx:1.202.3 nodeName:直接指定节点
nodeName 直接指定Pod运行的节点名称,绕过Scheduler。
apiVersion: v1kind: Podmetadata: name: nginx-specific-nodespec: nodeName: k8s-node1 # 直接指定节点名称 containers: - name: nginx image: nginx:1.20⚠️ 使用nodeName的风险:
✗ 绕过Scheduler,不检查资源是否充足✗ 绕过Taints检查,可能调度到不应该去的节点✗ 节点不存在或不可用时,Pod一直Pending✗ 不推荐在生产环境使用
适用场景:✓ 调试和测试✓ DaemonSet(每个节点运行一个Pod)✓ 静态Pod3. 污点与容忍(Taints & Tolerations)
3.1 什么是污点和容忍
污点(Taint) 是节点的属性,用于排斥Pod。 容忍(Toleration) 是Pod的属性,用于容忍节点的污点。
生活化比喻:
想象一个公寓楼的租房场景:
节点(Node) = 公寓房间污点(Taint) = 房间的"缺点标签"(如:靠马路吵、没有电梯、只租给程序员)Pod = 租客容忍(Toleration) = 租客能接受的"缺点"
场景1:房间标签"靠马路=吵:NoSchedule" → 普通租客不愿意住(Pod不会调度) → 能忍受噪音的租客可以住(Pod配置了对应Toleration)
场景2:Master节点的污点"node-role.kubernetes.io/control-plane:NoSchedule" → 普通Pod不会调度到Master → 系统组件(如CoreDNS)配置了容忍,可以运行在Master上工作原理图:
┌─────────────────────────────────────────────────────────────┐│ 节点有污点 + Pod没有对应容忍 = Pod不会调度到该节点 ││ 节点有污点 + Pod有对应容忍 = Pod可以调度到该节点 ││ 节点没有污点 = Pod可以调度到该节点 │└─────────────────────────────────────────────────────────────┘
示例:┌─────────────┐ ┌─────────────┐│ Node1 │ │ Node2 ││ 无污点 │ │ Taint: gpu │└─────────────┘ └─────────────┘ ↑ ↑ │ ✓ 可以调度 │ ✗ 不能调度 │ │┌─────────────┐ ┌─────────────┐│ Pod A │ │ Pod A ││ 无容忍 │ │ 无容忍 │└─────────────┘ └─────────────┘
┌─────────────┐ ┌─────────────┐│ Node1 │ │ Node2 ││ 无污点 │ │ Taint: gpu │└─────────────┘ └─────────────┘ ↑ ↑ │ ✓ 可以调度 │ ✓ 可以调度 │ │┌─────────────┐ ┌─────────────┐│ Pod B │ │ Pod B ││ Toleration: │ │ Toleration: ││ gpu │ │ gpu │└─────────────┘ └─────────────┘3.2 污点的类型与效果
污点格式: key=value:effect
三种Effect(效果):
| Effect | 说明 | 已运行的Pod |
|---|---|---|
| NoSchedule | 不调度新Pod | 不影响(继续运行) |
| PreferNoSchedule | 尽量不调度新Pod(软性) | 不影响 |
| NoExecute | 不调度新Pod + 驱逐已运行的Pod | 驱逐! |
详细对比:
NoSchedule(不调度):┌──────────────────────────────────────────────┐│ 效果:新Pod不会调度到该节点 ││ 已有Pod:不受影响,继续运行 ││ ││ 使用场景: ││ - Master节点(不运行业务Pod) ││ - 专用节点(GPU节点只给特定应用) │└──────────────────────────────────────────────┘
PreferNoSchedule(尽量不调度):┌──────────────────────────────────────────────┐│ 效果:尽量不调度,但如果没有其他节点可以调度 ││ 已有Pod:不受影响,继续运行 ││ ││ 使用场景: ││ - 资源紧张的节点(希望新Pod去其他节点) ││ - 维护预备节点(准备下线,但不紧急) │└──────────────────────────────────────────────┘
NoExecute(不调度+驱逐):┌──────────────────────────────────────────────┐│ 效果:新Pod不调度 + 驱逐已有Pod ││ 已有Pod:被驱逐!(除非有容忍) ││ ││ 使用场景: ││ - 节点维护(需要清空节点) ││ - 节点故障(自动添加,触发Pod迁移) ││ - 节点隔离(安全原因需要清空) │└──────────────────────────────────────────────┘污点管理命令:
# 添加污点kubectl taint nodes k8s-node1 key=value:NoSchedule
# 查看节点污点kubectl describe node k8s-node1 | grep Taints
# 删除污点(key后加减号)kubectl taint nodes k8s-node1 key=value:NoSchedule-
# 删除某个key的所有污点kubectl taint nodes k8s-node1 key-
# 示例:添加GPU专用节点污点kubectl taint nodes k8s-node1 gpu=nvidia:NoSchedule
# 示例:添加维护污点(会驱逐Pod)kubectl taint nodes k8s-node1 maintenance=true:NoExecute3.3 容忍的配置方式
容忍配置格式:
tolerations:- key: "key" # 污点的key operator: "Equal" # 操作符:Equal或Exists value: "value" # 污点的value(Exists时不需要) effect: "NoSchedule" # 污点的effect(可选,不填则匹配所有effect) tolerationSeconds: 3600 # 容忍时间(仅NoExecute有效)操作符说明:
| Operator | 说明 | 示例 |
|---|---|---|
| Equal | key和value都必须匹配 | key=value |
| Exists | 只需要key存在 | 任意value都匹配 |
配置示例1:精确匹配
apiVersion: v1kind: Podmetadata: name: nginx-tolerationspec: tolerations: - key: "gpu" operator: "Equal" value: "nvidia" effect: "NoSchedule" containers: - name: nginx image: nginx:1.20配置示例2:只匹配key
apiVersion: v1kind: Podmetadata: name: nginx-toleration-existsspec: tolerations: - key: "gpu" operator: "Exists" # 只要有gpu这个key就容忍 effect: "NoSchedule" containers: - name: nginx image: nginx:1.20配置示例3:容忍所有污点
apiVersion: v1kind: Podmetadata: name: nginx-tolerate-allspec: tolerations: - operator: "Exists" # 容忍所有污点(危险!) containers: - name: nginx image: nginx:1.20配置示例4:NoExecute + tolerationSeconds
apiVersion: v1kind: Podmetadata: name: nginx-toleration-secondsspec: tolerations: - key: "maintenance" operator: "Equal" value: "true" effect: "NoExecute" tolerationSeconds: 3600 # 容忍3600秒后被驱逐 containers: - name: nginx image: nginx:1.20tolerationSeconds说明:
当节点添加NoExecute污点后:- Pod没有对应容忍 → 立即驱逐- Pod有容忍,无tolerationSeconds → 永不驱逐- Pod有容忍,tolerationSeconds=3600 → 3600秒后驱逐
使用场景:- 节点维护时,给应用一定时间优雅退出- 节点故障时,等待一段时间再迁移Pod3.4 内置污点
K8s会自动为节点添加一些内置污点:
| 污点Key | 说明 | 何时添加 |
|---|---|---|
node.kubernetes.io/not-ready | 节点未就绪 | 节点状态NotReady |
node.kubernetes.io/unreachable | 节点不可达 | 节点失联 |
node.kubernetes.io/memory-pressure | 内存压力 | 节点内存不足 |
node.kubernetes.io/disk-pressure | 磁盘压力 | 节点磁盘不足 |
node.kubernetes.io/pid-pressure | PID压力 | 节点PID不足 |
node.kubernetes.io/network-unavailable | 网络不可用 | 节点网络故障 |
node.kubernetes.io/unschedulable | 节点不可调度 | kubectl cordon |
默认容忍:
# Kubernetes默认为所有Pod添加以下容忍(300秒后驱逐)tolerations:- key: "node.kubernetes.io/not-ready" operator: "Exists" effect: "NoExecute" tolerationSeconds: 300- key: "node.kubernetes.io/unreachable" operator: "Exists" effect: "NoExecute" tolerationSeconds: 3004. Pod亲和性与反亲和性
4.1 什么是Pod亲和性
Pod亲和性(podAffinity) 根据已运行Pod的标签,决定新Pod调度到哪个节点。
使用场景:
场景1:将前端和后端调度到同一节点(减少网络延迟) → 前端Pod亲和后端Pod
场景2:将同一服务的多个副本分散到不同节点(高可用) → Pod反亲和(podAntiAffinity)
场景3:将日志收集器调度到有应用Pod的节点 → 日志收集Pod亲和应用Pod关键概念 - topologyKey:
topologyKey定义"同一位置"的范围:
topologyKey: kubernetes.io/hostname → 同一节点(最常用)
topologyKey: topology.kubernetes.io/zone → 同一可用区
topologyKey: topology.kubernetes.io/region → 同一区域
示例:┌──────────────────────────────────────────────────────────┐│ Region: asia-east ││ ┌─────────────────────┐ ┌─────────────────────┐ ││ │ Zone: zone-a │ │ Zone: zone-b │ ││ │ ┌─────┐ ┌─────┐ │ │ ┌─────┐ ┌─────┐ │ ││ │ │Node1│ │Node2│ │ │ │Node3│ │Node4│ │ ││ │ └─────┘ └─────┘ │ │ └─────┘ └─────┘ │ ││ └─────────────────────┘ └─────────────────────┘ │└──────────────────────────────────────────────────────────┘
topologyKey=hostname → Node1和Node2是不同位置topologyKey=zone → Node1和Node2是同一位置(zone-a)topologyKey=region → 所有节点是同一位置(asia-east)4.2 Pod亲和性配置
配置示例:将缓存Pod调度到Web Pod所在节点
apiVersion: v1kind: Podmetadata: name: cache-podspec: affinity: podAffinity: requiredDuringSchedulingIgnoredDuringExecution: # 硬性要求 - labelSelector: matchExpressions: - key: app operator: In values: - web # 选择标签app=web的Pod topologyKey: kubernetes.io/hostname # 同一节点 containers: - name: redis image: redis:6.0软性偏好示例:
apiVersion: v1kind: Podmetadata: name: cache-pod-preferredspec: affinity: podAffinity: preferredDuringSchedulingIgnoredDuringExecution: # 软性偏好 - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - web topologyKey: kubernetes.io/hostname containers: - name: redis image: redis:6.04.3 Pod反亲和性配置
反亲和性(podAntiAffinity) 确保Pod不会调度到同一位置。
使用场景:高可用部署
apiVersion: apps/v1kind: Deploymentmetadata: name: nginx-haspec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: # 硬性要求 - labelSelector: matchExpressions: - key: app operator: In values: - nginx # 反亲和自己(相同标签的Pod) topologyKey: kubernetes.io/hostname # 不在同一节点 containers: - name: nginx image: nginx:1.20效果:
┌──────────────────────────────────────────────────────┐│ 3个nginx副本分散到3个不同节点: ││ ││ ┌─────────┐ ┌─────────┐ ┌─────────┐ ││ │ Node1 │ │ Node2 │ │ Node3 │ ││ │ nginx-1 │ │ nginx-2 │ │ nginx-3 │ ││ └─────────┘ └─────────┘ └─────────┘ ││ ││ 如果只有2个节点,第3个Pod会Pending! │└──────────────────────────────────────────────────────┘软性反亲和(推荐生产使用):
apiVersion: apps/v1kind: Deploymentmetadata: name: nginx-ha-softspec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: # 软性偏好 - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - nginx topologyKey: kubernetes.io/hostname containers: - name: nginx image: nginx:1.205. 优先级与抢占
5.1 PriorityClass
当集群资源不足时,高优先级Pod可以抢占低优先级Pod。
创建PriorityClass:
apiVersion: scheduling.k8s.io/v1kind: PriorityClassmetadata: name: high-priorityvalue: 1000000 # 优先级值,越大越优先globalDefault: false # 是否为默认优先级preemptionPolicy: PreemptLowerPriority # 可以抢占低优先级Poddescription: "用于关键业务Pod"
---apiVersion: scheduling.k8s.io/v1kind: PriorityClassmetadata: name: low-priorityvalue: 1000globalDefault: falsepreemptionPolicy: Never # 不抢占其他Poddescription: "用于非关键业务Pod"使用PriorityClass:
apiVersion: v1kind: Podmetadata: name: critical-podspec: priorityClassName: high-priority # 使用高优先级 containers: - name: app image: nginx:1.20抢占流程:
资源不足时:1. 高优先级Pod进入Pending2. Scheduler检查是否可以通过抢占低优先级Pod来调度3. 选择被抢占的Pod(尽量选择影响最小的)4. 驱逐被抢占的Pod(优雅终止)5. 调度高优先级Pod6. 实战演练
6.1 实验准备
# 创建实验目录mkdir -p /root/k8s-yaml/schedulingcd /root/k8s-yaml/scheduling
# 查看当前节点和标签kubectl get nodes --show-labels6.2 实验1:nodeSelector基础调度
目标: 将Pod调度到特定标签的节点
步骤1:为节点添加标签
# 为node1添加标签kubectl label nodes k8s-node1 disk-type=ssd env=production
# 为node2添加标签kubectl label nodes k8s-node2 disk-type=hdd env=testing
# 验证标签kubectl get nodes -L disk-type,env步骤2:创建使用nodeSelector的Pod
cat > nodeselector-pod.yaml <<EOFapiVersion: v1kind: Podmetadata: name: nginx-ssdspec: nodeSelector: disk-type: ssd containers: - name: nginx image: nginx:1.20EOF
kubectl apply -f nodeselector-pod.yaml
# 查看Pod调度到哪个节点kubectl get pod nginx-ssd -o wide# 应该调度到k8s-node1(disk-type=ssd)步骤3:验证调度限制
# 创建一个不存在标签的Podcat > nodeselector-notexist.yaml <<EOFapiVersion: v1kind: Podmetadata: name: nginx-notexistspec: nodeSelector: disk-type: nvme # 没有节点有这个标签 containers: - name: nginx image: nginx:1.20EOF
kubectl apply -f nodeselector-notexist.yaml
# 查看Pod状态(应该是Pending)kubectl get pod nginx-notexistkubectl describe pod nginx-notexist | grep -A5 Events# Warning FailedScheduling ... 0/3 nodes are available: 3 node(s) didn't match Pod's node affinity/selector清理:
kubectl delete pod nginx-ssd nginx-notexist6.3 实验2:nodeAffinity高级调度
目标: 使用nodeAffinity实现复杂的节点选择逻辑
步骤1:创建硬性要求的Pod
cat > nodeaffinity-required.yaml <<EOFapiVersion: v1kind: Podmetadata: name: nginx-affinity-requiredspec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: disk-type operator: In values: - ssd - nvme # disk-type=ssd 或 disk-type=nvme containers: - name: nginx image: nginx:1.20EOF
kubectl apply -f nodeaffinity-required.yaml
# 查看调度结果kubectl get pod nginx-affinity-required -o wide步骤2:创建软性偏好的Pod
cat > nodeaffinity-preferred.yaml <<EOFapiVersion: v1kind: Podmetadata: name: nginx-affinity-preferredspec: affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 80 preference: matchExpressions: - key: disk-type operator: In values: - ssd - weight: 20 preference: matchExpressions: - key: env operator: In values: - production containers: - name: nginx image: nginx:1.20EOF
kubectl apply -f nodeaffinity-preferred.yaml
# 查看调度结果(应该优先选择ssd节点)kubectl get pod nginx-affinity-preferred -o wide步骤3:测试NotIn操作符(排除)
cat > nodeaffinity-notin.yaml <<EOFapiVersion: v1kind: Podmetadata: name: nginx-not-hddspec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: disk-type operator: NotIn values: - hdd # 不调度到hdd节点 containers: - name: nginx image: nginx:1.20EOF
kubectl apply -f nodeaffinity-notin.yaml
# 应该调度到node1(ssd),不会调度到node2(hdd)kubectl get pod nginx-not-hdd -o wide清理:
kubectl delete pod nginx-affinity-required nginx-affinity-preferred nginx-not-hdd6.4 实验3:污点与容忍
目标: 使用Taint和Toleration实现节点隔离
步骤1:为节点添加污点
# 为node2添加污点(GPU专用节点)kubectl taint nodes k8s-node2 gpu=nvidia:NoSchedule
# 查看污点kubectl describe node k8s-node2 | grep Taints# Taints: gpu=nvidia:NoSchedule步骤2:创建普通Pod(无法调度到node2)
cat > pod-no-toleration.yaml <<EOFapiVersion: v1kind: Podmetadata: name: nginx-no-tolerationspec: containers: - name: nginx image: nginx:1.20EOF
kubectl apply -f pod-no-toleration.yaml
# 多次创建,观察调度情况(都不会调度到node2)kubectl get pod nginx-no-toleration -o wide步骤3:创建带容忍的Pod(可以调度到node2)
cat > pod-with-toleration.yaml <<EOFapiVersion: v1kind: Podmetadata: name: nginx-with-tolerationspec: tolerations: - key: "gpu" operator: "Equal" value: "nvidia" effect: "NoSchedule" containers: - name: nginx image: nginx:1.20EOF
kubectl apply -f pod-with-toleration.yaml
# 可以调度到任意节点(包括node2)kubectl get pod nginx-with-toleration -o wide步骤4:强制调度到污点节点
cat > pod-force-tainted-node.yaml <<EOFapiVersion: v1kind: Podmetadata: name: nginx-force-node2spec: nodeSelector: kubernetes.io/hostname: k8s-node2 # 指定node2 tolerations: - key: "gpu" operator: "Equal" value: "nvidia" effect: "NoSchedule" containers: - name: nginx image: nginx:1.20EOF
kubectl apply -f pod-force-tainted-node.yaml
# 必定调度到node2kubectl get pod nginx-force-node2 -o wide步骤5:测试NoExecute驱逐
# 先创建一个Pod在node2上运行cat > pod-on-node2.yaml <<EOFapiVersion: v1kind: Podmetadata: name: nginx-on-node2spec: nodeSelector: kubernetes.io/hostname: k8s-node2 tolerations: - key: "gpu" operator: "Equal" value: "nvidia" effect: "NoSchedule" containers: - name: nginx image: nginx:1.20EOF
kubectl apply -f pod-on-node2.yamlkubectl get pod nginx-on-node2 -o wide
# 添加NoExecute污点(会驱逐不容忍的Pod)kubectl taint nodes k8s-node2 maintenance=true:NoExecute
# 查看Pod状态(被驱逐,变成Pending或Terminating)kubectl get pods -o wide
# 删除污点kubectl taint nodes k8s-node2 maintenance=true:NoExecute-kubectl taint nodes k8s-node2 gpu=nvidia:NoSchedule-清理:
kubectl delete pod nginx-no-toleration nginx-with-toleration nginx-force-node2 nginx-on-node26.5 实验4:Pod反亲和实现高可用
目标: 将Deployment的多个副本分散到不同节点
步骤1:创建带反亲和的Deployment
cat > nginx-ha-deployment.yaml <<EOFapiVersion: apps/v1kind: Deploymentmetadata: name: nginx-haspec: replicas: 3 selector: matchLabels: app: nginx-ha template: metadata: labels: app: nginx-ha spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - nginx-ha topologyKey: kubernetes.io/hostname containers: - name: nginx image: nginx:1.20 ports: - containerPort: 80EOF
kubectl apply -f nginx-ha-deployment.yaml
# 查看Pod分布(应该分散在不同节点)kubectl get pods -l app=nginx-ha -o wide预期结果:
NAME READY STATUS NODEnginx-ha-xxx-aaa 1/1 Running k8s-node1nginx-ha-xxx-bbb 1/1 Running k8s-node2nginx-ha-xxx-ccc 1/1 Running k8s-master(如果master允许调度)步骤2:测试硬性反亲和(可能导致Pending)
cat > nginx-ha-strict.yaml <<EOFapiVersion: apps/v1kind: Deploymentmetadata: name: nginx-ha-strictspec: replicas: 5 # 5个副本,但只有2-3个节点 selector: matchLabels: app: nginx-ha-strict template: metadata: labels: app: nginx-ha-strict spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: # 硬性要求 - labelSelector: matchExpressions: - key: app operator: In values: - nginx-ha-strict topologyKey: kubernetes.io/hostname containers: - name: nginx image: nginx:1.20EOF
kubectl apply -f nginx-ha-strict.yaml
# 查看Pod状态(部分会Pending)kubectl get pods -l app=nginx-ha-strict -o widekubectl describe pod -l app=nginx-ha-strict | grep -A3 Events清理:
kubectl delete deployment nginx-ha nginx-ha-strictkubectl label nodes k8s-node1 disk-type- env-kubectl label nodes k8s-node2 disk-type- env-7. 总结
7.1 调度策略选择指南
┌─────────────────────────────────────────────────────────────┐│ 调度策略选择决策树 │├─────────────────────────────────────────────────────────────┤│ ││ 需求:Pod必须运行在特定节点 ││ ┌─────────────────────────────────────────────────────┐ ││ │ 简单场景 → nodeSelector │ ││ │ 复杂场景 → nodeAffinity.required │ ││ │ 调试/紧急 → nodeName(不推荐生产) │ ││ └─────────────────────────────────────────────────────┘ ││ ││ 需求:Pod尽量运行在特定节点(不强制) ││ ┌─────────────────────────────────────────────────────┐ ││ │ → nodeAffinity.preferred │ ││ └─────────────────────────────────────────────────────┘ ││ ││ 需求:某些节点不允许普通Pod调度 ││ ┌─────────────────────────────────────────────────────┐ ││ │ → Taint (NoSchedule) │ ││ │ + 特定Pod配置Toleration │ ││ └─────────────────────────────────────────────────────┘ ││ ││ 需求:多副本Pod分散到不同节点(高可用) ││ ┌─────────────────────────────────────────────────────┐ ││ │ → podAntiAffinity.preferred(推荐) │ ││ │ → podAntiAffinity.required(节点充足时) │ ││ └─────────────────────────────────────────────────────┘ ││ ││ 需求:相关Pod调度到一起(减少延迟) ││ ┌─────────────────────────────────────────────────────┐ ││ │ → podAffinity │ ││ └─────────────────────────────────────────────────────┘ ││ │└─────────────────────────────────────────────────────────────┘7.2 生产最佳实践
# 生产环境Deployment推荐配置apiVersion: apps/v1kind: Deploymentmetadata: name: production-appspec: replicas: 3 selector: matchLabels: app: production-app template: metadata: labels: app: production-app spec: # 1. 软性反亲和:尽量分散到不同节点 affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: production-app topologyKey: kubernetes.io/hostname
# 2. 容忍节点临时故障(默认300秒) tolerations: - key: "node.kubernetes.io/not-ready" operator: "Exists" effect: "NoExecute" tolerationSeconds: 300 - key: "node.kubernetes.io/unreachable" operator: "Exists" effect: "NoExecute" tolerationSeconds: 300
containers: - name: app image: myapp:v1 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi7.3 常用命令总结
# === 节点标签管理 ===kubectl get nodes --show-labelskubectl label nodes <node> key=valuekubectl label nodes <node> key-
# === 污点管理 ===kubectl taint nodes <node> key=value:effectkubectl taint nodes <node> key:effect-kubectl describe node <node> | grep Taints
# === 查看调度结果 ===kubectl get pods -o widekubectl describe pod <pod> | grep -A10 Events
# === 调试调度问题 ===kubectl get events --field-selector reason=FailedSchedulingkubectl describe pod <pending-pod>