1. 为什么需要分布式爬虫?
在数据采集领域,单机爬虫面临着明显的性能瓶颈。当我们需要采集百万级甚至千万级页面时,单台机器的网络带宽、计算资源和存储能力都会成为限制因素。我曾经遇到过一个案例:某电商网站的商品数据采集任务,单机爬虫每天只能完成约3万条记录,而业务需求是每天至少50万条。这就是分布式爬虫的典型应用场景。
分布式爬虫的核心优势在于:
- 横向扩展能力:通过增加工作节点,可以线性提升采集能力
- 容错机制:单个节点故障不会导致整个采集任务中断
- 资源利用率:可以充分利用多台机器的带宽和计算资源
2. Scrapy框架的分布式改造方案
2.1 原生Scrapy的局限性
Scrapy本身是一个优秀的单机爬虫框架,但其设计上并不原生支持分布式。主要限制包括:
- 调度器(Scheduler)和去重(DupeFilter)存储在内存中
- 请求队列无法在多个爬虫实例间共享
- 采集结果集中存储,可能成为性能瓶颈
2.2 分布式架构设计
基于Scrapy构建分布式爬虫通常采用主从架构:
code复制[主节点]
├── 任务队列(Redis)
├── 去重过滤器(Redis)
└── 结果收集器(Redis/MongoDB)
[从节点×N]
├── Scrapy爬虫实例
└── 从任务队列获取URL
这种架构下,主节点负责任务调度和状态维护,从节点专注于页面下载和解析。我们团队在实际项目中验证过,8个从节点可以使采集效率提升6-8倍。
3. 关键技术实现细节
3.1 Redis集成配置
首先需要安装必要的Python包:
bash复制pip install scrapy-redis redis
然后在Scrapy项目的settings.py中添加以下配置:
python复制# 使用Redis调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 使用Redis去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# Redis连接设置
REDIS_HOST = '192.168.1.100' # 主节点IP
REDIS_PORT = 6379
REDIS_PASSWORD = 'your_password'
# 保持爬虫状态
SCHEDULER_PERSIST = True
# 任务队列类型
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'
3.2 分布式爬虫类改造
需要继承RedisSpider而不是原生Spider:
python复制from scrapy_redis.spiders import RedisSpider
class MyDistributedSpider(RedisSpider):
name = 'distributed_spider'
redis_key = 'myspider:start_urls' # Redis中的起始URL键名
def parse(self, response):
# 解析逻辑与普通Spider相同
for item in response.css('div.product'):
yield {
'title': item.css('h2::text').get(),
'price': item.css('.price::text').get()
}
# 自动从Redis获取下一页URL
# 也可以手动添加新URL到队列
# yield scrapy.Request(next_page, callback=self.parse)
3.3 数据存储优化
对于大规模采集,建议使用管道批量写入:
python复制class RedisPipeline(object):
def __init__(self, redis_conn):
self.redis = redis_conn
self.buffer = []
self.batch_size = 100
@classmethod
def from_settings(cls, settings):
redis_conn = redis.StrictRedis(
host=settings['REDIS_HOST'],
port=settings['REDIS_PORT'],
password=settings['REDIS_PASSWORD']
)
return cls(redis_conn)
def process_item(self, item, spider):
self.buffer.append(item)
if len(self.buffer) >= self.batch_size:
self._commit()
return item
def _commit(self):
if not self.buffer:
return
pipe = self.redis.pipeline()
for item in self.buffer:
pipe.lpush('results:items', json.dumps(item))
pipe.execute()
self.buffer = []
4. 性能调优实战经验
4.1 合理设置并发参数
在settings.py中需要优化以下参数:
python复制# 并发请求数
CONCURRENT_REQUESTS = 100
# 每个域的并发数
CONCURRENT_REQUESTS_PER_DOMAIN = 20
# 下载延迟
DOWNLOAD_DELAY = 0.5
# 自动限速扩展
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 1
AUTOTHROTTLE_MAX_DELAY = 10
重要提示:这些参数需要根据目标网站的反爬策略和自身服务器性能调整。我们曾因CONCURRENT_REQUESTS设置过高(200+)导致IP被封禁。
4.2 Redis性能优化
对于大规模爬虫,Redis需要特别配置:
bash复制# redis.conf关键配置
maxmemory 4gb
maxmemory-policy allkeys-lru
timeout 300
tcp-keepalive 60
同时建议对Redis进行分片:
- 任务队列使用一个Redis实例
- 去重过滤器使用单独实例
- 结果存储可以使用MongoDB替代
5. 常见问题与解决方案
5.1 任务积压问题
现象:Redis内存持续增长,爬取速度变慢
解决方案:
- 增加从节点数量
- 优化解析逻辑,减少单个页面处理时间
- 对任务队列进行分片处理
5.2 去重内存膨胀
现象:Redis内存占用过高,主要是去重集合过大
优化方案:
python复制# 使用布隆过滤器替代集合去重
DUPEFILTER_CLASS = "scrapy_redis_bloomfilter.dupefilter.RFPDupeFilter"
BLOOMFILTER_HASH_NUMBER = 6
BLOOMFILTER_BIT = 30 # 占用约128MB内存,可存储上亿URL
5.3 断点续爬实现
分布式环境下需要特别注意爬虫的启动和停止:
bash复制# 优雅停止爬虫(会完成队列中的请求)
curl http://localhost:6800/schedule.json -d project=myproject -d spider=distributed_spider -d jobid=123 -d setting=CLOSESPIDER_TIMEOUT=60
# 从特定URL重新开始
redis-cli lpush myspider:start_urls "http://example.com/start_page"
6. 监控与维护方案
6.1 基础监控指标
建议监控以下关键指标:
- Redis内存使用量
- 任务队列长度
- 各节点采集速度
- 错误率(4xx/5xx响应比例)
可以使用Grafana+Prometheus构建监控看板,示例配置:
yaml复制# prometheus.yml
scrape_configs:
- job_name: 'scrapy'
static_configs:
- targets: ['node1:6800', 'node2:6800']
- job_name: 'redis'
static_configs:
- targets: ['redis:9121']
6.2 日志集中管理
建议使用ELK栈收集各节点日志:
python复制# settings.py
LOG_LEVEL = 'INFO'
LOG_STDOUT = True
LOG_FORMAT = '%(asctime)s [%(name)s] %(levelname)s: %(message)s'
LOG_DATEFORMAT = '%Y-%m-%d %H:%M:%S'
配合Filebeat将日志发送到Logstash,便于问题排查。
7. 反爬虫策略应对
分布式爬虫更容易触发反爬机制,需要特别注意:
7.1 IP轮换方案
python复制# middlewares.py
class ProxyMiddleware(object):
def process_request(self, request, spider):
proxy = get_proxy_from_pool() # 从代理池获取
request.meta['proxy'] = f"http://{proxy}"
# settings.py
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.ProxyMiddleware': 100,
}
7.2 请求指纹多样化
python复制# 随机化请求头
USER_AGENTS = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1...'
]
# 中间件实现
class RandomUserAgentMiddleware:
def process_request(self, request, spider):
request.headers['User-Agent'] = random.choice(USER_AGENTS)
8. 扩展架构:容器化部署
对于生产环境,建议使用Docker部署:
dockerfile复制# Dockerfile
FROM python:3.8
RUN pip install scrapy scrapy-redis redis bloomfilter
COPY . /app
WORKDIR /app
CMD ["scrapy", "crawl", "distributed_spider"]
配合Kubernetes可以实现自动扩缩容:
yaml复制# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: scrapy-worker
spec:
replicas: 10
selector:
matchLabels:
app: scrapy
template:
metadata:
labels:
app: scrapy
spec:
containers:
- name: spider
image: my-scrapy-image
resources:
limits:
cpu: "1"
memory: 1Gi
这种架构下,我们可以根据任务队列长度自动调整worker数量,既保证采集效率,又避免资源浪费。
