Blog

Cenário: o índice que faltava e derrubou a Black Friday.

Este é um cenário montado para estudo, não o incidente de nenhuma empresa específica. A anatomia se repete com frequência incômoda em sistema de varejo, e é bem mais instrutiva quando destilada.

O que acontece

Sexta-feira, 20h. Início da campanha. Tráfego sobe cinco vezes, o que era esperado e planejado.

20h14 o tempo de resposta da listagem de pedidos começa a subir. 20h22 o pool de conexão do banco satura. 20h25 o site inteiro está fora, incluindo as páginas que não tocam o banco, porque a aplicação está com todas as threads esperando conexão.

A causa

Uma query de dois anos atrás:

select * from orders
where customer_id = $1
order by created_at desc
limit 20;

Existia índice em customer_id. Não existia índice composto com created_at.

Durante dois anos isso não importou, porque o cliente médio tinha oito pedidos. O banco usava o índice, trazia oito linhas, ordenava em memória, devolvia. Quarenta milissegundos.

Na campanha, dois fatores mudaram ao mesmo tempo. Os clientes recorrentes, que são os que mais compram em promoção, tinham centenas de pedidos acumulados ao longo de dois anos. E o volume de chamadas dessa query multiplicou.

Para um cliente com quatrocentos pedidos, o banco trazia quatrocentas linhas completas, com todas as colunas, ordenava e descartava 380. Sozinho, ainda rápido. Multiplicado pelo tráfego, o banco passou a mover dezenas de vezes mais dado do que precisava, o cache de páginas foi expulso, e o disco entrou na conta.

O que era linear virou não linear porque duas variáveis cresceram juntas.

Por que ninguém viu chegando

O teste de carga rodou. Com base de teste, onde nenhum cliente tinha mais de vinte pedidos.

Esse é o ponto que vale levar embora: teste de carga com dado sintético uniforme não reproduz o comportamento de produção, porque produção tem distribuição de cauda longa. A média não te ajuda. O cliente do percentil 99 é o que derruba o sistema, e ele não existe na sua base de teste.

O monitoramento também não ajudou, porque olhava tempo médio de query. A média estava em 60ms. O percentil 99 estava em 4 segundos havia semanas, subindo devagar, e ninguém tinha esse gráfico.

A correção

Durante o incidente, uma linha:

create index concurrently idx_orders_customer_created
  on orders (customer_id, created_at desc);

CONCURRENTLY porque criar índice comum numa tabela daquele tamanho travaria a escrita, e travar a escrita durante uma campanha é trocar um problema por outro pior. Levou onze minutos e o sistema voltou antes de terminar, porque o planejador começou a usar o índice assim que ele ficou válido.

Com o índice composto, o banco desce direto na posição certa e lê vinte linhas. Não ordena, porque o índice já está na ordem. Tempo de resposta em um dígito de milissegundo, independente de quantos pedidos o cliente tenha.

Escrevi sobre a regra de ordenação de colunas em índices no Postgres: igualdade primeiro, faixa e ordenação por último.

O que foi feito depois

Três coisas, em ordem de valor.

Percentil no lugar de média. Todo painel passou a mostrar p95 e p99. A média esconde exatamente o caso que derruba o sistema, e olhar média dá uma sensação de saúde que não corresponde a nada.

Base de teste com distribuição real. Não a base de produção, por razões óbvias de dado pessoal. Um gerador que reproduz a forma da distribuição: a maioria dos clientes com poucos pedidos, alguns com centenas, um punhado com milhares.

Limite de tempo por query. statement_timeout configurado. Uma query que passa de alguns segundos é abortada em vez de segurar a conexão. Não conserta a causa, e impede que uma query ruim consuma o pool inteiro e derrube o que não tinha nada a ver.

alter role app_user set statement_timeout = '5s';

Essa última é a que eu recomendaria pra qualquer sistema que ainda não tenha. Custa uma linha e transforma queda total em degradação parcial.

O aprendizado mais desconfortável

O código não mudou. O índice estava faltando desde o começo.

O que mudou foi o dado: dois anos de acúmulo, e uma campanha que trouxe justamente os clientes com mais histórico.

Sistema envelhece sem ninguém tocar nele. A query que era rápida em janeiro pode não ser em dezembro, e nenhum deploy vai avisar. A única defesa é olhar o percentil alto de vez em quando, e perguntar, antes de cada pico previsível, quais consultas dependem de quantidade que cresce com o tempo.

← Voltar pro blog