Um caso real: a mesma página, o mesmo código, o mesmo cluster — 8 s num pod e 0,7 s em outro. O painel de CPU dizia que estava tudo bem.
A real case: same page, same code, same cluster — 8 s in one pod, 0.7 s in another. The CPU dashboard said everything was fine.
Do sintoma à métrica que ninguém coloca no painel. O caso real é o fio condutor; a teoria entra quando o número pede.
From the symptom to the metric nobody puts on a dashboard. A real case is the thread; theory shows up only when a number demands it.
8 s × 0,7 s com o mesmo código. O que foi descartado e a pista que entregou a causa.
8 s vs 0.7 s with the same code. What was ruled out, and the clue that gave it away.
Período, cota, throttling. Por que 100 m não é "um core lento" e por que nó ocioso não ajuda.
Period, quota, throttling. Why 100m is not "a slow core" and why an idle node doesn't help.
O painel plota o que o container conseguiu executar. O tempo parado esperando cota mora em outra métrica.
The dashboard plots what the container managed to run. Time spent waiting for quota lives in another metric.
Por fora (curl, concorrência) e por dentro (cpu.max, cpu.stat, getrusage). PromQL e alerta.
From outside (curl, concurrency) and inside (cpu.max, cpu.stat, getrusage). PromQL and an alert.
Da correção mais barata à mais estrutural. Checklist para qualquer projeto "lento sem motivo".
From the cheapest fix to the structural one. A checklist for any "slow for no reason" project.
Pode significar "não deixaram usar". Esse é o ponto para levar embora.
It may mean "it wasn't allowed to use it". That's the one thing to take home.
GET /report levava 8 s na instância app A. O mesmo código, na instância app B, respondia em 0,7 s. Nada de diferente no deploy visível: mesma imagem, mesmo cluster.
GET /report took 8 s on instance app A. The same code, on instance app B, answered in 0.7 s. Nothing visibly different in the deployment: same image, same cluster.
| app A | app B | |
|---|---|---|
| Tempo de respostaResponse time | 8 s | 0,7 s0.7 s |
| Código / imagemCode / image | mesmasame | mesmasame |
| Cluster | mesmosame | mesmosame |
limits.cpu | 100m | validarverify |
| Gráfico de CPUCPU graph | "encostado em 100 m""flat at 100 m" | — |
A página precisava de ~1 s de CPU: um loop lia e decodificava 141× um JSON de 420 KB. Detalhe que só importa quando alguém fecha a torneira.
The page needed ~1 s of CPU: a loop read and decoded a 420 KB JSON 141 times. A detail that only matters once someone closes the tap.
Cada hipótese plausível caiu com um número. Nenhuma caiu por opinião.
Every plausible hypothesis fell to a number. None fell to an opinion.
DEBUG=SUma request sozinha levava 8 s. Três simultâneas não levaram 8 s cada — levaram ~3× mais, terminando em escada. Isso é uma assinatura: espera de rede é paralela; disputa por CPU/cota serializa.
A single request took 8 s. Three concurrent ones didn't take 8 s each — they took ~3× longer, finishing in a staircase. That's a signature: network waits run in parallel; CPU/quota contention serializes.
Mecanismo do kernel Linux que agrupa processos e aplica limites (CPU, memória, IO). Cada container é um cgroup.
Linux kernel mechanism that groups processes and enforces limits (CPU, memory, IO). Every container is a cgroup.
Completely Fair Scheduler: o escalonador de CPU do Linux. Decide quem roda em cada core e por quanto tempo.
Completely Fair Scheduler: the Linux CPU scheduler. Decides who runs on each core, and for how long.
cpu.cfs_period_us. Janela de contagem. Default 100 ms (100.000 µs).
cpu.cfs_period_us. The accounting window. Default 100 ms (100,000 µs).
cpu.cfs_quota_us. Quanto tempo de CPU o cgroup pode usar dentro de cada período, somando todos os cores. É o que implementa limits.cpu.
cpu.cfs_quota_us. How much CPU time the cgroup may use within each period, summed across all cores. This is what implements limits.cpu.
O cgroup gastou a cota antes do fim do período e é removido da CPU até o próximo período — mesmo com cores ociosos no nó.
The cgroup spent its quota before the period ended and is taken off the CPU until the next period — even with idle cores on the node.
cgroup v2. cpu.max = "cota período" (ex.: 10000 100000 = 100 m). cpu.stat traz nr_periods, nr_throttled, throttled_usec, usage_usec.
cgroup v2. cpu.max = "quota period" (e.g. 10000 100000 = 100m). cpu.stat has nr_periods, nr_throttled, throttled_usec, usage_usec.
1000 m = 1 core. 100 m = 10% de um core = 10 ms a cada 100 ms.
1000m = 1 core. 100m = 10% of a core = 10 ms every 100 ms.
requests: reserva para o scheduler + peso relativo sob disputa — não limita. limits: vira cota do CFS — limita sempre.
requests: reservation for the scheduler + relative weight under contention — never limits. limits: becomes the CFS quota — always limits.
limits.cpu: 100m significa: a cada período de 100 ms, o container pode usar 10 ms de CPU (somando todos os cores). Gastou os 10 ms? Fica parado até o próximo período começar. Não importa se o nó tem 64 cores ociosos.limits.cpu: 100m means: every 100 ms period, the container may use 10 ms of CPU (summed across all cores). Spent the 10 ms? It stops until the next period starts. It doesn't matter that the node has 64 idle cores.
Um request que precisa de 1 s de CPU leva no mínimo 10 s de relógio: 10 ms por período × 100 períodos. Um único usuário, numa página só, já paga o pedágio inteiro — não precisa de tráfego para ficar lento. Latência vira função da cota, não da carga.A request that needs 1 s of CPU takes at least 10 s of wall clock: 10 ms per period × 100 periods. A single user, on a single page, pays the whole toll — no traffic needed to be slow. Latency becomes a function of the quota, not of load.
Com limits.cpu: 500m a cota é de 50 ms por período: o mesmo 1 s de CPU cabe em 20 períodos → 2 s. A cota é absoluta e não escala com o nó; trocar de nó não muda 10 ms/100 ms — muda só a qualidade desses 10 ms.With limits.cpu: 500m the quota is 50 ms per period: the same 1 s of CPU fits in 20 periods → 2 s. The quota is absolute and doesn't scale with the node; moving nodes doesn't change 10 ms/100 ms — only the quality of those 10 ms.
Parecem um par (mínimo / máximo). Não são: um fala com o scheduler do Kubernetes, o outro vira cota do CFS no kernel. Só um deles trava o processo.
They look like a pair (min / max). They aren't: one talks to the Kubernetes scheduler, the other becomes a CFS quota in the kernel. Only one of them stops your process.
cpu.weight / cpu.shares): só entra em jogo quando há disputa pelo core.Relative weight (cpu.weight / cpu.shares): only matters under contention for the core.cpu.max / cpu.cfs_quota_us no cgroup do container.Becomes cpu.max / cpu.cfs_quota_us in the container's cgroup.limits.memory mata o container (OOMKill, restart, evento visível). limits.cpu só atrasa — lentidão silenciosa, sem evento, sem restart, sem log.Don't confuse it with memory. limits.memory kills the container (OOMKill, restart, visible event). limits.cpu only delays — silent slowness, no event, no restart, no log.Se a lentidão fosse um timeout de rede (DNS, backend, banco), três requests simultâneas esperariam em paralelo: esperar não consome o recurso escasso. As três terminariam juntas, em ~T.If the slowness were a network timeout (DNS, backend, database), three concurrent requests would wait in parallel: waiting doesn't consume the scarce resource. All three would finish together, at ~T. Sob cota, as três disputam os mesmos 10 ms por período. Cada uma recebe ~1/3 da cota e leva ~3× mais. No caso real: 23,5 / 24,8 / 26,4 s — a escada é a assinatura. Serialização = CPU ou lock, não rede.Under quota, all three fight for the same 10 ms per period. Each gets ~1/3 of the quota and takes ~3× longer. In the real case: 23.5 / 24.8 / 26.4 s — the staircase is the signature. Serialization = CPU or a lock, not the network.
A cota é compartilhada entre todas as threads do container. Apache com 8 workers, JVM com GC paralelo, Node com threads do libuv: 8 threads consomem os 10 ms em 1,25 ms de relógio — e o container fica parado 98,75 ms.
The quota is shared by all threads in the container. Apache with 8 workers, a JVM with parallel GC, Node with libuv threads: 8 threads burn the 10 ms in 1.25 ms of wall clock — and the container stops for 98.75 ms.
limits.cpu baixo.JVM: parallel GC with N threads drains the quota in an instant → huge GC pauses with a low limits.cpu.MaxRequestWorkers muito maior que a cota em cores.Apache/PHP: MaxRequestWorkers far above the quota in cores.UV_THREADPOOL_SIZE (default 4) mais o event loop.Node: UV_THREADPOOL_SIZE (default 4) plus the event loop.GOMAXPROCS = cores do nó, não da cota.Go: GOMAXPROCS = cores of the node, not of the quota.
O painel de CPU padrão (OpenShift, Grafana, kube-prometheus) plota container_cpu_usage_seconds_total: quanto o container conseguiu executar. Com cota, isso é por construção ≤ limite. A linha "encostada em 100 m" parece "usa pouco e está confortável".The default CPU panel (OpenShift, Grafana, kube-prometheus) plots container_cpu_usage_seconds_total: how much the container managed to run. Under a quota that is ≤ limit by construction. The line "flat at 100 m" reads as "uses little and is comfortable".
O tempo parado esperando cota não é uso — é espera, e mora em container_cpu_cfs_throttled_seconds_total, que nenhum painel padrão mostra. No caso: 51,9 s parado contra 12,7 s executando — 4× mais tempo na fila do que rodando. Hidrômetro × torneira meio fechada: o hidrômetro mostra vazão baixa e constante; não mostra a fila de baldes.Time stopped waiting for quota is not usage — it's waiting, and it lives in container_cpu_cfs_throttled_seconds_total, which no default panel shows. In the case: 51.9 s stopped vs 12.7 s running — 4× more time queued than running. Water meter vs half-closed tap: the meter shows a low, steady flow; it doesn't show the line of buckets waiting.
| MétricaMetric | O que medeWhat it measures | Está no painel padrão?On the default panel? | Como lerHow to read it |
|---|---|---|---|
| container_cpu_usage_seconds_total | Quanto o container conseguiu executarHow much the container managed to run | SimYes | Por construção ≤ limite. Encostado no limite = suspeita, não conforto.≤ limit by construction. Flat at the limit = suspicion, not comfort. |
| container_cpu_cfs_periods_total | Períodos de 100 ms decorridos100 ms periods elapsed | NãoNo | Denominador da fração throttled.Denominator of the throttled ratio. |
| container_cpu_cfs_throttled_periods_total | Períodos em que houve throttlingPeriods in which throttling happened | NãoNo | throttled / periods > 25% sustentado já dá latência visível.throttled / periods > 25% sustained already means visible latency. |
| container_cpu_cfs_throttled_seconds_total | Segundos parado esperando cotaSeconds stopped waiting for quota | NãoNo | rate() > 1 = mais tempo parado do que rodando.rate() > 1 = more time stopped than running. |
usage está encostado no limite e você não vê throttling, você não está olhando throttling — não que ele não exista.Rule: if usage is flat at the limit and you don't see throttling, you're not looking at throttling — not that it isn't there.Equivalentes de dentro do pod (cgroup v2, /sys/fs/cgroup/cpu.stat): usage_usec · nr_periods · nr_throttled · throttled_usec.In-pod equivalents (cgroup v2, /sys/fs/cgroup/cpu.stat): usage_usec · nr_periods · nr_throttled · throttled_usec.
limits.cpu diferente, tempo proporcional.Comparing quotas closes the case: same code, different limits.cpu, proportional time.| Leitura no pod (caso real)Read in the pod (real case) | ValorValue | SignificaMeaning |
|---|---|---|
| cpu.max | 10000 100000 | 100 m |
| nr_periods | 1571 | períodos decorridosperiods elapsed |
| nr_throttled | 1137 | 72% dos períodos (98% durante os requests)72% of periods (98% during the requests) |
| throttled_usec | 51.880.18451,880,184 | 51,9 s parado51.9 s stopped |
| usage_usec | 12.739.72412,739,724 | 12,7 s executando12.7 s running |
| loop 141 decodes141-decode loop | real 8,58 s · cpu 0,96 sreal 8.58 s · cpu 0.96 s | 89% do tempo esperando89% of the time waiting |
limits.cpu. Rápido, reversível, o primeiro teste. Diagnóstico, não cura.limits.cpu. Fast, reversible, the first test. A diagnosis, not a cure.
Manter só requests: cpu.weight já garante fatia justa sob disputa. Cautela: QoS muda (Guaranteed → Burstable); LimitRange/ResourceQuota podem exigir limite.Keep only requests: cpu.weight already guarantees a fair share under contention. Caution: QoS changes (Guaranteed → Burstable); LimitRange/ResourceQuota may require a limit.
Cota baixa é uma lupa sobre desperdício. 141 leituras + decodes do mesmo JSON → cachear em static → 1 leitura. Beneficia todos os pods, com qualquer cota.A low quota is a magnifying glass on waste. 141 reads + decodes of the same JSON → cache it in a static → 1 read. Helps every pod, under any quota.
kubelet --cpu-cfs-quota-period=10ms: pausas de 9 ms em vez de 90 ms. Config de nó; reduz a latência do throttle, não a média.kubelet --cpu-cfs-quota-period=10ms: 9 ms pauses instead of 90 ms. Node config; reduces throttle latency, not the average.
MaxRequestWorkers · -XX:ActiveProcessorCount · GOMAXPROCS · UV_THREADPOOL_SIZE. Evita 8 threads torrarem a cota em 1 ms.MaxRequestWorkers · -XX:ActiveProcessorCount · GOMAXPROCS · UV_THREADPOOL_SIZE. Keeps 8 threads from burning the quota in 1 ms.
100m → 500m levou de 8 s para 1,5 s. Mas com 500 m ainda throttlava 60% dos períodos → o trabalho por request era o problema real.In the case: 100m → 500m took it from 8 s to 1.5 s. But at 500m it still throttled 60% of periods → the work per request was the real problem.limits.memory mata (restart visível); limits.cpu só atrasa (silêncio).Raising the quota is a test, not a cure. The cure is measuring throttling and cutting CPU per request. Reminder: limits.memory kills (visible restart); limits.cpu only delays (silence).Respostas salvas neste navegador (localStorage · cpu-throttling-checklist).Answers saved in this browser (localStorage · cpu-throttling-checklist).
100 m não é um core lento: é um core rápido por 10 ms e nada por 90 ms.100m isn't a slow core: it's a fast core for 10 ms and nothing for 90 ms.
A cota é aplicada mesmo com 63 cores vazios. Trocar de nó não muda 10 ms/100 ms.The quota is enforced even with 63 empty cores. Moving nodes doesn't change 10 ms/100 ms.
Não precisa de carga. Uma request de 1 s de CPU paga 10 s sozinha.No load needed. One request worth 1 s of CPU pays 10 s on its own.
A cura é medir throttling e reduzir CPU por request. Com 500 m ainda throttlava 60%.The cure is measuring throttling and cutting CPU per request. At 500m it still throttled 60%.
Rede espera em paralelo; cota serializa. 23,5 / 24,8 / 26,4 s contam a história.The network waits in parallel; quota serializes. 23.5 / 24.8 / 26.4 s tell the story.