全部產品
Search
文件中心

Container Service for Kubernetes:使用NVMe雲端硬碟多重掛載及Reservation實現應用間的資料共用

更新時間:Aug 13, 2024

支援NVMe(Non-Volatile Memory Express)協議的ESSD雲端硬碟稱為NVMe雲端硬碟。NVMe雲端硬碟支援多重掛載能力,最多可以同時掛載到16個ECS執行個體上;同時也基於多重掛載實現了符合NVMe協議規範的Reservation功能。這些特性可以協助您實現應用的多個副本間的資料共用以提升資料讀寫效能。本文通過簡單的樣本介紹如何在ACK叢集中使用NVMe雲端硬碟多重掛載及Reservation功能。

閱讀前提示

為了讓您更好地使用NVMe雲端硬碟多重掛載及Reservation功能,建議您在閱讀本文檔之前,瞭解以下內容:

應用情境

雲端硬碟多重掛載主要有以下應用情境:

  • 資料共用

    NVMe最簡單的應用情境為資料共用,當資料被寫入雲端硬碟後,其他節點均可以訪問該資料,從而有效節省成本並提升讀寫效能。例如,在雲上容器鏡像情境,同一套系統的鏡像通常相似,因此多個不同執行個體可以讀取載入同一份鏡像。

  • 高可用容錯移轉

    業務高可用是共用盤最常見的應用情境之一。傳統基於SAN的資料庫,例如Oracle RAC、SAP HANA以及雲原生高可用資料庫等情境中,實際業務使用過程中可能存在單點故障,確保故障情況下商務持續性是高可用系統的核心能力,在雲上儲存和網路具備極高的可用性。而計算節點則經常受斷電、宕機、硬體故障等影響,所以業務通常搭建主備模式解決計算的高可用問題。

    例如資料庫情境,當主庫故障時迅速切換到備庫對外提供服務,執行個體切換後,可以通過NVMe PR命令釋放舊執行個體的寫入許可權,從而確保舊執行個體不再寫入資料確保資料一致性。如圖所示,容錯移轉流程說明如下:

    說明

    PR(PersistentReservation)屬於NVMe協議的一部分,PR可精確地控制某個雲端硬碟的讀寫權限,從而確保計算端按照預期寫入資料。更多資訊,請參見NVMe PR協議

    1. 資料庫主執行個體1故障,導致業務停止。

    2. 下發NVMe PR命令,禁止資料庫執行個體1繼續寫入資料,允許資料庫執行個體2寫入資料。

    3. 資料庫執行個體2通過日誌回放等方式恢複到和資料庫執行個體1一致的狀態。

    4. 切換資料庫執行個體2為主執行個體,繼續對外提供服務。

  • 分布式緩衝加速

    開啟多重掛載功能的雲端硬碟具備較高的IOPS和吞吐效能,可以為其他中低速的儲存系統提供效能加速能力。例如資料湖情境,資料湖通常基於OSS搭建,可同時被多個用戶端訪問,同時具備較高的順序讀吞吐和追加寫吞吐能力,但是其順序讀寫吞吐和延遲較差,其隨機讀寫效能較差。通過在計算節點上掛載高速雲端硬碟作為緩衝,可以極大地提升資料湖等情境的訪問效能。

  • 機器學習

    在分布式機器學習訓練中,將樣本標註寫入後,會將資料集分割成小塊分發到多個計算節點上平行處理。雲端硬碟多重掛載使得每個計算節點都能直接存取共用的儲存資源,無需通過網路頻繁傳輸資料,減少了資料轉送的延遲,從而加速了模型訓練過程。雲端硬碟的高效能與多重掛載功能相結合,為機器學習情境提供了一個高效、靈活的儲存解決方案,特別是針對需要高速資料訪問和處理的大規模模型訓練任務,能夠顯著提升整個機器學習流程的效率和效果。

使用限制

  • 單個NVMe雲端硬碟支援同時掛載到同一可用性區域內的最多16個ECS執行個體。

  • ACK僅支援通過volumeDevices的方式掛載可從多個節點讀寫的雲端硬碟,即不能通過檔案系統訪問。

  • 更多使用限制,請參見多重掛載使用限制

前提條件

  • 已建立ACK託管叢集,且叢集為1.20及以上版本。具體操作,請參見建立ACK託管叢集

  • 已安裝csi-plugin和csi-provisioner組件,且組件為v1.24.10-7ae4421-aliyun及以上版本。關於csi-plugin和csi-provisioner組件的升級操作,請參見管理CSI組件

  • 叢集至少包含2個在同一可用性區域的且支援使用多重掛載功能的節點,支援的執行個體規格類型系列詳見多重掛載使用限制

  • 已準備好業務應用且符合以下要求,然後將應用打包至容器鏡像中用於在ACK叢集中部署。

    • 應用支援同時從多個副本中訪問同一雲端硬碟中的資料。

    • 應用能自行通過標準的NVMe Reservation等功能確保資料的一致性。

計費說明

雲端硬碟多重掛載功能不會產生額外費用,支援NVMe協議的相關資源仍保持各資源原有的計費方式。關於雲端硬碟相關計費的更多資訊,請參見計費說明

應用樣本

本文使用下方應用樣本的原始碼和Dockerfile,將其構建後上傳至鏡像倉庫以便後續在叢集中部署。該樣本應用中的多個副本共同管理一個租約,但僅有一個副本持有該租約。若該副本無法正常工作,其他副本將自動搶佔該租約。編寫應用注意事項如下:

  • 樣本中使用O_DIRECT開啟塊裝置進行讀寫,避免任何緩衝對測試的影響。

  • 樣本中使用Linux核心提供的Reservation簡化介面,應用也可使用以下兩種方法執行與Reservation相關的命令,以下方法需要特權。

    • C代碼:ioctl(fd, NVME_IOCTL_IO_CMD, &cmd);

    • 命令列工具:nvme-cli

  • 關於NVMe Reservation功能的詳細資料,請參見NVMe Specification

展開查看應用樣本的原始碼

#define _GNU_SOURCE
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/pr.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <time.h>
#include <unistd.h>

const char *disk_device = "/dev/data-disk";
uint64_t magic = 0x4745D0C5CD9A2FA4;

void panic(const char *restrict format, ...) {
    va_list args;
    va_start(args, format);
    vfprintf(stderr, format, args);
    va_end(args);
    exit(EXIT_FAILURE);
}

struct lease {
    uint64_t magic;
    struct timespec acquire_time;
    char holder[64];
};

volatile bool shutdown = false;
void on_term(int signum) {
    shutdown = true;
}

struct lease *lease;
const size_t lease_alloc_size = 512;

void acquire_lease(int disk_fd) {
    int ret;

    struct pr_registration pr_reg = {
        .new_key = magic,
        .flags = PR_FL_IGNORE_KEY,
    };
    ret = ioctl(disk_fd, IOC_PR_REGISTER, &pr_reg);
    if (ret != 0)
        panic("failed to register (%d): %s\n", ret, strerror(errno));

    struct pr_preempt pr_pre = {
        .old_key = magic,
        .new_key = magic,
        .type  = PR_WRITE_EXCLUSIVE,
    };
    ret = ioctl(disk_fd, IOC_PR_PREEMPT, &pr_pre);
    if (ret != 0)
        panic("failed to preempt (%d): %s\n", ret, strerror(errno));

    // register again in case we preempted ourselves
    ret = ioctl(disk_fd, IOC_PR_REGISTER, &pr_reg);
    if (ret != 0)
        panic("failed to register (%d): %s\n", ret, strerror(errno));
    fprintf(stderr, "Register as key %lx\n", magic);


    struct pr_reservation pr_rev = {
        .key   = magic,
        .type  = PR_WRITE_EXCLUSIVE,
    };
    ret = ioctl(disk_fd, IOC_PR_RESERVE, &pr_rev);
    if (ret != 0)
        panic("failed to reserve (%d): %s\n", ret, strerror(errno));

    lease->magic = magic;
    gethostname(lease->holder, sizeof(lease->holder));

    while (!shutdown) {
        clock_gettime(CLOCK_MONOTONIC, &lease->acquire_time);
        ret = pwrite(disk_fd, lease, lease_alloc_size, 0);
        if (ret < 0)
            panic("failed to write lease: %s\n", strerror(errno));
        fprintf(stderr, "Refreshed lease\n");
        sleep(5);
    }
}

int timespec_compare(const struct timespec *a, const struct timespec *b) {
    if (a->tv_sec < b->tv_sec)
        return -1;
    if (a->tv_sec > b->tv_sec)
        return 1;
    if (a->tv_nsec < b->tv_nsec)
        return -1;
    if (a->tv_nsec > b->tv_nsec)
        return 1;
    return 0;
}

int main() {
    assert(lease_alloc_size >= sizeof(struct lease));
    lease = aligned_alloc(512, lease_alloc_size);
    if (lease == NULL)
        panic("failed to allocate memory\n");

    // char *reg_key_str = getenv("REG_KEY");
    // if (reg_key_str == NULL)
    //     panic("REG_KEY env not specified");

    // uint64_t reg_key = atoll(reg_key_str) | (magic << 32);
    // fprintf(stderr, "Will register as key %lx", reg_key);


    int disk_fd = open(disk_device, O_RDWR|O_DIRECT);
    if (disk_fd < 0)
        panic("failed to open disk: %s\n", strerror(errno));

    // setup signal handler
    struct sigaction sa = {
        .sa_handler = on_term,
    };
    sigaction(SIGTERM, &sa, NULL);
    sigaction(SIGINT, &sa, NULL);

    struct timespec last_active_local;
    struct timespec last_active_remote;

    int ret = pread(disk_fd, lease, lease_alloc_size, 0);
    if (ret < 0)
        panic("failed to read lease: %s\n", strerror(errno));

    if (lease->magic != magic) {
        // new disk, no lease
        acquire_lease(disk_fd);
    } else {
        // someone else has the lease
        while (!shutdown) {
            struct timespec now;
            clock_gettime(CLOCK_MONOTONIC, &now);
            if (timespec_compare(&lease->acquire_time, &last_active_remote)) {
                fprintf(stderr, "Remote %s refreshed lease\n", lease->holder);
                last_active_remote = lease->acquire_time;
                last_active_local = now;
            } else if (now.tv_sec - last_active_local.tv_sec > 20) {
                // remote is dead
                fprintf(stderr, "Remote is dead, preempting\n");
                acquire_lease(disk_fd);
                break;
            }
            sleep(5);
            int ret = pread(disk_fd, lease, lease_alloc_size, 0);
            if (ret < 0)
                panic("failed to read lease: %s\n", strerror(errno));
        }
    }

    close(disk_fd);
}
#!/bin/bash

set -e

DISK_DEVICE="/dev/data-disk"
MAGIC=0x4745D0C5CD9A2FA4

SHUTDOWN=0
trap "SHUTDOWN=1" SIGINT SIGTERM

function acquire_lease() {
    # racqa:
    # 0: aquire
    # 1: preempt

    # rtype:
    # 1: write exclusive

    nvme resv-register $DISK_DEVICE --iekey --nrkey=$MAGIC
    nvme resv-acquire $DISK_DEVICE --racqa=1 --rtype=1 --prkey=$MAGIC --crkey=$MAGIC
    # register again in case we preempted ourselves
    nvme resv-register $DISK_DEVICE --iekey --nrkey=$MAGIC
    nvme resv-acquire $DISK_DEVICE --racqa=0 --rtype=1 --prkey=$MAGIC --crkey=$MAGIC

    while [[ $SHUTDOWN -eq 0 ]]; do
        echo "$MAGIC $(date +%s) $HOSTNAME" | dd of=$DISK_DEVICE bs=512 count=1 oflag=direct status=none
        echo "Refreshed lease"
        sleep 5
    done
}

LEASE=$(dd if=$DISK_DEVICE bs=512 count=1 iflag=direct status=none)

if [[ $LEASE != $MAGIC* ]]; then
    # new disk, no lease
    acquire_lease
else
    last_active_remote=-1
    last_active_local=-1
    while [[ $SHUTDOWN -eq 0 ]]; do
        now=$(date +%s)
        read -r magic timestamp holder < <(echo $LEASE)
        if [ "$last_active_remote" != "$timestamp" ]; then
            echo "Remote $holder refreshed the lease"
            last_active_remote=$timestamp
            last_active_local=$now
        elif (($now - $last_active_local > 10)); then
            echo "Remote is dead, preempting"
            acquire_lease
            break
        fi
        sleep 5
        LEASE=$(dd if=$DISK_DEVICE bs=512 count=1 iflag=direct status=none)
    done
fi

下文部署所用YAML檔案僅適用於C語言版本,Bash版本部署時需要在YAML中為容器授權:

securityContext:
  capabilities:
    add: ["SYS_ADMIN"]

展開查看Dockerfile

C語言版本的Dockerfile:

# syntax=docker/dockerfile:1.4

FROM buildpack-deps:bookworm as builder

COPY lease.c /usr/src/nvme-resv/
RUN gcc -o /lease -O2 -Wall /usr/src/nvme-resv/lease.c

FROM debian:bookworm-slim

COPY --from=builder --link /lease /usr/local/bin/lease
ENTRYPOINT ["/usr/local/bin/lease"]

Bash版本的Dockerfile:

# syntax=docker/dockerfile:1.4
FROM debian:bookworm-slim

RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources && \
    rm -f /etc/apt/apt.conf.d/docker-clean && \
    apt-get update && \
    apt-get install -y nvme-cli

COPY --link lease.sh /usr/local/bin/lease
ENTRYPOINT ["/usr/local/bin/lease"]

步驟一:部署應用並配置多重掛載

建立名為alicloud-disk-shared的StorageClass,並開啟雲端硬碟的多重掛載功能。

建立名為data-disk的PVC,並設定accessModesReadWriteManyvolumeModeBlock

建立名為lease-test的StatefulSet應用,使用本文應用樣本的鏡像。

  1. 使用以下內容,建立lease.yaml檔案。

    請將以下YAML中容器鏡像地址替換為您實際應用的鏡像地址。

    重要
    • 由於NVMe Reservation在節點維度生效,同一節點上的多個Pod可能會互相干擾,所以本樣本中通過podAntiAffinity以避免多個Pod調度到同一個節點上。

    • 如果您的叢集中包括其他不使用NVMe協議的節點,您需要自行設定親和性,以確保將Pod調度到使用NVMe協議的節點上。

    展開查看lease.yaml檔案

    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: alicloud-disk-shared
    parameters:
      type: cloud_essd
      multiAttach: "true"
    provisioner: diskplugin.csi.alibabacloud.com
    reclaimPolicy: Delete
    volumeBindingMode: WaitForFirstConsumer
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: data-disk
    spec:
      accessModes: [ "ReadWriteMany" ]
      storageClassName: alicloud-disk-shared
      volumeMode: Block
      resources:
        requests:
          storage: 20Gi
    ---
    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: lease-test
    spec:
      replicas: 2
      serviceName: lease-test
      selector:
        matchLabels:
          app: lease-test
      template:
        metadata:
          labels:
            app: lease-test
        spec:
          affinity:
            podAntiAffinity:
              requiredDuringSchedulingIgnoredDuringExecution:
              - labelSelector:
                  matchExpressions:
                  - key: app
                    operator: In
                    values:
                    - lease-test
                topologyKey: "kubernetes.io/hostname"
          containers:
          - name: lease
            image: <IMAGE OF APP>   # 替換為您應用的鏡像地址。
            volumeDevices:
            - name: data-disk
              devicePath: /dev/data-disk  
          volumes:
          - name: data-disk
            persistentVolumeClaim:
              claimName: data-disk

    參數

    使用多重掛載功能配置說明

    普通掛載配置說明

    StorageClass

    parameters.multiAttach

    設定為true,以開啟雲端硬碟的多重掛載功能。

    無需配置

    PVC

    accessModes

    ReadWriteMany

    ReadWriteOnce

    volumeMode

    Block

    Filesystem

    儲存卷掛載方式

    volumeDevices:直接通過塊裝置訪問雲端硬碟中的資料。

    volumeMounts:主要用於掛載檔案系統類型的Volume。

  2. 執行以下命令,部署應用。

    kubectl apply -f lease.yaml

步驟二:驗證多重掛載及Reservation效果

為了確保NVMe雲端硬碟的資料一致性,您可以在應用中通過Reservation控制讀寫權限,如果一個Pod進行寫操作,其他Pod就只能進行讀操作。

多個節點可讀寫同一個雲端硬碟

執行以下命令,查看Pod日誌。

kubectl logs -l app=lease-test --prefix -f

預期輸出:

[pod/lease-test-0/lease] Register as key 4745d0c5cd9a2fa4
[pod/lease-test-0/lease] Refreshed lease
[pod/lease-test-0/lease] Refreshed lease
[pod/lease-test-1/lease] Remote lease-test-0 refreshed lease
[pod/lease-test-0/lease] Refreshed lease
[pod/lease-test-1/lease] Remote lease-test-0 refreshed lease
[pod/lease-test-0/lease] Refreshed lease
[pod/lease-test-1/lease] Remote lease-test-0 refreshed lease
[pod/lease-test-0/lease] Refreshed lease
[pod/lease-test-1/lease] Remote lease-test-0 refreshed lease

預期輸出表明,Pod lease-test-1可以即時讀取到Pod lease-test-0的寫入的內容。

NVMe Reservation建立成功

  1. 執行以下命令,擷取雲端硬碟ID。

    kubectl get pvc data-disk -ojsonpath='{.spec.volumeName}'
  2. 登入兩個節點中的任意一個節點,執行以下命令確認NVMe Reservation是否建立成功。

    請替換以下代碼中2zxxxxxxxxxxx為您上一步擷取到的雲端硬碟ID中d-之後的內容。

    nvme resv-report -c 1 /dev/disk/by-id/nvme-Alibaba_Cloud_Elastic_Block_Storage_2zxxxxxxxxxxx

    預期輸出:

    NVME Reservation status:
    
    gen       : 3
    rtype     : 1
    regctl    : 1
    ptpls     : 1
    regctlext[0] :
      cntlid     : ffff
      rcsts      : 1
      rkey       : 4745d0c5cd9a2fa4
      hostid     : 4297c540000daf4a4*****

    預期輸出表明,NVMe Reservation已建立成功。

通過Reservation可阻斷異常節點的寫入IO

  1. 登入Pod lease-test-0所在的節點上執行以下命令,暫停該進程用於類比故障情境。

    pkill -STOP -f /usr/local/bin/lease
  2. 等待30秒後,執行以下命令,再次查看日誌。

    kubectl logs -l app=lease-test --prefix -f

    預期輸出:

    [pod/lease-test-1/lease] Remote lease-test-0 refreshed lease
    [pod/lease-test-1/lease] Remote is dead, preempting
    [pod/lease-test-1/lease] Register as key 4745d0c5cd9a2fa4
    [pod/lease-test-1/lease] Refreshed lease
    [pod/lease-test-1/lease] Refreshed lease
    [pod/lease-test-1/lease] Refreshed lease

    預期輸出表明,此時Pod lease-test-1已接管,持有租約成為服務的主節點。

  3. 再次登入Pod lease-test-0所在的節點上執行以下命令,恢複之前暫停進程。

    pkill -CONT -f /usr/local/bin/lease
  4. 執行以下命令,再次查看日誌。

    kubectl logs -l app=lease-test --prefix -f

    預期輸出:

    [pod/lease-test-0/lease] failed to write lease: Invalid exchange

    預期輸出表明,Pod lease-test-0將無法再寫入該雲端硬碟,容器lease自動重啟。說明其寫入IO的操作已成功被Reservation阻斷。

相關文檔

如果您的NVMe雲端硬碟空間不滿足要求或磁碟已滿,請參見擴容雲端硬碟儲存卷