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

Container Service for Kubernetes:アプリケーション間のデータ共有のための NVMe クラウドディスクのマルチアタッチと Reservation の使用

最終更新日:Nov 09, 2025

複数のノードが同じクラウドディスクに同時に読み書きして、効率的なデータ共有と高速なフェイルオーバーを実現する必要がある場合、マルチアタッチ機能を使用して、単一の ESSD、ESSD AutoPL、または他のタイプのクラウドディスクを同じゾーン内の NVMe プロトコルをサポートする複数のノードにアタッチしたり、単一のゾーン冗長ストレージ ESSD を同じリージョン内の複数のノードにアタッチしたりできます。このトピックでは、ACK クラスターで NVMe クラウドディスクのマルチアタッチと Reservation 機能を使用する方法を説明します。

始める前に

NVMe クラウドディスクのマルチアタッチと Reservation 機能をより良く使用するために、このドキュメントを読む前に以下の情報を理解することをお勧めします。

シナリオ

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

  • データ共有

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

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

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

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

    説明

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

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

    2. NVMe PR コマンドを実行して、データベースインスタンス 1 へのデータ書き込みを防止し、セカンダリデータベースインスタンス (データベースインスタンス 2) へのデータ書き込みを許可します。

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

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

  • 分散データキャッシュアクセラレーション

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

  • 機械学習

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

制限事項

  • 単一の NVMe クラウドディスクは、同じゾーン内の最大 16 個の ECS インスタンスに同時にアタッチできます。

  • 複数のノードからクラウドディスクに同時に読み書きする場合は、volumeDevices を使用してクラウドディスクをマウントする必要があります。このメソッドは、クラウドディスクをブロックデバイスとしてマウントし、ファイルシステムを介したアクセスをサポートしません。

  • 制限事項の詳細については、「マルチアタッチ機能の制限事項」をご参照ください。

準備

  • ACK マネージドクラスターが作成され、クラスターの Kubernetes バージョンが 1.20 以降です。詳細については、「ACK マネージドクラスターの作成」をご参照ください。

  • csi-plugin および csi-provisioner コンポーネントがインストールされており、コンポーネントのバージョンは v1.24.10-7ae4421-aliyun 以降です。csi-plugin および csi-provisioner コンポーネントのアップグレード方法の詳細については、「csi-plugin および csi-provisioner コンポーネントの管理」をご参照ください。

  • クラスターには、同じゾーンにあり、マルチアタッチ機能をサポートする少なくとも 2 つのノードが含まれています。マルチアタッチ機能をサポートするインスタンスファミリーの詳細については、「マルチアタッチ機能の制限事項」をご参照ください。

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

    • アプリケーションは、複数のレプリカから同じクラウドディスク上のデータに同時にアクセスすることをサポートします。

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

課金に関する説明

マルチアタッチ機能に追加料金は発生しません。NVMe プロトコルをサポートするリソースは、引き続き元の課金方法に基づいて課金されます。クラウドディスクの課金の詳細については、「Elastic Block Storage ボリューム」をご参照ください。

アプリケーション

このトピックでは、次のアプリケーション例のソースコードと Dockerfile を使用します。アプリケーションがビルドされた後、クラスターにデプロイするためにイメージリポジトリにアップロードします。このアプリケーション例では、複数のレプリカが共同でリースを管理しますが、リースを保持するのは 1 つのレプリカのみです。レプリカが正常に動作しない場合、他のレプリカが自動的にリースを引き継ぎます。アプリケーションを作成する際には、次の点に注意してください。

  • この例では、テストに影響を与えるキャッシュを防ぐために、ブロックデバイスを読み書き用に開くために 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));

    // 自身をプリエンプトした場合に備えて再登録
    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));

    // シグナルハンドラの設定
    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) {
        // 新しいディスク、リースなし
        acquire_lease(disk_fd);
    } else {
        // 他の誰かがリースを保持している
        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) {
                // リモートが停止している
                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: 取得
    # 1: プリエンプト

    # rtype:
    # 1: 排他的書き込み

    nvme resv-register $DISK_DEVICE --iekey --nrkey=$MAGIC
    nvme resv-acquire $DISK_DEVICE --racqa=1 --rtype=1 --prkey=$MAGIC --crkey=$MAGIC
    # 自身をプリエンプトした場合に備えて再登録
    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
    # 新しいディスク、リースなし
    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"]

ステップ 1: アプリケーションをデプロイし、マルチアタッチ機能を設定する

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

data-disk という名前の PVC を作成し、accessModesReadWriteMany に、volumeModeBlock に設定します。

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 # 現在、cloud_essd、cloud_auto、および cloud_regional_disk_auto をサポートしています
      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: 主にファイルシステムタイプのボリュームをマウントするために使用されます。

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

    kubectl apply -f lease.yaml

ステップ 2: マルチアタッチと Reservation の効果を検証する

NVMe クラウドディスク上のデータ整合性を確保するために、アプリケーションで Reservation を介して読み書き権限を制御できます。1 つの 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. 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 は異常なノードからの書き込み I/O 操作をブロックできる

  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] リモートの lease-test-0 がリースをリフレッシュしました
    [pod/lease-test-1/lease] リモートは停止しており、プリエンプトしています
    [pod/lease-test-1/lease] キー 4745d0c5cd9a2fa4 として登録しました
    [pod/lease-test-1/lease] リースをリフレッシュしました
    [pod/lease-test-1/lease] リースをリフレッシュしました
    [pod/lease-test-1/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 がクラウドディスクに書き込むことができなくなり、リースコンテナが自動的に再起動することを示しています。これは、書き込み I/O 操作が Reservation によって正常にブロックされたことを示しています。

関連ドキュメント

NVMe クラウドディスクの容量が不足しているか、いっぱいになっている場合は、「クラウドディスクボリュームの拡張」をご参照ください。