默认情况下容器中的磁盘文件是非持久化的,对于运行在容器中的应用来说面临两个问题:

  1. 当容器挂掉kubelet将重启启动它时,文件将会丢失;
  2. 当Pod中同时运行多个容器,容器之间需要共享文件时。

Kubernetes的Volume解决了这两个问题。内部实现中,一个Volume只是一个目录,目录中可能有一些数据,pod的容器可以访问这些数据。至于这个目录是如何产生的、支持它的介质、其中的数据内容是什么,这些都由使用的特定Volume类型来决定。

K8s 支持很多类型的卷,包括持久卷(PersistentVolume)和临时卷(EphemeralVolume)。临时卷的生命周期与 Pod 一致,如 emptyDir,其存储的数据会随着 Pod 的销毁而消失,仅可用作临时存储空间。而持久卷的生命周期比 Pod 长,即使 Pod 销毁也可保留下来。值得注意的是,HostPath 虽然也是一种持久卷,但若 Pod 漂移到其他 Node 或当前 Node 故障,也无法保证数据持久化,因此有时被称为“半持久化存储”。

Kubernetes支持Volume类型有很多,比如:

  • emptyDir
  • hostPath
  • local
  • persistentVolumeClaim
  • image
  • nfs
  • iscsi
  • fc (fibre channel)
  • configMap
  • secret
  • downwardAPI
  • projected

参考文档:https://kubernetes.io/zh-cn/docs/concepts/storage/volumes

这么多看着头晕啊,下面介绍几个常用的:

emptyDir

使用emptyDir,当Pod分配到Node上时,将会创建emptyDir,并且只要Node上的Pod一直运行,Volume就会一直存在。当Pod(不管任何原因)从Node上被删除时,emptyDir也同时会删除,存储的数据也将永久删除。注:删除容器不影响emptyDir。

emptyDir是在Pod分配到node时创建的,它的初始内容为空,并且无需指定宿主机上对应的目录文件,因为这是k8s自动分配的,当Pod从node上移除时,emptyDir中的数据也会被永久删除。

下面看一个Pod中有两个容器,共享同一个emptyDir的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
apiVersion: v1
kind: Pod
metadata:
  name: datagrand
spec:
  containers:
    - name: test1
      image: nginx:1.7.9
      volumeMounts:
        - name: log-storage
          mountPath: /usr/share/nginx/html
    - name: test2
      image: centos
      volumeMounts:
        - name: log-storage
          mountPath: /html
      command: [ "/bin/sh","-c" ]
      args:
        - while true;do
          data >> /html/index.html;
          sleep 1;
          done
  volumes:
    - name: log-storage
      emptyDir: { }

将上面的配置文件保存成emptyDir.yaml,在K8s主机上执行:

1
2
kubectl create -f emptyDir.yaml
kubectl get po --all-namespaces 

下面分别进入两个容器操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 进入容器test1
kubectl exec -it datagrand -c test1 /bin/bash -n test
root@datagrand:/# cd /usr/share/nginx/html
root@datagrand:/usr/share/nginx/html# ls
index.html
# 添加内容
root@datagrand:/usr/share/nginx/html# echo "this is a test" >> index.html 

# 进入容器test2
kubectl exec -it datagrand -c test2 /bin/bash -n test
[root@datagrand /]		# html
[root@datagrand html]	# ls
index.html
[root@datagrand html]	# cat index.html 
this is a test

# 看出来emptyDir卷是两个容器(test1和test2)共享的

hostPath

hostPath允许挂载Node上的文件系统到Pod里面去。如果Pod需要使用Node上的文件,可以使用hostPath。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: gcr.io/google_containers/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /test-pd
      name: test-volume
  volumes:
  - name: test-volume
    hostPath:
      # directory location on host
      path: /chende

这里/chende就是node上的目录。/test-pd是容器中的目录。

这种挂载方式比emptyDir更为持久,除非所在Node发生故障。不过,除了挂载一些配置文件和二进制文件之外,一般不采用该类挂载方式,因为这样的挂载操作增加了Pod文件与Node主机文件的耦合,不利于统一管理。

PV && PVC

PersistentVolume(PV)和PersistentVolumeClaim(PVC)

这两个概念用于pod和volume之间解耦。Pod根据自己的需要提出数据卷的申请,k8s系统将符合条件的数据卷返回给pod。这样一来pod就无需直接和数据卷本身强绑定了。Pod无需知道数据卷的细节信息,比如具体用的是什么存储。

Pod中对数据卷的申请为PVC,用来和PVC绑定的数据卷称为PV。PV可以有多种存储方式,比如NFS,iSCSI等。PV和PVC的生命周期如下:

A. 供应阶段

PV有两种供应方式

  • static 静态方式由系统管理员创建PV
  • dynamic 如果没有静态的PV。系统会动态创建出PV来满足PVC。PV的提供者为StorageClass。要使用动态供应,PVC必须要指定一个StorageClass。如果StorageClass设置为空,那么针对这个PVC的动态供应特性会被禁用。

B. 绑定阶段

这个阶段指的是PV和PVC绑定的过程。系统有一个机制,循环检查新创建的PVC,然后找到一个符合条件的PV,把他们绑定到一起。如果有多个满足要求的PV可以绑定,会使用消耗资源最小的那个。PV和PVC之间是一对一的关系。如果找不到符合条件的PV,PVC会一直保持未绑定状态。

C. 使用阶段

PVC绑定合适的PV之后,Pod使用这个PV。和PVC绑定并处于使用状态的PV不会被系统删除,防止数据丢失。如果用户删除了Pod正在使用的PVC,这个PVC不会被立即移除。直到这个PVC不被任何Pod使用的时候,才会被真正的删除。

D. 重新声明阶段

当PV不再使用的时候,k8s根据不同的策略来回收这些PV。如下三种策略:

  1. Retain策略(默认):在volume不再使用的时候,不删除旧的数据,等待管理员处理。
  2. Delete策略:会删除不再使用的PV,同时删除这些PV对应真实存储的资源。
  3. Recycle策略:删除volume内的内容,使得volume可供下次使用。

常用命令

1
2
# 查看 PV 支持的字段信息
kubectl explain pv

下面看看简单示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: task-pv-volume
  labels:
    type: local
spec:
  storageClassName: manual
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/chende"

# pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: task-pv-claim
spec:
  storageClassName: manual # 要和PV相同
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi # <= PV size

# pvc-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: task-pv-pod
spec:
  volumes:
    - name: task-pv-storage
      persistentVolumeClaim:
        claimName: task-pv-claim
  containers:
    - name: task-pv-container
      image: nginx
      ports:
        - containerPort: 80
          name: "http-server"
      volumeMounts:
        - mountPath: "/usr/share/nginx/html"
          name: task-pv-storage

K8s Master上分别执行kubectl create -f xxx.yaml之后,看看效果。

PersistentVolume(PV)的回收策略、访问策略和状态是Kubernetes存储管理中的重要概念。

回收策略

  1. Retain:当PV的回收策略设置为Retain时,即使对应的PersistentVolumeClaim(PVC)被删除,PV也不会被自动删除。PV将进入Released状态,此时管理员可以手动回收PV,或者重新创建一个PVC来绑定这个PV。如果PV中包含重要数据,这种策略可以确保数据不会因PVC的删除而丢失。
  2. Recycle:当PV的回收策略设置为Recycle时,当PVC被删除后,PV中的数据将被清除(类似于执行rm -rf /thevolume/*),然后PV将变为Available状态,可以被新的PVC重新绑定。这种策略可以节省存储空间,但需要注意数据丢失的风险。
  3. Delete:当PV的回收策略设置为Delete时,当PVC被删除后,对应的PV也会被自动删除。这种策略适用于那些不再需要的PV,或者PV中的数据可以重新生成的情况。

访问策略

PV的访问策略通过accessModes字段来指定,它决定了PV可以以何种方式被访问。Kubernetes支持以下三种访问模式:

  1. ReadWriteOnce(RWO):PV可以被单个节点以读写模式挂载。这是最常见的访问模式,适用于大多数应用。
  2. ReadOnlyMany(ROX):PV可以被多个节点以只读模式挂载。这种模式适用于需要共享数据但不需要写入的应用。
  3. ReadWriteMany(RWX):PV可以被多个节点以读写模式挂载。这是最少见的访问模式,因为它需要存储后端支持多个节点同时读写。
  4. ReadWriteOncePod(RWOP):k8s v1.22引入的一种新的PV访问模式。与ReadWriteOnce(RWO)模式类似,RWOP也允许一个节点以读写方式挂载PV,但是RWOP更进一步地限制了这个访问权限,确保整个集群中只有一个Pod可以读取或写入该PV。

在选择PV的回收策略和访问策略时,需要根据应用的需求和存储后端的特性来进行权衡。例如,如果PV中包含重要数据,那么可能需要选择Retain策略来避免数据丢失;如果PV中的数据可以重新生成,那么可以选择Delete策略来节省存储空间。同样,如果应用需要同时从多个节点读写数据,那么需要选择RWX访问模式;如果只需要从单个节点读写数据,那么可以选择RWO访问模式。

状态

PV的状态可以反映其当前的可用性和使用情况。以下是PV可能的状态及其含义:

  1. Available(可用):PV当前未被任何PVC(PersistentVolumeClaim)绑定,可以被任何匹配的PVC绑定。
  2. Bound(已绑定):PV已经被一个PVC绑定,并且该PVC已经被一个Pod使用。此时,PV的存储资源已经被Pod独占使用。
  3. Released(已释放):PVC被删除,但是对应的PV还没有被集群自动回收。此时,PV处于空闲状态,但还不能被其他PVC绑定,需要等待一段时间(由管理员设置)后才能重新变为Available状态。
  4. Failed(失败):PV自动回收失败。这可能是因为PV所在的存储后端出现问题,或者PV的回收策略设置不正确等原因导致的。

需要注意的是,PV的状态是动态变化的,随着PVC的创建、绑定、删除等操作而发生变化。管理员需要密切关注PV的状态,并根据实际情况进行相应的操作,以确保集群中的存储资源能够被高效地利用和管理。

参考阅读:

https://www.jianshu.com/p/b3665b72126e

https://zhuanlan.zhihu.com/p/564242088

各存储方式的特点和使用场景

  • EmptyDir‌:适用于临时文件交换或缓存,不适合长期存储数据。
  • HostPath‌:简单易用,但不适合生产环境,因为可移植性和容错性较差。
  • PersistentVolume‌:适用于需要稳定存储资源的应用,支持动态和静态 provisioning。
  • 网络存储‌:适用于需要跨节点共享数据的应用,如NFS、GlusterFS等。
  • Local Volume‌:适用于需要高性能存储的应用,但需要注意节点故障时的数据恢复。
  • StatefulSet‌:适用于有状态应用,每个实例都有唯一的存储卷,确保数据的一致性和持久性。

持久化存储方案大比拼

随着 K8s 的普及,存储也成为 K8s 用户关注的一个重要问题:为了满足不同的场景需求,K8s 可以支持基于不同架构的多种存储方案,比如本地磁盘、CSI 外接商用存储、K8s 原生存储 3 种 K8s 持久化存储方案。

image-20240920145421373

本地存储小规模系统可以优先考虑。外接商用存储绝大多数场景不推荐使用。下面着重看K8s原始存储方案。

K8s 原生存储

K8s 原生存储是专为支持容器而构建的存储方案。这种存储与 K8s 的集成程度更深,具有容器级别的数据服务粒度和自动化存储资源运维能力,也因此能够为 K8s 上的容器应用提供灵活扩展能力与自动化运维能力。

更多关于 K8s 原生存储的特性与能力,请参考这篇内容:

https://www.smartx.com/blog/2023/06/cloud-native-storage-concept/

image-20240920152902028

目前,主流 K8s 原生存储主要有两种类型:开源产品(以 Rook(基于 Ceph)和 Longhorn 为代表)和闭源商用产品(以 Portworx 和 IOMesh 为代表)。这两种方案都能提供 K8s 原生的数据存储功能,也各有利弊:开源产品没有采购成本,具有技术实力的客户可以自行开发,具有社区支持,但若出现严重故障或漏洞,很难像商业厂商那样通过专业团队提供快速相应、深度解决问题的服务支持。另外,通过性能测试可以看出,目前基于自研闭源技术的 K8s 存储方案,在性能和稳定性方面要优于开源产品。欲了解测试详情,请阅读主流 K8s 持久化存储方案特性与性能对比(Longhorn / Rook / OpenEBS / Portworx / IOMesh):

https://www.smartx.com/blog/2023/07/kubernetes-persistent-storage-comparison/

三种方案总体对比

image-20240920145333475

K8s原生存储产品

image-20240920153912475

参考阅读:

https://www.51cto.com/article/608369.html

(完)