在 Kubernetes 上管理数据存储

195 阅读46分钟

没有无状态的架构。所有应用程序都会在某个地方存储状态。

——Alex Chircop, StorageOS CEO

在上一章中,我们描绘了一个可能的近未来场景,其中强大的、有状态的、数据密集型应用程序在 Kubernetes 上运行。要实现这一目标,我们需要构建用于持久化、流处理和分析的数据基础设施。为了构建这些基础设施,我们需要利用 Kubernetes 提供的原语来帮助管理云计算的三大核心资源:计算、网络和存储。在接下来的几章中,我们将开始探讨这些原语,首先从存储开始,看看它们如何组合在一起,创建我们所需的数据基础设施。

正如 Alex Chircop 提出的观点,所有应用程序都必须在某个地方存储它们的状态,这就是为什么我们将在本章中重点关注 Kubernetes 提供的用于与存储交互的基本抽象。我们还将研究存储供应商和开源项目所提供的新兴创新,这些创新旨在为 Kubernetes 创建符合云原生原则的存储基础设施。

让我们从探索容器化应用程序中的持久化管理开始,以此作为我们深入研究 Kubernetes 上数据存储的起点。

Docker、容器与状态

在分布式云原生应用程序中管理状态的问题并非 Kubernetes 独有。快速搜索一下会发现,有状态工作负载在其他容器编排平台(如 Mesos 和 Docker Swarm)上也是一个关注的领域。部分原因与容器编排的性质有关,部分原因则源于容器本身的特性。

首先,让我们考虑一下容器。容器的关键价值之一在于其短暂性。容器被设计为一次性且可替代的,因此它们需要快速启动,并尽可能少地使用资源进行开销处理。出于这个原因,大多数容器镜像都是从包含精简的、基于 Linux 的开源操作系统(如 Ubuntu)的基础镜像构建的,这些系统能够快速启动,并仅包含所需的应用程序或微服务的基本库。顾名思义,容器被设计为自包含的,包含所有依赖项的不可变镜像,而其配置和数据则是外部化的。这些特性使得容器具有可移植性,可以在任何兼容的容器运行时上运行。

如图 2-1 所示,容器比传统的虚拟机(VM)需要更少的开销。传统的 VM 每个都运行一个客户操作系统,并通过一个虚拟机管理程序层来将系统调用实现到底层主机操作系统上。

image.png

尽管容器使应用程序变得更加可移植,但让其数据变得可移植则是一个更大的挑战。由于容器本身是短暂的,因此要在容器生命周期之外存续的任何数据都必须定义为外部存储。对于容器技术来说,关键特性在于提供连接到持久存储的机制,而对于容器编排技术来说,关键特性在于能够调度容器,以便它们能够高效地访问持久存储。

在 Docker 中管理状态

让我们看看最流行的容器技术 Docker,看看容器如何存储数据。Docker 中的关键存储概念是卷(volume)。从 Docker 容器的角度来看,卷是一个可以支持只读或读写访问的目录。Docker 支持将多个数据存储挂载为卷。我们将介绍几种选项,以便稍后可以在 Kubernetes 中找到它们的对应项。

绑定挂载(Bind Mounts)

创建卷的最简单方法是将容器中的目录绑定到主机系统上的目录。这称为绑定挂载,如图 2-2 所示。

image.png

在 Docker 中启动容器时,可以使用 --volume-v 选项指定绑定挂载,并指定要使用的本地文件系统路径和容器路径。例如,您可以启动一个 Nginx Web 服务器实例,并将开发机上的本地项目文件夹映射到容器中。如果您已安装 Docker,可以在自己的环境中测试以下命令:

docker run -it --rm -d --name web -p 8080:80 \
  -v ~/site-content:/usr/share/nginx/html nginx

这个命令将 Web 服务器暴露在本地主机的 8080 端口上。如果本地路径目录不存在,Docker 运行时将创建它。Docker 允许您以只读或读写权限创建绑定挂载。由于卷被表示为一个目录,运行在容器中的应用程序可以将任何能够表示为文件的内容放入卷中——甚至是数据库。

绑定挂载对于开发工作非常有用。然而,在生产环境中使用绑定挂载并不合适,因为这会导致容器依赖于特定主机上的文件存在。这在单机部署中可能没问题,但生产部署通常分布在多个主机上。另一个问题是容器对主机文件系统的访问可能会造成潜在的安全漏洞。出于这些原因,我们需要在生产部署中采用另一种方法。

卷(Volumes)

在 Docker 中,首选的选项是使用卷。Docker 卷在主机文件系统的特定目录下由 Docker 创建和管理。可以使用 docker volume create 命令来创建卷。例如,您可以创建一个名为 site-content 的卷来存储网站文件:

docker volume create site-content

如果未指定名称,Docker 会分配一个随机名称。创建后,生成的卷可以使用 -v VOLUME-NAME:CONTAINER-PATH 的形式挂载到容器中。例如,您可以使用刚创建的卷让一个 Nginx 容器读取内容,同时允许另一个容器编辑内容,并使用 ro 选项来设置只读权限:

bash
复制代码
docker run -it --rm -d --name web \
  -v site-content:/usr/share/nginx/html:ro nginx

DOCKER 卷挂载语法

Docker 还支持一种 --mount 语法,允许您更明确地指定源文件夹和目标文件夹。这种表示法被认为是更现代的,但也更加冗长。前面示例中展示的语法仍然有效,并且更为常用。

正如我们所暗示的,Docker 卷可以同时挂载到多个容器中,如图 2-3 所示。

使用 Docker 卷的优势在于,Docker 管理容器的文件系统访问,这使得在容器上实施容量和安全限制变得更加简单。

image.png

Tmpfs 挂载

Docker 支持两种与主机操作系统相关的挂载类型:tmpfs(或临时文件系统)和命名管道。命名管道在 Docker for Windows 中可用,但由于它们通常不在 Kubernetes 中使用,所以我们在这里不会详细讨论。

当在 Linux 上运行 Docker 时,可以使用 tmpfs 挂载。tmpfs 挂载只在容器的生命周期内存在于内存中,因此内容永远不会存储在磁盘上,如图 2-4 所示。tmpfs 挂载对于那些需要持久化相对少量数据的应用程序特别有用,尤其是那些您不希望写入主机文件系统的敏感数据。由于数据存储在内存中,因此更快的访问速度是一个附带的好处。

image.png

要创建 tmpfs 挂载,可以使用 docker run 命令中的 --tmpfs 选项。例如,您可以使用如下命令指定一个 tmpfs 卷来存储处理敏感数据的 Web 服务器的 Nginx 日志:

docker run -it --rm -d --name web --tmpfs /var/log/nginx nginx

您还可以使用 --mount 选项,以便对可配置选项进行更精细的控制。

卷驱动(Volume Drivers)

Docker Engine 具有可扩展的架构,允许您通过插件添加自定义行为,这些插件涵盖了网络、存储和授权等功能。对于多个开源和商业提供商,包括公共云和各种网络文件系统,均有第三方存储插件可用。要利用这些插件,首先需要将插件安装到 Docker Engine 中,然后在启动使用该存储的 Docker 容器时指定相关的卷驱动程序,如图 2-5 所示。

image.png

有关使用 Docker 支持的各种类型卷的更多信息,请参阅 Docker 存储文档以及 docker run 命令的相关文档。

文件、块和对象存储

在现代云架构中,存储传统上通过三种主要格式提供给应用程序:文件、块和对象。每种格式以不同的方式存储和提供数据访问:

文件存储

文件存储将数据表示为文件夹的层次结构,每个文件夹都可以包含文件。文件是存储和检索的基本访问单元。要访问的根目录会挂载到容器文件系统中,使其看起来像其他目录一样。每个公共云都有其自己的文件存储服务(例如,Google Cloud Filestore 或 Amazon Elastic Filestore)。Gluster 是一个开源的分布式文件系统。这些系统中的许多都兼容网络文件系统(NFS),这是一种由 Sun Microsystems 在 1984 年发明的分布式文件系统协议,至今仍被广泛使用。

块存储

块存储将数据组织为块,并将这些块分配到一组管理的卷中。当您将数据提供给块存储系统时,它会将数据分割成大小不同的块,并分布这些块以最有效地利用底层卷。当您查询块存储系统时,它会从各种位置检索这些块并将数据返回给您。这种灵活性使得块存储在您拥有异构存储设备集时成为一个很好的解决方案。块存储不提供大量的元数据处理,这可能会给应用程序带来更大的负担。

对象存储

对象存储将数据组织为称为对象的单元。每个对象都有一个唯一标识符或密钥,并支持丰富的元数据标记以实现搜索。对象存储在桶(buckets)中组织。这种扁平的、非层次化的组织使对象存储易于扩展。S3 是对象存储的经典例子,大多数对象存储产品都声称兼容 S3 API。

如果您负责构建或选择数据基础设施,您需要了解这些模式的优缺点。在本书的其余部分,您将学习每种存储类型在各种数据基础设施项目中的使用方式。在选择存储格式以及是否使用集中式或分布式存储架构时,需要权衡一些因素。例如,在第 7 章中,我们将探讨一个经过重构的 Cassandra 版本,该版本使用对象存储进行长期持久化,而不是在本地磁盘上使用文件存储。

Kubernetes 数据存储资源

现在您已经了解了容器和云存储的基本概念,让我们看看 Kubernetes 提供了什么。在本节中,我们将介绍一些关键的 Kubernetes 概念或 API 中的资源,这些资源用于将存储附加到容器化应用程序。即使您已经对这些资源有所了解,您也不妨继续关注,因为我们将特别关注每个资源与有状态数据的关系。

Pods 和 Volumes

新用户在使用 Kubernetes 时首先接触的资源之一是 Pod。Pod 是 Kubernetes 工作负载的基本部署单元。它为运行容器提供了环境,Kubernetes 控制平面负责将 Pods 部署到 Kubernetes 工作节点(Worker Nodes)。

Kubelet 是运行在每个工作节点上的 Kubernetes 控制平面的一个组件。它负责在节点上运行 Pods,并监控这些 Pods 及其内部容器的健康状况。这些元素在图 2-6 中进行了总结。

image.png

尽管一个 Pod 可以包含多个容器,但最佳实践是让一个 Pod 只包含一个应用程序容器,以及可选的辅助容器,如图 2-6 所示。这些辅助容器可能包括在主应用程序容器之前运行的初始化容器(init containers),用于执行配置任务,或者与主应用程序容器一起运行的 sidecar 容器,提供诸如可观测性或管理等辅助服务。在后续章节中,您将看到数据基础设施部署如何利用这些架构模式。

现在让我们看看如何在这种 Pod 架构中支持持久性。与 Docker 类似,容器中的“磁盘上的”数据在容器崩溃时会丢失。Kubelet 负责重新启动容器,但这个新容器只是原容器的替代品——它将有一个不同的身份,并从一个全新的状态开始。

在 Kubernetes 中,术语“卷”(volume)用于表示 Pod 内对存储的访问。通过使用卷,容器可以持久化数据,这些数据将比容器(甚至可能比 Pod)存续更长时间。一个卷可以被 Pod 中的多个容器访问。每个容器在 Pod 内都有自己的 volumeMount,指定应将其挂载到的目录,从而允许挂载点在不同的容器之间有所不同。

在多个场景中,您可能希望在 Pod 中的容器之间共享数据:

  • 一个初始化容器创建一个自定义配置文件,供应用程序容器挂载以获取配置值。
  • 应用程序 Pod 写入日志,而 sidecar Pod 读取这些日志以识别报告给外部监控工具的警报条件。

然而,您可能希望避免多个容器写入同一个卷的情况,因为您必须确保多个写入操作不会发生冲突——Kubernetes 不会为您处理这些冲突。

准备运行示例代码

本书中的示例假设您可以访问正在运行的 Kubernetes 集群。对于本章中的示例,您可以使用在本地机器上运行的开发集群,例如 kind、K3s 或 Docker Desktop,这些都应该足够了。本节中使用的源代码可以在本书的代码仓库中找到。

在 Pod 中使用卷需要两个步骤:定义卷和在需要访问该卷的每个容器中挂载卷。让我们看看一个示例 YAML 配置,该配置定义了一个包含单个应用程序容器(Nginx Web 服务器)和一个卷的 Pod。源代码在本书的代码仓库中:

yaml
复制代码
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: my-app
    image: nginx
    volumeMounts:
    - name: web-data
      mountPath: /app/config
  volumes:
  - name: web-data

注意配置中的两个部分:卷在 spec.volumes 下定义,而卷的使用在 spec.containers.volumeMounts 下定义。首先,卷的名称在 volumeMounts 中引用,并通过 mountPath 指定要挂载的目录。在声明 Pod 规范时,卷和卷挂载必须结合在一起。要使您的配置有效,必须先声明卷,然后才能引用它,且卷必须由 Pod 中至少一个容器使用。

您可能还注意到,这个卷只有一个名称,并没有指定任何其他信息。您认为这会如何运行呢?您可以通过使用示例源代码文件 nginx-pod.yaml 或将上述配置剪切并粘贴到具有该名称的文件中,来自己尝试一下,然后在配置好的 Kubernetes 集群上执行 kubectl 命令:

bash
复制代码
kubectl apply -f nginx-pod.yaml

您可以使用 kubectl get pod 命令获取有关已创建 Pod 的更多信息,例如:

bash
复制代码
kubectl get pod my-pod -o yaml | grep -A 5 "  volumes:"

结果可能如下所示:

yaml
复制代码
volumes:
- emptyDir: {}
  name: web-data
- name: default-token-2fp89
  secret:
    defaultMode: 420

如您所见,Kubernetes 在创建请求的卷时提供了其他信息,并将其默认设置为 emptyDir 类型。其他默认属性可能会因您使用的 Kubernetes 引擎不同而有所差异,但我们在这里不再进一步讨论它们。

容器中可以挂载多种类型的卷,让我们来看一下。

临时卷

您可能还记得我们之前讨论 Docker 卷时提到的 tmpfs 卷,它为单个容器的生命周期提供临时存储。Kubernetes 提供了类似的概念,称为临时卷(ephemeral volume),不过它的作用范围是 Pod。前面示例中的 emptyDir 就是一种临时卷。

临时卷对于希望创建高速访问缓存的数据基础设施或其他应用程序来说非常有用。虽然它们不会在 Pod 的生命周期之外持久存在,但它们仍然可以表现出一些其他卷的典型特性,例如快照功能。临时卷的设置比持久卷(PersistentVolumes)稍微简单一些,因为它们完全内联声明在 Pod 定义中,无需引用其他 Kubernetes 资源。正如您接下来将看到的,创建和使用持久卷涉及更多步骤。

其他临时存储提供者

我们接下来将讨论的一些内置存储驱动程序和 CSI 存储驱动程序,这些驱动程序不仅提供持久卷(PersistentVolumes),还提供临时卷(ephemeral volume)选项。您可以查阅具体提供者的文档,了解有哪些可用选项。

配置卷

Kubernetes 提供了几种结构用于将配置数据以卷的形式注入到 Pod 中。这些卷类型也被视为临时卷,因为它们不提供允许应用程序持久化其自身数据的机制。

以下几种卷类型与本书的探索相关,因为它们为在 Kubernetes 上运行的应用程序和数据基础设施提供了有用的配置手段。我们将简要介绍它们:

ConfigMap 卷

ConfigMap 是一种 Kubernetes 资源,用于将应用程序外部的配置值存储为一组键值对。例如,一个应用程序可能需要底层数据库的连接详细信息,如 IP 地址和端口号。将这些信息定义在 ConfigMap 中是一种将其外部化的好方法。生成的配置数据可以作为卷挂载到应用程序中,在那里它将显示为一个目录。每个配置值都表示为一个文件,其中文件名是键,文件的内容包含值。有关将 ConfigMap 作为卷挂载的更多信息,请参阅 Kubernetes 文档。

Secret 卷

Secret 类似于 ConfigMap,但它旨在保护需要保护的敏感数据的访问。例如,您可能希望创建一个 Secret,其中包含数据库访问凭据,如用户名和密码。配置和访问 Secrets 与使用 ConfigMap 类似,但额外的好处是 Kubernetes 在 Pod 内访问时会帮助解密 Secret。有关将 Secret 作为卷挂载的更多信息,请参阅 Kubernetes 文档。

Downward API 卷

Kubernetes 的 Downward API 通过环境变量或卷暴露有关 Pods 和容器的元数据。这些元数据是由 kubectl 和其他客户端使用的相同元数据。

可用的 Pod 元数据包括 Pod 的名称、ID、命名空间、标签和注释。容器化应用程序可能会使用 Pod 信息进行日志记录和指标报告,或用于确定数据库或表的名称。

可用的容器元数据包括请求和最大资源量,如 CPU、内存和临时存储。容器化应用程序可能会利用这些信息来限制其自身的资源使用。有关将 Pod 信息作为卷注入的示例,请参阅 Kubernetes 文档。

hostPath 卷

hostPath 卷将文件或目录从运行 Pod 的 Kubernetes 工作节点(Worker Node)挂载到 Pod 中。这类似于 Docker 中讨论的绑定挂载概念。在使用 hostPath 卷时,相较于 emptyDir 卷有一个优势:数据在 Pod 重启后仍然存在。

然而,使用 hostPath 卷也有一些缺点。首先,为了让替代 Pod 访问原始 Pod 的数据,它需要在同一个工作节点上重新启动。虽然 Kubernetes 允许您使用亲和性控制 Pod 的放置节点,但这往往会限制 Kubernetes 调度程序对 Pod 的最佳放置选择,并且如果节点因某种原因宕机,hostPath 卷中的数据将会丢失。其次,与 Docker 绑定挂载一样,hostPath 卷在允许访问本地文件系统方面存在安全问题。出于这些原因,hostPath 卷仅推荐用于开发部署。

云卷

可以创建引用 Pod 所在工作节点之外的存储位置的 Kubernetes 卷,如图 2-7 所示。这些卷可以分为由命名的云提供商提供的卷类型,以及那些尝试提供更通用接口的卷类型。

这些包括以下内容:

  • awsElasticBlockStore 卷类型用于在 Amazon Web Services (AWS) 的 Elastic Block Store (EBS) 上挂载卷。许多数据库使用块存储作为其底层存储层。
  • gcePersistentDisk 卷类型用于挂载 Google Compute Engine (GCE) 的持久磁盘 (PD),这也是块存储的一个例子。
  • Microsoft Azure 支持两种类型的卷:azureDisk 用于 Azure 数据磁盘卷,azureFile 用于 Azure 文件卷。
  • cinder 卷类型可以用于访问 OpenStack 部署的 OpenStack Cinder 卷。

image.png

使用这些类型的存储通常需要在云提供商上进行配置,并且从 Kubernetes 集群访问时通常限制在同一云区域和账户内的存储。请查阅您的云提供商文档以获取更多详细信息。

其他卷提供者

还有许多其他卷提供者,它们提供的存储类型各不相同。以下是几个示例:

  • fibreChannel 卷类型可用于实现 Fibre Channel 协议的 SAN 解决方案。
  • gluster 卷类型用于通过之前提到的 Gluster 分布式文件系统访问文件存储。
  • iscsi 卷可将现有的 Internet 小型计算机系统接口(iSCSI)卷挂载到您的 Pod 中。
  • nfs 卷允许将现有的 NFS 共享挂载到 Pod 中。

我们将在“容器附加存储”一章中进一步探讨实现容器附加存储模式的更多卷提供者。表 2-1 比较了我们迄今为止讨论的 Docker 和 Kubernetes 存储概念。

表 2-1. 比较 Docker 和 Kubernetes 的存储选项

存储类型DockerKubernetes
访问来自不同提供商的持久存储Volume(通过卷驱动程序访问)Volume(通过内置或 CSI 驱动程序访问)
访问主机文件系统(不推荐用于生产环境)Bind mounthostPath volume
容器(或 Pod)运行时可用的临时存储TmpfsemptyDir 和其他临时卷
配置和环境数据(只读)(无直接等效)ConfigMap、Secret、Downward API

如何选择 Kubernetes 存储解决方案?

鉴于可用的存储选项繁多,确定应该为您的应用程序使用哪种存储类型确实可能令人感到有些困惑。除了确定您需要文件、块还是对象存储之外,您还需要考虑您的延迟和吞吐量要求,以及预期的存储容量。例如,如果您的读取延迟要求非常严格,您很可能需要一个将数据保存在与访问数据的同一数据中心内的存储解决方案。

接下来,您需要考虑您已有的承诺或资源。也许您的组织有使用特定云提供商服务的规定或倾向。云提供商通常会为使用其服务提供成本激励,但您需要权衡这种激励与被锁定到特定服务的风险之间的关系。或者,您可能在本地数据中心的存储解决方案上已有投资,需要加以利用。

总体而言,成本往往是选择存储解决方案时的决定性因素,尤其是在长期使用时。确保您的模型不仅包括物理存储和任何托管服务的成本,还包括管理所选解决方案所涉及的运营成本。

在本节中,我们讨论了如何使用卷为同一个 Pod 内的多个容器提供共享存储。虽然使用卷可以满足某些用例的需求,但它并不能解决所有问题。卷无法在多个 Pod 之间共享存储资源,因为特定存储位置的定义与 Pod 的定义紧密关联。当您的 Kubernetes 集群中部署的 Pod 数量增加时,为单个 Pod 管理存储并不是一个可扩展的解决方案。

值得庆幸的是,Kubernetes 提供了额外的原语,帮助简化为单个 Pod 和一组相关 Pod 提供存储卷的配置和挂载过程。我们将在接下来的几个部分探讨这些概念。

持久卷 (PersistentVolumes)

Kubernetes 开发者为管理存储引入的关键创新是持久卷子系统 (PersistentVolume subsystem)。该子系统由三个相互协作的 Kubernetes 资源组成:持久卷 (PersistentVolumes)、持久卷声明 (PersistentVolumeClaims) 和存储类 (StorageClasses)。这些资源允许您将存储的定义和生命周期与 Pod 的使用方式分离,如图 2-8 所示:

  • 集群管理员定义持久卷,可以显式地定义,也可以通过创建一个可以动态配置新持久卷的存储类来实现。
  • 应用程序开发者创建持久卷声明,描述其应用程序对存储资源的需求,并且这些持久卷声明可以作为 Pod 中卷定义的一部分进行引用。
  • Kubernetes 控制平面管理持久卷声明与持久卷的绑定过程。

image.png

首先,让我们来看一下持久卷资源(通常缩写为 PV),它定义了对特定位置存储的访问权限。持久卷通常由集群管理员定义,供应用程序开发者使用。每个 PV 都可以代表前一节中讨论的相同类型的存储,例如由云提供商提供的存储、网络存储或直接位于工作节点上的存储,如图 2-9 所示。由于它们与特定的存储位置绑定,持久卷在 Kubernetes 集群之间不可移植。

image.png

本地持久卷 (Local PersistentVolumes)

图 2-9 还介绍了一种称为 local 的持久卷类型,它代表直接挂载在 Kubernetes 工作节点上的存储,例如磁盘或分区。与 hostPath 卷类似,本地卷也可以表示一个目录。本地卷与 hostPath 卷的一个关键区别在于,当使用本地卷的 Pod 重新启动时,Kubernetes 调度程序会确保该 Pod 在同一节点上重新调度,以便它可以连接到相同的持久状态。出于这个原因,本地卷经常被用作管理自身复制的数据基础设施的后备存储,我们将在第 4 章中详细探讨这一点。

定义持久卷的语法看起来很熟悉,因为它类似于在 Pod 中定义卷。例如,下面是一个定义本地持久卷的 YAML 配置文件。源代码可以在本书的代码仓库中找到:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: my-volume
spec:
  capacity:
    storage: 3Gi
  accessModes:
    - ReadWriteOnce
  local:
    path: /app/data
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - node1

如您所见,这段代码定义了一个名为 my-volume 的本地卷,位于工作节点 node1 上,大小为 3 GB,访问模式为 ReadWriteOnce。持久卷支持以下访问模式:

  • ReadWriteOnce:该卷可以被单个节点以读写方式挂载,尽管在该节点上运行的多个 Pod 可以访问该卷。
  • ReadOnlyMany:该卷可以同时被多个节点以只读方式挂载。
  • ReadWriteMany:该卷可以同时被多个节点以读写方式挂载。
选择卷访问模式

选择适合特定卷的访问模式取决于工作负载的类型。例如,许多分布式数据库将配置为每个 Pod 拥有专用存储,这使得 ReadWriteOnce 成为一个不错的选择。

除了容量和访问模式之外,持久卷的其他属性还包括:

  • volumeMode,默认为 Filesystem,但可以覆盖为 Block
  • reclaimPolicy 定义了当 Pod 释放对该持久卷的声明时会发生什么。合法值包括 RetainRecycleDelete
  • nodeAffinity 用于指定哪些工作节点可以访问该卷。对于大多数类型来说这是可选的,但对于本地卷类型则是必需的。
  • class 属性将该 PV 绑定到特定的存储类 (StorageClass),我们将在本章后面介绍这一概念。
  • 某些持久卷类型会暴露特定类型的 mountOptions
卷选项的差异

不同卷类型之间的选项有所不同。例如,并非每种访问模式或回收策略都适用于每种持久卷类型,因此请查阅您选择的卷类型的文档以获取更多详细信息。

您可以使用 kubectl describe persistentvolume 命令(或简写为 kubectl describe pv)查看持久卷的状态:

kubectl describe pv my-volume

输出结果可能如下所示:

Name:              my-volume
Labels:            <none>
Annotations:       <none>
Finalizers:        [kubernetes.io/pv-protection]
StorageClass:
Status:            Available
Claim:
Reclaim Policy:    Retain
Access Modes:      RWO
VolumeMode:        Filesystem
Capacity:          3Gi
Node Affinity:
  Required Terms:
    Term 0:        kubernetes.io/hostname in [node1]
Message:
Source:
    Type:  LocalVolume (a persistent volume backed by local storage on a node)
    Path:  /app/data
Events:    <none>

当持久卷首次创建时,其状态为 Available。持久卷可以有多种状态值:

  • Available:持久卷是空闲的,尚未绑定到任何声明。
  • Bound:持久卷已绑定到一个持久卷声明,这个声明在 describe 输出的其他部分列出。
  • Released:对持久卷的现有声明已被删除,但资源尚未被回收,因此资源尚未处于 Available 状态。
  • Failed:卷的自动回收失败。

现在您已经了解了如何在 Kubernetes 中定义存储资源,下一步是学习如何在应用程序中使用这些存储资源。

持久卷声明 (PersistentVolumeClaims)

正如我们所讨论的,Kubernetes 将存储的定义与使用分开。这些任务通常由不同的角色来执行:集群管理员定义存储,而应用程序开发人员使用存储。持久卷 (PersistentVolumes) 通常由管理员定义,并引用特定于该集群的存储位置。然后,开发人员可以使用持久卷声明 (PersistentVolumeClaims, PVCs) 来指定其应用程序的存储需求,Kubernetes 使用这些声明将符合指定条件的持久卷与 Pod 关联起来。如图 2-10 所示,持久卷声明用于引用我们之前介绍的各种卷类型,包括本地持久卷和由云或网络存储供应商提供的外部存储。

image.png

从应用程序开发人员的角度来看,这个过程是怎样的?

首先,您将创建一个表示所需存储条件的持久卷声明 (PVC)。例如,以下是一个请求 1 GB 存储和独占读写访问的声明。源代码可以在本书的代码仓库中找到:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-claim
spec:
  storageClassName: ""
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

您可能注意到一个有趣的地方,这个声明中的 storageClassName 被设置为空字符串。我们将在下一节讨论存储类 (StorageClasses) 时解释这一点的重要性。您可以在 Pod 的定义中引用该声明,如下所示。源代码也在本书的代码仓库中:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: nginx
    image: nginx
    volumeMounts:
    - mountPath: "/app/data"
      name: my-volume
  volumes:
  - name: my-volume
    persistentVolumeClaim:
      claimName: my-claim

如您所见,持久卷在 Pod 内被表示为一个卷。该卷被赋予一个名称,并引用了声明。这被视为 persistentVolumeClaim 类型的卷。与其他卷一样,该卷被挂载到容器中的一个特定挂载点——在本例中,它被挂载到主应用程序 Nginx 容器中的 /app/data 路径。

PVC 也有一个状态,您可以通过获取状态来查看:

kubectl describe pvc my-claim

输出示例如下:

Name:          my-claim
Namespace:     default
StorageClass:
Status:        Bound
Volume:        my-volume
Labels:        <none>
Annotations:   pv.kubernetes.io/bind-completed: yes
               pv.kubernetes.io/bound-by-controller: yes
Finalizers:    [kubernetes.io/pvc-protection]
Capacity:      3Gi
Access Modes:  RWO
VolumeMode:    Filesystem
Mounted By:    <none>
Events:        <none>

PVC 具有两种状态值之一:Bound 表示它已绑定到某个卷(如本例所示),或者 Pending,表示它尚未绑定到任何卷。通常,Pending 状态意味着没有符合该声明的 PV 存在。

背后的工作原理

Kubernetes 将 Pod 中引用为卷的 PVC 纳入考虑,在调度 Pod 时会考虑这些 PVC。Kubernetes 识别与声明属性匹配的持久卷,并将最小的可用模块绑定到声明上。这些属性可能包括标签或节点亲和性(正如我们之前在本地卷中看到的)。

启动 Pod 时,Kubernetes 控制平面确保持久卷被挂载到工作节点。然后,每个请求的存储卷都会被挂载到 Pod 中的指定挂载点。

存储类 (StorageClasses)

前面的示例演示了 Kubernetes 如何将 PVC 绑定到已经存在的持久卷 (PersistentVolumes)。这种在 Kubernetes 集群中显式创建持久卷的模式称为静态配置 (static provisioning)。Kubernetes 的持久卷子系统还支持使用存储类 (StorageClasses, SC) 进行动态配置。存储类负责根据集群中运行的应用程序的需求来配置(和撤销配置)持久卷,如图 2-11 所示。

image.png

根据您使用的 Kubernetes 集群,可能至少已经有一个存储类可用。您可以使用以下命令验证这一点:

kubectl get sc

如果您在本地机器上运行一个简单的 Kubernetes 发行版并且没有看到任何存储类,您可以使用以下命令从 Rancher 安装一个开源的本地存储提供者:

set GH_LINK=https://raw.githubusercontent.com
kubectl apply -f \
  $GH_LINK/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml

此存储提供者已预装在 K3s 中,这是 Rancher 提供的一个桌面发行版。如果您查看该命令中引用的 YAML 配置,您会看到以下存储类的定义。源代码可以在本书的代码仓库中找到:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-path
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete

从定义中可以看出,存储类由以下几个关键属性定义:

  • provisioner:接口与底层存储提供者(如公共云或存储系统)进行交互,以分配实际存储。provisioner 可以是 Kubernetes 内置的 provisioner(称为 in-tree,因为它们是 Kubernetes 源代码的一部分),也可以是符合容器存储接口 (CSI) 的 provisioner,我们将在本章稍后详细讨论这一点。
  • reclaimPolicy:描述当持久卷被删除时,存储是否会被回收。默认值是 Delete,可以覆盖为 Retain,在这种情况下,存储管理员将负责与存储提供者一起管理该存储的未来状态。
  • volumeBindingMode:控制存储的配置和绑定时间。如果值为 Immediate,则在创建引用该存储类的持久卷声明 (PersistentVolumeClaim) 时立即配置持久卷,并将声明绑定到持久卷,无论该声明是否在 Pod 中被引用。许多存储插件还支持另一种模式,即 WaitForFirstConsumer,在这种情况下,直到创建引用该声明的 Pod 时才会配置持久卷。这种行为被认为更优,因为它为 Kubernetes 调度程序提供了更大的灵活性。

尽管在这个示例中没有显示,但还有一个可选的 allowVolumeExpansion 标志。它表示存储类是否支持扩展卷的能力。如果为 true,则可以通过增加持久卷声明 (PersistentVolumeClaim) 中 storage.request 字段的大小来扩展卷。默认值为 false

一些存储类还定义了参数,这些是传递给 provisioner 的特定存储提供者的配置选项。常见的选项包括文件系统类型、加密设置和 I/O 操作每秒 (IOPS) 的吞吐量。有关更多详细信息,请查阅存储提供者的文档。

动态配置的限制

本地持久卷 (Local PVs) 无法通过存储类动态配置,因此您必须手动创建它们。

应用程序开发人员可以在创建 PVC 时通过在定义中添加 storageClass 属性来引用特定的存储类。例如,以下是一个引用 local-path 存储类的 PVC 的 YAML 配置文件。源代码在本书的代码仓库中:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-local-path-claim
spec:
  storageClassName: local-path
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

如果在声明中没有指定存储类,则使用默认存储类。默认存储类可以由集群管理员设置。如我们在“持久卷”部分所示,您可以通过使用空字符串来选择不使用存储类,这表明您正在使用静态配置的存储。

存储类提供了一个有用的抽象,集群管理员和应用程序开发人员可以将其用作合同:管理员定义存储类,开发人员通过名称引用存储类。底层存储类实现的细节可能因 Kubernetes 平台提供者而异,从而促进应用程序的可移植性。

这种灵活性允许管理员创建代表各种存储选项的存储类——例如,区分不同的服务质量保证(通过吞吐量或延迟)。这种概念在其他存储系统中被称为“配置文件”(profiles)。有关如何以创新方式利用存储类的更多想法,请参阅“开发者如何推动 Kubernetes 存储的未来”。

Kubernetes 存储架构

在前面的章节中,我们讨论了 Kubernetes 通过其 API 支持的各种存储资源。在本章的剩余部分,我们将探讨这些解决方案的构建方式,因为它们可以为我们构建云原生数据解决方案提供宝贵的见解。

定义云原生存储

我们在本章中讨论的大多数存储技术都包含在 CNCF 生态系统中的“云原生存储”解决方案中。CNCF 的《存储白皮书》是一个有用的资源,它定义了云原生存储的关键术语和概念。这些资源都会定期更新。

Flexvolume

最初,Kubernetes 代码库中包含了多个 in-tree 存储插件(即与 Kubernetes 代码的其他部分包含在同一个 GitHub 仓库中)。这有助于标准化连接不同存储平台的代码,但也有一些缺点。首先,许多 Kubernetes 开发人员对所包含的广泛存储提供商了解有限。更重要的是,存储插件的升级能力与 Kubernetes 的发布周期挂钩,这意味着如果您需要修复或增强某个存储插件,您必须等到它被包含在某个 Kubernetes 版本中。这减缓了 Kubernetes 存储技术的发展速度,因此,采用率也随之降低。

Kubernetes 社区创建了 Flexvolume 规范,以允许独立开发插件——即不受 Kubernetes 源代码树约束,从而不受 Kubernetes 发布周期的限制。大约在同一时间,其他容器编排系统的存储插件标准开始出现,这些社区的开发人员开始质疑为解决相同基本问题而开发多个标准的智慧。

Flexvolume 的未来支持

在 Kubernetes 1.23 版本中,Flexvolume 功能已被弃用,取而代之的是容器存储接口 (CSI)。

容器存储接口 (CSI)

容器存储接口 (CSI) 计划作为容器化应用程序的存储行业标准建立。CSI 是一个开放标准,用于定义在包括 Kubernetes、Mesos 和 Cloud Foundry 在内的容器编排系统中通用的插件。如 Google 工程师、Kubernetes 存储特别兴趣小组 (SIG) 主席 Saad Ali 在 The New Stack 的《The State of State in Kubernetes》一文中所述,“容器存储接口允许 Kubernetes 直接与任意存储系统进行交互。”

CSI 规范可在 GitHub 上获取。Kubernetes 从 1.x 版本开始支持 CSI,并在 1.13 版本中正式发布 (GA)。Kubernetes 继续跟踪 CSI 规范的更新。

额外的 CSI 资源

CSI 文档站点为有兴趣开发符合 CSI 标准的驱动程序的开发人员和存储提供商提供指导。该站点还提供了一个非常有用的 CSI 驱动程序列表。这个列表通常比 Kubernetes 文档站点上提供的列表更为及时。

一旦在 Kubernetes 集群上部署了 CSI 实现,就可以通过标准的 Kubernetes 存储资源(如 PVC、PV 和 SC)访问其功能。在后台,每个 CSI 实现必须提供两个插件:一个节点插件 (node plug-in) 和一个控制器插件 (controller plug-in),如图 2-12 所示。

CSI 规范使用 gRPC 定义了这些插件的必需接口,但并未具体规定插件应如何部署。让我们简要看看这些服务的作用:

  • 控制器插件
    该插件支持卷的操作,如创建、删除、列出、发布/取消发布、跟踪和扩展卷容量。它还跟踪卷的状态,包括每个卷连接到的节点。控制器插件还负责快照的创建和管理,并使用快照克隆卷。控制器插件可以运行在任何节点上——它是一个标准的 Kubernetes 控制器。
  • 节点插件
    该插件运行在每个 Kubernetes 工作节点上,已配置的卷将在这些节点上附加。节点插件负责本地存储,以及将卷挂载到节点和从节点卸载卷。在安排需要使用卷的 Pod 之前,Kubernetes 控制平面会指示插件先挂载卷。

image.png

CSI 迁移

Kubernetes 社区非常重视版本之间的向前和向后兼容性,从 in-tree 存储插件过渡到 CSI 也不例外。Kubernetes 中的功能通常作为 alpha 特性引入,然后逐步发展为 beta,最终发布为 GA(一般可用)。引入像 CSI 这样的新 API 具有更复杂的挑战,因为它还涉及到旧 API 的弃用。

为了为存储插件的用户提供一致的体验,Kubernetes 社区引入了 CSI 迁移方法。当某个等效的 CSI 驱动程序可用时,每个对应的 in-tree 插件的实现将被更改为一个外观模式 (facade),并将对 in-tree 插件的调用委派给底层的 CSI 驱动程序。迁移功能本身是一项可以在 Kubernetes 集群上启用的特性。

这种方法允许现有集群在更新到新版本的 Kubernetes 时采用分阶段的方式迁移。每个应用程序都可以独立更新,以使用 CSI 驱动程序而不是 in-tree 驱动程序。这种成熟和替换 API 的方法有助于促进整个平台的稳定性,并为管理员提供对新 API 迁移的控制权。

容器附加存储 (Container Attached Storage)

虽然 CSI 在跨容器编排器标准化存储管理方面迈出了重要一步,但它并未提供关于存储软件如何或在哪里运行的实现指南。一些 CSI 实现基本上是围绕运行在 Kubernetes 集群外部的遗留存储管理软件的薄封装。虽然这种重用现有存储资产的方法确实有其优势,但许多开发人员希望能够有存储管理解决方案可以与他们的应用程序一起完全在 Kubernetes 中运行。

容器附加存储 (Container Attached Storage, CAS) 是一种设计模式,提供了一种更具云原生特性的存储管理方法。用于管理存储操作(如将卷附加到应用程序)的逻辑本身由运行在容器中的微服务组成。这使得存储层具备与部署在 Kubernetes 上的其他应用程序相同的特性,并减少了管理员需要跟踪的不同管理接口数量。存储层因此成为了另一个 Kubernetes 应用程序。

正如 Evan Powell 在 CNCF 博客上的《容器附加存储:初学者指南》一文中所指出的那样:

容器附加存储反映了一个更广泛的趋势,即通过在 Kubernetes 和微服务上构建,并为基于 Kubernetes 的微服务环境提供功能,重新发明特定类别或创建新的解决方案。例如,云原生生态系统中已经出现了新的项目用于安全性、DNS、网络、网络策略管理、消息传递、跟踪、日志记录等。

有几个项目和产品体现了 CAS 方法的存储理念。让我们来看看几个开源选项。

OpenEBS

OpenEBS 是一个由 MayaData 创建并捐赠给 CNCF 的项目,该项目在 2019 年成为 CNCF 的沙箱项目 (Sandbox Project)。其名称是对亚马逊弹性块存储 (Elastic Block Store, EBS) 的一种戏仿,OpenEBS 试图提供一个与这一流行托管服务等效的开源版本。OpenEBS 提供用于管理本地和 NVMe 持久卷的存储引擎。

OpenEBS 是一个很好的示例,展示了如何将 CSI 兼容实现部署到 Kubernetes 上,如图 2-13 所示。控制平面包括 OpenEBS 配置器,它实现了 CSI 控制器接口,以及 OpenEBS API 服务器,它为客户端提供配置接口,并与 Kubernetes 控制平面的其他部分进行交互。

OpenEBS 的数据平面包括节点磁盘管理器 (Node Disk Manager, NDM) 以及每个持久卷的专用 Pod。NDM 运行在将访问存储的每个 Kubernetes 工作节点上。它实现了 CSI 节点接口,并提供了自动检测附加到工作节点的块存储设备的有用功能。

image.png

OpenEBS 为每个卷创建多个 Pod。一个控制器 Pod 被创建为主副本,并在其他 Kubernetes 工作节点上创建额外的副本 Pod,以实现高可用性。每个 Pod 都包括侧车容器 (sidecars),这些容器提供用于指标收集和管理的接口,使得控制平面能够监控和管理数据平面。

Longhorn

Longhorn 是一个开源的分布式块存储系统,专为 Kubernetes 设计。它最初由 Rancher 开发,并于 2019 年成为 CNCF 的沙箱项目 (Sandbox Project)。Longhorn 旨在提供云供应商存储和昂贵的外部存储阵列的替代方案。Longhorn 支持将增量备份保存到 NFS 或兼容 S3 的存储中,并且能够将实时复制到另一个 Kubernetes 集群中以进行灾难恢复。

Longhorn 采用与 OpenEBS 类似的架构;根据文档,“Longhorn 为每个块设备卷创建一个专用存储控制器,并同步复制该卷到存储在多个节点上的多个副本中。存储控制器和副本本身通过 Kubernetes 进行编排。”Longhorn 还提供了一个集成的用户界面,以简化操作。

Rook 和 Ceph

根据其官网描述,“Rook 是一个开源的云原生存储编排器,提供了一个平台、框架以及对各种存储解决方案的支持,使它们能够原生集成到云原生环境中。”Rook 最初被创建为可在 Kubernetes 中部署的 Ceph 容器化版本。Ceph 是一个开源的分布式存储框架,提供块存储、文件存储和对象存储。Rook 是第一个被 CNCF 接受的存储项目,现在已被视为 CNCF 的毕业项目。

Rook 是一个真正的 Kubernetes 原生实现,因为它使用了 Kubernetes 的自定义资源 (CRDs) 和被称为操作器 (operators) 的自定义控制器。Rook 为 Ceph、Cassandra 和 NFS 提供了操作器。我们将在第 4 章中进一步了解自定义资源和操作器。

一些商业 Kubernetes 解决方案也体现了 CAS 模式。这些解决方案包括 MayaData(OpenEBS 的创造者)、Pure Storage 的 Portworx、Robin.io 和 StorageOS。这些公司不仅提供块和文件格式的原始存储,还提供用于简化数据库和流处理解决方案等数据基础设施部署的集成。

容器对象存储接口 (COSI)

CSI 提供了对文件和块存储的支持,但对象存储 API 需要不同的语义,不太适合 CSI 挂载卷的范式。2020 年秋季,以 MinIO 为首的一些公司开始致力于为容器编排平台设计一个新的对象存储 API:即容器对象存储接口 (COSI)。COSI 为 Kubernetes 提供了一个更适合配置和访问对象存储的 API,定义了一个 bucket(桶)自定义资源,并包括创建桶和管理桶访问的操作。COSI 的控制平面和数据平面的设计借鉴了 CSI 的模型。COSI 是一个新兴的标准,起步良好,具有在 Kubernetes 社区乃至更广泛范围内被广泛采用的潜力。

开发者如何推动 Kubernetes 存储的未来

作者:Kiran Mova,MayaData 联合创始人兼 CTO 以及 Kubernetes 存储 SIG 成员

许多组织才刚刚开始他们的容器化之旅。Kubernetes 是一个引人注目的对象,大家都想在 Kubernetes 上运行一切。但并不是所有团队都为 Kubernetes 做好准备,更不用说在 Kubernetes 上管理有状态的工作负载了。

应用程序开发者是推动 Kubernetes 上有状态工作负载的动力。这些开发者从可用的云资源开始,甚至使用单节点的 Kubernetes 集群,并认为他们已经准备好在生产环境中运行。开发者正在将内部应用程序“Kubernetize”(即适配 Kubernetes),而这些应用程序对存储的需求与支持他们的平台团队习惯的方式有很大不同。

微服务和 Kubernetes 改变了存储卷的配置方式。平台团队习惯于从配置具有所需吞吐量或容量的卷的角度来考虑数据。在过去,平台团队会与应用程序团队会面,估算数据的大小,经过一个月的规划后,配置一个 2-3 TB 的卷并将其挂载到虚拟机或裸机服务器中,这样就能为接下来的一年提供足够的存储容量。

在 Kubernetes 上,配置变得更加随意和容易。通过采用 Kubernetes,您可以以高性价比和敏捷的方式运行工作负载。但许多平台团队仍在努力跟上进度。有些团队只是专注于正确配置存储,而另一些团队则开始关注“第二天”操作,例如自动化配置、扩展卷或断开和销毁卷。

平台团队还没有一种万无一失的方法在 Kubernetes 上运行有状态的工作负载,因此他们经常将持久性工作负载卸载到公共云提供商。公共云强烈推荐他们的托管服务,声称它们拥有运行存储系统所需的一切,但一旦您开始为有状态的工作负载使用托管服务,您可能会依赖这些云提供商,并陷入困境。

与此同时,存储技术的创新正在并行发生:

  • 生态系统在超融合和解耦之间来回转换。这种重构发生在堆栈的所有层面,不仅仅是软件:还包括数据消费者的流程和人员。
  • 硬件趋势正在推动低延迟解决方案的发展,包括 NVMe 和 DPDK/SPDK,以及 Linux 内核的变化,如 io_uring,以利用更快的硬件。
  • 容器附加存储 (CAS) 将帮助我们更有效地管理存储——例如,在工作负载减少时能够回收存储空间。这在数据分布在多个节点上的情况下可能是一个困难的问题。我们需要更好的逻辑将数据重新定位到现有节点上。
  • 引入更多自动化的合规性和操作技术也正在逐渐出现。

面对所有这些创新,理解全局并确定如何最大化利用这些技术可能会让人感到不知所措。平台 SRE 需要学习 Kubernetes、声明式部署、GitOps 原则、新的卷类型,甚至是最终一致性等数据库概念。

我们设想一个未来,应用程序开发者将根据所需的服务质量(如 IOPS 和吞吐量)来指定他们在 Kubernetes 上的存储需求。开发者应该能够用更接近人类理解的术语为他们的工作负载指定不同的存储需求。例如,平台团队可以定义“快速存储”和“慢速存储”或“元数据存储”和“数据存储”的存储类。这些存储类将做出不同的成本/性能权衡,并提供特定的服务级别协议 (SLAs)。我们甚至可能看到一些标准定义开始为这些新存储类出现。

理想情况下,应用程序团队不应选择存储解决方案。应用程序开发者唯一需要关心的就是为他们的应用程序指定所需存储类的 PersistentVolumeClaim(持久卷声明)。管理存储的其他细节应该被隐藏,尽管存储子系统仍会通过标准的 Kubernetes 机制报告错误,包括状态和日志。这种能力将使应用程序开发变得更加简单,无论他们是在部署数据库还是其他有状态的工作负载。

这些创新将引导我们在 Kubernetes 上的存储管理达到一个理想的状态。如今,部署基础设施很容易。让我们共同努力,使部署正确的基础设施也变得容易。

正如您所见,Kubernetes 上的存储领域充满了创新,包括多个开源项目和商业供应商,它们竞相提供最易用、最具成本效益和性能的解决方案。CNCF 生态系统中的云原生存储部分提供了存储供应商和相关工具的有用列表,包括本章中提到的技术以及更多。

总结

在本章中,我们探讨了在 Docker 这样的容器系统和 Kubernetes 这样的容器编排系统中如何管理持久性数据。你已经了解了各种可以用来管理有状态工作负载的 Kubernetes 资源,包括 Volumes、PersistentVolumes、PersistentVolumeClaims 和 StorageClasses。我们还看到,容器存储接口 (CSI) 和容器附加存储 (CAS) 模式为更具云原生特性的存储管理方法指明了方向。现在,你已经准备好学习如何使用这些构建块和设计原则来管理有状态工作负载,包括数据库、流数据等。