trilha

System Design/03 - Qualidade/Performance Antipatterns2 min

Chatty IO

Perguntas-guia
  • Que problema isso resolve?
  • Quando usar / quando NÃO usar?
  • Qual o principal trade-off?
  • Como isso falha em produção?

Conceito

Muitas requisições pequenas onde poucas maiores resolveriam. O custo não está no volume de dados — está no custo fixo por operação, pago repetidamente.

Cada ida e volta carrega latência de rede, handshake, serialização, autenticação e enfileiramento. Com 1 ms de latência e 1.000 chamadas, são 1.000 ms gastos em espera pura.

Onde aparece

Forma Exemplo
N+1 em ORM Carregar 100 pedidos, depois o cliente de cada um
Laço com chamada remota for id in ids: api.buscar(id)
Leitura/escrita registro a registro INSERT em laço em vez de lote
Muitas mensagens minúsculas Overhead do protocolo domina
API granular demais Uma chamada por campo

A correção

Técnica Como
Lote WHERE id IN (...), insertMany, mget
Junção / eager loading Trazer o relacionado na mesma consulta
Endpoint composto Uma chamada devolve o agregado (Gateway Aggregation)
Cache Elimina a repetição (Caching)
Streaming Um canal contínuo em vez de N requisições

Trade-offs

A correção tem limite: agrupar demais leva ao antipadrão oposto, Extraneous Fetching — uma consulta gigante que traz muito mais do que se usa, consome memória e trava por mais tempo.

Lotes muito grandes também aumentam latência do primeiro resultado, ocupam mais memória e, em caso de falha, perdem o lote inteiro. O tamanho de lote é um parâmetro a medir, não a adivinhar — tipicamente entre 100 e 1.000 itens.

Há ainda um caso em que chatty é aceitável: quando as chamadas podem ser paralelizadas e a latência total passa a ser a da mais lenta, não a soma. Isso reduz o sintoma sem eliminar o custo — cada chamada ainda consome conexão e CPU do servidor.

Exemplo prático

O N+1 clássico, e por que ele é invisível em desenvolvimento:

pedidos = db.query("SELECT * FROM pedidos LIMIT 100")
for p in pedidos:
    p.cliente = db.query("SELECT * FROM clientes WHERE id = ?", p.cliente_id)
# 101 consultas

Com o banco local (0,1 ms), isso custa 10 ms — imperceptível. Em produção, com 2 ms de latência de rede, custa 200 ms. Com 1.000 pedidos, 2 segundos.

pedidos = db.query("SELECT * FROM pedidos LIMIT 100")
ids = {p.cliente_id for p in pedidos}
clientes = db.query("SELECT * FROM clientes WHERE id IN (...)", ids)
# 2 consultas

De 101 para 2. A mudança é pequena e o efeito é de ordem de grandeza — e é por isso que N+1 é o primeiro item a procurar em qualquer trace lento.

Relacionado


Parte de Performance Antipatterns · roadmap.sh/system-design

Buscar

Busca por título, seção e texto das notas