Todos los productos
Search
Centro de documentos

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

Última actualización: Sep 04, 2026

Utilice multi-attach para compartir un único disco en la nube ESSD, ESSD AutoPL u otro compatible con NVMe entre un máximo de 16 instancias ECS dentro de la misma zona de forma simultánea. También puede compartir un disco ESSD con almacenamiento redundante por zona entre nodos de la misma región. Al combinar esta función con NVMe Persistent Reservation (PR), multi-attach proporciona a sus cargas de trabajo acceso a almacenamiento compartido con un control preciso de los permisos de escritura, lo que permite compartir datos de manera eficiente y realizar conmutaciones por error rápidas en un clúster ACK.

Casos de uso

  • Compartición de datos: Cuando un nodo escribe datos en un disco NVMe compartido, todos los demás nodos conectados pueden leerlos de inmediato. Varias instancias que ejecutan el mismo sistema operativo pueden cargar una sola imagen de contenedor almacenada en un disco NVMe, lo que reduce los costos de almacenamiento y mejora el rendimiento de lectura y escritura.

    image

  • Conmutación por error de alta disponibilidad: Las bases de datos en clúster tradicionales, como Oracle Real Application Clusters (RAC), SAP High-performance ANalytic Appliance (HANA) y las bases de datos nativas de la nube de alta disponibilidad (HA), son vulnerables a puntos únicos de fallo (SPOF). Un disco NVMe compartido mantiene el almacenamiento accesible incluso si falla un nodo de cómputo. Despliegue su carga de trabajo en modo principal/secundario: cuando la instancia principal falle, ejecute un comando NVMe PR para revocar sus permisos de escritura y luego promueva la instancia secundaria. Esto evita escrituras tipo split-brain y garantiza la consistencia de los datos. La secuencia de conmutación por error es la siguiente:

    1. La instancia de base de datos principal (Database Instance 1) falla y deja de atender tráfico.

    2. Ejecute un comando NVMe PR para bloquear las escrituras en Database Instance 1 y otorgar acceso de escritura a Database Instance 2.

    3. Restaure Database Instance 2 al mismo estado que tenía Database Instance 1 (por ejemplo, mediante la reproducción de registros).

    4. Database Instance 2 asume el rol de instancia principal.

    PR forma parte de la especificación NVMe. Controla los permisos de lectura y escritura a nivel de disco para asegurar que los nodos de cómputo escriban los datos según lo esperado. Para más detalles, consulte NVM Express Base Specification .

    image

  • Aceleración de caché de datos distribuidos: Los lagos de datos construidos sobre Object Storage Service (OSS) ofrecen un alto rendimiento de escritura por anexión, pero presentan alta latencia y bajo rendimiento en lecturas y escrituras aleatorias. Conecte un disco en la nube de alta velocidad con multi-attach habilitado como capa de caché compartida entre los nodos de cómputo para mejorar significativamente el rendimiento de acceso.

    image

  • Aprendizaje automático: Una vez etiquetados y escritos los datos de muestra, distribúyalos entre los nodos para entrenamiento paralelo sin necesidad de copiar datos a través de la red. Cada nodo de cómputo lee directamente del disco compartido, lo que reduce la latencia de transferencia y acelera el entrenamiento de modelos a gran escala.

    image

Facturación

La función multi-attach no genera cargos adicionales. Los recursos compatibles con el protocolo NVMe se facturan según sus métodos de facturación originales. Para consultar los precios de los discos en la nube, véase Elastic Block Storage volumes.

Límites

  • Un solo disco en la nube NVMe se puede conectar a un máximo de 16 instancias ECS en la misma zona simultáneamente.

  • Para leer y escribir en un disco en la nube desde varios nodos de forma concurrente, monte el disco mediante volumeDevices. Esto monta el disco como dispositivo de bloque y no admite acceso al sistema de archivos. Utilice volumeMode: Block y accessModes: ReadWriteMany en su PersistentVolumeClaim (PVC).

  • Para consultar la lista completa de límites, véase Limits of the multi-attach feature.

Requisitos previos

Antes de empezar, asegúrese de contar con lo siguiente:

  • Un clúster administrado ACK que ejecute Kubernetes 1.20 o posterior. Para crear uno, consulte Create an ACK managed cluster.

  • csi-plugin y csi-provisioner en la versión v1.24.10-7ae4421-aliyun o posterior. Para actualizarlos, consulte Manage the csi-plugin and csi-provisioner components.

  • Como mínimo dos nodos en la misma zona que admitan la función multi-attach. Para consultar las familias de instancias compatibles, véase Limits of the multi-attach feature.

  • Una aplicación contenerizada que cumpla con los dos requisitos siguientes:

    • Admite acceso concurrente al mismo disco en la nube desde varias réplicas.

    • Garantiza la consistencia de los datos mediante NVMe Reservation o un mecanismo equivalente.

Para obtener información de contexto, consulte:

Ejemplo de aplicación

El siguiente ejemplo de aplicación demuestra una elección de líder basada en concesiones (lease) sobre un dispositivo de bloque NVMe compartido. Varias réplicas compiten por una concesión escrita directamente en el disco. Solo una réplica posee la concesión en un momento dado; si deja de renovarla, otra réplica la reemplaza mediante comandos NVMe Reservation.

Notas clave de diseño:

  • Se utiliza O_DIRECT para abrir el dispositivo de bloque, omitiendo la caché de páginas y asegurando que las lecturas reflejen exactamente lo escrito en el disco.

  • El ejemplo utiliza la interfaz simplificada de Reservation del kernel de Linux (ioctls de <linux/pr.h>). Alternativas que requieren privilegios elevados:

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

    • CLI: nvme-cli

  • Para consultar la especificación completa de NVMe Reservation, véase NVMe Specification.

Expand to view the source code of the application example

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

El YAML en los pasos siguientes aplica a la versión en C. Para la versión en Bash, agregue lo siguiente al securityContext de su contenedor:

securityContext:
  capabilities:
    add: ["SYS_ADMIN"]

Expand to view the Dockerfile

Dockerfile para la versión en 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 para la versión en 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"]

Paso 1: Desplegar la aplicación y configurar multi-attach

Cree una StorageClass que habilite multi-attach, una PVC configurada como dispositivo de bloque y un StatefulSet que utilice la imagen de la aplicación de concesión.

  1. Cree un archivo llamado lease.yaml con el siguiente contenido. Reemplace la dirección de la imagen del contenedor por la dirección real de su imagen.

    Importante
    • NVMe Reservation surte efecto a nivel de nodo. Si varios pods se ejecutan en el mismo nodo, pueden interferir entre sí. Este ejemplo utiliza podAntiAffinity para evitarlo.

    • Si su clúster tiene nodos que no utilizan el protocolo NVMe, configure la afinidad de nodos para restringir la programación a nodos compatibles con NVMe.

    Expand to view the lease.yaml file

    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

    La siguiente tabla resume las diferencias clave entre las configuraciones de multi-attach y el montaje estándar:

    Recurso

    Campo

    Multi-attach

    Montaje estándar

    StorageClass

    parameters.multiAttach

    "true"

    No obligatorio

    PVC

    accessModes

    ReadWriteMany

    ReadWriteOnce

    PVC

    volumeMode

    Block

    Filesystem

    Montaje de volumen

    Método

    volumeDevices — acceso directo a dispositivo de bloque

    volumeMounts — montaje de sistema de archivos

  2. Despliegue la aplicación:

    kubectl apply -f lease.yaml

Paso 2: Comprobar multi-attach y Reservation

Comprobar que varios nodos pueden leer y escribir en el mismo disco

Ejecute el siguiente comando para consultar los registros del pod:

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

Salida esperada:

[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 lee inmediatamente los datos escritos por lease-test-0, lo que confirma que multi-attach funciona correctamente.

Comprobar que NVMe Reservation está activo

  1. Obtenga el ID del disco en la nube:

    kubectl get pvc data-disk -ojsonpath='{.spec.volumeName}'
  2. Inicie sesión en cualquiera de los dos nodos y ejecute el siguiente comando. Reemplace 2zxxxxxxxxxxx con la parte posterior a d- en el ID del disco obtenido en el paso anterior.

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

    Salida esperada:

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

    rtype: 1 (escritura exclusiva) y regctl: 1 confirman que NVMe Reservation está activo.

Comprobar que Reservation bloquea las escrituras desde un nodo fallido

  1. Inicie sesión en el nodo que ejecuta lease-test-0 y pause el proceso para simular un fallo:

    pkill -STOP -f /usr/local/bin/lease
  2. Espere 30 segundos y luego revise los registros:

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

    Salida esperada:

    [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 ha reemplazado la concesión y asumido el rol de principal.

  3. Reanude el proceso pausado en el nodo lease-test-0:

    pkill -CONT -f /usr/local/bin/lease
  4. Revise nuevamente los registros:

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

    Salida esperada:

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

    lease-test-0 ya no puede escribir en el disco. El error Invalid exchange confirma que Reservation ha bloqueado correctamente la E/S de escritura del nodo anteriormente principal, y el contenedor de concesión se reinicia automáticamente.

Pasos siguientes

Si su disco en la nube NVMe se queda sin espacio, consulte Expand a cloud disk volume.