一、业务场景与原生单机爬虫痛点
我所在团队负责 Taocarts 反向海淘代购系统商品采集模块,业务需求是每日合规采集 1688、淘宝零售商品基础数据,同步至 Bidfins 跨境前端展示,初期使用单机 Scrapy 爬虫上线后暴露出大量不可控线上问题。
单机架构存在四大致命短板:第一,单进程抓取上限仅 8000 条 / 天,无法支撑大促 10 万 + 日采集需求;第二,任务无持久化,服务器重启、进程崩溃后全部待爬 URL 丢失,只能全量重爬,浪费接口调用成本;第三,去重基于内存 Set,单机内存超过 16G 频繁 OOM,大量重复请求触发平台风控 IP 封禁;第四,无法横向扩容,业务高峰期只能临时换高配 ECS,资源利用率不足 20%,云成本极高。
初期尝试自建单机 Redis 做任务存储,但无集群主从同步、无持久化策略,Redis 宕机后采集任务全部丢失,多次造成商品价格、库存同步滞后,引发前端超卖投诉。经过两周重构,基于阿里云 Redis 集群版搭建分布式爬虫架构,实现多 ECS 节点协同抓取、任务断点续爬、分布式全局 URL 去重,稳定支撑日 10 万商品采集,下文完整拆解架构、可运行代码、线上踩坑与阿里云优化方案。
二、分布式爬虫整体架构拆解
整体四层架构:采集层(多台阿里云 ECS 部署 Scrapy 节点)、调度层(阿里云 Redis 集群 6.0)、存储层(阿里云 RDS MySQL + OSS)、计算层(MaxCompute 离线清洗)。
- Redis 集群职责:存储待爬任务队列、分布式指纹去重、请求限流计数器、代理 IP 池状态缓存;
- ECS 节点:多台轻量应用服务器横向扩容,无状态,可随时弹性增减;
- 合规约束:单 IP 每分钟请求≤15 次,搭配代理池轮换,所有采集仅用于自有代购业务,不对外分发数据,符合平台数据合规要求。
2.1 阿里云 Redis 集群关键配置要点
开通阿里云 Redis 集群版,选择 2 主 2 从架构,开启 RDB+AOF 双重持久化,开启集群读写分离,连接地址使用内网 VPC 地址,避免公网延迟;安全组仅开放 ECS 内网 IP 访问,RAM 子账号分配 Redis 只读 / 读写细分权限,杜绝全量权限泄露。
三、完整可运行工程代码(Python3.9 + scrapy-redis 0.8.0)
3.1 settings.py 分布式 Redis 核心配置(对接阿里云内网 Redis)
# 分布式爬虫开启
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 任务持久化,重启不丢失
SCHEDULER_PERSIST = True
# 阿里云Redis集群内网连接地址,替换为实例内网端点
REDIS_URL = "redis://:xxxx@r-xxxx.redis.rds.aliyuncs.com:6379/0"
# 爬虫并发、限流配置(合规采集核心)
CONCURRENT_REQUESTS = 12
DOWNLOAD_DELAY = 4
RANDOMIZE_DOWNLOAD_DELAY = True
# 全局请求限速,Redis计数器控制单IP 15次/分钟
DOWNLOADER_MIDDLEWARES = {
'taocarts_spider.middlewares.RateLimitMiddleware': 543,
'scrapy.downloadermiddlewares.retry.RetryMiddleware': 550,
'scrapy_redis.downloadmiddleware.RedisRetryMiddleware': 560,
}
# 数据入库Pipeline:RDS MySQL + OSS图片存储
ITEM_PIPELINES = {
'taocarts_spider.pipelines.MysqlPipeline': 300,
'taocarts_spider.pipelines.OssImgPipeline': 350,
}
# 关闭本地文件缓存,全部缓存至Redis
HTTPCACHE_ENABLED = False
3.2 自定义限流中间件 RateLimitMiddleware.py(Redis 计数器限流,规避风控封禁)
import redis
import time
from scrapy.http import HtmlResponse
from scrapy.utils.response import response_status_message
from scrapy_redis import get_redis_from_settings
class RateLimitMiddleware:
def __init__(self, settings):
self.redis = get_redis_from_settings(settings)
self.rate_key = "crawl:rate:ip_limit"
self.max_request = 15 # 单IP每分钟最大请求15次,合规阈值
self.expire = 60
@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings)
def process_request(self, request, spider):
# Redis自增计数,控制请求频率
current = self.redis.incr(self.rate_key)
if current == 1:
self.redis.expire(self.rate_key, self.expire)
if current > self.max_request:
spider.logger.warning(f"请求超限,休眠5s,当前计数:{current}")
time.sleep(5)
return None
3.3 分布式爬虫主 spider taobao_spider.py(RedisSpider,多节点共享任务)
from scrapy_redis.spiders import RedisSpider
from taocarts_spider.items import GoodsItem
import json
class TaobaoGoodsRedisSpider(RedisSpider):
name = "taobao_goods"
# Redis队列key,多节点共用
redis_key = "taobao:goods:start_urls"
custom_settings = {
"LOG_LEVEL": "INFO"
}
def parse(self, response):
# 解析商品基础字段
goods_list = response.xpath("//div[@class='item J_MouserOnverReq']")
for goods in goods_list:
item = GoodsItem()
item["goods_id"] = goods.xpath("./@data-nid").get()
item["title"] = goods.xpath(".//a[@class='J_ClickStat']/@title").get().strip()
item["price"] = goods.xpath(".//strong/text()").get()
item["img_url"] = goods.xpath(".//img/@src").get()
item["source_type"] = "taobao"
yield item
# 分页请求推入Redis队列,分布式共享
next_page = response.xpath("//a[contains(text(),'下一页')]/@href").get()
if next_page:
yield response.follow(next_page, callback=self.parse)
3.4 OSS 图片存储 Pipeline oss_pipeline.py(阿里云 OSS Python SDK)
import oss2
from scrapy.utils.project import get_project_settings
class OssImgPipeline:
def __init__(self):
settings = get_project_settings()
# 阿里云OSS RAM子账号密钥,最小权限
auth = oss2.Auth(settings.get("OSS_ACCESS_KEY_ID"), settings.get("OSS_ACCESS_KEY_SECRET"))
self.bucket = oss2.Bucket(auth, settings.get("OSS_ENDPOINT"), settings.get("OSS_BUCKET_NAME"))
self.img_prefix = "crawl/goods/"
def process_item(self, item, spider):
if item.get("img_url"):
img_name = f"{item['source_type']}/{item['goods_id']}.jpg"
try:
# 流式上传商品图至OSS,配合CDN加速前端访问
self.bucket.put_object_from_url(f"{self.img_prefix}{img_name}", item["img_url"])
# 覆盖为OSS CDN访问地址
item["oss_img_url"] = f"https://cdn.xxxx.oss-cn-shanghai.aliyuncs.com/{self.img_prefix}{img_name}"
except Exception as e:
spider.logger.error(f"OSS上传失败 goods_id:{item['goods_id']}, err:{str(e)}")
item["oss_img_url"] = ""
return item
四、线上分布式爬虫八大踩坑复盘(一线真实故障,高收录加分核心)
坑 1:单机 Redis 无集群,主节点宕机任务全部丢失
现象:凌晨阿里云单节点 Redis 磁盘满宕机,待爬 URL 队列清空,当日 3 万采集任务全部丢失,前端商品库存停滞 4 小时。
根因:未开通 Redis 集群版,无从节点热备,仅开启 RDB 未定时持久化。
阿里云修复方案:升级 2 主 2 从 Redis 集群,开启每日自动 RDB 快照备份,配置云监控磁盘使用率告警阈值 80%。
坑 2:多 ECS 节点并发过高,未做 Redis 分布式限流,IP 批量封禁
现象:扩容至 4 台 ECS 后并发达到 40,单 IP 一分钟请求超 60 次,采集代理 IP 池 120 个 IP 全部被平台风控封禁,采集中断 12 小时。
根因:原限流中间件仅单机计数,未使用 Redis 全局计数器,多节点计数隔离。
修复:改造 RateLimitMiddleware 使用全局 Redis key 做全分布式限流,统一管控所有节点请求频率。
坑 3:OSS 图片上传频繁超时,公网 Endpoint 延迟高
现象:爬虫上传商品图平均超时 3s,大量图片存储失败,前端无商品封面。
根因:爬虫 ECS 与 OSS 使用公网域名传输,跨公网带宽波动大。
优化:切换阿里云 VPC 内网 OSS Endpoint,内网传输无流量费,上传耗时降至 200ms 内。
坑 4:Scrapy-Redis 指纹去重内存暴涨,Redis 内存占用超 90%
现象:运行 7 天 Redis 内存占用从 10G 涨到 28G,集群触发内存淘汰,待爬任务被随机删除。
根因:RFPDupeFilter 永久存储指纹,无过期清理逻辑,商品 URL 指纹无限堆积。
解决方案:自定义去重过滤器,给指纹 key 设置 30 天过期,每日定时清理无效商品指纹,Redis 内存稳定控制在 10G 以内。
坑 5:ECS 轻量服务器内存不足,Playwright 辅助爬虫 OOM 被杀
现象:带有动态渲染页面的采集节点,每日凌晨内存溢出进程被系统 kill,断点续爬失效。
优化:ECS 升级 2C4G 配置,阿里云云监控配置内存使用率告警,Redis 任务分段分片抓取,单次不超过 500 条 URL。
坑 6:RDS MySQL 写入频繁超时,爬虫批量入库无事务分批提交
现象:Pipeline 单次一次性写入 1000 条商品数据,数据库连接池耗尽,写入超时。
修复:MysqlPipeline 改造批量分批次提交,单次 200 条开启事务,阿里云 RDS 开启读写分离,写入走主库、离线查询走从库。
坑 7:Redis 集群内网端口未限制安全组,存在公网扫描爆破风险
现象:云安全中心告警 Redis 暴力破解扫描记录,安全组开放 6379 全网段访问。
修复:安全组仅允许爬虫 ECS 内网网段访问 6379,RAM 子账号精细化权限,禁止 Redis 公网访问。
坑 8:爬虫无日志归集,异常封禁只能登录服务器看本地日志
现象:批量 IP 封禁后,无法快速定位是哪类 URL 触发风控,本地日志分散多台 ECS 排查效率极低。
阿里云优化:接入 SLS 日志服务,所有爬虫 stdout/stderr 日志实时归集,配置关键词 “封禁、403、timeout” 告警,钉钉实时推送异常。
五、阿里云架构优化后量化性能提升数据
- 采集吞吐量:8000 条 / 天 → 10.6 万条 / 天,扩容至 6 台 ECS 无风控封禁;
- 任务丢失率:12% → 0.03%,Redis 集群持久化保障崩溃续爬;
- 商品图片加载速度:前端海外用户平均 4.2s → 600ms,OSS 搭配全球 CDN;
- 云资源成本:同等采集量下,集群 Redis 比多台单机 Redis 节省 38% 云服务费;
- 故障排查耗时:单次采集故障 4 小时定位 → 10 分钟通过 SLS 日志定位根因。