すべてのプロダクト
Search
ドキュメントセンター

Container Service for Kubernetes:NVMeディスクのマルチアタッチ機能とNVMe予約機能を使用する

最終更新日:Dec 11, 2024

不揮発性メモリエクスプレス (NVMe) プロトコルをサポートするエンタープライズSSD (ESSD) は、NVMeディスクと呼ばれます。 NVMeディスクはマルチアタッチ機能をサポートしています。 この機能を使用すると、最大16個のElastic Compute Service (ECS) インスタンスにNVMeディスクを接続し、さらにNVMe仕様に準拠したNVMe予約機能を使用できます。 これらの機能により、アプリケーションのポッドを実行するすべてのノード間でのデータ共有が容易になり、データの読み書きパフォーマンスが向上します。 このトピックでは、Container Service for Kubernetes (ACK) クラスターでNVMeディスクのマルチアタッチおよびNVMe予約機能を使用する方法について説明します。

始める前に

NVMeディスクのマルチアタッチ機能とNVMe予約機能をより有効に活用するために、このトピックを読む前に次の内容を理解することをお勧めします。

  • NVMeプロトコルの詳細については、「NVMeプロトコル」をご参照ください。

  • NVMeディスクの詳細については、「NVMeディスク」をご参照ください。

シナリオ

マルチアタッチ機能は、次のシナリオに適しています。

  • データ共有

    データ共有は、NVMeの最も単純な使用シナリオです。 1つの接続ノードから共有NVMeディスクにデータが書き込まれた後、他のすべての接続ノードはデータにアクセスできます。 これにより、ストレージコストが削減され、読み取り /書き込みパフォーマンスが向上します。 たとえば、クラウド内の単一のNVMe対応コンテナイメージは、同じオペレーティングシステムを実行する複数のインスタンスによって読み取り、ロードできます。

    image
  • 高可用性フェールオーバー

    高サービス可用性は、共有ディスクの最も一般的なアプリケーションシナリオの1つです。 Oracle Real Application Clusters (RAC) 、SAP High-performance Aanalytic Appliance (HANA) 、クラウドネイティブの高可用性データベースなどの従来のSANベースのデータベースは、実際のビジネスシナリオで単一障害点 (SPOF) に遭遇する可能性があります。 共有NVMeディスクを使用して、SPOFの場合にクラウドベースのストレージとネットワークに関してビジネスの継続性と高可用性を確保できます。 計算ノードでは、頻繁な停止、ダウンタイム、ハードウェア障害が発生します。 コンピューティングノードの高可用性を実現するために、プライマリ /セカンダリモードでビジネスをデプロイできます。

    たとえば、データベースのシナリオでは、プライマリデータベースに障害が発生した場合、セカンダリデータベースがすぐにサービスを提供します。 プライマリデータベースをホストするインスタンスをセカンダリデータベースをホストするインスタンスに切り替えた後、NVMe Persistent Reservation (PR) コマンドを実行して、障害のあるプライマリデータベースに対する書き込み権限を取り消すことができます。 これにより、データが障害のあるプライマリデータベースに書き込まれるのを防ぎ、データの一貫性を保証します。 次の図は、フェールオーバープロセスを示しています。

    説明

    PRはNVMeプロトコルの一部であり、クラウドディスクの読み取りおよび書き込み権限を正確に制御して、計算ノードが期待どおりにデータを書き込むことができるようにします。 詳細については、「NVM Expressベース仕様」をご参照ください。

    1. プライマリデータベースインスタンス (データベースインスタンス1) に障害が発生し、サービスが停止します。

    2. NVMe PRコマンドを実行して、データがデータベースインスタンス1に書き込まれないようにし、データがセカンダリデータベースインスタンス (データベースインスタンス2) に書き込まれるようにします。

    3. ログの再生など、さまざまな方法を使用して、データベースインスタンス2をデータベースインスタンス1と同じ状態に復元します。

    4. データベースインスタンス2がプライマリデータベースインスタンスとして引き継ぎ、外部にサービスを提供します。

    image
  • 分散データキャッシュの高速化

    マルチアタッチ対応のクラウドディスクは、高いパフォーマンス、IOPS、およびスループットを実現し、低速および中速のストレージシステムのパフォーマンスを高速化できます。 たとえば、データレイクは通常、Object Storage Service (OSS) の上に構築されます。 各データレイクは、複数のクライアントによって同時にアクセスすることができる。 データレイクは、高いシーケンシャル読み取りスループットおよび高いアペンド書き込みスループットを提供するが、低いシーケンシャル読み取り /書き込みスループット、高いレイテンシ、および低いランダム読み取り /書き込みパフォーマンスを有する。 データレイクなどのシナリオでのアクセスパフォーマンスを大幅に向上させるために、高速でマルチアタッチ対応のクラウドディスクをキャッシュとして計算ノードにアタッチできます。

    image
  • 機械学習

    機械学習シナリオでは、サンプルにラベルを付けて書き込んだ後、サンプルを分割して複数のノードに分散させ、並列分散コンピューティングを容易にします。 マルチアタッチ機能により、ネットワークを介して頻繁にデータを送信する必要なく、各計算ノードが共有ストレージリソースに直接アクセスできます。 これにより、データ転送の待ち時間が短縮され、モデルトレーニングプロセスが高速化されます。 高性能とマルチアタッチ機能の組み合わせにより、クラウドディスクは、高速データアクセスと処理を必要とする大規模なモデルトレーニングタスクなどの機械学習シナリオに対応する効率的で柔軟なストレージソリューションを提供できます。 ストレージソリューションは、機械学習プロセスの効率と有効性を大幅に向上させます。

    image

制限事項

  • 1つのNVMeディスクを、同じゾーンにある最大16のECSインスタンスに接続できます。

  • マルチアタッチ機能を使用する場合、ACKではvolumeDevicesメソッドを使用してのみNVMeディスクをアタッチできます。 この場合、複数のノード上のディスクからデータを読み書きできますが、ファイルシステムからディスクにアクセスすることはできません。

  • 詳細については、「マルチアタッチの有効化」トピックの制限セクションを参照してください。

前提条件

  • Kubernetes 1.20以降を実行するACKマネージドクラスターが作成されます。 詳細については、「ACK管理クラスターの作成」をご参照ください。

  • V1.24.10-7ae4421-aliyun以降のcsi-pluginおよびcsi-provisionerコンポーネントがインストールされます。 csi-pluginおよびcsi-provisionerコンポーネントを更新する方法の詳細については、「CSIプラグインの管理」をご参照ください。

  • ACKクラスタは、同じゾーンに存在し、マルチアタッチ機能をサポートする少なくとも2つのノードを含む。 マルチアタッチ機能をサポートするインスタンスファミリーの詳細については、「マルチアタッチの有効化」トピックの制限セクションをご参照ください。

  • 以下の要件を満たすアプリケーションを用意する。 アプリケーションは、ACKクラスターにデプロイするためにコンテナイメージにパッケージ化されます。

    • アプリケーションは、複数のレプリケートされたポッドからディスク上のデータへの同時アクセスをサポートします。

    • アプリケーションは、NVMe予約機能などの標準機能を使用して、データの一貫性を確保できます。

課金

NVMeディスクのマルチアタッチ機能は無料です。 個々の課金方法に基づいて、NVMeをサポートするリソースに対して課金されます。 ディスク課金の詳細については、「課金」をご参照ください。

サンプル申請

この例では、次のソースコードとDockerfileを使用してサンプルアプリケーションを開発します。 ソースコードとDockerfileはイメージリポジトリにアップロードされ、その後クラスターにデプロイされます。 複数のレプリケートされたポッドが共同でコンテナリースを管理しますが、リースをマスターするポッドは1つだけです。 コンテナリースをマスターするポッドが期待どおりに実行できない場合、他のポッドが自動的にこのリースを先取りします。 サンプルアプリケーションを開発するときは、次の項目に注意してください。

  • サンプルアプリケーションでは、O_DIRECTを使用してブロックストレージデバイスを開き、読み取りおよび書き込み操作を実行します。 これにより、この例のテストがキャッシュの影響を受けないようにします。

  • サンプルアプリケーションでは、Linuxカーネルが提供する簡易予約用のインターフェイスを使用しています。 次のいずれかの方法を使用して、NVMe予約コマンドを実行することもできます。 この場合、コンテナに必要な権限を付与する必要があります。

    • C: ioctl(fd、NVME_IOCTL_IO_CMD、&cmd);

    • コマンドラインツール: nvme-cli

  • NVMe予約機能の詳細については、「NVMe仕様」をご参照ください。

サンプルアプリケーションのソースコードの表示

#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

この例で使用したlease.yamlファイルは、Cを使用してアプリケーションをデプロイする場合にのみ適用されます。bashを使用してアプリケーションをデプロイする場合は、YAMLファイル内のコンテナーに必要な権限を付与する必要があります。 サンプルコード:

securityContext:
  capabilities:
    add: ["SYS_ADMIN"]

Dockerfileの表示

C:

# 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"]

バッシュ:

# 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"]

手順1: アプリケーションのデプロイとマルチアタッチの設定

alicloud-disk-sharedという名前のStorageClassを作成し、NVMeディスクのマルチアタッチ機能を有効にします。

data-diskという名前の永続ボリュームクレーム (PVC) を作成し、accessModesパラメーターをReadWriteManyに設定し、volumeModeパラメーターをBlockに設定します。

この例のサンプルアプリケーションのイメージを使用して、リーステストという名前のStatefulSetを作成します。

  1. 次のコンテンツを含むlease.yamlファイルを作成します。

    次のYAMLファイルのコンテナイメージURLをアプリケーションのイメージURLに置き換えます。

    重要
    • NVMe予約機能はノードで有効になります。 同じノード上の複数のポッドが互いに干渉する可能性があります。 この例では、podAntiAffinity設定は、複数のポッドが同じノードにスケジュールされないように設定されています。

    • クラスターにNVMeプロトコルを使用しない他のノードが含まれている場合は、nodeAffinity設定を構成して、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>   # Specify the image URL of your application. 
            volumeDevices:
            - name: data-disk
              devicePath: /dev/data-disk  
          volumes:
          - name: data-disk
            persistentVolumeClaim:
              claimName: data-disk

    パラメータまたは設定

    マルチアタッチ機能の設定

    通常のアタッチ機能の設定

    StorageClass

    パラメータ. multiAttach

    true: NVMeディスクのマルチアタッチ機能を有効にします。

    設定は必要ありません。

    PVC

    accessModes

    ReadWriteMany

    ReadWriteOnce

    volumeMode

    ブロック

    ファイルシステム

    ボリューム接続方法

    volumeDevices: ブロックストレージデバイスを使用してディスク上のデータにアクセスします。

    volumeMounts: ファイルシステムのボリュームをアタッチします。

  2. 次のコマンドを実行して、アプリケーションをデプロイします。

    kubectl apply -f lease.yaml

ステップ2: マルチアタッチ機能とNVMe予約機能を確認する

NVMeクラウドディスクのデータの一貫性を確保するために、予約機能を使用して、アプリケーションの読み取りおよび書き込み権限を制御できます。 1つのポッドが書き込み操作を実行している場合、他のポッドは読み取り操作のみを実行できます。

複数のノードのディスクからのデータの読み取りとディスクへのデータの書き込み

次のコマンドを実行して、ポッドログを照会します。

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予約を取得したかどうかを確認する

  1. 次のコマンドを実行して、ディスクIDを照会します。

    kubectl get pvc data-disk -ojsonpath='{.spec.volumeName}'
  2. 2つのノードのいずれかにログインし、次のコマンドを実行して、NVMe予約が取得されているかどうかを確認します。

    次のコードの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予約が取得されることを示す。

NVMe予約機能を使用した異常なノードでの書き込み操作のブロック

  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

    予想される結果は、ポッドのリーステスト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がディスクにデータを書き込むことができなくなり、コンテナリースが自動的に再開されることを示しています。 これは、ポッドリーステスト0の書き込み操作がNVMe予約機能によってブロックされていることを示しています。

関連ドキュメント

NVMeディスクに十分な容量がない場合は、「ディスクボリュームの拡張」をご参照ください。