Implementar um sistema de enfileiramento de jobs com compartilhamento de cota entre namespaces no GKE

Neste tutorial, usamos o Kueue para mostrar como implementar um sistema de enfileiramento de jobs, configurar o recurso de carga de trabalho e o compartilhamento de cota entre diferentes namespaces no Google Kubernetes Engine (GKE) e maximizar a utilização do cluster.

Contexto

Como engenheiro de infraestrutura ou administrador de cluster, maximizar a utilização entre namespaces é muito importante. Um lote de jobs em um namespace pode não utilizar totalmente a cota completa atribuída ao namespace, enquanto outro namespace pode ter vários jobs pendentes. Para usar com eficiência os recursos do cluster entre jobs em namespaces diferentes e aumentar a flexibilidade do gerenciamento de cotas, configure coortes no Kueue. Uma coorte é um grupo de ClusterQueues que podem pedir emprestados cotas não utilizadas uns dos outros. Um ClusterQueue controla um conjunto de recursos, como aceleradores de hardware, memória e CPU.

Você pode encontrar uma definição mais detalhada de todos esses conceitos na documentação do Kueue

Criar o ResourceFlavors

Um ResourceFlavor representa variações de recursos nos nós do cluster, como VMs diferentes (por exemplo, spot versus sob demanda), arquiteturas (por exemplo, CPUs x86 x ARM), marcas e modelos (por exemplo, Nvidia A100 versus GPUs T4).

Os ResourceFlavors usam rótulos e taints de nós para fazer a correspondência com um conjunto de nós no cluster.

apiVersion: kueue.x-k8s.io/v1beta1
kind: ResourceFlavor
metadata:
  name: on-demand # This ResourceFlavor will be used for the CPU resource
spec:
  nodeLabels:
    cloud.google.com/gke-provisioning: standard # This label was applied automatically by GKE
---
apiVersion: kueue.x-k8s.io/v1beta1
kind: ResourceFlavor
metadata:
  name: spot # This ResourceFlavor will be used as added resource for the CPU resource
spec:
  nodeLabels:  
    cloud.google.com/gke-provisioning: spot # This label was applied automatically by GKE

Nesse manifesto:

  • O ResourceFlavor on-demand tem o rótulo definido como cloud.google.com/gke-provisioning: standard.
  • O ResourceFlavor spot tem o rótulo definido como cloud.google.com/gke-provisioning: spot.

Quando uma carga de trabalho é atribuída a um ResourceFlavor, o Kueue atribui os pods da carga aos nós que correspondem aos rótulos de nós definidos para o ResourceFlavor.

Implantar o ResourceFlavor:

kubectl apply -f flavors.yaml

Criar o ClusterQueue e a LocalQueue

Crie duas ClusterQueues cq-team-a e cq-team-b e as LocalQueues lq-team-a e lq-team-b correspondentes com os namespaces team-a e team-b, respectivamente.

ClusterQueues são objetos com escopo de cluster que controlam um pool de recursos, como aceleradores de hardware, memória e CPU. Os administradores em lote podem restringir a visibilidade desses objetos a usuários em lote.

LocalQueues são objetos com namespace que os usuários em lote podem listar. Eles apontam para CluterQueues, a partir dos quais os recursos são alocados para executar as cargas de trabalho da LocalQueue.

apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: cq-team-a
spec:
  cohort: all-teams # cq-team-a and cq-team-b share the same cohort
  namespaceSelector:
    matchLabels:
      kubernetes.io/metadata.name: team-a #Only team-a can submit jobs direclty to this queue, but will be able to share it through the cohort
  resourceGroups:
  - coveredResources: ["cpu", "memory"]
    flavors:
    - name: on-demand
      resources:
      - name: "cpu"
        nominalQuota: 10
        borrowingLimit: 5
      - name: "memory"
        nominalQuota: 10Gi
        borrowingLimit: 15Gi
    - name: spot # This ClusterQueue doesn't have nominalQuota for spot, but it can borrow from others
      resources:
      - name: "cpu"
        nominalQuota: 0
      - name: "memory"
        nominalQuota: 0
---
apiVersion: kueue.x-k8s.io/v1beta1
kind: LocalQueue
metadata:
  namespace: team-a # LocalQueue under team-a namespace
  name: lq-team-a
spec:
  clusterQueue: cq-team-a # Point to the ClusterQueue team-a-cq

ClusterQueues permite que os recursos tenham diversas variações. Nesse caso, as duas QueueQueues têm duas variações, on-demand e spot, cada uma fornecendo recursos cpu. A cota de spot do ResourceFlavor está definida como 0 e não será usada no momento.

As duas ClusterQueues compartilham a mesma coorte chamada all-teams, definida em .spec.cohort. Quando duas ou mais ClusterQueues compartilham a mesma coorte, é possível pedir emprestado uma cota não utilizada entre si.

Saiba mais sobre como as coortes funcionam e a semântica do empréstimo na documentação do Kueue

Implante as ClusterQueues e LocalQueues:

kubectl apply -f cq-team-a.yaml
kubectl apply -f cq-team-b.yaml

(Opcional) Monitorar cargas de trabalho usando o kube-prometheus

É possível usar o Prometheus para monitorar suas cargas de trabalho ativas e pendentes do Kueue. Para monitorar as cargas de trabalho que estão sendo criadas e observar a carga em cada ClusterQueue, implante o kube-prometheus no cluster no namespace monitoring:

  1. Faça o download do código-fonte do operador do Prometheus:

    cd
    git clone https://github.com/prometheus-operator/kube-prometheus.git
    
  2. Crie os CustomResourceDefinitions(CRDs):

    kubectl create -f kube-prometheus/manifests/setup
    
  3. Crie os componentes de monitoramento:

    kubectl create -f kube-prometheus/manifests
    
  4. Permita que o prometheus-operator colete métricas dos componentes do Kueue:

    kubectl apply -f https://github.com/kubernetes-sigs/kueue/releases/download/$VERSION/prometheus.yaml
    
  5. Mude para o diretório de trabalho:

    cd kubernetes-engine-samples/batch/kueue-cohort
    
  6. Configure o encaminhamento de portas para o serviço Prometheus em execução no cluster do GKE:

    kubectl --namespace monitoring port-forward svc/prometheus-k8s 9090
    
  7. Abra a interface da Web do Prometheus em localhost:9090 no navegador.

    No Cloud Shell:

    1. Clique em Visualização da Web.

    2. Clique em Alterar porta e defina o número da porta como 9090.

    3. Clique em Alterar e visualizar.

    A seguinte UI da Web do Prometheus aparece.

    Captura de tela da UI da Web do Prometheus

  8. Na caixa de consulta Expressão, insira a consulta a seguir para criar o primeiro painel que monitora as cargas de trabalho ativas da ClusterQueue cq-team-a:

    kueue_pending_workloads{cluster_queue="cq-team-a", status="active"} or kueue_admitted_active_workloads{cluster_queue="cq-team-a"}
    
  9. Clique em Adicionar painel.

  10. Na caixa de consulta Expressão, insira a consulta a seguir para criar outro painel que monitora as cargas de trabalho ativas da ClusterQueue cq-team-b:

    kueue_pending_workloads{cluster_queue="cq-team-b", status="active"} or kueue_admitted_active_workloads{cluster_queue="cq-team-b"}
    
  11. Clique em Adicionar painel.

  12. Na caixa de consulta Expressão, insira a consulta a seguir para criar um painel que monitora o número de nós no cluster:

    count(kube_node_info)
    

(Opcional) Monitorar cargas de trabalho usando o Google Cloud Managed Service para Prometheus

É possível usar o Google Cloud Managed Service para Prometheus para monitorar as cargas de trabalho ativas e pendentes do Kueue. Confira a lista completa de métricas na documentação do Kueue.

  1. Configure a identidade e o RBAC para acesso às métricas:

    A configuração a seguir cria quatro recursos do Kubernetes que fornecem acesso a métricas para os coletores do Google Cloud Managed Service para Prometheus.

    • Uma ServiceAccount chamada kueue-metrics-reader no namespace kueue-system será usada para autenticar ao acessar as métricas do Kueue.

    • Um Secret associado à conta de serviço kueue-metrics-reader, armazena um token de autenticação usado pelo coletor para se autenticar com o endpoint de métricas exposto pela implantação do Kueue.

    • Uma função chamada kueue-secret-reader no namespace kueue-system, que permite ler o secret que contém o token da conta de serviço.

    • Um ClusterRoleBinding que concede à conta de serviço kueue-metrics-reader o ClusterRole kueue-metrics-reader.

    apiVersion: v1
    kind: ServiceAccount
    metadata:
     name: kueue-metrics-reader
     namespace: kueue-system
    ---
    apiVersion: v1
    kind: Secret
    metadata:
     name: kueue-metrics-reader-token
     namespace: kueue-system
     annotations:
       kubernetes.io/service-account.name: kueue-metrics-reader
    type: kubernetes.io/service-account-token
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
     name: kueue-secret-reader
     namespace: kueue-system
    rules:
    -   resources:
     -   secrets
     apiGroups: [""]
     verbs: ["get", "list", "watch"]
     resourceNames: ["kueue-metrics-reader-token"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
     name: kueue-metrics-reader
    subjects:
    -   kind: ServiceAccount
     name: kueue-metrics-reader
     namespace: kueue-system
    roleRef:
     kind: ClusterRole
     name: kueue-metrics-reader
     apiGroup: rbac.authorization.k8s.io
    
  2. Configure o RoleBinding para o Google Cloud Managed Service para Prometheus:

    Dependendo se você estiver usando um cluster Autopilot ou Standard, será necessário criar o RoleBinding no namespace gke-gmp-system ou gmp-system. Esse recurso permite que a conta de serviço do coletor acesse o segredo kueue-metrics-reader-token para autenticar e extrair as métricas do Kueue.

    Piloto automático

      apiVersion: rbac.authorization.k8s.io/v1
      kind: RoleBinding
      metadata:
        name: gmp-system:collector:kueue-secret-reader
        namespace: kueue-system
      roleRef:
        name: kueue-secret-reader
        kind: Role
        apiGroup: rbac.authorization.k8s.io
      subjects:
      -   name: collector
        namespace: gke-gmp-system
        kind: ServiceAccount
    

    Padrão

      apiVersion: rbac.authorization.k8s.io/v1
      kind: RoleBinding
      metadata:
        name: gmp-system:collector:kueue-secret-reader
        namespace: kueue-system
      roleRef:
        name: kueue-secret-reader
        kind: Role
        apiGroup: rbac.authorization.k8s.io
      subjects:
      -   name: collector
        namespace: gmp-system
        kind: ServiceAccount
    
  3. Configure o recurso PodMonitoring:

    O recurso a seguir configura o monitoramento da implantação do Kueue e especifica que as métricas são expostas no caminho /metrics por HTTPS. Ele usa o secret kueue-metrics-reader-token para autenticação ao extrair as métricas.

    apiVersion: monitoring.googleapis.com/v1
    kind: PodMonitoring
    metadata:
    name: kueue
    namespace: kueue-system
    spec:
    selector:
     matchLabels:
       control-plane: controller-manager
    endpoints:
    -   port: 8443
     interval: 30s
     path: /metrics
     scheme: https
     tls:
       insecureSkipVerify: true
     authorization:
       type: Bearer
       credentials:
         secret:
           name: kueue-metrics-reader-token
           key: token
    

Consultar métricas exportadas

Exemplos de consultas PromQL para monitorar sistemas baseados no Kueue

Com essas consultas do PromQL, é possível monitorar as principais métricas do Kueue, como capacidade de processamento de jobs, uso de recursos por fila e tempos de espera da carga de trabalho, para entender o desempenho do sistema e identificar possíveis gargalos.

Capacidade de jobs

Isso calcula a taxa por segundo de cargas de trabalho aceitas em cinco minutos para cada cluster_queue. Essa métrica pode ajudar a detalhar por fila, identificar gargalos e somar o throughput geral do sistema.

Consulta:

sum(rate(kueue_admitted_workloads_total[5m])) by (cluster_queue)

Uso de recursos

Isso pressupõe que metrics.enableClusterQueueResources esteja ativado. Ele calcula a proporção entre o uso atual da CPU e a cota nominal de CPU para cada fila. Um valor próximo de 1 indica alta utilização. Você pode adaptar isso para memória ou outros recursos mudando o rótulo do recurso.

Para instalar uma versão lançada do Kueue com configuração personalizada no cluster, siga a documentação do Kueue.

Consulta:

sum(kueue_cluster_queue_resource_usage{resource="cpu"}) by (cluster_queue) / sum(kueue_cluster_queue_nominal_quota{resource="cpu"}) by (cluster_queue)

Tempo de espera na fila

Isso fornece o tempo de espera do 90º percentil para cargas de trabalho em uma fila específica. É possível modificar o valor do quantil (por exemplo, 0,5 para mediana, 0,99 para o percentil 99) para entender a distribuição do tempo de espera.

Consulta:

histogram_quantile(0.9, kueue_admission_wait_time_seconds_bucket{cluster_queue="QUEUE_NAME"})

Criar Jobs e observar as cargas de trabalho admitidas

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

Gerar jobs para os ClusterQueues que ficarão suspensos por 10 segundos, com três jobs em paralelo e com três conclusões concluídas. Em seguida, ele será limpo após 60 segundos.

apiVersion: batch/v1
kind: Job
metadata:
  namespace: team-a # Job under team-a namespace
  generateName: sample-job-team-a-
  labels:
    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:
      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"
      restartPolicy: Never

job-team-a.yaml cria jobs no namespace team-a e aponta para o LocalQueue lq-team-a e o ClusterQueue cq-team-a.

Da mesma forma, job-team-b.yaml cria jobs no namespace team-b e aponta para o LocalQueue lq-team-b e a ClusterQueue cq-team-b.

  1. Inicie um novo terminal e execute este script para gerar um job a cada segundo:

    ./create_jobs.sh job-team-a.yaml 1
    
  2. Inicie outro terminal e crie jobs para o namespace team-b:

    ./create_jobs.sh job-team-b.yaml 1
    
  3. Observe os jobs que estão sendo enfileirados no Prometheus. Ou com este comando:

    watch -n 2 kubectl get clusterqueues -o wide
    

A saída será semelhante a esta:

    NAME        COHORT      STRATEGY         PENDING WORKLOADS   ADMITTED WORKLOADS
    cq-team-a   all-teams   BestEffortFIFO   0                   5
    cq-team-b   all-teams   BestEffortFIFO   0                   4

Pedir emprestado cota não utilizada com coortes

As ClusterQueues podem não estar com capacidade total o tempo todo. O uso de cotas não é maximizado quando as cargas de trabalho não são distribuídas uniformemente entre os ClusterQueues. Se ClusterQueues compartilharem a mesma coorte entre si, elas poderão pegar emprestado cotas de outras ClusterQueues para maximizar a utilização da cota.

  1. Quando houver jobs enfileirados para os ClusterQueues cq-team-a e cq-team-b, interrompa o script do namespace team-b pressionando CTRL+c no terminal correspondente.

  2. Depois que todos os jobs pendentes do namespace team-b forem processados, os jobs do namespace team-a poderão usar os recursos disponíveis em cq-team-b:

    kubectl describe clusterqueue cq-team-a
    

    Como cq-team-a e cq-team-b compartilham a mesma coorte chamada all-teams, essas ClusterQueues podem compartilhar recursos que não são utilizados.

      Flavors Usage:
        Name:  on-demand
        Resources:
          Borrowed:  5
          Name:      cpu
          Total:     15
          Borrowed:  5Gi
          Name:      memory
          Total:     15Gi
    
  3. Retome o script para o namespace team-b.

    ./create_jobs.sh job-team-b.yaml 3
    

    Observe como os recursos emprestados de cq-team-a retornam a 0, enquanto os recursos de cq-team-b são usados para as próprias cargas de trabalho:

    kubectl describe clusterqueue cq-team-a
    
      Flavors Usage:
        Name:  on-demand
        Resources:
          Borrowed:  0
          Name:      cpu
          Total:     9
          Borrowed:  0
          Name:      memory
          Total:     9Gi
    

Aumentar a cota com VMs do Spot

Quando a cota precisar ser aumentada temporariamente, por exemplo, para atender à alta demanda em cargas de trabalho pendentes, é possível configurar o Kueue para acomodar a demanda adicionando mais ClusterQueues à coorte. ClusterQueues com recursos não utilizados podem compartilhá-los com outras ClusterQueues que pertencem à mesma coorte.

No início do tutorial, você criou um pool de nós chamado spot usando VMs do Spot e um ResourceFlavor chamado spot com o rótulo definido como cloud.google.com/gke-provisioning: spot. Crie um ClusterQueue para usar este pool de nós e o ResourceFlavor que o representa:

  1. Crie um novo ClusterQueue chamado cq-spot com a coorte definida como all-teams:

    apiVersion: kueue.x-k8s.io/v1beta1
    kind: ClusterQueue
    metadata:
      name: spot-cq
    spec:
      cohort: all-teams # Same cohort as cq-team-a and cq-team-b
      resourceGroups:
      - coveredResources: ["cpu", "memory"]
        flavors:
        - name: spot
          resources:
          - name: "cpu"
            nominalQuota: 40
          - name: "memory"
            nominalQuota: 144Gi

    Como essa ClusterQueue compartilha a mesma coorte com cq-team-a e cq-team-b, a cq-team-a e a cq-team-b podem receber recursos de até 15 solicitações de CPU e 15 Gi de memória.

    kubectl apply -f cq-spot.yaml
    
  2. No Prometheus, observe como as cargas de trabalho admitidas aumentam para cq-team-a e cq-team-b graças à cota adicionada por cq-spot que compartilha a mesma coorte. Ou com este comando:

    watch -n 2 kubectl get clusterqueues -o wide
    
  3. No Prometheus, observe o número de nós no cluster. Ou com este comando:

    watch -n 2 kubectl get nodes -o wide
    
  4. Interrompa os dois scripts pressionando CTRL+c para os namespaces team-a e team-b.