Kubernetes Volume 相关概念
缺省情况下,一个运行中的容器对文件系统的写入都是发生在其分层文件系统的可写层。一旦容器运行结束,所有写入都会被丢弃。如果数据需要长期存储,那就需要对容器数据做持久化支持。
Kubernetes 和 Docker 类似,也是通过 Volume 的方式提供对存储的支持。Volume 被定义在 Pod 上,可以被 Pod 里的多个容器挂载到相同或不同的路径下。Kubernetes 中 Volume 的 概念与Docker 中的 Volume 类似,但不完全相同。具体区别如下:
Kubernetes 中的 Volume 与 Pod 的生命周期相同,但与容器的生命周期不相关。当容器终止或重启时,Volume 中的数据也不会丢失。
当 Pod 被删除时,Volume 才会被清理。并且数据是否丢失取决于 Volume 的具体类型,比如:emptyDir 类型的 Volume 数据会丢失,而 PV 类型的数据则不会丢失。
Volume 的核心是目录,可以通过 Pod 中的容器来访问。该目录是如何形成的、支持该目录的介质以及其内容取决于所使用的特定卷类型。要使用 Volume,需要为 Pod 指定为 Volume (spec.volumes
字段) 以及将它挂载到容器的位置 (spec.containers.volumeMounts
字段)。Kubernetes 支持多种类型的卷,一个 Pod 可以同时使用多种类型的 Volume。
容器中的进程看到的是由其 Docker 镜像和 Volume 组成的文件系统视图。 Docker 镜像位于文件系统层次结构的根目录,任何 Volume 都被挂载在镜像的指定路径中。Volume 无法挂载到其他 Volume 上或与其他 Volume 的硬连接。Pod 中的每个容器都必须独立指定每个 Volume 的挂载位置。
Kubernetes 目前支持多种 Volume 类型,大致如下:
awsElasticBlockStore
azureDisk
azureFile
cephfs
csi
downwardAPI
emptyDir
fc (fibre channel)
flocker
gcePersistentDisk
gitRepo
glusterfs
hostPath
iscsi
local
nfs
persistentVolumeClaim
projected
portworxVolume
quobyte
rbd
scaleIO
secret
storageos
vsphereVolume
注:这些 Volume 并非全部都是持久化的,比如: emptyDir、secret、gitRepo 等,就会随着 Pod 的消亡而消失。
Kubernetes 非持久化存储方式
下面我们对一些常见的 Volume 做一个基本的介绍。
emptryDir
emptryDir,顾名思义是一个空目录,它的生命周期和所属的 Pod 是完全一致的。emptyDir 类型的 Volume 在 Pod 分配到 Node 上时会被创建,Kubernetes 会在 Node 上自动分配一个目录,因此无需指定 Node 宿主机上对应的目录文件。这个目录的初始内容为空,当 Pod 从 Node 上移除(Pod 被删除或者 Pod 发生迁移)时,emptyDir 中的数据会被永久删除。
emptyDir Volume 主要用于某些应用程序无需永久保存的临时目录,在多个容器之间共享数据等。缺省情况下,emptryDir 是使用主机磁盘进行存储的。你也可以使用其它介质作为存储,比如:网络存储、内存等。设置 emptyDir.medium
字段的值为 Memory 就可以使用内存进行存储,使用内存做为存储可以提高整体速度,但是要注意一旦机器重启,内容就会被清空,并且也会受到容器内存的限制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 apiVersion: v1 kind: Pod metadata: name: test-pd spec: containers: - image: gcr.io/google_containers/test-webserver name: test-container volumeMounts: - mountPath: /cache name: cache-volume volumes: - name: cache-volume emptyDir: {}
hostPath
hostPath 类型的 Volume 允许用户挂载 Node 宿主机上的文件或目录到 Pod 中。大多数 Pod 都用不到这种 Volume,其缺点比较明显,比如:
由于每个节点上的文件都不同,具有相同配置(例如:从 podTemplate 创建的)的 Pod 在不同节点上的行为可能会有所不同。
在底层主机上创建的文件或目录只能由 root 写入。您需要在特权容器中以 root 身份运行进程,或修改主机上的文件权限才可以写入 hostPath 卷。
当然,存在即合理。这种类型的 Volume 主要用在以下场景中:
运行中的容器需要访问 Docker 内部的容器,使用 /var/lib/docker 来做为 hostPath 让容器内应用可以直接访问 Docker 的文件系统。
在容器中运行 cAdvisor,使用 /dev/cgroups 来做为 hostPath。
和 DaemonSet 搭配使用,用来操作主机文件。例如:日志采集方案 FLK 中的 FluentD 就采用这种方式来加载主机的容器日志目录,达到收集本主机所有日志的目的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 apiVersion: v1 kind: Pod metadata: name: test-pd spec: containers: - image: k8s.gcr.io/test-webserver name: test-container volumeMounts: - mountPath: /test-pd name: test-volume volumes: - name: test-volume hostPath: path: /data type: Directory
Kubernetes 持久化存储方式
Kubernetes 目前可以使用 PersistentVolume、PersistentVolumeClaim、StorageClass 三种 API 资源来进行持久化存储,下面分别介绍下各种资源的概念。
PV
PV 的全称是:PersistentVolume(持久化卷)。PersistentVolume 是 Volume 的一种类型,是对底层的共享存储的一种抽象。PV 由集群管理员进行创建和配置,就像节点 (Node) 是集群中的资源一样,PV 也是集群资源的一种。PV 包含存储类型、存储大小和访问模式。PV 的生命周期独立于 Pod,例如:当使用它的 Pod 销毁时对 PV 没有影响。
PersistentVolume 通过插件机制实现与共享存储的对接。Kubernetes 目前支持以下插件类型:
GCEPersistentDisk
AWSElasticBlockStore
AzureFile
AzureDisk
FC (Fibre Channel)
FlexVolume
Flocker
NFS
iSCSI
RBD (Ceph Block Device)
CephFS
Cinder (OpenStack block storage)
Glusterfs
VsphereVolume
Quobyte Volumes
HostPath
VMware Photon
Portworx Volumes
ScaleIO Volumes
StorageOS
PVC
PVC 的全称是:PersistentVolumeClaim(持久化卷声明),PVC 是用户对存储资源的一种请求。PVC 和 Pod 比较类似,Pod 消耗的是节点资源,PVC 消耗的是 PV 资源。Pod 可以请求 CPU 和内存,而 PVC 可以请求特定的存储空间和访问模式。对于真正使用存储的用户不需要关心底层的存储实现细节,只需要直接使用 PVC 即可。
StorageClass
由于不同的应用程序对于存储性能的要求也不尽相同,比如:读写速度、并发性能、存储大小等。如果只能通过 PVC 对 PV 进行静态申请,显然这并不能满足任何应用对于存储的各种需求。为了解决这一问题,Kubernetes 引入了一个新的资源对象:StorageClass,通过 StorageClass 的定义,集群管理员可以先将存储资源定义为不同类型的资源,比如快速存储、慢速存储等。
当用户通过 PVC 对存储资源进行申请时,StorageClass 会使用 Provisioner(不同 Volume 对应不同的 Provisioner)来自动创建用户所需 PV。这样应用就可以随时申请到合适的存储资源,而不用担心集群管理员没有事先分配好需要的 PV。
自动创建的 PV 以 ${namespace}-${pvcName}-${pvName}
这样的命名格式创建在后端存储服务器上的共享数据目录中。
自动创建的 PV 被回收后会以 archieved-${namespace}-${pvcName}-${pvName}
这样的命名格式存在后端存储服务器上。
Kubernetes 访问存储资源的方式
Kubernetes 目前可以使用三种方式来访问存储资源。
该种方式移植性比较差,可扩展能力差。把 Volume 的基本信息完全暴露给用户,有安全隐患。
集群管理员提前手动创建一些 PV。它们带有可供集群用户使用的实际存储的细节,之后便可用于 PVC 消费。
注:这种方式请求的 PVC 必须要与管理员创建的 PV 保持一致,如:存储大小和访问模式,否则不能将 PVC 绑定到 PV 上。
当集群管理员创建的静态 PV 都不匹配用户的 PVC 时,PVC 请求存储类 StorageClass,StorageClass 动态的为 PVC 创建所需的 PV。
注:此功能需要基于 StorageClass。集群管理员必须先创建并配置好请求的 StorageClass,只有请求的 StorageClass 存在的情况下才能进行动态的创建。
使用 PV 进行持久化存储实例
这里我们将介绍如何使用 PV 资源进行数据持久化,这也是本文的重点内容。我们将以 NFS 做为后端存储结合 PV 为例,讲解 Kubernetes 如何实现数据持久化。
部署 NFS 服务器
安装 NFS 服务端
1 2 # Ubuntu / Debian $ sudo apt install nfs-kernel-server
新建数据目录和设置目录权限
1 2 $ sudo mkdir -p /data/kubernetes/ $ sudo chmod 755 /data/kubernetes/
配置 NFS 服务端
NFS 的默认配置文件是 /etc/exports
,在该配置文件中添加下面的配置信息。
1 2 $ sudo vim /etc/exports /data/kubernetes *(rw,sync,no_root_squash)
配置文件说明:
/data/kubernetes 设置共享的数据的目录。
*
表示任何人都有权限连接,当然也可以设置成是一个网段、一个 IP、或者是域名。
rw 设置共享目录的读写权限。
sync 表示文件同时写入硬盘和内存。
no_root_squash 当登录 NFS 主机使用共享目录的使用者是 root 时,其权限将被转换成为匿名使用者,通常它的 UID 与 GID,都会变成 nobody 身份。
启动 NFS 服务端
1 $ sudo systemctl restart nfs-kernel-server
1 2 3 4 $ sudo rpcinfo -p|grep nfs 100003 3 tcp 2049 nfs 100003 4 tcp 2049 nfs 100003 3 udp 2049 nfs
1 2 $ cat /var/lib/nfs/etab /data/kubernetes *(rw,sync,wdelay,hide,nocrossmnt,secure,no_root_squash,no_all_squash,no_subtree_check,secure_locks,acl,no_pnfs,anonuid=65534,anongid=65534,sec=sys,rw,secure,no_root_squash,no_all_squash)
如果以上步骤都正常的话,到这里 NFS 服务端就已经正常安装完成。
安装 NFS 客户端
1 $ sudo apt-get install nfs-common
注:所有 Node 宿主机都需要安装 NFS 客户端。
1 2 3 4 5 6 7 8 9 $ sudo systemctl status rpcbind.service ● rpcbind.service - RPC bind portmap service Loaded: loaded (/lib/systemd/system/rpcbind.service; enabled; vendor preset: enabled) Active: active (running) since Tue 2018-08-07 09:54:29 CST; 49s ago Docs: man:rpcbind(8) Main PID: 17501 (rpcbind) Tasks: 1 (limit: 2313) CGroup: /system.slice/rpcbind.service └─17501 /sbin/rpcbind -f -w
1 2 3 $ sudo showmount -e 192.168.100.213 Export list for 192.168.100.213: /data/kubernetes *
1 2 $ sudo mkdir -p /data/kubernetes/ $ sudo mount -t nfs 192.168.100.213:/data/kubernetes/ /data/kubernetes/
挂载成功后,在客户端上面的目录中新建一个文件,然后检查在 NFS 服务端的共享目录下是否也会出现该文件。
1 2 3 4 5 6 7 # 在 NFS 客户端新建 $ sudo touch /data/kubernetes/test.txt # 在 NFS 服务端查看 $ sudo ls -ls /data/kubernetes/ total 0 0 -rw-r--r-- 1 root root 0 Aug 7 09:59 test.txt
实现静态 PV
新建 PV 资源
完成上面的共享存储后,我们就可以来使用 PV 和 PVC 来管理和使用这些共享存储。PV 作为存储资源主要包括存储能力、访问模式、存储类型、回收策略等关键信息。
下面我们来新建一个 PV 对象并使用 NFS 做为后端存储类型,该 PV 包括 1G 的存储空间、访问模式为 ReadWriteOnce、回收策略为 Recyle。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ vim pv1-nfs.yaml apiVersion: v1 kind: PersistentVolume metadata: name: pv1-nfs spec: capacity: storage: 1Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Recycle nfs: path: /data/kubernetes server: 192.168 .100 .213
注:Kubernetes 支持的 PV 类型有很多,比如常见的 Ceph、GlusterFs、NFS 等。更多的支持类型可以查看官方文档 。
我们先使用 Kubectl 创建该 PV 资源。
1 2 $ kubectl create -f pv1-nfs.yaml persistentvolume "pv1-nfs" created
从下面的结果,我们可以看到 pv1-nfs 已经创建成功。状态是 Available,这表示 pv1-nfs 准备就绪,可以被 PVC 申请。
1 2 3 $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pv1-nfs 1Gi RWO Recycle Available 46s
我们对上面的 PV 属性来做一个简单的解读。
Capacity(存储能力)
一般来说,一个 PV 对象都要指定一个存储能力,通过 PV 的 Capacity 属性来设置。这里的 storage=1Gi 表示设置存储空间的大小。
AccessModes(访问模式)
AccessModes 是用来对 PV 进行访问模式的设置,用于描述用户应用对存储资源的访问权限,访问权限包括下面几种方式:
ReadWriteOnce(RWO):读写权限,但是只能被单个节点挂载。
ReadOnlyMany(ROX):只读权限,可以被多个节点挂载。
ReadWriteMany(RWX):读写权限,可以被多个节点挂载。
注:一些 PV 可能支持多种访问模式,但是在挂载的时候只能使用一种访问模式,多种访问模式是不会生效的。
下图是一些常用的 Volume 插件支持的访问模式:
PersistentVolumeReclaimPolicy(回收策略)
当前 PV 设置的回收策略,我们这里指定的 PV 的回收策略为 Recycle。目前 PV 支持的策略有三种:
Retain(保留)- 保留数据,需要管理员手工清理数据。
Recycle(回收)- 清除 PV 中的数据,效果相当于执行 rm -rf /thevoluem/*
。
Delete(删除)- 与 PV 相连的后端存储完成 Volume 的删除操作,这种方式常见于云服务商的存储服务,比如 ASW EBS。
注:目前只有 NFS 和 HostPath 两种类型支持回收策略。设置为 Retain 这种策略会更加保险一些。
状态
一个 PV 的生命周期中,可能会处于 4 种不同的阶段。
Available(可用):表示可用状态,还未被任何 PVC 绑定。
Bound(已绑定):表示 PV 已经被 PVC 绑定。
Released(已释放):PVC 被删除,但是资源还未被集群重新声明。
Failed(失败): 表示该 PV 的自动回收失败。
新建 PVC 资源
我们平时真正使用的资源其实是 PVC,就类似于我们的服务是通过 Pod 来运行的,而不是 Node,只是 Pod 跑在 Node 上而已。
首先,我们新建一个数据卷声明,向 PV 请求 1Gi 的存储容量。其访问模式设置为 ReadWriteOnce。
1 2 3 4 5 6 7 8 9 10 11 12 $ vim pvc1-nfs.yaml kind: PersistentVolumeClaim apiVersion: v1 metadata: name: pvc1-nfs spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi
在新建 PVC 之前,我们可以看下之前创建的 PV 的状态。
1 2 3 $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pv1-nfs 1Gi RWO Recycle Available 28m
我们可以看到当前 pv1-nfs 是在 Available 的一个状态,所以这个时候我们的 PVC 可以和这个 PV 进行绑定。
1 2 3 4 5 6 $ kubectl create -f pvc1-nfs.yaml persistentvolumeclaim "pvc1-nfs" created $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pvc1-nfs Bound pv1-nfs 1Gi RWO 34s
从上面的结果可以看到 pvc1-nfs 创建成功了,并且状态是 Bound 状态。这个时候我们再看下 PV 的状态。
1 2 3 $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pv1-nfs 1Gi RWO Recycle Bound default/pvc1-nfs 31m
同样我们可以看到 PV 也是 Bound 状态,对应的声明是 default/pvc1-nfs,表示 default 命名空间下面的 pvc1-nfs,表示我们刚刚新建的 pvc1-nfs 和 pv1-nfs 绑定成功。
PV 和 PVC 的绑定是系统自动完成的,不需要显示指定要绑定的 PV。系统会根据 PVC 中定义的要求去查找处于 Available 状态的 PV。
如果找到合适的 PV 就完成绑定。
如果没有找到合适的 PV 那么 PVC 就会一直处于 Pending 状态,直到找到合适的 PV 完成绑定为止。
下面我们来看一个例子,这里声明一个 PVC 的对象,它要求 PV 的访问模式是 ReadWriteOnce、存储容量 2Gi 和标签值为 app=nfs。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ vim pvc2-nfs.yaml kind: PersistentVolumeClaim apiVersion: v1 metadata: name: pvc2-nfs spec: accessModes: - ReadWriteOnce resources: requests: storage: 2Gi selector: matchLabels: app: nfs
我们先查看下当前系统的所有 PV 资源。
1 2 3 $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pv1-nfs 1Gi RWO Recycle Bound default/pvc1-nfs 52m
从结果可以看到,目前所有 PV 都是 Bound 状态,并没有 Available 状态的 PV。所以我们现在用上面新建的 PVC 是无法匹配到合适的 PV 的。我们来创建 PVC 看看:
1 2 3 4 5 6 7 $ kubectl create -f pvc2-nfs.yaml persistentvolumeclaim "pvc2-nfs" created $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pvc1-nfs Bound pv1-nfs 1Gi RWO 28m pvc2-nfs Pending 16s
从结果我们可以看到 pvc2-nfs 当前就是 Pending 状态,因为并没有合适的 PV 给这个 PVC 使用。现在我们来新建一个合适该 PVC 使用的 PV。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ vim pv2-nfs.yaml apiVersion: v1 kind: PersistentVolume metadata: name: pv2-nfs labels: app: nfs spec: capacity: storage: 2Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Recycle nfs: server: 192.168 .100 .213 path: /data/kubernetes
使用 Kubectl 创建该 PV。
1 2 3 4 5 6 7 $ kubectl create -f pv2-nfs.yaml persistentvolume "pv2-nfs" created $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pv1-nfs 1Gi RWO Recycle Bound default/pvc1-nfs 1h pv2-nfs 2Gi RWO Recycle Bound default/pvc2-nfs 18s
创建完 pv2-nfs 后,从上面的结果你会发现该 PV 已经是 Bound 状态了。其对应的 PVC 是 default/pvc2-nfs,这就证明 pvc2-nfs 终于找到合适的 PV 且完成了绑定。
1 2 3 4 $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pvc1-nfs Bound pv1-nfs 1Gi RWO 36m pvc2-nfs Bound pv2-nfs 2Gi RWO 9m
注:如果 PVC 申请的容量大小小于 PV 提供的大小,PV 同样会分配该 PV 所有容量给 PVC,如果 PVC 申请的容量大小大于 PV 提供的大小,此次申请就会绑定失败。
使用 PVC 资源
这里我们已经完成了 PV 和 PVC 创建,现在我们就可以使用这个 PVC 了。这里我们使用 Nginx 的镜像来创建一个 Deployment,将容器的 /usr/share/nginx/html
目录通过 Volume 挂载到名为 pvc2-nfs 的 PVC 上,并通过 NodePort 类型的 Service 来暴露服务。
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 $ vim nfs-pvc-deploy.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: nfs-pvc spec: replicas: 3 template: metadata: labels: app: nfs-pvc spec: containers: - name: nginx image: nginx:1.7.9 imagePullPolicy: IfNotPresent ports: - containerPort: 80 name: web volumeMounts: - name: www mountPath: /usr/share/nginx/html volumes: - name: www persistentVolumeClaim: claimName: pvc2-nfs --- apiVersion: v1 kind: Service metadata: name: nfs-pvc labels: app: nfs-pvc spec: type: NodePort ports: - port: 80 targetPort: web selector: app: nfs-pvc
使用 Kubectl 创建这个 Deployment。
1 2 3 4 5 6 7 8 9 10 11 $ kubectl create -f nfs-pvc-deploy.yaml deployment.extensions "nfs-pvc" created service "nfs-pvc" created $ kubectl get pods -o wide|grep nfs-pvc nfs-pvc-789788587b-ctp58 1/1 Running 0 4m 172.30.24.6 dev-node-02 nfs-pvc-789788587b-q294p 1/1 Running 0 4m 172.30.92.6 dev-node-03 nfs-pvc-789788587b-rtl5s 1/1 Running 0 4m 172.30.87.9 dev-node-01 $ kubectl get svc|grep nfs-pvc nfs-pvc NodePort 10.254.5.24 <none> 80:8682/TCP 5m
通过 NodePort 访问该服务。
1 2 3 4 5 6 7 $ curl -I http://192.168.100.211:8682 HTTP/1.1 403 Forbidden Server: nginx/1.7.9 Date: Tue, 07 Aug 2018 03:42:30 GMT Content-Type: text/html Content-Length: 168 Connection: keep-alive
我们可以看到 Nginx 返回了 403,这是因为我们用 NFS 中的共享目录做为 Nginx 的默认站点目录,目前这个 NFS 共享目录中没有可用的 index.html 文件。
1 2 $ ls /data/kubernetes/ test.txt
在 NFS 服务端共享目录下新建一个 index.html 的文件。
1 2 3 $ sudo sh -c "echo '<h1>Hello Kubernetes~</h1>' > /data/kubernetes/index.html" $ ls /data/kubernetes/ index.html test.txt
再次通过 NodePort 访问该服务。
1 2 $ curl http://192.168.100.211:8682 <h1>Hello Kubernetes~</h1>
使用 subPath 对同一个 PV 进行隔离
从上面的例子中,我们可以看到容器中的数据是直接放到共享数据目录根目录下的。如果有多个容器都使用一个 PVC 的话,这样就很容易造成文件冲突。Pod 中 volumeMounts.subPath
属性可用于指定引用卷内的路径,只需设置该属性就可以解决该问题。
修改刚才创建 Deployment 的 YAML 文件,增加 subPath 行。
1 2 3 4 5 6 7 8 $ vim nfs-pvc-deploy.yaml ... volumeMounts: - name: www subPath: nginx-pvc-test mountPath: /usr/share/nginx/html ...
更改完 YAML 文件后,重新更新下 Deployment 即可。
1 2 3 4 5 $ kubectl apply -f nfs-pvc-deploy.yaml Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply deployment.extensions "nfs-pvc" configured Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply service "nfs-pvc" configured
更新完后,NFS 的数据共享目录下就会自动新增一个同 subPath 名字一样的目录。
1 2 $ ls /data/kubernetes/ index.html nginx-pvc-test/ test.txt
同样 nginx-pvc-test 目录下默认是空的。
1 $ ls /data/kubernetes/nginx-pvc-test/
新增一个 index 文件后访问该服务,一切安好。
1 2 3 $ sudo sh -c "echo '<h1>Hello Kubernetes~</h1>' > /data/kubernetes/nginx-pvc-test/index.html" $ curl http://192.168.100.211:8682 <h1>Hello Kubernetes~</h1>
验证 PVC 中的数据持久化
上面我们已经成功的在 Pod 中使用了 PVC 来做为存储,现在我们来验证下数据是否会丢失。我们分两种情况来验证:一种是直接删除 Deployment 和 Service,另一种是先删除 PVC 后再删除 Deployment 和 Service。
直接删除 Deployment 和 Service
在这种情况下数据会永久保存下来,删除 Deployment 和 Service 不会对数据造成任何影响。
1 2 3 $ kubectl delete -f nfs-pvc-deploy.yaml service "nfs-pvc" deleted deployment.extensions "nfs-pvc" deleted
1 2 $ ls /data/kubernetes/nginx-pvc-test/ index.html
先删除 PVC 后再删除 Deployment 和 Service
1 2 $ kubectl delete pvc pvc2-nfs persistentvolumeclaim "pvc2-nfs" deleted
我们可以看到 PVC 状态已经变成了 Terminating,但是现在数据共享目录中的文件和服务都是可以正常访问的。
1 2 3 4 5 6 7 8 9 10 $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pvc1-nfs Bound pv1-nfs 1Gi RWO 3h pvc2-nfs Terminating pv2-nfs 2Gi RWO 6m $ ls /data/kubernetes/nginx-pvc-test/ index.html $ curl http://192.168.100.211:8928 <h1>Hello Kubernetes~</h1>
这是因为还有 Pod 正在使用 pvc2-nfs 这个 PVC,那么对应的资源依然可用。如果无 Pod 继续使用 pvc2-nfs 这个 PVC,则相应 PVC 对应的资源就会被收回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ kubectl delete -f nfs-pvc-deploy.yaml deployment.extensions "nfs-pvc" deleted service "nfs-pvc" deleted $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pvc1-nfs Bound pv1-nfs 1Gi RWO 3h $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pv1-nfs 1Gi RWO Recycle Bound default/pvc1-nfs 4h pv2-nfs 2Gi RWO Recycle Available 25m $ ls /data/kubernetes/
从上面的结果我们可以看到 pvc2-nfs 这个 PVC 已经不存在了,pv2-nfs 这个 PV 的状态也变成 Available 了。由于我们设置的 PV 的回收策略是 Recycle,我们可以发现 NFS 的共享数据目录下面的数据也没了,这是因为我们把 PVC 给删除掉后回收了数据。
使用 StorageClass 实现动态 PV
上面的例子中我们学习了静态 PV 和 PVC 的使用方法,所谓静态 PV 就是我要使用的一个 PVC 的话就必须手动去创建一个 PV。
这种方式在很多使用场景下使用起来都不灵活,需要依赖集群管理员事先完成 PV 的建立。特别是对于 StatefulSet 类型的应用,简单的使用静态的 PV 就不是很合适了。这种情况下我们就需要用到动态 PV,动态 PV 的实现需要用到 StorageClass。
创建 Provisioner
要使用 StorageClass,我们就得安装对应的自动配置程序。比如:我们这里存储后端使用的是 NFS,那么我们就需要使用到一个对应的自动配置程序。支持 NFS 的自动配置程序就是 nfs-client ,我们把它称作 Provisioner。这个程序可以使用我们已经配置好的 NFS 服务器,来自动创建持久卷,也就是自动帮我们创建 PV。
以 Deployment 方式部署一个 Provisioner
根据实际情况将下面的环境变量 NFS_SERVER
、NFS_PATH
和 NFS 相关配置替换成你的对应的值。
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 $ vim nfs-client.yaml kind: Deployment apiVersion: extensions/v1beta1 metadata: name: nfs-client-provisioner spec: replicas: 1 strategy: type: Recreate template: metadata: labels: app: nfs-client-provisioner spec: serviceAccountName: nfs-client-provisioner containers: - name: nfs-client-provisioner image: quay.io/external_storage/nfs-client-provisioner:latest volumeMounts: - name: nfs-client-root mountPath: /persistentvolumes env: - name: PROVISIONER_NAME value: fuseim.pri/ifs - name: NFS_SERVER value: 192.168 .100 .213 - name: NFS_PATH value: /data/kubernetes volumes: - name: nfs-client-root nfs: server: 192.168 .100 .213 path: /data/kubernetes
使用 Kubectl 命令建立这个 Deployment
1 2 $ kubectl create -f nfs-client.yaml deployment.extensions "nfs-client-provisioner" created
给 nfs-client-provisioner 创建 ServiceAccount
从 Kubernetes 1.6 版本开始,API Server 启用了 RBAC 授权。Provisioner 要想在 Kubernetes 中创建对应的 PV 资源,就得有对应的权限。
这里我们新建一个名为 nfs-client-provisioner 的 ServiceAccount 并绑定在一个名为 nfs-client-provisioner-runner 的 ClusterRole 上。该 ClusterRole 包含对 PersistentVolumes 的增、删、改、查等权限。
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 $ vim nfs-client-sa.yaml apiVersion: v1 kind: ServiceAccount metadata: name: nfs-client-provisioner --- kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 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: ["list", "watch", "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
使用 Kubectl 命令建立这个 ServiceAccount。
1 2 3 4 $ kubectl create -f nfs-client-sa.yaml serviceaccount "nfs-client-provisioner" created clusterrole.rbac.authorization.k8s.io "nfs-client-provisioner-runner" created clusterrolebinding.rbac.authorization.k8s.io "run-nfs-client-provisioner" created
这里我们创建了一个名为 course-nfs-storage 的 StorageClass 对象,注意下面的 Provisioner 对应的值一定要和上面的 Deployment下面 PROVISIONER_NAME 这个环境变量的值一样。
1 2 3 4 5 6 7 $ vim nfs-client-class.yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: course-nfs-storage provisioner: fuseim.pri/ifs # or choose another name, must match deployment's env PROVISIONER_NAME'
使用 Kubectl 命令建立这个 StorageClass。
1 2 $ kubectl create -f nfs-client-class.yaml storageclass.storage.k8s.io "course-nfs-storage" created
以上都创建完成后查看下相关资源的状态。
1 2 3 4 5 6 7 $ kubectl get pods|grep nfs-client NAME READY STATUS RESTARTS AGE nfs-client-provisioner-9d94b899c-nn4c7 1/1 Running 0 1m $ kubectl get storageclass NAME PROVISIONER AGE course-nfs-storage fuseim.pri/ifs 1m
手动创建的一个 PVC 对象
我们这里就来建立一个能使用 StorageClass 资源对象来动态建立 PV 的 PVC,要创建使用 StorageClass 资源对象的 PVC 有以下两种方法。
方法一:在这个 PVC 对象中添加一个 Annotations 属性来声明 StorageClass 对象的标识。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 这里我们声明了一个 PVC 对象,采用 ReadWriteMany 的访问模式并向 PV 请求 100Mi 的空间。 $ vim test-pvc.yaml kind: PersistentVolumeClaim apiVersion: v1 metadata: name: test-pvc annotations: volume.beta.kubernetes.io/storage-class: "course-nfs-storage" spec: accessModes: - ReadWriteMany resources: requests: storage: 100Mi
方法二:把名为 course-nfs-storage 的 StorageClass 设置为 Kubernetes 的默认后端存储。
1 2 $ kubectl patch storageclass course-nfs-storage -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}' storageclass.storage.k8s.io "course-nfs-storage" patched
上面这两种方法都是可以的,为了不影响系统的默认行为,这里我们采用第一种方法,直接使用 YAML 文件创建即可。
1 2 $ kubectl create -f test-pvc.yaml persistentvolumeclaim "test-pvc" created
创建完成后,我们来看看对应的资源是否创建成功。
1 2 3 4 5 6 7 8 9 10 $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pvc1-nfs Bound pv1-nfs 1Gi RWO 4h test-pvc Bound pvc-3d8d6ecf-9a13-11e8-9a96-001c42c61a79 100Mi RWX course-nfs-storage 41s $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pv1-nfs 1Gi RWO Recycle Bound default/pvc1-nfs 5h pv2-nfs 2Gi RWO Recycle Available 1h pvc-3d8d6ecf-9a13-11e8-9a96-001c42c61a79 100Mi RWX Delete Bound default/test-pvc course-nfs-storage 2m
从上面的结果我们可以看到一个名为 test-pvc 的 PVC 对象创建成功并且状态已经是 Bound 了。对应也自动创建了一个名为 pvc-3d8d6ecf-9a13-11e8-9a96-001c42c61a79 的 PV 对象,其访问模式是 RWX,回收策略是 Delete。STORAGECLASS 栏中的值也正是我们创建的 StorageClass 对象 course-nfs-storage。
我们用一个简单的示例来测试下用 StorageClass 方式声明的 PVC 对象是否能正常存储。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 $ vim test-pod.yaml kind: Pod apiVersion: v1 metadata: name: test-pod spec: containers: - name: test-pod image: busybox imagePullPolicy: IfNotPresent command: - "/bin/sh" args: - "-c" - "touch /mnt/SUCCESS && exit 0 || exit 1" volumeMounts: - name: nfs-pvc mountPath: "/mnt" restartPolicy: "Never" volumes: - name: nfs-pvc persistentVolumeClaim: claimName: test-pvc
上面这个 Pod 的作用非常简单,就是在一个 busybox 容器里的 /mnt 目录下面新建一个 SUCCESS 的文件,而 /mnt 目录是挂载到 test-pvc 这个资源对象上的。
1 2 $ kubectl create -f test-pod.yaml pod "test-pod" created
完成 Pod 创建后,我们可以在 NFS 服务器的共享数据目录下面查看数据是否存在。我们可以看到下面有一个名字很长的文件夹,这个文件夹的命名方式是:${namespace}-${pvcName}-${pvName}
。
1 2 $ ls /data/kubernetes/ default-test-pvc-pvc-3d8d6ecf-9a13-11e8-9a96-001c42c61a79
再看下这个文件夹下面的文件。
1 2 $ ls /data/kubernetes/default-test-pvc-pvc-3d8d6ecf-9a13-11e8-9a96-001c42c61a79/ SUCCESS
我们看到下面有一个 SUCCESS 的文件,说明 PV 对应的存储里可以成功写入文件。
自动创建的一个 PVC 对象
在上面的演示过程中,我们可以看到是手动创建的一个 PVC 对象,而在实际使用中更多使用 StorageClass 的是 StatefulSet 类型的服务。
StatefulSet 类型的服务是可以通过一个 volumeClaimTemplates 属性来直接使用 StorageClass。volumeClaimTemplates 其实就是一个 PVC 对象的模板,类似于 StatefulSet 下面的 template,而这种模板可以动态的去创建相应的 PVC 对象。
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 $ vim test-statefulset-nfs.yaml apiVersion: apps/v1beta1 kind: StatefulSet metadata: name: nfs-web spec: serviceName: "nginx" replicas: 3 template: metadata: labels: app: nfs-web spec: terminationGracePeriodSeconds: 10 containers: - name: nginx image: nginx:1.7.9 ports: - containerPort: 80 name: web volumeMounts: - name: www mountPath: /usr/share/nginx/html volumeClaimTemplates: - metadata: name: www annotations: volume.beta.kubernetes.io/storage-class: course-nfs-storage spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 1Gi
使用 Kubectl 命令建立这个 StatefulSet 对象。
1 2 $ kubectl create -f test-statefulset-nfs.yaml statefulset.apps "nfs-web" created
创建完成后可以看到上面 StatefulSet 对象中定义的 3 个 Pod 已经运行成功。
1 2 3 4 5 $ kubectl get pods NAME READY STATUS RESTARTS AGE nfs-web-0 1/1 Running 0 19s nfs-web-1 1/1 Running 0 16s nfs-web-2 1/1 Running 0 6s
再查看下 PVC 和 PV 对象。
1 2 3 4 5 6 7 8 9 10 11 $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE www-nfs-web-0 Bound pvc-16ba792f-9a15-11e8-9a96-001c42c61a79 1Gi RWO course-nfs-storage 1m www-nfs-web-1 Bound pvc-18c631d4-9a15-11e8-9a96-001c42c61a79 1Gi RWO course-nfs-storage 1m www-nfs-web-2 Bound pvc-1ed50c38-9a15-11e8-9a96-001c42c61a79 1Gi RWO course-nfs-storage 1m $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pvc-16ba792f-9a15-11e8-9a96-001c42c61a79 1Gi RWO Delete Bound default/www-nfs-web-0 course-nfs-storage 3m pvc-18c631d4-9a15-11e8-9a96-001c42c61a79 1Gi RWO Delete Bound default/www-nfs-web-1 course-nfs-storage 3m pvc-1ed50c38-9a15-11e8-9a96-001c42c61a79 1Gi RWO Delete Bound default/www-nfs-web-2 course-nfs-storage 3m
我们可以看到生成了 3 个 PVC 对象,名称由模板名称加上 Pod 的名称组合而成,而这 3 个 PVC 对象也都是绑定状态。
1 2 3 4 5 6 $ ls /data/kubernetes/ -l total 16 drwxrwxrwx 2 root root 4096 Aug 7 15:32 default-test-pvc-pvc-3d8d6ecf-9a13-11e8-9a96-001c42c61a79 drwxrwxrwx 2 root root 4096 Aug 7 15:40 default-www-nfs-web-0-pvc-16ba792f-9a15-11e8-9a96-001c42c61a79 drwxrwxrwx 2 root root 4096 Aug 7 15:40 default-www-nfs-web-1-pvc-18c631d4-9a15-11e8-9a96-001c42c61a79 drwxrwxrwx 2 root root 4096 Aug 7 15:40 default-www-nfs-web-2-pvc-1ed50c38-9a15-11e8-9a96-001c42c61a79
部署一个使用 StorageClass 的应用
上面的例子中都是简单的运行了一个 Nginx 来演示功能,接下来我们用 Helm 来部署一个具体的应用看看效果。如果你对 Helm 还不够了解,可以先读读 「Helm 入门指南 」一文。
这里我们同样以部署 DokuWiki 的为例。在「利用 Helm 快速部署 Ingress 」一文中我们在部署时关闭了 PersistentVolume。现在我们就演示加上 PersistentVolume 的效果。
DokuWiki 默认是启用 Persistence 特性的,这里主要通过 persistence.apache.storageClass
、persistence.apache.size
和 persistence.dokuwiki.storageClass
、persistence.dokuwiki.size
几个参数来设置 Apache 和 DokuWiki 两个应用对应的 storageClass 名称和存储大小 。
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 $ cd /home/k8s/charts/stable $ helm install --name dokuwiki --set "ingress.enabled=true,ingress.hosts[0].name=wiki.hi-linux.com,persistence.apache.storageClass=course-nfs-storage,persistence.apache.size=500Mi,persistence.dokuwiki.storageClass=course-nfs-storage,persistence.dokuwiki.size=500Mi" dokuwiki NAMESPACE: default STATUS: DEPLOYED RESOURCES: ==> v1/Secret NAME TYPE DATA AGE dokuwiki-dokuwiki Opaque 1 6m ==> v1/PersistentVolumeClaim NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE dokuwiki-dokuwiki-apache Bound pvc-1bfd0981-9af0-11e8-9a96-001c42c61a79 500Mi RWO course-nfs-storage 6m dokuwiki-dokuwiki-dokuwiki Bound pvc-1bffad3d-9af0-11e8-9a96-001c42c61a79 500Mi RWO course-nfs-storage 6m ==> v1/Service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE dokuwiki-dokuwiki LoadBalancer 10.254.95.241 <pending> 80:8592/TCP,443:8883/TCP 6m ==> v1beta1/Deployment NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE dokuwiki-dokuwiki 1 1 1 1 6m ==> v1beta1/Ingress NAME HOSTS ADDRESS PORTS AGE dokuwiki-dokuwiki wiki.hi-linux.com 80 6m ==> v1/Pod(related) NAME READY STATUS RESTARTS AGE dokuwiki-dokuwiki-bf9fb965c-d9x2w 1/1 Running 1 6m NOTES: ** Please be patient while the chart is being deployed ** 1. Get the DokuWiki URL indicated on the Ingress Rule and associate it to your cluster external IP: export CLUSTER_IP=$(minikube ip) # On Minikube. Use: `kubectl cluster-info` on others K8s clusters export HOSTNAME=$(kubectl get ingress --namespace default dokuwiki-dokuwiki -o jsonpath='{.spec.rules[0].host}') echo "Dokuwiki URL: http://$HOSTNAME/" echo "$CLUSTER_IP $HOSTNAME" | sudo tee -a /etc/hosts 2. Login with the following credentials echo Username: user echo Password: $(kubectl get secret --namespace default dokuwiki-dokuwiki -o jsonpath="{.data.dokuwiki-password}" | base64 --decode)
1 2 3 $ helm list NAME REVISION UPDATED STATUS CHART NAMESPACE dokuwiki 1 Wed Aug 8 17:47:48 2018 DEPLOYED dokuwiki-2.0.3 default
1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 应用对应的数据目录已经自动创建 $ ls /data/kubernetes/*dokuwiki* -ld drwxrwxrwx 3 root root 4096 Aug 8 17:52 /data/kubernetes/default-dokuwiki-dokuwiki-apache-pvc-1bfd0981-9af0-11e8-9a96-001c42c61a79 drwxrwxrwx 5 daemon daemon 4096 Aug 8 17:53 /data/kubernetes/default-dokuwiki-dokuwiki-dokuwiki-pvc-1bffad3d-9af0-11e8-9a96-001c42c61a79 # 查看应用对应的数据目录下文件 $ ls /data/kubernetes/default-dokuwiki-dokuwiki-dokuwiki-pvc-1bffad3d-9af0-11e8-9a96-001c42c61a79/ -l total 12 drwxr-xr-x 2 daemon daemon 4096 Aug 8 17:49 conf drwxr-xr-x 12 daemon daemon 4096 Aug 8 17:48 data drwxr-xr-x 5 daemon daemon 4096 Aug 8 17:48 lib $ ls /data/kubernetes/default-dokuwiki-dokuwiki-apache-pvc-1bfd0981-9af0-11e8-9a96-001c42c61a79/conf/ bitnami deflate.conf extra httpd.conf magic mime.types original vhosts
根据提示生成相应的登陆用户名和密码。
1 2 3 4 5 $ echo Username: user Username: user $ echo Password: $(kubectl get secret --namespace default dokuwiki-dokuwiki -o jsonpath="{.data.dokuwiki-password}" | base64 --decode) Password: e2GrABBkwF
通过浏览器访问该应用。效果图如下:
参考文档
http://www.google.com
http://t.cn/RmDscuQ
http://t.cn/RDqXk2U
http://t.cn/RDqX1qi
http://t.cn/RDqT4Xw
http://t.cn/RmDscuQ
http://t.cn/RDqg4D0
http://t.cn/RDqkyoC
http://t.cn/RDVE0bW
http://t.cn/R6GaBUK