Todos os produtos
Search
Central de documentação

Container Service for Kubernetes:Uso de multi-attach de disco em nuvem NVMe e Reservation para compartilhamento de dados entre aplicações

Última atualização: Jun 27, 2026

Use o recurso multi-attach para compartilhar um único disco em nuvem ESSD, ESSD AutoPL ou outro compatível com NVMe entre até 16 instâncias ECS na mesma zona simultaneamente. Alternativamente, compartilhe um ESSD com armazenamento redundante por zona entre nós na mesma região. Combinado ao NVMe Persistent Reservation (PR), o multi-attach oferece às cargas de trabalho acesso a armazenamento compartilhado com controle preciso de permissões de escrita, permitindo compartilhamento eficiente de dados e failover rápido em um cluster ACK.

Casos de uso

  • Compartilhamento de dados: Após um nó gravar dados em um disco NVMe compartilhado, todos os outros nós anexados podem lê-los imediatamente. Várias instâncias que executam o mesmo sistema operacional podem carregar uma única imagem de contêiner armazenada em um disco NVMe, o que reduz custos de armazenamento e melhora o desempenho de leitura e escrita.

    image

  • Failover de alta disponibilidade: Bancos de dados clusterizados tradicionais — incluindo Oracle Real Application Clusters (RAC), SAP High-performance ANalytic Appliance (HANA) e bancos de dados nativos da nuvem com alta disponibilidade (HA) — são vulneráveis a pontos únicos de falha (SPOFs). Um disco NVMe compartilhado mantém o armazenamento acessível mesmo quando um nó de computação falha. Implante sua carga de trabalho no modo primário/secundário: quando a instância primária falhar, execute um comando NVMe PR para revogar as permissões de escrita dela e promova a instância secundária. Isso evita escritas do tipo split-brain e garante a consistência dos dados. A sequência de failover é:

    1. A instância primária do banco de dados (Instância de Banco de Dados 1) falha e para de atender ao tráfego.

    2. Execute um comando NVMe PR para bloquear escritas na Instância de Banco de Dados 1 e conceder acesso de escrita à Instância de Banco de Dados 2.

    3. Restaure a Instância de Banco de Dados 2 para o mesmo estado da Instância de Banco de Dados 1 (por exemplo, reexecutando logs).

    4. A Instância de Banco de Dados 2 assume como instância primária.

    O PR faz parte da especificação NVMe. Ele controla as permissões de leitura e escrita no nível do disco para garantir que os nós de computação gravem os dados conforme esperado. Para mais detalhes, consulte NVM Express Base Specification .

    image

  • Aceleração de cache de dados distribuído: Data lakes construídos sobre o Object Storage Service (OSS) oferecem alto throughput de escrita por adição, mas sofrem com alta latência e baixo desempenho de leitura/escrita aleatória. Anexe um disco em nuvem de alta velocidade com suporte a multi-attach como camada de cache compartilhada entre os nós de computação para melhorar significativamente o desempenho de acesso.

    image

  • Machine learning: Após rotular e gravar os dados de amostra, distribua-os entre os nós para treinamento paralelo sem copiar dados pela rede. Cada nó de computação lê diretamente do disco compartilhado, o que reduz a latência de transferência e acelera o treinamento de modelos em grande escala.

    image

Faturamento

O recurso multi-attach não gera taxas adicionais. Recursos compatíveis com o protocolo NVMe seguem seus métodos de faturamento originais. Para preços de discos em nuvem, consulte Volumes do Elastic Block Storage.

Limitações

  • Um único disco em nuvem NVMe pode ser anexado a no máximo 16 instâncias ECS na mesma zona simultaneamente.

  • Para ler e gravar em um disco em nuvem a partir de vários nós simultaneamente, monte o disco usando volumeDevices. Essa ação monta o disco como dispositivo de bloco e não oferece suporte a acesso ao sistema de arquivos. Use volumeMode: Block e accessModes: ReadWriteMany no PersistentVolumeClaim (PVC).

  • Para a lista completa de limites, consulte Limites do recurso multi-attach.

Pré-requisitos

Antes de começar, verifique se você tem:

  • Um cluster gerenciado ACK executando Kubernetes 1.20 ou posterior. Para criar um, consulte Criar um cluster gerenciado ACK.

  • csi-plugin e csi-provisioner na versão v1.24.10-7ae4421-aliyun ou posterior. Para atualizar, consulte Gerenciar os componentes csi-plugin e csi-provisioner.

  • Pelo menos dois nós na mesma zona compatíveis com o recurso multi-attach. Para famílias de instâncias compatíveis, consulte Limites do recurso multi-attach.

  • Uma aplicação conteinerizada que atenda aos dois requisitos seguintes:

    • Ofereça suporte a acesso simultâneo ao mesmo disco em nuvem por múltiplas réplicas.

    • Garanta a consistência dos dados usando NVMe Reservation ou mecanismo equivalente.

Para informações de contexto, consulte:

Exemplo de aplicação

O exemplo de aplicação a seguir demonstra uma eleição de líder baseada em concessão (lease) sobre um dispositivo de bloco NVMe compartilhado. Várias réplicas competem por uma concessão gravada diretamente no disco. Apenas uma réplica detém a concessão por vez. Se ela parar de renová-la, outra réplica a preemptará usando comandos NVMe Reservation.

Notas importantes de design:

  • O_DIRECT abre o dispositivo de bloco, ignora o cache de páginas e garante que as leituras reflitam o que foi realmente gravado no disco.

  • O exemplo utiliza a interface simplificada de Reservation do kernel Linux (ioctls de <linux/pr.h>). Alternativas que exigem privilégios elevados:

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

    • CLI: nvme-cli

  • Para a especificação completa do NVMe Reservation, consulte NVMe Specification.

Expandir para visualizar o código-fonte do exemplo de aplicação

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

O YAML nas etapas abaixo aplica-se à versão em C. Para a versão em bash, adicione o seguinte ao securityContext do seu contêiner:

securityContext:
  capabilities:
    add: ["SYS_ADMIN"]

Expandir para visualizar o Dockerfile

Dockerfile para a versão em 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 a versão em 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"]

Etapa 1: Implantar a aplicação e configure o multi-attach

crie uma StorageClass que habilite o multi-attach, um PVC configurado como dispositivo de bloco e um StatefulSet que utilize a imagem da aplicação de concessão (lease).

  1. crie um arquivo chamado lease.yaml com o conteúdo a seguir. Substitua o endereço da imagem do contêiner pelo endereço real da sua imagem.

    Importante
    • O NVMe Reservation entra em vigor no nível do nó. Se vários pods forem executados no mesmo nó, eles poderão interferir uns nos outros. Este exemplo usa podAntiAffinity para evitar isso.

    • Se o seu cluster tiver nós que não utilizam o protocolo NVMe, configure a afinidade de nó para restringir o agendamento apenas a nós compatíveis com NVMe.

    Expandir para visualize o arquivo 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

    A tabela a seguir resume as principais diferenças entre as configurações de multi-attach e montagem padrão:

    Recurso

    Campo

    Multi-attach

    Montagem padrão

    StorageClass

    parameters.multiAttach

    "true"

    Não necessário

    PVC

    accessModes

    ReadWriteMany

    ReadWriteOnce

    PVC

    volumeMode

    Block

    Filesystem

    Montagem de volume

    Método

    volumeDevices — acesso direto ao dispositivo de bloco

    volumeMounts — montagem de sistema de arquivos

  2. Implante a aplicação:

    kubectl apply -f lease.yaml

Etapa 2: verifique o multi-attach e o Reservation

verifique se vários nós podem ler e gravar no mesmo disco

execute o comando a seguir para visualize os logs dos pods:

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

Saída 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

O pod lease-test-1 lê imediatamente os dados gravados pelo lease-test-0, o que confirme o funcionamento do multi-attach.

verifique se o NVMe Reservation está ativo

  1. Obtenha o ID do disco em nuvem:

    kubectl get pvc data-disk -ojsonpath='{.spec.volumeName}'
  2. Faça login em qualquer um dos dois nós e execute o comando a seguir. Substitua 2zxxxxxxxxxxx pela parte após d- no ID do disco obtido na etapa anterior.

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

    Saída esperada:

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

    Os valores rtype: 1 (escrita exclusiva) e regctl: 1 confirme que o NVMe Reservation está ativo.

verifique se o Reservation bloqueia escritas de um nó com falha

  1. Faça login no nó que executa o lease-test-0 e pause o processo para simular uma falha:

    pkill -STOP -f /usr/local/bin/lease
  2. Aguarde 30 segundos e verifique os logs:

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

    Saída 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

    O pod lease-test-1 preemptou a concessão e assumiu como primário.

  3. Retome o processo pausado no nó lease-test-0:

    pkill -CONT -f /usr/local/bin/lease
  4. verifique os logs novamente:

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

    Saída esperada:

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

    O pod lease-test-0 não consegue mais gravar no disco. O erro Invalid exchange confirme que o Reservation bloqueou com sucesso a E/S de escrita do nó anteriormente primário, e o contêiner de concessão reinicia automaticamente.

Próximos passos

Se o seu disco em nuvem NVMe ficar sem espaço, consulte Expandir um volume de disco em nuvem.