为什么要持久化储存

因为 Pod 是“临时工”,数据不能跟着它一起消失。

  • Pod 一重启/删除/漂移,容器里的数据就没了 容器文件系统随 Pod 生命周期走,Pod 挂了数据就丢。

  • 但很多数据必须活下来 比如 MySQL 数据、用户上传的文件、日志、配置。

  • 多个 Pod 可能还要共享同一份数据 比如多个副本同时读写同一个存储。

  • 所以要把数据放在 Pod 之外 用 PV/PVC/StorageClass 把外部存储挂进来,Pod 怎么换,数据都还在。

一句话:Pod 可以死,数据不能死,所以 K8s 要做持久化。

储存卷 emptyDir

emptyDir是一种共享pod生命周期的"临时存储"类型(pod被删除的话,临时目录里面数据也会被删除)。

nginx的Deployment内容 (前置测试Deployment)

 apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: deploy-nginx
   namespace: default
   labels:
     app: nginx
 spec:
   selector:
     matchLabels:
       app: nginx
   template:
     metadata:
       labels:
         app: nginx
     spec:
       containers:
         - name: container-nginx
           image: nginx:1.26.2
           ports:
             - containerPort: 80

操作

 # 创建deploy
 kubectl apply -f deploy.yaml
 kubectl get pod -o wide
 ​
 # 进入pod
 kubectl exec -it <pod-name> -- bash
 ​
 # 改写index.html文件的内容
 echo 'laoli' > /usr/share/nginx/html/index.html
 ​
 # 退出后访问pod, 测试改写效果
 curl <pod-ip>
 ​
 # 重启pod
 kubectl rollout restart deploy deploy-nginx
 ​
 # 再次访问pod, 测试改写效果是否保留

查看存储卷信息

 kubectl explain pod.spec.volumes

emptyDir 使用格式

 spec:
   volumes:
     - name: vol-emptydir   # 存储卷名字,必须
       emptyDir:            # emptyDir类型的存储卷,必须
         sizeLimit: 200Mi   # 存储卷的容量限制,非必须
   containers:
     volumeMounts:
       - name: vol-emptydir               # 引用的存储卷名字
         mountPath: /usr/share/nginx/html # 容器挂载路径

实验实际操作

 # 创建 emptyDir.yaml
 vim emptyDir.yaml
 ​
 # 填写以下内容 
 apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: deploy-nginx
   namespace: default
   labels:
     app: nginx
 spec:
   selector:
     matchLabels:
       app: nginx
   template:
     metadata:
       labels:
         app: nginx
     spec:
       volumes:
         - name: vol-emptydir
           emptyDir:
             sizeLimit: 200Mi
       containers:
         - name: container-alpine
           image: alpine:3.7
           command: ["bin/sh", "-c", "sleep 3600"]
           volumeMounts:
             - name: vol-emptydir
               mountPath: /home
         - name: container-nginx
           image: nginx:1.26.2
           ports:
             - containerPort: 80
           volumeMounts:
             - name: vol-emptydir
               mountPath: /usr/share/nginx/html
 ​
 # 创建deploy并查看pod调度的节点信息
 kubectl apply -f emptyDir.yaml
 kubectl get pod -o wide
 ​
 # 查看pod的UID(UID是Pod实例的唯一标识符)
 kubectl get pod <pod-name> -o yaml
 ​
 # 在pod调度的节点上查看
 ls /var/lib/kubelet/pods/<Pod-UID>/volumes/kubernetes.io~empty-dir/<存储卷名字>
 ​
 # 进入nginx容器,查看挂载路径
 kubectl exec -it <pod-name> -c container-nginx -- ls /usr/share/nginx/html
 ​
 # 进入alpine容器,查看挂载路径
 kubectl exec -it <pod-name> -c container-alpine -- sh
 ls /home
 ​
 # alpine容器的挂载路径下新增index.html
 echo 'laoli' > /home/index.html
 ​
 # 退出容器查看nginx首页
 curl <pod-ip>
 ​
 # 查看pod调度的节点存储卷
 ls /var/lib/kubelet/pods/<Pod-UID>/volumes/kubernetes.io~empty-dir/<存储卷名字>
 ​
 # 删除pod
 kubectl delete -f emptyDir.yaml
 或者
 kubectl delete pod <pod-name>
 ​
 # 查看pod调度的节点存储卷
 ls /var/lib/kubelet/pods/<Pod-UID>

储存卷 hostPath

hostPath这种类型的存储卷,允许将节点的文件系统上的文件或目录挂载到Pod中。与上面的emptyDir其实存在相似之处,都属于"local级别"的数据持久化方案。两者不同的地方在于;

  • hostPath类型即使Pod被删除,数据也持久地保存在节点上

  • hostPath类型可挂载单个文件或整个目录,而emptyDir只能挂载目录

容器时间跟宿主机时间不一致可以读取系统配置实现时间一致

文件 hostPath.yaml

 apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: deploy-nginx
   namespace: default
   labels:
     app: nginx
 spec:
   selector:
     matchLabels:
       app: nginx
   template:
     metadata:
       labels:
         app: nginx
     spec:
       volumes:
         - name: vol-hostpath
           hostPath:
             path: /root/nginx
             type: DirectoryOrCreate
       containers:
         - name: container-nginx
           image: nginx:1.26.2
           ports:
             - containerPort: 80
           volumeMounts:
             - name: vol-hostpath
               mountPath: /usr/share/nginx/html

实验操作

 # 创建hostPath.yaml
 kubectl apply -f hostPath.yaml
 ​
 # 查看创建的pod
 kubectl get pod -o wide
 ​
 # 查看pod挂载路径
 kubectl exec -it <pod-name> -- ls /usr/share/nginx/html
 ​
 # 节点上创建index.html
 echo 'laoli' > /root/nginx/index.html
 ​
 # 查看pod挂载路径下的index.html
 kubectl exec -it <pod-name> -- cat /usr/share/nginx/html/index.html
 ​
 # 进入pod修改index.html文件内容
 kubectl exec -it <pod-name> -- sh
 echo 'hello' > /usr/share/nginx/html/index.html
 ​
 # 返回节点上查看index.html的内容
 cat /root/nginx/index.html

hostPath 使用格式

 spec:
   volumes:
   - name: vol-hostpath
     hostPath:
       # 挂载卷的类型:包含""(默认值)、Directory、File、DirectoryOrCreate(权限设置为755)、FileOrCreate(权限设置为644)等。
       type: DirectoryOrCreate
       # hostPath卷的节点路径
       path: /root/nginx
   containers:
   volumeMounts:
     # 引用的hostPath卷的名字
   - name: vol-hostpath
     # 容器中的挂载路径
     mountPath: /usr/share/nginx/html

存储卷 nfs

NFS即"网络文件系统",全称是Network File System。它将文件系统挂载到远程服务器上,客户端和服务器之间通过 RPC(Remote Procedure Call)协议实现访问。NFS独立于 K8s 集群,即使整个集群崩溃数据也不受影响。NFS基于TCP/IP协议且独立于操作系统,可以跨平台使用,这使得不同系统的计算机可以共享文件。

小结:NFS简单易用,支持跨平台,数据集中存储,这些都是它的优点。不足之处体现在

  • 高并发场景下性能不佳

  • 未提供数据备份需要额外配置

  • pod可以任意的向NFS写数据,存储资源的使用不可控,可能导致NFS服务端崩溃

操作服务端 (安装单独的NFS服务器)

# 安装rpcbind和nfs-utils(基于Ubuntu)
sudo apt update
sudo apt install nfs-kernel-server -y

# 创建nfs服务器的共享数据目录
sudo mkdir -p /root/nfs/data

# 编辑exports配置文件,参数含义:
0. *代表所有客户端地址,也可以指定,例如:192.168.1.50
1. insecure-使用1024以上的端口,secure则使用1024以下的端口
2. rw-可读写,ro只读
3. sync-数据同步写入内存和硬盘(效率低不丢数据),async数据先缓存到内存,待硬盘空闲再写入硬盘
4. no_root_squash-客户端用root访问共享文件夹时不压缩root权限。root_squash则将root访问转为匿名访问。all_squash则将所有用户转为匿名。
5. no_subtree_check-关闭子树检查。默认的subtree_check会验证客户端请求的文件确实位于导出的目录之下,防止越界访问;关闭后性能更好,且目录内文件被重命名/移动时不易出现stale file handle错误,是NFSv3后期及NFSv4推荐的默认选项。
echo "/root/nfs/data *(insecure,rw,sync,no_root_squash,no_subtree_check)" | sudo tee /etc/exports

# 启动服务及开机自启
sudo systemctl start rpcbind
sudo systemctl start nfs-kernel-server
sudo systemctl enable rpcbind
sudo systemctl enable nfs-kernel-server

# 重新载入
sudo exportfs -ra

操作客户端 (安装工作节点上)

注意:客户端上的挂载目录不允许直接删除,除非卸载(解除挂载)

# 安装客户端(安装后只需启动rpcbind服务,不需要启动nfs服务,所有节点执行)
sudo apt update
sudo apt install rpcbind nfs-common -y
sudo systemctl start rpcbind && sudo systemctl enable rpcbind

# 查看服务器的共享资源列表
showmount -e <nfs服务器ip>

# 客户端挂载到nfs服务器
sudo mkdir -p /root/nfs
sudo mount -t nfs <nfs服务器ip>:/root/nfs/data /root/nfs

# 查看挂载点
mount | grep nfs    # 或者 df -t nfs4

# 客户端上创建文件hello.txt
echo 'laoli' | sudo tee /root/nfs/hello.txt

# nfs服务器上查看hello
cat /root/nfs/data/hello.txt

# 卸载(-f 表示强制卸载)
sudo umount -f /root/nfs

# 查看挂载点(确认已卸载,应无输出)
df -t nfs4

# 删除客户端上的挂载目录
sudo rm -rf /root/nfs

NFS使用格式

spec:
  volumes:
    # 存储卷的名字
    - name: vol-nfs
      nfs:
        # nfs-server的IP地址
        server: 192.168.7.70
        # nfs-server的共享目录路径
        path: /root/nfs/data
  containers:
    volumeMounts:
      # 引用的nfs卷的名字
      - name: vol-nfs
        # 容器中的挂载路径
        mountPath: /usr/share/nginx/html

文件 nfs.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  namespace: default
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      volumes:
        - name: vol-nfs
          nfs:
            path: /root/nfs/data
            server: 192.168.7.70
      containers:
        - name: container-nginx
          image: nginx:1.26.2
          ports:
            - containerPort: 80
          volumeMounts:
            - name: vol-nfs
              mountPath: /usr/share/nginx/html

实验操作

# 创建
kubectl apply -f nfs.yaml

# 查看pod
kubectl get pod -o wide

# 查看节点的nfs挂载点
df -t nfs4

# nfs服务器的共享目录创建index.html
echo 'laoli' | sudo tee /root/nfs/data/index.html

# 查看nginx首页
curl <pod-ip>

# 删除然后返回节点再次查看挂载点
kubectl delete -f nfs.yaml
df -t nfs4

PV和PVC

总起概括:PV、PVC都是K8S用来做存储管理的资源对象,它们让存储资源的使用变得可控,从而保障系统。

pv操作

使用格式

apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv1
  labels:
    pv: nfs-pv1
spec:
  # 1.存储能力
  capacity:
    # 存储大小(Mi、Gi)
    storage: 200Mi

  # 2.访问模式(HostPath仅支持ReadWriteOnce,NFS除ReadWriteOncePod外的都支持)
  # https://kubernetes.io/docs/concepts/storage/persistent-volumes/#access-modes
  accessModes:
      # RWO访问模式。只允许被单个节点读写挂载
    - ReadWriteOnce
      # RWOP访问模式。只允许被单个Pod读写挂载
    - ReadWriteOncePod
      # ROX访问模式。允许被多个节点只读挂载
    - ReadOnlyMany
      # RWX访问模式。允许被多个节点读写挂载
    - ReadWriteMany

  # 3.回收策略
  # Retain 保留。删除PVC不会同步删除PV,等管理员手动去处理PV里的数据。NFS、HostPath都支持该项
  # Delete 删除。删除PVC同步删除PV。NFS、HostPath都不支持该项
  # Recycle 回收。删除PVC同步删除PV且重回可用状态。K8s v1.9后该回收策略标记为过时
  persistentVolumeReclaimPolicy: Retain

  # nfs服务器设置
  nfs:
    path: /root/nfs/data/pv1 # 确保/root/nfs/data/pv1事先存在
    server: 192.168.7.70

文件pv.yaml (文件内容是两个容器)

apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv1
  labels:
    pv: nfs-pv1
spec:
  capacity:
    storage: 200Mi
  accessModes:
    - ReadOnlyMany
  persistentVolumeReclaimPolicy: Retain
  nfs:
    path: /root/nfs/data/pv1
    server: 192.168.7.70
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv2
  labels:
    pv: nfs-pv2
spec:
  capacity:
    storage: 500Mi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  nfs:
    path: /root/nfs/data/pv2
    server: 192.168.7.70

操作

# nfs-server上创建相应共享目录
mkdir /root/nfs/data/{pv1,pv2} -p

# 创建pv
kubectl apply -f pv.yaml

# 查看pv
kubectl get pv

pvc操作

PVC 即Persistent Volume Claim,中文叫做"持久化卷声明"。Pod通过 PVC向PV 申请磁盘空间。如果 PVC一直无法寻找到匹配的Py对象,则 PVC进入"Pending"状态。 值得注意的是:PVC支持命名空间,Pod在使用 PVC 时必须与 PVC在同一个namespace 中。

文件pvc.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc
  namespace: default
  labels:
    pv: nfs-pvc
spec:
  resources:
    requests:
      storage: 200Mi # 小于等于PV对象的容量
  accessModes:
    - ReadOnlyMany   # 匹配PV对象的访问模式
  selector:
    matchLabels:
      pv: nfs-pv1    # 匹配PV对象的标签

操作

# 创建pvc
kubectl apply -f pvc.yaml

# 查看pv和pvc
kubectl get pv,pvc

# 在nfs-server上创建文件
echo 'I am pv2' > /root/nfs/data/pv1/index.html
cat /root/nfs/data/pv1/index.html

# 删除pvc(pv对象进入Released状态)
kubectl delete pvc nfs-pvc

# 重新创建pvc(无法与Released状态的pv对象绑定)
kubectl apply -f pvc.yaml

# 删除pv
kubectl delete pv nfs-pv1

# 重新创建pv
kubectl apply -f pv.yaml

创建pod 使用 PVC 格式

spec:
  volumes:
    - name: vol-pvc
      persistentVolumeClaim:
        claimName: nfs-pvc      # 引用名为 nfs-pvc 的 PVC
  containers:
    - name: container-nginx
      image: nginx:1.26.2
      ports:
        - containerPort: 80
      volumeMounts:
        - name: vol-pvc          # 和上面 volumes 的 name 对应
          mountPath: /usr/share/nginx/html   # 把 PVC 挂到 nginx 的网页根目录

文件 deploy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mydeploy
  namespace: default
  labels:
    app: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      volumes:
        - name: vol-pvc
          persistentVolumeClaim:
            claimName: nfs-pvc
      containers:
        - name: container-nginx
          image: nginx:1.26.2
          ports:
            - containerPort: 80
          volumeMounts:
            - name: vol-pvc
              mountPath: /usr/share/nginx/html

验证

# 创建pod
kubectl apply -f deploy.yaml

# 查看pod
kubectl get pod -o wide

# 访问pod
curl <pod-ip>

# 查看节点上nfs挂载信息
df -t nfs4

# 注意:卷访问模式不会在存储已经被挂载的情况下为其实施写保护!
# 进入pod修改index.html内容
kubectl exec -it <pod-ip> -- sh
echo 'hello' > /usr/share/nginx/html/index.html

# 再次测试访问pod,并查看nfs-server共享目录下的文件
cat /root/nfs/data/pv1/index.html

StorageClass

K8s的存储资源有两种供应模式:静态模式和动态模式。静态模式下,管理员预先创建许多各种各样的PV,等待PVC申请使用。

动态模式下,管理员无须预先创建PV,而是通过 StorageClass 自动完成PV的创建以及与PVC的绑定。

部署NFS服务(独立的NFS服务器)测试下可以用K8s节点

# 安装NFS服务
sudo apt update && sudo apt install -y nfs-kernel-server

# 若为CentOS/RHEL
# sudo yum install -y nfs-utils

# 创建共享目录
sudo mkdir -p /data/nfs-share
sudo chmod 777 /data/nfs-share
sudo chown nobody:nogroup /data/nfs-share 2>/dev/null || sudo chown nobody:nobody /data/nfs-share

# 配置NFS共享
sudo vim /etc/exports

# 添加如下内容(测试环境允许所有IP)
/data/nfs-share  *(rw,sync,no_subtree_check,no_root_squash)

# 生效exports配置
sudo exportfs -r

# 重启NFS服务
sudo systemctl restart nfs-kernel-server  # Ubuntu/Debian
# sudo systemctl restart nfs-server       # CentOS/RHEL

# 确认共享生效
showmount -e localhost
# 输出应包含:/data/nfs-share *

K8s节点安装NFS客户端

所有K8s节点(包括控制平面和工作节点)必须安装NFS客户端,否则无法挂载NFS共享:

# Ubuntu/Debian
sudo apt install -y nfs-common

# CentOS/RHEL
# sudo yum install -y nfs-utils

# 验证连通性(替换<NFS服务器IP>)
showmount -e <NFS服务器IP>
# 若输出共享目录,则说明网络和权限正常

部署部署 NFS Provisioner(master 节点执行)

# 创建安装目录
mkdir -p ~/nfs-subdir && cd ~/nfs-subdir

# 创建 rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
  namespace: default
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nfs-client-provisioner-runner
rules:
  - apiGroups: [""]
    resources: ["nodes"]
    verbs: ["get", "list", "watch"]
  - 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"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: run-nfs-client-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: default
roleRef:
  kind: ClusterRole
  name: nfs-client-provisioner-runner
  apiGroup: rbac.authorization.k8s.io
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-client-provisioner
  namespace: default
rules:
  - apiGroups: [""]
    resources: ["endpoints"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-client-provisioner
  namespace: default
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: default
roleRef:
  kind: Role
  name: leader-locking-nfs-client-provisioner
  apiGroup: rbac.authorization.k8s.io
  
# 创建 deployment.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
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccountName: nfs-client-provisioner
      containers:
        - name: nfs-client-provisioner
          # 国内环境用 DaoCloud 同步镜像,避免 registry.k8s.io 被墙
          image: m.daocloud.io/registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: k8s-sigs.io/nfs-subdir-external-provisioner  # 固定值,需与 StorageClass 匹配
            - name: NFS_SERVER
              value: 192.168.7.70        # 替换为你的NFS服务器IP
            - name: NFS_PATH
              value: /data/nfs-share      # 替换为NFS共享目录
      volumes:
        - name: nfs-client-root
          nfs:
            server: 192.168.7.70          # 替换为你的NFS服务器IP
            path: /data/nfs-share         # 替换为NFS共享目录
            
# 创建 storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-storageclass
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner  # 必须与 env 中 PROVISIONER_NAME 完全一致
reclaimPolicy: Retain
volumeBindingMode: Immediate

# 一键应用
kubectl apply -f rbac.yaml
kubectl apply -f deployment.yaml
kubectl apply -f storageclass.yaml

# 查看nfs-client-provisioner资源的状态(应为 Running)
kubectl get pod -l app=nfs-client-provisioner

# 查看storageclass资源的状态
kubectl get sc

创建测试 PVC

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-pvc
spec:
  storageClassName: nfs-storageclass
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
EOF

kubectl get pvc test-pvc -w
# STATUS 应从 Pending 变为 Bound

验证

# PV 自动创建
kubectl get pv
# 应出现自动创建的 PV,名字类似 pvc-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

# 验证 NFS 目录
ls /data/nfs-share
# 应多出类似 default-test-pvc-pvc-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 的目录

# 清理测试资源
kubectl delete pvc test-pvc

# 因 reclaimPolicy: Retain,PV 会变为 Released,不会自动删除
kubectl get pv | grep Released
kubectl delete pv <那个PV名字>