Scrapy-Redis集群中的代理调度链路是什么?

标准链路是任务入队、节点消费、请求取代理、结果回收和失败重试。

Scrapy-Redis能够让多个爬虫节点共享待处理请求,但不会自动管理代理IP。一次请求通常经过以下步骤:

  1. 任务生产端将URL和请求参数写入Redis。
  2. 爬虫节点从共享队列中领取请求。
  3. 下载器中间件从代理池领取一个可用代理。
  4. 请求携带代理发送到目标地址。
  5. 中间件根据响应或异常更新代理状态。
  6. 成功请求归还代理,代理异常则进入冷却队列。
  7. 超过重试次数的请求进入失败队列。

因此,代理池应该作为独立资源层接入,而不是把代理地址直接写入每个请求的回调函数。

settings.py应该配置哪些内容?

基础配置要先保证任务队列可共享,再接入代理中间件和重试机制。

# settings.py

REDIS_URL = "redis://127.0.0.1:6379/0"

SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER_PERSIST = True

RETRY_ENABLED = True
RETRY_TIMES = 3
RETRY_HTTP_CODES = [
    408,
    429,
    500,
    502,
    503,
    504,
]

CONCURRENT_REQUESTS = 32
CONCURRENT_REQUESTS_PER_DOMAIN = 8
DOWNLOAD_TIMEOUT = 20

DOWNLOADER_MIDDLEWARES = {
    "project.middlewares.DistributedProxyMiddleware": 550,
    "scrapy.downloadermiddlewares.retry.RetryMiddleware": 550,
}

生产环境不要把代理账号、密码和固定地址直接提交到代码仓库,可以通过环境变量注入:

import os

REDIS_URL = os.getenv(
    "REDIS_URL",
    "redis://127.0.0.1:6379/0",
)

PROXY_API_URL = os.getenv("PROXY_API_URL")
PROXY_USERNAME = os.getenv("PROXY_USERNAME")
PROXY_PASSWORD = os.getenv("PROXY_PASSWORD")

需要注意的是,不同Scrapy版本的中间件执行顺序可能存在差异。部署前应通过测试任务确认代理注入、异常捕获和重试逻辑是否按照预期执行。

Redis中应该怎样设计代理池键?

代理池至少要区分可用、租约、冷却和统计四类状态。

Redis键类型用途
proxy:readyList等待分配的代理
proxy:leasedList已被节点领取的代理
proxy:cooldownSorted Set按恢复时间保存异常代理
proxy:deadSet暂停使用的代理
proxy:stats:<id>Hash保存代理成功和失败统计
proxy:heartbeat:<node>String记录节点心跳
proxy:request:<id>Hash记录请求与代理的关系

代理进入冷却队列时,可以将预计恢复时间作为分数:

import time

cooldown_until = int(time.time()) + 60

redis_client.zadd(
    "proxy:cooldown",
    {proxy: cooldown_until},
)

恢复任务周期性取出已经到期的代理:

def restore_cooldown_proxies(redis_client):
    now = int(time.time())

    proxies = redis_client.zrangebyscore(
        "proxy:cooldown",
        0,
        now,
    )

    for proxy in proxies:
        redis_client.zrem("proxy:cooldown", proxy)
        redis_client.rpush("proxy:ready", proxy)

生产环境应使用Lua脚本或其他原子操作,将查询、删除和回收合并处理,避免多个恢复进程同时取出同一条代理记录。

代理中间件应该怎样编写?

下载器中间件负责请求前领取代理,响应后归还代理,复杂的评分逻辑则交给独立的状态处理函数。

下面是一个简化的中间件示例:

import time
import redis

from scrapy.exceptions import IgnoreRequest


class DistributedProxyMiddleware:

    def __init__(self, redis_url):
        self.redis = redis.Redis.from_url(
            redis_url,
            decode_responses=True,
        )

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            crawler.settings.get("REDIS_URL")
        )

    def acquire_proxy(self):
        # 原子地将代理从可用队列移动到租约队列
        return self.redis.rpoplpush(
            "proxy:ready",
            "proxy:leased",
        )

    def release_proxy(self, proxy, healthy=True):
        if not proxy:
            return

        self.redis.lrem(
            "proxy:leased",
            1,
            proxy,
        )

        if healthy:
            self.redis.rpush(
                "proxy:ready",
                proxy,
            )
            return

        cooldown_until = int(time.time()) + 60

        self.redis.zadd(
            "proxy:cooldown",
            {proxy: cooldown_until},
        )

    def process_request(self, request, spider):
        if request.meta.get("proxy"):
            return None

        proxy = self.acquire_proxy()

        if not proxy:
            request.meta["proxy_waiting"] = True
            raise IgnoreRequest(
                "proxy pool is temporarily empty"
            )

        request.meta["proxy"] = proxy
        request.meta["proxy_lease"] = proxy

        return None

    def process_response(self, request, response, spider):
        proxy = request.meta.get("proxy_lease")

        if response.status in (200, 206):
            self.release_proxy(proxy, healthy=True)

        elif response.status in (407, 502, 503):
            self.release_proxy(proxy, healthy=False)

        elif response.status in (403, 429):
            # 目标限制不一定代表代理失效
            self.release_proxy(proxy, healthy=True)

        else:
            self.release_proxy(proxy, healthy=True)

        return response

    def process_exception(self, request, exception, spider):
        proxy = request.meta.get("proxy_lease")
        self.release_proxy(proxy, healthy=False)

        return None

这段代码用于展示接入结构。生产环境还需要补充以下逻辑:

  • 代理池为空时,将请求延迟后重新入队,不能直接丢失。
  • 代理领取、租约写入和过期时间设置应保持原子性。
  • 节点退出后,租约中的代理要能够自动回收。
  • 代理账号和密码不能通过日志输出。
  • Redis同步操作不能长时间阻塞Scrapy事件循环。

如果代理池规模较大,可以把代理调度单独拆成服务,由爬虫节点通过接口领取和归还代理,避免每个Scrapy进程直接承担复杂的状态管理。

请求重试怎样避免无限循环?

重试次数要同时记录请求次数、代理次数和任务总时长。

只配置RETRY_TIMES还不够。一次请求可能不断切换代理,如果没有额外限制,任务仍可能长时间循环。

可以在请求的meta中记录重试状态:

def build_retry_request(request, reason):
    retry_count = request.meta.get(
        "proxy_retry_count",
        0,
    )

    if retry_count >= 3:
        return None

    retry_request = request.copy()
    retry_request.dont_filter = True

    retry_request.meta[
        "proxy_retry_count"
    ] = retry_count + 1

    retry_request.meta[
        "retry_reason"
    ] = reason

    retry_request.meta.pop("proxy", None)
    retry_request.meta.pop("proxy_lease", None)

    return retry_request

不同错误应采用不同策略:

错误类型是否更换代理处理建议
连接超时是代理进入短暂冷却
代理认证失败是检查凭证,必要时暂停代理
目标返回429视情况降低请求频率并延迟重试
目标返回403视情况检查请求参数和访问节奏
页面内容为空否优先检查解析逻辑
字段缺失否检查页面结构和选择器
DNS解析失败通常是检查代理和节点网络

页面内容为空或字段缺失时,直接切换代理通常不能解决问题,反而会增加无效请求和代理消耗。

多个爬虫节点怎样共享代理质量评分?

代理评分必须写入Redis,不能只保存在单个节点的内存中。

每个代理可以维护以下字段:

success_count
timeout_count
auth_error_count
target_error_count
last_success_at
last_failure_at
cooldown_until

评分可以采用时间窗口,避免历史数据掩盖当前状态:

近期调度分数 =
最近10分钟成功率 × 0.6
+ 最近1小时成功率 × 0.3
+ 历史成功率 × 0.1

这个分数只用于调度,不等同于最终业务成功率。真正的采集质量还需要结合有效数据率、页面完整度和重复内容比例。

当单个代理连续失败时,可以延长冷却时间;当多个代理同时出现相同错误时,应优先检查任务参数、目标响应和节点网络,不能一次性把整个代理池全部判定为失效。

集群部署时怎样验证代理配置?

先用小规模任务验证状态闭环,再逐步增加节点和并发。

建议按以下顺序测试:

  1. 启动两个爬虫节点,确认它们消费同一个Redis任务队列。
  2. 为每个节点设置不同的NODE_ID。
  3. 检查同一个请求是否只被一个节点处理。
  4. 检查代理是否从ready进入leased。
  5. 验证请求结束后代理能否正确归还。
  6. 注入一个失效代理,确认它进入冷却队列。
  7. 关闭一个节点,确认租约过期后代理能够回收。
  8. 模拟429响应,确认任务会延迟重试而不是快速循环。
  9. 让代理池暂时为空,确认请求会进入等待或延迟队列。
  10. 提高并发后观察Redis队列、代理库存和业务成功率。

日志至少应包含以下字段:

request_id
node_id
target_host
proxy_id
retry_count
response_status
elapsed_ms
result_status

只记录URL和状态码,后续很难判断问题来自代理、节点、目标站点还是解析器。

哪些指标能判断代理调度是否正常?

代理连接指标只能说明链路是否可用,业务有效数据指标才反映采集结果。

建议同时监控三类指标:

指标类别关注指标判断目的
任务指标队列长度、消费速度、重试次数判断任务是否积压
代理指标可用数、租约数、冷却数、回收数判断代理池是否枯竭
连接指标超时率、认证失败率、平均响应时间判断网络和代理质量
业务指标有效页面率、字段提取率、空响应率判断采集是否有效
集群指标节点数量、单节点吞吐、重启次数判断节点是否均衡
Redis指标内存、连接数、慢查询、阻塞命令判断中心组件是否成为瓶颈

例如,代理连接成功率较高,但有效字段提取率明显下降,问题可能来自页面结构变化或解析器失效,而不是代理池。反过来,如果多个目标同时出现连接超时,则应优先检查代理库存和网络链路。

总结

代理IP池接入Scrapy-Redis时,应把任务队列和代理状态分开设计,通过Redis键空间、下载器中间件、租约机制、失败冷却和重试上限完成统一调度,再用任务、代理、连接和业务指标验证集群是否真正稳定。

FAQ

Q:代理IP池应该由每个爬虫节点单独维护吗?

不建议完全独立维护。每个节点各自保存代理列表,容易出现代理重复使用、失败状态不同步和资源利用率不均。更合理的方式是由Redis或独立调度服务保存共享状态,爬虫节点只负责领取和归还。

Q:代理池没有可用IP时,应该直接丢弃请求吗?

不应该。可以把请求放入延迟队列,等待代理恢复后重新执行,并设置最大等待时间和最大重试次数。超过限制后再写入失败队列,方便后续检查。

Q:一个代理应该承担多少并发请求?

没有固定值,需要结合代理类型、目标响应速度、请求体大小和访问频率进行测试。建议从较低并发开始,观察超时率、429比例和有效数据率,再逐步调整。

Q:目标返回403或429时,是否一定要更换代理?

不一定。403或429可能与请求频率、请求参数、会话状态和目标访问规则有关。应先区分代理连接失败和目标限制,再决定是降速、延迟重试还是短暂更换代理。

Q:节点异常退出后,代理会不会一直处于占用状态?

如果没有租约过期和回收机制,确实可能一直处于占用状态。因此每次领取代理都应写入节点ID、请求ID和过期时间,并通过心跳或定时任务回收异常租约。

青果网络代理IP - CTA Banner
点赞(23)
第一次为企业采购IP代理,怎样靠业务试用筛出合适的厂商
数据采集 代理IP配置 代理IP 国外代理IP
2026-11-06

第一次采购代理IP,难点不是收集更多厂商参数,而是把真实业务压缩成一套可重复的试用流程。文章以同任务、同并发、同时间窗口和同验收口径,对八家厂商进行业务验证,帮助技术与采购团队缩小候选范围。

代理测速为什么不能只看成功请求?HTTP延迟统计中的幸存者偏差
HTTP代理 代理IP 代理IP池 IP代理
2026-11-06

代理测速若只统计成功响应,超时、连接失败和重试请求就会从延迟分布中消失,结果往往比真实任务更乐观。文章从幸存者偏差出发,给出覆盖全部请求、失败成本和重试链路的测速方法。

房价数据跟踪:隧道代理与API提取有什么区别?
隧道IP 隧道代理 隧道代理IP 代理IP
2026-11-04

隧道代理更像持续运行的网络通道,适合高频、长时间、请求模式稳定的房价采集;API提取更像按需领取代理资源,适合低频、分批、需要明确控制IP和时长的任务。最终应按有效请求成本,而不是单次调用价格选择。

白名单鉴权自检体系:上线前、运行中、异常后的检查项
代理IP 爬虫代理 IP代理 HTTP代理
2026-11-04

白名单鉴权自检体系应由四层组成:权限对象、配置状态、访问证据和处置闭环。上线前确认能否正确放行与拒绝,运行中确认授权是否仍然适用,异常后确认边界已恢复并完成根因复盘。

返回
顶部