Cotas de CPU e RAM no OpenShift OKD

Se você trabalha com OpenShift OKD em ambientes corporativos ou de laboratório, já deve ter se deparado com situações em que um pod monopoliza recursos, derruba workloads vizinhos ou simplesmente não sobe porque o namespace está no limite. O controle de cotas de CPU e RAM no OpenShift OKD é o mecanismo que evita exatamente esses cenários — e entender como ele funciona em profundidade faz diferença entre uma plataforma estável e um cluster caótico.

Neste artigo você vai aprender como funcionam os três objetos que formam a hierarquia de controle de recursos no OpenShift OKD: o ClusterResourceQuota, o ResourceQuota e o LimitRange. Além disso, vamos cobrir a diferença fundamental entre requests e limits, as classes de QoS geradas automaticamente pelo kubelet e os comandos essenciais para operar e inspecionar cotas no dia a dia.

Por que controlar recursos no OpenShift OKD?

Antes de entrar nos objetos Kubernetes, vale entender o problema que as cotas resolvem. Um cluster OpenShift OKD é, na maioria dos casos, um ambiente compartilhado — múltiplas equipes, múltiplos projetos, múltiplos ambientes (dev, staging, prod) convivendo nos mesmos nodes. Sem controle de recursos, um único namespace mal configurado pode:

  • Consumir toda a CPU disponível de um node e causar throttling em pods de produção
  • Alocar memória sem limite e disparar o OOM Killer do kernel em containers críticos
  • Criar centenas de pods e esgotar o capacity de agendamento do scheduler
  • Provisionar dezenas de PersistentVolumeClaims e saturar o storage

O mecanismo de cotas no OpenShift OKD opera em dois momentos distintos: no momento da admissão (quando o objeto é criado via API) e em tempo de execução (quando o container está rodando no node). Essa distinção é central para entender como ResourceQuota e LimitRange se complementam.

A hierarquia de controle de recursos

O OpenShift OKD organiza o controle de recursos em três camadas sobrepostas. Cada camada tem um escopo e uma responsabilidade diferente.

Primeira camada: ClusterResourceQuota

O ClusterResourceQuota é um objeto exclusivo do OpenShift — ele não existe no Kubernetes vanilla. Sua função é aplicar uma cota agregada que abrange múltiplos namespaces simultaneamente, usando um seletor de labels ou de usuário para definir quais namespaces fazem parte do escopo.

Em clusters compartilhados por equipes, o ClusterResourceQuota é o mecanismo ideal para implementar isolamento financeiro e operacional por squad ou por business unit. Ele funciona como uma “conta” global: os namespaces de dev, staging e prod de uma equipe consomem de um budget comum, e o admission controller rejeita qualquer novo pod que ultrapasse o teto — independentemente de qual namespace está tentando criar o recurso.

A estrutura básica do objeto é a seguinte:

apiVersion: quota.openshift.io/v1
kind: ClusterResourceQuota
metadata:
  name: equipe-backend
spec:
  selector:
    labels:
      matchLabels:
        team: backend
  quota:
    hard:
      requests.cpu: "16"
      requests.memory: 32Gi
      limits.cpu: "32"
      limits.memory: 64Gi
      pods: "50"
      persistentvolumeclaims: "20"

O campo selector define quais namespaces entram no escopo. Todos os namespaces que possuem o label team: backend terão seu consumo somado e contabilizado contra os limites definidos em quota.hard. O OpenShift mantém esse contador atualizado em tempo real no status do objeto.

Você também pode selecionar namespaces pelo criador, usando o annotation openshift.io/requester:

spec:
  selector:
    annotations:
      openshift.io/requester: [email protected]

Isso é útil em plataformas de self-service onde cada developer cria seus próprios namespaces via oc new-project. O OpenShift registra automaticamente quem criou o namespace no annotation, então a cota por usuário funciona sem nenhuma intervenção manual de labels.

Quando os dois seletores são usados juntos (labels e annotations), o namespace precisa satisfazer ambos — é um AND lógico.

Inspecionando o ClusterResourceQuota

# Ver estado atual com consumo agregado
oc describe clusterresourcequota equipe-backend

# Output relevante:
# Resource            Used    Hard
# --------            ----    ----
# limits.cpu          28      32
# limits.memory       58Gi    64Gi
# persistentvolumeclaims  15  20
# pods                43      50
# requests.cpu        13      16
# requests.memory     29Gi    32Gi

# Listar namespaces cobertos e contribuição individual
oc describe clusterresourcequota equipe-backend | grep -A 2 "Namespace"

# Exportar o objeto completo
oc get clusterresourcequota equipe-backend -o yaml

Segunda camada: ResourceQuota

O ResourceQuota opera no escopo de um único namespace e define o teto de consumo agregado de todos os pods e objetos dentro dele. É o mecanismo padrão do Kubernetes, disponível tanto no OKD quanto em qualquer distribuição vanilla.

Quando um ResourceQuota existe em um namespace, o admission controller passa a exigir que todo pod declare explicitamente seus requests e limits de CPU e memória. Pods sem essa declaração são rejeitados na criação. Esse comportamento é intencional — a quota só consegue contabilizar recursos declarados.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: backend-prod-quota
  namespace: backend-prod
spec:
  hard:
    requests.cpu: "8"
    requests.memory: 16Gi
    limits.cpu: "16"
    limits.memory: 32Gi
    pods: "20"
    services: "10"
    services.loadbalancers: "2"
    services.nodeports: "0"
    persistentvolumeclaims: "10"
    requests.storage: 500Gi
    count/deployments.apps: "30"
    count/configmaps: "50"
    count/secrets: "50"

Repare que o ResourceQuota não se limita a CPU e memória. Você pode cotar praticamente qualquer tipo de objeto Kubernetes: services, configmaps, secrets, deployments, ingresses, e até storage. Isso é especialmente útil para evitar explosão de objetos em namespaces de desenvolvimento onde times criam recursos e esquecem de limpar.

A diferença entre requests.cpu e limits.cpu na quota merece atenção: você pode definir um limits.cpu maior que o requests.cpu, permitindo overcommit controlado. O scheduler usa os requests para alocar os pods nos nodes, mas os containers podem consumir mais CPU até atingir o limit. Em cenários de produção com SLA rígido, é comum manter requests e limits iguais para garantir performance previsível.

Inspecionando o ResourceQuota

# Ver consumo atual do namespace
oc describe quota -n backend-prod

# Ver em formato YAML com status
oc get resourcequota -n backend-prod -o yaml

# Listar todas as quotas de todos os namespaces
oc get resourcequota --all-namespaces

Terceira camada: LimitRange

O LimitRange é o controle mais granular da hierarquia — ele opera por container individual, não por namespace. Sua função principal é tripla: definir valores padrão (default) que são aplicados automaticamente quando o developer não declara resources, impor um mínimo aceitável abaixo do qual um container não pode ser criado, e estabelecer um teto por container que nenhum pod pode ultrapassar.

apiVersion: v1
kind: LimitRange
metadata:
  name: backend-prod-limits
  namespace: backend-prod
spec:
  limits:
  - type: Container
    default:
      cpu: 500m
      memory: 256Mi
    defaultRequest:
      cpu: 100m
      memory: 128Mi
    max:
      cpu: "4"
      memory: 4Gi
    min:
      cpu: 50m
      memory: 64Mi
  - type: Pod
    max:
      cpu: "8"
      memory: 8Gi
  - type: PersistentVolumeClaim
    max:
      storage: 100Gi
    min:
      storage: 1Gi

O LimitRange resolve um problema crítico de operação: quando um namespace tem ResourceQuota configurada, todo pod precisa declarar resources — mas desenvolvedores frequentemente esquecem. Sem o LimitRange com default e defaultRequest, esses pods são rejeitados com uma mensagem de erro confusa. Com o LimitRange, o OpenShift injeta automaticamente os valores padrão, e o pod sobe normalmente.

O campo type: Pod define um teto agregado por pod — útil para evitar pods com muitos containers que, individualmente, ficam dentro do limite por container mas juntos consomem demais. O type: PersistentVolumeClaim controla o tamanho dos volumes solicitados.

Inspecionando o LimitRange

# Ver limites aplicados no namespace
oc describe limitrange -n backend-prod

# Output:
# Type        Resource  Min   Max  Default Request  Default Limit
# ----        --------  ---   ---  ---------------  -------------
# Container   cpu       50m   4    100m             500m
# Container   memory    64Mi  4Gi  128Mi            256Mi
# Pod         cpu       -     8    -                -
# Pod         memory    -     8Gi  -                -

Requests vs Limits: a distinção que muda tudo

Essa é a parte mais importante da gestão de recursos no OpenShift OKD e também a mais mal compreendida. Requests e limits são dois conceitos completamente diferentes que atuam em momentos diferentes do ciclo de vida de um pod.

Requests: a promessa ao scheduler

O request é o valor que o scheduler usa para decidir em qual node um pod vai rodar. Quando você declara requests.cpu: 500m, você está dizendo ao scheduler: “reserve 500 millicores para este pod neste node”. O scheduler soma todos os requests dos pods já alocados em cada node e só aceita novos pods se houver capacity disponível.

O request é uma garantia mínima. O container sempre terá pelo menos aquela quantidade de CPU e memória disponível. Nenhum outro pod pode tomar esse recurso.

Limits: o teto de runtime

O limit é aplicado em tempo de execução pelo kernel do Linux via cgroups. É o teto absoluto de consumo do container. O que acontece quando o container tenta ultrapassar o limit depende do tipo de recurso:

CPU: o kernel aplica throttling via cgroup CPU quota. O processo continua rodando, mas é artificialmente limitado. Isso pode causar latência e degradação de performance sem que o container reinicie. É o comportamento mais silencioso e perigoso — você pode ter uma aplicação “funcionando” mas respondendo lentamente por causa de CPU throttling que ninguém está monitorando.

Memória: o kernel dispara o OOM Killer (Out Of Memory Killer) e mata o processo. O kubelet detecta o crash e reinicia o container com status OOMKilled. Esse comportamento é visível nos eventos e logs do pod.

Unidades de medida

CPU é medida em millicores: 1000m = 1 CPU = 1 vCPU = 1 core. Valores comuns para aplicações web são 100m a 500m de request e 500m a 2000m de limit.

Memória usa sufixos binários: Ki (kibibytes), Mi (mebibytes), Gi (gibibytes). Atenção: Mi e M são valores diferentes. 256Mi = 268.435.456 bytes, 256M = 256.000.000 bytes. Sempre use o sufixo binário para evitar discrepâncias nos dashboards do OpenShift.

QoS Classes: como o OpenShift prioriza pods em situações de pressão

O kubelet classifica automaticamente cada pod em uma das três Quality of Service (QoS) classes com base nos valores de requests e limits declarados. Essa classificação determina a ordem de evicção quando um node fica sob pressão de memória.

Guaranteed

Condição: todos os containers do pod têm request == limit para CPU e memória.

resources:
  requests:
    cpu: "1"
    memory: 1Gi
  limits:
    cpu: "1"
    memory: 1Gi

Pods Guaranteed são os últimos a serem evictados. O kernel os trata com prioridade máxima. Use para workloads críticos de produção onde latência previsível é obrigatória.

Burstable

Condição: pelo menos um container tem request < limit, ou nem todos os containers declararam resources completos.

resources:
  requests:
    cpu: 250m
    memory: 256Mi
  limits:
    cpu: "2"
    memory: 2Gi

Pods Burstable podem consumir mais do que o request quando o node tem folga, mas são evictados antes dos Guaranteed em situações de pressão. É o perfil mais comum para aplicações stateless que toleram alguma variação de performance.

BestEffort

Condição: nenhum container declara resources.

resources: {}

Pods BestEffort são os primeiros a ser evictados. O kubelet os sacrifica antes de qualquer outra classe. Use apenas para jobs batch não críticos ou ferramentas de debug temporárias.

Interação entre as três camadas

Um pod precisa satisfazer os três níveis de controle para ser criado. O admission controller avalia em sequência:

  1. O LimitRange injeta defaults se o container não declarou resources
  2. O ResourceQuota verifica se o namespace ainda tem capacity para o novo pod
  3. O ClusterResourceQuota verifica se o total agregado dos namespaces selecionados ainda está dentro do limite global

Se qualquer verificação falhar, o pod é rejeitado e o evento é registrado no namespace. Esse event é o primeiro lugar para olhar quando um pod não sobe:

oc get events -n backend-prod --field-selector reason=FailedCreate --sort-by='.lastTimestamp'

A mensagem de erro típica quando a quota é excedida:

Error creating: pods "meu-app-7d9f6b-xk2p" is forbidden:
exceeded quota: backend-prod-quota,
requested: requests.cpu=500m,
used: requests.cpu=7500m,
limited: requests.cpu=8

Estratégias avançadas de configuração

Overcommit controlado por ambiente

Uma estratégia comum em plataformas internas é usar níveis diferentes de overcommit por ambiente:

# namespace dev: overcommit 4:1
requests.cpu: "4"
limits.cpu: "16"

# namespace staging: overcommit 2:1
requests.cpu: "8"
limits.cpu: "16"

# namespace prod: sem overcommit (Guaranteed)
requests.cpu: "8"
limits.cpu: "8"

Isso maximiza a densidade de pods no cluster em ambientes de desenvolvimento (onde a carga é baixa e imprevisível) enquanto garante performance determinística em produção.

Quota por tipo de objeto para controle de custo

spec:
  hard:
    count/services.loadbalancers: "3"     # cada LB custa dinheiro em cloud
    count/persistentvolumeclaims: "10"
    requests.storage: 1Ti
    count/jobs.batch: "20"

ClusterResourceQuota hierárquico por BU

# Nível 1: teto por Business Unit
apiVersion: quota.openshift.io/v1
kind: ClusterResourceQuota
metadata:
  name: bu-engenharia
spec:
  selector:
    labels:
      matchLabels:
        bu: engenharia
  quota:
    hard:
      requests.cpu: "64"
      requests.memory: 128Gi

---
# Nível 2: teto por squad dentro da BU (ResourceQuota por namespace)
# namespace: squad-backend-prod
apiVersion: v1
kind: ResourceQuota
metadata:
  name: squad-backend-prod-quota
spec:
  hard:
    requests.cpu: "16"
    requests.memory: 32Gi

Operação do dia a dia: comandos essenciais

Criando e aplicando os objetos

# Aplicar ResourceQuota
oc apply -f resourcequota.yaml -n backend-prod

# Aplicar LimitRange
oc apply -f limitrange.yaml -n backend-prod

# Criar ClusterResourceQuota (requer cluster-admin)
oc apply -f clusterresourcequota.yaml

# Adicionar label ao namespace para entrar no escopo da CRQ
oc label namespace backend-prod team=backend

Monitorando consumo

# Consumo atual de todos os namespaces da CRQ
oc describe clusterresourcequota equipe-backend

# Top pods por consumo real (requer metrics-server)
oc adm top pods -n backend-prod --containers --sort-by=cpu

# Top nodes
oc adm top nodes

# Ver QoS class de cada pod
oc get pods -n backend-prod -o custom-columns=\
"NAME:.metadata.name,QOS:.status.qosClass,PHASE:.status.phase"

Diagnóstico de problemas

# Pod não sobe: verificar eventos de quota
oc get events -n backend-prod --field-selector reason=FailedCreate

# Ver resources declarados de todos os pods
oc get pods -n backend-prod -o json | \
  jq '.items[] | {name: .metadata.name, containers: [.spec.containers[] | {name: .name, requests: .resources.requests, limits: .resources.limits}]}'

# Identificar pods sem resources declarados (BestEffort)
oc get pods -n backend-prod -o json | \
  jq '.items[] | select(.status.qosClass == "BestEffort") | .metadata.name'

# Verificar se um namespace tem LimitRange
oc get limitrange -n backend-prod

Ajustando cotas em produção

# Aumentar limite de CPU da quota (patch direto)
oc patch resourcequota backend-prod-quota -n backend-prod \
  --type='json' \
  -p='[{"op": "replace", "path": "/spec/hard/requests.cpu", "value": "12"}]'

# Verificar resultado imediatamente
oc describe quota -n backend-prod

Armadilhas comuns e como evitá-las

Namespace com ResourceQuota mas sem LimitRange: se você cria uma ResourceQuota em um namespace sem um LimitRange com defaults, todos os pods sem resources declarados passam a ser rejeitados. A solução é sempre criar o LimitRange antes ou junto com a ResourceQuota. Uma convenção segura é manter um LimitRange em todo namespace da plataforma, mesmo em ambientes de desenvolvimento.

Confundir limits.cpu com requests.cpu na quota: a soma do campo limits.cpu da quota é a soma dos limits de todos os pods — não dos requests. Um pod com requests.cpu: 100m e limits.cpu: 2000m contribui com 100m para o campo requests.cpu da quota e com 2000m para o campo limits.cpu. Se sua quota só define requests.cpu, ela não está controlando o overcommit.

ClusterResourceQuota não substitui ResourceQuota local: os dois coexistem e são avaliados independentemente. Mesmo com uma ClusterResourceQuota bem configurada, um único namespace sem ResourceQuota local pode consumir todo o budget da equipe antes que outros namespaces consigam criar pods.

Unidades de memória incorretas: 256M é diferente de 256Mi. Em ambientes que misturam as duas notações, os dashboards de consumo podem mostrar valores inconsistentes. Padronize sempre com sufixos binários (Ki, Mi, Gi) em todos os manifests da plataforma.

CPU throttling silencioso: um container pode estar sendo throttled por limit de CPU sem que nenhum evento seja gerado. O único sinal visível é aumento de latência na aplicação. Para detectar, use:

oc exec -n backend-prod <pod-name> -- cat /sys/fs/cgroup/cpu/cpu.stat | grep throttled

Ou, em versões mais recentes com cgroup v2:

oc exec -n backend-prod <pod-name> -- cat /sys/fs/cgroup/cpu.stat | grep throttled_usec

Boas práticas para plataformas OpenShift OKD

Definir um LimitRange com defaults razoáveis em todos os namespaces garante que pods sem declaração explícita de resources ainda obedeçam a limites mínimos, protegendo o cluster de outliers acidentais.

Usar ClusterResourceQuota por equipe em clusters compartilhados cria isolamento real de recursos sem precisar de clusters dedicados por squad — o que reduz custo operacional e simplifica a gestão.

Monitorar o percentual de utilização das cotas continuamente, seja via Prometheus com as métricas kube_resourcequota e openshift_clusterresourcequota_usage, permite agir proativamente antes que o namespace atinja o teto e comece a rejeitar pods em produção.

Revisar as cotas periodicamente com os times de produto garante que os limites definidos no início do projeto ainda façam sentido para a escala atual da aplicação. Cotas muito apertadas são tão problemáticas quanto cotas inexistentes.

Documentar a política de QoS por ambiente no runbook da plataforma — exigindo Guaranteed em produção e permitindo Burstable em dev/staging — cria consistência e evita que configurações incorretas passem despercebidas em code review.

O controle de cotas de CPU e RAM no OpenShift OKD não é apenas uma feature de segurança — é a base de uma plataforma compartilhada funcional. Sem essa hierarquia de controle bem configurada, a promessa de multi-tenancy do OpenShift se torna uma fonte constante de incidentes.

O ClusterResourceQuota dá visibilidade e controle em nível de equipe ou business unit. O ResourceQuota protege os namespaces individualmente. O LimitRange fecha as brechas garantindo que nenhum container rode sem limites declarados. E a distinção entre requests e limits determina tanto onde os pods são alocados quanto como se comportam sob pressão em runtime.

Dominar esses três objetos — e saber como eles interagem — é o que separa uma plataforma OpenShift OKD bem administrada de um cluster que vive apagando incêndios.