9136 字
46 分钟
06.Kubernetes 学习笔记:存储管理与数据持久化

九、存储管理:数据持久化的艺术#

1. 为什么Kubernetes需要存储管理#

1.1 容器数据的”短命”问题#

容器的本质特性是临时的,删除后数据全部丢失。这对于有状态应用(如数据库)是致命的。

演示问题:

Terminal window
# 启动MySQL,写入数据
kubectl run mysql --image=mysql:8.0 --env="MYSQL_ROOT_PASSWORD=pass"
kubectl exec mysql -- mysql -uroot -ppass -e "CREATE DATABASE testdb;"
# 删除Pod
kubectl 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 exist

Kubernetes的存储挑战:

  1. Pod漂移:Pod可能在不同节点重启
  2. 多副本共享:多个Pod需访问同一数据
  3. 生命周期管理:存储生命周期独立于Pod

1.2 存储架构总览#

Kubernetes通过四层架构解决存储问题:

graph TB subgraph "用户层" Pod["Pod<br/>使用存储"] end subgraph "申请层" PVC["PVC<br/>存储申请"] end subgraph "资源层" PV["PV<br/>存储资源"] SC["StorageClass<br/>自动创建PV"] end subgraph "实现层" Backend["后端存储<br/>NFS/Ceph/云盘"] end Pod -->|volumes| PVC PVC -.绑定.-> PV SC -.动态创建.-> PV PV -->|挂载| Backend

四大组件:

组件角色说明
Volume最基础Pod内定义,生命周期绑定Pod
PV存储资源集群级,代表真实存储空间
PVC存储申请用户申请,自动匹配PV
StorageClass自动化动态创建PV,存储分类

静态 vs 动态供给:

静态供给:
管理员手动创建PV → 用户创建PVC → 自动绑定
动态供给:
用户创建PVC(指定StorageClass)→ 自动创建PV → 自动绑定

2. Volume:最基础的存储#

2.1 emptyDir:临时共享存储#

emptyDir 是最简单的Volume,Pod创建时自动创建,Pod删除时数据丢失。

使用场景:

  • 容器间数据共享
  • 临时缓存
  • 计算中间结果

示例:

apiVersion: v1
kind: Pod
metadata:
name: emptydir-demo
spec:
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: v1
kind: Pod
metadata:
name: hostpath-demo
spec:
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: DirectoryOrCreate

hostPath类型:

type说明
DirectoryOrCreate目录不存在则创建
Directory必须存在的目录
File必须存在的文件

3. PersistentVolume(PV):持久化存储资源#

3.1 PV核心概念#

PV 是集群级别的存储资源,由管理员创建或StorageClass动态创建。

特点:

  • 独立于Pod生命周期
  • 集群级资源(无namespace)
  • 代表真实存储空间

PV生命周期:

Available(可用)→ Bound(已绑定)→ Released(已释放)→ Failed(失败)

3.2 PV配置详解#

apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-nfs-001
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: nfs-storage
nfs:
server: 192.168.100.14
path: /data/nfs/pv-001

3.3 访问模式(AccessModes)#

模式缩写说明适用场景
ReadWriteOnceRWO单节点读写数据库
ReadOnlyManyROX多节点只读静态资源
ReadWriteManyRWX多节点读写共享文件系统

不同存储支持的模式:

存储类型RWOROXRWX
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: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pvc
namespace: default
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: nfs-storage

4.3 PVC绑定规则#

K8s如何选择PV?

匹配条件(全部满足):
1. accessModes 匹配
2. storage大小满足(PV >= PVC)
3. storageClassName 匹配
优先级:
1. 大小精确匹配
2. 最小满足(PV略大于PVC)

4.4 Pod使用PVC#

apiVersion: v1
kind: Pod
metadata:
name: mysql-pod
spec:
containers:
- name: mysql
image: mysql:8.0
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
volumes:
- name: mysql-data
persistentVolumeClaim:
claimName: mysql-pvc

5. StorageClass:自动化存储供应#

5.1 StorageClass是什么#

StorageClass 是存储的”自动售货机”,用户申请存储时自动创建PV。

功能:

  • 定义存储类型和参数
  • 自动创建PV(动态供给)
  • 不同性能等级(SSD/HDD)

5.2 StorageClass配置#

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-storage
provisioner: nfs-provisioner
parameters:
archiveOnDelete: "false"
reclaimPolicy: Delete
volumeBindingMode: Immediate

关键字段:

字段说明
provisioner存储供应器(如nfs-provisioner)
parameters供应器特定参数
reclaimPolicyPV回收策略
volumeBindingMode绑定模式(Immediate/WaitForFirstConsumer)

5.3 设置默认StorageClass#

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-storage
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: nfs-provisioner
Terminal window
# 查看默认StorageClass
kubectl get storageclass
# NAME PROVISIONER AGE
# nfs-storage (default) nfs-provisioner 1d

6. 主流后端存储对比#

后端存储是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 选择建议#

graph TD Start[选择存储方案] --> Q1{生产环境?} Q1 -->|否| Dev[开发测试环境] Q1 -->|是| Q2{云环境?} Dev --> hostPath[hostPath<br/>快速简单] Dev --> NFS1[NFS<br/>共享文件] Q2 -->|是| Cloud[云盘<br/>EBS/GCE Disk] Q2 -->|否| Q3{需要RWX?} Q3 -->|是| Q4{规模大?} Q3 -->|否| Local[本地盘<br/>或云盘RWO] Q4 -->|是| Ceph[Ceph/Rook<br/>企业级] Q4 -->|否| NFS2[NFS<br/>小规模]

⚠️ 生产环境建议:

1. 能用云存储就用云存储(省心)
2. 自建存储需要专业运维团队
3. 数据库等核心应用使用高可用存储
4. 定期备份,备份,备份!

7. 实战1:使用hostPath实现持久化#

7.1 实战目标#

使用hostPath验证数据持久化,理解存储的基本原理。

场景: 创建Nginx Pod,使用hostPath存储网页,验证Pod删除后数据是否保留。

7.2 准备工作#

Terminal window
# 在node1节点创建目录
ssh root@192.168.100.21
mkdir -p /data/nginx-html
echo "<h1>Hello from hostPath - v1</h1>" > /data/nginx-html/index.html

7.3 创建使用hostPath的Pod#

hostpath-nginx.yaml:

apiVersion: v1
kind: Pod
metadata:
name: nginx-hostpath
labels:
app: nginx
spec:
# 指定调度到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: v1
kind: Service
metadata:
name: nginx-hostpath
spec:
selector:
app: nginx
ports:
- port: 80
nodePort: 30090
type: NodePort
Terminal window
kubectl apply -f hostpath-nginx.yaml

7.4 测试数据持久化#

Terminal window
# 1. 访问Nginx
curl 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. 删除Pod
kubectl delete pod nginx-hostpath
# 5. 重新创建Pod
kubectl 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漂移问题:

Terminal window
# 删除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:

Terminal window
# 安装NFS服务
yum install -y nfs-utils rpcbind
# 创建共享目录
mkdir -p /data/nfs-storage
chmod 777 /data/nfs-storage
# 配置NFS共享
cat >> /etc/exports << EOF
/data/nfs-storage *(rw,sync,no_root_squash,no_all_squash)
EOF
# 启动NFS服务
systemctl start rpcbind
systemctl start nfs-server
systemctl enable rpcbind
systemctl enable nfs-server
# 刷新NFS配置
exportfs -r
# 查看共享目录
showmount -e localhost
# 输出:/data/nfs-storage *

在所有K8s节点安装NFS客户端:

Terminal window
# 在master、node1、node2上执行
yum install -y nfs-utils
# 测试挂载
mount -t nfs 192.168.100.14:/data/nfs-storage /mnt
ls /mnt
umount /mnt

8.3 部署NFS Provisioner#

创建nfs-provisioner.yaml:

# RBAC权限
apiVersion: v1
kind: ServiceAccount
metadata:
name: nfs-provisioner
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: nfs-provisioner
rules:
- 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/v1
kind: ClusterRoleBinding
metadata:
name: nfs-provisioner
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: nfs-provisioner
subjects:
- kind: ServiceAccount
name: nfs-provisioner
namespace: kube-system
---
# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-provisioner
namespace: kube-system
spec:
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
---
# StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-storage
annotations:
storageclass.kubernetes.io/is-default-class: "true" # 设为默认
provisioner: nfs-provisioner
parameters:
archiveOnDelete: "false" # 删除PVC时不归档数据
reclaimPolicy: Delete
volumeBindingMode: Immediate
Terminal window
kubectl apply -f nfs-provisioner.yaml
# 查看Provisioner状态
kubectl get pods -n kube-system -l app=nfs-provisioner
# 查看StorageClass
kubectl get storageclass
# NAME PROVISIONER RECLAIMPOLICY
# nfs-storage (default) nfs-provisioner Delete

8.4 测试动态供给#

创建test-pvc.yaml:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
storageClassName: nfs-storage
Terminal window
kubectl apply -f test-pvc.yaml
# 查看PVC(自动绑定)
kubectl get pvc test-pvc
# NAME STATUS VOLUME CAPACITY
# test-pvc Bound pvc-abc123-... 1Gi
# 查看自动创建的PV
kubectl 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: v1
kind: Pod
metadata:
name: test-nfs-pod
spec:
containers:
- name: app
image: nginx:1.20
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html
volumes:
- name: data
persistentVolumeClaim:
claimName: test-pvc
Terminal window
kubectl 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.yaml
kubectl exec test-nfs-pod -- cat /usr/share/nginx/html/index.html

8.5 测试回收策略#

Terminal window
# 删除PVC
kubectl 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: v1
kind: Service
metadata:
name: mysql-headless
spec:
clusterIP: None # Headless Service
selector:
app: mysql
ports:
- port: 3306
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
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: 5Gi
Terminal window
kubectl apply -f mysql-statefulset.yaml

9.3 验证独立存储#

Terminal window
# 查看Pod
kubectl 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 测试扩缩容#

Terminal window
# 缩容到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 <- 数据还在!

总结#

本章学习了:

  1. 存储架构

    • Volume/PV/PVC/StorageClass关系
    • 静态供给 vs 动态供给
  2. 存储类型

    • Volume:emptyDir、hostPath
    • PV:访问模式、回收策略
    • PVC:绑定规则
    • StorageClass:自动化供给
  3. 后端存储选择

    • hostPath:开发测试
    • NFS:小规模生产
    • Ceph:企业级
    • 云盘:云环境
  4. 实战经验

    • 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的调度流程:

flowchart TD A["监听未调度的 Pod(Watch API Server)"] --> B["读取 Pod 需求(requests / affinity / tolerations)"] B --> C["预选 Filtering:剔除不满足条件的节点"] C --> D["优选 Scoring:对候选节点打分排序"] D --> E["绑定 Binding:选择最高分节点并绑定 Pod"] E --> F["Kubelet 创建容器:在目标节点拉镜像并启动容器"]

详细流程解析:

┌─────────────────────────────────────────────────────────────┐
│ 用户创建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(最高优先级)
↓ 直接指定节点,绕过Scheduler
2. 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: v1
kind: Pod
metadata:
name: nginx-ssd
spec:
nodeSelector: # 节点选择器
disk-type: ssd # 只调度到标签为disk-type=ssd的节点
containers:
- name: nginx
image: nginx:1.20

节点标签管理:

Terminal window
# 查看节点标签
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=ssd

nodeSelector的局限性:

✗ 只支持精确匹配(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: v1
kind: Pod
metadata:
name: nginx-affinity-required
spec:
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: v1
kind: Pod
metadata:
name: nginx-affinity-preferred
spec:
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: v1
kind: Pod
metadata:
name: nginx-affinity-combo
spec:
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.20

2.3 nodeName:直接指定节点#

nodeName 直接指定Pod运行的节点名称,绕过Scheduler。

apiVersion: v1
kind: Pod
metadata:
name: nginx-specific-node
spec:
nodeName: k8s-node1 # 直接指定节点名称
containers:
- name: nginx
image: nginx:1.20

⚠️ 使用nodeName的风险:

✗ 绕过Scheduler,不检查资源是否充足
✗ 绕过Taints检查,可能调度到不应该去的节点
✗ 节点不存在或不可用时,Pod一直Pending
✗ 不推荐在生产环境使用
适用场景:
✓ 调试和测试
✓ DaemonSet(每个节点运行一个Pod)
✓ 静态Pod

3. 污点与容忍(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迁移) │
│ - 节点隔离(安全原因需要清空) │
└──────────────────────────────────────────────┘

污点管理命令:

Terminal window
# 添加污点
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:NoExecute

3.3 容忍的配置方式#

容忍配置格式:

tolerations:
- key: "key" # 污点的key
operator: "Equal" # 操作符:Equal或Exists
value: "value" # 污点的value(Exists时不需要)
effect: "NoSchedule" # 污点的effect(可选,不填则匹配所有effect)
tolerationSeconds: 3600 # 容忍时间(仅NoExecute有效)

操作符说明:

Operator说明示例
Equalkey和value都必须匹配key=value
Exists只需要key存在任意value都匹配

配置示例1:精确匹配

apiVersion: v1
kind: Pod
metadata:
name: nginx-toleration
spec:
tolerations:
- key: "gpu"
operator: "Equal"
value: "nvidia"
effect: "NoSchedule"
containers:
- name: nginx
image: nginx:1.20

配置示例2:只匹配key

apiVersion: v1
kind: Pod
metadata:
name: nginx-toleration-exists
spec:
tolerations:
- key: "gpu"
operator: "Exists" # 只要有gpu这个key就容忍
effect: "NoSchedule"
containers:
- name: nginx
image: nginx:1.20

配置示例3:容忍所有污点

apiVersion: v1
kind: Pod
metadata:
name: nginx-tolerate-all
spec:
tolerations:
- operator: "Exists" # 容忍所有污点(危险!)
containers:
- name: nginx
image: nginx:1.20

配置示例4:NoExecute + tolerationSeconds

apiVersion: v1
kind: Pod
metadata:
name: nginx-toleration-seconds
spec:
tolerations:
- key: "maintenance"
operator: "Equal"
value: "true"
effect: "NoExecute"
tolerationSeconds: 3600 # 容忍3600秒后被驱逐
containers:
- name: nginx
image: nginx:1.20

tolerationSeconds说明:

当节点添加NoExecute污点后:
- Pod没有对应容忍 → 立即驱逐
- Pod有容忍,无tolerationSeconds → 永不驱逐
- Pod有容忍,tolerationSeconds=3600 → 3600秒后驱逐
使用场景:
- 节点维护时,给应用一定时间优雅退出
- 节点故障时,等待一段时间再迁移Pod

3.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-pressurePID压力节点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: 300

4. 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: v1
kind: Pod
metadata:
name: cache-pod
spec:
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: v1
kind: Pod
metadata:
name: cache-pod-preferred
spec:
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution: # 软性偏好
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web
topologyKey: kubernetes.io/hostname
containers:
- name: redis
image: redis:6.0

4.3 Pod反亲和性配置#

反亲和性(podAntiAffinity) 确保Pod不会调度到同一位置。

使用场景:高可用部署

apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-ha
spec:
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/v1
kind: Deployment
metadata:
name: nginx-ha-soft
spec:
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.20

5. 优先级与抢占#

5.1 PriorityClass#

当集群资源不足时,高优先级Pod可以抢占低优先级Pod。

创建PriorityClass:

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000 # 优先级值,越大越优先
globalDefault: false # 是否为默认优先级
preemptionPolicy: PreemptLowerPriority # 可以抢占低优先级Pod
description: "用于关键业务Pod"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: low-priority
value: 1000
globalDefault: false
preemptionPolicy: Never # 不抢占其他Pod
description: "用于非关键业务Pod"

使用PriorityClass:

apiVersion: v1
kind: Pod
metadata:
name: critical-pod
spec:
priorityClassName: high-priority # 使用高优先级
containers:
- name: app
image: nginx:1.20

抢占流程:

资源不足时:
1. 高优先级Pod进入Pending
2. Scheduler检查是否可以通过抢占低优先级Pod来调度
3. 选择被抢占的Pod(尽量选择影响最小的)
4. 驱逐被抢占的Pod(优雅终止)
5. 调度高优先级Pod

6. 实战演练#

6.1 实验准备#

Terminal window
# 创建实验目录
mkdir -p /root/k8s-yaml/scheduling
cd /root/k8s-yaml/scheduling
# 查看当前节点和标签
kubectl get nodes --show-labels

6.2 实验1:nodeSelector基础调度#

目标: 将Pod调度到特定标签的节点

步骤1:为节点添加标签

Terminal window
# 为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

Terminal window
cat > nodeselector-pod.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
name: nginx-ssd
spec:
nodeSelector:
disk-type: ssd
containers:
- name: nginx
image: nginx:1.20
EOF
kubectl apply -f nodeselector-pod.yaml
# 查看Pod调度到哪个节点
kubectl get pod nginx-ssd -o wide
# 应该调度到k8s-node1(disk-type=ssd)

步骤3:验证调度限制

Terminal window
# 创建一个不存在标签的Pod
cat > nodeselector-notexist.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
name: nginx-notexist
spec:
nodeSelector:
disk-type: nvme # 没有节点有这个标签
containers:
- name: nginx
image: nginx:1.20
EOF
kubectl apply -f nodeselector-notexist.yaml
# 查看Pod状态(应该是Pending)
kubectl get pod nginx-notexist
kubectl 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

清理:

Terminal window
kubectl delete pod nginx-ssd nginx-notexist

6.3 实验2:nodeAffinity高级调度#

目标: 使用nodeAffinity实现复杂的节点选择逻辑

步骤1:创建硬性要求的Pod

Terminal window
cat > nodeaffinity-required.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
name: nginx-affinity-required
spec:
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.20
EOF
kubectl apply -f nodeaffinity-required.yaml
# 查看调度结果
kubectl get pod nginx-affinity-required -o wide

步骤2:创建软性偏好的Pod

Terminal window
cat > nodeaffinity-preferred.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
name: nginx-affinity-preferred
spec:
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.20
EOF
kubectl apply -f nodeaffinity-preferred.yaml
# 查看调度结果(应该优先选择ssd节点)
kubectl get pod nginx-affinity-preferred -o wide

步骤3:测试NotIn操作符(排除)

Terminal window
cat > nodeaffinity-notin.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
name: nginx-not-hdd
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: disk-type
operator: NotIn
values:
- hdd # 不调度到hdd节点
containers:
- name: nginx
image: nginx:1.20
EOF
kubectl apply -f nodeaffinity-notin.yaml
# 应该调度到node1(ssd),不会调度到node2(hdd)
kubectl get pod nginx-not-hdd -o wide

清理:

Terminal window
kubectl delete pod nginx-affinity-required nginx-affinity-preferred nginx-not-hdd

6.4 实验3:污点与容忍#

目标: 使用Taint和Toleration实现节点隔离

步骤1:为节点添加污点

Terminal window
# 为node2添加污点(GPU专用节点)
kubectl taint nodes k8s-node2 gpu=nvidia:NoSchedule
# 查看污点
kubectl describe node k8s-node2 | grep Taints
# Taints: gpu=nvidia:NoSchedule

步骤2:创建普通Pod(无法调度到node2)

Terminal window
cat > pod-no-toleration.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
name: nginx-no-toleration
spec:
containers:
- name: nginx
image: nginx:1.20
EOF
kubectl apply -f pod-no-toleration.yaml
# 多次创建,观察调度情况(都不会调度到node2)
kubectl get pod nginx-no-toleration -o wide

步骤3:创建带容忍的Pod(可以调度到node2)

Terminal window
cat > pod-with-toleration.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
name: nginx-with-toleration
spec:
tolerations:
- key: "gpu"
operator: "Equal"
value: "nvidia"
effect: "NoSchedule"
containers:
- name: nginx
image: nginx:1.20
EOF
kubectl apply -f pod-with-toleration.yaml
# 可以调度到任意节点(包括node2)
kubectl get pod nginx-with-toleration -o wide

步骤4:强制调度到污点节点

Terminal window
cat > pod-force-tainted-node.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
name: nginx-force-node2
spec:
nodeSelector:
kubernetes.io/hostname: k8s-node2 # 指定node2
tolerations:
- key: "gpu"
operator: "Equal"
value: "nvidia"
effect: "NoSchedule"
containers:
- name: nginx
image: nginx:1.20
EOF
kubectl apply -f pod-force-tainted-node.yaml
# 必定调度到node2
kubectl get pod nginx-force-node2 -o wide

步骤5:测试NoExecute驱逐

Terminal window
# 先创建一个Pod在node2上运行
cat > pod-on-node2.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
name: nginx-on-node2
spec:
nodeSelector:
kubernetes.io/hostname: k8s-node2
tolerations:
- key: "gpu"
operator: "Equal"
value: "nvidia"
effect: "NoSchedule"
containers:
- name: nginx
image: nginx:1.20
EOF
kubectl apply -f pod-on-node2.yaml
kubectl 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-

清理:

Terminal window
kubectl delete pod nginx-no-toleration nginx-with-toleration nginx-force-node2 nginx-on-node2

6.5 实验4:Pod反亲和实现高可用#

目标: 将Deployment的多个副本分散到不同节点

步骤1:创建带反亲和的Deployment

Terminal window
cat > nginx-ha-deployment.yaml <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-ha
spec:
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: 80
EOF
kubectl apply -f nginx-ha-deployment.yaml
# 查看Pod分布(应该分散在不同节点)
kubectl get pods -l app=nginx-ha -o wide

预期结果:

NAME READY STATUS NODE
nginx-ha-xxx-aaa 1/1 Running k8s-node1
nginx-ha-xxx-bbb 1/1 Running k8s-node2
nginx-ha-xxx-ccc 1/1 Running k8s-master(如果master允许调度)

步骤2:测试硬性反亲和(可能导致Pending)

Terminal window
cat > nginx-ha-strict.yaml <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-ha-strict
spec:
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.20
EOF
kubectl apply -f nginx-ha-strict.yaml
# 查看Pod状态(部分会Pending)
kubectl get pods -l app=nginx-ha-strict -o wide
kubectl describe pod -l app=nginx-ha-strict | grep -A3 Events

清理:

Terminal window
kubectl delete deployment nginx-ha nginx-ha-strict
kubectl 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/v1
kind: Deployment
metadata:
name: production-app
spec:
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: 512Mi

7.3 常用命令总结#

Terminal window
# === 节点标签管理 ===
kubectl get nodes --show-labels
kubectl label nodes <node> key=value
kubectl label nodes <node> key-
# === 污点管理 ===
kubectl taint nodes <node> key=value:effect
kubectl taint nodes <node> key:effect-
kubectl describe node <node> | grep Taints
# === 查看调度结果 ===
kubectl get pods -o wide
kubectl describe pod <pod> | grep -A10 Events
# === 调试调度问题 ===
kubectl get events --field-selector reason=FailedScheduling
kubectl describe pod <pending-pod>
06.Kubernetes 学习笔记:存储管理与数据持久化
https://dev-null-sec.github.io/posts/06-k8s学习笔记-存储管理与数据持久化/
作者
DevNull
发布于
2025-05-10
许可协议
CC BY-NC-SA 4.0