Pular para o conteúdo

Carga, fila e latência

Avançado16 min de leituraconfiabilidade

Um comportamento que confunde muita gente: o serviço está com 70% de CPU e a latência dobrou. A intuição diz que ainda há 30% de folga, então o desempenho deveria estar inalterado. A teoria de filas diz o contrário — e ela explica boa parte dos incidentes de lentidão.

A relação mais útil de toda a teoria de filas:

L = λ × W
L = itens no sistema (requisições em andamento)
λ = taxa de chegada (requisições por segundo)
W = tempo médio no sistema (latência)

Ela é exata e não depende de distribuição nenhuma. Serve para calcular o que você não está medindo:

  • 500 req/s com latência de 200 ms → 100 requisições simultâneas em andamento. É esse número que precisa caber no seu pool de threads e de conexões.
  • Fila com 2000 mensagens sendo consumidas a 50/s → 40 segundos de espera até a última.
  • Pool com 20 conexões e consulta de 50 ms → teto de 400 req/s, por mais CPU que sobre.

O último caso é o mais comum: o gargalo não é CPU, é concorrência limitada por um recurso compartilhado.

Para um sistema com chegada e serviço variáveis, o tempo de espera cresce aproximadamente com:

espera ∝ ρ / (1 − ρ) onde ρ = utilização (0 a 1)

O que isso produz:

Utilização Fator de espera relativo
50%
70% 2,3×
80%
90%
95% 19×
99% 99×

Entre 50% e 70% quase nada acontece. De 80% em diante, cada ponto percentual custa caro. E observe que a curva vai ao infinito antes de 100% — 100% de utilização sustentada não significa desempenho máximo, significa fila crescendo sem limite.

É por isso que a folga recomendada em capacidade fica na faixa de 60% a 70% no pico planejado. Não é desperdício: é o que mantém a latência previsível.

O mesmo raciocínio vale para pessoas. Um time 100% alocado forma fila de trabalho, exatamente como um servidor saturado.

Duas outras fontes de espera, além da utilização média:

  • Rajada. Chegada irregular cria fila mesmo com utilização média baixa. Média não descreve pico.
  • Variabilidade do tempo de serviço. Se algumas requisições demoram muito mais que outras, elas bloqueiam a fila atrás delas — e o p99 do sistema passa a ser ditado por poucos casos lentos.

Daí uma recomendação prática: separe filas por classe de trabalho. Uma requisição pesada de relatório não deve compartilhar o pool com as requisições rápidas de checkout.

Toda fila ocupa memória em algum lugar: buffer de socket, fila do runtime, thread pool, mensagens acumuladas. Fila ilimitada é vazamento de memória com outro nome, e ela tem um efeito perverso — quanto maior a fila, maior a latência de tudo que entra nela.

Pior: em fila muito longa, boa parte do trabalho já é inútil quando começa a ser processado, porque o cliente desistiu ou já retentou. O sistema gasta capacidade em respostas que ninguém vai ler.

Por isso: filas curtas e limitadas, com descarte explícito ao encher. Rejeitar rápido é melhor que aceitar e demorar.

Quando a chegada supera a capacidade, existem exatamente três saídas: enfileirar (adia e piora), rejeitar, ou desacelerar quem envia. As duas últimas são backpressure.

Formas práticas:

  • Limite de concorrência (semáforo) na entrada de cada dependência.
  • Limite de taxa por cliente, com resposta 429 e Retry-After.
  • Fila limitada que descarta ou recusa quando cheia.
  • Deadline propagado: se o cliente já desistiu, não processe.
  • Descarte por prioridade: preserve checkout, rejeite relatório.

A regra que costuma inverter a intuição: rejeitar 5% do tráfego com erro rápido é melhor que degradar 100% para além do aceitável. Um serviço que recusa mantém o SLO para a maior parte; um que aceita tudo entra no colapso metaestável descrito em modos de falha.

Se você olha apenas CPU, vai ver o problema tarde. Os sinais que antecipam:

# saturação: fila, espera e throttling importam mais que utilização
sum by (pod) (rate(container_cpu_cfs_throttled_seconds_total[5m]))
  • Profundidade de fila e tempo de espera nela.
  • Conexões em uso / tamanho do pool — alerte acima de 80%.
  • Requisições em andamento (o L da lei de Little).
  • p99 junto do p50: a distância entre eles cresce antes da média piorar.
  • Taxa de rejeição — se você implementou backpressure, ela é um sinal, não um erro.