14 de setembro
Passamos semanas esperando por esse dia. Nosso provedor de proxies residenciais tinha ficado fora do ar por uma falha de cobrança ainda em julho, e por dois meses uma fatia cada vez maior das nossas 1.375 fontes vinha sendo rastreada de forma degradada ou nem isso — requisições expirando, IPs sendo bloqueados no meio da sessão, o tipo de sangria lenta que não aparece como uma queda, mas como um número cada vez menor que ninguém percebe até ficar embaraçoso. Nós renovamos. Pool novo, 20.000 linhas de sessão residenciais rotativas, cada saída testada antes de deixar qualquer coisa chegar perto da produção. 100% do teste passou. AE, DE, JP — tudo no ar, tudo limpo.
Então rodamos um crawl supervisionado, do tipo em que alguém realmente observa o painel em vez de confiar no cron. Eu esperava o oposto de um desastre.
O que tivemos foi um banco de dados que havia perdido 84.401 linhas em uma única passagem, com 102 fontes caindo mais de 25% em relação ao snapshot anterior. Pior do que os dois meses de proxies escassos que tínhamos acabado de resolver.
A teoria errada, em poucas palavras
Meu primeiro instinto — e vou admitir isso abertamente — foi que o pool novo era, de alguma forma, pior do que o teste sugeria. Talvez os IPs de sessão estivessem sendo limitados por taxa no meio do crawl de um jeito que um teste pontual não detectaria. É uma teoria razoável. Também está errada, e acreditar nela por meio dia já nos custou um tempo que não tínhamos, porque o cron de crawl fica pausado durante todo o período em que se está depurando uma teoria assim — não se quer que uma passagem ruim sobrescreva um bom snapshot enquanto ainda se está adivinhando.
O indício que derrubou a teoria foi o dubizzle. Uma das nossas fontes de maior volume nos Emirados Árabes Unidos, ele na verdade buscou 32.595 anúncios durante aquela execução — um número perfeitamente saudável, com os proxies claramente funcionando — e depois descartou quase todos eles. Não falhou ao buscar. Buscou, e então jogou fora.
16 de setembro: encontrando o bug real
Foi quando parou de ser uma história de proxies e se tornou uma história de substituição de linhas. Nossa camada de persistência registra cada fonte como uma sobrescrita completa do <site>_cars_for_sale.csv a cada passagem — apenas os anúncios atuais, substituindo a tabela inteira. Esse é o comportamento correto quando um crawl termina. É catastrófico quando um crawl expira no meio do caminho e o scraper grava o lote parcial que conseguiu coletar como se fosse o snapshot completo. Uma fonte que normalmente mantém 9.000 anúncios ativos é sobrescrita pelos 1.200 que conseguiu raspar antes de o tempo esgotar, e os outros 7.800 desaparecem — não porque os carros foram vendidos, mas porque nosso próprio código decidiu que uma busca incompleta era uma busca completa.
Os proxies não estavam mentindo para nós. A lógica de novas tentativas era o problema, e ela já estava silenciosamente ruim havia muito tempo, apenas mascarada por um pool mais saudável que costumava terminar as buscas antes que a varredura importasse.
Eis o que essa varredura realmente custou, depois que fizemos as contas:
| Varredura de retentativas antiga | Varredura corrigida | |
|---|---|---|
| Retentativas por proxy | 3 | 1 |
| Atraso por tentativa | 1s | — |
| Teto efetivo em um pool de 50 proxies | ~56 minutos por URL | orçamento total de 120 segundos |
| Comportamento em caso de expiração | grava o lote parcial como snapshot completo | fonte ignorada, snapshot anterior mantido |
Cinquenta e seis minutos para desistir de uma única URL. Multiplique isso por um crawl que toca mais de mil fontes e você obtém exatamente o tipo de falha silenciosa e desigualmente distribuída que vimos — algumas fontes bem, 102 delas destroçadas, nada nos logs gritando "falha de proxy" porque, tecnicamente, não havia nenhuma.
25 de setembro: a correção, e a parte que importa para você
O patch lançado nove dias depois fez duas coisas pouco glamorosas. Ele limitou a matemática das retentativas para que uma busca travada falhe rápido em vez de consumir quase uma hora, e — esta é a que de fato evita a perda de dados — uma busca parcial não substitui mais o conjunto completo de linhas. Se uma fonte só consegue completar 1.200 dos seus habituais 9.000 anúncios, mantemos o snapshot completo anterior em vez de sobrescrevê-lo com uma fração. No mesmo dia, entrou uma segunda mudança que grava uma flag visível CRAWL_PAUSED que nosso monitoramento verifica antes de alertar alguém sobre uma queda — assim, uma pausa deliberada é lida como pausa, não como alerta.
Por que um leitor procurando um Hyundai usado deveria se importar com qualquer coisa disso? Porque os sinalizadores de abaixo do mercado que publicamos — 12.823 deles ativos no site nesta semana — só são honestos se o grupo com o qual comparamos um anúncio for o mercado regional real e completo, e não um fragmento truncado por expiração de tempo. Um selo de oferta construído contra um snapshot que perdeu três quartos do seu conjunto de comparação não é um selo de oferta, é ruído vestindo um rótulo verde. Preferimos pausar um crawl por nove dias do que publicar isso.
O conjunto de dados hoje está em 1.108.535 anúncios em 73 regiões e 1.375 fontes — maior do que era antes de tudo isso começar, com os proxies renovados e o bug de substituição de linhas corrigido. Mas o número ao qual continuo voltando não é o total. É 84.401 — a diferença entre um crawl que parecia saudável na camada de proxies e um que realmente era.



