1 - Taints e Tolerâncias
Afinidade de nó
é uma propriedade dos Pods que os associa a um conjunto de nós (seja como uma preferência ou uma exigência). Taints são o oposto -- eles permitem que um nó repudie um conjunto de pods.
Tolerâncias são aplicadas em pods e permitem, mas não exigem, que os pods sejam alocados em nós com taints correspondentes.
Taints e tolerâncias trabalham juntos para garantir que pods não sejam alocados em nós inapropriados. Um ou mais taints são aplicados em um nó; isso define que o nó não deve aceitar nenhum pod que não tolera essas taints.
Conceitos
Você adiciona um taint a um nó utilizando kubectl taint.
Por exemplo,
kubectl taint nodes node1 key1=value1:NoSchedule
define um taint no nó node1
. O taint tem a chave key1
, valor value1
e o efeito NoSchedule
.
Isso significa que nenhum pod conseguirá ser executado no nó node1
a menos que possua uma tolerância correspondente.
Para remover o taint adicionado pelo comando acima, você pode executar:
kubectl taint nodes node1 key1=value1:NoSchedule-
Você especifica uma tolerância para um pod na especificação do Pod. Ambas as seguintes tolerâncias "correspondem" ao taint criado pelo kubectl taint
acima, e assim um pod com qualquer uma delas poderia ser executado no node1
:
tolerations:
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoSchedule"
tolerations:
- key: "key1"
operator: "Exists"
effect: "NoSchedule"
Aqui está um exemplo de um pod que utiliza tolerâncias:
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
env: test
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
tolerations:
- key: "example-key"
operator: "Exists"
effect: "NoSchedule"
O valor padrão de operator
é Equal
.
Uma tolerância "casa" um taint se as chaves e efeitos são os mesmos, e:
- o valor de
operator
é Exists
(no caso nenhum value
deve ser especificado), ou
- o valor de
operator
é Equal
e os valores de value
são iguais.
Nota: Existem dois casos especiais:
Uma key
vazia com o operador Exists
"casa" todas as chaves, valores e efeitos, o que significa que o pod irá tolerar tudo.
Um effect
vazio "casa" todos os efeitos com a chave key1
.
O exemplo acima usou effect
de NoSchedule
. De forma alternativa, você pode usar effect
de PreferNoSchedule
.
Nesse efeito, o sistema tentará evitar que o pod seja alocado ao nó caso ele não tolere os taints definidos, contudo a alocação não será evitada de forma obrigatória. Pode-se dizer que o PreferNoSchedule
é uma versão permissiva do NoSchedule
. O terceiro tipo de effect
é o NoExecute
que será descrito posteriormente.
Você pode colocar múltiplos taints no mesmo nó e múltiplas tolerâncias no mesmo pod.
O jeito que o Kubernetes processa múltiplos taints e tolerâncias é como um filtro: começa com todos os taints de um nó, em seguida ignora aqueles para os quais o pod tem uma tolerância relacionada; os taints restantes que não foram ignorados indicam o efeito no pod. Mais especificamente,
- se existe pelo menos um taint não tolerado com o efeito
NoSchedule
, o Kubernetes não alocará o pod naquele nó
- se existe um taint não tolerado com o efeito
NoSchedule
, mas existe pelo menos um taint não tolerado com o efeito PreferNoSchedule
, o Kubernetes tentará não alocar o pod no nó
- se existe pelo menos um taint não tolerado com o efeito
NoExecute
, o pod será expulso do nó (caso já esteja em execução) e não será alocado ao nó (caso ainda não esteja em execução).
Por exemplo, imagine que você tem um nó com os seguintes taints
kubectl taint nodes node1 key1=value1:NoSchedule
kubectl taint nodes node1 key1=value1:NoExecute
kubectl taint nodes node1 key2=value2:NoSchedule
E um pod com duas tolerâncias:
tolerations:
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoSchedule"
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoExecute"
Nesse caso, o pod não será alocado ao nó porque não possui uma tolerância para o terceiro taint. Porém, se ele já estiver rodando no nó quando o taint foi adicionado, não será afetado e continuará rodando, tendo em vista que o terceiro taint é o único não tolerado pelo pod.
Normalmente, se um taint com o efeito NoExecute
é adicionado a um nó, qualquer pod que não o tolere será expulso imediatamente e pods que o toleram nunca serão expulsos. Contudo, uma tolerância com efeito NoExecute
pode especificar de forma opcional o campo tolerationSeconds
, que determina quanto tempo o pod continuará alocado ao nó depois que o taint é adicionado. Por exemplo,
tolerations:
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoExecute"
tolerationSeconds: 3600
significa que se esse pod está sendo executado e um taint correspondente é adicionado ao nó, o pod irá continuar rodando neste nó por 3600 segundos e depois será expulso. Se o taint for removido antes desse tempo acabar, o pod não será expulso.
Exemplos de Casos de Uso
Taints e tolerâncias são um modo flexível de conduzir pods para fora dos nós ou expulsar pods que não deveriam estar sendo executados. Alguns casos de uso são
-
Nós Dedicados: Se você quiser dedicar um conjunto de nós para uso exclusivo de um conjunto específico de usuários, poderá adicionar um taint nesses nós. (digamos, kubectl taint nodes nodename dedicated=groupName:NoSchedule
) e em seguida adicionar uma tolerância correspondente para seus pods (isso seria feito mais facilmente com a escrita de um controlador de admissão customizado).
Os pods com tolerância terão sua execução permitida nos nós com taints (dedicados), assim como em qualquer outro nó no cluster. Se você quiser dedicar nós a esses pods e garantir que eles usem apenas os nós dedicados, precisará adicionar uma label similar ao taint para o mesmo conjunto de nós (por exemplo, dedicated=groupName
), e o controle de admissão deverá adicionar uma afinidade de nó para exigir que os pods podem ser executados apenas nos nós definidos com a label dedicated=groupName
.
-
Nós com hardware especial: Em um cluster no qual um pequeno grupo de nós possui hardware especializado (por exemplo, GPUs), é desejável manter pods que não necessitem desse tipo de hardware fora desses nós, dessa forma o recurso estará disponível para pods que precisem do hardware especializado. Isso pode ser feito aplicando taints nos nós com o hardware especializado (por exemplo, kubectl taint nodes nodename special=true:NoSchedule
or kubectl taint nodes nodename special=true:PreferNoSchedule
) e aplicando uma tolerância correspondente nos pods que usam o hardware especial. Assim como no caso de uso de nós dedicados, é provavelmente mais fácil aplicar as tolerâncias utilizando um controlador de admissão.
Por exemplo, é recomendado usar Extended Resources para representar hardware especial, adicione um taint ao seus nós de hardware especializado com o nome do recurso estendido e execute o controle de admissão ExtendedResourceToleration. Agora, tendo em vista que os nós estão marcados com um taint, nenhum pod sem a tolerância será executado neles. Porém, quando você submete um pod que requisita o recurso estendido, o controlador de admissão ExtendedResourceToleration
irá adicionar automaticamente as tolerâncias necessárias ao pod que irá, por sua vez, ser alocado no nó com hardware especial. Isso garantirá que esses nós de hardware especial serão dedicados para os pods que requisitarem tal recurso e você não precisará adicionar manualmente as tolerâncias aos seus pods.
-
Expulsões baseadas em Taint: Um comportamento de expulsão configurada por pod quando problemas existem em um nó, o qual será descrito na próxima seção.
Expulsões baseadas em Taint
ESTADO DA FUNCIONALIDADE: Kubernetes v1.18 [stable]
O efeito de taint NoExecute
, mencionado acima, afeta pods que já estão rodando no nó da seguinte forma
- pods que não toleram o taint são expulsos imediatamente
- pods que toleram o taint sem especificar
tolerationSeconds
em sua especificação de tolerância, ficam alocados para sempre
- pods que toleram o taint com um
tolerationSeconds
especificado, permanecem alocados pela quantidade de tempo definida
O controlador de nó automaticamente adiciona um taint ao Nó quando certas condições se tornam verdadeiras. Os seguintes taints são embutidos:
node.kubernetes.io/not-ready
: Nó não está pronto. Isso corresponde ao NodeCondition Ready
com o valor "False
".
node.kubernetes.io/unreachable
: Nó é inalcançável a partir do controlador de nó. Isso corresponde ao NodeCondition Ready
com o valor "Unknown
".
node.kubernetes.io/memory-pressure
: Nó possui pressão de memória.
node.kubernetes.io/disk-pressure
: Nó possui pressão de disco.
node.kubernetes.io/pid-pressure
: Nó possui pressão de PID.
node.kubernetes.io/network-unavailable
: A rede do nó está indisponível.
node.kubernetes.io/unschedulable
: Nó não é alocável.
node.cloudprovider.kubernetes.io/uninitialized
: Quando o kubelet é iniciado com um provedor de nuvem "externo", esse taint é adicionado ao nó para que ele seja marcado como não utilizável. Após o controlador do cloud-controller-manager inicializar o nó, o kubelet remove esse taint.
No caso de um nó estar prestes a ser expulso, o controlador de nó ou kubelet adicionam os taints relevantes com o efeito NoExecute
. Se a condição de falha retorna ao normal, o kubelet ou controlador de nó podem remover esses taints.
Nota: A camada de gerenciamento limita a taxa de adição de novos taints aos nós. Esse limite gerencia o número de expulsões que são disparadas quando muitos nós se tornam inalcançáveis ao mesmo tempo (por exemplo: se ocorre uma falha na rede).
Você pode especificar tolerationSeconds
em um Pod para definir quanto tempo ele ficará alocado em um nó que está falhando ou está sem resposta.
Por exemplo, você talvez queira manter uma aplicação com vários estados salvos localmente alocado em um nó por um longo período na ocorrência de uma divisão na rede, esperando que essa divisão se recuperará e assim a expulsão do pod pode ser evitada.
A tolerância que você define para esse Pod poderia ficar assim:
tolerations:
- key: "node.kubernetes.io/unreachable"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 6000
Nota: O Kubernetes automaticamente adiciona uma tolerância para node.kubernetes.io/not-ready
e node.kubernetes.io/unreachable
com tolerationSeconds=300
, a menos que você, ou um controlador, defina essas tolerâncias explicitamente.
Essas tolerâncias adicionadas automaticamente significam que Pods podem continuar alocados aos Nós por 5 minutos após um desses problemas ser detectado.
Pods do tipo DaemonSet são criados com tolerâncias NoExecute
sem a propriedade tolerationSeconds
para os seguintes taints:
node.kubernetes.io/unreachable
node.kubernetes.io/not-ready
Isso garante que esses pods do DaemonSet nunca sejam expulsos por conta desses problemas.
Taints por condições de nó
A camada de gerenciamento, usando o controlador do nó, cria taints automaticamente com o efeito NoSchedule
para condições de nó.
O agendador verifica taints, não condições de nó, quando realiza suas decisões de agendamento. Isso garante que as condições de nó não afetem diretamente o agendamento.
Por exemplo, se a condição de nó DiskPressure
está ativa, a camada de gerenciamento adiciona o taint node.kubernetes.io/disk-pressure
e não aloca novos pods no nó afetado. Se a condição MemoryPressure
está ativa, a camada de gerenciamento adiciona o taint node.kubernetes.io/memory-pressure
.
Você pode ignorar condições de nó para pods recém-criados adicionando tolerâncias correspondentes. A camada de controle também adiciona a tolerância node.kubernetes.io/memory-pressure
em pods que possuem uma classe de QoS diferente de BestEffort
. Isso ocorre porque o Kubernetes trata pods nas classes de QoS Guaranteed
ou Burstable
(até mesmo pods sem requisitos de memória definidos) como se fossem capazes de lidar com pressão de memória, enquanto novos pods com BestEffort
não são alocados no nó afetado.
O controlador DaemonSet adiciona automaticamente as seguintes tolerâncias de NoSchedule
para todos os daemons, prevenindo que DaemonSets quebrem.
node.kubernetes.io/memory-pressure
node.kubernetes.io/disk-pressure
node.kubernetes.io/pid-pressure
(1.14 ou superior)
node.kubernetes.io/unschedulable
(1.10 ou superior)
node.kubernetes.io/network-unavailable
(somente rede do host)
Adicionando essas tolerâncias garante retro compatibilidade. Você também pode adicionar tolerâncias de forma arbitrária aos DaemonSets.
Próximos passos
2 - Escalonador do Kubernetes
No Kubernetes, escalonamento refere-se a garantir que os Pods
sejam correspondidos aos Nodes para que o
Kubelet possa executá-los.
Visão geral do Escalonamento
Um escalonador observa Pods recém-criados que não possuem um Node atribuído.
Para cada Pod que o escalonador descobre, ele se torna responsável por
encontrar o melhor Node para execução do Pod. O escalonador chega a essa decisão de alocação levando em consideração os princípios de programação descritos abaixo.
Se você quiser entender por que os Pods são alocados em um Node específico
ou se planeja implementar um escalonador personalizado, esta página ajudará você a
aprender sobre escalonamento.
kube-scheduler
kube-scheduler
é o escalonador padrão do Kubernetes e é executado como parte do
control plane.
O kube-scheduler é projetado para que, se você quiser e precisar, possa
escrever seu próprio componente de escalonamento e usá-lo.
Para cada Pod recém-criado ou outros Pods não escalonados, o kube-scheduler
seleciona um Node ideal para execução. No entanto, todos os contêineres nos Pods
têm requisitos diferentes de recursos e cada Pod também possui requisitos diferentes.
Portanto, os Nodes existentes precisam ser filtrados de acordo com os requisitos de
escalonamento específicos.
Em um cluster, Nodes que atendem aos requisitos de escalonamento para um Pod
são chamados de Nodes viáveis. Se nenhum dos Nodes for adequado, o Pod
permanece não escalonado até que o escalonador possa alocá-lo.
O escalonador encontra Nodes viáveis para um Pod e, em seguida, executa um conjunto de
funções para pontuar os Nodes viáveis e escolhe um Node com a maior
pontuação entre os possíveis para executar o Pod. O escalonador então notifica
o servidor da API sobre essa decisão em um processo chamado binding.
Fatores que precisam ser levados em consideração para decisões de escalonamento incluem
requisitos individuais e coletivos de recursos,
restrições de hardware / software / política, especificações de afinidade e anti-afinidade,
localidade de dados, interferência entre cargas de trabalho e assim por diante.
Seleção do Node no kube-scheduler
O kube-scheduler seleciona um Node para o Pod em uma operação que consiste em duas etapas:
- Filtragem
- Pontuação
A etapa de filtragem localiza o conjunto de Nodes onde é possível
alocar o Pod. Por exemplo, o filtro PodFitsResources verifica se um Node
candidato possui recursos disponíveis suficientes para atender às solicitações
de recursos específicas de um Pod. Após esta etapa, a lista de Nodes contém
quaisquer Nodes adequados; frequentemente, haverá mais de um. Se a lista estiver vazia,
esse Pod (ainda) não é escalonável.
Na etapa de pontuação, o escalonador classifica os Nodes restantes para escolher
o mais adequado. O escalonador atribui uma pontuação a cada Node
que sobreviveu à filtragem, baseando essa pontuação nas regras de pontuação ativa.
Por fim, o kube-scheduler atribui o Pod ao Node com a classificação mais alta.
Se houver mais de um Node com pontuações iguais, o kube-scheduler seleciona
um deles aleatoriamente.
Existem duas maneiras suportadas de configurar o comportamento de filtragem e pontuação
do escalonador:
-
Políticas de Escalonamento permitem configurar Predicados para filtragem e Prioridades para pontuação.
-
Perfis de Escalonamento permitem configurar Plugins que implementam diferentes estágios de escalonamento, incluindo: QueueSort
, Filter
, Score
, Bind
, Reserve
, Permit
, e outros. Você também pode configurar o kube-scheduler para executar diferentes perfis.
Próximos passos
3 - Sobrecarga de Pod
ESTADO DA FUNCIONALIDADE: Kubernetes v1.18 [beta]
Quando você executa um Pod num nó, o próprio Pod usa uma quantidade de recursos do sistema. Estes
recursos são adicionais aos recursos necessários para executar o(s) contêiner(s) dentro do Pod.
Sobrecarga de Pod, do inglês Pod Overhead, é uma funcionalidade que serve para contabilizar os recursos consumidos pela
infraestrutura do Pod para além das solicitações e limites do contêiner.
No Kubernetes, a sobrecarga de Pods é definido no tempo de
admissão
de acordo com a sobrecarga associada à
RuntimeClass do Pod.
Quando é ativada a Sobrecarga de Pod, a sobrecarga é considerada adicionalmente à soma das
solicitações de recursos do contêiner ao agendar um Pod. Semelhantemente, o kubelet
incluirá a sobrecarga do Pod ao dimensionar o cgroup do Pod e ao
executar a classificação de prioridade de migração do Pod em caso de drain do Node.
Habilitando a Sobrecarga de Pod
Terá de garantir que o Feature Gate
PodOverhead
esteja ativo (está ativo por padrão a partir da versão 1.18)
em todo o cluster, e uma RuntimeClass
utilizada que defina o campo overhead
.
Exemplo de uso
Para usar a funcionalidade PodOverhead, é necessário uma RuntimeClass que define o campo overhead
.
Por exemplo, poderia usar a definição da RuntimeClass abaixo com um agente de execução de contêiner virtualizado
que use cerca de 120MiB por Pod para a máquina virtual e o sistema operacional convidado:
---
kind: RuntimeClass
apiVersion: node.k8s.io/v1beta1
metadata:
name: kata-fc
handler: kata-fc
overhead:
podFixed:
memory: "120Mi"
cpu: "250m"
As cargas de trabalho que são criadas e que especificam o manipulador RuntimeClass kata-fc
irão
usar a sobrecarga de memória e cpu em conta para os cálculos da quota de recursos, agendamento de nós,
assim como dimensionamento do cgroup do Pod.
Considere executar a seguinte carga de trabalho de exemplo, test-pod:
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
runtimeClassName: kata-fc
containers:
- name: busybox-ctr
image: busybox
stdin: true
tty: true
resources:
limits:
cpu: 500m
memory: 100Mi
- name: nginx-ctr
image: nginx
resources:
limits:
cpu: 1500m
memory: 100Mi
No tempo de admissão o controlador de admissão RuntimeClass
atualiza o PodSpec da carga de trabalho de forma a incluir o overhead
como descrito na RuntimeClass. Se o PodSpec já tiver este campo definido
o Pod será rejeitado. No exemplo dado, como apenas o nome do RuntimeClass é especificado, o controlador de admissão muda o Pod de forma a
incluir um overhead
.
Depois do controlador de admissão RuntimeClass, pode verificar o PodSpec atualizado:
kubectl get pod test-pod -o jsonpath='{.spec.overhead}'
A saída é:
map[cpu:250m memory:120Mi]
Se for definido um ResourceQuota, a soma das requisições dos contêineres assim como o campo overhead
são contados.
Quando o kube-scheduler está decidindo que nó deve executar um novo Pod, o agendador considera o overhead
do pod,
assim como a soma de pedidos aos contêineres para esse Pod. Para este exemplo, o agendador adiciona as requisições e a sobrecarga, depois procura um nó com 2.25 CPU e 320 MiB de memória disponível.
Assim que um Pod é agendado a um nó, o kubelet nesse nó cria um novo cgroup
para o Pod. É dentro deste Pod que o agente de execução de contêiners subjacente vai criar contêineres.
Se o recurso tiver um limite definido para cada contêiner (QoS garantida ou Burstrable QoS com limites definidos),
o kubelet definirá um limite superior para o cgroup do Pod associado a esse recurso (cpu.cfs_quota_us para CPU
e memory.limit_in_bytes de memória). Este limite superior é baseado na soma dos limites do contêiner mais o overhead
definido no PodSpec.
Para CPU, se o Pod for QoS garantida ou Burstrable QoS, o kubelet vai definir cpu.shares
baseado na soma dos
pedidos ao contêiner mais o overhead
definido no PodSpec.
Olhando para o nosso exemplo, verifique as requisições ao contêiner para a carga de trabalho:
kubectl get pod test-pod -o jsonpath='{.spec.containers[*].resources.limits}'
O total de requisições ao contêiner são 2000m CPU e 200MiB de memória:
map[cpu: 500m memory:100Mi] map[cpu:1500m memory:100Mi]
Verifique isto comparado ao que é observado pelo nó:
kubectl describe node | grep test-pod -B2
A saída mostra que 2250m CPU e 320MiB de memória são solicitados, que inclui PodOverhead:
Namespace Name CPU Requests CPU Limits Memory Requests Memory Limits AGE
--------- ---- ------------ ---------- --------------- ------------- ---
default test-pod 2250m (56%) 2250m (56%) 320Mi (1%) 320Mi (1%) 36m
Verificar os limites cgroup do Pod
Verifique os cgroups de memória do Pod no nó onde a carga de trabalho está em execução. No seguinte exemplo, crictl
é usado no nó, que fornece uma CLI para agentes de execução compatíveis com CRI. Isto é um
exemplo avançado para mostrar o comportamento do PodOverhead, e não é esperado que os usuários precisem verificar
cgroups diretamente no nó.
Primeiro, no nó em particular, determine o identificador do Pod:
# Execute no nó onde o Pod está agendado
POD_ID="$(sudo crictl pods --name test-pod -q)"
A partir disto, pode determinar o caminho do cgroup para o Pod:
# Execute no nó onde o Pod está agendado
sudo crictl inspectp -o=json $POD_ID | grep cgroupsPath
O caminho do cgroup resultante inclui o contêiner pause
do Pod. O cgroup no nível do Pod está um diretório acima.
"cgroupsPath": "/kubepods/podd7f4b509-cf94-4951-9417-d1087c92a5b2/7ccf55aee35dd16aca4189c952d83487297f3cd760f1bbf09620e206e7d0c27a"
Neste caso especifico, o caminho do cgroup do Pod é kubepods/podd7f4b509-cf94-4951-9417-d1087c92a5b2
. Verifique a configuração cgroup de nível do Pod para a memória:
# Execute no nó onde o Pod está agendado
# Mude também o nome do cgroup para combinar com o cgroup alocado ao Pod.
cat /sys/fs/cgroup/memory/kubepods/podd7f4b509-cf94-4951-9417-d1087c92a5b2/memory.limit_in_bytes
Isto é 320 MiB, como esperado:
335544320
Observabilidade
Uma métrica kube_pod_overhead
está disponível em kube-state-metrics
para ajudar a identificar quando o PodOverhead está sendo utilizado e para ajudar a observar a estabilidade das cargas de trabalho
em execução com uma sobrecarga (Overhead) definida. Esta funcionalidade não está disponível na versão 1.9 do kube-state-metrics,
mas é esperado em uma próxima versão. Os usuários necessitarão entretanto construir o kube-state-metrics a partir do código fonte.
Próximos passos