scrapy-redis分布式爬虫使用及docker swarm集群部署

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组件。

  1. 下载scrapy-redis组件:pip3 install scrapy-redis

  2. redis配置文件的配置(使用docker不需配置):注释bind 127.0.0.1,表示可以让其他ip访问redis,

    protected-mode no,表示可以让其他ip操作redis。

  3. 修改爬虫文件中的相关代码:基于Spider的类将父类修改成RedisSpider,基于CrawlSpider的,将其父类修改成RedisCrawlSpider。spider中定义一个redis_key (用于往redis中放入start_urls),示例如下

    • class Crawl90Spider(RedisCrawlSpider):
          name = 'crawl90'
          redis_key = 'comicSpider'
      
  4. settings修改

    1. 加入以下代码
      # 使用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}
      
    2. 应用pipeline(可直接用RedisPipeline,我用的是自己写的mongo管道)
      ITEM_PIPELINES = {
          'comics90.pipelines.MongoPipeline': 300,
          # 'scrapy_redis.pipelines.RedisPipeline': 400
      }
      
  5. 开启爬虫程序

  6. 打开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 例如 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)

你可能感兴趣的:(docker-compose,docker,分布式,爬虫)