Teste de performance no Google Kubernetes Engine

Para testar a performance de leitura e gravação de uma carga de trabalho do Google Kubernetes Engine (GKE) em vários clientes do GKE, use a ferramenta de comparação IOR. As instruções a seguir mostram como automatizar a configuração do cliente e usar o IOR com mpirun em SSH sem senha entre pods do Kubernetes para testar a E/S agregada.

Pré-requisitos

  • Uma instância do Managed Lustre já provisionada.

  • Um ambiente Docker local configurado e autenticado para enviar ao Google Artifact Registry ou ao Container Registry (consulte Métodos de autenticação).

  • Verifique se o valor mtu da rede está definido como 8896.

Criar um cluster do GKE

Para testar a performance, você precisa de um cluster do GKE com o driver CSI do Managed Lustre ativado. Para cargas de trabalho de armazenamento de alta performance, configure os pools de nós do GKE com famílias de máquinas otimizadas para computação (por exemplo, c2 ou c3) e rede TIER_1.

Execute o comando a seguir para criar um cluster do GKE Standard otimizado para testes de performance:

gcloud container clusters create CLUSTER_NAME \
    --zone=ZONE \
    --machine-type=MACHINE_TYPE \
    --addons=LustreCsiDriver \
    --network-performance-configs=total-egress-bandwidth-tier=TIER_1 \
    --network=NETWORK \
    --num-nodes=NUM_NODES
  • Substitua ZONE e NETWORK pelos valores de implantação específicos. O cluster precisa estar na mesma rede VPC que a instância do Managed Lustre.

  • Escolha um MACHINE_TYPE. Consulte Considerações sobre performance para informações sobre como escolher tipos de máquinas para obter a melhor capacidade de processamento.

  • Se o tipo de máquina não oferecer suporte à rede TIER_1, exclua a linha --network-performance-configs do comando.

  • Especifique o NUM_NODES. Para saturar o sistema de arquivos, a capacidade de rede agregada do cluster precisa exceder a capacidade de processamento provisionada do sistema de arquivos em aproximadamente 20%.

    Para máquinas com a rede de nível 1 ativada, um único nó pode enviar entre 25 Gbps e 200 Gbps (aproximadamente 3.000 a 25.000 MBps), dependendo da família de VMs e da contagem de CPUs. Para instâncias padrão, a saída é normalmente limitada a cerca de 2 Gbps por vCPU.

    Por exemplo, se a capacidade da instância do Managed Lustre gerar 100.000 MBps de capacidade de processamento teórica, você precisará de uma saída de cliente agregada de 120.000 MBps (100,000 * 1.2) para saturá-la:

    • Com instâncias padrão: se cada nó tiver uma saída publicada de 2.000 MBps, provisione pelo menos 60 nós (120,000 / 2,000).
    • Com a rede de nível 1: se cada nó tiver uma saída publicada de 10.000 MBps (aproximadamente 80 Gbps), provisione pelo menos 12 nós (120,000 / 10,000).

Criar a imagem do Docker do IOR

Crie uma imagem de contêiner com o OpenMPI e o IOR instalados. Compile o IOR com suporte a E/S assíncrona (AIO) para melhor performance.

  1. Crie um arquivo chamado Dockerfile localmente:

    FROM ubuntu:22.04
    
    # Prevent interactive prompts during installation
    ENV DEBIAN_FRONTEND=noninteractive
    
    # Install dependencies, SSH, and required Autotools packages
    RUN apt-get update && apt-get install -y \
      openssh-server \
      openmpi-bin \
      libopenmpi-dev \
      wget \
      git \
      make \
      gcc \
      g++ \
      automake \
      autoconf \
      libtool \
      pkg-config \
      libaio-dev \
      sudo \
      && rm -rf /var/lib/apt/lists/*
    
    # Build IOR from source (version 4.0.0) with Asynchronous I/O (AIO) support
    RUN git clone -b 4.0.0 https://github.com/hpc/ior /tmp/ior \
      && cd /tmp/ior \
      && ./bootstrap \
      && ./configure --disable-dependency-tracking --with-aio \
      && make -j"$(nproc)" \
      && make install \
      && rm -rf /tmp/ior
    
    # Configure SSH for OpenMPI passwordless communication
    RUN mkdir /var/run/sshd
    RUN echo 'root:root' | chpasswd
    RUN sed -i 's/^#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
    RUN sed -i 's/^#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
    
    # SSH login fix so user isn't kicked out after container initialization
    RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd
    
    EXPOSE 22
    CMD ["/usr/sbin/sshd", "-D"]
    
  2. Crie e envie essa imagem para o registro de contêiner de sua preferência. As instruções neste documento usam o Artifact Registry.

    export IMAGE_TAG="gcr.io/PROJECT_ID/lustre-ior-benchmark:latest"
    docker build -t $IMAGE_TAG .
    docker push $IMAGE_TAG
    

Gerar chaves SSH sem senha para MPI

O OpenMPI exige comunicação entre nós usando SSH sem senha. Crie uma chave SSH e armazene-a em um secret do Kubernetes.

  1. Gere as chaves RSA:

    ssh-keygen -t rsa -b 4096 -C "mpi-user" -N '' -f "./id_rsa"
    
  2. Crie o secret do Kubernetes:

    kubectl create secret generic mpi-ssh-secret \
      --from-file=id_rsa=./id_rsa \
      --from-file=id_rsa.pub=./id_rsa.pub \
      --from-file=authorized_keys=./id_rsa.pub
    

Criar volume e reivindicação persistentes

Conecte os pods do GKE à instância do Managed Lustre usando o provisionamento estático.

  1. Crie um arquivo chamado lustre-pv.yaml. Substitua:

    • CAPACITY pela capacidade de armazenamento da instância em GiB.
    • EXTENDED_LUSTRE_ID pelo identificador do Managed Lustre, no formato PROJECT_ID/ZONE/INSTANCE_NAME. Por exemplo, project-123/us-west1-a/my-lustre-instance.
    • LUSTRE_IP pelo endereço IP de ativação da instância.
    • FS_NAME pelo nome do sistema de arquivos da instância.

    Esses valores podem ser recuperados com o gcloud lustre instances describe comando.

    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: my-lustre-pv
    spec:
      storageClassName: ""
      claimRef:
        name: my-lustre-pvc
        namespace: default
      accessModes:
        - ReadWriteMany
      capacity:
        storage: CAPACITYGi   # retain `Gi` suffix
      persistentVolumeReclaimPolicy: Retain
      volumeMode: Filesystem
      csi:
        driver: lustre.csi.storage.gke.io
        volumeHandle: EXTENDED_LUSTRE_ID   # project-name/zone/instance-name
        volumeAttributes:
          ip: LUSTRE_IP
          filesystem: FS_NAME
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: my-lustre-pvc
    spec:
      storageClassName: ""
      volumeName: my-lustre-pv
      accessModes:
        - ReadWriteMany
      resources:
        requests:
          storage: CAPACITYGi
    
  2. Aplique o manifesto:

    kubectl apply -f lustre-pv.yaml
    

Implantar os workers do MPI

Para escalonar tarefas do IOR em vários nós, implante um StatefulSet com a imagem de comparação.

  1. Crie um arquivo chamado mpi-workers.yaml. Especifique o PROJECT_ID, e defina NUM_NODES como o número de nós no cluster.

    apiVersion: v1
    kind: Service
    metadata:
      name: mpi-workers
      labels:
        app: mpi-worker
    spec:
      clusterIP: None
      selector:
        app: mpi-worker
      ports:
      - port: 22
        name: ssh
    ---
    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: mpi-worker
    spec:
      serviceName: "mpi-workers"
      replicas: NUM_NODES
      selector:
        matchLabels:
          app: mpi-worker
      template:
        metadata:
          labels:
            app: mpi-worker
        spec:
          tolerations:
            - operator: "Exists"
          containers:
            - name: mpi-worker
              image: gcr.io/PROJECT_ID/lustre-ior-benchmark:latest
              command: ["/bin/sh", "-c"]
              args:
                - >-
                  mkdir -p /var/run/sshd &&
                  ssh-keygen -A &&
                  mkdir -p /root/.ssh &&
                  echo "Host *" > /root/.ssh/config &&
                  echo "    StrictHostKeyChecking no" >> /root/.ssh/config &&
                  echo "    UserKnownHostsFile=/dev/null" >> /root/.ssh/config &&
                  cp /mnt/mpi-ssh-keys/id_rsa /root/.ssh/id_rsa &&
                  cp /mnt/mpi-ssh-keys/id_rsa.pub /root/.ssh/id_rsa.pub &&
                  cp /mnt/mpi-ssh-keys/authorized_keys /root/.ssh/authorized_keys &&
                  chmod 700 /root/.ssh &&
                  chmod 600 /root/.ssh/* &&
                  exec /usr/sbin/sshd -D
              ports:
                - containerPort: 22
              volumeMounts:
                - name: lustre-mount
                  mountPath: /lustre
                - name: ssh-key-secret
                  mountPath: /mnt/mpi-ssh-keys
                  readOnly: true
          volumes:
            - name: lustre-mount
              persistentVolumeClaim:
                claimName: my-lustre-pvc
            - name: ssh-key-secret
              secret:
                secretName: mpi-ssh-secret
    
  2. Aplique o manifesto:

    kubectl apply -f mpi-workers.yaml
    

Executar a comparação IOR

Inicie a comparação no primeiro pod (mpi-worker-0), tratando-o como o nó principal.

  1. Gere um arquivo de host contendo os endereços IP internos dos workers e copie-o para o nó principal:

    kubectl get pods -l app=mpi-worker -o jsonpath='{range .items[*]}{.status.podIP}{"\n"}{end}' > hosts.txt
    kubectl cp hosts.txt mpi-worker-0:/root/hostfile
    
  2. Abra uma sessão do bash no pod principal:

    kubectl exec -it mpi-worker-0 -- /bin/bash
    
  3. No pod, crie um diretório de teste:

    mkdir -p /lustre/test
    
  4. Defina as variáveis de teste:

    export NUM_NODES="NUM_NODES"
    export PROCESSES_PER_NODE="PROCESSES_PER_NODE"
    export NUM_PROCESSES=$(( NUM_NODES * PROCESSES_PER_NODE ))
    

    Em que:

    • NUM_NODES: o número total de pods de worker que participam do teste.

    • PROCESSES_PER_NODE: o número de classificações de MPI a serem executadas em cada contêiner. Recomendamos começar definindo esse valor para corresponder ao número de núcleos físicos (ou metade do número de vCPUs) nas máquinas cliente. Para tipos de máquinas de alta performance, definir esse valor entre 8 e 16 normalmente gera a melhor capacidade de processamento de rede.

  5. Execute os comandos de comparação:

    Capacidade de processamento de gravação

    Esse comando grava continuamente por 60 segundos para testar a capacidade de processamento máxima de estado estável. O tamanho do arquivo por tarefa é definido como um limite máximo arbitrariamente grande de 50 TiB para manter as tarefas gravando até que o timer de 60 segundos expire.

    mpirun \
      --allow-run-as-root \
      --mca plm_rsh_no_tree_spawn 1 \
      --mca opal_set_max_sys_limits 1 \
      --mca plm_rsh_num_concurrent ${NUM_NODES} \
      --mca plm_rsh_args "-o StrictHostKeyChecking=no" \
      --npernode ${PROCESSES_PER_NODE} \
      --np ${NUM_PROCESSES} \
      --hostfile ~/hostfile \
      /usr/local/bin/ior \
      -a AIO \
      --posix.odirect \
      -F -g -v -w -k \
      -t 4m -b 50t \
      -D 60 \
      -O stoneWallingWearOut=1 \
      -O stoneWallingStatusFile=/lustre/test/ior-easy.stonewall \
      -o /lustre/test/ior_file

    Capacidade de leitura

    Essa fase lê exatamente a quantidade de dados que foi gravada com sucesso durante o teste de capacidade de gravação de 60 segundos. Embora o tamanho do arquivo por tarefa (-b) seja definido como 50 TiB para corresponder à geometria da fase de gravação, o flag -O stoneWallingWearOut=1 instrui o IOR a parar de ler assim que atingir o limite de dados exato registrado no arquivo de status de stonewall.

    mpirun \
      --allow-run-as-root \
      --mca plm_rsh_no_tree_spawn 1 \
      --mca opal_set_max_sys_limits 1 \
      --mca plm_rsh_num_concurrent ${NUM_NODES} \
      --mca plm_rsh_args "-o StrictHostKeyChecking=no" \
      --npernode ${PROCESSES_PER_NODE} \
      --np ${NUM_PROCESSES} \
      --hostfile ~/hostfile \
      /usr/local/bin/ior \
      -a AIO \
      --posix.odirect \
      -F -g -v -r -k \
      -t 4m -b 50t \
      -D 60 \
      -O stoneWallingWearOut=1 \
      -O stoneWallingStatusFile=/lustre/test/ior-easy.stonewall \
      -o /lustre/test/ior_file

    IOPS de gravação

    Esse teste usa tamanhos de transferência pequenos de 4 KiB e tamanhos de arquivo de 8 GiB por tarefa para medir o número máximo de operações de entrada/saída por segundo (IOPS) que o sistema de arquivos pode processar.

    mpirun \
      --allow-run-as-root \
      --mca plm_rsh_no_tree_spawn 1 \
      --mca opal_set_max_sys_limits 1 \
      --mca plm_rsh_num_concurrent ${NUM_NODES} \
      --mca plm_rsh_args "-o StrictHostKeyChecking=no" \
      --np ${NUM_PROCESSES} \
      --oversubscribe \
      --map-by node \
      --bind-to socket \
      --hostfile ~/hostfile \
      /usr/local/bin/ior \
      -e \
      -t 4k \
      -b 8g \
      -s 1 \
      -a AIO \
      --posix.odirect \
      --aio.max-pending=256 \
      -w \
      -F \
      -z \
      -Q 1 \
      -G 1745405099 \
      -D 45 \
      -O stoneWallingWearOut=1 \
      -o /lustre/test/ior-random

    IOPS de leitura

    Para evitar a leitura de um arquivo esparso, esse teste usa dois comandos: uma gravação para criar um arquivo sólido usando tamanhos de transferência de 4 MiB e tamanhos de arquivo de 8 GiB por tarefa, seguida pelo teste real de IOPS de leitura aleatória de 4 KiB.

    1. Crie o arquivo a ser lido:

      mpirun \
        --allow-run-as-root \
        --mca plm_rsh_no_tree_spawn 1 \
        --mca opal_set_max_sys_limits 1 \
        --mca plm_rsh_num_concurrent ${NUM_NODES} \
        --mca plm_rsh_args "-o StrictHostKeyChecking=no" \
        --npernode ${PROCESSES_PER_NODE} \
        --np ${NUM_PROCESSES} \
        --oversubscribe \
        --map-by node \
        --bind-to socket \
        --hostfile ~/hostfile \
        /usr/local/bin/ior \
        -a AIO \
        --posix.odirect \
        -w \
        -F \
        -k \
        -t 4m \
        -b 8g \
        -s 1 \
        -Q 1 \
        -G 1745405099 \
        -o /lustre/test/ior_rand_read
    2. Execute o teste de leitura do IOR:

      mpirun \
        --allow-run-as-root \
        --mca plm_rsh_no_tree_spawn 1 \
        --mca opal_set_max_sys_limits 1 \
        --mca plm_rsh_num_concurrent ${NUM_NODES} \
        --mca plm_rsh_args "-o StrictHostKeyChecking=no" \
        --oversubscribe \
        --map-by node \
        --bind-to socket \
        --npernode ${PROCESSES_PER_NODE} \
        --np ${NUM_PROCESSES} \
        --hostfile ~/hostfile \
        /usr/local/bin/ior \
        -a AIO \
        --posix.odirect \
        --aio.max-pending 256 \
        -r \
        -F \
        -z \
        -t 4k \
        -b 8g \
        -s 1 \
        -Q 1 \
        -G 1745405099 \
        -D 45 \
        -O stoneWallingWearOut=1 \
        -o /lustre/test/ior_rand_read

    Os flags mpirun são:

    • --mca plm_rsh_no_tree_spawn 1: desativa a geração de daemons baseada em árvore para melhorar a confiabilidade de inicialização em nós.
    • --mca opal_set_max_sys_limits 1: tenta definir automaticamente os limites do sistema (como o número máximo de arquivos abertos) para os valores permitidos mais altos.
    • --mca plm_rsh_num_concurrent: define o número máximo de conexões SSH simultâneas que mpirun usará ao iniciar daemons de worker.
    • --mca plm_rsh_args ...: ignora a verificação estrita da chave do host para evitar que os prompts SSH interativos bloqueiem a inicialização do processo MPI.
    • --prefix ...: define explicitamente o caminho de instalação do OpenMPI para Rocky Linux e RHEL para que os nós de worker possam encontrar o daemon necessário (orted).
    • --allow-run-as-root: permite que mpirun seja executado como o usuário raiz.
    • --oversubscribe: permite que o MPI programe mais processos em um nó do que há núcleos físicos disponíveis.
    • --map-by node: distribui os processos MPI de maneira uniforme entre os nós disponíveis.
    • --bind-to socket: vincula os processos MPI a soquetes de CPU físicos para otimizar o acesso à memória e a performance do cache.
    • --npernode: o número de processos por nó.
    • --np: o número total de processos MPI a serem iniciados.
    • --hostfile: especifica o arquivo que contém a lista de hosts em que será executado.

    Os flags ior são:

    • -a AIO --posix.odirect: usa o mecanismo de E/S assíncrona (AIO) combinado com a E/S direta POSIX. Isso ignora o cache de página da RAM do lado do cliente e força gravações simultâneas não bloqueadoras diretamente nos servidores de armazenamento, garantindo que a comparação meça a performance real do armazenamento de rede em vez de buffers de memória.
    • --aio.max-pending=256: determina o número máximo de operações de E/S assíncronas simultâneas em trânsito por processo.
    • -C: reordena as tarefas para uma performance de leitura ideal.
    • -F: modo de arquivo por processo.
    • -g: usa barreiras para separar as fases de gravação e leitura do teste.
    • -v: gera registros detalhados.
    • -w / -r: instrui o IOR a executar o teste de performance de gravação (-w) ou leitura (-r) .
    • -k: impede que o IOR exclua o arquivo de teste após a gravação, disponibilizando-o para o teste de leitura.
    • -e: executa um fsync após a fase de gravação para garantir que os dados sejam confirmados nas unidades de armazenamento.
    • -z: instrui o IOR a executar E/S de acesso aleatório em vez de acesso sequencial.
    • -s 1: define o número de segmentos como 1.
    • -Q 1: define o deslocamento de tarefa por nó, alinhando as tarefas para que a comparação seja coordenada corretamente em todos os nós.
    • -G 1745405099: codifica o carimbo de data/hora da semente aleatória para que a fase de leitura gere exatamente os mesmos deslocamentos de arquivo aleatórios usados pela fase de gravação.
    • -t: define o tamanho da transferência para cada operação de E/S (por exemplo, 4m para capacidade de processamento, 4k para IOPS).
    • -b: define o tamanho do bloco de destino por processo (por exemplo, 50t para capacidade de processamento, 8g para IOPS) para garantir que o teste não fique sem dados de payload durante a execução cronometrada.
    • -D: restringe a duração do tempo de execução do teste a um número específico de segundos (por exemplo, 60 ou 45). Esse "stonewalling" encerra a comparação de maneira uniforme para capturar uma medição de estado estável real.
    • -O stoneWallingWearOut=1: força todas as linhas de execução simultâneas a continuar gerando carga de gravação contínua durante todo o período, impedindo que as linhas de execução mais rápidas sejam concluídas mais cedo e diminuam a pressão geral da rede.
    • -O stoneWallingStatusFile=<path>: grava um arquivo de verificação de estado no final da fase de gravação. A fase de leitura subsequente usa esse arquivo para ler apenas os blocos que foram confirmados com sucesso, evitando erros de ponteiro nulo durante as leituras.
    • -o: o caminho para o arquivo de teste no sistema de arquivos do Managed Lustre

Ver os resultados

Quando a comparação é concluída, ela mostra as métricas de performance agregadas de todas as VMs ou pods do cliente diretamente no terminal. Procure a tabela Results na parte de baixo da saída para encontrar a capacidade de processamento ou IOPS máxima.

Principais métricas

  • aggregate filesize: a quantidade total de dados gravados ou lidos durante o teste em todos os clientes participantes.

  • bw(MiB/s) / Max Write / Max Read: a métrica mais importante para testes sequenciais. Mostra a largura de banda agregada alcançada pelo sistema de arquivos do Managed Lustre.

  • IOPS: a métrica mais importante para testes de E/S aleatória. Mostra o número máximo de operações de entrada/saída por segundo.

Exportar resultados para um arquivo

Se você quiser analisar os resultados de maneira programática, inseri-los em um banco de dados ou salvá-los para análise posterior, poderá instruir o IOR a exportar os dados de resumo nos formatos JSON e CSV em vez de apenas imprimi-los na tela.

Para fazer isso, anexe os flags -O ao final da string de comando ior:

-O summaryFormat=JSON \
-O summaryFile=/lustre/test/perf-results/summary.json \
-O saveRankPerformanceDetailsCSV=/lustre/test/perf-results/details.csv

O diretório de saída precisa existir no sistema de arquivos antes de executar a comparação.

Performance esperada x real

É possível calcular a capacidade de processamento máxima matemática do sistema de arquivos com base na capacidade provisionada e no nível de performance. Como a capacidade de armazenamento é provisionada em gibibytes (GiB) e os níveis são classificados em tebibytes (TiB), primeiro é necessário converter a capacidade:

(Capacity GiB / 1024) * Tier MBps = MBps máximos teóricos

Por exemplo, uma instância de 216.000 GiB no nível de 500 MBps por TiB fornece matematicamente 105.469 MBps de capacidade de processamento ((216000 / 1024) * 500).

A capacidade de processamento máxima observada sempre será limitada pela capacidade de processamento provisionada do sistema de arquivos ou pelos limites de saída de rede combinados das máquinas cliente, o que for menor.

Alguns motivos comuns para que os números de comparação não atinjam as velocidades teóricas incluem:

  • Sobrecarga de TCP/IP:a encapsulamento de rede padrão e os cabeçalhos de pacotes consomem aproximadamente 5 a 10% da largura de banda bruta. O máximo matemático inclui essa sobrecarga, mas a comparação IOR mede apenas o payload bruto gravado no disco.

  • Limites de rede do cliente:as máquinas cliente têm limites de largura de banda de saída estritos. Se você usar um pequeno número de clientes ou nós ou tipos de máquinas sem a rede de nível 1 ativada, os clientes vão limitar a comparação antes que o sistema de arquivos do Managed Lustre atinja o limite.

  • Troca de contexto MPI:se PROCESSES_PER_NODE estiver definido como um valor maior que o número de núcleos físicos nas máquinas cliente, a contenção de CPU e a sobrecarga de troca de contexto vão degradar artificialmente a performance de E/S da comparação.

  • E/S direta ausente:se o flag --posix.odirect for omitido, os dados vão passar pelo cache de página da RAM do cliente. Isso introduz gargalos de memória e sobrecarga de CPU que mascaram a performance real do armazenamento de rede.

Limpar

Para evitar cobranças na sua Google Cloud conta pelos recursos usados nesta página, siga estas etapas:

  1. Exclua os arquivos de teste do volume do Managed Lustre:

    kubectl exec mpi-worker-0 -- rm -rf /lustre/test
    
  2. Exclua o cluster do GKE:

    gcloud container clusters delete CLUSTER_NAME --zone=ZONE
    

    A exclusão do cluster também exclui os pods do GKE, o secret do Kubernetes e a reivindicação de volume permanente.

  3. Se você enviou a imagem Docker de comparação e não precisa mais dela, exclua a imagem do repositório:

    gcloud container images delete gcr.io/PROJECT_ID/lustre-ior-benchmark:latest --force-delete-tags
    
  4. Se você criou a instância do Managed Lustre especificamente para esse teste e não precisa mais dela, exclua-a:

    gcloud lustre instances delete INSTANCE_ID --location=LOCATION
    

Solução de problemas de gargalos comuns de comparação

Se os resultados da comparação forem significativamente menores que o nível de performance de armazenamento esperado, consulte Solução de problemas de gargalos comuns.