Linux开源存储漫谈(9)kubernetes及持久化
Kubernetes,简称k8s ,2014年发布,其核心特性脱胎于Google基础设施系统(Borg/Omega)的设计经验,并得益于Docker 项目和容器技术发展演进,使用声明式API定义容器化业务和容器间关系,为用户提供一个功能强大的容器编排工具;同时,按照某种规则,把容器调度到某个节点上运行起来并管理其生命周期;此外,k8s还提供了路由网关、水平扩展、监控、备份、灾难恢复等一系列运维能力。k8s的真正价值在于提供了一套基于容器构建分布式系统的基础依赖
本文主要介绍k8s持久卷,需要对k8s有一定的了解,知晓基本的k8s操作,并有可用k8s环境
k8s全局架构

k8s由 Master 和 Node 两种节点组成,分别对应着控制节点和计算节点。控制节点,包括三个独立组件,kube-apiserver负责 API 服务,kube-scheduler负责调度,kube-controller-manager负责容器编排,三个组件紧密协作,协同完成编排、管理、调度用户提交的作业。集群持久化数据,则由 kube-apiserver 处理后保存在 Etcd 中。计算节点,核心是一个叫作 kubelet 的组件,主要负责,调用CRI(Container Runtime Interface)远程调用接口同容器运行时(比如 Docker 项目)打交道,CRI定义了容器运行时的各项核心操作,比如:启动一个容器需要的所有参数
具体的容器运行时,通过 OCI 这个容器运行时规范同底层的 Linux 操作系统进行交互,即:把 CRI 请求翻译成对 Linux 操作系统的调用(操作 Linux Namespace 和 Cgroups 等)。此外,kubelet 还通过 gRPC 协议同Device Plugin 的插件进行交互。另外,kubelet调用网络插件CNI(Container Networking Interface)为容器配置网络,存储插件CSI(Container Storage Interface)为容器配置持久化存储
k8s持久化存储卷
Linux开源存储漫谈(8)容器及容器存储_arvey8888的博客-CSDN博客详细阐述了Docker Volume,Docker通过挂载本地目录进入容器的方式,解除存储卷与容器生命周期的紧耦合,完成容器的持久化。这一套方案有以下明显的不足,其一,不够灵活,需要与宿主机绑定;其二,不够通用,受限于文件系统;其三,难于运维管理。
开篇讲过,k8s使用声明式API定义容器化业务和容器间关系,不同于命令式,声明式API是面向对象的设计思想,业务被抽象成API对象,操作则被设计为对象的行为。k8s持久化存储相关API对象包括PV(Persistent Volume)、PVC(Persistent Volume Claim)、StorageClass
PV(Persistent Volume)
PV描述的,是持久化存储数据卷,不同于临时卷持久化存储卷的生命周期不受Pod生命周期的限制,PV是独立的k8s API对象,有独立的生命周期管理。而PV的类型包括诸如FlexVolume、iSCSI、hostPath、NFS等等,比较低入门坎的如NFS,只用有一个网络连通的NFS server (搭建NFS服务器),并且,在每一台k8s的Node节点上安装NFS相关软件包(Ubuntu 22.04,运行apt install -y nfs-common),就可以开启NFS类型的PV的测试。如下所示:
root@k8s01:~/k8s# cat > nfs-pv.yml << EOF
> apiVersion: v1
> kind: PersistentVolume
> metadata:
> name: nfs-pv
> spec:
> capacity:
> storage: 10Gi
> accessModes:
> - ReadWriteMany
> nfs:
> server: 192.168.2.111
> path: "/mnt/nvme/share"
> EOF
root@k8s01:~/k8s# kubectl apply -f nfs-pv.yml
persistentvolume/nfs-pv created
root@k8s01:~/k8s#
root@k8s01:~/k8s# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
nfs-pv 10Gi RWX Retain Available 30s
root@k8s01:~/k8s#
PVC(Persistent Volume Claim)
而 PVC 描述的,则是 Pod 所期望的持久化存储的属性。如持久化存储卷大小、可读写权限等等。PVC 就像程序设计中的“接口”,负责解耦Pod和持久化存储卷,它提供了对某种持久化存储的描述,但不提供具体的实现;而这个持久化存储的实现部分则由 PV 负责完成。这样做的好处是,应用开发者只需要跟 PVC 这个“接口”打交道,而不必关心具体的实现是 NFS 还是其它。如下,定义nfs-pvc
root@k8s01:~/k8s# cat > nfs-pvc.yml << EOF
> apiVersion: v1
> kind: PersistentVolumeClaim
> metadata:
> name: nfs-pvc
> spec:
> accessModes:
> - ReadWriteMany
> resources:
> requests:
> storage: 10Gi
> EOF
root@k8s01:~/k8s#
root@k8s01:~/k8s# kubectl apply -f nfs-pvc.yml
persistentvolumeclaim/nfs-pvc created
root@k8s01:~/k8s# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
nfs-pv 10Gi RWX Retain Bound default/nfs-pvc 2m33s
root@k8s01:~/k8s# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
nfs-pvc Bound nfs-pv 10Gi RWX 18s
root@k8s01:~/k8s# kubectl get pvc nfs-pvc -o=jsonpath='{.spec.volumeName}'
nfs-pv
root@k8s01:~/k8s#
PV和PVC对象是如何完成绑定的呢?即Kubernetes如何实现nfs-pvc和nfs-pv绑定呢,在 Kubernetes 中,实际上存在着一个专门处理持久化存储的控制器,叫作 Volume Controller。这个 Volume Controller 维护着多个控制循环,其中有一个循环负责完成PVC 和 PV 的绑定操作,它就是PersistentVolumeController,PersistentVolumeController会监听所有的PVC对象是不是已经处于 Bound(已绑定)状态,如果不是,那它就会遍历所有的、可用的 PV,并尝试将其与这个状态不是已绑定的PVC进行绑定,有合适的 PV 出现,它就能够很快进入绑定状态,而绑定依据包括两个条件,其一,PV 和 PVC 的 spec 字段。比如,PV 的存储(storage)大小,就必须满足 PVC 的要求;其二, PV 和 PVC 的 storageClassName 字段必须一样。绑定成功后pvc对象的volumeName字段由被绑定PV的name字段填充
Kubernetes持久化卷的两阶段处理
部署一个Pod测试NFS持久化卷
root@k8s01:~/k8s# cat > busybox.yml << EOF
> apiVersion: apps/v1
> kind: Deployment
> metadata:
> labels:
> app: busybox
> name: busybox
> spec:
> selector:
> matchLabels:
> app: busybox
> template:
> metadata:
> labels:
> app: busybox
> spec:
> containers:
> - args:
> - sleep 100000
> command:
> - /bin/sh
> - -c
> image: busybox
> name: busybox
> volumeMounts:
> - mountPath: /nfs/share
> name: nfs-volume
> volumes:
> - name: nfs-volume
> persistentVolumeClaim:
> claimName: nfs-pvc
> EOF
root@k8s01:~/k8s# kubectl apply -f busybox.yml
deployment.apps/busybox created
root@k8s01:~/k8s# kubectl get pod
NAME READY STATUS RESTARTS AGE
busybox-7df5d77db6-llwvd 1/1 Running 0 58s
root@k8s01:~/k8s# kubectl exec -it busybox-7df5d77db6-llwvd -- /bin/sh
/ #
/ # cd /nfs/share
/nfs/share # ls
incontai incontainer nfstest
/nfs/share # touch create-in-container
/nfs/share # ls
create-in-container incontai incontainer nfstest
/nfs/share #
root@k8s01:~/k8s# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
busybox-7df5d77db6-llwvd 1/1 Running 0 14m 10.10.1.12 k8s02 <none> <none>
root@k8s01:~/k8s# kubectl get node -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k8s01 Ready control-plane,master 14h v1.23.3 192.168.2.101 <none> Ubuntu 22.04.2 LTS 5.15.0-72-generic docker://20.10.21
k8s02 Ready <none> 14h v1.23.3 192.168.2.102 <none> Ubuntu 22.04.2 LTS 5.15.0-72-generic docker://20.10.21
k8s03 Ready <none> 14h v1.23.3 192.168.2.103 <none> Ubuntu 22.04.2 LTS 5.15.0-72-generic docker://20.10.21
root@k8s01:~/k8s#
示例代码中的最后两条命令"kubectl get pod -o wide && kubectl get node -o wide",显示容器busybox-7df5d77db6-llwvd被调度到k8s02这个NODE节点,IP地址是192.168.2.102,登录192.168.2.102查看其挂载信息
root@k8s02:~# mount | grep "192.168.2.111"
192.168.2.111:/mnt/nvme/share on /var/lib/kubelet/pods/d3385ee5-af38-4332-aef6-fed485abdeb5/volumes/kubernetes.io~nfs/nfs-pv type nfs (rw,relatime,vers=3,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,mountaddr=192.168.2.111,mountvers=3,mountport=49784,mountproto=udp,local_lock=none,addr=192.168.2.111)
root@k8s02:~#
root@k8s02:~# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
420228c53814 busybox "/bin/sh -c 'sleep 1…" 24 minutes ago Up 24 minutes k8s_busybox_busybox-7df5d77db6-llwvd_default_d3385ee5-af38-4332-aef6-fed485abdeb5_0
......
root@k8s02:~# docker inspect 420228c53814
{
......
"Mounts": [
......
{
"Type": "bind",
"Source": "/var/lib/kubelet/pods/d3385ee5-af38-4332-aef6-fed485abdeb5/volumes/kubernetes.io~nfs/nfs-pv",
"Destination": "/nfs/share",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
],
......
}
NFS卷192.168.2.111:/mnt/nvme/share,也就是nfs-pv里nfs信息,被挂载在/var/lib/kubelet/pods/d3385ee5-af38-4332-aef6-fed485abdeb5/volumes/kubernetes.io~nfs/nfs-pv。之后,通过“docker inspect 420228c53814”命令查看容器的挂载信息,非常熟悉容器绑定挂载完成了192.168.2.111:/mnt/nvme/share到容器内"/nfs/share"的挂载过程
两阶段处理
示例中展示的Pod挂载的是NFS持久卷,NFS持久卷的特点是Linux OS直接支持NFS挂载(只需要安装nfs相关rpcbind包,ubuntu安装nfs-common),并不像iSCSI、rbd等提供块服务一样,挂载到本文件系统之前,需要先挂载块设备。如挂载iSCSI块设备,第一步,先在iSCSI Target端执行targetcli创建target、LUN、ACL等;第二步,在iSCSI Initiator端,运行iscsiadm完成discovery和login成功后,远端存储服务(磁盘)才能以块设备的形式出现在本地设备列表中(通过lsblk查看);第三步,挂载到本地文件系统的一个挂载点(目录)下。第二步,即两阶段处理的第一阶段,即Attach,其标志是远端存储服务(磁盘)已经准备好,Pod所在的宿主机已经可以使用如iSCSI协议或rbd等客户端以设备的形式挂载到本地,或者通过调用共有云或具体存储项目的 API完成Pod所在的宿主机挂载远程磁盘的操作。而第三步,即两阶段处理的第二阶段,Mount,不同于Attach阶段,Mount阶段完成远端存储到本地文件系统目录的挂载,供容器使用。挂载NFS不需要执行Attach阶段处理,直接执行Mount处理即可完成挂载
Kubernetes中两阶段的实现
在Kubernetes中,一切控制都是Controller, Attach(Dettach)操作,是由 Volume Controller 负责维护的,即AttachDetachController,通过不断地检查每一个 Pod 对应的 PV和这个 Pod 所在宿主机之间挂载情况。从而决定,是否需要对这个 PV 进行 Attach( Dettach)操作。Mount(Unmount)操作必然发生在 Pod 对应的宿主机上,它是 kubelet 组件的一部分,独立于 kubelet 主循环VolumeManagerReconciler控制循环,调用kubelet的Volume插件完成,如示例volume-nfs
在具体的 Volume 插件的实现接口上,Kubernetes 分别给这两个阶段提供了两种不同的参数列表。Attach阶段,Kubernetes 提供的可用参数是 nodeName,即宿主机的名字;Mount阶段,Kubernetes 提供的可用参数是dir,即 Volume 的宿主机目录。作为一个存储插件,只需要根据需求进行实现。而经过了Attach和Mount的处理,完成宿主机对持久化卷的挂载,接下来,kubelet会把这个 Volume 目录通过 CRI 里的 Mounts 参数,传递给 Docker,然后就可以为 Pod 里的容器挂载这个持久化卷了
StorageClass
PV和PVC面向对象的设计思想实现对持久化存储卷的管理(Static Provisioning),能不能更近一步,实现更加智能的自动创建 PV 的机制呢(Dynamic Provisioning)?答案是另一个API对象——StorageClass,StorageClass对象的作用,就是创建 PV 的模板,前面的nfs-pv和nfs-pvc的示例,可以优化一下,使用nfs-subdir-external-provisioner实现基于StorageClass的自动化创建基于nfs子目录的PV
# 部署nfs-subdir-external-provisioner
root@k8s01:~# git clone https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner.git
root@k8s01:~# cd nfs-subdir-external-provisioner/deploy
root@k8s01:~/nfs-subdir-external-provisioner/deploy#
# 修改deployment.yaml,包括image: 修改为阿里云镜像,NFS_SERVER 和 NFS_PATH,及volumes声明的server和path
root@k8s01:~/nfs-subdir-external-provisioner/deploy# cat deployment.yaml
......
- name: nfs-client-provisioner
image: registry.cn-beijing.aliyuncs.com/mydlq/nfs-subdir-external-provisioner:v4.0.0
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME
value: k8s-sigs.io/nfs-subdir-external-provisioner
- name: NFS_SERVER
value: 192.168.2.111
- name: NFS_PATH
value: /mnt/nvme/share
volumes:
- name: nfs-client-root
nfs:
server: 192.168.2.111
path: /mnt/nvme/share
# 依次执行
root@k8s01:~/nfs-subdir-external-provisioner/deploy# kubectl apply -f rbac.yaml
root@k8s01:~/nfs-subdir-external-provisioner/deploy# kubectl apply -f class.yaml
root@k8s01:~/nfs-subdir-external-provisioner/deploy# kubectl apply -f deployment.yaml
待nfs-subdir-external-provsioner pod执行起来以后
root@k8s01:~/k8s# cat > scnfs-pvc.yml << EOF
> apiVersion: v1
> kind: PersistentVolumeClaim
> metadata:
> name: scnfs-pvc
> spec:
> storageClassName: nfs-client
> accessModes:
> - ReadWriteMany
> resources:
> requests:
> storage: 1Gi
> EOF
root@k8s01:~/k8s# kubectl apply -f scnfs-pvc.yml
persistentvolumeclaim/scnfs-pvc created
root@k8s01:~/k8s# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
nfs-pv 10Gi RWX Retain Bound default/nfs-pvc 10h
pvc-72d24b4f-0e64-4340-a3ae-8d05f0755894 1Gi RWX Delete Bound default/scnfs-pvc nfs-client 9s
root@k8s01:~/k8s# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
nfs-pvc Bound nfs-pv 10Gi RWX 10h
scnfs-pvc Bound pvc-72d24b4f-0e64-4340-a3ae-8d05f0755894 1Gi RWX nfs-client 20s
root@k8s01:~/k8s#
StorageClass原理分析
class.yaml源代码
root@k8s01:~/nfs-subdir-external-provisioner/deploy# cat class.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-client
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner # or choose another name, must match deployment's env PROVISIONER_NAME'
parameters:
archiveOnDelete: "false"
root@k8s01:~/nfs-subdir-external-provisioner/deploy# cat deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-client-provisioner
labels:
app: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: nfs-client-provisioner
template:
metadata:
labels:
app: nfs-client-provisioner
spec:
serviceAccountName: nfs-client-provisioner
containers:
- name: nfs-client-provisioner
image: registry.cn-beijing.aliyuncs.com/mydlq/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
- name: NFS_SERVER
value: 192.168.2.111
- name: NFS_PATH
value: /mnt/nvme/share
volumes:
- name: nfs-client-root
nfs:
server: 192.168.2.111
path: /mnt/nvme/share
关键代码是第五行,即provisioner行,其注释代码指示必需与deployment.yaml中PROVISIONER_NAME相同。浏览nfs-subdir-external-provisioner源代码可以发现,环境变量PROVISIONER_NAME的值用于创建ProvisionController
root@nvme:/data/github/*-provisioner# vim provisioner.go
......
42 const (
43 provisionerNameKey = "PROVISIONER_NAME"
44 )
......
82 func (p *nfsProvisioner) Provision(ctx context.Context, options controller.ProvisionOptions) (*v1.PersistentVolume, controller.ProvisioningState, error) {
......
91 pvName := strings.Join([]string{pvcNamespace, pvcName, options.PVName}, "-")
......
102 fullPath := filepath.Join(mountPath, pvName)
......
115 if err := os.MkdirAll(fullPath, 0o777); err != nil {
116 return nil, controller.ProvisioningFinished, errors.New("unable to create directory to provision new pv: " + err.Error())
117 }
......
143 return pv, controller.ProvisioningFinished, nil
144 }
......
208 func main() {
......
220 provisionerName := os.Getenv(provisionerNameKey)
221 if provisionerName == "" {
222 glog.Fatalf("environment variable %s is not set! Please set it.", provisionerNameKey)
223 }
......
269 // Start the provision controller which will dynamically provision efs NFS
270 // PVs
271 pc := controller.NewProvisionController(clientset,
272 provisionerName,
273 clientNFSProvisioner,
274 serverVersion.GitVersion,
275 controller.LeaderElection(leaderElection),
276 )
277 // Never stops.
278 pc.Run(context.Background())
279 }
# controller.NewProvisionController, controller包为sigs.k8s.io/sig-storage-lib-external-provisioner/v6/controller,NewProvisionController函数返回ProvisionController实例
root@nvme:/data/github/nfs-subdir-external-provisioner/vendor/*-provisioner/v6/controller# vim controller.go
100 // ProvisionController is a controller that provisions PersistentVolumes for
101 // PersistentVolumeClaims.
102 type ProvisionController struct {
103 client kubernetes.Interface
104
105 // The name of the provisioner for which this controller dynamically
106 // provisions volumes. The value of annDynamicallyProvisioned and
107 // annStorageProvisioner to set & watch for, respectively
108 provisionerName string
当一个"storageClassName"的值为"nfs-client"的PVC被创建时,注册名称为"k8s-sigs.io/nfs-subdir-external-provisioner"的ProvisionController会被Kubernetes调用到,即会执行provisioner.go的82行Provision函数,91-102行,生成PV在容器内绝对路径,即是使用"-"连接namespace、pvc名称、及由Kubernetes生成PV名称,第115行调用os.MkdirAll(fullPath, 0o777)在挂载点"mountPath: /persistentvolumes"下创建子目录,最后,在pvc创建成功后,查看pv及pvc的信息:
root@k8s01:~/k8s# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
nfst-pvc Bound pvc-ea893912-3cbc-4cc2-b09a-0608ecb6afce 2Gi RWX nfs-client 7m51s
root@k8s01:~/k8s# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-ea893912-3cbc-4cc2-b09a-0608ecb6afce 2Gi RWX Delete Bound default/nfst-pvc nfs-client 7m56s
root@k8s01:~/k8s# kubectl describe pv pvc-ea893912-3cbc-4cc2-b09a-0608ecb6afce
Name: pvc-ea893912-3cbc-4cc2-b09a-0608ecb6afce
Labels: <none>
Annotations: pv.kubernetes.io/provisioned-by: k8s-sigs.io/nfs-subdir-external-provisioner
Finalizers: [kubernetes.io/pv-protection]
StorageClass: nfs-client
Status: Bound
Claim: default/nfst-pvc
Reclaim Policy: Delete
Access Modes: RWX
VolumeMode: Filesystem
Capacity: 2Gi
Node Affinity: <none>
Message:
Source:
Type: NFS (an NFS mount that lasts the lifetime of a pod)
Server: 192.168.2.111
Path: /mnt/nvme/share/default-nfst-pvc-pvc-ea893912-3cbc-4cc2-b09a-0608ecb6afce
ReadOnly: false
Events: <none>
root@k8s01:~/k8s#
欢迎转载,请注明出处