scrapy-redis分布式爬虫使用及docker swarm集群部署
成果
实现了用docker swarm 集群部署scrapy-redis分布式漫画爬虫,数据统一存储至mongo。
概述
本文大致分为两部分
- scrapy-redis分布式爬虫使用流程
- 使用docker部署分布式爬虫
部署流程逐渐从手动创建容器到容器编排部署。演变流程大致如下
单机Dockerfile+mongo+redis --> 单机docker-compose up --> 分布式 单机docker-compose +修改源码ip连通容器 --> 分布式 docker swarm 手动create服务 --> 分布式 docker-stack部署服务
文中的爬虫代码,Dockerfile,docker-compose.yml,都可在我的github项目 90漫画爬虫 对照观看。
想要直接尝试以下该分布式爬虫可以复制docker-compose.yml 到服务器,创建一个docker swarm集群 。然后 在该目录运行docker stack deploy -c docker-compose.yml <一个名称>
命令即可。
scrapy-redis分布式爬虫
原生scrapy无法实现分布式爬虫,为了实现分布式爬虫,可以采用scrapy-redis组件。
scrapy-redis组件中为我们封装好了可以被多台机器共享的调度器,我们可以直接使用并实现分布式数据爬取。
使用流程
通过对原生的scrapy代码进行部分修改就可以使用scrapy-redis组件。
-
下载scrapy-redis组件:pip3 install scrapy-redis
-
redis配置文件的配置(使用docker不需配置):注释bind 127.0.0.1,表示可以让其他ip访问redis,
protected-mode no,表示可以让其他ip操作redis。
-
修改爬虫文件中的相关代码:基于Spider的类将父类修改成RedisSpider,基于CrawlSpider的,将其父类修改成RedisCrawlSpider。spider中定义一个redis_key (用于往redis中放入start_urls),示例如下
-
class Crawl90Spider(RedisCrawlSpider): name = 'crawl90' redis_key = 'comicSpider'
-
-
settings修改
-
加入以下代码 # 使用scrapy-redis组件的去重队列 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 使用scrapy-redis组件自己的调度器 SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 是否允许暂停 SCHEDULER_PERSIST = True # redis编码 REDIS_ENCODING = 'utf-8' # 所使用redis的主机的ip,使用docker compose编排的话可以写成服务名 REDIS_HOST = '106.52.33.199' # redis监听的端口 REDIS_PORT = 21111 # 认证密码 REDIS_PARAMS = {'password':yourpwd}
-
应用pipeline(可直接用RedisPipeline,我用的是自己写的mongo管道) ITEM_PIPELINES = { 'comics90.pipelines.MongoPipeline': 300, # 'scrapy_redis.pipelines.RedisPipeline': 400 }
-
-
开启爬虫程序
-
打开redis-cli,输入 lpush redis_key值 原生scrapy的start_url值
完成
自定义pipeline
pipeline并不是一定需要用scrapy-redis自带的RedisPipeline,只要我们的pipeline都往同一个数据库存item就可以实现统一存储。
先看看人家的RedisPipeline咋写的,处理item的主要代码为process_item及_process_item。
class RedisPipeline(object):
··· 其他代码 ···
def process_item(self, item, spider):
return deferToThread(self._process_item, item, spider)
def _process_item(self, item, spider):
key = self.item_key(item, spider)
data = self.serialize(item)
self.server.rpush(key, data)
return item
可以看出 RedisPipeline和我们自己写的pipeline没什么区别,就多了一个twisted.internet.threads中的deferToThread方法实现异步写入。只要我们重写_process_item应该也可以达到同样效果(其实直接写process_item方法在数据库中插入数据也是性的通的),若要用其他数据库也不必先把数据存入redis再读写到其他数据库了。
docker部署分布式爬虫
docker run分别部署
先用Dockerfile构建一个镜像,代码如下
# 从python:3.8.2 镜像开始构建
FROM python:3.8.2
# 维护者
MAINTAINER lymmurrain
# 将爬虫文件复制到/root目录下
ADD ./comics90 /root/
# 安装依赖
RUN pip3 install -i https://pypi.doubanio.com/simple/ scrapy
RUN pip3 install pymongo -i https://pypi.doubanio.com/simple/
RUN pip3 install -i https://pypi.doubanio.com/simple/ pillow
RUN pip3 install -i https://pypi.doubanio.com/simple/ scrapy_redis
# 将工作目录移到 /root/comics90/script
WORKDIR /root/comics90/script
# 启动容器时使用命令 python start.py 开始爬虫
CMD ["python", "start.py"]
将DockerFile最外层comics90同一目录,结构如下
.
├── comics90
│ ├── comics90
│ └── scrapy.cfg
└── Dockerfile
该目录运行docker build . --tag <Repository /name:version> 例如 docker build . --tag lymmurrain/90spider:12.0 ,注意有个点,意义是构建镜像的上下文。
然后创建一个 bridge类型的network,默认就是 bridge网络。
然后就是run一个mongo,一个redis,一个爬虫容器,--network参数都为自建的network,往redis放入start_url即可,其他机器改改连接数据库的ip就能使用。手动run每一个容器并不是我们的重点,如果读者对docker还不怎么熟悉可以自己尝试一下分别run部署。
Docker-compose 部署
由于我们这个爬虫需要redis,mongo与爬虫三个容器,手动部署起来麻烦,此时对于单机多容器部署就需要Docker-compose了。docker-compose是一个在单个服务器或主机上创建多个容器的工具,
将DockerFile,docker-compose.yml移动至与最外层comics90同一目录
文件结构如下
.
├── comics90
│ ├── comics90
│ └── scrapy.cfg
├── docker-compose.yml
├── docker-compose.yml.bk #备份
└── Dockerfile
先看一下docker-compose.yml写了什么,代码的意义在注释中
此时为单机部署,可以先不看deploy,deploy为docker swarm用stack集群部署用的
# 版本
version: "3.5"
# 服务
services:
# 定义一个spider服务
spider:
# 用那个image开启服务
image: "lymmurrain/90spider:12.0"
# 数据卷,code在一级key的volumes中已定义
volumes:
# 挂载卷用作调试及查看日志,注意卷里有源码,建议不要像我这样,我只是贪图方便。
- code:/root/comics90
# 依赖于mongodb及redis服务,但启动顺序并不一定会先启动玩mongo和redis再启动spider
depends_on:
- "mongodb"
- "redis"
# deploy为stack部署的参数
deploy:
# mode为 global,即每个节点部署一个该服务
mode: global
# 所用网络,在一级key的networks中已定义,stack部署会自动创建一个overlay网络用作容器通信。除非网络已存在
# 而docker-compose单机部署会自动创建一个bridge网络。除非网络已存在
networks:
- "spider"
mongodb:
image: "mongo"
# 挂载卷作持续存储
volumes:
- mongodb:/data/db
# 端口映射
ports:
- "22222:27017"
networks:
- "spider"
deploy:
# 服务副本数为一,因为只需统一储存在一个服务器的mongo中。
replicas: 1
# 该服务该放在哪个节点的配置
placement:
# 约束条件,放在manager节点上,由于只有一个manager节点,
# 所以只在该manager节点上部署
constraints: [node.role == manager]
# 环境变量,设置root权限的username和密码
environment:
MONGO_INITDB_ROOT_USERNAME: yourusername
MONGO_INITDB_ROOT_PASSWORD: yourpwd
# 运行该服务所用的命令
command:
# 由于服务器性能较差,设置wiredTigerCacheSizeGB为0.3
--wiredTigerCacheSizeGB 0.3
redis:
image: "redis"
ports:
- "21111:6379"
networks:
- "spider"
# 部署redis服务的时候要执行的命令
command: redis-server --requirepass yourpwd
deploy:
replicas: 1
placement:
constraints: [node.role == manager]
# 定义服务所需volumes
volumes:
code:
mongodb:
# 定义服务所需networks
networks:
spider:
启动服务
docker-compose up -d
服务启动时会自动创建一个birdge网络,并覆盖到提供服务的容器,所以爬虫源码中连接redis和mongo的ip地址可以直接写服务名称,该服务名解析的是容器的ip而不是宿主的ip哦,这和我们连接数据库的爬虫代码有很大关系,例如
# 连接mongo容器不需要ip而是直接写mongodb,即service中的服务名称,redis同理
# 看我连接的端口是27017而不是端口映射的22222
client = pymongo.MongoClient('mongodb',port=27017)
服务启动后进入redis容器中放入start_url
reids-cli #进入redis客户端
127.0.0.1:6379> lpush <你爬虫中定义的redis_key> <要爬取的start_url>
(integer) 1
放入之后爬虫就会启动
可通过进入数据卷查看爬虫日志判断是否启动成功
# 输入以下命令查看爬虫日志
root@VM-0-6-ubuntu:~# cd /var/lib/docker/volumes/spider_code/_data/script/
root@VM-0-6-ubuntu:/var/lib/docker/volumes/spider_code/_data/script# cat log.log
由于docker-compose单机部署所用的网络是birdge网络,不同宿主机中的容器是无法通过服务名互通的。
所以,此时如果用多台机器实现分布式爬取,需要修改源码中的redis及mongo ip地址再启动,但这不还是太麻烦了吗。
所以docker swarm 以及docker stack部署就呼之欲出了。
当然别看到两个新东西就怕,其实它们的命令都是一脉相承的,学起来是很流畅的。
数据库安全杂谈(与爬虫,swarm无关)
请谨记暴露在公网的数据库一定要,一定要加认证。我docker-compose中写的端口映射不是数据库默认端口的映射,如mongo不是27017:27017,一是因为能看清楚容器服务名称映射的是容器ip,而是防止默认的端口扫描。并且我两个数据库容器都加了认证。
为什么要这么谨慎,因为被逼急了。我刚开始为了贪图方便采用默认端口映射+无认证暴露在公网。在测试的时候无论是mongo容器还是redis容器里的数据都遭到黑客的破坏,mongo是删库,redis更过分,删数据+留下key为backup的数据,里面是指向挖矿程序脚本的下载并运行。并且看别人的遭遇,还可以利用redis的快照功能实现服务器的免密登录。还好我用的是docker,并没有对宿主机造成太大影响,甚至再开了一个容器看了看给我留下的脚本有什么用...。
就算这样也对我产生了很大的影响,由于redis是充当调度器的,一被删除数据就会认为爬取任务没有了,导致爬虫运行了十几分钟就无任务可爬(可以看出被黑得有多频繁,刚创建服务十几分钟就要被黑)。在花了一天的时间排查代码bug,重写代码生成了10个版本的镜像之后,最终才发现根本不是我代码的问题,是数据库被黑了。
一定要注意数据库安全呐
docker swarm
docker swarm就不说太多定义了,有兴趣深入了解最好进入官网学习。
docker swam的作用是部署跨主机的容器集群服务,Docker Swarm 与Docker Compose 都用于容器编排项目,不同的是,Docker Compose 是一个在单个服务器或主机上创建多个容器的工具,而 Docker Swarm 则用于多个服务器或主机上创建容器集群服务。
通过docker swarm,我们能很轻松在多台主机上管理docker容器来提供服务。
docker swarm 集群构建
首先要确定我们的架构需要一个redis服务充当爬虫的调度器,一个mongo服务持久化数据,一个爬虫服务用于爬取数据。
redis及mongo放在一台服务器上,而爬虫在集群中的每一台服务器都要部署。
开放端口
每个节点都要开放,注意7946端口是TCP,UDP都要开放
- 2377/TCP,用于客户端与swarm进行安全通信
- 7946/TCP,7946/UDP,用于控制面gossip分发
- 4789/UDP 用于VXLAN覆盖网络
初始化swarm集群
选定一台服务器作manager节点,初始化一个sawrm,初始化后则会自动将该台服务器作为manager节点加入swarm
root@VM-8-12-ubuntu:/home/ubuntu# docker swarm init --advertise-addr <指定其他节点连接到当前管理节点ip和端口>
Swarm initialized: current node (8ncpdezobapdi9okty3kcwcr8) is now a manager.
To add a worker to this swarm, run the following command:
#用该命令可将其他服务器加入swarm
docker swarm join --token SWMTKN-1-25rq6u6cb7jslkw63zb2ee3aqx1su2en734ftikjsmaflei2h7-1rdoqwrz1o54wbbie317op978 <你指定的 --advertise-addr>:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
注意--advertise-addr参数的设置,当你的服务器可以通过内网连接时推荐用内网ip保证传输速度与可靠,也可以指定一个节点上没有的ip如负载均衡的ip,由于我的两台服务器分居两地,我这就直接写公网地址了。
其他服务器加入swarm
复制初始化swarm时给出的命令,并且注意要加上--advertise-addr参数,其值是本机指定其他节点连接到当前管理节点ip和端口,我填的是本机的公网ip。
刚开始用时我没填--advertise-addr 导致程序无法通过服务名解析到相应的容器,说是该值还没定义。
如果要以manager节点身份加入,则用docker swarm join-token manager <初始化给的token> <--advertise-addr>
实例
root@VM-0-6-ubuntu:~# docker swarm join --token SWMTKN-1-0lxu61e2pcnhu40lt83znqupra04b4736h7gjitijpg1o6zn56-2jhrw6d5yyzzwxsrwplb20xwh 106.52.33.199:2377 --advertise-addr 101.32.176.13
This node joined a swarm as a worker.
创建服务
swarm集群创建完后就可以创建服务了,可用create命令创建服务,与run命令相似。但如此手动部署服务略麻烦,还需自己配置网络用于不同宿主机的容器通信,所以重点讲docker stack。
stack部署
stack部署就要看懂docker-compose.yml中的deploy项的配置了
概括一下就是,mongo与redis 容器部署在一台manager节点上,每个节点都部署一个spider容器
root@VM-8-12-ubuntu:/home/ubuntu# docker stack deploy -c docker-compose.yml spider
Creating network spider_spider
Creating service spider_redis
Creating service spider_spider
Creating service spider_mongodb
可以看到该命令创建了一个名为spider_spider的网络,该网络是overlay网络,供不同宿主机的容器使用,然后创建三个服务。
查看服务部署情况
root@VM-8-12-ubuntu:/home/ubuntu# docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
qix3dqr8m9wt spider_mongodb replicated 1/1 mongo:latest *:27017->27017/tcp
jpc621vf7rez spider_redis replicated 1/1 redis:latest *:6379->6379/tcp
wh3w2vgsahwd spider_spider global 2/2 lymmurrain/90spider:5.0
分别去两台机器看容器部署情况
# manager节点
root@VM-8-12-ubuntu:/home/ubuntu# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
149be693c653 lymmurrain/90spider:5.0 "python start.py" 4 minutes ago Up 4 minutes spider_spider.k00scjdvmp2nwelvpmvax6ajr.r3ckst6zowyud809nqy5q36fj
45376e9672b4 mongo:latest "docker-entrypoint.s…" 5 minutes ago Up 5 minutes 27017/tcp spider_mongodb.1.bhogfwjb2nqbgnb1nflk34z64
fab1bb6047fb redis:latest "docker-entrypoint.s…" 6 minutes ago Up 6 minutes 6379/tcp spider_redis.1.lcxmbgcq3j52u7lihybkhkyjk
# worker节点
root@VM-0-6-ubuntu:~# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7c3f0d60c93e lymmurrain/90spider:5.0 "python start.py" 5 minutes ago Up 4 minutes spider_spider.ta0dgxii7ccyznzrc4mjuvhl0.qp0rahgu6856mdfzzjevzytz8
进入manager节点的redis容器中放入start_url
reids-cli #进入redis客户端
127.0.0.1:6379> lpush <你爬虫中定义的redis_key> <要爬取的start_url>
(integer) 1
查看spider服务的数据卷中的日志确认是否出现问题
# 查看有哪些数据卷
root@VM-0-6-ubuntu:~# docker volume ls
DRIVER VOLUME NAME
local spider_code
# 查看spider服务数据卷的详细信息,找到Mountpoint
root@VM-0-6-ubuntu:~# docker volume inspect spider_code
[
{
"CreatedAt": "2020-12-05T14:23:23+08:00",
"Driver": "local",
"Labels": {
"com.docker.stack.namespace": "spider"
},
"Mountpoint": "/var/lib/docker/volumes/spider_code/_data",
"Name": "spider_code",
"Options": null,
"Scope": "local"
}
]
# 进入数据卷查看爬虫日志
root@VM-0-6-ubuntu:~# cd /var/lib/docker/volumes/spider_code/_data
root@VM-0-6-ubuntu:/var/lib/docker/volumes/spider_code/_data# cd script/
root@VM-0-6-ubuntu:/var/lib/docker/volumes/spider_code/_data/script# cat log.log
# 放入start_url前
2020-12-05 06:23:26 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2020-12-05 06:23:26 [scrapy.extensions.telnet] INFO: Telnet console listening on 127.0.0.1:6023
2020-12-05 06:24:26 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
# 放入start_url后
2020-12-05 06:26:26 [scrapy.extensions.logstats] INFO: Crawled 1 pages (at 1 pages/min), scraped 0 items (at 0 items/min)
2020-12-05 06:27:30 [scrapy.extensions.logstats] INFO: Crawled 29 pages (at 28 pages/min), scraped 14 items (at 14 items/min)
为什么会1分钟才爬28个页面呢,用过scrapy的都知道它的速度是很迅猛的。
我的爬虫之所以这么慢原因有三:
- 该台服务器在香港,且宽带只有1M,不慢是不可能的
- 爬虫的DOWNLOAD_DELAY 设置得大,毕竟要照顾到人家网站的承受能力,咱只取需要的东西,不杀鸡取卵。也希望大家如果玩爬虫的时候顾及以下对方网站的承受能力。
- docker swarm 的部署,manger节点在广州,worker节点在香港,无法通过内网连接,任务分发,pipline存储的传输时间长
至于为什么知道原因所在但不把效率优化,原因有二:
- 怂
- 穷
至此,利用docker swarm 集群部署 分布式scrapy爬虫完成。至于如何停止,扩缩容,更新,就有待读者深入研究了。
如有纰漏,欢迎斧正
参考文献
Docker三剑客之Docker Swarm
Docker官网文档
深入浅出Docker(Docker Deep Dive)