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'
    # 使用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
    
    自定义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)