Extração de dados

Construindo um Gerenciador de Proxy Residencial: Rotação, Verificações de Saúde e Tentativas

A lógica que decide qual saída usar, quando retirá-la de uso e como se recuperar deve ficar em um único componente. Veja como projetá-lo e qual estado ele precisa.

Chris Collins

Chris Collins

26 de agosto de 2026 · 10 min de leitura

Na maioria das bases de código de scraping, cresce um gerenciador de proxy, independentemente de alguém tê-lo projetado ou não. Começa como um helper que constrói uma URL de proxy, depois alguém adiciona uma nova tentativa, depois um caso especial para um destino que precisa de sessões persistentes, depois um contador para parar de martelar um endereço que continua falhando. Dezoito meses depois, essa lógica está espalhada por quatro módulos e ninguém consegue dizer o que acontece quando uma requisição falha duas vezes na mesma sessão.

Vale a pena construir isso deliberadamente, porque as decisões envolvidas são genuinamente compartilhadas: qual saída essa requisição usa, essa saída está saudável, o que acontece quando ela falha, e como controlamos o ritmo disso. Aqui está um design para esse componente e o estado que ele precisa manter.

Pelo que o gerenciador é responsável

Trace o limite com rigor. Um gerenciador de proxy decide como uma requisição sai da sua infraestrutura e o que fazer quando isso falha. Ele não analisa páginas, não sabe como é uma listagem de produto, e não decide quais URLs visitar. Mantê-lo ignorante da sua lógica de negócio é o que permite que todo job o compartilhe.

Isso deixa quatro responsabilidades: selecionar uma saída para cada requisição de acordo com uma política, rastrear a saúde do que é entregue, aplicar o tratamento de falhas quando algo dá errado, e impor o ritmo para que os chamadores não sobrecarreguem individualmente um destino. Tudo o mais pertence a outro lugar.

Seleção: a política é por job, não global

A primeira coisa que o gerenciador precisa é uma noção do tipo de saída que um chamador quer, em vez de um único modo global. Na prática, existem três.

Rotativo, em que cada requisição recebe uma saída nova. Este é o padrão para coleta em massa e é expresso omitindo um identificador de sessão, que é como o modelo de rotação foi projetado para ser usado.

Persistente (sticky), em que um chamador mantém uma saída para uma sequência, e o gerenciador deve devolver o mesmo identificador de sessão durante toda a vida dessa sequência, e depois liberá-lo. As compensações estão em sticky versus rotating.

Fixado por geografia (geo-pinned), em que a saída precisa estar em um país ou cidade específico, o que, para trabalhos multi-mercado, é ortogonal aos dois primeiros: um job pode ser rotativo-na-Alemanha ou persistente-em-Chicago.

Modele isso como um lease com escopo de requisição em vez de uma configuração global, para que um chamador peça o que precisa e o gerenciador atenda:

from dataclasses import dataclass
from typing import Optional
import itertools, time

@dataclass
class Lease:
    country: str
    session: Optional[str] = None          # None means rotate per request
    ttl: Optional[int] = None              # only meaningful with a session

    def username(self, customer="USERNAME"):
        parts = [f"customer-{customer}", f"country-{self.country}"]
        if self.session:
            parts.append(f"sid-{self.session}")
            if self.ttl:
                parts.append(f"ttl-{self.ttl}")
        return "-".join(parts)

    def proxies(self, password="PASSWORD", host="p.shifter.io:443"):
        url = f"http://{self.username()}:{password}@{host}"
        return {"http": url, "https": url}

O trabalho do gerenciador é produzir um Lease, entregá-lo ao chamador, e observar o que acontece com ele.

Saúde: rastreie sessões, não endereços

Aqui é onde a maioria dos gerenciadores caseiros erra. Em um gateway agrupado, você não mantém uma lista de endereços IP para marcar como bons ou ruins, porque você nunca os escolheu e não pode mantê-los. O que você pode rastrear é a saúde de uma sessão, e a saúde agregada de uma rota, ou seja, uma combinação de país e destino.

Então mantenha duas coisas. Por sessão persistente ativa, um pequeno registro de falhas consecutivas e respostas de desafio, para que uma sessão que claramente ficou ruim possa ser retirada e substituída por um identificador novo. Por rota, uma taxa de sucesso móvel, que indica se toda uma combinação país-destino se degradou, em vez de uma sessão com má sorte isolada.

Uma verificação de saúde, nesse contexto, não é um ping periódico. Fazer ping em uma saída indica que ela consegue alcançar um endpoint de teste, o que não é a questão; a questão é se ela consegue alcançar o seu destino e obter conteúdo real. Então use verificação de saúde passiva: cada requisição real é a verificação de saúde, e seu resultado validado atualiza o registro. Sondagem ativa só vale a pena para um canário pequeno e barato contra cada destino, útil para distinguir “esse destino está fora do ar para todo mundo” de “nossas rotas para ele se degradaram”.

Crucialmente, julgue a saúde com base em respostas validadas, não em códigos de status. Uma página de desafio com um 200 é uma falha para fins de saúde, e um gerenciador que a conta como sucesso continuará reutilizando uma sessão sobre a qual o destino já tomou uma decisão. Essa é a mesma disciplina de validação de detectar conteúdo bloqueado ou falso.

class RouteHealth:
    def __init__(self, window=50):
        self.window, self.results = window, []

    def record(self, ok: bool):
        self.results.append(ok)
        if len(self.results) > self.window:
            self.results.pop(0)

    @property
    def success_rate(self):
        return sum(self.results) / len(self.results) if self.results else 1.0

    @property
    def degraded(self):
        return len(self.results) >= 10 and self.success_rate < 0.7

Tratamento de falhas: classifique, depois aja

O gerenciador é o responsável pela decisão do que uma falha significa, o que é o que evita que essa lógica seja reinventada por job. Três classes cobrem isso.

Sinais de limite de taxa significam desacelerar: um 429 ou um Retry-After. A resposta correta é esperar, e especificamente não trocar para uma saída nova, para que o mesmo ritmo possa continuar, já que é isso que transforma uma rota estrangulada em uma rota queimada.

Sinais de identidade significam que essa saída está encerrada para esse destino: uma página de bloqueio, um 403 persistente, ou desafios repetidos em uma sessão. A resposta correta é retirar a sessão, obter uma nova, e continuar, que é o padrão de failover.

Erros terminais significam parar: um 404, uma URL malformada, ou um 407, que é um problema de autenticação que será igualmente errado em toda nova tentativa e deve falhar de forma visível em vez de entrar em loop, como abordado em corrigindo erros 407 e de credenciais.

Tudo o mais é transitório e recebe backoff exponencial com jitter, limitado por um teto de tentativas, conforme retry e backoff. O ponto arquitetural importante é que a nova tentativa reside no gerenciador, para que cada chamador herde o mesmo comportamento, e o gerenciador possa impor um orçamento de tentativas entre chamadores em vez de cada job tentar novamente de forma independente contra o mesmo destino que já está com dificuldades.

Adicione também um circuit breaker por rota. Quando a saúde de uma rota cai abaixo de um limiar, pare de enviar para ela por um período de resfriamento, depois permita um fluxo mínimo para testar a recuperação. Isso protege o destino do seu acúmulo de requisições e protege sua conta do acúmulo de falhas contra um site que não está respondendo a ninguém.

O ritmo também pertence aqui

Como o gerenciador vê cada requisição de saída, é o lugar natural para impor limites de taxa por destino e tetos de concorrência, que é o mecanismo descrito em limitação de taxa e throttling de requisições. Colocar isso aqui tem um benefício específico: as novas tentativas respeitam automaticamente o limitador, porque passam pelo mesmo caminho que qualquer outra requisição. Uma nova tentativa que contorna o ritmo é como um destino com dificuldades se torna um destino bloqueado.

Juntando tudo

A superfície toda é pequena, e esse é o ponto:

class ProxyManager:
    def __init__(self, limiter_factory, health_factory):
        self.limiters = {}          # host -> TargetLimiter
        self.health = {}            # (country, host) -> RouteHealth
        self.limiter_factory, self.health_factory = limiter_factory, health_factory

    def get(self, url, host, country, session=None, attempts=4):
        route = self.health.setdefault((country, host), self.health_factory())
        limiter = self.limiters.setdefault(host, self.limiter_factory(host))
        sid = session
        for attempt in range(attempts):
            if route.degraded:
                raise RouteUnavailable(country, host)      # circuit open
            limiter.acquire()                              # pacing, retries included
            lease = Lease(country=country, session=sid, ttl=600 if sid else None)
            outcome = self._send(url, lease)               # returns (klass, response)
            route.record(outcome.klass == "ok")
            if outcome.klass == "ok":
                return outcome.response
            if outcome.klass == "terminal":
                raise TerminalError(url, outcome.response)
            if outcome.klass == "identity" and sid:
                sid = new_session_id()                     # retire, do not reuse
            if outcome.klass == "rate":
                time.sleep(outcome.retry_after or backoff(attempt))
            else:
                time.sleep(backoff(attempt))               # transient
        raise Exhausted(url)

Duas notas de design. O gerenciador retorna respostas e lança erros tipados em vez de retornar None, para que os chamadores não possam tratar silenciosamente uma falha como dado vazio. E ele nunca engole um erro terminal, porque um problema de autenticação ou configuração deve interromper um job em vez de ser retentado até a invisibilidade.

Preocupações operacionais

Torne-o observável. O gerenciador vê cada requisição, então é onde a taxa de sucesso por rota, a proporção de novas tentativas, e bytes por requisição devem ser emitidos, que é exatamente o dado a partir do qual as métricas em KPIs de proxy são construídas e o que o monitoramento de pipeline consome.

Compartilhe o estado entre processos. Um gerenciador por processo multiplica sua taxa efetiva pelo número de workers e fragmenta o rastreamento de saúde. Em uma implantação distribuída, limitadores, circuit breakers e a saúde das rotas pertencem a um armazenamento compartilhado, que é a mesma consideração de escalar com Kubernetes.

Mantenha credenciais fora dele. O gerenciador compõe nomes de usuário; ele deve ler a senha de um cofre de segredos em vez de mantê-la na configuração, e a rotação deve ser uma etapa de implantação em vez de uma mudança de código.

Não abstraia demais. Resista a uma arquitetura de plugins para provedores hipotéticos. Um único gateway, expresso de forma limpa, é mais fácil de raciocinar sobre do que uma camada de abstração sobre fornecedores que você não usa.

Conclusão

Um gerenciador de proxy é o componente que decide como uma requisição sai do seu sistema e o que acontece quando ela falha, e centralizar essas decisões é o que impede que a mesma lógica seja reimplementada, de forma inconsistente, em cada job. Dê aos chamadores um lease com escopo de requisição, para que rotação, persistência e geografia sejam por job em vez de globais. Rastreie a saúde no nível de sessão e de rota, em vez de fingir gerenciar endereços individuais, e julgue-a com base em respostas validadas, para que uma página de desafio conte como a falha que ela é. Centralize a classificação de falhas em um único lugar: espere em sinais de limite de taxa, retire a sessão em sinais de identidade, falhe de forma visível em erros terminais, e recue com jitter em tudo o mais, com um circuit breaker por rota. Coloque o ritmo no mesmo caminho para que as novas tentativas não possam contorná-lo. Depois emita métricas a partir dele, porque é o único componente que vê tudo.

Por baixo disso está a rede de proxies residenciais: um único gateway em que país, cidade, sessão e TTL são todos expressos no nome de usuário, o que é o que faz um gerenciador como esse ser uma pequena quantidade de código em vez de um projeto de integração, com preços por GB para que a eficiência que isso compra apareça diretamente na fatura.

Pronto para começar?

Experimente os proxies residenciais da Shifter, mais de 205M IPs, mais de 195 países, a partir de $0,75/GB.

Começar