Kubernetes 实操手册-数据持久化

简介

Pod 本身存在生命周期,因此其内部的容器及数据均无法持久存在。对于 Stateful 的应用(如数据库)以及日志文件等,我们都需要持久化数据,避免在应用重启后丢失数据。Docker 支持配置容器使用存储卷将数据持久化到容器自身文件系统外的存储空间之中。而 Kubernetes 提供了基于 Pod 的存储卷功能,Kubernetes 给我们提供了很多相关的资源用于管理持久化的数据,最常用的就是 PersistentVolumesPersistentVolumeClaims,下面我们将详细讲讲如何在 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 的定义部分。

常见的存储卷类型

这里只介绍几种常见的存储卷类型,详细的列表和介绍可以参考官方文档

emptyDirhostPath 属于节点级别的卷类型,他们依赖于特定的节点。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 后,根据设置删除存储卷或者手动删除。

image.png

几个生命周期相关的配置项如下:

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 等网络存储。对于数据库产品,使用数据库本身的集群复制、主从方案,在性能和可用性上要远远好于定期备份存储卷的方案,存储卷的备份可以作为一种辅助手段。 这篇文章就不在深入,后面有机会聊聊。

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容