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.
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
.
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.
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.
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.
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, ovalue
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
serfalse
.node.kubernetes.io/unreachable
: o nó está inacessível pelo controlador de nó, corresponde a condição/status do nóReady
serUnkown
.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:
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:
- 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:
- 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.
- 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:
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:
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 orequest
, mas nunca ultrapassará o valor dolimit
.
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 deOutOfMemory
(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.
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.
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.
pods: "10"
: Limita o número total de pods que podem ser criados no namespacedefault
a 10. Se um usuário tentar criar mais do que 10 pods, o Kubernetes irá bloquear a criação.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.requests.memory: "1Gi"
: Limita o total de memória solicitada pelos pods no namespace para 1 GiB. Esse é o valor máximo acumulado derequests.memory
que todos os pods combinados podem solicitar.limits.cpu: "4"
: Define que o consumo máximo de CPU para o namespace é de 4 núcleos. Esse limite considera o uso delimits.cpu
, o que significa que o Kubernetes permitirá a criação de pods até que o consumo total de CPU atinja esse valor.limits.memory: "2Gi"
: Define que o total máximo de memória, usandolimits.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.