V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
tikazyq
V2EX  ›  Go 编程语言

爬虫平台 Crawlab 核心原理--分布式架构

  •  
  •   tikazyq ·
    tikazyq · 2019-08-09 17:10:16 +08:00 · 4590 次点击
    这是一个创建于 1693 天前的主题,其中的信息可能已经有所发展或是发生改变。

    背景

    Crawlab 自第一版发布已经几个月了,其中经历了好几次迭代:版本从v0.1到了v0.3.0;后端语言从 Python 到了 Golang ;从最初使用 Celery 作为任务调度引擎,到自己开发分布式任务调度引擎;从只能运行自定义爬虫到可以运行可配置爬虫(虽然还没迁移到最新版本);从手动部署爬虫到自动部署爬虫;从自己搭建环境到Docker 部署;从手动执行任务到定时任务;等等(详情见CHANGELOG)。在使用者们的反馈下,Crawlab 爬虫平台也逐渐变得稳定和实用,能够真正帮助到有爬虫管理需求的用户。如今在 Github 上有近 1k 的 star,相关社区(微信群、微信公众号)也建立起来,四分之一的用户表示已经将 Crawlab 应用于企业爬虫管理。可以看出,Crawlab 是受开发者们关注和喜欢的。

    Github: https://github.com/tikazyq/crawlab

    为什么需要爬虫管理平台

    对于一般的爬虫爱好者来说,写一个单机爬虫脚本已经足够,而且 Scrapy 这样的优秀爬虫框架能够让开发者轻松调试、编写一个简单的爬虫,需要定时任务就直接上 Crontab,分分钟搞定。然而,一般的企业对爬虫的要求相对较高,其中主要涉及一个问题,也就是规模( Scale )。当然,这里的规模是指大型规模。爬虫的规模分为两种:一种是爬虫需要抓取大量的数据( Volume ),例如全网抓取淘宝的商品;另一种是爬虫需要涵盖大量的网站( Coverage ),例如搜索引擎。

    不同的规模需要有不同的架构策略(如下图):

    1. 当网站数量只有一个,抓取结果不多时,只需要单机即可,并不需要分布式爬虫;
    2. 但当需要抓取结果量级提升时,例如全网抓取淘宝,就需要用分布式爬虫了,这是因为单个机器的带宽和计算资源不足以做到全网抓取,而且为了应对反爬虫技术,还需要大量的代理 IP ;
    3. 同理,当需要抓取网站的数量增多时,例如你需要创建一个新闻搜索引擎,你同样需要多台机器来获取足够的带宽和计算资源;
    4. 对于同时要求 Volume 和 Coverage 的应用,不是一般的小企业或个人能做的,对不管是人力和机器的资源都非常高。

    而爬虫管理平台就是针对情况(2)、(3)、(4)而存在的分布式管理平台,能够让用户轻松管理多个爬虫或多机运行的爬虫。

    Crawlab 从诞生之初就解决了分布式爬虫问题,最早采用了 Celery 作为分布式任务调度引擎,以 Redis 作为消息队列,HTTP 请求作为节点通信媒介,简单地实现了分布式管理。但随着用户不断使用 Crawlab,发现这样的方式并不是很方便,用户需要指定节点的 IP 地址和 API 端口,而且还不能指定节点执行任务。因为各种问题,在最新版本v0.3.0用 Golang 重构后端的时候,就将 Celery 弃用了,转而自己开发分布式节点的监控和通信应用,这样更加灵活和高效。本文是核心原理介绍,下面将着重介绍 Crawlab 的分布式架构原理( Golang 版本)。

    整体架构

    Crawlab 的整体架构如下图,由五大部分组成:

    1. 主节点( Master Node ):负责任务派发、API、部署爬虫等;
    2. 工作节点( Worker Node ):负责执行爬虫任务;
    3. MongoDB 数据库:存储节点、爬虫、任务等日常运行数据;
    4. Redis 数据库:储存任务消息队列、节点心跳等信息。
    5. 前端客户端:Vue 应用,负责前端交互和向后端请求数据。

    以执行爬虫任务为例,它是 Crawlab 中常见的使用场景,我们来看看它具体是如何工作的:

    1. 前端向主节点发起请求,要求指定在某一工作节点执行任务;
    2. 主节点收到该请求,并将任务数据推送到 Redis 任务队列中;
    3. 工作节点持续监听 Redis 任务队列,并利用 LPOP 获取任务;
    4. 工作节点执行任务,并将结果写回到储存数据库;

    以上就是执行爬虫任务的大致流程。当然,这还不是全部,我们还需要考虑日志处理、并发执行、取消任务等细节问题。具体的处理信息,请查看相关文档源代码

    总的来说,可以将主节点看作是 Crawlab 整体架构的中控系统,理解为 Crawlab 的大脑;工作节点是实际干活的部分,是 Crawlab 的运动躯体; MongoDB 和 Redis 是负责通信交流的,可以看作 Crawlab 的血液和神经网络。这些模块一起构成了一个完整、自洽、相互协作的系统。

    节点注册和监控

    节点监控主要是通过 Redis 来完成的(如下图)。

    工作节点会不断更新心跳信息在 Redis 上,利用HSET nodes <node_id> <msg>,心跳信息<msg>包含节点 MAC 地址,IP 地址,当前时间戳。

    主节点会周期性获取 Redis 上的工作节点心跳信息。如果有工作节点的时间戳在 60 秒之前,则考虑该节点为离线状态,会在 Redis 中删除该节点的信息,并在 MongoDB 中设置为"离线";如果时间戳在过去 60 秒之内,则保留该节点信息,在 MongoDB 中设置为"在线"。

    该架构的优点

    这样,就做到了一个监控节点是否在线的节点注册系统。这样架构的好处在于,节点之间根本不用像 HTTP、RPC 那样 IP 或端口,只需要知道 Redis 的地址就可以完成节点注册和监控。因此,也就减少了用户配置节点的操作,简化了使用流程。同时,由于隐藏了 IP 地址和端口,也更为安全。另外,相较于 Celery 版本的监控,我们去除了 Flower 服务,不用在服务中单独起一个 Flower 服务的进程,减少了开销。

    下图是 Crawlab UI 界面上的节点之间的关系图(拓扑图)。

    该架构的缺点

    相较于一些常见的分布式架构,例如 Zookeeper,Crawlab 还存在一些不完善的地方。

    高可用性( High Availability )是 Crawlab 暂时还没有做得很好的。例如,当主节点宕机的时候,整个系统就会瘫痪,因为主节点是 Crawlab 的大脑中枢,负责很多功能。如果主节点宕机,前端就无法获取 API 数据,任务无法调度,当然也无法监控节点了。虽然 Zookeeper 没有将可用性( Availability )做得非常完善,但其投票选举机制保证了其一定程度的高可用。如果 Crawlab 要改善这一点的话,会在主节点宕机后,用一定的方式选举出另一个主节点,保证高可用。

    节点通信

    如果仔细看上面的整体架构图的话,你可能会注意到 Crawlab 中通信有两种。一种是同步信息( Sync via Msg ),另一种是派发任务( Assign Tasks )。这两种通信分别叫即时通信延迟通信。下面分别介绍。

    即时通信

    即时通信是指某节点 A 通过某种媒介向另一节点 B 发送信息,取决于是否为双向通信,节点 B 收到信息后可能会通过同一种媒介将信息回复给节点 A。

    Crawlab 的即时通信是通过 Redis 的 PubSub 来实现的(如下图)。

    所谓 PubSub,简单来说是一个发布订阅模式。订阅者( Subscriber )会在 Redis 上订阅( Subscribe )一个通道,其他任何一个节点都可以作为发布者( Publisher )在该通道上发布( Publish )消息。

    在 Crawlab 中,主节点会订阅 nodes:master 通道,其他节点如果需要向主节点发送消息,只需要向nodes:master发布消息就可以了。同理,各工作节点会各自订阅一个属于自己的通道nodes:<node_id>( node_id 是 MongoDB 里的节点 ID,是 MongoDB ObjectId ),如果需要给工作节点发送消息,只需要发布消息到该通道就可以了。

    一个网络请求的简单过程如下:

    1. 客户端(前端应用)发送请求给主节点( API );
    2. 主节点通过 Redis PubSub 的<nodes:<node_id>通道发布消息给相应的工作节点;
    3. 工作节点收到消息之后,执行一些操作,并将相应的消息通过<nodes:master>通道发布给主节点;
    4. 主节点收到消息之后,将消息返回给客户端。

    Crawlab 的获取日志、获取系统信息、取消任务、告知节点获取爬虫文件都是通过即时通信完成的。

    而实现代码相对来说有些复杂。下面是主节点的 PubSub 回调函数。

    func MasterNodeCallback(channel string, msgStr string) {
    	// 反序列化
    	var msg NodeMessage
    	if err := json.Unmarshal([]byte(msgStr), &msg); err != nil {
    		log.Errorf(err.Error())
    		debug.PrintStack()
    		return
    	}
    
    	if msg.Type == constants.MsgTypeGetLog {
    		// 获取日志
    		fmt.Println(msg)
    		time.Sleep(10 * time.Millisecond)
    		ch := TaskLogChanMap.ChanBlocked(msg.TaskId)
    		ch <- msg.Log
    	} else if msg.Type == constants.MsgTypeGetSystemInfo {
    		// 获取系统信息
    		fmt.Println(msg)
    		time.Sleep(10 * time.Millisecond)
    		ch := SystemInfoChanMap.ChanBlocked(msg.NodeId)
    		sysInfoBytes, _ := json.Marshal(&msg.SysInfo)
    		ch <- string(sysInfoBytes)
    	}
    }
    

    这里其实是用msg.Type来区分消息类别,如果要扩展的话需要写不少if/else。工作节点的回调函数也需要写类似的逻辑。

    这个可能跟 HTTP 请求和 RPC 通信相较来说麻烦一些。不过,这其实和 WebSocket 非常像(对 WebSocket 不了解的同学可以看看韦世东最近的文章《开发者必知必会的 WebSocket 协议》),都需要在客户端和服务端定义回调函数。一个改进方法是不用if/else来区分信息类别,转而用 PubSub 频道名称,监听多个频道。总之,具体实践中怎么选择,还需要考虑实际情况。

    延迟通信

    延迟通信对即时性要求不高,不需要节点或客户端对请求即时回复。通常来说,延迟通信的实现方式有队列、轮询等方式。这样的方式不要求即时性。延迟通信主要是用作需要长时间的操作,例如发送邮件、数据处理、构建应用等等。

    Crawlab 中的延迟通信主要包含任务队列以及轮询,都是通过 Redis 来实现的。任务队列是用作爬虫任务执行:主节点接收抓取请求后,将执行任务的消息推到任务队列中,工作节点不断轮询任务队列,获取任务并执行(如下图)。Crawlab 爬虫任务执行的详情请参见相关文档源代码

    爬虫任务执行

    Crawlab 的延迟通信主要包括爬虫任务执行和爬虫部署。爬虫任务执行这里不再赘述。下面简单介绍一下爬虫部署(流程如下图)。

    爬虫部署

    整个爬虫部署的生命周期:

    1. 主节点每 5 秒,会从爬虫的目录获取爬虫信息,然后更新到数据库(这个过程不涉及文件上传);
    2. 主节点每 60 秒,从数据库获取所有的爬虫信息,然后将爬虫打包成 zip 文件,并上传到 MongoDB GridFS,并且在 MongoDB 的 spiders 表里写入 file_id 文件 ID ;
    3. 主节点通过 Redis PubSub 发布消息( file.upload 事件,包含文件 ID )给工作节点,通知工作节点获取爬虫文件;
    4. 工作节点接收到获取爬虫文件的消息,从 MongoDB GridFS 获取 zip 文件,并解压储存在本地。

    这样,所有爬虫将被周期性的部署在工作节点上。

    在后续的开发中,Crawlab 将会加入邮件通知、短信通知、微信推送等功能。而这些都是属于延迟通信的范畴,主要实现方法无外乎队列和轮询。

    分布式实践 - 抓取上百个新闻网站

    下面将介绍一个多机爬虫的实际应用场景,帮助大家深入理解 Crawlab 的分布式原理。

    首先,你可能需要足够的网络带宽,因为需要抓取的网站上百了,不是简简单单的单机爬虫,你需要多台机器。这里只是简单介绍下拓扑架构,并不会详细介绍大规模爬虫的去重、反爬、容错等逻辑。如下图,每一个工作节点可以限制抓取一部分网站,总共 M 个网站平均分配给 N 个工作节点。在 Crawlab 中就是用指定节点的方式了,这个不难。另外,你也可以通过随机分配的方式来派发任务,每一个工作节点统计上也会均匀分配到任务,这其实也就是一个负载均衡( Load Balancing )的过程。

    当然,你可能好奇上百个新闻网站是不是需要写上百个爬虫。对于这个问题,没有确切的准确回答。答案应该是“看情况”。对于自动提取字段的算法不够自信的开发者来说,可以选择 Crawlab 的可配置爬虫(看这篇文章《我是如何在 3 分钟内开发完一个爬虫的》),这样的开发成本相对来说比较小。但是对于已经有技术实力的可以写出很好的通用提取规则的选手来说,只写一个通用爬虫就足够了(简单的列表页提取规则参考《爬虫平台 Crawlab 核心原理--自动提取字段算法》),抓取多个网站等于抓取一个网站,不过还是需要部署在多个机器上,以求最大的带宽和计算资源。当然,不管是哪一种,都绕不开去重、反爬、错误监控,不过这些不在本文讨论范围,网上有很多教程可以多学习一下。

    社区

    如果您觉得 Crawlab 对您的日常开发或公司有帮助,请加作者微信 tikazyq1 并注明"Crawlab",作者会将你拉入群。欢迎在 Github 上进行 star,以及,如果遇到任何问题,请随时在 Github 上提 issue。另外,欢迎您对 Crawlab 做开发贡献。

    往期文章

    7 条回复    2019-08-09 20:52:36 +08:00
    Cellei
        1
    Cellei  
       2019-08-09 17:57:21 +08:00
    已 Star
    tikazyq
        2
    tikazyq  
    OP
       2019-08-09 18:34:06 +08:00 via iPhone
    @Cellei,感谢支持
    xxxy
        3
    xxxy  
       2019-08-09 19:00:51 +08:00 via Android
    楼主你好,想问下你的爬虫有自动触发页面所有的事件以获取足够多的内容吗? 最近在写一个爬虫,被这个需求卡住了
    tikazyq
        4
    tikazyq  
    OP
       2019-08-09 19:27:29 +08:00 via iPhone
    @xxxy 我这个不是爬虫,而是爬虫管理平台,有兴趣可以加我微信加群讨论这个问题 tikazyq1
    tikazyq
        5
    tikazyq  
    OP
       2019-08-09 19:28:36 +08:00 via iPhone
    @xxxy 而且,不知道你触发事件具体是指什么,是否是指所有 addEventListener 的事件,还是所有可以点击的按钮,之类的
    xxxy
        6
    xxxy  
       2019-08-09 20:14:40 +08:00 via Android
    @tikazyq 其实就是触发 dom 上绑定的所有事件
    tikazyq
        7
    tikazyq  
    OP
       2019-08-09 20:52:36 +08:00 via iPhone
    @xxxy 这个你需要去研究下前端知识
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2715 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 00:22 · PVG 08:22 · LAX 17:22 · JFK 20:22
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.