Tous les produits
Search
Centre de documentation

Container Service for Kubernetes:Using NVMe cloud disk multi-attach and Reservation for data sharing between applications

Dernière mise à jour :Aug 11, 2026

Le multi-attach permet de partager un seul disque ESSD, ESSD AutoPL ou tout autre disque cloud compatible NVMe entre jusqu'à 16 instances ECS situées dans la même zone, ou de partager un disque ESSD à stockage redondant interzone entre les nœuds d'une même région. Associé à la réservation persistante NVMe (PR), le multi-attach offre à vos charges de travail un accès partagé au stockage avec un contrôle précis des autorisations d'écriture, permettant ainsi un partage efficace des données et un basculement rapide au sein d'un cluster ACK.

Cas d'utilisation

  • Partage de données : une fois qu'un nœud a écrit des données sur un disque NVMe partagé, tous les autres nœuds attachés peuvent les lire immédiatement. Une seule image de conteneur stockée sur un disque NVMe peut être chargée par plusieurs instances exécutant le même système d'exploitation, ce qui réduit les coûts de stockage et améliore les performances en lecture/écriture.

    image

  • Basculement haute disponibilité : les bases de données en cluster traditionnelles — notamment Oracle Real Application Clusters (RAC), SAP High-performance ANalytic Appliance (HANA) et les bases de données haute disponibilité (HA) natives du cloud — sont vulnérables aux points de défaillance uniques (SPOF). Un disque NVMe partagé garantit l'accessibilité du stockage en cas de panne d'un nœud de calcul. Déployez votre charge de travail en mode primaire/secondaire : lorsqu'une instance principale tombe en panne, exécutez une commande NVMe PR pour révoquer ses autorisations d'écriture, puis promouvez l'instance secondaire. Cela évite les écritures en mode split-brain et assure la cohérence des données. Séquence de basculement :

    1. L'instance de base de données principale (Instance de base de données 1) tombe en panne et cesse de traiter le trafic.

    2. Exécutez une commande NVMe PR pour bloquer les écritures vers l'Instance de base de données 1 et accorder l'accès en écriture à l'Instance de base de données 2.

    3. Restaurez l'Instance de base de données 2 dans le même état que l'Instance de base de données 1 (par exemple, en rejouant les journaux).

    4. L'Instance de base de données 2 prend le relais en tant qu'instance principale.

    La réservation persistante (PR) fait partie de la spécification NVMe. Elle contrôle les autorisations de lecture et d'écriture au niveau du disque pour garantir que les nœuds de calcul écrivent les données comme prévu. Pour plus de détails, consultez la Spécification de base NVM Express .

    image

  • Accélération du cache de données distribué : les data lakes construits sur Object Storage Service (OSS) offrent un débit élevé en écriture séquentielle, mais souffrent d'une latence élevée et de faibles performances en lecture/écriture aléatoire. Attachez un disque cloud rapide, activé pour le multi-attach, en tant que couche de cache partagée entre les nœuds de calcul pour améliorer considérablement les performances d'accès.

    image

  • Apprentissage automatique : après l'étiquetage et l'écriture des données d'échantillon, distribuez-les sur les nœuds pour un entraînement parallèle sans copier les données sur le réseau. Chaque nœud de calcul lit directement depuis le disque partagé, ce qui réduit la latence de transfert et accélère l'entraînement des modèles à grande échelle.

    image

Facturation

La fonctionnalité multi-attach n'entraîne pas de frais supplémentaires. Les ressources compatibles avec le protocole NVMe sont facturées selon leurs méthodes de facturation d'origine. Pour connaître les tarifs des disques cloud, consultez la rubrique Volumes Elastic Block Storage.

Limites

  • Un seul disque cloud NVMe peut être attaché à un maximum de 16 instances ECS dans la même zone simultanément.

  • Pour lire et écrire sur un disque cloud depuis plusieurs nœuds simultanément, montez le disque cloud à l'aide de volumeDevices. Cette méthode monte le disque en tant que périphérique bloc et ne prend pas en charge l'accès via un système de fichiers. Utilisez volumeMode: Block et accessModes: ReadWriteMany dans votre PersistentVolumeClaim (PVC).

  • Pour la liste complète des limites, consultez la rubrique Limites de la fonctionnalité multi-attach.

Prérequis

Avant de commencer, assurez-vous que vous disposez des éléments suivants :

  • Un cluster géré ACK exécutant Kubernetes 1.20 ou version ultérieure. Pour en créer un, consultez la rubrique Créer un cluster géré ACK.

  • Les composants csi-plugin et csi-provisioner en version v1.24.10-7ae4421-aliyun ou ultérieure. Pour effectuer la mise à niveau, consultez la rubrique Gérer les composants csi-plugin et csi-provisioner.

  • Au moins deux nœuds dans la même zone prenant en charge la fonctionnalité multi-attach. Pour connaître les familles d'instances prises en charge, consultez la rubrique Limites de la fonctionnalité multi-attach.

  • Une application conteneurisée répondant aux deux exigences suivantes :

    • Prise en charge de l'accès simultané au même disque cloud depuis plusieurs réplicas.

    • Garantie de la cohérence des données à l'aide de la réservation NVMe ou d'un mécanisme équivalent.

Pour en savoir plus sur le contexte, consultez :

Exemple d'application

L'exemple d'application suivant illustre l'élection d'un leader basée sur un bail (lease) via un périphérique bloc NVMe partagé. Plusieurs réplicas entrent en concurrence pour obtenir un bail écrit directement sur le disque. Un seul réplica détient le bail à la fois ; s'il cesse de le renouveler, un autre réplica le préempte à l'aide de commandes de réservation NVMe.

Notes clés sur la conception :

  • O_DIRECT est utilisé pour ouvrir le périphérique bloc, contournant ainsi le cache de pages et garantissant que les lectures reflètent ce qui a été effectivement écrit sur le disque.

  • L'exemple utilise l'interface de réservation simplifiée du noyau Linux (appels système ioctl <linux/pr.h>). Alternatives nécessitant des privilèges élevés :

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

    • CLI : nvme-cli

  • Pour consulter la spécification complète de la réservation NVMe, rendez-vous sur la page Spécification NVMe.

Développer pour afficher le code source de l'exemple d'application

C

#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);
}

Bash

#!/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

Le fichier YAML présenté dans les étapes ci-dessous s'applique à la version C. Pour la version Bash, ajoutez les éléments suivants au champ securityContext de votre conteneur :

securityContext:
  capabilities:
    add: ["SYS_ADMIN"]

Développer pour afficher le Dockerfile

Dockerfile pour la version 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"]

Dockerfile pour la version Bash :

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

Étape 1 : Déployer l'application et configurer le multi-attach

Créez une StorageClass activant le multi-attach, un PVC configuré en tant que périphérique bloc et un StatefulSet utilisant l'image de l'application de gestion des baux.

  1. Créez un fichier nommé lease.yaml avec le contenu suivant. Remplacez l'adresse de l'image du conteneur par votre adresse réelle.

    Important
    • La réservation NVMe s'applique au niveau du nœud. Si plusieurs pods s'exécutent sur le même nœud, ils peuvent interférer entre eux. Cet exemple utilise podAntiAffinity pour éviter ce problème.

    • Si votre cluster comporte des nœuds n'utilisant pas le protocole NVMe, configurez l'affinité des nœuds pour restreindre la planification aux nœuds compatibles NVMe.

    Développer pour afficher le fichier lease.yaml

    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: alicloud-disk-shared
    parameters:
      type: cloud_essd # Currently supports cloud_essd, cloud_auto, and 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>   # Replace with the image address of your application.
            volumeDevices:
            - name: data-disk
              devicePath: /dev/data-disk
          volumes:
          - name: data-disk
            persistentVolumeClaim:
              claimName: data-disk

    Le tableau suivant résume les principales différences entre les configurations multi-attach et les montages standard :

    Ressource Champ Multi-attach Montage standard
    StorageClass parameters.multiAttach "true" Non requis
    PVC accessModes ReadWriteMany ReadWriteOnce
    PVC volumeMode Block Filesystem
    Montage du volume Méthode volumeDevices — accès direct au périphérique bloc volumeMounts — montage du système de fichiers
  2. Déployez l'application :

    kubectl apply -f lease.yaml

Étape 2 : Vérifier le multi-attach et la réservation

Vérifier que plusieurs nœuds peuvent lire et écrire sur le même disque

Exécutez la commande suivante pour afficher les journaux des pods :

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

Résultat attendu :

[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

lease-test-1 lit immédiatement les données écrites par lease-test-0, ce qui confirme que le multi-attach fonctionne correctement.

Vérifier que la réservation NVMe est active

  1. Obtenez l'ID du disque cloud :

    kubectl get pvc data-disk -ojsonpath='{.spec.volumeName}'
  2. Connectez-vous à l'un des deux nœuds et exécutez la commande suivante. Remplacez 2zxxxxxxxxxxx par la partie située après d- dans l'ID du disque obtenu à l'étape précédente.

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

    Résultat attendu :

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

    rtype: 1 (écriture exclusive) et regctl: 1 confirment que la réservation NVMe est active.

Vérifier que la réservation bloque les écritures depuis un nœud défaillant

  1. Connectez-vous au nœud exécutant lease-test-0 et suspendez le processus pour simuler une panne :

    pkill -STOP -f /usr/local/bin/lease
  2. Attendez 30 secondes, puis vérifiez les journaux :

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

    Résultat attendu :

    [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

    lease-test-1 a préempté le bail et a pris le relais en tant que nœud principal.

  3. Reprenez le processus suspendu sur le nœud lease-test-0 :

    pkill -CONT -f /usr/local/bin/lease
  4. Vérifiez à nouveau les journaux :

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

    Résultat attendu :

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

    lease-test-0 ne peut plus écrire sur le disque. L'erreur Invalid exchange confirme que la réservation a bien bloqué les opérations d'écriture I/O provenant de l'ancien nœud principal, et le conteneur de gestion du bail redémarre automatiquement.

Étapes suivantes

Si votre disque cloud NVMe manque d'espace, consultez la rubrique Étendre un volume de disque cloud.