Requests, Limits, Classes de QoS e OOMKilled
Objective
Todo container pode declarar requests (o que o scheduler reserva para ele) e limits (o que o kernel deixa ele usar). Os dois são aplicados por partes completamente diferentes do sistema, e memória e CPU se comportam de forma diferente no limite: passar do limit de memória mata o processo (OOMKilled), passar do limit de CPU só o deixa mais lento (throttling). A combinação de requests e limits também coloca cada Pod numa classe de QoS, que decide quem sofre eviction primeiro quando o nó fica sem recursos. Este conceito cobre os três, com a assinatura de OOMKilled que você vai ver na prática.
Use Cases
- Um serviço reiniciando com exit code 137 e
Reason: OOMKilled. - Picos de latência num serviço cujo uso de CPU "nunca chega a 100%".
- Proteger um banco de dados de sofrer eviction antes de Pods menos importantes.
- Impor padrões sensatos num namespace onde os desenvolvedores esquecem de definir resources.
Deep Dive
Requests e limits
plaintextresources: requests: cpu: 250m # um quarto de core reservado para o agendamento memory: 512Mi limits: memory: 768Mi # teto rígido: acima disso, o kernel mata o processo # limit de CPU omitido de propósito, veja abaixo
- Requests são um contrato de agendamento. O scheduler só coloca o Pod num nó cujo allocatable menos os requests dos outros Pods os cubra. Em execução, o request de CPU vira um peso de cgroup: sob disputa, a CPU é dividida proporcionalmente aos requests.
- Limits são aplicados pelo kernel Linux através de cgroups no nó, independentemente do scheduler.
- Unidades: CPU em cores ou millicores (
500m= meio core); memória em bytes com sufixos binários (Mi,Gi).512M(decimal) e512Mi(binário) diferem em cerca de 5%.
Limit de memória: o processo morre
Passar do limit de memória não tem negociação. O OOM killer do kernel encerra o processo no container, e o kubelet reporta. Verificado no k3s com um limit de 64 Mi e um container que aloca 300 MiB:
plaintext$ kubectl get pod oom NAME READY STATUS RESTARTS AGE oom 0/1 OOMKilled 3 (26s ago) 45s $ kubectl get pod oom -o jsonpath='{.status.containerStatuses[0].lastState}' {"terminated":{"exitCode":137,"reason":"OOMKilled", ...}}
Exit code 137 = 128 + 9 (SIGKILL). O kubectl describe pod mostra o mesmo em Last State: Terminated, Reason: OOMKilled. Entre os restarts o Pod mostra CrashLoopBackOff.
Um container OOMKilled usou mais memória do que o limit. A correção é um limit maior ou menos uso de memória. Runtimes com gerenciamento de memória próprio (JVM, Node.js, .NET) leem o limit do container e dimensionam o heap a partir dele; um heap configurado perto demais do limit não deixa espaço para o resto do processo e termina em OOMKilled em vez de um erro de falta de memória dentro do processo.
Não confunda com eviction: quando o próprio nó fica com pouca memória, o kubelet faz eviction de Pods inteiros (Status: Failed, Reason: Evicted), até de Pods abaixo dos seus limits.
Limit de CPU: o processo fica lento
Um limit de CPU é aplicado por cota do CFS: o container recebe limit x 100ms de tempo de CPU a cada período de 100 ms. Um processo com várias threads e limit de 500m pode gastar os seus 50 ms de cota nos primeiros 10 ms de um período em várias threads e depois ficar throttled por 90 ms. O uso médio de CPU parece baixo, mas as requisições travam. A métrica container_cpu_cfs_throttled_periods_total no Prometheus mostra isso.
É por isso que muitos times definem requests de CPU sempre e limits de CPU raramente: o request garante uma fatia justa sob disputa, e sem limit o serviço pode usar CPU ociosa em picos como a inicialização ou o aquecimento de cache. Limits de memória, ao contrário, devem ser sempre definidos, porque memória não é compressível.
Classes de QoS
O Kubernetes deriva a classe dos resources de todos os containers do Pod:
| Classe | Regra | Prioridade de eviction |
|---|---|---|
Guaranteed |
todo container tem requests de CPU e memória iguais aos limits | sai por último |
Burstable |
pelo menos um request ou limit definido, mas não é Guaranteed | no meio |
BestEffort |
nenhum request e nenhum limit | sai primeiro |
Verificado: um Pod com request = limit = 1Gi de memória, mas request de CPU 500m e limit de CPU 2, é Burstable (os valores de CPU diferem); um Pod só com limit de memória também é Burstable (o request assume o valor do limit quando só o limit é informado); um Pod sem nada definido é BestEffort.
plaintextkubectl get pods -o custom-columns=NAME:.metadata.name,QOS:.status.qosClass
Sob pressão de memória no nó, o kubelet faz eviction primeiro dos Pods BestEffort, depois dos Burstable que mais usam memória acima do request. Isso também afeta o OOM score do kernel: Pods Guaranteed são os últimos candidatos quando o nó inteiro fica sem memória.
Para um banco de dados num nó compartilhado, Guaranteed é a classe a buscar, e isso exige limits de CPU iguais aos requests. Esse é um lugar legítimo para aceitar um limit de CPU.
Padrões por namespace: LimitRange
Pods sem resources são BestEffort e invisíveis para o scheduler. Uma LimitRange preenche padrões e impõe limites:
plaintextapiVersion: v1 kind: LimitRange metadata: {name: defaults, namespace: team-a} spec: limits: - type: Container defaultRequest: {cpu: 100m, memory: 256Mi} default: {memory: 512Mi} # limit padrão max: {memory: 4Gi}
Uma ResourceQuota limita o total do namespace (requests.memory: 8Gi), útil quando vários times dividem um nó.
Trade-offs
- Limits de CPU vs nenhum. Limits dão previsibilidade e isolamento entre tenants; também causam throttling que parece latência misteriosa. Num nó que você controla, requests sem limits de CPU são uma escolha comum e defensável, exceto onde você quer
Guaranteed. - Limit de memória = request vs limit > request. Valores iguais significam nenhuma surpresa e nenhum overcommit. Um limit maior deixa um serviço ter picos mas permite overcommit no nó, o que termina em evictions quando vários Pods têm pico ao mesmo tempo.
- Limits apertados economizam capacidade, limits folgados economizam incidentes. Comece generoso, meça com
kubectl tope Prometheus, depois aperte.