Implantar um sistema em lote usando o Kueue

Neste tutorial, mostramos como otimizar os recursos disponíveis programando jobs no Google Kubernetes Engine (GKE) com o Kueue. Neste tutorial, você vai aprender a usar o Kueue para gerenciar e programar jobs em lote de maneira eficaz, melhorar a utilização de recursos e simplificar o gerenciamento de cargas de trabalho. Você configura um cluster compartilhado para duas equipes de locatários, em que cada uma tem o próprio namespace e cria jobs que compartilham recursos globais. Você também configura o Kueue para programar os jobs com base nas cotas de recursos definidas.

Este tutorial é destinado a arquitetos de nuvem e engenheiros de plataforma interessados em implementar um sistema em lote usando o GKE. Para saber mais sobre papéis comuns e exemplos de tarefas referenciados no conteúdo do Google Cloud, consulte Funções e tarefas comuns do usuário do GKE.

Antes de ler esta página, confira se você conhece os seguintes conceitos:

Contexto

Os jobs são aplicativos executados até a conclusão, como machine learning, renderização, simulação, análise, CI/CD e cargas de trabalho semelhantes.

O Kueue é um programador de jobs nativos da nuvem que funciona com o programador padrão do Kubernetes, o controlador de jobs e o escalonador automático de clusters para fornecer um sistema de lotes de ponta a ponta. O Kueue implementa o enfileiramento de jobs para decidir quando os jobs devem esperar e quando devem começar, com base em cotas e em uma hierarquia para compartilhar recursos entre as equipes.

O Kueue tem as seguintes características:

  • Ele é otimizado para arquiteturas de nuvem, em que os recursos são heterogêneos, intercambiáveis e escalonáveis.
  • Ele oferece um conjunto de APIs para gerenciar cotas e gerenciar jobs na fila.
  • Ele não reimplementa as funcionalidades atuais, como escalonamento automático, programação de pods ou gerenciamento do ciclo de vida do job.
  • O Kueue tem suporte integrado para a API batch/v1.Job do Kubernetes.
  • Pode ser integrado a outras APIs de job.

O Kueue se refere a jobs definidos com qualquer API como cargas de trabalho, para evitar confusão com a API do Job do Kubernetes específica.

Criar o ResourceFlavor

Um ResourceFlavor é um objeto que representa as variações nos nós disponíveis no seu cluster, associando-os a rótulos e taints de nó. Por exemplo, é possível usar o ResourceFlavors para representar VMs com diferentes garantias de provisionamento (por exemplo, spot vs. sob demanda), arquiteturas (por exemplo, CPUs x86 vs. ARM), marcas e modelos (por exemplo, GPUs Nvidia A100 vs. T4).

Neste tutorial, o cluster kueue-autopilot tem recursos homogêneos. Como resultado, crie um único ResourceFlavor para o CPU, memória, armazenamento temporário e GPUs, sem identificadores ou taints.

apiVersion: kueue.x-k8s.io/v1beta1
kind: ResourceFlavor
metadata:
  name: default-flavor # This ResourceFlavor will be used for all the resources
Implantar o ResourceFlavor:

kubectl apply -f flavors.yaml

Criar o ClusterQueue

Um ClusterQueue é um objeto com escopo de cluster que gerencia um pool de recursos, como CPU, memória e GPU. Ele gerencia os ResourceFlavors e limita o uso e determina a ordem em que as cargas de trabalho são admitidas.

apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: cluster-queue
spec:
  namespaceSelector: {} # Available to all namespaces
  queueingStrategy: BestEffortFIFO # Default queueing strategy
  resourceGroups:
  - coveredResources: ["cpu", "memory", "nvidia.com/gpu", "ephemeral-storage"]
    flavors:
    - name: "default-flavor"
      resources:
      - name: "cpu"
        nominalQuota: 10
      - name: "memory"
        nominalQuota: 10Gi
      - name: "nvidia.com/gpu"
        nominalQuota: 10
      - name: "ephemeral-storage"
        nominalQuota: 10Gi

Implante o ClusterQueue:

kubectl apply -f cluster-queue.yaml

A ordem de consumo é determinada por .spec.queueingStrategy, que apresenta duas configurações:

  • BestEffortFIFO

    • A configuração padrão da estratégia de enfileiramento.
    • A entrada da carga de trabalho segue a regra "primeiro a entrar, primeiro a sair" (PEPS). No entanto, se não houver cota suficiente para admitir a carga de trabalho no cabeçalho da fila, a próxima na linha será testada.
  • StrictFIFO

    • Garante a semântico método PEPS.
    • A carga de trabalho no cabeçalho da fila pode bloquear a fila até que ela possa ser aceita.

Em cluster-queue.yaml, você cria um novo ClusterQueue chamado cluster-queue. Esse ClusterQueue gerencia quatro recursos, cpu, memory, nvidia.com/gpu e ephemeral-storage, com a variação criada em flavors.yaml. A cota é consumida pelas solicitações nas especificações do pod da carga de trabalho.

Cada variação inclui limites de uso representados como .spec.resourceGroups[].flavors[].resources[].nominalQuota. Nesse caso, o ClusterQueue aceitará cargas de trabalho se:

  • A soma das solicitações da CPU for menor ou igual a 10
  • A soma das solicitações de memória for menor ou igual a 10 Gi
  • A soma das solicitações da GPU for menor ou igual a 10
  • A soma do armazenamento usado for menor ou igual a 10 Gi

Criar o LocalQueue

Um LocalQueue é um objeto com namespace que aceita cargas de trabalho de usuários no namespace. LocalQueues de diferentes namespaces podem apontar para o mesmo ClusterQueue em que podem compartilhar a cota dos recursos. Nesse caso, o LocalQueue do namespace team-a e team-b aponta para o mesmo ClusterQueue cluster-queue em .spec.clusterQueue.

apiVersion: kueue.x-k8s.io/v1beta1
kind: LocalQueue
metadata:
  namespace: team-a # LocalQueue under team-a namespace
  name: lq-team-a
spec:
  clusterQueue: cluster-queue # Point to the ClusterQueue
---
apiVersion: kueue.x-k8s.io/v1beta1
kind: LocalQueue
metadata:
  namespace: team-b # LocalQueue under team-b namespace
  name: lq-team-b
spec:
  clusterQueue: cluster-queue # Point to the ClusterQueue

Cada equipe envia as cargas de trabalho para o LocalQueue no próprio namespace. Em seguida, são alocados recursos pelo ClusterQueue.

Implantar os LocalQueues:

kubectl apply -f local-queue.yaml

Criar Jobs e observar as cargas de trabalho admitidas

Nesta seção, você cria jobs do Kubernetes no namespace team-a. Um controlador de job no Kubernetes cria um ou mais pods e garante que eles executem uma tarefa específica com sucesso.

O job no namespace team-a tem os seguintes atributos:

  • Ele aponta para a lq-team-a LocalQueue.
  • Ele solicita recursos de GPU definindo o campo nodeSelector como nvidia-tesla-t4.
  • Ele é composto por três pods que ficam suspensos por 10 segundos em paralelo. Os jobs são limpos após 60 segundos de acordo com o valor definido no campo ttlSecondsAfterFinished.
  • Ele requer 1.500 miliCPU, 1.536 Mi de memória, 1.536 Mi de armazenamento temporário e três GPUs, já que há três pods.
apiVersion: batch/v1
kind: Job
metadata:
  namespace: team-a # Job under team-a namespace
  generateName: sample-job-team-a-
  annotations:
    kueue.x-k8s.io/queue-name: lq-team-a # Point to the LocalQueue
spec:
  ttlSecondsAfterFinished: 60 # Job will be deleted after 60 seconds
  parallelism: 3 # This Job will have 3 replicas running at the same time
  completions: 3 # This Job requires 3 completions
  suspend: true # Set to true to allow Kueue to control the Job when it starts
  template:
    spec:
      nodeSelector:
        cloud.google.com/gke-accelerator: "nvidia-tesla-t4" # Specify the GPU hardware
      containers:
      - name: dummy-job
        image: gcr.io/k8s-staging-perf-tests/sleep:latest
        args: ["10s"] # Sleep for 10 seconds
        resources:
          requests:
            cpu: "500m"
            memory: "512Mi"
            ephemeral-storage: "512Mi"
            nvidia.com/gpu: "1"
          limits:
            cpu: "500m"
            memory: "512Mi"
            ephemeral-storage: "512Mi"
            nvidia.com/gpu: "1"
      restartPolicy: Never

Os jobs também são criados no arquivo job-team-b.yaml, em que o namespace pertence a team-b, com solicitações para representar equipes com necessidades distintas.

Para saber mais, consulte Como implantar cargas de trabalho da GPU no Autopilot.

  1. Em um novo terminal, observe o status do ClusterQueue que é atualizado a cada dois segundos:

    watch -n 2 kubectl get clusterqueue cluster-queue -o wide
    
  2. Em um novo terminal, observe o status dos nós:

    watch -n 2 kubectl get nodes -o wide
    
  3. Em um novo terminal, crie jobs para a fila local a partir do namespace team-a e team-b a cada 10 segundos:

    ./create_jobs.sh job-team-a.yaml job-team-b.yaml 10
    
  4. Observe os jobs sendo enfileirados, aceitos no ClusterQueue e os nós sendo criados com o Autopilot do GKE.

  5. Consiga um job do namespace team-a:

    kubectl -n team-a get jobs
    

    O resultado será assim:

    NAME                      COMPLETIONS   DURATION   AGE
    sample-job-team-b-t6jnr   3/3           21s        3m27s
    sample-job-team-a-tm7kc   0/3                      2m27s
    sample-job-team-a-vjtnw   3/3           30s        3m50s
    sample-job-team-b-vn6rp   0/3                      40s
    sample-job-team-a-z86h2   0/3                      2m15s
    sample-job-team-b-zfwj8   0/3                      28s
    sample-job-team-a-zjkbj   0/3                      4s
    sample-job-team-a-zzvjg   3/3           83s        4m50s
    
  6. Copie o nome de um job da etapa anterior e observe o status e os eventos de admissão de um job usando a API do Workloads:

    kubectl -n team-a describe workload JOB_NAME
    
  7. Quando os jobs pendentes começam a aumentar a partir do ClusterQueue, encerre o script ao pressionar CTRL + C no script em execução.

  8. Quando todos os jobs forem concluídos, observe que os nós estão sendo reduzidos.