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 vezO 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.jsonUse /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.txtComo 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
| Recurso | Request recomendado |
|---|---|
| CPU | uso observado mais 15% de folga, arredondado para cima em 10m (mínimo 10m) |
| Memória | uso 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
| Regra | Tipo | O que checa |
|---|---|---|
| RC001 | waste | Request de CPU muito acima do uso observado |
| RC002 | waste | Request de memória muito acima do uso observado |
| RC003 | risk | Uso de CPU acima do request |
| RC004 | risk | Uso de memória acima do request |
| RC005 | risk | Uso de memória a menos de 10% do limit (um OOMKill de distância) |
| RC006 | data | Nenhum request definido |
| RC007 | data | Nenhum uso encontrado para o container |
| RC008 | data | O uso é um instantâneo do kubectl top, não uma janela |
| RC009 | caveat | Um HPA ou escalador do KEDA em CPU ou memória escalaria para fora com o request recomendado |
| RC010 | caveat | Um VPA que não está em modo Off define os requests deste workload, e uma mudança à mão é sobrescrita |
| RC011 | data | O 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: undersizedO 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
AverageValuee 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 comkubectl get hpa -A -o yamle 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
generateNamecustomizado, 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
- 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). - Comece pelos
undersized: um request ausente ou abaixo do uso é risco hoje. - Depois baixe os
oversizedde maior vCPU recuperável, um de cada vez, conferindo os HPAs. - 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.
- 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.
Artigos relacionados
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 conversaReceba os novos artigos por e-mail
Um e-mail quando sair artigo novo sobre infraestrutura, backend e IA. Sem spam, cancele quando quiser.