Pular para o conteúdo
11 min de leitura

Requests, Limits, QoS e OOMKilled: Governança de Recursos que Separa Cluster Estável de Caos

Compartilhar

Por Equipe Nebular ·

Guia técnico de recursos no Kubernetes: requests x limits, classes de QoS, CPU throttling, OOMKilled e as armadilhas de dimensionar CPU e memória de Pods.

Neste artigo

Se existe um único ajuste que, feito errado, causa mais incidentes silenciosos e frustração em clusters de Kubernetes do que qualquer outro, é a configuração de recursos dos Pods. É um tema aparentemente banal — dois camposinhos, requests e limits, com valores de CPU e memória — e justamente por parecer trivial ele é negligenciado, copiado de exemplo em exemplo sem entendimento, ou simplesmente omitido. As consequências não aparecem no dia do deploy. Elas aparecem semanas depois, na forma de um Pod que é morto misteriosamente com o código OOMKilled, de uma aplicação que fica inexplicavelmente lenta apesar de a CPU do nó parecer ociosa, ou de um nó inteiro que despenca porque um único Pod ganancioso consumiu toda a memória e derrubou os vizinhos junto.

Entender recursos no Kubernetes é entender como o scheduler decide onde colocar Pods, como o kernel do Linux divide CPU e memória entre containers, e o que exatamente acontece quando um Pod tenta usar mais do que devia. Este guia destrincha requests e limits, as três classes de QoS que governam quem é sacrificado sob pressão, a diferença crucial de como CPU e memória são tratadas, e as armadilhas — throttling invisível e OOMKill — que atormentam quem dimensiona recursos no chute.

Requests e limits não são a mesma coisa

O primeiro passo é internalizar que requests e limits respondem a perguntas diferentes, dirigidas a atores diferentes.

O request é uma promessa de reserva, dirigida ao scheduler. Quando você declara requests: { cpu: 250m, memory: 256Mi }, você está dizendo: "este container precisa de pelo menos um quarto de CPU e 256 mebibytes de memória garantidos". O scheduler usa esse número para decidir em qual nó o Pod cabe — ele soma os requests de tudo que já roda num nó e só coloca o novo Pod ali se ainda houver request disponível. O request é sobre agendamento e garantia mínima. Um nó com 4 CPUs de capacidade não aceita Pods cujos requests somados passem de 4 CPUs, mesmo que na prática ninguém esteja usando tudo. Request é o que o nó reserva, não necessariamente o que se usa.

O limit é um teto rígido, imposto pelo kernel em tempo de execução. Quando você declara limits: { cpu: 500m, memory: 512Mi }, você está dizendo: "este container nunca pode passar de meia CPU nem de 512 mebibytes". O kernel do Linux, através dos cgroups, impõe esse teto — e aqui mora a diferença mais importante de todo o tema, porque CPU e memória são tratadas de formas completamente distintas quando o limite é atingido.

CPU é compressível; memória não é

Essa única frase explica metade dos incidentes de recursos. CPU é um recurso compressível: você pode dar menos dela a um processo sem matá-lo — ele só fica mais lento. Memória é um recurso incompressível: você não pode dar menos memória a um processo que já a alocou; ou ele tem a memória, ou ele morre. Essa assimetria física define como o Kubernetes reage ao estouro de limite em cada caso.

Quando um container atinge seu limit de CPU, o kernel não o mata — ele o estrangula (CPU throttling). O processo simplesmente recebe menos fatias de tempo de CPU do que gostaria, e portanto executa mais devagar. Nada quebra visivelmente; a aplicação só fica lenta. E é por isso que o throttling é tão traiçoeiro: você olha o dashboard, vê o nó com CPU sobrando, vê a aplicação lenta, e não entende — porque o gargalo não é a CPU do nó, é o limite artificial que você impôs ao container, que está sendo respeitado à risca pelo kernel. A latência sobe, os p99 explodem, e a causa está invisível num campo de configuração.

Quando um container atinge seu limit de memória, a história é brutalmente diferente. Não há como "ir mais devagar" com memória. O kernel invoca o OOM Killer (Out Of Memory), que mata o processo. O container morre com o status OOMKilled, e o Kubernetes o reinicia. Se o problema persistir — se a aplicação genuinamente precisa de mais memória do que o limite —, o Pod entra em CrashLoopBackOff, morrendo e reiniciando sem parar. O OOMKilled é o sintoma mais direto de um limite de memória mal dimensionado, e ao contrário do throttling, ele é ruidoso: o Pod reinicia, os alertas disparam. Mas a causa raiz — "o limite era baixo demais" ou "a aplicação vaza memória" — ainda exige investigação.

As três classes de QoS

Aqui as coisas se juntam. A combinação de como você define requests e limits determina automaticamente a classe de Quality of Service (QoS) do Pod, e a QoS decide quem é sacrificado primeiro quando o nó fica sob pressão de memória. São três classes, em ordem de proteção:

  • Guaranteed — a classe mais protegida. O Pod se qualifica quando, para todos os containers, requests é igual a limits, tanto para CPU quanto para memória. Ao pedir exatamente o que se limita, o Pod tem seus recursos plenamente reservados e é o último a ser morto quando o nó precisa liberar memória. É a classe das cargas críticas que não podem ser sacrificadas.
  • Burstable — a classe do meio. O Pod tem requests definidos, mas os limits são maiores que os requests (ou nem todos os containers têm ambos). Ele tem uma garantia mínima (o request) mas pode "estourar" além dela até o limite, se houver folga no nó. É a classe mais comum e razoável para a maioria das aplicações. Sob pressão, é morto depois dos BestEffort mas antes dos Guaranteed.
  • BestEffort — a classe sem rede de proteção. O Pod não define request nem limit para nada. Ele usa o que sobrar, sem garantia alguma, e é o primeiro a ser morto quando o nó fica sem memória. É a classe de quem não configurou recursos — e, não por acaso, a receita para instabilidade.

A lição operacional é direta: cargas críticas devem ser Guaranteed ou Burstable com requests honestos; deixar um Pod importante como BestEffort é entregá-lo como primeiro cordeiro no altar sempre que o nó apertar.

O caso especial do request sem limit

Uma configuração merece destaque porque muitos times experientes a adotam deliberadamente: definir **request de memória e CPU, limit de memória, mas nenhum limit de CPU**. A lógica é elegante. O request garante o mínimo e alimenta o scheduler. O limit de memória protege contra vazamentos que derrubariam o nó (memória é incompressível — você precisa de um teto). Mas omitir o limit de CPU evita o throttling artificial: como CPU é compressível e o kernel já divide a CPU proporcionalmente aos requests sob contenção, o Pod pode usar CPU ociosa do nó quando ela existe, sem ser estrangulado por um teto arbitrário. Ele só é "comprimido" quando há disputa real, e mesmo assim de forma proporcional e justa. Não é uma regra universal — ambientes com necessidade de previsibilidade estrita ou de billing por CPU podem querer limites —, mas é um padrão que vale conhecer, porque ataca diretamente a armadilha do throttling invisível.

Como descobrir os números certos

Definir requests e limits no chute é como calçar sapato sem medir o pé. Os valores devem sair de dados, não de suposição. O caminho é observar o consumo real da aplicação sob carga representativa — as métricas de uso de CPU e memória ao longo do tempo, incluindo picos e não só médias. O request de memória deve cobrir o uso típico com folga; o limit de memória deve dar margem para picos legítimos sem abrigar um vazamento. O request de CPU deve refletir o consumo em operação normal. Ferramentas de recomendação, como as que o Vertical Pod Autoscaler oferece em modo de recomendação, analisam o histórico e sugerem valores — um bom ponto de partida a ser refinado com julgamento. O importante é o princípio: recursos são uma medida empírica, revisada conforme a aplicação e o tráfego mudam, não um número mágico copiado uma vez e esquecido.

LimitRange e ResourceQuota: governança no namespace

Confiar que todo desenvolvedor vai configurar recursos corretamente é otimismo perigoso. O Kubernetes oferece dois mecanismos de governança no nível do namespace. O LimitRange define padrões e limites por container: você pode estabelecer que todo container sem request declarado receba um default sensato, e que ninguém possa pedir mais que um teto ou menos que um piso. Isso elimina os BestEffort acidentais — o container que esqueceu de declarar recursos ganha valores padrão em vez de ficar sem rede. O ResourceQuota age no agregado: ele limita a soma total de requests e limits de todo um namespace, impedindo que um time (ou uma aplicação descontrolada) consuma a capacidade inteira do cluster. Juntos, esses mecanismos transformam "espero que configurem direito" em "o cluster garante limites sãos", e são a ponte natural entre recursos de Pod e a disciplina de custo e capacidade da plataforma.

As armadilhas

A primeira, já anunciada, é o CPU throttling invisível. É o problema mais subdiagnosticado dos recursos, porque não gera crash nem alerta óbvio — só lentidão. A aplicação está devagar, o nó tem CPU sobrando, e a causa é o limit de CPU do container estrangulando-o. O sinal para procurar é a métrica de throttling do cgroup: se ela é alta, seu limit de CPU está apertado demais para os picos da aplicação. A solução costuma ser afrouxar (ou remover) o limit de CPU, conforme discutido.

A segunda é o OOMKilled por limit apertado versus vazamento. Quando um Pod morre com OOMKilled, há dois diagnósticos possíveis e opostos. Ou o limite era genuinamente baixo demais para o uso legítimo da aplicação (conserto: aumentar o limit), ou a aplicação tem um vazamento de memória e vai estourar qualquer limite dado o tempo suficiente (conserto: achar o vazamento, não aumentar o limite). Aumentar o limite de uma aplicação que vaza só adia o crash e desperdiça memória. Distinguir os dois exige olhar o padrão: uso que cresce e estabiliza sugere limite baixo; uso que cresce sem parar até o OOM sugere vazamento.

A terceira é o request inflado que desperdiça o cluster. Se o throttling e o OOM ensinam a não pedir de menos, o excesso de zelo ensina o erro inverso: pedir muito mais do que se usa "por segurança". Como o scheduler reserva o request inteiro, requests inflados fazem o nó parecer cheio enquanto está, na prática, ocioso — Pods ficam Pending por falta de request disponível num cluster que tem recursos reais de sobra. Você paga por capacidade que ninguém usa. Requests devem ser honestos: cobrir o uso real com margem razoável, não com paranoia.

A quarta é o BestEffort em produção. Já dito, mas central: o Pod sem requests nem limits é o primeiro a morrer sob pressão e não dá ao scheduler nenhuma informação para agendá-lo bem. Nenhuma carga que importa deveria ser BestEffort. O LimitRange existe justamente para impedir que isso aconteça por esquecimento.

A quinta é dimensionar pela média e ignorar o pico. Uma aplicação pode ter uso médio baixo e picos altos e curtos. Se você dimensiona pela média, o pico estoura o limite de memória (OOMKill) ou estrangula na CPU. Recursos precisam contemplar o comportamento nos picos legítimos, não só o repouso.

Fechando

A configuração de recursos é o alicerce silencioso da estabilidade de um cluster, e ela recompensa quem a entende de verdade. Requests são reservas dirigidas ao scheduler; limits são tetos impostos pelo kernel — e a assimetria entre CPU compressível (que é estrangulada quando estoura) e memória incompressível (que é morta quando estoura) explica por que os sintomas são tão diferentes: lentidão invisível de um lado, OOMKilled ruidoso do outro. A combinação de requests e limits define a classe de QoS, que decide quem sobrevive à pressão — e nenhuma carga crítica deveria ficar na base dessa hierarquia como BestEffort. Os números certos vêm de observar o consumo real sob carga, incluindo picos, não do chute; e mecanismos como LimitRange e ResourceQuota transformam boas intenções em garantias do cluster. Acerte isso — requests honestos, limits de memória que protegem sem sufocar, cuidado redobrado com o throttling de CPU que ninguém vê — e você elimina de uma vez a categoria de incidente mais comum e mais evitável do Kubernetes: o Pod que morre, ou arrasta, por um número mal escolhido em dois campos que pareciam não importar.

Compartilhar

Compartilhar

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly