本文发表已超过一年。旧文章可能包含过时的内容。请检查页面中的信息自发布以来是否仍然正确。

如何在数据密集型 Kubernetes 环境中处理数据重复

为何复制数据?

为每个团队创建带有其状态副本的应用副本是很方便的。例如,你可能需要一个独立的数据库副本来测试一些重大的 schema 变更,或者开发其他破坏性操作,比如批量插入/删除/更新...

复制数据需要大量时间。这是因为你需要先将所有数据从源块存储提供商下载到计算层,然后再将其发送回存储提供商。这个过程会消耗大量的网络流量和 CPU/RAM 资源。通过将某些耗时操作卸载到专用硬件实现的硬件加速总是能带来巨大的性能提升。它能将完成操作所需的时间减少几个数量级。

卷快照来救援

Kubernetes 在 1.12 中引入了作为 alpha 特性的卷快照(VolumeSnapshots),在 1.17 中转为 beta,并在 1.20 中正式发布 (GA)。卷快照使用存储提供商的专用 API 来复制数据卷。

由于数据已经存在于同一个存储设备(设备阵列)中,对于支持本地快照的存储提供商(绝大多数本地存储提供商)而言,复制数据通常只是一个元数据操作。你只需让一个新磁盘指向一个不可变快照,然后只保存增量数据(或者让它进行全盘复制)。作为存储后端内部的操作,这要快得多,而且通常不涉及网络流量。公有云存储提供商的底层工作方式有所不同。它们将快照保存到对象存储中,然后在“复制”磁盘时再从对象存储复制回块存储。从技术上讲,云提供商会花费大量的计算和网络资源,但从 Kubernetes 用户的角度来看,无论存储提供商是使用本地快照还是远程快照,卷快照的工作方式都是一样的,并且在这个操作中不涉及计算和网络资源。

听起来我们已经找到解决方案了,对吗?

实际上,卷快照是受 Namespace 限制的,Kubernetes 会保护 Namespace 内的数据不被不同租户(Namespace)共享。Kubernetes 的这一限制是一个有意的设计决策,以防止在不同 Namespace 中运行的 Pod 挂载其他应用的持久卷声明(PersistentVolumeClaim,PVC)

一种解决方法是在同一个 Namespace 中创建多个包含重复数据的卷。但是,你很容易引用错误的副本。

所以,想法是通过 Namespace 来隔离团队/项目,以避免上述问题,并通常限制对生产 Namespace 的访问。

解决方案?外部创建黄金快照

绕过这个设计限制的另一种方法是在 Kubernetes 之外(而不是通过 Kubernetes)创建快照。这也被称为手动预置快照。接下来,我将把它导入为一个多租户黄金快照,可用于多个 Namespace。下面将以 AWS EBS (Elastic Block Storage) 和 GCE PD (Persistent Disk) 服务为例进行说明。

准备黄金快照的总体计划

  1. 在云提供商中找到包含你要克隆数据的磁盘(EBS/Persistent Disk)
  2. 创建磁盘快照(在云提供商控制台)
  3. 获取磁盘快照 ID

为每个团队克隆数据的总体计划

  1. 创建 Namespace “sandbox01”
  2. 将磁盘快照 (ID) 作为 VolumeSnapshotContent 导入 Kubernetes
  3. 在 Namespace "sandbox01" 中创建映射到 VolumeSnapshotContent 的 VolumeSnapshot
  4. 从 VolumeSnapshot 创建 PersistentVolumeClaim
  5. 使用 PVC 安装 Deployment 或 StatefulSet

步骤 1:确定磁盘

首先,你需要确定你的黄金源。在我的例子中,它是在“production” Namespace 中 PersistentVolumeClaim “postgres-pv-claim” 上的 PostgreSQL 数据库。

kubectl -n <namespace> get pvc <pvc-name> -o jsonpath='{.spec.volumeName}'

输出将类似于

pvc-3096b3ba-38b6-4fd1-a42f-ec99176ed0d90

步骤 2:准备黄金源

你需要这样做一次,或者每次你想刷新你的黄金数据时这样做。

创建磁盘快照

前往 AWS EC2 或 GCP Compute Engine 控制台,搜索标签与上次输出匹配的 EBS 卷(在 AWS 上)或 Persistent Disk(在 GCP 上)。在本例中,我看到:pvc-3096b3ba-38b6-4fd1-a42f-ec99176ed0d9

点击“创建快照”并为其命名。你可以在控制台手动操作,在 AWS CloudShell / Google Cloud Shell 中操作,或者在终端中操作。要在终端中创建快照,必须安装并配置 AWS CLI 工具 (aws) 或 Google 的 CLI (gcloud)。

以下是在 GCP 上创建快照的命令

gcloud compute disks snapshot <cloud-disk-id> --project=<gcp-project-id> --snapshot-names=<set-new-snapshot-name> --zone=<availability-zone> --storage-location=<region>
Screenshot of a terminal showing volume snapshot creation on GCP

GCP 快照创建

GCP 通过其 PVC 名称识别磁盘,因此是直接映射。在 AWS 中,你需要首先通过 CSIVolumeName AWS 标签(其值为 PVC 名称)找到卷,然后才能用于创建快照。

Screenshot of AWS web console, showing EBS volume identification

在 AWS 上确定磁盘 ID

标记已完成的卷 (volume-id) vol-00c7ecd873c6fb3ec,然后在 AWS 控制台创建 EBS 快照,或使用 aws cli

aws ec2 create-snapshot --volume-id '<volume-id>' --description '<set-new-snapshot-name>' --tag-specifications 'ResourceType=snapshot'

步骤 3:获取磁盘快照 ID

在 AWS 中,上述命令将输出类似于

"SnapshotId": "snap-09ed24a70bc19bbe4"

如果你使用 GCP 云,可以通过查询快照的给定名称,从 gcloud 命令中获取快照 ID

gcloud compute snapshots --project=<gcp-project-id> describe <new-snapshot-name> | grep id:

你应该会得到类似于

id: 6645363163809389170

步骤 4:为每个团队创建开发环境

现在我有了我的黄金快照,它是不可变的数据。每个团队都将获得这份数据的副本,团队成员可以根据需要修改它,因为会为每个团队创建一个新的 EBS/持久磁盘。

下面我将为每个 Namespace 定义一个清单。为了节省时间,你可以使用 sedyq 等工具,或者使用 Kustomize 这种 Kubernetes 感知的模板工具,或者在 CI/CD 流水线中使用变量替换来替换 Namespace 名称(例如将 “sandbox01” 更改为 “sandbox42”)。

以下是一个示例清单

---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotContent
metadata:
 name: postgresql-orders-db-sandbox01
 namespace: sandbox01
spec:
 deletionPolicy: Retain
 driver: pd.csi.storage.gke.io
 source:
   snapshotHandle: 'gcp/projects/staging-eu-castai-vt5hy2/global/snapshots/6645363163809389170'
 volumeSnapshotRef:
   kind: VolumeSnapshot
   name: postgresql-orders-db-snap
   namespace: sandbox01
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
 name: postgresql-orders-db-snap
 namespace: sandbox01
spec:
 source:
   volumeSnapshotContentName: postgresql-orders-db-sandbox01

在 Kubernetes 中,VolumeSnapshotContent (VSC) 对象是无 Namespace 限制的。但是,我需要为每个不同的 Namespace 使用一个单独的 VSC,因此每个 VSC 的 metadata.name 也必须不同。为了简化这一点,我将目标 Namespace 作为名称的一部分。

现在是时候将 driver 字段替换为你 K8s 集群中安装的 CSI (Container Storage Interface) 驱动程序了。主要的云提供商都有支持 VolumeSnapshots 的块存储 CSI 驱动程序,但 CSI 驱动程序默认情况下通常不会安装,请咨询你的 Kubernetes 提供商。

上面的清单定义了一个适用于 GCP 的 VSC。在 AWS 上,driver 和 SnashotHandle 的值可能看起来像

  driver: ebs.csi.aws.com
  source:
    snapshotHandle: "snap-07ff83d328c981c98"

此时,我需要使用 Retain(保留)策略,以便 CSI 驱动程序不会尝试删除我手动创建的 EBS 磁盘快照。

对于 GCP,你需要手动构建此字符串 - 添加完整的项目 ID 和快照 ID。对于 AWS,它只是一个简单的快照 ID。

VSC 还需要指定哪个 VolumeSnapshot (VS) 将使用它,因此 VSC 和 VS 是相互引用的。

现在我可以从上面的 VS 创建 PersistentVolumeClaim。首先设置这个很重要

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
 name: postgres-pv-claim
 namespace: sandbox01
spec:
 dataSource:
   kind: VolumeSnapshot
   name: postgresql-orders-db-snap
   apiGroup: snapshot.storage.k8s.io
 accessModes:
   - ReadWriteOnce
 resources:
   requests:
     storage: 21Gi

如果默认的 StorageClass 设置了 WaitForFirstConsumer 策略,那么实际的云磁盘只会当某个 Pod 绑定该 PVC 时才从黄金快照创建。

现在我将该 PVC 分配给我的 Pod(在我的例子中是 Postgresql),就像我处理其他任何 PVC 一样。

kubectl -n <namespace> get volumesnapshotContent,volumesnapshot,pvc,pod

VS 和 VSC 都应该是 READYTOUSE 为 true,PVC 已绑定,并且 Pod(来自 Deployment 或 StatefulSet)正在运行。

为了继续使用我的黄金快照中的数据,我只需要为下一个 namespace 重复此过程,大功告成!无需在复制过程上浪费时间和计算资源。