/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
🤔很多刚学K8s小伙伴都会有这些疑问:
本文带你从零拆解实验,每一条命令、每一个参数、返回结果全部保留,逐行讲解作用,理清完整实验思路,看完你可以独立把整套实验跑通,面试相关问题也能从容回答。
🎯实验前置思考:我们为什么要做这套实验?生产运维有什么用?
🔍旧方案痛点(静态PV + Deployment)
| 静态PV:管理员手动创建每一个PV | 开发要存储,必须找运维人工创建PV,效率极低,人力成本高,容易出错 |
| Deployment无固定Pod名称、随机IP | 数据库、Redis、etcd这类集群组件,节点需要固定身份通信,Pod重建IP/主机名变化,集群直接分裂故障🚨 |
| Deployment存储是共享PVC | 多个Pod共用同一个卷,多个实例读写同一份数据,引发数据错乱、文件损坏 |
✨本实验解决的两大核心能力
动态卷供应(StorageClass) 📦
现实运维价值:开发只需要写PVC申请存储资源,K8s自动调用制备器Provisioner自动生成PV,不需要运维手动创建PV,实现存储自助化。 两种实现:Local‑Path(节点本地磁盘动态卷)、NFS‑Subdir‑Provisioner(NFS共享存储动态卷)。
StatefulSet有状态控制器 🧬
现实运维价值:专门跑数据库、Redis、ETCD、ZooKeeper这类有状态集群应用。提供固定主机名、DNS域名、有序扩缩容、每个Pod独立PVC存储。Pod删除重建,身份不变、数据不丢失。
🧠整体完整实验思路流程图
1. 理解静态卷缺点 → 引入StorageClass动态存储模板
├─ 实验1:部署Local‑Path Provisioner本地磁盘动态卷
│ ├─ 部署全套RBAC权限、ConfigMap、Deployment、StorageClass资源
│ ├─ 创建PVC(指定storageClassName)
│ └─ 创建Pod消费PVC,验证自动生成PV,删除资源自动清理本地磁盘
├─ 实验2:部署NFS服务 + NFS‑Client‑Provisioner网络共享动态卷
│ ├─ 搭建NFS服务端,配置共享目录,所有节点安装NFS客户端工具
│ ├─ 创建ServiceAccount、RBAC授权、部署provisioner控制器Pod
│ ├─ 创建NFS类型StorageClass
│ └─ 创建PVC+Deployment验证动态生成NFS子目录PV
└─ 实验3:StatefulSet有状态应用实战(基于上面NFS动态存储)
├─ 搞懂Headless无头Service作用(提供固定DNS解析,**无负载均衡**)
├─ 部署Nginx StatefulSet,使用volumeClaimTemplates PVC模板,每个Pod自动生成独立PVC
├─ 验证有序创建Pod、固定域名、Pod删除重建数据不丢失
└─ 对比Deployment,理解两者适用场景
⚠️重要提醒:所有实验操作我们全部带完整提示符,复制即可执行;每个命令后面附带逐行参数解释,执行完的输出结果原样展示。
第一部分:K8s动态卷供应 StorageClass 概念科普🤔
什么是静态卷?
之前使用PV,需要集群管理员提前手动写yaml创建PV资源,用户写PVC去绑定已经存在的PV。
缺点:人力介入,规模大的时候工作量爆炸。
什么是动态卷供应?🎁
StorageClass核心字段详解📋
| provisioner | 制备器,造PV的工具,核心必选;云厂商有内置,NFS、local-path需要第三方 |
| reclaimPolicy | PV回收策略:Delete删除PVC同步删后端存储;Retain删PVC,保留PV和数据 |
| volumeBindingMode | 绑定时机:Immediate创建PVC立刻造卷;WaitForFirstConsumer等Pod使用PVC才创建卷(本地存储必须选这个) |
| mountOptions | 挂载额外参数,存储不支持会直接报错 |
| parameters | 给制备器传递参数,不同provisioner参数各不相同 |
📌注意:StorageClass一旦创建完成,名字和参数不能修改!想要改只能新建一个StorageClass。
常见存储是否内置Provisioner对照表
| AWS EBS | ✅是 | 公有云AWS |
| Azure Disk | ✅是 | 微软公有云 |
| Ceph RBD | ❌否 | 需要部署外部组件 |
| NFS | ❌否 | 需要nfs‑subdir‑external‑provisioner |
| Local本地磁盘 | ❌否 | 需要rancher local‑path‑provisioner |
🧪实验一:部署Local‑Path Provisioner本地磁盘动态卷
🎯实验目的:实现节点本地磁盘的动态PV自动创建;volumeBindingMode: WaitForFirstConsumer,PVC刚创建是Pending,必须等Pod使用这个PVC才会真正创建PV和本地目录。
步骤1:下载官方yaml资源清单
# root@master30:~# 是命令提示符,#后面是注释,不要复制执行!
root@master30:~# wget [https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml](https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml)
## wget:网络下载工具
## 后面url:rancher官方提供local‑path‑provisioner全套yaml定义文件
# 查看下载好的yaml文件内容
root@master30:~# cat local-path-storage.yaml
📂这个yaml一次性定义5大类K8s资源:
步骤2:应用全部资源到k8s集群
root@master30:~# kubectl apply -f local-path-storage.yaml
## kubectl apply:向K8s提交yaml资源,创建/更新资源
## -f :-f filename,指定yaml文件路径
# 查看刚创建的StorageClass
root@master30:~# kubectl get sc local-path
## kubectl get sc:get查看StorageClass资源;local‑path 指定资源名字
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path rancher.io/local-path Delete WaitForFirstConsumer false 31s
✅解读输出结果:
- PROVISIONER:rancher.io/local‑path 制备器名称
- RECLAIMPOLICY:Delete,PVC删除时,本地磁盘目录同步删除
- VOLUMEBINDINGMODE:WaitForFirstConsumer,等待Pod消费PVC才创建PV!
# 查看local‑path‑storage命名空间下全部资源
root@master30:~# kubectl get all -n local-path-storage
## -n 参数:–namespace 指定命名空间,不加就是default命名空间
NAME READY STATUS RESTARTS AGE
pod/local-path-provisioner-74b5c4bf98-955sk 1/1 Running 0 63s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/local-path-provisioner 1/1 1 1 63s
NAME DESIRED CURRENT READY AGE
replicaset.apps/local-path-provisioner-74b5c4bf98 1 1 1 63s
🎯现象解读:Pod状态Running代表provisioner控制器启动成功;如果状态不是Running,实验直接无法成功!排查Pod日志找错误。
步骤3:切换操作命名空间到default
root@master30:~# kubectl config set-context –current –namespace default
## config set‑context –current:修改当前上下文,后续kubectl命令默认使用default命名空间
步骤4:创建PVC资源,申请5Gi存储,使用local‑path存储类
root@master30:~# cat > local-path-pvc.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: local-path-pvc
namespace: default
spec:
accessModes: ["ReadWriteOnce"] # ReadWriteOnce:只能单个节点读写访问
resources:
requests:
storage: 5Gi # 向存储申请5G存储空间
storageClassName: local-path # 指定使用我们刚刚创建的StorageClass模板
EOF
## cat > 文件名 <<'EOF' :shell Here‑Doc语法,把两个EOF中间文本写入local‑path‑pvc.yaml文件,单引号EOF代表不解析变量
root@master30:~# kubectl apply -f local-path-pvc.yaml
# 观察PVC状态
root@master30:~# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
local-path-pvc Pending local-path <unset> 6s
🚨重点现象!PVC状态是Pending!! 原因:StorageClass的volumeBindingMode: WaitForFirstConsumer,还没有Pod来使用这个PVC,不会创建PV!所以PVC处于等待状态,属于正常现象,不是故障!
步骤5:创建Deployment,Pod挂载上面这个PVC,触发动态创建PV
root@master30:~# cat > local-path-deployment.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: webapp
name: webapp
namespace: default
spec:
replicas: 2 # 副本数2,启动2个pod
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
– image: hub.laoma.cloud/library/nginx # nginx镜像
name: nginx
volumeMounts:
– name: webapp-data # volume名字,和下面volumes.name对应
mountPath: /usr/share/nginx/html # 容器内部挂载路径,nginx网页根目录
volumes:
– name: webapp-data
persistentVolumeClaim:
claimName: local-path-pvc # 引用上面我们创建的pvc名字
EOF
root@master30:~# kubectl apply -f local-path-deployment.yaml
# 查看pod状态 -o wide:输出更多信息,可以看到Pod调度在哪一个node节点
root@master30:~# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
webapp-6d46b54487-6nqmh 1/1 Running 0 114s 10.224.19.21 worker31.laoma.cloud <none> <none>
webapp-6d46b54487-kzkd5 1/1 Running 0 114s 10.224.19.22 worker31.laoma.cloud <none> <none>
🎯现象解读:两个Pod全部调度到worker31节点!因为local‑path的PV创建在worker31节点本地磁盘,别的节点访问不到这个本地卷,k8s调度器把Pod全部调度到能提供存储的节点。
# 再次查看PVC状态
root@master30:~# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
local-path-pvc Bound pvc-e992833c-af6a-4d4d-aff5-fc544394db6d 5Gi RWO local-path <unset> 4m41s
✅现在PVC状态变成Bound!自动生成了对应的PV,PV名字是一长串uuid字符串。
步骤6:向容器内挂载的卷写入测试数据,验证存储生效
# kubectl exec:进入Pod容器执行命令;– 后面是要在容器内部执行的shell命令
root@master30:~# kubectl exec webapp-6d46b54487-6nqmh — bash -c 'echo Hello World From Nginx > /usr/share/nginx/html/index.html'
## bash -c:执行后面的字符串shell脚本,把文本写入网页index.html
# 登录worker31节点,查看本地磁盘自动生成的目录
root@worker31:~# ls /opt/local-path-provisioner/pvc-e992833c-af6a-4d4d-aff5-fc544394db6d_default_local-path-pvc/
index.html
# cat查看文件内容,验证数据成功落到宿主机本地磁盘
root@worker31:~# cat /opt/local-path-provisioner/pvc-e992833c-af6a-4d4d-aff5-fc544394db6d_default_local-path-pvc/index.html
Hello World From Nginx
🎯实验现象:provisioner自动在worker31节点/opt/local‑path‑provisioner生成对应目录,Pod写的数据持久化保存在宿主机磁盘。
步骤7:清理实验资源,验证回收策略Delete效果
# 删除deployment
root@master30:~# kubectl delete deployment webapp
# 删除pvc,reclaimPolicy=Delete,PVC删除,后端本地存储目录会自动清除
root@master30:~# kubectl delete pvc local-path-pvc
# worker31节点验证目录已经被自动删除
root@worker31:~# ls /opt/local-path-provisioner/pvc-e992833c-af6a-4d4d-aff5-fc544394db6d_default_local-path-pvc/
ls: cannot access '/opt/local-path-provisioner/pvc-e992833c-af6a-4d4d-aff5-fc544394db6d_default_local-path-pvc/': No such file or directory
✅现象:目录直接消失,符合reclaimPolicy: Delete回收策略。
⚠️坑提醒:Local‑Path存储缺点:数据绑定单个节点磁盘,如果这个节点宕机,卷无法被别的节点使用!适合测试环境,生产慎重。
🧪实验二:部署NFS动态卷供应 nfs‑subdir‑external‑provisioner🎯
🤔Local‑Path存储只能单节点使用,有很大局限性;NFS网络文件系统,所有集群节点都可以访问,所以volumeBindingMode可以设置Immediate(PVC创建立刻生成PV)。 原理:nfs‑client‑provisioner控制器,收到PVC申请,自动在NFS共享目录创建独立子目录充当PV。
2‑1 部署NFS服务端(master30节点)
root@master30:~# apt install -y nfs-kernel-server
## apt install -y:Debian/Ubuntu包管理器,‑y自动确认全部交互;nfs‑kernel‑server NFS服务端软件
# 创建NFS共享目录,权限777所有用户读写
root@master30:~# mkdir -m 777 /shares/
## mkdir -m 777 :创建目录同时设置权限777,所有用户可读可写可执行
# 编写NFS配置文件/etc/exports,定义共享规则
root@master30:~# cat << EOF > /etc/exports
/shares *(rw,sync,no_root_squash,no_all_squash,insecure)
EOF
## cat <<EOF >:覆盖写入/etc/exports
## /shares :要共享的本地目录
## *:允许所有客户端IP访问
## rw:客户端读写权限
## sync:同步写入磁盘
## no_root_squash:客户端root用户访问不压缩权限(测试环境使用,生产不要!)
## insecure:允许非1024以上端口访问
# 重启nfs‑server服务,让配置生效
root@master30:~# systemctl restart nfs-server.service
## systemctl restart:重启systemd管理服务
# 所有worker节点安装NFS客户端工具,否则节点无法挂载NFS卷!
root@worker31:~# apt install -y nfs-common
root@worker32:~# apt install -y nfs-common
## nfs‑common NFS客户端工具包,集群每个计算节点必须安装,否则挂载PVC会失败!
2‑2 创建命名空间kube‑storage,存放nfs provisioner组件
root@master30:~# kubectl create ns kube-storage
## kubectl create ns:创建namespace命名空间,ns是namespace简写
# 将当前kubectl上下文切换到kube‑storage命名空间
root@master30:~# kubectl config set-context –current –namespace kube-storage
2‑3 创建RBAC权限资源 nfs‑rbac.yaml
🤔为什么要RBAC?nfs‑provisioner这个Pod需要去k8s API创建、删除PV、监听PVC事件,如果不给权限,控制器什么都干不了!
root@master30:~# cat > nfs-rbac.yaml <<'EOF'
apiVersion: v1
kind: ServiceAccount
metadata:
name: nfs-client-provisioner
namespace: kube-storage
—
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: nfs-client-provisioner-runner
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"]
– apiGroups: [""]
resources: ["endpoints"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
—
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: run-nfs-client-provisioner
subjects:
– kind: ServiceAccount
name: nfs-client-provisioner
namespace: kube-storage
roleRef:
kind: ClusterRole
name: nfs-client-provisioner-runner
apiGroup: rbac.authorization.k8s.io
EOF
root@master30:~# kubectl apply -f nfs-rbac.yaml
📖字段解释: ServiceAccount:给Pod使用的账号; ClusterRole:定义一堆权限规则,可以操作PV/PVC/StorageClass/events; ClusterRoleBinding:把上面ClusterRole权限绑定给nfs‑client‑provisioner这个ServiceAccount。
2‑4 部署nfs‑client‑provisioner控制器Deployment
root@master30:~# cat > nfs-provisioner.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-client-provisioner
namespace: kube-storage
spec:
replicas: 1 # 副本1,只运行一个provisioner Pod
strategy:
type: Recreate # 更新策略:重建Pod,不能滚动更新
selector:
matchLabels:
app: nfs-client-provisioner
template:
metadata:
labels:
app: nfs-client-provisioner
spec:
serviceAccountName: nfs-client-provisioner # 使用上面RBAC的sa账号
volumes:
– name: nfs-client-root
nfs:
server: 10.1.8.30 # NFS服务端IP地址!改成你自己master节点IP
path: /shares # NFS共享目录,和/etc/exports保持一致
containers:
– name: nfs-client-provisioner
image: hub.laoma.cloud/sig-storage/nfs-subdir-external-provisioner:v4.0.2
volumeMounts:
– name: nfs-client-root
mountPath: /persistentvolumes # 在控制器Pod内部挂载NFS共享目录
env:
– name: PROVISIONER_NAME
value: fuseim.pri/ifs # 制备器名字!后面StorageClass必须和这个字符串完全一模一样!
– name: NFS_SERVER
value: 10.1.8.30 # NFS服务器IP,和上面保持一致
– name: NFS_PATH
value: /shares # NFS共享目录
EOF
root@master30:~# kubectl apply -f nfs-provisioner.yaml
# 查看deployment状态
root@master30:~# kubectl get deployments.apps -n kube-storage
NAME READY UP-TO-DATE AVAILABLE AGE
nfs-client-provisioner 1/1 1 1 1m
⚠️超级大坑🚨:PROVISIONER_NAME: fuseim.pri/ifs,后面StorageClass里面provisioner字段必须一字不差!写错字符就无法自动创建PV,PVC一直Pending!
2‑5 创建NFS类型StorageClass存储模板
root@master30:~# cat > nfs-storageclass.yaml <<'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-storage
namespace: kube-storage
provisioner: fuseim.pri/ifs # 必须和provisioner环境变量PROVISIONER_NAME完全相同!
parameters:
onDelete: retain
archiveOnDelete: "false"
reclaimPolicy: Delete # 删除PVC,删除NFS里面子目录
allowVolumeExpansion: true # ✅允许存储卷扩容
volumeBindingMode: Immediate # 创建PVC立刻生成PV,不需要等待Pod消费
EOF
root@master30:~# kubectl apply -f nfs-storageclass.yaml
# 查看StorageClass资源
root@master30:~# kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
nfs-storage fuseim.pri/ifs Delete Immediate true 2s
2‑6 验证NFS动态卷,切换回default命名空间
root@master30:~# kubens default
## kubens小工具切换命名空间;没有装就执行 kubectl config set-context –current –namespace default
# 创建PVC,使用nfs‑storage存储类
root@master30:~# cat > nfs-pvc.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: webclaim
namespace: default
spec:
accessModes:
– ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: "nfs-storage"
EOF
root@master30:~# kubectl apply -f nfs-pvc.yaml
# 查看PVC状态
root@master30:~# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
webclaim Bound pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc 5Gi RWO nfs-storage <unset> 3s
# 查看自动生成的PV
root@master30:~# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc 5Gi RWO Delete Bound default/webclaim nfs-storage <unset> 65s
# master节点查看NFS共享目录自动生成子目录
root@master30:~# ls /shares/
default-webclaim-pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc
# 手动写入测试网页内容
root@master30:~# echo Hello World From Nginx > /shares/default-webclaim-pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc/index.html
🎯现象解读:Immediate模式,PVC一创建直接Bound,不需要Pod运行就生成PV和NFS子目录,对比前面local‑path的Pending状态,两者差异来自volumeBindingMode!
2‑7 创建Deployment挂载PVC,访问验证
root@master30:~# cat > nfs-deploy.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: webapp
name: webapp
spec:
replicas: 2
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
– image: hub.laoma.cloud/library/nginx
name: nginx
volumeMounts:
– name: webapp-data
mountPath: /usr/share/nginx/html
volumes:
– name: webapp-data
persistentVolumeClaim:
claimName: webclaim
EOF
root@master30:~# kubectl apply -f nfs-deploy.yaml
# 创建ClusterIP Service,暴露nginx服务
root@master30:~# kubectl expose deployment webapp –target-port 80 –port 80 –protocol TCP
## kubectl expose:把deployment暴露成service资源
## –target‑port:容器内部端口;–port service访问端口;–protocol TCP协议
# 查看service
root@master30:~# kubectl get svc webapp
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
webapp ClusterIP 10.109.140.205 <none> 80/TCP 5m11s
# curl访问cluster‑ip,验证页面输出
root@master30:~# curl 10.109.140.205
Hello World From Nginx
root@master30:~# curl 10.109.140.205
Hello World From Nginx
2‑8 清理资源,观察NFS数据行为
root@master30:~# kubectl delete deployments.apps webapp
# 删除PVC,reclaimPolicy=Delete,provisioner会删除NFS子目录
root@master30:~# kubectl delete pvc webclaim
persistentvolumeclaim "webclaim" deleted
# 查看pv,pv也被清除
root@master30:~# kubectl get pv
No resources found
# 看后端NFS目录,子目录被删除
root@master30:~# ls /shares/
💡本实验保留NFS‑provisioner组件不要删除!下一节StatefulSet实验直接复用这套NFS动态存储。
🧪实验三 StatefulSet 有状态应用实战🎯
🤔思考:Deployment能干啥?不能干啥? ✅Deployment:无状态应用,Pod随机名字、随机IP,Pod之间没有身份区分;多个Pod可以共用同一个PVC。 ❌Deployment不适合数据库、Redis集群:Pod重建IP主机名变,集群节点身份乱掉;多个实例共用存储会发生数据错乱。
📖概念科普 StatefulSet VS Deployment对比表
| 适用场景 | 数据库、Redis、ETCD、ZooKeeper(有状态集群) | Web、API服务(无状态业务) |
| Pod标识 | 固定序号 hostname sts‑name‑0 sts‑name‑1,稳定DNS域名 | 随机名字随机IP,每次重建全部改变 |
| 扩缩容顺序 | 有序!扩容0→1→2;缩容2→1→0,等上一个Ready才创建下一个 | 无序,瞬间批量创建删除Pod |
| 存储 | volumeClaimTemplatesPVC模板,每个Pod独立生成自己的PVC,Pod删除PVC保留 | PVC手动写,所有Pod可以共用同一个PVC |
| 依赖服务 | 必须搭配Headless无头Service(clusterIP: None),提供DNS域名解析 | 普通ClusterIP Service做负载均衡 |
✨StatefulSet三大核心组件
🎯现实运维价值:生产搭建MySQL主从、Redis哨兵集群、ETCD集群,优先使用StatefulSet,保障节点身份稳定和数据持久。
3‑1 环境准备
# 创建statefulset命名空间,本次实验全部运行在这里
root@master30:~# kubectl create ns statefulset
# 切换当前操作上下文到statefulset命名空间
root@master30:~# kubectl config set-context –current –namespace statefulset
# 确保所有worker节点已经提前装好nfs‑common(前面实验已经操作过)
root@worker31:~# apt install -y nfs-common
root@worker32:~# apt install -y nfs-common
3‑2 第一步:创建Headless无头Service nginx‑service.yaml
⚠️StatefulSet必须关联Headless Service!serviceName字段要和下面sts配置一一对应!
root@master30:~# vim nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx # Service名字!后面sts的serviceName必须填这个值!
namespace: statefulset
spec:
selector:
app: nginx # selector标签匹配statefulset管理的pod标签
clusterIP: None # 🎯核心!设置为None,代表Headless无头服务,不分配集群IP,只做DNS解析
ports:
– port: 80
targetPort: 80
name: nginx-port
root@master30:~# kubectl apply -f nginx-service.yaml
# 查看svc,可以看到CLUSTER‑IP那一列是None
root@master30:~# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx ClusterIP None <none> 80/TCP 16m
3‑3 创建StatefulSet资源 nginx‑statefulset.yaml
重点:volumeClaimTemplatesPVC模板,引用我们上一节搭建好的nfs‑storage动态StorageClass;每个Pod自动生成独立PVC。
root@master30:~# vim nginx-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nginx # sts名字,pod主机名会是 nginx‑0 nginx‑1 nginx‑2
spec:
selector:
matchLabels:
app: nginx
serviceName: "nginx" # 🚨必须!绑定上面Headless Service名字,一字不差!
replicas: 3 # 启动3个Pod,序号0、1、2
minReadySeconds: 10 # Pod就绪之后等待10秒,再创建下一个Pod,保障有序启动
template:
metadata:
labels:
app: nginx
spec:
terminationGracePeriodSeconds: 10 # 删除Pod优雅等待时间
containers:
– name: nginx
image: hub.laoma.cloud/library/nginx:latest
ports:
– containerPort: 80
name: nginx
volumeMounts:
– name: nginx-data # 和volumeClaimTemplates.metadata.name对应!
mountPath: /usr/share/nginx/html # nginx网页容器内挂载路径
volumeClaimTemplates: # 🎯PVC模板!自动为每个Pod生成PVC
– metadata:
name: nginx-data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "nfs-storage" # 使用前面NFS动态存储类!
resources:
requests:
storage: 10Gi # 每个Pod申请10G存储
root@master30:~# kubectl apply -f nginx-statefulset.yaml
# watch实时观察pod创建过程,观察有序创建!先nginx‑0 Ready,再nginx‑1,再nginx‑2
root@master30:~# watch kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-0 1/1 Running 0 10m
nginx-1 1/1 Running 0 10m
nginx-2 1/1 Running 0 9m41s
🎯重要实验现象:严格顺序启动!不会同时三个Pod一起创建!
# 查看statefulset资源状态
root@master30:~# kubectl get sts
NAME READY AGE
nginx 3/3 10m
# 查看自动生成的PVC!三个独立PVC分别绑定nginx‑0 nginx‑1 nginx‑2
root@master30 ~# watch "kubectl get pvc | awk '/nginx-data-nginx/ {print $1,$3}'"
nginx-data-nginx-0 pvc-8eb06bf1-a5de-4e94-bf83-514941f73c9f
nginx-data-nginx-1 pvc-e853eccd-8060-4326-8c45-0b88c34a3d97
nginx-data-nginx-2 pvc-27d944f7-1e62-4352-9023-3874e6867ef9
🤔思考:如果是Deployment,3副本只会有1个PVC;StatefulSet这里3个Pod,3个完全独立PVC!每个Pod自己的存储!
3‑4 给每个Pod的卷写入独立测试网页
root@master30 ~# cat > prepare_nginx_content.sh <<'EOF'
nfs_share_path=/shares
namespace=statefulset
kubectl get pvc | awk '/nginx-data-nginx/ {print $1,$3}' > pvc.list
cat pvc.list | while read pvc
do
pvc_name=$(echo -n $pvc | awk '{print $1}')
pv_name=$(echo -n $pvc | awk '{print $2}')
echo ${pvc_name} > ${nfs_share_path}/${namespace}–${pvc_name}–${pv_name}/index.html
done
rm pvc.list
EOF
## shell脚本逐行解释:
# nfs_share_path=/shares 设置NFS根共享目录
# namespace=statefulset 当前命名空间
# kubectl get pvc 过滤sts生成pvc,输出pvc列表写入pvc.list临时文件
# while read循环读取每一行pvc名字和pv uuid
# awk '{print $1}'取第一列pvc名字;awk '{print $2}'取第二列pv名字
# echo内容写入NFS后端目录index.html,每个卷写入不同标识文本
# rm pvc.list 删除临时文件
# 赋予脚本可执行权限
root@master30 ~# bash prepare_nginx_content.sh
3‑5 验证Headless Service DNS域名解析能力
# 临时跑一个测试Pod,用来在集群内部测试域名访问
root@master30 ~# kubectl run test –image nginx -it — bash
If you don't see a command prompt, try pressing enter.
# 在test容器内部curl访问各个pod的固定域名
root@test:/# curl nginx-0.nginx
nginx-data-nginx-0
root@test:/# curl nginx-1.nginx
nginx-data-nginx-1
root@test:/# curl nginx-2.nginx
nginx-data-nginx-2
✅实验现象!!!
nginx‑0.nginx域名直接解析到nginx‑0这个Pod,不需要知道Pod的IP地址!这就是Headless Service给有状态集群带来稳定通信能力。数据库集群节点之间互相发现通信,就是靠这套DNS。
3‑6 模拟Pod故障删除,验证StatefulSet重建Pod,数据不丢失!🎯
# 在master节点删除nginx‑2 Pod,模拟Pod崩溃故障
root@master30:~# kubectl delete pod nginx-2
# 立刻查看pod状态,sts控制器自动重建nginx‑2
root@master30:~# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-0 1/1 Running 0 49m
nginx-1 1/1 Running 0 49m
nginx-2 0/1 ContainerCreating 0 3s
🎯关键点:PVC不会被删除!!重建出来的nginx‑2 Pod继续绑定原来那个旧PVC!
等待nginx‑2 Running,再次进入测试Pod访问域名:
root@master30 ~# kubectl exec test -it — bash
If you don't see a command prompt, try pressing enter.
root@test:/# curl nginx-2.nginx
nginx-data-nginx-2
✅输出还是原来内容!Pod删掉重建,数据完好无损!!
⚠️对比Deployment:Deployment如果Pod重建,PVC如果没删除数据还在,但Pod主机名IP全部变;而StatefulSet:主机名不变、DNS域名不变、PVC不变,全部身份保留。
3‑7(拓展)给StatefulSet增加统一访问入口普通ClusterIP Service
🤔思考:Headless Service只负责DNS解析,没有负载均衡能力;外部访问我们需要新建普通ClusterIP Service,转发到这一组nginx pod。
root@master30 ~# vim nginx-service-www.yaml
apiVersion: v1
kind: Service
metadata:
labels:
app: www
name: www
spec:
ports:
– name: 80-80
port: 80
protocol: TCP
targetPort: 80
selector:
app: nginx
type: ClusterIP
root@master30 ~# kubectl apply -f nginx-service-www.yaml
# 查看service
root@master30 ~# kubectl get svc www
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
www ClusterIP 10.100.163.95 <none> 80/TCP 20s
# 多次curl访问,轮询后端3个pod
root@master30 ~# curl 10.100.163.95
nginx-data-nginx-0
root@master30 ~# curl 10.100.163.95
nginx-data-nginx-1
root@master30 ~# curl 10.100.163.95
nginx-data-nginx-2
3‑8 清理本实验资源
root@master30:~# kubectl delete sts nginx
root@master30:~# kubectl delete svc nginx www
root@master30:~# kubectl delete pvc nginx-data-nginx-{0..2}
root@master30 ~# rm -fr /shares/statefulset-nginx-data-nginx-{0..2}-*
root@master30:~# kubectl delete pod test
💡实验总结(面试直接背)
1. StorageClass动态卷
2. StatefulSet关键点🎯
🚨小白踩坑故障排查清单
| PVC一直Pending | 1.provisioner Pod是否Running;2.storageClassName名字拼写错误;3.Local‑Path需要等待Pod消费PVC才Bound,属于正常;4.NFS客户端工具没有在node节点安装 |
| StatefulSet Pod无法启动 | 1.检查Headless Service是否存在,sts中serviceName名字完全匹配;2.PVC是否正常Bound;3.NFS挂载失败,检查节点nfs‑common软件 |
| Pod删除重建数据丢失 | 不要手动删除PVC!StatefulSet删除sts不会自动删除PVC;如果手动删除PVC,数据会被清除 |
| Headless Service域名无法解析 | 确认service clusterIP: None,标签selector匹配Pod标签;coredns组件状态正常。 |

