欢迎光临
我们一直在努力

Deployment 跑数据库直接崩?吃透 StorageClass 动态存储与 StatefulSet 有状态应用全实战

/* 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存储,每次要管理员手动创建PV?用户申请PVC还得等管理员干活?😵
  • Deployment跑数据库,Pod删掉重建数据直接没了,IP还变,集群直接崩掉🤯
  • PVC创建之后一直Pending,不知道到底哪里出问题⚠️
  • StatefulSet和Deployment到底有啥不一样?什么时候该选StatefulSet?
  • 本文带你从零拆解实验,每一条命令、每一个参数、返回结果全部保留,逐行讲解作用,理清完整实验思路,看完你可以独立把整套实验跑通,面试相关问题也能从容回答。


    🎯实验前置思考:我们为什么要做这套实验?生产运维有什么用?

    🔍旧方案痛点(静态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的工具)
  • 用户只提交PVC,并且在PVC中指定storageClassName选择上面的套餐
  • K8s调用Provisioner自动生成PV,自动完成PVC和PV绑定,用户直接使用PVC。
  • 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对照表

    存储类型是否K8s内置制备器备注
    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资源:

  • Namespace:local‑path‑storage 命名空间,隔离这套组件所有资源
  • ConfigMap:local‑path‑config,定义本地磁盘默认存储路径/opt/local‑path‑provisioner
  • ServiceAccount + RBAC(Role、RoleBinding、ClusterRole、ClusterRoleBinding):给provisioner程序授权操作PV、PVC、Pod等k8s资源权限,没有权限无法创建PV!
  • Deployment:运行local‑path‑provisioner核心控制器Pod,负责监听PVC,动态创建本地PV
  • StorageClass:名字叫local‑path,供PVC引用的存储模板
  • 步骤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对比表

    对比项StatefulSet(有状态)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三大核心组件

  • StatefulSet控制器:管控Pod有序创建删除、维护序号、处理PVC模板
  • Headless Service无头服务 clusterIP: None:不做负载均衡,只做DNS域名解析。域名格式pod-name.svc-name.namespace.svc.cluster.local,例如nginx‑0.nginx.statefulset.svc.cluster.local,集群内部Pod可以直接用域名互相访问。
  • volumeClaimTemplates PVC模板:不用手动写一堆PVC,StatefulSet会给每一个序号Pod自动生成独立PVC;Pod被删除,PVC不会被删除!重建Pod会复用旧PVC,数据不丢失!
  • 🎯现实运维价值:生产搭建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一起创建!

  • 先启动nginx‑0,等待它状态Running、minReadySeconds等待10秒
  • 才会创建nginx‑1,等待就绪
  • 最后创建nginx‑2。Deployment是一次性全部启动,这是巨大区别!
  • # 查看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动态卷

  • Local‑Path Provisioner:本地磁盘动态存储;WaitForFirstConsumer,Pod消费PVC才创建PV;卷绑定单个节点,节点挂掉卷不可用。
  • NFS‑Subdir‑Provisioner:NFS共享存储动态卷;Immediate,PVC创建立刻生成PV;存储可以被集群全部节点访问。
  • reclaimPolicy回收策略:Delete删除PVC清除后端存储;Retain保留数据。
  • 2. StatefulSet关键点🎯

  • 专门管理有状态应用(数据库、中间件集群),Deployment适合无状态业务。
  • 依赖Headless Service(clusterIP: None),提供固定DNS域名,实现集群节点稳定通信。
  • volumeClaimTemplatesPVC模板,每个Pod自动拥有独立PVC;删除Pod不会删除PVC,重建复用旧数据。
  • 有序扩缩容:扩容0→1→2,缩容2→1→0,保证集群启动顺序。
  • Pod删除重建,主机名、DNS域名、PVC身份全部保持不变,业务数据不丢失。
  • 🚨小白踩坑故障排查清单

    故障现象排查思路
    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组件状态正常。
    赞(0)
    未经允许不得转载:171主机测评 » Deployment 跑数据库直接崩?吃透 StorageClass 动态存储与 StatefulSet 有状态应用全实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址