Sessão técnica · Kubernetes / OpenShiftTech session · Kubernetes / OpenShift

CPU Throttling
o gargalo que o gráfico
não mostra

CPU Throttling
the bottleneck your
dashboard doesn't show

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.

Luciano Scorsin · 29 de agosto de 2026Luciano Scorsin · August 29, 2026
LIMITS.CPU: 100M · PERÍODO DE 100 MSLIMITS.CPU: 100M · 100 MS PERIOD 10 ms 90 ms parado90 ms throttled O QUE O GRÁFICO DE CPU MOSTRAWHAT THE CPU GRAPH SHOWS limit 100 m "usa 100 m, está tranquilo""using 100 m, looks fine"
CPU Throttling
Agenda
RoteiroOutline

Cinco perguntas, uma causa

Five questions, one root cause

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.

01

O caso

The case

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.

02

Como a cota funciona

How the quota works

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.

03

Por que o gráfico mente

Why the graph lies

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.

04

Como diagnosticar

How to diagnose

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.

05

Como corrigir

How to fix it

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.

TeseThesis

Uso abaixo do limite ≠ sobra de CPU

Usage below the limit ≠ spare CPU

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.

CPU Throttling
Agenda · Item 01
O caso · uma página, duas instânciasThe case · one page, two instances

Mesmo código, mesmo cluster, 11× mais lento

Same code, same cluster, 11× slower

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 Aapp B
Tempo de respostaResponse time8 s0,7 s0.7 s
Código / imagemCode / imagemesmasamemesmasame
Clustermesmosamemesmosame
limits.cpu100mvalidarverify
Gráfico de CPUCPU graph"encostado em 100 m""flat at 100 m"
11×8 s ÷ 0,7 s · a mesma página, uma request, um usuário8 s ÷ 0.7 s · same page, one request, one user

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.

CPU Throttling
Agenda · Item 01
O caso · hipótesesThe case · hypotheses

O que foi descartado — com medição

What was ruled out — by measuring

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.

Release novaNew release
Rollback para a versão anterior → continuou 8 sRolled back to the previous version → still 8 s
"foi o deploy de ontem""it was yesterday's deploy"
Include chamando backendInclude calling a backend
Primeiro byte do HTML saía em 40 ms (TTFB) — o tempo estava depoisFirst byte of HTML out in 40 ms (TTFB) — the time was spent after that
"algum include está travando""some include is hanging"
DNS
Probe de resolução falhava em 10 ms — rápido demais para ser esperaResolution probe failed in 10 ms — too fast to be a wait
"timeout de resolução""resolver timeout"
Banco / sessãoDatabase / session
APIs do mesmo pod respondiam em 70 msAPIs from the same pod answered in 70 ms
"o banco está lento""the database is slow"
DEBUG=S
Removido → continuou 8 sRemoved → still 8 s
"é o modo debug""it's debug mode"
Medir antes de acreditar. Cinco hipóteses razoáveis, cinco testes de menos de um minuto cada. O que sobrou não era nenhuma das suspeitas óbvias — e o gráfico de CPU não estava ajudando.Measure before you believe. Five reasonable hypotheses, five tests under a minute each. What remained was none of the obvious suspects — and the CPU graph wasn't helping.
CPU Throttling
Agenda · Item 01
O caso · a pistaThe case · the clue

Três requests ao mesmo tempo: 23,5 · 24,8 · 26,4 s

Three requests at once: 23.5 · 24.8 · 26.4 s

Uma 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.

Se fosse espera de rede / timeoutIf it were a network wait / timeout req 1~8 s req 2~8 s req 3~8 s todas terminam juntasall finish togetherparalelo é de graça: nada é divididoparallel is free: nothing is shared O que aconteceu · sob cota de 100 mWhat happened · under a 100m quota req 1 23,5 s23.5 s req 2 24,8 s24.8 s req 3 26,4 s26.4 s as três dividem os mesmos 10 ms por períodoall three share the same 10 ms per periodazul = executando · hachurado = parado esperando o próximo períodoblue = running · hatched = stopped, waiting for the next period Serialização é o sinal. Timeout de rede não serializa — cota serializa.Serialization is the signal. Network timeouts don't serialize — quota does.
CPU Throttling
Agenda · Item 02
Vocabulário mínimoMinimum vocabulary

Oito termos para entender o resto

Eight terms to understand the rest

cgroup

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.

CFS

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.

períodoperiod

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).

cotaquota

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.

throttling

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.

cpu.max · cpu.stat

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.

millicore (m)

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 × limits

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.

CPU Throttling
Agenda · Item 02
Como a cota funciona, de verdadeHow the quota really works

100 m não é um core lento: é 10 ms e depois nada

100m is not a slow core: it's 10 ms and then nothing

1 · Cota de 10 ms1 · 10 ms quota2 · Request de 1 s de CPU2 · A 1 s-of-CPU request3 · Cota de 50 ms3 · 50 ms quota→ avança o estágio→ next stage

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.

limits.cpu: 100m · cpu.max = 10000 100000 0 ms100 ms200 ms300 ms 100 m 10 ms 90 ms parado (throttled)90 ms stopped (throttled) throttled throttled Um core rápido por 10 ms, e nada por 90 ms — mesmo com 63 cores vazios no nó.A fast core for 10 ms, then nothing for 90 ms — even with 63 empty cores on the node. Request que precisa de 1 s de CPUA request that needs 1 s of CPU 0 s10 s de relógio10 s wall clock … 100 períodos × 10 ms (a barra mostra 1 a cada 10)… 100 periods × 10 ms (the bar shows 1 in 10) 1 s de CPU ÷ 10 ms por período = 100 períodos = 10 s. Uma request, um usuário, nó ocioso.1 s of CPU ÷ 10 ms per period = 100 periods = 10 s. One request, one user, idle node. limits.cpu: 500m · cpu.max = 50000 100000 500 m 50 ms50 ms50 ms 1 s de CPU ÷ 50 ms por período = 20 períodos = 2 s. Mesmo trabalho, 5× mais rápido — e ainda 50% do tempo parado.1 s of CPU ÷ 50 ms per period = 20 periods = 2 s. Same work, 5× faster — and still stopped 50% of the time.
CPU Throttling
Agenda · Item 02
Dois campos, dois mecanismosTwo fields, two mechanisms

requestslimits

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.

requests.cpu

reserva + pesoreservation + weight

  • Scheduler: decide em que nó o pod cabe (soma dos requests ≤ capacidade alocável).Scheduler: decides which node the pod fits on (sum of requests ≤ allocatable).
  • Peso relativo (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.
  • Nó vazio? O pod usa o que quiser. Não limita.Empty node? The pod uses whatever it wants. Never limits.
resources: requests: cpu: 100m # reserva 0,1 core no nó# reserves 0.1 core on the node memory: 256Mi
limits.cpu

cota do CFSCFS quota

  • Vira cpu.max / cpu.cfs_quota_us no cgroup do container.Becomes cpu.max / cpu.cfs_quota_us in the container's cgroup.
  • Aplicado sempre, a cada período, mesmo com o nó vazio.Enforced always, every period, even on an empty node.
  • Gastou a cota? Parado até o próximo período. Cada thread desconta da mesma cota.Quota spent? Stopped until the next period. Every thread draws from the same quota.
resources: limits: cpu: 100m # cpu.max = 10000 100000 memory: 512Mi # memória: mata (OOMKill)# memory: kills (OOMKill) # CPU: só atrasa, em silêncio# CPU: only delays, silently
Não confundir com memória. 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.
CPU Throttling
Agenda · Item 02
Consequência 3Consequence 3

Requests concorrentes serializam

Concurrent requests serialize

1 · Espera de rede1 · Network wait2 · Sob cota2 · Under quota→ avança o estágio→ next stage

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.

0T (≈ 8 s)2T3T Espera de rede · 3 requests em paraleloNetwork wait · 3 requests in parallel req 1 req 2 req 3 todas terminam em ~Tall finish at ~Tesperar é de graça: nada é divididowaiting is free: nothing is shared Sob cota de 100 m · os mesmos 10 ms divididos por 3Under a 100m quota · the same 10 ms split three ways req 1 23,5 s23.5 s req 2 24,8 s24.8 s req 3 26,4 s26.4 s
CPU Throttling
Agenda · Item 02
Consequência 4Consequence 4

Multi-thread torra a cota

Multi-threading burns the quota

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.

Um período de 100 ms · limits.cpu: 100m · 8 threadsOne 100 ms period · limits.cpu: 100m · 8 threads 01,25 ms1.25 ms100 ms thread 1 thread 2 thread 3 thread 4 thread 5 thread 6 thread 7 thread 8 8 × 1,25 ms = 10 ms de cota8 × 1.25 ms = 10 ms of quotacontainer inteiro parado por 98,75 mswhole container stopped for 98.75 ms
  • JVM: GC paralelo com N threads esgota a cota num instante → GC pauses gigantes com limits.cpu baixo.JVM: parallel GC with N threads drains the quota in an instant → huge GC pauses with a low limits.cpu.
  • Apache/PHP: MaxRequestWorkers muito maior que a cota em cores.Apache/PHP: MaxRequestWorkers far above the quota in cores.
  • Node: UV_THREADPOOL_SIZE (default 4) mais o event loop.Node: UV_THREADPOOL_SIZE (default 4) plus the event loop.
  • Go: GOMAXPROCS = cores do , não da cota.Go: GOMAXPROCS = cores of the node, not of the quota.
Regra prática: threads ativas ≈ cota em cores. Com 100 m, qualquer paralelismo é um multiplicador de throttling, não de velocidade.Rule of thumb: active threads ≈ quota in cores. At 100m, any parallelism multiplies throttling, not speed.
CPU Throttling
Agenda · Item 03
Por que o gráfico menteWhy the graph lies

O painel mostra o que executou — não o que esperou

The dashboard shows what ran — not what waited

1 · O que o painel mostra1 · What the panel shows2 · O que ele esconde2 · What it hides→ avança o estágio→ next stage

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.

CPU usage · pod app-a rate(container_cpu_usage_seconds_total[5m]) 0100 m300 m500 m limit 100 m "usa 100 m, está tranquilo""using 100 m, looks fine"a leitura natural — e erradathe natural reading — and the wrong one rate(container_cpu_cfs_throttled_seconds_total[5m]) parado 4× mais do que executandostopped 4× longer than runningthrottled 51,9 s × usage 12,7 s (cpu.stat)throttled 51.9 s vs usage 12.7 s (cpu.stat) AnalogiaAnalogy HidrômetroWater meter mede o que passou pelo cano:measures what went through the pipe: vazão constante e baixa.a low, steady flow. uso "100 m"usage "100 m" Torneira meio fechadaHalf-closed tap o hidrômetro não mostra a filathe meter doesn't show the queue de baldes esperando.of buckets waiting. fila = throttled_secondsqueue = throttled_seconds
CPU Throttling
Agenda · Item 03
As métricas certas · cAdvisorThe right metrics · cAdvisor

Quatro séries, uma regra de leitura

Four series, one reading rule

MétricaMetricO que medeWhat it measuresEstá no painel padrão?On the default panel?Como lerHow to read it
container_cpu_usage_seconds_totalQuanto o container conseguiu executarHow much the container managed to runSimYesPor construção ≤ limite. Encostado no limite = suspeita, não conforto.≤ limit by construction. Flat at the limit = suspicion, not comfort.
container_cpu_cfs_periods_totalPeríodos de 100 ms decorridos100 ms periods elapsedNãoNoDenominador da fração throttled.Denominator of the throttled ratio.
container_cpu_cfs_throttled_periods_totalPeríodos em que houve throttlingPeriods in which throttling happenedNãoNothrottled / periods > 25% sustentado já dá latência visível.throttled / periods > 25% sustained already means visible latency.
container_cpu_cfs_throttled_seconds_totalSegundos parado esperando cotaSeconds stopped waiting for quotaNãoNorate() > 1 = mais tempo parado do que rodando.rate() > 1 = more time stopped than running.
Regra: se 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.

CPU Throttling
Agenda · Item 04
Diagnóstico · pelo lado de foraDiagnosis · from the outside

Sem acesso ao pod: três testes com curl

No access to the pod: three tests with curl

# 1. TTFB × total: onde o tempo vai?# 1. TTFB vs total: where does the time go? curl -s -o /dev/null \ -w "total=%{time_total} ttfb=%{time_starttransfer}\n" \ https://host/report # ttfb pequeno, total grande → o tempo está gerando o corpo# small ttfb, large total → time is spent generating the body # (no caso: primeiro byte em 40 ms, total 8 s)# (in the case: first byte at 40 ms, total 8 s) # 2. Sinal de cota: N requests simultâneas serializam?# 2. Quota signal: do N concurrent requests serialize? for i in 1 2 3; do curl -s -o /dev/null -w "total=%{time_total}\n" https://host/report & done; wait # paralelo (todos ~T) → espera de rede / timeouts# parallel (all ~T) → network wait / timeouts # serial (T, 2T, 3T) → CPU ou lock — suspeite de cota# serial (T, 2T, 3T) → CPU or lock — suspect the quota # (no caso: 23,5 / 24,8 / 26,4 s)# (in the case: 23.5 / 24.8 / 26.4 s) # 3. Mesmo código em outro pod com outra cota → compara# 3. Same code in another pod with another quota → compare kubectl get deploy X -o jsonpath='{.spec.template.spec.containers[*].resources}'
  • TTFB baixo + total alto descarta DNS, conexão e handshake: o servidor já respondeu; o tempo está no processamento.Low TTFB + high total rules out DNS, connection and handshake: the server already answered; the time is in processing.
  • Serialização é o teste mais barato e mais discriminante: rede não serializa; cota (ou lock) serializa.Serialization is the cheapest and most discriminating test: the network doesn't serialize; a quota (or a lock) does.
  • Comparar cotas fecha o caso: mesmo código, limits.cpu diferente, tempo proporcional.Comparing quotas closes the case: same code, different limits.cpu, proportional time.
Três comandos, menos de um minuto, antes de abrir o pod. Se os três apontam para CPU, o próximo passo é a prova de dentro.Three commands, under a minute, before opening the pod. If all three point at CPU, the next step is the proof from inside.
CPU Throttling
Agenda · Item 04
Diagnóstico · de dentro do podDiagnosis · from inside the pod

A prova: cpu.stat e real × cpu

The proof: cpu.stat and real vs cpu

# cgroup v2 cat /sys/fs/cgroup/cpu.max # "quota periodo"# "quota period" cat /sys/fs/cgroup/cpu.stat # nr_periods, nr_throttled, ... # cgroup v1 cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us /sys/fs/cgroup/cpu/cpu.cfs_period_us cat /sys/fs/cgroup/cpu/cpu.stat # Delta durante uma carga: antes → gera carga → depois.# Delta under load: read → generate load → read again. # nr_throttled crescendo quase igual a nr_periods = severo.# nr_throttled growing almost as fast as nr_periods = severe. # Trabalho isolado: real >> cpu consumida = esperando cota (ex. em PHP)# Isolated work: real >> cpu consumed = waiting for quota (PHP example) php -r '$t=microtime(true); /* work */ $r=getrusage(); printf("real=%.2f cpu=%.2f\n", microtime(true)-$t, $r["ru_utime.tv_sec"]+$r["ru_utime.tv_usec"]/1e6 +$r["ru_stime.tv_sec"]+$r["ru_stime.tv_usec"]/1e6);'
Leitura no pod (caso real)Read in the pod (real case)ValorValueSignificaMeaning
cpu.max10000 100000100 m
nr_periods1571períodos decorridosperiods elapsed
nr_throttled113772% dos períodos (98% durante os requests)72% of periods (98% during the requests)
throttled_usec51.880.18451,880,18451,9 s parado51.9 s stopped
usage_usec12.739.72412,739,72412,7 s executando12.7 s running
loop 141 decodes141-decode loopreal 8,58 s · cpu 0,96 sreal 8.58 s · cpu 0.96 s89% do tempo esperando89% of the time waiting
4× mais tempo parado do que executando. Não é lentidão de código — é o kernel segurando o processo em 98% dos períodos.4× more time stopped than running. It's not slow code — it's the kernel holding the process back in 98% of the periods.
CPU Throttling
Agenda · Item 04
Prometheus · OpenShift Observe → Metrics

Três queries e um alerta — configure uma vez

Three queries and one alert — set it up once

# fração de períodos throttled (>25% sustentado já dá latência visível)# fraction of throttled periods (>25% sustained already means visible latency) sum by (pod) (rate(container_cpu_cfs_throttled_periods_total{namespace="NS",container!=""}[5m])) / sum by (pod) (rate(container_cpu_cfs_periods_total{namespace="NS",container!=""}[5m])) # segundos parados por segundo (>1 = mais tempo parado do que rodando)# seconds stopped per second (>1 = more time stopped than running) sum by (pod) (rate(container_cpu_cfs_throttled_seconds_total{namespace="NS",container!=""}[5m])) # uso × limite, para colocar no mesmo painel# usage vs limit, to put on the same panel sum by (pod) (rate(container_cpu_usage_seconds_total{namespace="NS",container!=""}[5m])) / sum by (pod) (kube_pod_container_resource_limits{namespace="NS",resource="cpu"})
# PrometheusRule (sugestão)# PrometheusRule (suggestion) - alert: CPUThrottlingHigh expr: | sum by (namespace, pod) (rate(container_cpu_cfs_throttled_periods_total{container!=""}[5m])) / sum by (namespace, pod) (rate(container_cpu_cfs_periods_total{container!=""}[5m])) > 0.25 for: 10m labels: { severity: warning } annotations: summary: "{{ $labels.pod }} throttled em {{ $value | humanizePercentage }} dos períodos""{{ $labels.pod }} throttled in {{ $value | humanizePercentage }} of periods"
  • > 25% por 10 min: limiar sugerido. Acima disso a latência já é visível para um usuário só.> 25% for 10 min: suggested threshold. Above that, latency is already visible to a single user.
  • Coloque uso × limite e throttled no mesmo painel: a linha encostada ganha contexto.Put usage vs limit and throttled on the same panel: the flat line gets context.
  • Uma vez configurado, nunca mais se cai nessa.Set it up once and you never fall for this again.
CPU Throttling
Agenda · Item 05
Como corrigirHow to fix it

Do mais barato ao mais estrutural

From the cheapest to the structural

01

Subir o limiteRaise the limit

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.

02

Remover o limiteRemove the limit

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.

03

Reduzir o trabalhoReduce the work

Cota baixa é uma lupa sobre desperdício. 141 leituras + decodes do mesmo JSON → cachear em static1 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 static1 read. Helps every pod, under any quota.

04

Ajustar o períodoTune the period

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.

05

Threads = cotaThreads = quota

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.

No caso: 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.
Subir cota é teste, não cura. A cura é medir throttling e reduzir CPU por request. Lembrete: 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).
CPU Throttling
Agenda · Item 05
Para qualquer projeto "lento sem motivo"For any "slow for no reason" project

Checklist

Respostas salvas neste navegador (localStorage · cpu-throttling-checklist).Answers saved in this browser (localStorage · cpu-throttling-checklist).

CPU Throttling
FechamentoClosing
Contra-intuições para fixarCounter-intuitions to remember

Cinco frases para levar embora

Five sentences to take home

01

Cota não é velocidade; é tempo por janela.Quota isn't speed; it's time per window.

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.

02

Nó ocioso não ajuda.An idle node doesn't help.

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.

03

Um único usuário sofre.A single user suffers.

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.

04

Subir cota é teste, não cura.Raising the quota is a test, not a cure.

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%.

05

Serialização é a assinatura.Serialization is the signature.

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.

"Uso abaixo do limite" "sobra CPU". Pode significar "não deixaram usar"."Usage below the limit" "spare CPU". It may mean "it wasn't allowed to use it".