Voltar para o blog

Seus requests de CPU e memória estão inflados? Uma ferramenta open source para descobrir

O scheduler do Kubernetes reserva o que o pod pede (o request), não o que ele usa. Um request quatro vezes maior que o uso real mantém os nós meio vazios, e o Karpenter não consegue consolidar pods que dizem precisar de uma capacidade que não usam. Antes de falar em Spot ou Graviton, a primeira alavanca de custo costuma ser essa: pedir o que se usa.

O rightsizing-check é uma ferramenta de linha de comando, open source, que lista os requests para baixar, quanto isso libera e quais workloads estão pedindo de menos, o que é um risco por si só. Por padrão ela funciona offline: lê os seus manifests e um arquivo de uso que você exportou, e nunca se conecta ao cluster. Opcionalmente, lê o uso direto de um Prometheus (--prometheus, com uma --window como 7d), só com requisições GET.

Por que mais uma ferramenta

Já existem o Goldilocks e o KRR, e são boas. Elas rodam contra um cluster ou um Prometheus ao vivo. O rightsizing-check serve para quando você não pode ou não quer fazer isso: um job de CI, a revisão do cluster de um cliente a partir de arquivos, um ambiente isolado. Você exporta os números uma vez, e a análise é uma função pura dos seus arquivos: com os mesmos arquivos, o mesmo resultado. Quando você tem um Prometheus, --prometheus busca as duas queries por você.

De onde vem o uso

Você escolhe uma das quatro fontes. As duas primeiras são as que merecem confiança.

Direto do Prometheus. Uma flag busca as duas queries abaixo e lê o resultado, sobre a janela que você escolher (ela precisa incluir o seu período mais cheio; 7 dias é um bom começo):

rightsizing-check scan ./k8s --prometheus https://prometheus.exemplo.com --window 7d
rightsizing-check scan ./k8s --prometheus https://prometheus.exemplo.com --window 1d \
  --prom-selector 'namespace=~"loja|nps"'     # uma parte do cluster por vez

O endereço pode ser o da interface web (/query e /graph são removidos). A ferramenta só envia requisições GET para /api/v1/query, uma query de cada vez, a de memória primeiro. Um token vai por --prom-token-env, lido de uma variável de ambiente.

Janela longa é pesada. A query de CPU é uma subquery sobre a janela inteira, então o custo cresce com o número de séries vezes a janela. Num Prometheus pequeno, uma janela de 14 dias fez o pod usar memória suficiente para ser despejado pelo kubelet enquanto a ferramenta era testada contra um servidor real. Uma janela de 7 dias sobre cerca de 190 mil séries levou 45 segundos e foi tranquila. Por isso uma janela acima de 7 dias é recusada, a menos que você passe --prom-allow-long-window. Comece com 1d ou 7d, leia alguns namespaces por vez com --prom-selector, evite o horário de pico e nunca repita uma query que respondeu 504 ou 503: o servidor pode ainda estar executando.

Prometheus, à mão. As mesmas duas queries instantâneas, sobre a mesma janela. Exporte cada resultado como ele vem:

PROM=http://prometheus:9090

# CPU: o percentil 95 do uso por container, em cores
curl -sG "$PROM/api/v1/query" --data-urlencode \
  'query=max by (namespace, pod, container) (quantile_over_time(0.95, rate(container_cpu_usage_seconds_total{container!="",container!="POD"}[5m])[7d:5m]))' > cpu.json

# Memória: o máximo do working set, em bytes
curl -sG "$PROM/api/v1/query" --data-urlencode \
  'query=max by (namespace, pod, container) (max_over_time(container_memory_working_set_bytes{container!="",container!="POD"}[7d]))' > mem.json

Use /api/v1/query e não query_range: a ferramenta quer um valor por container. A query de CPU é uma subquery e pode ser pesada em um Prometheus grande, então reduza a janela ou consulte por namespace.

CSV. Do Grafana, de um notebook ou de qualquer lugar. O cabeçalho é obrigatório, e um valor em branco significa que aquele número não é conhecido:

namespace,pod,container,cpu_millis,memory_bytes
shop,api-5d8f7c9b6-x2k4j,app,120,629145600

`kubectl top`. Rápido e o mais fraco: um instante, não um percentil. A ferramenta avisa quando é a fonte (RC008), porque um workload ocioso agora e ocupado à noite parece superdimensionado.

kubectl top pods -A --containers > top.txt

Como os pods são ligados aos workloads

O uso vem por pod e o request vem por workload, então os pods são ligados pelo nome, no mesmo namespace, do jeito que os controladores nomeiam: nome-<hash>-<sufixo> para um Deployment, nome-<ordinal> para um StatefulSet, nome-<sufixo> para um DaemonSet e o nome exato para um Pod avulso. As partes aleatórias usam o alfabeto do Kubernetes, que não tem vogais, então api-gateway-extra nunca é confundido com um pod do api. Pods que não casam com nada são contados no resumo. Jobs e CronJobs não são ligados, porque os pods deles duram pouco.

Com várias réplicas, a mais ocupada decide: uma réplica quente precisa da reserva, mesmo que as irmãs fiquem ociosas.

A recomendação

RecursoRequest recomendado
CPUuso observado mais 15% de folga, arredondado para cima em 10m (mínimo 10m)
Memóriauso observado mais 20% de folga, arredondado para cima em 4Mi (mínimo 16Mi)

Um request é inflado quando passa de 1,5 vez a recomendação (--slack), o que equivale a uma utilização abaixo de uns 58%. As folgas são --cpu-headroom e --mem-headroom. A memória não é compressível, então a recomendação parte do máximo da janela, não da média.

As regras

RegraTipoO que checa
RC001wasteRequest de CPU muito acima do uso observado
RC002wasteRequest de memória muito acima do uso observado
RC003riskUso de CPU acima do request
RC004riskUso de memória acima do request
RC005riskUso de memória a menos de 10% do limit (um OOMKill de distância)
RC006dataNenhum request definido
RC007dataNenhum uso encontrado para o container
RC008dataO uso é um instantâneo do kubectl top, não uma janela
RC009caveatUm HPA ou escalador do KEDA em CPU ou memória escalaria para fora com o request recomendado
RC010caveatUm VPA que não está em modo Off define os requests deste workload, e uma mudança à mão é sobrescrita
RC011dataO VPA tem a própria recomendação, mostrada ao lado da desta ferramenta

Cada workload termina em um status: oversized (algum request muito acima do uso e nada em risco), undersized (um request ou limit abaixo do uso, ou um request ausente), ok ou no-data. O undersized ganha do oversized: um workload com risco não é candidato a encolher. O rightsizing-check explain RC001 documenta qualquer regra.

Um exemplo de saída

Este é o resultado real sobre o testdata do repositório, um namespace inventado:

STATUS      WORKLOAD                     CONTAINER  REPLICAS  CPU REQ  CPU USE  CPU REC  MEM REQ  MEM USE  MEM REC  FINDINGS
oversized   shop/deployment/api          app        3         1000m    120m     140m     2Gi      600Mi    720Mi    RC001,RC002
oversized   kube-system/daemonset/agent  agent      -         500m     40m      50m      512Mi    120Mi    144Mi    RC001,RC002
undersized  shop/deployment/worker       worker     2         100m     320m     370m     256Mi    576Mi    692Mi    RC003,RC004,RC005
undersized  shop/statefulset/db          db         1         -        450m     520m     -        1.5Gi    1844Mi   RC006
ok          shop/deployment/web          web        2         150m     120m     140m     200Mi    144Mi    176Mi    -
no-data     shop/cronjob/report          report     -         -        ?        ?        -        ?        ?        RC007

6 workloads: 2 oversized, 2 undersized, 1 ok, 1 without data
Reclaimable requests: 2.6 vCPU (73% of the 3.5 requested) and 3.9 GiB (56% of 6.9).

A leitura, workload por workload:

  • `api` pede 1000m e 2Gi, e usa 120m e 600Mi. A recomendação é 140m e 720Mi, o que libera 860m e 1328Mi por pod, e como são 3 réplicas, 2,6 vCPU no total.
  • `worker` é o contrário: usa 320m e pede 100m (RC003), usa 576Mi e pede 256Mi (RC004), e está a 90% do limit de 640Mi (RC005). Ele não é candidato a encolher, é candidato a investigar.
  • `db` não tem request nenhum (RC006), então o scheduler o trata como se não precisasse de nada. A ferramenta recomenda 520m e 1844Mi a partir do uso.
  • `report` é um CronJob, e CronJobs não são ligados a pods, então fica sem dados (RC007).
  • O DaemonSet aparece na tabela por pod, mas fica fora das somas, porque o número de réplicas depende dos nós.

VPAs e Argo Rollouts

Um VerticalPodAutoscaler que não está em modo Off (Auto, Recreate, Initial ou InPlaceOrRecreate; um modo não definido é Auto) escreve os requests dos pods que controla, então um request mudado à mão no manifest não dura. Quando a ferramenta mudaria um request que um VPA controla, a RC010 avisa, e o resumo diz quanto da CPU recuperável está nesses workloads: mude por meio do VPA (minAllowed, maxAllowed ou o modo Off), não no manifest. Quando o objeto do VPA traz um status (um cluster vivo, ou um manifest exportado com kubectl get vpa -A -o yaml), a RC011 mostra o alvo dele ao lado da recomendação da ferramenta: os dois podem diferir porque cada um aprende de uma janela diferente.

Um Rollout do Argo com template de pod é analisado como um Deployment, e os pods dele são ligados do mesmo jeito. Um Rollout que aponta para um Deployment (workloadRef) não tem template próprio: o Deployment é o workload analisado.

Modo cluster, só leitura

Com --cluster, a ferramenta lê workloads, HPAs, ScaledObjects do KEDA, VPAs e Argo Rollouts direto do cluster, e o uso de cada container da API de métricas (metrics.k8s.io, o que o kubectl top lê), a menos que você dê outra fonte de uso.

rightsizing-check scan --cluster
rightsizing-check scan --cluster --prometheus https://prometheus.exemplo.com --window 7d

É só leitura: só chamadas list são enviadas (os testes conferem que nenhum outro verbo é usado), paginadas de 500 em 500 e com poucas requisições por segundo, um recurso por vez. O uso do metrics-server é um instante e é marcado (RC008): com --prometheus, a API de métricas nem é lida.

GitHub Action

O repositório também é uma GitHub Action: instala a release com verificação de checksum, roda uma varredura que escreve o relatório e o JSON (com --prometheus, o servidor é lido uma vez por execução), comenta no pull request e falha a pipeline no nível do fail-on só depois do comentário. O comentário é achado por um marcador, então a Action nunca edita o de outra ferramenta.

- uses: robertasolimandonofreo/rightsizing-check@v0.4.1
  with:
    path: ./k8s
    prometheus: https://prometheus.exemplo.com
    window: 7d
    comment: "true"
    fail-on: undersized

O que significam os números

Requests recuperáveis é o que as recomendações liberam: para cada request inflado, o request menos a recomendação, vezes as réplicas. Cobre Deployments, StatefulSets e Pods, e as porcentagens são sobre os requests dos workloads que têm dados de uso.

A linha de custo (--cpu-price e --mem-price, por vCPU-mês e por GiB-mês) é o seu preço vezes o que é recuperável. A ferramenta não tem preços próprios. É um número em requests, não na sua fatura: ele só vira economia quando os nós consolidam, que é o que o Karpenter faz quando os pods deixam de pedir a capacidade.

Cuidados antes de baixar um request

  • HPA. Um HorizontalPodAutoscaler que escala por utilização usa o request como denominador: baixar o request aumenta a utilização que ele enxerga, e ele pode escalar para fora. A ferramenta lê HPAs e ScaledObjects do KEDA dos manifests e calcula a utilização com o request recomendado; a RC009 só dispara quando ela passaria do alvo. Alvos AverageValue e os outros gatilhos do KEDA (RabbitMQ, Prometheus, cron) não dependem do request e são ignorados. Um HPA que só existe no cluster não é visto nos manifests: use --cluster, ou exporte com kubectl get hpa -A -o yaml e passe o arquivo.
  • A janela decide. Um job mensal ou um pico que você não capturou faz um request parecer inflado. Use uma janela que cubra o seu período mais cheio e baixe um workload de cada vez, acompanhando throttling e latência.
  • Memória. Não reduza um container marcado com RC005 antes de entender por que ele está perto do limit. Um limit abaixo do uso mata o container.
  • CPU limits, init containers e sidecars injetados na admissão não são analisados. Um sidecar injetado não tem entrada no manifest.

O que ele não faz

  • A recomendação é um ponto de partida, tão boa quanto a janela dos seus dados.
  • Pods são ligados pelo nome. Um generateName customizado, ou um CRD que cria pods, não é reconhecido e aparece como não ligado.
  • Os leitores do Prometheus e do `kubectl top` foram testados com saídas de exemplo, não com todas as versões deles. Se um arquivo for rejeitado, o --csv é a saída.
  • Ele nunca edita os seus arquivos.

Próximos passos para o seu cluster

  1. Rode rightsizing-check scan ./k8s --prometheus https://prometheus.exemplo.com --window 7d (ou exporte o uso por 7 dias e use --prom-cpu cpu.json --prom-mem mem.json).
  2. Comece pelos undersized: um request ausente ou abaixo do uso é risco hoje.
  3. Depois baixe os oversized de maior vCPU recuperável, um de cada vez, conferindo os HPAs.
  4. Com requests justos, a consolidação do Karpenter tem o que empacotar. Use o gerador de NodePool do Karpenter e a calculadora de economia em Kubernetes para estimar o ganho.
  5. Só então mova cargas para Spot e Graviton. O spot-readiness e o graviton-readiness respondem a pergunta equivalente para cada um.

A página da ferramenta, com instalação e exemplo, está em /ferramentas/rightsizing-check, e o código em github.com/robertasolimandonofreo/rightsizing-check, com licença Apache-2.0. Se você quiser que eu revise o seu cluster, valide as recomendações com os seus dados e monte um plano de redução de custo, o assessment de eficiência é o ponto de partida.

Quer aplicar isso no seu ambiente?

Agende uma conversa de 30 minutos sem compromisso. Eu olho o seu cenário e digo por onde começar.

Agendar uma conversa

Receba os novos artigos por e-mail

Um e-mail quando sair artigo novo sobre infraestrutura, backend e IA. Sem spam, cancele quando quiser.