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

具有存储容量跟踪的临时卷:增强版的 EmptyDir

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

Kubernetes 已经支持多种此类临时卷,但其功能仅限于 Kubernetes 内部实现的功能。

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

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

新方法的优点是:

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

这使得通用临时卷成为多种用例的合适解决方案:

使用场景

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

memcached 的最新版本增加了对使用持久内存 (PMEM) 而非标准 DRAM 的支持。通过某个应用程序控制器部署 memcached 时,通用临时卷使得可以从 CSI 驱动程序(例如PMEM-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 中,供调度器使用。与对通用临时卷的支持不同,存储容量跟踪必须在部署 CSI 驱动程序时启用:必须告知 external-provisioner 发布容量信息,然后它通过正常的 GetCapacity 调用从 CSI 驱动程序检索这些信息。

当 Kubernetes 调度器需要为具有未绑定卷的 Pod 选择节点时,如果该卷使用延迟绑定且 CSI 驱动程序部署已通过设置CSIDriver.storageCapacity 标志选择此功能,调度器会自动过滤掉没有足够存储容量的节点。这适用于通用临时卷和持久卷,但**不**适用于 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 和邮件列表联系。