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
mtuda rede está definido como8896.
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-configsdo 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).
- Com instâncias padrão: se cada nó tiver uma saída publicada de
2.000 MBps, provisione pelo menos 60 nós (
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.
Crie um arquivo chamado
Dockerfilelocalmente: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"]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.
Gere as chaves RSA:
ssh-keygen -t rsa -b 4096 -C "mpi-user" -N '' -f "./id_rsa"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.
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 describecomando.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: CAPACITYGiAplique 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.
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-secretAplique 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.
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/hostfileAbra uma sessão do bash no pod principal:
kubectl exec -it mpi-worker-0 -- /bin/bashNo pod, crie um diretório de teste:
mkdir -p /lustre/testDefina 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
8e16normalmente gera a melhor capacidade de processamento de rede.
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=1instrui 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.
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
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
mpirunsã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 quempirunusará 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 quempirunseja 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
iorsã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 umfsyncapó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,4mpara capacidade de processamento,4kpara IOPS).-b: define o tamanho do bloco de destino por processo (por exemplo,50tpara capacidade de processamento,8gpara 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,60ou45). 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_NODEestiver 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.odirectfor 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:
Exclua os arquivos de teste do volume do Managed Lustre:
kubectl exec mpi-worker-0 -- rm -rf /lustre/testExclua o cluster do GKE:
gcloud container clusters delete CLUSTER_NAME --zone=ZONEA exclusão do cluster também exclui os pods do GKE, o secret do Kubernetes e a reivindicação de volume permanente.
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-tagsSe 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.