亚马逊AWS官方博客

HAQM EKS 存储应用:实例存储本地盘的应用实践方案

在 Kubernetes 环境中管理本地存储资源时,实例存储(Instance Store)因其高性能和低延迟特性成为关键选择。实例存储为您的 EC2 实例提供临时块级存储。此存储由已物理附加到主机的磁盘提供。实例存储非常适合临时存储频繁更改的信息,例如缓冲区、缓存、Scratch 数据和其他临时内容。它还可用于存储您在一组实例中复制的临时数据,例如负载均衡的 Web 服务器池。

实例存储由一个或多个显示为块储存设备的实例存储卷组成。实例存储的大小以及可用设备的数量因实例类型和实例大小而异。本文章将讨论使用 HAQM EKS 的 Pod 中使用实例存储,包括通过托管节点组和预启动命令初始化,通过部署本地卷静态配置器 CSI 驱动程序创建 PVC 以及通过 hostpath 使用。 客户可能希望利用本地 NVMe 存储卷实现比通用 HAQM EBS 启动卷更高的性能。

请注意,实例存储卷用于临时存储,当 HAQM EC2 实例停止或终止时,数据就会丢失。 要在实例的整个生命周期内保持实例存储卷中存储的数据,需要在应用层处理复制。

实例存储特性解析

核心特点

  • 临时存储性质:数据在实例停止/终止时自动清除
  • 高性能优势:NVMe SSD 提供比 EBS 卷更低的延迟
  • 物理绑定特性:存储设备直接连接到宿主机物理硬件
  • 容量扩展方案:支持通过 RAID-0 实现多盘聚合

方案对比分析

本地盘的挂载有两种方式实现:

  1. 使用 HostPath 的方式访问:在 Node 初始化的时候挂载 Local SSD,并且 Pod 中指定 HostPath 访问 Local SSD。
  2. 使用 PVC 的方式访问:Kubernetes PersistentVolume(PV)是一种集群资源,它定义了单个存储卷的功能和位置。一个 PersistentVolumeClaim(PVC)定义了对一定量存储资源的请求。当 Kubernetes Pod 需要存储时,它会引用一个 PVC。然后,Kubernetes 会将 PVC 与可用的 PV 匹配,如果 CSI 驱动程序支持动态调配,则会自动调配新的 PV。

这里我们对两种使用方式进行分析,可以根据这个分析选择适合自己的使用方案:

评估维度 直接挂载本地盘供 Pod 访问 PVC 动态绑定
1 配置复杂度 简单,直接在 Node 启动时初始化,Pod 中指定路径 较复杂,需要设置 Static Provisioner,但提供标准化配置
2 存储可见性 直接可见,易于调试,但缺乏抽象 通过 PV/PVC 提供更好的抽象,管理性更强
3 维护成本 直接访问维护简单,但大规模部署难度大 统一管理接口便于大规模部署,但需维护额外组件
4 容量隔离 可直接控制分配,但缺乏原生隔离机制 通过 PVC 更好控制和隔离容量
5 耦合性 与主机路径紧密耦合,可移植性较差 降低与具体主机耦合,提高抽象级别
6 亲和性 可直接控制 Pod 与 Node 亲和性,但管理繁琐 通过 PV/PVC 更好管理亲和性
7 监控指标 可用主机级监控工具,缺乏 K8s 集成指标 可利用 K8s 原生存储类监控指标
8 适用场景 小规模部署、直接访问需求、高性能要求 大规模部署、需统一管理和抽象、高存储管理要求

方案实现介绍

接下来,让我们一起探索这两种实例存储本地盘的使用方式。

方式一:直接挂载本地盘供 Pod 访问

实现思路

我们需要配置 HAQM EKS 托管节点组,以启动带有 fast-disk-node 标签的 EC2 实例,并在启动时将 NVMe 实例存储卷挂载到 /mnt/fast-disks 目录。

本文介绍了两种选项:

  1. 多个持久卷,每个 NVMe 实例存储卷对应一个
  2. 一个单独的持久卷 RAID-0 阵列,跨所有 NVMe 实例存储卷

这两个选项都为您的 Kubernetes Pod 提供高随机 I/O 性能和极低延迟的存储卷。两种方法根据使用场景提供不同的选择。

在选项 1 中,为每个 NVMe 实例存储卷创建一个持久卷。本博客中使用的 i4g.8xlarge 实例有 2 个 NVMe 卷。当多个 Pod 需要快速存储时,选项 1 将创建 2 个持久卷。

选项 2 使用 RAID-0 创建单个持久卷,这在只有一个 Pod 需要快速存储时很有用。

实施步骤

节点初始化本地盘

  • 选项 1:多个持久卷,每个 NVMe 实例存储卷对应一个

使用 eksctl 工具,我们创建一个新的 HAQM EKS 托管节点组。在此示例中,我们请求了两个 i3.8xlarge EC2 实例。在元数据中,将 eksworkshop-eksctl 和 us-west-2 替换为您各自的 EKS 集群名称和 AWS 区域。

将以下清单复制并保存为 pv-nvme-nodegroup.yaml

# pv-nvme-nodegroup.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  # Replace with your EKS Cluster's name
  name: eksworkshop-eksctl
  # Replace with the AWS Region your cluster is deployed in
  region: us-west-2

managedNodeGroups:
  # Name to give the managed node-group
  - name: eks-pv-nvme-ng
    # Label the nodes that they contain fast-disks 
    labels: { fast-disk-node: "pv-nvme" }
    instanceType: i3.8xlarge
    desiredCapacity: 2
    volumeSize: 100 # EBS Boot Volume size
    privateNetworking: true
    preBootstrapCommands:
      - |
        # Install NVMe CLI
        yum install nvme-cli -y
        
        # Get a list of instance-store NVMe drives
        nvme_drives=$(nvme list | grep "HAQM EC2 NVMe Instance Storage" | cut -d " " -f 1 || true)
        readarray -t nvme_drives <<< "$nvme_drives"

        for disk in "${nvme_drives[@]}"
        do
          # Format the disk to ext4
          mkfs.ext4 -F $disk
          
          # Get disk UUID
          uuid=$(blkid -o value -s UUID $disk)
   
          # Create a filesystem path to mount the disk
          mount_location="/mnt/fast-disks/${uuid}"
          mkdir -p $mount_location
   
          # Mount the disk
          mount $disk $mount_location
          
          # Mount the disk during a reboot
          echo $disk $mount_location ext4 defaults,noatime 0 2 >> /etc/fstab 
        done

使用如下命令,创建 EKS 托管节点组:

eksctl create nodegroup -f pv-nvme-nodegroup.yaml
  • 选项 2:一个单独的持久卷 RAID-0 阵列,跨所有 NVMe 实例存储卷

与上一个示例类似,我们使用 eksctl 工具并创建一个新的 HAQM EKS 托管节点组。但是,这次我们将在 NVMe 实例存储卷上创建一个软件 RAID-0 阵列。

在元数据中,将 eksworkshop-eksctl 和 us-west-2 替换为您各自的 EKS 集群名称和 AWS 区域。

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  # Replace with your EKS Cluster's name
  name: eksworkshop-eksctl
  # Replace with the AWS Region your cluster is deployed in
  region: us-west-2

managedNodeGroups:
  # Name to give the managed node-group
  - name: eks-pv-raid-ng
    # Label the nodes that they contain fast-disks 
    labels: { fast-disk-node: "pv-raid" }
    instanceType: i3.8xlarge
    desiredCapacity: 2
    volumeSize: 100 # EBS Boot Volume size
    privateNetworking: true
    preBootstrapCommands:
      - |
        # Install NVMe CLI
        yum install nvme-cli -y
        
        # Get list of NVMe Drives
        nvme_drives=$(nvme list | grep "HAQM EC2 NVMe Instance Storage" | cut -d " " -f 1 || true)
        readarray -t nvme_drives <<< "$nvme_drives"
        num_drives=${#nvme_drives[@]}
        
        # Install software RAID utility
        yum install mdadm -y
        
        # Create RAID-0 array across the instance store NVMe SSDs
        mdadm --create /dev/md0 --level=0 --name=md0 --raid-devices=$num_drives "${nvme_drives[@]}"

        # Format drive with Ext4
        mkfs.ext4 /dev/md0

        # Get RAID array's UUID
        uuid=$(blkid -o value -s UUID /dev/md0)
   
        # Create a filesystem path to mount the disk
        mount_location="/mnt/fast-disks/${uuid}"
        mkdir -p $mount_location
        
        # Mount RAID device
        mount /dev/md0 $mount_location
        
        # Have disk be mounted on reboot
        mdadm --detail --scan >> /etc/mdadm.conf 
        echo /dev/md0 $mount_location ext4 defaults,noatime 0 2 >> /etc/fstab

使用如下命令,创建 EKS 托管节点组:

eksctl create nodegroup --config-file=pv-raid-nodegroup.yaml

方式二:PVC动态绑定方案

核心组件:Local Volume Static Provisioner

Kubernetes 项目由多个特别兴趣小组(SIG)组成,这些小组专注于 Kubernetes 生态系统的某个特定部分。存储兴趣小组专注于不同类型的存储(块和文件),并确保容器调度时存储可用。存储 SIG 的子项目之一是本地卷静态配置器(Local Volume Static Provisioner),它是一个容器存储接口(CSI)驱动程序,可为实例启动期间连接的持久性磁盘创建 Kubernetes PersistentVolumes。

如果实现需要执行如下步骤:

1. 创建具有集群级权限的服务账户

CSI 驱动程序需要获得向 Kubernetes 控制平面发出 API 调用的权限,以管理 PV 的生命周期。 下面的清单定义了一个 Kubernetes 服务账户,并附加了一个授予必要 Kubernetes API 权限的 Kubernetes 集群角色。

复制下面的清单并保存为 service-account.yaml

# K8s service account for CSI Driver
apiVersion: v1
kind: ServiceAccount
metadata:
  name: local-volume-provisioner
  namespace: kube-system
---
# List of Permissions 
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: local-storage-provisioner-node-clusterrole
rules:
- apiGroups: [""]
  resources: ["persistentvolumes"]
  verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: ["storage.k8s.io"]
  resources: ["storageclasses"]
  verbs: ["get", "list", "watch"]
- apiGroups: [""]
  resources: ["events"]
  verbs: ["watch"]
- apiGroups: ["", "events.k8s.io"]
  resources: ["events"]
  verbs: ["create", "update", "patch"]
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["get"]
---
# Attach the K8s ClusterRole to our K8s ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: local-storage-provisioner-node-binding
  namespace: kube-system
subjects:
- kind: ServiceAccount
  name: local-volume-provisioner
  namespace: kube-system
roleRef:
  kind: ClusterRole
  name: local-storage-provisioner-node-clusterrole
  apiGroup: rbac.authorization.k8s.io

运行以下命令创建 ServiceAccount、ClusterRole 和 ClusterRoleBinding:

kubectl apply -f service-account.yaml

2. 为 CSI 驱动程序创建 ConfigMap

Local Volume Static Provisioner CSI 驱动程序在 Kubernetes ConfigMap 中存储了查找已挂载 EC2 NVMe 实例存储卷的位置,以及如何将它们作为 PV 暴露。 下面的 ConfigMap 指定 Local Volume Static Provisioner 在 /mnt/fast-disk 目录中查找已挂载的 NVMe 实例存储卷。

Kubernetes StorageClass 指定集群中可用的存储类型。 清单包括一个新的存储类别 fast-disks,以标识 PV 与 NVMe 实例存储卷有关。 复制并保存下面的清单为 config-map.yaml:

apiVersion: storage.k8s.io/v1kind: StorageClassmetadata:
name: fast-disksprovisioner: kubernetes.io/no-provisionervolumeBindingMode: WaitForFirstConsumer# Supported policies: Delete, Retain
reclaimPolicy: Retain---
# Configuration for our Local Persistent Volume CSI Driver
apiVersion: v1kind: ConfigMapmetadata:
name: local-volume-provisioner-config
  namespace: kube-system
data:
# Adds the node's hostname as a label to each PV created
nodeLabelsForPV: |
    - kubernetes.io/hostname
storageClassMap: |
    fast-disks:
      # Path on the host where local volumes of this storage class
      # are mounted under.
      hostDir: /mnt/fast-disks
# Optionally specify mount path of local volumes.
# By default, we use same path as hostDir in container.
mountDir: /mnt/fast-disks
 
      # The /scripts/shred.sh is contained in the CSI drivers container
# http://github.com/kubernetes-sigs/sig-storage-local-static-provisioner/blob/master/deployment/docker/scripts/shred.sh
blockCleanerCommand:
- "/scripts/shred.sh"
- "2"
# The volume mode of PV defines whether a device volume is
# intended to use as a formatted filesystem volume or to remain in block
# state. Value of Filesystem is implied when omitted.
volumeMode: Filesystem
      fsType: ext4
      
      # name pattern check
# only discover local disk mounted to path matching pattern("*" by default).
namePattern: "*"

运行以下命令创建 StorageClass 和 ConfigMap:

kubectl apply -f config-map.yaml

3. 创建 DaemonSet 以部署 CSI 驱动程序

本地卷静态配置器 CSI 驱动程序在每个需要将其 NVMe 实例存储卷作为 Kubernetes PV 暴露的 HAQM EKS 节点上运行。 通常,Kubernetes 集群中有多种实例类型,其中一些节点可能没有 NVMe 实例存储卷。 以下清单中的 DaemonSet 指定了一个 nodeAffinity 选择器,以便只在 HAQM EKS 节点上调度 DaemonSet,该节点的标签为 fast-disk-node,相应值为 pv-raid 或 pv-nvme。

复制并保存以下清单为 daemonset.yaml:

# The Local Persistent Volume CSI DaemonSet
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: local-volume-provisioner
  namespace: kube-system
  labels:
    app.kubernetes.io/name: local-volume-provisioner
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: local-volume-provisioner 
  template:
    metadata:
      labels:
        app.kubernetes.io/name: local-volume-provisioner
    spec:
      serviceAccountName: local-volume-provisioner
      containers:
          # The latest version can be found in the changelog.
          # In production, one might want to use the container digest hash 
          # over version for improved security.
          # http://github.com/kubernetes-sigs/sig-storage-local-static-provisioner/blob/master/CHANGELOG.md
        - image: "registry.k8s.io/sig-storage/local-volume-provisioner:v2.5.0"
          # In production you might want to set this to use a locally cached 
          # image by setting this to: IfNotPresent
          imagePullPolicy: "Always"
          name: provisioner 
          securityContext:
            privileged: true
          env:
          - name: MY_NODE_NAME
            valueFrom:
              fieldRef:
                fieldPath: spec.nodeName
          - name: MY_NAMESPACE
            valueFrom:
              fieldRef:
                fieldPath: metadata.namespace
          ports:
            # List of metrics at
            # http://github.com/kubernetes-sigs/sig-storage-local-static-provisioner/blob/cee9e228dc28a4355f664b4fe2236b1857fe4eca/pkg/metrics/metrics.go
            - name: metrics
              containerPort: 8080
          volumeMounts:
            - name: provisioner-config
              mountPath: /etc/provisioner/config
              readOnly: true             
            - mountPath:  /mnt/fast-disks 
              name: fast-disks
              mountPropagation: "HostToContainer" 
      volumes:
        - name: provisioner-config
          configMap:
            name: local-volume-provisioner-config
        - name: fast-disks
          hostPath:
            path: /mnt/fast-disks
      # Only run CSI Driver on the `fast-disk` tagged nodegroup 
      affinity:
        nodeAffinity: 
          requiredDuringSchedulingIgnoredDuringExecution: 
            nodeSelectorTerms: 
            - matchExpressions:
              - key: fast-disk-node
                operator: In
                values:
                - "pv-raid"
                - "pv-nvme"

运行以下命令创建 DaemonSet:

kubectl apply -f daemonset.yaml

4. 在托管节点组中利用 Pre-bootstrap Commands 初始化本地盘

部署的 ConfigMap 中,本地卷静态配置器 CSI 驱动程序会查找挂载在 /mnt/fast-disks 目录中的磁盘,并在标签为 fast-disk-node、值为 pv-raid 或 pv-nvme 的节点上运行。 现在,我们需要配置 HAQM EKS 受管节点组,以启动带有 fast-disk-node 标签的 EC2 实例,并在启动时将 NVMe 实例存储卷挂载到 /mnt/fast-disks 目录。

复制下面的清单并保存为 pv-nodegroup.yaml:

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: eksworkshop-eksctl
  region: us-west-2
managedNodeGroups:
  - name: storage-nvme
    desiredCapacity: 4
    instanceType: i4g.large
    preBootstrapCommands:
      - mkdir -p /mnt/fast-disks
      - |
          cat <<EOF > /etc/udev/rules.d/90-kubernetes-nvme-symlink.rules
          # Discover Instance Storage disks so kubernetes local provisioner can pick them up from
          KERNEL=="nvme[0-9]*n[0-9]*", ENV{DEVTYPE}=="disk", ATTRS{model}=="HAQM EC2 NVMe Instance Storage", ATTRS{serial}=="?*", SYMLINK+="/mnt/fast-disks/nvme-\\\$attr{model}_\\\$attr{serial}", OPTIONS="string_escape=replace"
          EOF
      - udevadm control --reload && udevadm trigger
      

提示:对于需要极致 IOPS 的场景,推荐使用 RAID-0 阵列模式,可通过 mdadm 工具创建软件 RAID,将多个 NVMe 设备聚合为单个逻辑卷。

运行以下命令创建托管节点组:

eksctl create nodegroup --config-file=pv-nodegroup.yaml

5. 测试创建 PVC 与 Pod

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: fast-pvc
spec:
  storageClassName: local-fast
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 500Gi  # 容量仅用于匹配PV

---
apiVersion: v1
kind: Pod
metadata:
  name: app-with-pvc
spec:
  containers:
  - name: app
    image: nginx
    volumeMounts:
    - name: data
      mountPath: /data
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: fast-pvc

该 CSI 驱动自动检测节点挂载点并创建 PV,实现存储资源抽象化。

总结

本文对比了直接访问与 PVC 管理两种本地存储使用方式。对于需要快速上手的简单场景,方案一更为便捷;而在需要动态分配、多应用共享的复杂环境中,方案二展示了 Kubernetes 存储管理的强大能力。开发者可根据实际需求灵活选择,充分发挥实例存储的高性能优势。

本篇作者

张凯

亚马逊云科技解决方案架构师,主要负责基于亚马逊云科技的解决方案架构设计和方案咨询。具有多年的架构设计、项目管理经验。

叶鹏勇

亚马逊云科技解决方案架构师,负责容器以及 DevOps 相关领域的方案和架构设计工作。在容器、微服务、SRE 相关领域有多年的实战经验。