大规模爬虫优化 - 内存管理、网络优化与性能调优详解

📂 所属阶段:第五阶段 — 战力升级(分布式与进阶篇)
🔗 相关章节:自动限速AutoThrottle · 数据去重与增量更新 · 分布式去重与调度

当你的爬虫从“单机小打小闹”升级到“全网海量采集”时,一定会遇到三大拦路虎:内存爆炸、请求被ban、数据堆积崩溃。今天道满就带你拆解真正企业级大规模爬虫的优化逻辑——不靠盲目堆配置,而是靠“精打细算”的资源管理、智能自适应策略和全方位的可观测性。全文精炼,所有代码均可落地👇


1. 先定目标,再动手 —— 四个核心优化目标

很多同学一上来就调大并发、改延迟,结果不是被封就是内存撑爆。优化之前,先明确我们要达到什么效果:

  • 稳定性:7×24小时无人值守,遇到异常能自动修复,不中断任务
  • 效率性:用最少的资源在单位时间内爬到最多的数据
  • 可扩展性:后续加机器、扩节点时,代码基本不用大改
  • 可观测性:一眼就能看出哪里慢、哪里挂了、哪里被限流

后面所有优化措施,都是围绕这四个目标展开的。


2. 内存管理 —— 最容易翻车的环节

爬虫最容易爆内存的几个场景:

  • URL队列中囤积了上千万条待抓取链接
  • 响应页面全部缓存到内存中解析
  • Item数据里包含超长文本(比如几千字的产品描述)不截断

针对这些,道满给你三个立竿见影的方案👇

2.1 Scrapy 原生配置“三板斧”

首先不要拍脑袋设置内存限制,要根据你当前机器的可用内存动态计算:

# scrapy_settings.py
import psutil

# 使用本机可用内存的70%,同时设一个8GB的上限,避免占满整个机器
available_mem = psutil.virtual_memory().available // 1024 // 1024
MEMUSAGE_ENABLED = True
MEMUSAGE_LIMIT_MB = min(int(available_mem * 0.7), 8192)   # 本机70%或8GB封顶
MEMUSAGE_WARNING_MB = int(MEMUSAGE_LIMIT_MB * 0.75)       # 使用到75%时发出警告
CLOSESPIDER_MEMUSAGE = MEMUSAGE_LIMIT_MB                   # 超过上限自动关闭爬虫

# 限制单个响应下载大小,防止大文件撑爆内存
DOWNLOAD_MAXSIZE = 50 * 1024 * 1024   # 50MB(按需调整)
DOWNLOAD_WARNSIZE = 10 * 1024 * 1024
RESPONSE_ENCODING = 'utf-8'           # 统一编码,避免因转码产生内存碎片

# 用磁盘队列暂存海量URL,减轻内存压力
SCHEDULER_PRIORITY_QUEUE = 'scrapy.pqueues.ScrapyPriorityQueue'
SCHEDULER_DISK_QUEUE = 'scrapy.squeues.PickleFifoDiskQueue'
SCHEDULER_MEMORY_QUEUE = 'scrapy.squeues.FifoMemoryQueue'

2.2 在中间件里“即用即弃”

Scrapy 默认会把每个响应、每个 Item 都保留一段时间,我们可以通过中间件,主动释放不再需要的数据:

# memory_optimization_middleware.py
import gc
import weakref
from scrapy import signals
from itemadapter import ItemAdapter

class MemoryOptimizationMiddleware:
    def __init__(self):
        # 使用弱引用保存Item,一旦没有其他强引用就会自动被垃圾回收
        self.item_weakref = weakref.WeakSet()
        # 只缓存最近1000条响应,多余的立即丢弃
        self.response_tmp = {}
        self.tmp_limit = 1000

    @classmethod
    def from_crawler(cls, crawler):
        ext = cls()
        crawler.signals.connect(ext.spider_closed, signal=signals.spider_closed)
        return ext

    def process_spider_input(self, response, spider):
        # 控制缓存大小,超出上限就删掉最旧的
        if len(self.response_tmp) >= self.tmp_limit:
            del self.response_tmp[list(self.response_tmp.keys())[0]]
        return None

    def process_spider_output(self, response, result, spider):
        for item_or_req in result:
            if hasattr(item_or_req, 'fields'):
                adapter = ItemAdapter(item_or_req)
                # 截断超长文本,比如文章正文限制在10000字符
                for k, v in adapter.items():
                    if isinstance(v, str) and len(v) > 10000:
                        adapter[k] = v[:10000] + "..."
                self.item_weakref.add(item_or_req)
            yield item_or_req

    def spider_closed(self, spider):
        # 爬虫结束时强制回收一次内存
        gc.collect()
        spider.logger.info("内存优化完成,已执行GC")

这样配置之后,你会发现内存曲线变得非常平稳,不会再出现“先平缓后陡升”的心跳图。


3. 网络优化与自适应限速 —— 爬得快又不被封

并发不是越大越好,无脑开大并发轻则 IP 被临时封禁,重则被封网段。我们要做的是智能感知目标站点的抗压能力,让请求速率始终处于“对方能承受的临界点”附近。

3.1 基础网络配置

# scrapy_settings.py
# 总并发数:32是一个比较安全的起步值
CONCURRENT_REQUESTS = 32
# 单域名并发:8 一般不会触发反爬,可根据目标站实际测出阈值
CONCURRENT_REQUESTS_PER_DOMAIN = 8
# 单IP并发(有代理池时可以适度放大)
CONCURRENT_REQUESTS_PER_IP = 4

# DNS缓存,避免每次请求都解析域名
DNSCACHE_ENABLED = True
DNSCACHE_SIZE = 10000
DNS_TIMEOUT = 30

# 仅对可恢复的状态码进行重试(比如服务器临时错误、限流)
RETRY_TIMES = 3
RETRY_HTTP_CODES = [500, 502, 503, 504, 408, 429]
RETRY_PRIORITY_ADJUST = -1    # 重试请求优先级降低,不影响正常请求

3.2 “更聪明”的自适应限速

Scrapy 自带的 AutoThrottle 只基于响应延迟来调整,但有时候延迟低也可能是因为对方返回了空内容或错误页。我们可以加入成功率和响应时间趋势两个维度,让限速器更“聪明”:

# adaptive_throttle.py
import statistics
from collections import defaultdict, deque
from scrapy.downloadermiddlewares.throttle import AutoThrottle

class SmartAutoThrottle(AutoThrottle):
    def __init__(self, crawler):
        super().__init__(crawler)
        # 为每个域名维护最近50条请求的记录(响应延迟 + 是否成功)
        self.stats = defaultdict(lambda: deque(maxlen=50))

    def _adjust_delay(self, slot, latency, response=None):
        domain = slot.key
        self.stats[domain].append({
            'latency': latency,
            'success': 200 <= response.status < 400 if response else True
        })

        # 至少收集到10条数据才开始调整
        if len(self.stats[domain]) < 10:
            return

        recent = list(self.stats[domain])[-10:]
        # 1. 根据最近10条的成功率调整
        success_rate = sum(1 for r in recent if r['success']) / 10
        if success_rate < 0.8:       # 成功率低于80%,放慢脚步
            slot.delay = min(slot.delay * 1.3, 60)
        elif success_rate > 0.95:    # 成功率高于95%,可以尝试提速
            slot.delay = max(slot.delay * 0.9, 0.5)

        # 2. 根据平均响应时间的趋势调整
        avg_latency = sum(r['latency'] for r in recent) / 10
        overall_latency = statistics.mean(r['latency'] for r in self.stats[domain])
        if avg_latency > overall_latency * 1.2:   # 近期响应变慢,降速
            slot.delay = min(slot.delay * 1.2, 60)
        elif avg_latency < overall_latency * 0.8: # 响应变快,可以加速
            slot.delay = max(slot.delay * 0.9, 0.5)

# 在 settings.py 中用我们自己的 SmartAutoThrottle 替换原生中间件
DOWNLOADER_MIDDLEWARES = {
    'scrapy.downloadermiddlewares.throttle.AutoThrottle': None,
    'your_project.adaptive_throttle.SmartAutoThrottle': 800,
}

这样,爬虫就会像一个经验丰富的老司机,根据路况自动调整油门,既不会闯红灯(被封),也不会龟速行驶(效率低)。


4. 可观测性与容错 —— 爬虫的最后一道防线

爬虫跑了几万条之后突然崩溃,却只知道一个模糊的错误日志?这时候健康检查和告警系统就派上用场了。我们通过简单的 Prometheus 指标 + 日志告警,就能实时监控爬虫的健康状态。

4.1 基于 Prometheus 的健康检查

# health_check.py
import time
import logging
from collections import deque
from scrapy import signals
from pydispatch import dispatcher
from prometheus_client import Gauge, start_http_server

logger = logging.getLogger(__name__)

# 定义三个仪表盘指标,可在本地 8000 端口查看
MEM_GAUGE = Gauge('scrapy_mem_rss_mb', 'Memory RSS usage', ['spider'])
CPU_GAUGE = Gauge('scrapy_cpu_percent', 'CPU usage', ['spider'])
ERR_GAUGE = Gauge('scrapy_error_rate', 'Error rate (last 100 requests)', ['spider'])

class HealthCheckExtension:
    def __init__(self, crawler):
        self.crawler = crawler
        self.spider_name = None
        self.last_errs = deque(maxlen=100)   # 记录最近100次请求的成功/失败
        self.check_interval = 60             # 每60秒汇总一次
        self.last_check = time.time()

        # 启动 Prometheus HTTP 服务,访问 http://localhost:8000 即可查看指标
        start_http_server(8000)
        dispatcher.connect(self.spider_opened, signals.spider_opened)
        dispatcher.connect(self.response_received, signals.response_received)
        dispatcher.connect(self.request_dropped, signals.request_dropped)

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler)

    def spider_opened(self, spider):
        self.spider_name = spider.name

    def response_received(self, response, request, spider):
        # 记录该请求是否成功(2xx 或 3xx 视为成功)
        self.last_errs.append(200 <= response.status < 400)
        self._check_and_alert()

    def request_dropped(self, request, response, spider):
        # 请求被丢弃也视为失败
        self.last_errs.append(False)
        self._check_and_alert()

    def _check_and_alert(self):
        current = time.time()
        if current - self.last_check < self.check_interval:
            return
        self.last_check = current

        import psutil
        p = psutil.Process()
        # 更新内存和CPU指标
        MEM_GAUGE.labels(spider=self.spider_name).set(p.memory_info().rss // 1024 // 1024)
        CPU_GAUGE.labels(spider=self.spider_name).set(p.cpu_percent(interval=0.1))

        # 计算最近50次请求的错误率
        if len(self.last_errs) >= 50:
            err_rate = 1 - sum(self.last_errs) / len(self.last_errs)
            ERR_GAUGE.labels(spider=self.spider_name).set(err_rate)

            if err_rate > 0.1:
                # 实际项目中可以换成邮件、钉钉/企业微信机器人发送告警
                logger.error(f"🚨 告警:错误率过高!当前错误率:{err_rate:.2%}")

现在,你不仅能在 Grafana 上看到漂亮的仪表盘,还能在出错时第一时间收到通知,再也不用半夜爬起来手动重启爬虫了。


5. 道满的生产环境“最佳实践”

核心配置清单

把上面的所有优化整合起来,就形成了一套可以直接上生产的基础配置。你可以按需调整参数,但记住下面三条铁律:

  1. 小步慢跑,逐渐加量
    新爬虫开始阶段,总并发设置为 4,单域名 2,跑 2 小时确认没有被封,再逐步调大。宁可慢一点,也不要上了黑名单。

  2. 相信数据,别信“约定俗成”
    不要完全依赖 robots.txt 约定的延迟,有些网站实际要求更严。用 SmartAutoThrottle 让爬虫自己“感受”目标的容忍度。

  3. 做好断点续爬和去重持久化
    URL 队列和去重集一定要能持久化存储(例如 Redis 或磁盘队列),即使机器突然断电,重启后也能从上次中断的位置继续。


🔗 相关教程推荐

🏷️ 标签云: 大规模爬虫 性能优化 内存管理 网络优化 并发控制 Scrapy调优