本文发布于一年多以前。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已变得不正确。

具有存储容量跟踪的临时卷:类固醇上的 EmptyDir

一些应用程序需要额外的存储空间,但不关心这些数据是否在重启后持久存储。例如,缓存服务通常受内存大小限制,可以将不常用的数据移动到比内存慢的存储中,而对整体性能影响很小。其他应用程序则希望某些只读输入数据以文件的形式存在,例如配置数据或密钥。

Kubernetes 已经支持几种这样的临时卷,但它们的功能仅限于 Kubernetes 内部实现的功能。

CSI 临时卷使得可以使用 CSI 驱动程序扩展 Kubernetes,这些驱动程序提供轻量级的本地卷。这些卷可以注入任意状态,例如配置、密钥、身份、变量或类似信息。必须修改 CSI 驱动程序以支持此 Kubernetes 功能,即普通、符合标准的 CSI 驱动程序将无法工作,并且根据设计,此类卷应该可以在为 Pod 选择的任何节点上使用。

对于在节点上消耗大量资源的卷,或者对于仅在某些节点上可用的特殊存储,这是一个问题。因此,Kubernetes 1.19 为在概念上更像 EmptyDir 卷的卷引入了两个新的 alpha 功能

新方法的优点是

  • 存储可以是本地的或网络连接的。
  • 卷可以具有应用程序永远无法超过的固定大小。
  • 可以与任何支持持久卷配置的 CSI 驱动程序以及(对于容量跟踪)实现 CSI GetCapacity 调用的驱动程序一起使用。
  • 卷可能具有一些初始数据,具体取决于驱动程序和参数。
  • 支持所有典型的卷操作(快照、调整大小、未来的存储容量跟踪等)。
  • 这些卷可以与任何接受 Pod 或卷规范的应用程序控制器一起使用。
  • Kubernetes 调度器本身会选择合适的节点,也就是说,不再需要实现和配置调度器扩展和变更 Webhook。

这使得通用临时卷成为几个用例的合适解决方案

用例

持久内存作为 memcached 的 DRAM 替代品

最近发布的 memcached 添加了对使用持久内存(PMEM)而不是标准 DRAM 的支持。当通过其中一个应用程序控制器部署 memcached 时,通用临时卷可以使用诸如PMEM-CSI之类的 CSI 驱动程序请求具有特定大小的 PMEM 卷。

本地 LVM 存储作为临时空间

处理超过 RAM 大小的数据集的应用程序可以请求具有性能特征或大小的本地存储,而普通的 Kubernetes EmptyDir 卷无法满足这些要求。例如,TopoLVM 就是为此目的而编写的。

对带有数据的卷的只读访问

配置卷可能会导致非空卷

此类卷可以以只读方式挂载。

工作原理

通用临时卷

通用临时卷背后的关键思想是,新的卷源(所谓的EphemeralVolumeSource)包含创建卷声明(历史上称为持久卷声明,PVC)所需的所有字段。kube-controller-manager 中的新控制器会等待嵌入此类卷源的 Pod,然后为该 Pod 创建 PVC。对于 CSI 驱动程序部署,该 PVC 看起来像任何其他 PVC,因此不需要特殊支持。

只要这些 PVC 存在,就可以像任何其他卷声明一样使用它们。特别是,它们可以在卷克隆或快照中作为数据源引用。PVC 对象还保存卷的当前状态。

自动创建的 PVC 的命名是确定的:名称是 Pod 名称和卷名称的组合,中间用连字符 (-) 连接。这种确定的命名使得与 PVC 进行交互更容易,因为一旦知道了 Pod 名称和卷名称,就不必再搜索它。缺点是该名称可能已经被使用。这会被 Kubernetes 检测到,然后阻止 Pod 启动。

为了确保卷与 Pod 一起删除,控制器使 Pod 成为卷声明的所有者。当 Pod 被删除时,正常的垃圾回收机制也会删除声明,从而删除卷。

声明通过正常的存储类机制选择存储驱动程序。尽管支持具有立即和延迟绑定(又名 WaitForFirstConsumer)的存储类,但对于临时卷,使用 WaitForFirstConsumer 更有意义:这样 Pod 调度可以在选择节点时同时考虑节点利用率和存储的可用性。这就是另一个新功能发挥作用的地方。

存储容量跟踪

通常,Kubernetes 调度器没有关于 CSI 驱动程序可能在何处创建卷的信息。它也没有办法直接与 CSI 驱动程序对话以检索该信息。因此,它会尝试不同的节点,直到找到一个可以使所有卷都可用的节点(延迟绑定),或者完全由驱动程序选择位置(立即绑定)。

新的CSIStorageCapacity alpha API允许将必要的信息存储在 etcd 中,调度器可以在 etcd 中访问这些信息。与对通用临时卷的支持相比,必须在部署 CSI 驱动程序时启用存储容量跟踪:必须告知 external-provisioner 发布容量信息,然后它通过正常的 GetCapacity 调用从 CSI 驱动程序检索该信息。

当 Kubernetes 调度器需要为使用延迟绑定且 CSI 驱动程序部署通过设置CSIDriver.storageCapacity 标志选择使用该功能时,为具有未绑定卷的 Pod 选择节点时,调度器会自动过滤掉无法访问足够存储容量的节点。这适用于通用临时卷和持久卷,但适用于 CSI 临时卷,因为后者的参数对 Kubernetes 是不透明的。

像往常一样,具有立即绑定的卷在调度 Pod 之前创建,其位置由存储驱动程序选择。因此,external-provisioner 的默认配置会跳过具有立即绑定的存储类,因为这些信息无论如何都不会被使用。

由于 Kubernetes 调度器必须根据可能过时的信息进行操作,因此不能保证在创建卷时容量仍然可用。尽管如此,无需重试即可创建卷的可能性应该更高。

安全性

CSIStorageCapacity

CSIStorageCapacity 对象是命名空间的。当在各自的命名空间中部署每个 CSI 驱动程序时,并且按照建议,将 CSIStorageCapacity 的 RBAC 权限限制在该命名空间中时,数据来源始终是显而易见的。但是,Kubernetes 不会检查这一点,并且通常驱动程序无论如何都安装在同一个命名空间中,因此最终期望驱动程序能够正常运行,而不是发布不正确的数据。

通用临时卷

如果用户有权创建 Pod(直接或间接),那么即使他们没有创建卷声明的权限,也可以创建通用临时卷。这是因为 RBAC 权限检查应用于创建 PVC 的控制器,而不是原始用户。这是一个根本性的变化,在在不允许不受信任的用户创建卷的集群中启用该功能之前,必须加以考虑

示例

PMEM-CSI 中的一个特殊分支包含在 QEMU VM 中启动 Kubernetes 1.19 集群并启用两个 alpha 功能的所有必要更改。PMEM-CSI 驱动程序代码保持不变,仅更新了部署。

在合适的机器上(Linux,非 root 用户可以使用 Docker - 请参阅 PMEM-CSI 文档中的QEMU 和 Kubernetes 部分),以下命令将启动一个集群并安装 PMEM-CSI 驱动程序

git clone --branch=kubernetes-1-19-blog-post https://github.com/intel/pmem-csi.git
cd pmem-csi
export TEST_KUBERNETES_VERSION=1.19 TEST_FEATURE_GATES=CSIStorageCapacity=true,GenericEphemeralVolume=true TEST_PMEM_REGISTRY=intel
make start && echo && test/setup-deployment.sh

如果一切顺利,输出将包含以下使用说明

The test cluster is ready. Log in with [...]/pmem-csi/_work/pmem-govm/ssh.0, run
kubectl once logged in.  Alternatively, use kubectl directly with the
following env variable:
   KUBECONFIG=[...]/pmem-csi/_work/pmem-govm/kube.config

secret/pmem-csi-registry-secrets created
secret/pmem-csi-node-secrets created
serviceaccount/pmem-csi-controller created
...
To try out the pmem-csi driver ephemeral volumes:
   cat deploy/kubernetes-1.19/pmem-app-ephemeral.yaml |
   [...]/pmem-csi/_work/pmem-govm/ssh.0 kubectl create -f -

CSIStorageCapacity 对象并非旨在让人类读取,因此需要进行一些后处理。以下 Golang 模板按示例使用的存储类过滤所有对象,并打印名称、拓扑和容量

kubectl get \
        -o go-template='{{range .items}}{{if eq .storageClassName "pmem-csi-sc-late-binding"}}{{.metadata.name}} {{.nodeTopology.matchLabels}} {{.capacity}}
{{end}}{{end}}' \
        csistoragecapacities
csisc-2js6n map[pmem-csi.intel.com/node:pmem-csi-pmem-govm-worker2] 30716Mi
csisc-sqdnt map[pmem-csi.intel.com/node:pmem-csi-pmem-govm-worker1] 30716Mi
csisc-ws4bv map[pmem-csi.intel.com/node:pmem-csi-pmem-govm-worker3] 30716Mi

一个单独的对象具有以下内容

kubectl describe csistoragecapacities/csisc-6cw8j
Name:         csisc-sqdnt
Namespace:    default
Labels:       <none>
Annotations:  <none>
API Version:  storage.k8s.io/v1alpha1
Capacity:     30716Mi
Kind:         CSIStorageCapacity
Metadata:
  Creation Timestamp:  2020-08-11T15:41:03Z
  Generate Name:       csisc-
  Managed Fields:
    ...
  Owner References:
    API Version:     apps/v1
    Controller:      true
    Kind:            StatefulSet
    Name:            pmem-csi-controller
    UID:             590237f9-1eb4-4208-b37b-5f7eab4597d1
  Resource Version:  2994
  Self Link:         /apis/storage.k8s.io/v1alpha1/namespaces/default/csistoragecapacities/csisc-sqdnt
  UID:               da36215b-3b9d-404a-a4c7-3f1c3502ab13
Node Topology:
  Match Labels:
    pmem-csi.intel.com/node:  pmem-csi-pmem-govm-worker1
Storage Class Name:           pmem-csi-sc-late-binding
Events:                       <none>

现在让我们创建一个带有一个通用临时卷的示例应用程序。pmem-app-ephemeral.yaml 文件包含

# This example Pod definition demonstrates
# how to use generic ephemeral inline volumes
# with a PMEM-CSI storage class.
kind: Pod
apiVersion: v1
metadata:
  name: my-csi-app-inline-volume
spec:
  containers:
    - name: my-frontend
      image: intel/pmem-csi-driver-test:v0.7.14
      command: [ "sleep", "100000" ]
      volumeMounts:
      - mountPath: "/data"
        name: my-csi-volume
  volumes:
  - name: my-csi-volume
    ephemeral:
      volumeClaimTemplate:
        spec:
          accessModes:
          - ReadWriteOnce
          resources:
            requests:
              storage: 4Gi
          storageClassName: pmem-csi-sc-late-binding

如上述使用说明所示创建后,我们有一个额外的 Pod 和 PVC

kubectl get pods/my-csi-app-inline-volume -o wide
NAME                       READY   STATUS    RESTARTS   AGE     IP          NODE                         NOMINATED NODE   READINESS GATES
my-csi-app-inline-volume   1/1     Running   0          6m58s   10.36.0.2   pmem-csi-pmem-govm-worker1   <none>           <none>
kubectl get pvc/my-csi-app-inline-volume-my-csi-volume
NAME                                     STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS               AGE
my-csi-app-inline-volume-my-csi-volume   Bound    pvc-c11eb7ab-a4fa-46fe-b515-b366be908823   4Gi        RWO            pmem-csi-sc-late-binding   9m21s

该 PVC 由 Pod 拥有

kubectl get -o yaml pvc/my-csi-app-inline-volume-my-csi-volume
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  annotations:
    pv.kubernetes.io/bind-completed: "yes"
    pv.kubernetes.io/bound-by-controller: "yes"
    volume.beta.kubernetes.io/storage-provisioner: pmem-csi.intel.com
    volume.kubernetes.io/selected-node: pmem-csi-pmem-govm-worker1
  creationTimestamp: "2020-08-11T15:44:57Z"
  finalizers:
  - kubernetes.io/pvc-protection
  managedFields:
    ...
  name: my-csi-app-inline-volume-my-csi-volume
  namespace: default
  ownerReferences:
  - apiVersion: v1
    blockOwnerDeletion: true
    controller: true
    kind: Pod
    name: my-csi-app-inline-volume
    uid: 75c925bf-ca8e-441a-ac67-f190b7a2265f
...

最终,pmem-csi-pmem-govm-worker1 的存储容量信息也会得到更新

csisc-2js6n map[pmem-csi.intel.com/node:pmem-csi-pmem-govm-worker2] 30716Mi
csisc-sqdnt map[pmem-csi.intel.com/node:pmem-csi-pmem-govm-worker1] 26620Mi
csisc-ws4bv map[pmem-csi.intel.com/node:pmem-csi-pmem-govm-worker3] 30716Mi

如果另一个应用程序需要的容量超过 26620Mi,则 Kubernetes 调度器将不再选择 pmem-csi-pmem-govm-worker1

后续步骤

这两个功能都在开发中。在 alpha 审查过程中已经提出了一些开放性问题。这两个增强提案记录了迁移到 beta 所需的工作以及已经考虑过但被拒绝的替代方案

您的反馈对于推动开发至关重要。SIG-Storage 定期开会,可以通过 Slack 和邮件列表联系。