Kubernetes Scheduling - Técnicas de agendamento

Aprenda sobre as principais técnicas de agendamento (scheduling) de pods no Kubernetes e entenda como otimizar a alocação de recursos no cluster.

Kubernetes Scheduling - Técnicas de agendamento

Um dos principais recursos do Kubernetes é seu sistema de Scheduling, responsável por alocar recursos de maneira eficiente para os contêineres em execução. Em termos simples, o Scheduling no Kubernetes é o processo pelo qual o sistema decide em qual nó (node) um contêiner específico deve ser executado, levando em consideração vários critérios, como recursos disponíveis, requisitos de aplicativos e políticas de alocação.

Ao compreender como o Scheduling funciona no Kubernetes, os desenvolvedores e administradores de sistemas podem otimizar a utilização dos recursos, garantir a escalabilidade e a confiabilidade dos aplicativos, além de facilitar a manutenção e a implantação contínua.

Este artigo é um resumo do curso que fiz sobre Kubernetes. Decidi focar no tema de Scheduling por considerá-lo essencial para entender como o Kubernetes gerencia a alocação de recursos e organiza os pods nos nós do cluster, revelando aspectos fundamentais do funcionamento interno da plataforma.

Agendamento manual

O agendamento manual de um pod no cluster se dá quando o serviço do kube-scheduler não está em execução no momento, que é o serviço padrão do Kubernetes para o escalonamento, nesse caso o agendamento não será possível de forma automática.

Para isso, é possível informar manualmente na definição do arquivo de manifesto em qual nó o pod em questão será agendado, isso é feito durante a criação do pod.

Na definição do arquivo do pod, use a propriedade nodeName com o valor do nome do nó, por exemplo node01.

# file: myapp.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels: 
    app: nginx
spec:
  containers:
    - name: nginx
      image: nginx
      ports:
        - containerPort: 8080
  nodeName: node01

Agendamento manual de um pod usando o atributo nodeName

Com esse manifesto, podemos aplicar ao cluster e veremos que o pod será escalado no nó node01, o mesmo que informamos manualmente.

kubectl apply -f myapp.yaml
'pod/nginx' created.

kubectl get pods -o wide
NAME         READY   STATUS      RESTARTS   AGE    IP            NODE
nginx        1/1     Running     0          35s    10.244.0.4    node01

Labels e Selectors

Labels

As labels (rótulos) são pares chave/valor que são adicionados no objeto com o objetivo de agrupá-los. Você pode usar para especificar atributos de identificação dos objetos que podem ser relevantes para os usuários, por exemplo, agrupar serviços por seu ambiente com uma label environment=dev , ou um conjunto de aplicações que executam rotinas de processamento de vídeo, poderia conter uma label tier=video-processing. Labels também pode ser usado para selecionar subconjuntos de objetos.

metadata:
  labels:
    environment: "dev"
    tier: "video-processing"

Definição de labels

Selectors

Os selectors (seletores), são filtros que podem ser aplicados em cima das labels existentes de um objeto, através dos critérios dos filtros você teria o resultado dos grupos de pods.

Temos abaixo o manifesto de um pod usando a imagem do Nginx, com duas labels e um pod usando a imagem do Mysql. A seguir podemos ver como interagir com os objetos usando os seletores.

apiVersion: v1
kind: Pod
metadata:
  name: webserver
  labels:
    environment: dev
    app: nginx
spec:
  containers:
  - name: nginx
    image: nginx
    ports:
    - containerPort: 80
---
apiVersion: v1
kind: Pod
metadata:
  name: mysql
  labels:
    environment: prod
    app: mysql
spec:
  containers:
  - name: mysql
    image: mysql
    ports:
    - containerPort: 3306

Definição de Pods com Nginx e Mysql

Podemos listar todas as labels adicionadas à um objeto com o parâmetro --show-labels.

kubectl get pods --show-labels
NAME               READY   STATUS             RESTARTS           AGE   LABELS
nginx              1/1     Running            0				     2m    app=nginx,enviroment=dev

Para listar todos os objetos filtrando por uma determinada label, pode ser usado a flag --selector ou -l que é a sua abreviação. Os seguintes operadores são suportados: '=' , '==' e '!='.

kubectl get pods -l app=nginx
NAME        READY   STATUS    RESTARTS   AGE
webserver   1/1     Running   0          32s

kubectl get pods --selector app!=nginx
NAME    READY   STATUS    RESTARTS   AGE
mysql   1/1     Running   0          41s

kubectl get pods --selector app=nginx,environment=dev
NAME        READY   STATUS    RESTARTS   AGE
webserver   1/1     Running   0          47

kubectl get pods --selector app=mysql,environment=dev
No resources found in default namespace.

Um ponto interessante para entendermos, é que o Kubernetes internamente usa as labels e selector para vincular um replica set em um pod, um service no deployment, e assim por diante.

Taints e Tolerations

Os taints são uma forma de marcar (ou manchar) um nó para que esse não receba novos pods que não correspondam a um determinado critério (tolerância), ou seja, repelir os pods.

As tolerations seriam marcar um pod que seja tolerante a essa restrição, ou seja, para um determinado nó que foi marcado para não receber pods, um pod com tolerations irá ignorar essa restrição e será agendado nesses nós marcados.

Exemplo:

O nó node01 for marcado da seguinte forma:

kubectl taint nodes node01 monitoring=prometheus:NoSchedule

Dessa forma, somente pods que contenham uma tolerância poderá ser agendado nesse nó. No exemplo a seguir, esse pod contém uma tolerância e será agendado no nó que foi marcado.

apiVersion: v1
kind: Pod
metadata:
  name: nginx-toleration
spec:
  containers:
  - name: nginx
    image: nginx
  tolerations:
  - key: "monitoring"
    operator: "Equal"
    value: "prometheus"
    effect: "NoSchedule"

Definição de Pod com tolerância ao nó

Vamos entender um pouco mais...

Uma tolerância corresponde a uma mancha (taint) se as chaves forem iguais e os efeitos forem os mesmos, e:

  • O operator é Exists (nesse caso, o value não deve ser especificado), ou
  • O operator é Equal e os valores devem ser iguais.

Para o campo effect, são permitidos os seguintes valores:

NoExecute:

Isso afeta os pods que já estão em execução no nó da seguinte maneira:

  • Os pods que não toleram a contaminação são despejados imediatamente.
  • Os pods que toleram a contaminação sem especificar tolerationsSeconds em sua especificação de tolerância permanecem vinculados para sempre.
  • Os pods que toleram a contaminação com a propriedade tolerationsSeconds especificada, permanecem vinculados pelo período informado, após esse tempo, os pods são despejados dos nós.

NoSchedule:

Nenhum pod será agendado no nó contaminado a menos que tenha uma tolerância correspondente. Os pods em execução não são removidos.

PreferNoSchedule:

Essa é uma versão preferencial do NoSchedule, o plano de controle tentará evitar colocar o pod que não tolere a contaminação, mas não é garantido.

Despejos baseados em contaminação

O controlador de nó contamina automaticamente um nó quando certas condições são verdadeiras. Exemplo:

  • node.kubernetes.io/not-ready: o nó não está pronto, corresponde a condição/status do nó Ready ser false.
  • node.kubernetes.io/unreachable: o nó está inacessível pelo controlador de nó, corresponde a condição/status do nó Ready ser Unkown.
  • node.kubernetes.io/memory-pressure: o nó tem pressão de memória.
  • node.kubernetes.io/disk-pressure: o nó tem pressão de disco.
  • node.kubernetes.io/pid-pressure: o nó tem pressão PID.
  • node.kubernetes.io/network-unvailable: a rede do nó está indisponível.
  • node.kubernetes.io/unschedulable: o nó não é programável.

Os pods que são criados a partir de um DaemonSet são criados com tolerâncias para que evitem de serem removidos no caso de uma contaminação, por exemplo.

O pod já vem com essa duas tolerâncias:

  • node.kubernetes.io/unreachable
  • node.kubernetes.io/no-ready

NodeSelector

O seletor de nó é a forma mais simples para determinar em qual nó o pod deve ser agendado através de seus rótulos. Você pode adicionar a propriedade nodeSelector à especificação do pod e informar qual o rótulo correspondente para que o pod seja agendado, se vários nós corresponderem ao rótulo informado, o scheduler do Kubernetes decidirá em qual nó o pod será colocado com base em outros fatores, como capacidade de recursos e afinidade.

Para visualizar todos as labels existentes em um nó, use o seguinte comando:

kubectl get nodes --show-labels

NAME      STATUS    ROLES    AGE     VERSION        LABELS
worker1   Ready     <none>   1d      v1.24.0        ...,kubernetes.io/hostname=worker1

Para adicionar uma label ao nó, execute o comando:

kubectl label nodes worker1 disktype=ssd 

kubectl get nodes --show-labels

NAME      STATUS    ROLES    AGE     VERSION        LABELS
worker1   Ready     <none>   1d      v1.24.0        ...,disktype=ssd

Agora a propriedade nodeSelector, pode ser usada da seguinte maneira:

...
spec:
  containers:
  - name: nginx
    image: nginx
  nodeSelector:
    disktype: ssd

Definição de um Pod com a propriedade nodeSelector

Dessa forma, o pod será agendado em nó que corresponder a esse rótulo.

Node Affinity e anti-affinity

Afinidade e anti-afinidade expandem os tipos de restrições que você pode definir para o agendamento. Alguns benefícios de usar afinidade e anti-afinidade:

  • Poder usar lógicas mais elaboradas para escolher o nó de destino, ao contrário do nodeSelector que apenas nós com as labels especificadas são escolhidas.
  • Poder escolher se a regra é flexível ou preferencial para que o agendador agende um pod mesmo que não consiga encontrar um nó correspondente.

Afinidade de nó

A afinidade do nó tem o mesmo conceito da propriedade nodeSelector, que permite restringir em quais nós o pod dever se agendado. Existem dois tipos de afinidade de nó:

  • requiredDuringSchedulingIgnoredDuringExecution: O agendador irá agendar o pod conforme a regra definida. Caso não encontre um nó que atenda as condições, o pod não será agendado.
  • preferredDuringSchedulingIgnoredDuringExecution: O agendador irá tentar encontrar um nó que atenda a regra definida. Caso não encontre um nó que corresponda as condições, o agendar ainda irá agendar o pod.

Dado o seguinte manifesto, temos:

apiVersion: v1
kind: Pod
metadata:
  name: pod-affinity
spec:
  containers:
  - name: nginx
    image: nginx
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: disktype
            operator: In
            values:
            - ssd

Definição de Pod com a propriedade nodeAffinity

  • Este pod só será agendado em nós que tenham o rótulo disktype=ssd.
  • Se não houver nós com este rótulo, o pod ficará em estado pending.

Para entender melhor sobre cada tipo de afinidade, vamos dividir em duas partes cada propriedade do tipo de afinidade:

  1. Primeira parte é o início de cada tipo de afinidade:

requiredDuringScheduling: Nesse trecho, estamos dizendo para o agendador que as condições definidas são obrigadas durante o agendamento, ou seja, deverá atender estritamente as regras para que o agendamento possa ser realizado.

preferredDuringScheduling: Nesse trecho, estamos dizendo para o agendador que se as condições propostas ao pod não for possível atender, se por exemplo, o label disktype=ssd não corresponder a nenhum nó, então o agendador poderá escolher outro nó para agendar esse pod.

  1. Segunda parte é o final de cada tipo de afinidade:

ignoreDuringExecution: Nesse trecho, estamos dizendo ao Kubernetes que se um pod que já está em execução em um nó e uma label é removida desse nó, esse pod não será removido. Exemplo: o pod foi agendado no nó worker1 devido a label disktype=ssd que correspondeu aos critérios do pod, se essa label for removida, o agendador não irá remover esse pod do nó, pois será ignorado durante a execução.

Anti-afinidade de nó

Anti-afinidade de nó funciona no mesmo conceito de afinidade de nó, só que ao invés de trabalhar com regras de "este pod deve", será da forma de "este pod não deve" ser agendado em um determinado nó.

Dado o seguinte manifesto:

spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/os
            operator: NotIn
            values:
            - linux

Definição de Pod com anti-affinity

No exemplo acima, estamos instruindo o Kubernetes a não agendar o pod em nós que estejam executando o sistema operacional Linux. Ele restringe o agendamento apenas para nós que rodam outro sistema operacional, como Windows.

Se não houver nós com sistema operacional diferente de Linux, o pod ficará pendente e não será agendado.

Requests e limits

Os recursos de solicitações e limites são informações de consumo de CPU e memória que você pode especificar opcionalmente para um contêiner.

Ao usar a solicitação de recurso para os contêineres em um pod, o agendador usa essas informações para decidir em qual nó o pod deve ser agendado.

O agendador irá sempre buscar um nó que tenha recursos disponíveis para que o pod seja agendado. Caso não encontre nenhum nó, o pod ficará com o status Pending e provavelmente se visualizar o status do pod, usando o comando kubectl describe pod verá que houve uma falha no agendamento devido a CPU insuficiente Insufficient cpu (3).

Recursos de CPU

As solicitações e limites de CPU são medidos em unidade de CPU, no Kubernetes, 1 unidade de CPU equivale a 1 núcleo de CPU físico ou 1 núcleo virtual.

São permitidas usar solicitações fracionárias, por exemplo 0.5, está solicitando metade do tempo de CPU, em relação a uma CPU de 1 core.

Recursos de Memória

As solicitações e limites de memória são medidos em bytes, você pode informar a memória como um número inteiro simples ou como um número de ponto fixo usando os sufixos de quantidade.

Exemplo de uso:

spec:
  containers:
  - name: app
    image: images.my-company.example/app:v4
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"

Definição de Pod com requests e limits

resources: Esta seção configura os recursos de CPU e memória que o contêiner solicita e o limite máximo que pode usar. Ela contém dois subitens:

  • requests: Define a quantidade mínima de recursos que o contêiner precisa para ser executado. Isso garante que, quando o pod for agendado, o nó terá pelo menos essa quantidade de recursos disponível.
  • limits: Define a quantidade máxima de recursos que o contêiner pode usar. O contêiner pode consumir mais do que o request, mas nunca ultrapassará o valor do limit.

Análise Detalhada dos Recursos

requests

  • memory: "64Mi": Especifica que o contêiner solicita 64 MiB (Mebibytes) de memória. Isso significa que o Kubernetes tentará agendar este pod apenas em nós que tenham pelo menos 64 MiB de memória disponível.
  • cpu: "250m": Especifica que o contêiner solicita 250 milicores (ou 0,25 core) de CPU. Esse é o recurso de CPU mínimo que o Kubernetes garantirá para o contêiner, mas ele pode usar mais até o limite especificado.

limits

  • memory: "128Mi": Especifica que o contêiner terá um limite máximo de 128 MiB de memória. Se o contêiner tentar usar mais do que isso, o Kubernetes poderá terminá-lo com um erro de OutOfMemory (OOM).
  • cpu: "500m": Especifica que o contêiner terá um limite máximo de 500 milicores (ou 0,5 core) de CPU. Isso significa que o contêiner poderá consumir até metade de um núcleo de CPU, mas não mais do que isso.

Limit Range

Por padrão os contêineres são executados com recursos de computação ilimitados no cluster. O limit range é uma política para restringir as alocações de recursos que você pode especificar para objetos como Pod e PersistenVolumeClaim em um namespace específico.

Um LimitRange fornece restrições que podem:

  • Definir o uso mínimo e máximo de recursos de computação por pod ou contêiner em um namespace.
  • Aplicar solicitação de armazenamento mínimo e máximo por PersistentVolumeClaim em um namespace.
  • Definir uma proporção entre solicitação e limite para um recurso em um namespace.
  • Definir solicitação/limite padrão para recursos de computação em um namespace e injetá-los automaticamente em contêineres em tempo de execução.
apiVersion: v1
kind: LimitRange
metadata:
  name: resources-limits
  namespace: default
spec:
  limits:
  - type: Container
    max:
      cpu: "2"           # Máximo de 2 cores de CPU por contêiner
      memory: "1Gi"      # Máximo de 1 GiB de memória por contêiner
    min:
      cpu: "200m"        # Mínimo de 200 milicores (0.2 cores) de CPU por contêiner
      memory: "100Mi"    # Mínimo de 100 MiB de memória por contêiner
    default:
      cpu: "500m"        # Solicitação padrão de CPU se não especificado
      memory: "200Mi"    # Solicitação padrão de memória se não especificado
    defaultRequest:
      cpu: "300m"        # Request padrão de CPU para contêineres que não especificarem um valor
      memory: "150Mi"    # Request padrão de memória para contêineres que não especificarem um valor

Definição de LimitRange para Contêiner no namespace default

LimitRange para PersistentVolumeClaim

Outro uso comum do LimitRange é para definir limites em PersistentVolumeClaims (PVCs). Isso pode garantir que o armazenamento seja usado de forma responsável.

apiVersion: v1
kind: LimitRange
metadata:
  name: storage-limits
  namespace: default
spec:
  limits:
  - type: PersistentVolumeClaim
    max:
      storage: "10Gi"    # Máximo de 10 GiB de armazenamento por PVC
    min:
      storage: "1Gi"     # Mínimo de 1 GiB de armazenamento por PVC

Definição de LimitRange para PVCs no namespace default

Resource Quotas

A cota por recursos é definido por um objeto ResourceQuota, e fornece restrições que limitam o consumo de recursos por namespace. Ele pode limitar a quantidade de objetos que podem ser criados em um namespace por tipo.

Para recursos de CPU e Memória, ResourceQuota impõe que cada novo pod nesse namespace defina um limite para esse recurso. Se você aplicar uma cota de recursos em um namespace para CPU ou memória, o requests e limits sempre deverão ser especificados para o pod enviado. Caso contrário a admissão do pod poderá ser rejeitada.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: resource-quota
  namespace: default
spec:
  hard:
    pods: "10"                      # Limita o número máximo de pods para 10
    requests.cpu: "2"               # Limita o total de requests de CPU para 2 cores
    requests.memory: "1Gi"          # Limita o total de requests de memória para 1 GiB
    limits.cpu: "4"                 # Limita o total de CPU para 4 cores
    limits.memory: "2Gi"            # Limita o total de memória para 2 GiB

Definição de ResourceQuota para o namespace default

  1. pods: "10": Limita o número total de pods que podem ser criados no namespace default a 10. Se um usuário tentar criar mais do que 10 pods, o Kubernetes irá bloquear a criação.
  2. requests.cpu: "2": Define que a soma de todas as solicitações de CPU (requests.cpu) dos pods no namespace não pode ultrapassar 2 núcleos. Ou seja, se os pods no namespace já estiverem usando 2 núcleos de CPU solicitados, não será possível agendar novos pods que aumentem essa demanda.
  3. requests.memory: "1Gi": Limita o total de memória solicitada pelos pods no namespace para 1 GiB. Esse é o valor máximo acumulado de requests.memory que todos os pods combinados podem solicitar.
  4. limits.cpu: "4": Define que o consumo máximo de CPU para o namespace é de 4 núcleos. Esse limite considera o uso de limits.cpu, o que significa que o Kubernetes permitirá a criação de pods até que o consumo total de CPU atinja esse valor.
  5. limits.memory: "2Gi": Define que o total máximo de memória, usando limits.memory, é de 2 GiB. Isso limita o uso total de memória dentro do namespace.

Conclusão

Entender as diferentes formas de agendamento de pods no Kubernetes é essencial para aproveitar ao máximo a infraestrutura e garantir que os recursos sejam utilizados de maneira inteligente e equilibrada.

Desde as políticas de afinidade e anti-afinidade até quotas e limites de recursos, cada técnica contribui para um ambiente mais resiliente e adaptado às necessidades da sua aplicação.

Até a próxima.