提示:本文原创作品,良心制作,干货为主,简洁清晰,一看就会
文章目录
- 前言
- 一、K8S存储管理
-
- [1.1 底层存储](#1.1 底层存储)
- [1.2 PV/PVC](#1.2 PV/PVC)
- [1.3 StorageClass](#1.3 StorageClass)
- 二、K8S中基于NFS的动态存储实战
-
- [2.1 基于 NFS 的动态存储供给流程](#2.1 基于 NFS 的动态存储供给流程)
- [2.2 部署NFS](#2.2 部署NFS)
- [2.3 定义storage并授权](#2.3 定义storage并授权)
- [2.4 部署自动创建PV的pod服务](#2.4 部署自动创建PV的pod服务)
- [2.5 测试无状态服务](#2.5 测试无状态服务)
- [2.6 测试有状态服务](#2.6 测试有状态服务)
前言
一、K8S存储管理
K8s存储管理的核心是「把存储和容器解耦」,通过一套分层抽象,解决容器数据临时化、跨节点共享和持久化的问题
1.1 底层存储
底层存储也就是数据真正存在的地方
- 本地存储(hostPath/emptyDir):存在节点本地磁盘
- 云厂商存储(AWS EBS/阿里云盘):存在云厂商的块存储
- 开源分布式存储(Ceph/NFS):存在集群化的存储服务
- K8s对象存储(ConfigMap/Secret):存在etcd里的配置数据
1.2 PV/PVC
PV相当于存储的"货架",PVC相当于"申请单"
- PV(持久化卷):管理员在底层存储上划出来的"可用货架",比如100G的Ceph卷、20G的云盘,定义好容量、读写模式等
- PVC(持久化卷声明):应用侧提交的"领用申请",声明自己需要多大容量、什么读写模式的存储
- 绑定机制:K8s会自动把PVC和匹配的PV绑定,Pod只需要挂载PVC,不用关心底层用的是什么存储
1.3 StorageClass
StorageClass动态创建PV的"自动化货架"
- 解决手动创建PV的麻烦:管理员定义好一个存储模板(比如"Ceph 10G 标准卷"),交给StorageClass
- 应用侧PVC只要指定这个StorageClass,K8s就会自动调用底层存储驱动,动态创建对应的PV并绑定给PVC,不用管理员提前准备
总的来说:数据存在底层存储里 → 管理员用PV把它包装成可用资源 → 应用用PVC申请资源 → 用StorageClass实现动态创建,Pod只需要挂载PVC就能用持久化存储
二、K8S中基于NFS的动态存储实战
2.1 基于 NFS 的动态存储供给流程

1,kubectl 创建了一个 StatefulSet,它的 volumeClaimTemplates 指定了 storageClassName: managed-nfs-storage
2,K8s 控制器发现 PVC 没有对应的 PV,就去问 managed-nfs-storage 这个 StorageClass 定义的 provisioner 是谁
3,发现是 nfs-client-provisioner,于是把创建 PV 的请求发给它
4,nfs-client-provisioner Pod 收到请求后,去 NFS 服务器 192.168.13.137 上创建一个目录,然后创建一个 PV 并和 PVC 绑定
5,StatefulSet 的 Pod 启动后,成功挂载这个 PV,数据就持久化到 NFS 上了
2.2 部署NFS
yaml
## 准备一台机器安装NFS,后续的PV/PVC会到NFS中拿空间
root@NFS:~# apt -y install nfs-kernel-server
root@NFS:~# systemctl start nfs-kernel-server
root@NFS:~# mkdir /data
root@NFS:~# vim /etc/exports
/data *(rw,sync,no_root_squash,no_subtree_check)
root@NFS:~# chmod 777 /data
root@NFS:~# systemctl restart nfs-kernel-server
root@NFS:~# systemctl enable nfs-kernel-server
2.3 定义storage并授权
yaml
## 1. 创建sc
root@k8s-master1:~# vim storageclass-nfs.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: managed-nfs-storage
## 指定外部NFS供给器的名字
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
root@k8s-master1:~# kubectl apply -f storageclass-nfs.yaml
root@k8s-master1:~# kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
managed-nfs-storage k8s-sigs.io/nfs-subdir-external-provisioner Delete Immediate false 38s
因为storage自动创建pv需要经过kube-apiserver,所以要进行授权
yaml
## 2. 创建RBAC
root@k8s-master1:~# vim rbac.yaml
## ServiceAccount:NFS provisioner pod 使用的身份
apiVersion: v1
kind: ServiceAccount
metadata:
name: nfs-client-provisioner
# 需要指定命名空间
namespace: default
---
## ClusterRole:定义集群维度的权限
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: nfs-client-provisioner-ClusterRole
rules:
- apiGroups: [""]
resources: ["nodes"] # 需要获取节点信息,用于拓扑感知
verbs: ["get","list","watch"]
- apiGroups: [""]
resources: ["persistentvolumes"] # 需要全量操作PV
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"] # 需要监控PVC,并更新其状态
verbs: ["get", "list", "watch", "update"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"] # 需要获取storageclasses信息
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"] # 需要记录事件
verbs: ["create", "update", "patch"]
---
## ClusterRoleBinding:将ServiceAccount绑定到ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: nfs-clinet-provisioner-ClusterRoleBinding
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
# 必须添加:ServiceAccount所在命名空间
namespace: default
roleRef:
kind: ClusterRole
name: nfs-clinet-provisioner-ClusterRole
apiGroup: rbac.authorization.k8s.io
---
## Role: 命名空间维度的权限,可以和clusterrole合并
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: nfs-clinet-provisioner-Role
namespace: default
rules:
- apiGroups: [""]
resources: ["endpoints"] # 某些provisioner实现会使用endpoints做leader election
verbs: ["get","list","watch","create","update","patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: nfs-clinet-provisioner-RoleBinding
namespace: default
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
# 必须添加:ServiceAccount所在命名空间
namespace: default
roleRef:
kind: Role
name: nfs-clinet-provisioner-Role
apiGroup: rbac.authorization.k8s.io
root@k8s-master1:~# kubectl apply -f rbac.yaml
2.4 部署自动创建PV的pod服务
yaml
root@k8s-master1:~# vim deploy-nfs.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-client-provisioner
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: nfs-client-provisioner
strategy: # 升级策略
type: Recreate # 当需要更新Pod时,K8S会先删除现有的Pod,再创建新的Pod;这适用于不能同时运行多个副本的有状态服务
template:
metadata:
labels:
app: nfs-client-provisioner
spec:
# 指定pod运行时使用的sa名称,其绑定的RBAC权限决定了provisioner能够访问哪些K8S API资源
serviceAccountName: nfs-client-provisioner
containers:
- name: nfs-client-provisioner
# 这里使用阿里云镜像仓库中的nfs-subdir-external-provisioner镜像,版本 v4.0.0;该镜像实现了nfs动态供给器
image: registry.cn-beijing.aliyuncs.com/pylixm/nfs-subdir-external-provisioner:v4.0.0
volumeMounts:
- name: nfs-root
# 将nfs共享挂载到容器内的 /persistentvolumes 路径,不建议改;provisioner 会在该目录下为每个pvc创建子目录
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME
# 必须与StorageClass中的 provisioner 字段完全一致,这样StorageClass才能调用该供给器
value: k8s-sigs.io/nfs-subdir-external-provisioner
- name: NFS_SERVER
value: 192.168.13.137 # nfs服务器的ip地址
- name: NFS_PATH
value: /data # nfs服务器上导出的共享目录路径
volumes:
- name: nfs-root
nfs:
server: 192.168.13.137
path: /data
yaml
root@k8s-master1:~# kubectl apply -f deploy-nfs.yaml
root@k8s-master1:~# kubectl get pod
NAME READY STATUS RESTARTS AGE
nfs-client-provisioner-65ddc6f649-7zmv8 1/1 Running 0 4s
至此,基于动态 PV/PVC 供给的存储架构已搭建完成。当前已部署的核心组件包括:
- StorageClass:定义存储类型与供给策略;
- ServiceAccount 及配套的 RBAC 资源:为自动供给程序授予必要的操作权限;
- NFS Provisioner Pod:负责动态创建 PV/PVC,该 Pod 已绑定上述 ServiceAccount,并通过 StorageClass 声明后端存储。
后续在创建工作负载(如 Pod、Deployment、StatefulSet)时,只需在 PVC 中通过 storageClassName 字段引用该 StorageClass,Kubernetes 便会自动完成 PV 的动态创建与 PVC 的绑定,无需手动干预
2.5 测试无状态服务
无状态 Deployment 模式:需要提前申请 PVC,然后Pod 再去引用 这个PVC------ 适合共享存储
yaml
root@k8s-master1:~# mkdir -p /k8s-test/sc/
root@k8s-master1:~# cd /k8s-test/sc/
## 创建一个PVC,绑定storageClass,让其自动生成pv
root@k8s-master1:/k8s-test/sc# vim pvc-1.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-stateless
spec:
storageClassName: managed-nfs-storage
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Mi
root@k8s-master1:/k8s-test/sc# kubectl apply -f pvc-1.yaml
root@k8s-master1:/k8s-test/sc# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-1e0871ac-8a6a-4864-b8e2-30e339a2887a 100Mi RWX Delete Bound default/pvc-stateless managed-nfs-storage 4s
root@k8s-master1:/k8s-test/sc# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
pvc-stateless Bound pvc-1e0871ac-8a6a-4864-b8e2-30e339a2887a 100Mi RWX managed-nfs-storage 7s
yaml
## 创建nginx的无状态控制器,副本为2,挂载网页到nfs上
root@k8s-master1:/k8s-test/sc# vim pod-1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app: new
spec:
replicas: 2 # 副本数,根据需要调整
selector:
matchLabels:
app: new
template:
metadata:
labels:
app: new
spec:
containers:
- name: nginx
image: nginx:1.24
ports:
- containerPort: 80
volumeMounts:
- mountPath: "/usr/share/nginx/html"
name: nginx-data
volumes:
- name: nginx-data
persistentVolumeClaim:
claimName: pvc-stateless
root@k8s-master1:/k8s-test/sc# kubectl apply -f pod-1.yaml
root@k8s-master1:/k8s-test/sc# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nfs-client-provisioner-65ddc6f649-7zmv8 1/1 Running 0 2m 10.244.36.104 k8s-node1 <none> <none>
nginx-6d7f475986-7zn6n 1/1 Running 0 1m 10.244.169.181 k8s-node2 <none> <none>
nginx-6d7f475986-jmbt8 1/1 Running 0 1m 10.244.36.105 k8s-node1 <none> <none>
yaml
## NFS 是网络文件系统,多个客户端可以同时挂载同一个远程目录,并且看到的内容完全一致
## 两个pod共享了同一个网络磁盘。就像两个人同时打开同一个网络共享文件夹,其中一人修改了文件,另一人刷新后就能看到变化
## 进入其中一个pod中,并添加页面
root@k8s-master1:/k8s-test/sc# kubectl exec -it nginx-6d7f475986-jmbt8 /bin/bash
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
root@nginx-6d7f475986-jmbt8:/# ls /usr/share/nginx/html/
root@nginx-6d7f475986-jmbt8:/# echo 11111 > /usr/share/nginx/html/index.html
root@nginx-6d7f475986-jmbt8:/# exit
exit
root@k8s-master1:/k8s-test/sc# curl 10.244.169.181
11111
## 访问另一个pod可以看到相同的页面
root@k8s-master1:/k8s-test/sc# curl 10.244.36.105
11111
# 在nfs机器上能看到刚才创建出的文件
root@NFS:~# ls /data/
default-pvc-stateless-pvc-1e0871ac-8a6a-4864-b8e2-30e339a2887a
root@NFS:~# ls /data/default-pvc-stateless-pvc-1e0871ac-8a6a-4864-b8e2-30e339a2887a/
index.html
2.6 测试有状态服务
StatefulSet 通常会配合 volumeClaimTemplates 使用。每个 Pod 启动时,会自动创建一个专属的 PVC,进而绑定一个专属的 PV不共享,每个 Pod 拥有独立存储
StatefulSet 使用 volumeClaimTemplates 时,不需要提前创建 PVC。它会为每个 Pod 自动生成 一个独立的 PVC
yaml
## 创建一个mysql的有状态控制器,副本为2,并且设置svc为Headless Service类型,挂载mysql的数据存放目录到nfs
root@k8s-master1:/k8s-test/sc# vim mysql.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
# 关联的Headless Service名称,用于生成稳定DNS
serviceName: "mysql"
replicas: 2
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:5.7
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: test-mysql
# 挂载到MySQL数据目录
mountPath: /var/lib/mysql
env:
- name: MYSQL_ROOT_PASSWORD
value: "Admin@123456"
## 动态PVC模板(StatefulSet特有)
volumeClaimTemplates:
- metadata:
name: test-mysql
spec:
# 访问模式:单节点读写
accessModes: ["ReadWriteOnce"]
# 使用已创建的StorageClass动态供给
storageClassName: "managed-nfs-storage"
resources:
requests:
storage: 2Gi
---
apiVersion: v1
kind: Service
metadata:
name: mysql
labels:
app: mysql
spec:
ports:
- port: 3306
name: database
# Headless Service(不分配ClusterIP)
clusterIP: None
selector:
app: mysql
yaml
## 创建mysql.yaml
root@k8s-master1:/k8s-test/sc# kubectl apply -f mysql.yaml
root@k8s-master1:/k8s-test/sc# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 37d
mysql ClusterIP None <none> 3306/TCP 17s
## 由创建时间可以看到mysql的pod不是同时启动的,mysql-0先running后mysql-1再启动
root@k8s-master1:/k8s-test/sc# kubectl get pod -o wide| grep mysql
mysql-0 1/1 Running 0 3m38s 10.244.36.106 k8s-node1 <none> <none>
mysql-1 1/1 Running 0 2m44s 10.244.169.182 k8s-node2 <none> <none>
## 自动创建pv/pvc
root@k8s-master1:/k8s-test/sc# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-1e0871ac-8a6a-4864-b8e2-30e339a2887a 100Mi RWX Delete Bound default/pvc-stateless managed-nfs-storage 176m
pvc-b21d9215-7128-4aaf-8d45-073c1b9b8083 2Gi RWO Delete Bound default/test-mysql-mysql-0 managed-nfs-storage 4m54s
pvc-d4ec3851-3021-478a-91d2-0af9995b00dc 2Gi RWO Delete Bound default/test-mysql-mysql-1 managed-nfs-storage 4m
root@k8s-master1:/k8s-test/sc# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
pvc-stateless Bound pvc-1e0871ac-8a6a-4864-b8e2-30e339a2887a 100Mi RWX managed-nfs-storage 176m
test-mysql-mysql-0 Bound pvc-b21d9215-7128-4aaf-8d45-073c1b9b8083 2Gi RWO managed-nfs-storage 4m56s
test-mysql-mysql-1 Bound pvc-d4ec3851-3021-478a-91d2-0af9995b00dc 2Gi RWO managed-nfs-storage 4m2s
## 查看nfs上的/data目录,可以看到数据挂载成功
root@NFS:~# ls /data/
default-pvc-stateless-pvc-1e0871ac-8a6a-4864-b8e2-30e339a2887a default-test-mysql-mysql-1-pvc-d4ec3851-3021-478a-91d2-0af9995b00dc
default-test-mysql-mysql-0-pvc-b21d9215-7128-4aaf-8d45-073c1b9b8083
root@NFS:~# ls /data/default-test-mysql-mysql-0-pvc-b21d9215-7128-4aaf-8d45-073c1b9b8083/
auto.cnf ca.pem client-key.pem ibdata1 ib_logfile1 mysql performance_schema public_key.pem server-key.pem
ca-key.pem client-cert.pem ib_buffer_pool ib_logfile0 ibtmp1 mysql.sock private_key.pem server-cert.pem sys
yaml
root@k8s-master1:/k8s-test/sc# kubectl exec -it mysql-0 /bin/bash
bash-4.2# mysql -h mysql-0.mysql.default.svc.cluster.local -P 3306 -u root -p'Admin@123456'
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> exit
Bye
bash-4.2# exit
exit
root@k8s-master1:/k8s-test/sc#
StatefulSet 中的每个 Pod 会获得形如 <pod-name>.<service-name>.<namespace>.svc.cluster.local 的稳定 DNS 名称
例如,mysql-0.mysql.default.svc.cluster.local,即使 Pod 重新调度,该域名也不会改变
| 特性 | 手动创建 PVC + Deployment | StatefulSet + volumeClaimTemplates |
|---|---|---|
| 是否需要预先创建 PVC | ✅ 必须提前手动创建 | ❌ 自动创建 |
| PVC 与 Pod 的对应关系 | 多个 Pod 可共享一个 PVC | 每个 Pod 独占一个 PVC(名称含 Pod 序号) |
| 存储是否共享 | 取决于是否共用同一 PVC | 默认不共享,相互隔离 |
| Pod 删除后 PVC 是否保留 | 保留(除非手动删除) | 保留(可配置保留策略) |
| 典型场景 | 静态文件、配置中心、共享缓存 | 数据库、ZooKeeper、Redis 集群 |
注:
文中若有疏漏,欢迎大家指正赐教。
本文为100%原创,转载请务必标注原创作者,尊重劳动成果。
求赞、求关注、求评论!你的支持是我更新的最大动力,评论区等你~