Somei o tempo de CPU por user-agent em 7 dias: 63% foi para cliente que nunca abre a segunda página
A conta do provedor subiu uns 38% em um trimestre e a reunião já tinha um culpado: a funcionalidade de IA que entrou em abril. Duas instâncias a mais estavam na mesa para aprovação. Antes disso eu pedi quarenta minutos e o log de acesso dos últimos sete dias.
O que está aqui vale para sistema que publica muita página gerada na hora: catálogo com filtro combinável na URL, relatório que aceita intervalo de datas, histórico paginado, busca com parâmetro indexável. Se o seu caso é site estático atrás de CDN, ou aplicação inteira atrás de login, pode parar de ler agora, porque a sua fatura subiu por outro motivo.
Primeiro problema: o log padrão não registra tempo
O formato combined do nginx não guarda tempo de resposta. Sem tempo você só consegue contar requisição, e contagem engana: rota barata em volume alto parece o vilão, rota cara em volume baixo desaparece. Antes de qualquer conta, o formato precisa mudar.
log_format timed '$remote_addr\t$time_iso8601\t$status\t$request_time\t'
'$upstream_response_time\t$request_uri\t$http_user_agent';
access_log /var/log/nginx/timed.log timed;
Tab como separador porque user-agent tem espaço, aspas e vírgula, e todo parser caseiro morre nisso mais cedo ou mais tarde.
A conta que muda a reunião
Sete dias de log, três baldes: quem se declara robô, quem parece navegador de verdade, e o resto. O terceiro grupo é o interessante, porque ele chega com Mozilla/5.0 completo e nunca pede um arquivo estático.
awk -F'\t' '
{ t = $5 + 0; ua = $7 }
ua ~ /(bot|crawler|spider|Bytespider|GPTBot|ClaudeBot|SemrushBot)/ {
d["bot declarado"] += t; n["bot declarado"]++; next }
ua ~ /Mozilla\/5\.0/ {
d["navegador"] += t; n["navegador"]++; next }
{ d["sem ua util"] += t; n["sem ua util"]++ }
END { for (k in d) printf "%-16s %12.1fs %10d req %7.0f ms/req\n",
k, d[k], n[k], d[k]/n[k]*1000 }
' timed.log | sort -k2 -rn
Saída:
sem ua util 17941.2s 241883 req 74 ms/req
navegador 6907.3s 612044 req 11 ms/req
bot declarado 3412.8s 58201 req 59 ms/req
Um terço das requisições, 63% do tempo de processamento. E repare no ms/req: quem não carrega asset nenhum pede justamente as rotas caras, porque asset é barato e enche a média do balde do navegador.
Separar robô de gente sem acreditar no user-agent
O user-agent é declaração, não evidência. O comportamento é evidência, e o mais simples deles é este: gente que abre uma página carrega o CSS e o JS junto. Coletor não carrega.
awk -F'\t' '
$6 ~ /^\/(static|assets|_next)\// { asset[$1]++; next }
$3 == 200 && $6 !~ /\./ { page[$1]++ }
END { for (ip in page) if (asset[ip] == 0) print page[ip], ip }
' timed.log | sort -rn | head -n 5
9412 45.xxx.xxx.xxx
8877 5.xxx.xxx.xxx
6301 185.xxx.xxx.xxx
4188 2a0d:xxxx::xxx
3970 91.xxx.xxx.xxx
Nove mil páginas HTML, zero folhas de estilo. Fica difícil sustentar a tese de que tem alguém do outro lado esperando a página renderizar.
A segunda medição explica por que o cache não ajudou nada:
awk -F'\t' '{print $6}' timed.log | sort | uniq -c | \
awk '{s += $1; n++} END {printf "%d urls distintas, %.2f req por url\n", n, s/n}'
418902 urls distintas, 2.18 req por url
Media de 2,18 acessos por endereço em uma semana. Para a maior parte desses endereços, a requisição do coletor é a primeira e a última. Você paga a geração inteira e não reaproveita nada, e é por isso que aumentar o tamanho do cache não move a agulha.
O kernel.org fez essa conta em público
Konstantin Ryabitsev, que cuida da infraestrutura do kernel.org, contou que o git.kernel.org queima mais ciclos de CPU renderizando commits em HTML para scrapers do que gasta com todo o acesso legítimo somado, incluindo git clone. São cinco nós geograficamente distribuídos e, a qualquer momento, catorze núcleos ocupados só com isso. Simon Willison publicou o relato no site dele e a discussão foi parar no Hacker News.
A forma do problema é a mesma da sua aplicação. Interface web de git multiplica commits por arquivos por visões possíveis (diff, blame, raw, árvore) e produz um espaço de endereços que ninguém consegue estimar de cabeça. Seu catálogo com quatro filtros combináveis na query string faz exatamente a mesma multiplicação, com menos glamour.
robots.txt não segura, e negar tarde também custa
O robots.txt é um pedido educado, e quem respeita já respeitava. O tráfego que aparece no primeiro balde da tabela acima sai de faixas residenciais que giram a cada punhado de requisições e não carrega nome para bloquear.
Negar tem custo: quando a sua regra decide barrar, o handshake TLS já aconteceu, o roteamento já aconteceu, alguém já leu o cabeçalho. O que muda é a ordem de grandeza, porque devolver 403 sai muito mais barato que montar um diff ou varrer dois anos de histórico. Só que a regra escrita dentro da aplicação, depois de abrir conexão com o banco, devolve quase nada. Ela precisa estar antes.
map $http_user_agent $suspeito {
default 0;
~*(bot|crawler|spider) 1;
"" 1;
}
limit_req_zone $binary_remote_addr zone=pesado:10m rate=1r/s;
location ~ ^/(relatorio|historico|busca) {
limit_req zone=pesado burst=5 nodelay;
if ($suspeito) { return 403; }
proxy_pass http://app;
}
O que moveu o número
Quatro mudanças, nenhuma com instância nova. Versão barata das rotas caras para quem não é gente, com a mesma informação e sem as consultas que montam a visão completa. Cache com chave por combinação de parâmetros e TTL generoso, já que ali o conteúdo muda pouco. Canonical apontando para a versão sem filtro, tirando a combinatória do índice. Limite por faixa e por sistema autônomo na borda.
Duas semanas depois, a fatia do primeiro balde caiu para perto de 20% do tempo de processamento, e o p95 das rotas de relatório caiu quase pela metade. As duas instâncias continuam não aprovadas.
Uma coisa que essa medição não responde: se o coletor que consome a sua CPU alimenta um buscador que te traz gente. Log separa robô de gente pelo comportamento, não pela intenção, e a decisão de quem merece catorze núcleos é sua.
Quem aqui já separou o log por classe de cliente? Qual foi a fatia do tráfego que chega com user-agent de navegador e nunca pede um asset, e o que vocês usaram para classificar sem cair em bloqueio por faixa de IP?
· · ·
Publicado originalmente no blog da Revin: https://revin.com.br/pt/blog/bots-scraping-conta-de-nuvem