Dominando Solicitações e Limites de Recursos do Kubernetes para Desempenho Máximo

Aprenda as diferenças cruciais entre Solicitações e Limites de Recursos do Kubernetes para CPU e Memória. Este guia explica como essas configurações determinam as classes de Qualidade de Serviço (QoS) (Garantida, Explosiva, Melhor Esforço), previnem instabilidade de nós e otimizam a eficiência de agendamento do cluster. Inclui exemplos práticos de YAML e melhores práticas para ajuste de desempenho.

Dominando Solicitações e Limites de Recursos do Kubernetes para Desempenho Máximo

As solicitações e limites de recursos do Kubernetes parecem simples no YAML, mas moldam quase todos os comportamentos de segundo dia em um cluster: onde os pods pousam, quais cargas de trabalho são removidas primeiro, se um aplicativo sofre limitação de CPU sob carga e quanta capacidade sobressalente você pensa que tem. Uma configuração ruim pode fazer um aplicativo saudável parecer quebrado. Uma configuração ausente pode fazer o agendador empacotar pods em um nó até que o primeiro pico de tráfego se transforme em um incidente de vizinho barulhento.

A parte que pega as equipes é que solicitações e limites são usados por sistemas diferentes. Solicitações são principalmente uma promessa de agendamento. Limites são um limite de aplicação. Tratá-los como a mesma coisa leva a resultados estranhos, especialmente com CPU.

Entendendo os Conceitos Principais: Solicitações vs. Limites

No Kubernetes, um contêiner pode definir o consumo esperado de recursos usando resources.requests e resources.limits. Eles não são tecnicamente obrigatórios, a menos que seu cluster use políticas como LimitRange ou controles de admissão, mas cargas de trabalho de produção geralmente devem definir pelo menos solicitações para CPU e memória. Sem solicitações, o agendador tem pouca informação útil e o pod cai em uma postura de qualidade de serviço mais fraca.

1. Solicitações de Recursos (requests)

Solicitações representam a quantidade de recursos que um contêiner tem garantido receber no momento do agendamento. Esta é a quantidade mínima de recursos que o kube-scheduler usa ao decidir em qual nó colocar um Pod.

  • Agendamento: Um nó deve ter recursos alocáveis disponíveis suficientes que satisfaçam a soma de todas as solicitações de Pod antes que um novo Pod possa ser agendado lá.
  • Prioridade em tempo de execução: Solicitações influenciam as cotas de CPU e as decisões de remoção de memória. Elas não são uma reserva mágica que impede qualquer lentidão, mas fornecem ao Kubernetes e ao kernel melhores informações durante a contenção.

2. Limites de Recursos (limits)

Limites definem a quantidade máxima de recursos que um contêiner pode consumir. Exceder esses limites resulta em comportamentos específicos e definidos para CPU e Memória.

  • Limites de CPU: Se um contêiner tentar usar mais CPU do que seu limite, os cgroups do kernel Linux irão limitar seu uso, impedindo-o de consumir mais ciclos.
  • Limites de Memória: Se um contêiner exceder seu limite de memória, o kernel pode encerrar um processo no contêiner. O Kubernetes relata isso como OOMKilled quando essa é a razão de término registrada.

Comportamento de CPU vs. Memória

É crucial entender a diferença qualitativa em como o Kubernetes aplica os limites de CPU versus Memória:

Recurso Comportamento ao Exceder o Limite Mecanismo de Aplicação
CPU Limitado (desacelerado) cgroups (controle de largura de banda da CPU)
Memória Encerrado (OOMKill) Killer OOM do Kernel

Cuidado prático: Limites de CPU podem proteger um nó de um processo descontrolado, mas também podem criar problemas de latência quando definidos muito baixos. Muitas equipes de plataforma definem limites de memória de forma consistente e são mais seletivas com limites de CPU para serviços sensíveis à latência, dependendo de sua tolerância ao risco e política do cluster.

Definindo Recursos em Especificações de Pod

Os recursos são definidos dentro do bloco spec.containers[*].resources. As quantidades são especificadas usando sufixos padrão do Kubernetes (por exemplo, m para mili-CPU, Mi para Mebibytes).

Definições de Unidade de CPU

  • 1 unidade de CPU equivale a 1 núcleo completo (ou vCPU em provedores de nuvem).
  • 1000m (milifios) equivale a 1 unidade de CPU.

Definições de Unidade de Memória

  • Mi (Mebibytes) ou Gi (Gibibytes) são comuns.
  • 1024Mi = 1Gi.

Exemplo de Configuração YAML

Considere um contêiner que requer um mínimo garantido de 500m de CPU e 256Mi de memória, mas nunca deve exceder 1 CPU e 512Mi:

resources:
  requests:
    memory: "256Mi"
    cpu: "500m"
  limits:
    memory: "512Mi"
    cpu: "1"

Os números devem vir do comportamento observado, não de suposições. Para uma pequena API HTTP, uma solicitação inicial pode ser baseada no uso normal p50 ou p90 durante o tráfego comercial, depois ajustada após testes de carga. Para um serviço JVM, a memória precisa incluir heap, metaspace, memória nativa, pilhas de threads, buffers diretos e sobrecarga de sidecar. Para um job em lote, o pico de memória durante a maior entrada pode importar mais do que a memória média.

Classes de Qualidade de Serviço (QoS)

A relação entre Solicitações e Limites determina a classe de Qualidade de Serviço (QoS) atribuída a um Pod. Esta classe dita a prioridade do Pod quando os recursos se tornam escassos e o nó precisa recuperar memória (remoção).

O Kubernetes define três classes de QoS:

1. Garantida (Guaranteed)

Definição: Todos os contêineres no Pod devem ter Solicitações e Limites idênticos e não nulos para ambos CPU e Memória.

  • Benefício: Esses Pods são os últimos a serem removidos durante a pressão de recursos, garantindo máxima estabilidade.
  • Caso de Uso: Componentes críticos do sistema ou bancos de dados que exigem isolamento de desempenho estrito.

2. Explosiva (Burstable)

Definição: Pelo menos um contêiner no Pod tem Solicitações definidas, mas ou Solicitações e Limites não são iguais para todos os contêineres, ou alguns recursos não são limitados (embora definir limites seja altamente recomendado).

  • Benefício: Permite que os contêineres ultrapassem suas solicitações, utilizando capacidade não utilizada no nó, até seus limites definidos.
  • Prioridade de Remoção: Removidos antes dos Pods BestEffort, mas depois dos Pods Garantidos.
  • Caso de Uso: A maioria dos aplicativos stateless padrão onde uma ligeira variação na latência é aceitável.

3. Melhor Esforço (BestEffort)

Definição: O Pod não tem Solicitações ou Limites definidos para nenhum contêiner.

  • Benefício: Nenhum, além da simplicidade.
  • Risco: Esses Pods são os primeiros candidatos à remoção quando o nó experimenta pressão de memória. Eles também podem competir mal por CPU porque nenhuma solicitação foi declarada.
  • Caso de Uso: Jobs em lote não críticos ou agentes de log que podem ser facilmente reiniciados.

Estratégias Práticas de Otimização

O gerenciamento eficaz de recursos requer medição, iteração e planejamento cuidadoso.

Estratégia 1: Meça e Defina Solicitações com Precisão

As solicitações devem refletir a quantidade de recursos que o aplicativo precisa para funcionar de forma aceitável na maior parte do tempo. Se você definir solicitações muito altas, desperdiça capacidade do cluster porque o agendador trata essa capacidade como já comprometida. Se você defini-las muito baixas, o agendador pode colocar muitos pods em um nó, e a carga de trabalho pode ser mais propensa a sofrer durante a contenção.

Use ferramentas de monitoramento como Prometheus e Grafana para comparar os valores de solicitação com o uso real. Um ponto de partida comum é observar vários dias de tráfego normal, ignorar incidentes pontuais óbvios e definir solicitações próximas a um percentil sustentado, em vez do pico único mais alto. O percentil exato é uma escolha de política; o ponto principal é usar dados e revisá-los.

Por exemplo, se um serviço normalmente usa 180m de CPU, atinge pico em torno de 450m durante o aquecimento de implantação e tem picos raros para 900m durante uma tarefa em lote conhecida, definir uma solicitação de 900m pode desperdiçar capacidade o dia todo. Definir uma solicitação de 50m pode tornar o pod barato de agendar, mas instável sob contenção. Uma solicitação na faixa normal sustentada, mais tratamento separado para o caminho do lote, é muitas vezes uma conversa melhor.

Estratégia 2: Defina Limites Conservadores

Os limites atuam como um limite de segurança, mas não são gratuitos. Para memória, um limite ligeiramente acima do pico de uso medido pode impedir que um contêiner consuma o nó. Para CPU, um limite impede que um processo descontrolado use CPU ilimitada, mas limites agressivos podem limitar um serviço mesmo quando o nó tem núcleos ociosos.

Aviso sobre Limites de CPU: Definir limites de CPU abaixo da demanda real pode causar latência visível através da limitação. A QoS Explosiva é uma opção razoável para muitos serviços stateless, enquanto a QoS Garantida é melhor reservada para cargas de trabalho onde a compensação de isolamento é intencional.

Estratégia 3: Aproveitando o Vertical Pod Autoscaler (VPA)

Ajustar manualmente os recursos é difícil e demorado. O Vertical Pod Autoscaler (VPA) monitora o uso em tempo de execução e pode recomendar ou atualizar solicitações de recursos, dependendo do seu modo. Em muitas configurações, o VPA é usado primeiro no modo de recomendação para que as equipes possam revisar as solicitações sugeridas antes de permitir atualizações automáticas.

Tenha cuidado ao combinar VPA com o Horizontal Pod Autoscaler. O HPA geralmente escala com base na utilização relativa às solicitações, portanto, alterar as solicitações pode alterar o comportamento de escalonamento. Pode funcionar bem, mas deve ser testado deliberadamente.

Estratégia 4: Cotas de Recursos para Namespaces

Para evitar o monopólio de recursos entre equipes ou ambientes, os administradores devem usar Cotas de Recursos no nível do Namespace. Um ResourceQuota impõe limites agregados na quantidade total de Solicitações e Limites de CPU/Memória que podem existir dentro desse namespace, garantindo justiça.

Exemplo de Cota de Namespace

apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-quota
  namespace: development
spec:
  hard:
    requests.cpu: "10"
    limits.memory: "20Gi"

Isso garante que a CPU total solicitada em todos os Pods no namespace development não pode exceder 10 núcleos, e os limites totais de memória não podem exceder 20Gi.

Como Configurações Ruins Aparecem em Clusters Reais

Erros de recursos raramente se anunciam como "erros de recursos". Eles geralmente parecem incidentes de aplicação.

Se os limites de CPU são muito baixos, você pode ver alta latência de requisição enquanto os gráficos de uso de CPU parecem limitados. O contêiner quer mais CPU, mas a limitação do cgroup o segura. Em configurações baseadas em Prometheus, métricas como container_cpu_cfs_throttled_periods_total e container_cpu_cfs_periods_total podem ajudar a mostrar se a limitação faz parte da história.

Se os limites de memória são muito baixos, o pod pode reiniciar com Reason: OOMKilled. Os logs da aplicação podem terminar abruptamente porque o processo não teve um desligamento gracioso. kubectl describe pod geralmente conta a verdade mais rápido que o log da aplicação neste caso.

Se as solicitações são muito altas, os pods podem ficar em Pending mesmo que os painéis mostrem uso médio do nó baixo. O agendador não coloca pods com base no uso real médio; ele compara recursos solicitados com recursos alocáveis. É por isso que um cluster pode parecer subutilizado e ainda assim recusar um novo pod.

Se as solicitações estão faltando, a carga de trabalho pode parecer bem durante períodos calmos e depois se tornar a primeira coisa a ser comprimida quando o nó está ocupado. Isso pode ser aceitável para jobs descartáveis, mas é um padrão ruim para serviços voltados ao usuário.

Um Fluxo de Trabalho de Ajuste Mais Seguro

Um loop de ajuste prático se parece com isso:

  1. Comece com solicitações explícitas de CPU e memória para cada contêiner de produção, incluindo sidecars.
  2. Defina limites de memória com base no pico observado mais margem de segurança, depois observe as reinicializações OOMKilled.
  3. Decida se os limites de CPU são necessários por política ou risco da carga de trabalho. Se você os usar, monitore a limitação.
  4. Compare a CPU e memória solicitadas com o uso real semanalmente ou mensalmente, especialmente após grandes lançamentos.
  5. Trate o redimensionamento como gerenciamento de mudanças. Uma solicitação mais baixa pode aumentar a densidade de empacotamento, mas também pode alterar o comportamento de falha durante a pressão do nó.

Sidecars merecem atenção. Um proxy de malha de serviço, coletor de logs ou agente de segurança pode consumir CPU ou memória suficiente para alterar a pegada real do pod. Se apenas o contêiner principal do aplicativo for ajustado, o pod ainda pode ser mal representado para o agendador.

Exemplo: Corrigindo um Pico de Latência Causado por Limitação de CPU

Imagine um contêiner de API com esta configuração:

resources:
  requests:
    cpu: "100m"
    memory: "256Mi"
  limits:
    cpu: "200m"
    memory: "512Mi"

Durante um evento de venda, a latência aumenta. O nó ainda tem CPU ociosa, mas o contêiner está limitado a 200m. Aumentar as réplicas pode ajudar, mas cada pod ainda está individualmente limitado. Uma correção melhor pode ser aumentar ou remover o limite de CPU, aumentar a solicitação para corresponder à demanda sustentada normal e usar HPA para que o serviço escale antes que a latência fique feia.

A lição importante é que o baixo uso de CPU no gráfico nem sempre significa que o aplicativo está ocioso. Pode significar que o aplicativo não tem permissão para usar mais.

Verificação Final

Solicitações dizem ao Kubernetes como colocar e priorizar o pod. Limites dizem ao kernel onde pará-lo. Bons valores vêm de métricas reais, testes de carga e uma decisão clara sobre o que importa mais para cada carga de trabalho: densidade, isolamento, latência ou custo. Revisite os valores após mudanças de tráfego, mudanças de dependência e atualizações de runtime. Configurações de recursos desatualizadas são uma das maneiras mais silenciosas de um cluster derivar para um desempenho ruim.