简介
Pod 本身存在生命周期,因此其内部的容器及数据均无法持久存在。对于 Stateful 的应用(如数据库)以及日志文件等,我们都需要持久化数据,避免在应用重启后丢失数据。Docker 支持配置容器使用存储卷将数据持久化到容器自身文件系统外的存储空间之中。而 Kubernetes 提供了基于 Pod 的存储卷功能,Kubernetes 给我们提供了很多相关的资源用于管理持久化的数据,最常用的就是 PersistentVolumes
和 PersistentVolumeClaims
,下面我们将详细讲讲如何在 Kubernetes 集群中持久化数据。以下也是以目前最新的 Kubernetes v.1.14.1 版本做介绍,下面大部分的内容都是总结自官方文档,若对基础比较熟悉的可以跳过,直接进入实操部分。
Kubernetes 中的持久化存储设计
Kubernetes 中的存储卷称为 Volume,每个 Pod 可以不挂载或者挂载多个 Volumes,这个 Volume 就类似于 Docker 的 Volume,但是它的概念更一般化,可以是宿主机的路径,或者是 NFS 等网络文件系统,甚至是云服务商提供的存储卷等,详细可以参考官方文档。在 Pod 中配置 Volume 类似下面这样
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:
# directory location on host
path: /data
# this field is optional
type: Directory
一个是 volumes
配置项,用于声明这个 Pod 定义了哪些 voluems,另一个是 volumeMounts
配置项,用于把定义的 Volume 挂载到具体的容器的一个路径上,通过 volume 的 name 字段相关联。其他类型的存储卷配置也是一样,不同的就是 volumes 的定义部分。
常见的存储卷类型
这里只介绍几种常见的存储卷类型,详细的列表和介绍可以参考官方文档
emptyDir
和 hostPath
属于节点级别的卷类型,他们依赖于特定的节点。emptyDir
的生命周期与 Pod 一致,在 Pod 删除后会一并删除,主要用于 Pod 内部多容器之间共享数据,hostPath
就是将节点的目录直接挂载到 Pod 上,但是如果 Pod 被调度到其他节点,那么数据将不可用。而 nfs,cephfs,glusterfs 这些是常用的网络文件系统,可以供多个 Pod 同时链接。但是首先需要部署相应的网络文件系统,而且相对的性能会比较差,对于 IO 要求高的应用不太适合。另外还有公有云服务商提供的存储卷,如 AWS 的 ElasticBlockStore,Azure 的 AzureDisk 等,可以使用基于公有云的存储服务提供满足需求的存储卷类型。
早期的 Kubernetes 将 Volume 整合在核心代码之中,这非常不便于功能的扩展,后来在 1.9 版本就提出了使用 CSI(Container Storage Interface)作为统一的存储卷接口(文档),1.14.1 版本处于 GA 阶段,详细文档可参考这里。这样存储卷也就类似于 CNI 一样,可以独立于核心代码,便于插件化扩展功能。
存储卷解藕
上面的方式可以快速创建存储卷,但是这样的用法有个问题,一般来说应用的模版是由开发人员编辑的,模版的编辑者必须知道存储卷的详细信息,但是不利于存储卷的统一管理。这里 Kubernetes 就设计了一个解藕的方案,即 PersistentVolumes
(PV) 和 PersistentVolumeClaims
(PVC),由存储卷管理员(或者运维部门)负责管理所有的存储卷,定义 PersistentVolumes
,这样 volumes 就类似于 node 一样是集群的资源,而开发人员只需要按需使用即可,声明 PersistentVolumeClaims
,无需关心各个存储卷的细节。
例如,管理员可以按如下模版定义 PV
apiVersion: v1
kind: PersistentVolume
metadata:
name: test-pv
labels:
type: test
spec:
capacity:
storage: "5Gi"
accessModes:
- ReadWriteOnce
mountOptions:
- hard
- nfsvers=4.1
nfs:
path: /tmp
server: 172.17.0.2
使用 kubectl get pv
可以看到 PV 的状态是 Available。
然后开发人员使用如下模版定义 PVC
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: test-pvc
labels:
type: test
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: "5Gi"
selector:
matchLabels:
type: test
然后查看 PV 的状态就是 Bound 了。
参数详解
PV
1. capacity: 指定 PV 的容量,目前只支持 storage 指定空间大小
2. accessModes: 指定 PV 的访问模式,有以下几种,PVC 指定的模式必须与对应的 PV 一致
a. ReadWriteOnce: 仅可被耽搁节点挂载读写,简写为 RWO
b. ReadOnlyMany: 可被多个节点同时只读挂载,简写为 ROX
c. ReadWriteMany: 可被多个节点同时读写挂载,简写为 RWX
3. persistentVolumeReclaimPolicy: PV 空间释放时的处理机制,有以下几种
a. Retain: 保持不动
b. Recycle: 回收空间,即删除所有文件,仅部分类型支持
c. Delete: 删除存储卷,仅部分云存储支持
4. volumeMode: 卷类型,用作文件系统还是裸格式的块设备,默认文件系统 Filesystem
5. storageClassName: PV 所属的存储类名称,默认为空,下面会讲
6. mountOptions: 挂载选项列表
PVC
1. accessModes: PVC 的访问模式,必须与 PV 一致
2. resources: PVC 需要占用的资源最小值,目前仅支持 storage 指定空间大小
3. seletor: 绑定 PV 的标签选择器或条件表达式,类似与 Pod 的 Node 选择器
4. storageClassName: 所依赖的存储类名称,下面会讲
5. volumeMode: 卷类型,同 PV
6. volumeName: 用于直接指定要绑定的 PV 卷名
注意,PVC 是命名空间隔离的,如果使用了多命名空间,ROX/RWX 类型的 claim 必须位于同一个命名空间。
存储类
上面的解藕方式能够区分管理员和开发人员的工作,但是每次新建存储卷都需要管理员预先创建 PV(麻烦),或者由管理员一次创建大量 PV 供开发人员使用(浪费)。对于管理人员来说还是不太友好(不够懒!),所以存储类(storage class)就应运而生了。
存储类是 Kubernetes 为管理 PV 创建的逻辑类别,类似于面向对象编程的类,而 PV 就是具体存储类的实例。有了存储类之后,管理员就可以预先定义许多不同类型的存储类,例如 ssd,fast,cold 等等,而后由开发人员发起 PVC 申请创建具体的 PV 使用,不需要管理员直接参与 PV 的创建了。
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: ebs-sc
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
定义如上的 storage class,在需要时创建 AWS 的 EBS 作为存储卷,然后开发人员用 PVC 申请即可。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ebs-claimspec:
accessModes:
- ReadWriteOnce
storageClassName: ebs-sc
resources:
requests:
storage: 4Gi
这样就实现了存储卷的动态供给。
参数详解
1. provisioner: 存储卷供给方
2. reclaimPolicy: 存储卷回收策略
a. Delete: 删除,默认
b. Retain: 保持不动,需要手动删除
3. parameters: 其他参数,根据供给方不同而不同
4. mountOptions: 挂载选项列表
5. volumeBindingMode: 控制绑定的方式
a. Immediate: 当 PVC 创建时立即创建,默认
b. WaitForFirstConsumer: 延迟到当 Pod 使用时创建
存储卷生命周期
PV 的生命周期如下所示,PV 由供给(Provisioniong)创建,管理员手动创建的是静态供给,由存储类生成的是动态供给。当有 PVC 申请 PV 后,PVC 与 PV 绑定,PV 和 PVC 都进入 Bound 状态。在 Pod 使用完成删除了 PVC 之后,PV 进入 Released 状态,表明 PV 与 PVC 解绑了,但还未回收。回收 PV 后,根据设置删除存储卷或者手动删除。

几个生命周期相关的配置项如下:
PV 的 persistentVolumeReclaimPolicy
控制 PV 的回收策略,StorageClass 的 reclaimPolicy
控制生成的 PV 的回收策略,volumeBindingMode
控制生成的 PV 的绑定策略,详见上面的参数详解。
生产实操
Kubernetes 使用 AWS EBS 作为存储
简介
如果在公有云上部署和运行 Kubernetes 集群,那么使用公有云提供的存储服务将是提高性能和可用性的最佳选择。我们以 AWS 为例,AWS 提供了 EBS 作为一般需求的块存储。EBS 提供了 SSD,高 IOPS SSD,HDD,冷数据 HDD 等多种类型可供选择,同时也提供了方便快速的快照功能,底层数据加密等,可满足日常的绝大部分使用场景。
前提
项目页面介绍了需要使用 AWS EBS 的前提条件
Get yourself familiar with how to setup Kubernetes on AWS and have a working Kubernetes cluster:
Enable flag --allow-privileged=true for kubelet and kube-apiserver
Enable kube-apiserver feature gates --feature-gates=CSINodeInfo=true,CSIDriverRegistry=true,CSIBlockVolume=true,VolumeSnapshotDataSource=true
Enable kubelet feature gates --feature-gates=CSINodeInfo=true,CSIDriverRegistry=true,CSIBlockVolume=true
具体的操作如下:
二进制方式安装的话,在 kube-apiserver 的启动参数上增加--allow-privileged=true --feature-gates=CSINodeInfo=true,CSIDriverRegistry=true,CSIBlockVolume=true,VolumeSnapshotDataSource=true
。
如果使用 kubeadm 初始化,则修改初始化配置文件像下面这样,详细的安装步骤可以参考这篇文章,主要是添加 api-server 的两行配置
apiServer:
extraArgs:
authorization-mode: Node,RBAC
allow-privileged: "true" # add allow-privileged for api-server
feature-gates: "CSINodeInfo=true,CSIDriverRegistry=true,CSIBlockVolume=true,VolumeSnapshotDataSource=true" # enable feature-gates for api-server
timeoutForControlPlane: 4m0s
apiVersion: kubeadm.k8s.io/v1beta1
certificatesDir: /etc/kubernetes/pki
clusterName: kubernetes
controlPlaneEndpoint: ""
controllerManager: {}
dns:
type: CoreDNS
etcd:
local:
dataDir: /var/lib/etcd
imageRepository: k8s.gcr.io
kind: ClusterConfiguration
kubernetesVersion: v1.14.1
networking:
dnsDomain: cluster.local
podSubnet: 10.244.0.0/16
serviceSubnet: 10.96.0.0/12
scheduler: {}
kubelet 的参数不用修改,1.14.1 版本的 kubelet 默认已启用。
安装
配置权限
首先确保集群的每个实例有足够的 IAM 权限来创建和删除 EBS,最简单的办法是给集群的每个实例赋予一个 IAM Role,给这个 IAM Role EC2 Full Access 的权限,或者至少包含如下的权限
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:AttachVolume",
"ec2:CreateSnapshot",
"ec2:CreateTags",
"ec2:CreateVolume",
"ec2:DeleteSnapshot",
"ec2:DeleteTags",
"ec2:DeleteVolume",
"ec2:DescribeInstances",
"ec2:DescribeSnapshots",
"ec2:DescribeTags",
"ec2:DescribeVolumes",
"ec2:DetachVolume"
],
"Resource": "*"
}
]
}
另外也可以用 AWS secret key 的方式写入每个机器的 profile 文件(参考文档)或者放入集群的 Secret。
部署驱动
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/aws-ebs-csi-driver/master/deploy/kubernetes/manifest.yaml
查看驱动是否运行
kubectl get pods -n kube-system
会有类似这样的 Pod
ebs-csi-controller-0 6/6 Running 0 2d16h
ebs-csi-node-nfttl 3/3 Running 0 2d16h
使用
更多的使用示例可以参考官方文档,这里详细说明下几个常用的场景。
动态分配
动态分配是由管理员创建存储类模版,而用户使用时只需要从对应的存储类中创建申请即可,用户不需要关心每个存储类的具体细节。
创建 storage class 模版storageclass.yaml
kind: StorageClassapiVersion: storage.k8s.io/v1
metadata:
name: ebs-sc
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
创建
kubectl apply -f storageclass.yaml
创建 PersistentVolumeClaim 存储申请模版claim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ebs-claim
spec:
accessModes:
- ReadWriteOnce
storageClassName: ebs-sc
resources:
requests:
storage: 4Gi
创建
kubectl apply -f claim.yaml
查看 PersistentVolumeClaim 的状态,显示为 Pending,有容器挂载后即可使用。
kubectl get pvc
创建一个测试用的 Pod pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: centos
command: ["/bin/sh"]
args: ["-c", "while true; do echo $(date -u) >> /data/out.txt; sleep 5; done"]
volumeMounts:
- name: persistent-storage
mountPath: /data
volumes:
- name: persistent-storage
persistentVolumeClaim:
claimName: ebs-claim
创建
kubectl apply -f pod.yaml
等待片刻,查看 PersistentVolumeClaim 和 PersistentVolume,即可看到状态显示为Bound
,且多了一块新创建的 Volume。
查看 Volume 的详细信息
kubectl describe pv
可看到类似下面这样的信息,描述了 EBS 的 volume ID,可以在 AWS 控制台看到。
Source:
Type: CSI (a Container Storage Interface (CSI) volume source)
Driver: ebs.csi.aws.com
VolumeHandle: vol-0e447f0fffaf978c9
ReadOnly: false
进入刚才创建的 Pod
kubectl exec app -ti bash
可以使用df -h
命令看到挂载的磁盘在/data
目录,且里面已经有内容在输出。
从快照创建盘
动态分配是每次启动 Pod 都会从设置的存储类中创建一个新的存储卷,而对于线上环境,我们往往需要的是持久化数据,而不是每次都新建。所以一种更常见的使用场景是我们定时对数据拍摄快照,而创建 Pod 后挂载使用最新快照的存储卷(数据更新相对不太频繁的情况)。
创建一个 snapshot class 资源snapshotclass.yaml
apiVersion: snapshot.storage.k8s.io/v1alpha1
kind: VolumeSnapshotClass
metadata:
name: csi-aws-vsc
snapshotter: ebs.csi.aws.com
创建
kubectl apply -f snapshotclass.yaml
像上面一样创建一个 Pod 并绑定一个 EBS,用于创建快照app.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: ebs-sc
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ebs-claim
spec:
accessModes:
- ReadWriteOnce
storageClassName: ebs-sc
resources:
requests:
storage: 4Gi
---
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: centos
command: ["/bin/sh"]
args: ["-c", "while true; do echo $(date -u) >> /data/out.txt; sleep 5; done"]
volumeMounts:
- name: persistent-storage
mountPath: /data
volumes:
- name: persistent-storage
persistentVolumeClaim:
claimName: ebs-claim
创建
kubectl apply -f app.yaml
查看创建的 Volume,并查看容器生产文件的时间,记一下这个时间,用于等会从快照创建后检查
kubectl describe pv
kubectl exec -it app cat /data/out.txt
然后从当前的 Volume Claim 创建 snapshot snapshot.yaml
apiVersion: snapshot.storage.k8s.io/v1alpha1
kind: VolumeSnapshot
metadata:
name: ebs-volume-snapshot
spec:
snapshotClassName: csi-aws-vsc
source:
name: ebs-claim
kind: PersistentVolumeClaim
创建
kubectl apply -f snapshot.yaml
查看 snapshot 的创建状态,也可以在 AWS 控制台上看到
kubectl describe volumesnapshot
等待状态栏显示的Ready To Use: true
,即表示快照创建成功。
然后我们删除 Pod 和 PersistentVolumeClaim,查看原先的 EBS 会被删除,但是快照还在。然后我们创建新的 PersistentVolumeClaim,并从之前的快照中恢复数据 restore-claim.yaml
。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ebs-snapshot-restored-claim
spec:
accessModes:
- ReadWriteOnce
storageClassName: ebs-sc
resources:
requests:
storage: 4Gi
dataSource:
name: ebs-volume-snapshot
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
注意dataSource
这段,创建
kubectl apply -f restore-claim.yaml
我们继续使用这个 PersistentVolumeClaim 创建新的 Pod restore-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: centos
command: ["/bin/sh"]
args: ["-c", "while true; do echo $(date -u) >> /data/out.txt; sleep 5; done"]
volumeMounts:
- name: persistent-storage
mountPath: /data
volumes:
- name: persistent-storage
persistentVolumeClaim:
claimName: ebs-snapshot-restored-claim
待容器启动后,我们可以查看一下生成的输出文件,是不是包含了之前的数据。
kubectl exec -it app cat /data/out.txt
这样就可以让 Pod 在失败后自动接上之前的数据,但是这还是基于快照的频率和数据更新的频率。对于两次快照之间的数据是没发恢复的,就需要采取其他的措施保留并恢复了,这里就不展开了。这种用法比较适用于数据不太频繁更新或者实时性要求不高的场景。
更多的 EBS 参数
我们可以修改创建的 EBS 的一些参数,详细的参数解释可以查看 AWS 的官方文档。主要是文件格式,EBS 类型,IOPS,加密。
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: ebs-sc
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
fsType: xfs
type: io1
iopsPerGB: "50"
encrypted: "true"
更复杂的场景
上面的用法主要适用于非频繁更新的场景,如每天一更新的只读数据,这样使用非常便于快速的横向扩展与成本控制。但是如果是频繁的写更新或者实时性要求较高的场景,如日志文件的收集,数据库等,建议的用法是使用各个应用提供的高可用方案,而不是使用存储卷来做高可用。例如,日志文件可以用 flufluentd 或者 logstash 等收集到 Elasticsearch 中持久化,长期备份文件可以用 NFS 或者 GlusterFS 等网络存储。对于数据库产品,使用数据库本身的集群复制、主从方案,在性能和可用性上要远远好于定期备份存储卷的方案,存储卷的备份可以作为一种辅助手段。 这篇文章就不在深入,后面有机会聊聊。
网友评论