分布式数据抓取系统
大约3个月前,我发表了一篇文章,解释了我们构建Cloud Application的方法和考虑因素 。 从本文开始,我将逐步分享我们的实用设计来解决这一挑战。
如前所述,我们的最终目标是构建一个Saas大数据分析应用程序,并将其部署在AWS服务器中。 为了实现此目标,我们需要构建分布式的爬网,索引编制和分布式培训系统。
本文的重点是如何构建分布式爬网系统。 这个系统的名字叫Black Widow 。
要求
和往常一样,让我们从系统的业务需求开始。 我们的目标是建立一个可伸缩的爬网系统,该系统可以部署在云上。 该系统应该能够在不可靠的高延迟网络中运行,并且可以从部分硬件或网络故障中自动恢复。
对于第一个版本,系统可以从3种来源进行爬网: Datasift , Twitter API和Rss feed。 爬回的数据称为
评论 。 Rss爬网程序假定阅读公共资源,例如网站或博客。 这个是免费的。 DataSift和Twitter均提供专有API来访问其流服务。 Datasift通过评论计数和CSLD(Curated Stream Definition Language,他们自己的查询语言)的复杂性向用户收费。 另一方面,Twitter提供免费的Twitter Sampler流。
为了进行成本控制,我们需要实现一种机制来限制从Datasift之类的商业来源爬取的评论数量。 由于Datasift提供了Twitter评论,因此可能有来自不同来源的单个评论。 目前,我们尚未尝试消除它并将其视为数据重复。 但是,可以通过用户配置手动解决此问题(避免同时选择Twitter和Datasift Twitter)。
为了将来扩展,系统应该能够将相关评论链接到对话中。
思想的食物
集中式架构
当需要时,我们的第一个想法是在节点上建立爬网(我们称为Spawn),并让集线器(我们称为Black Widow)来管理节点之间的协作。 这个想法很快被团队成员接受,因为它允许系统在中心执行有限的工作时很好地扩展。
与任何其他集中式系统一样,Black Widow遭受单点故障问题的困扰。 为了缓解此问题,我们允许节点在失去与Black Widow的连接后的短时间内独立运行。 这将为支持团队提供喘息的机会来启动备份服务器。
系统中的另一个瓶颈是数据存储。 对于要爬网的数据量(每秒轻松达到数千条记录), NoSQL显然是存储已爬网注释的选择。 我们有与Lucene和MongoDB合作的经验。 但是,经过研究和一些小实验,我们选择Cassandra作为NoSQL数据库。
出于这些想法,我们将可视化根据该原型构建的分布式爬网系统:
在上图中,Black Widow或集线器是唯一有权访问SQL数据库系统的服务器。 我们在这里存储配置以进行爬网。 因此,所有Spawns或爬网节点都是完全无状态的。 它只是唤醒,将自己注册到Black Widow并执行分配的作业。 得到评论后,Spawn将其存储到Cassandra集群中,还将其推送到一些队列中以进行进一步处理。
集体讨论可能的问题
为了向非技术人员解释设计,我们希望将业务需求与现实生活中的类似问题联系起来,以便于理解。 我们选择的类似问题是志愿者之间的合作。
想象一下,如果我们需要为即将到来的奥运会做大量准备工作,并决定招募全世界的志愿者来提供帮助。 我们不认识志愿者,但是志愿者知道我们的电子邮件,因此他们可以联系我们进行注册。 只有这样,我们才能知道他们的电子邮件,并可能通过电子邮件将任务发送给他们。 我们不想将一项任务发送给两名志愿者,也不想将某些任务无人照管。 我们希望平均分配任务,以使没有志愿者遭受太多痛苦。
由于费用问题,我们不会通过手机与他们联系。 但是,由于电子邮件的可靠性较差,因此当向志愿者发送任务时,我们将要求确认。 仅当志愿者回答确认后,才认为任务已分配。
在上面的示例中,志愿者代表Spawn节点,而电子邮件通信代表不可靠且高延迟的网络。 这是我们需要解决的一些问题:
1 /节点故障
对于此问题,最好的方法是定期检查。 如果志愿者停止响应常规进度检查电子邮件,则应将任务重新分配给其他人。
2 /优化任务分配
一些任务是相关的。 因此,将相关任务分配给同一个人可以帮助减少总精力。 我们的抓取也会发生这种情况,因为某些抓取配置具有相似的搜索字词,将它们组合在一起以共享流媒体频道将有助于减少最终费用。
另一个问题是在志愿者之间平均分配作品数量的公平性或能力。 我们可以想到的最简单的策略是循环赛,但通过记住较早的任务进行了一些细微的调整。 因此,如果一项任务与我们之前分配的任务非常相似,则可以从循环选择中跳过该任务,而直接将其分配给同一位志愿者。
3 /集线器不工作
如果由于某些原因,我们的电子邮件服务器已关闭并且我们无法再与志愿者联系,则最好让志愿者停止执行分配任务。 这里的主要问题是成本超支或浪费的精力。 但是,立即停止工作太匆忙,因为临时基础结构问题可能会导致通信问题。
因此,我们需要找到一个合理的时间,使节点从集线器分离后才能继续运行。
4 /成本控制
由于业务需求,我们需要实现两种成本控制。 首先是每个爬网程序正在爬网的注释总数,其次是所有爬网程序属于同一用户的爬网注释总数。
这是我们就实施成本控制的最佳方法进行辩论的地方。 为每个搜寻器实施限制非常简单。 我们可以简单地将此限制传递给Spawn节点,并在达到该限制时自动停止搜寻器。
但是,对于每个用户的限制,并不是那么简单,我们有两种可能的方法。 为了更简单的选择,我们可以将一个用户的所有爬网程序发送到同一节点。 然后,类似于先前的问题,Spawn节点知道收集的评论数量,并在达到限制时停止所有搜寻器。 这种方法很简单,但是它限制了在节点之间平均分配作业的能力。 另一种方法是让所有节点检索并更新全局计数器。 这种方法在内部产生了巨大的网络流量,并大大延长了评论处理时间。
在这一点上,我们暂时选择全局反制方法。 如果性能成为一个很大的问题,可以再次考虑。
5 /在云端部署
与其他任何Cloud应用程序一样,我们不能对网络或基础架构抱有太多信任。 这是我们使我们的应用程序符合上一篇文章中提到的清单的方式:
- 无状态的 :我们的生成节点是无状态的,但集线器不是。 因此,在我们的设计中,节点负责实际工作,而集线器仅协同工作。
- 等幂 :我们实施的hashCode和每一个抓取的配置相同的方法。 我们将搜寻器配置存储在“地图”或“集合”中。 因此,可以多次发送搜寻器配置,而不会产生任何其他副作用。 此外,我们的节点选择方法可确保将作业发送到同一节点。
- 数据访问对象 :我们在每个模型对象上应用JsonIgnore过滤器,以确保没有机密数据在网络中飞来飞去。
- 安全起见 :我们为每个节点和集线器本身实现运行状况检查API。 发生任何错误时,第一级支持将立即得到通知。
6 /恢复
我们尽力使系统从部分故障中恢复过来。 我们可以从某些类型的故障中恢复:
- 集线器故障 :节点在启动时将自身注册到集线器。 从那时起,这是一种仅当集线器将作业发送到节点并轮询状态更新时的单向通信。 如果该节点在预定时间内未能从集线器获得任何联系,则认为该节点已分离。 如果一个节点分离,它将清除所有作业配置并再次开始将其自身注册到集线器。 如果事件是由集线器故障引起的,则新的集线器将从数据库中检索爬网配置,然后再次开始分配作业。 当Spawn节点进入分离模式时,将清除Spawn节点上的所有现有作业。
- 节点故障 :当集线器无法轮询节点时,它将通过删除所有工作作业并从头开始重新分配到工作节点来进行硬重置。 此重新分发过程有助于确保优化的分发。
- 作业失败 :集线器发送和轮询作业时发生了两种失败。 如果作业在轮询过程中失败,但是Spawn节点仍然运行良好,Black Widow可以再次将作业重新分配给同一节点。 如果作业发送失败,可以执行相同的操作。
实作
数据源和订户
最初的想法是,每个搜寻器都可以打开自己的通道来检索数据,但是在进一步检查时,这不再有意义。 对于Rss,我们可以扫描所有URL一次,找出可能属于多个搜寻器的关键字。 对于Twitter,单个查询最多支持200个搜索词。 因此,我们可以打开为多个搜寻器服务的单个通道。 对于Datasift,它很少见,但是由于人为的错误或运气,可能会使搜寻器具有相同的搜索词。
这种情况导致我们将搜寻器分为两个实体:订户和数据源。 订户负责使用注释,而数据源负责对注释进行爬网。 通过这种设计,如果有两个具有相似关键字的搜寻器,则将创建一个数据源来服务两个订户,每个订户以自己的方式处理评论。
仅当不存在类似数据源时,才会创建数据源。 当第一个订阅者订阅它时,它开始工作;当最后一个订阅者取消订阅时,它退休。 借助Black Widow将相似的订户发送到同一节点,我们可以最大程度地减少创建的数据源的数量,并间接地使爬网成本最小化。
数据结构
数据结构最关心的是线程安全问题。 在Spawn节点中,我们必须将所有正在运行的订户和数据源存储在内存中。 在某些情况下,我们需要修改或访问这些数据:
- 当订户达到限制时,它将自动取消订阅数据源,这可能导致数据源停用。
- 当Black Widow将新订户发送到Spawn节点时。
- 当Black Widow发送取消订阅现有订户的请求时。
- 运行状况检查API公开了所有正在运行的订户和数据源。
- 黑寡妇会定期轮询每个分配订户的状态。
- Spawn节点定期检查并禁用孤立订阅者(未被Black Widow轮询的订阅者)。
数据结构的另一个问题是运算的幂等性。 以上任何操作都可能丢失或重复。 为了解决这个问题,这是我们的方法
- 为每个订户和数据源实现hashCode和equals方法。
- 我们选择Set或Map来存储订户和数据源的集合。 对于具有相同哈希码的记录,当有新插入时, Map将替换该记录,但是Set将跳过新记录。 因此,如果使用Set ,则需要确保新记录可以替换旧记录。
- 我们在数据访问代码中使用同步 。
- 如果Spawn节点收到的新订阅者与现有订阅者相似,它将进行比较并更喜欢更新现有订阅者,而不是替换现有订阅者。 这避免了取消订阅和订阅相同订户的过程,这可能会中断数据源流。
路由
如前所述,我们需要找到一种满足以下两个目的的路由机制:
- 在Spawn节点之间平均分配作业。
- 将相似的作业路由到相同的节点。
我们通过为每个名为uuid的查询生成唯一的表示形式来解决此问题。 之后,我们可以使用一个简单的模块化函数来查找要路由的注释:
int size = activeBwsNodes.size();
int hashCode = uuid.hashCode();
int index = hashCode % size;
assignedNode = activeBwsNodes.get(index);
通过此实现,具有类似uuid的订户将始终被发送到同一节点,并且每个节点被选择服务给订户的机会均等。
当活动Spawn节点的集合发生更改时,可以将整个实践搞砸。 因此,每当有节点更改时,Black Widow必须清除所有正在运行的作业,并从头开始重新分配。 但是,节点更改在生产环境中应该很少见。
握手
下面是Black Widow和Node合作的序列图:
黑寡妇不知道Spawn节点。 它等待Spawn节点将自己注册到Black Widow。 Black Widow从那里负责轮询节点以维护连接性。 如果Black Widow无法轮询节点,它将从其容器中删除该节点。 由于不再对其进行轮询,因此孤立节点最终将进入分离模式。 在此模式下,Spawn节点将清除现有作业,然后尝试重新注册自己。
下图是订户生命周期:
与上述类似,Black Widow负责轮询发送给Spawn节点的订户。 如果不再由Black Widow轮询订户,则Spawn节点会将订户视为孤儿并将其删除。 这种做法有助于消除Spawn节点运行废弃用户的威胁。
在Black Widow上,当订户轮询失败时,它将尝试获取一个新节点来分配作业。 如果订阅者的Spawn节点仍然可用,则由于我们使用的路由机制,同一作业很可能会再次到达同一节点。
监控方式
在令人满意的情况下,所有订户都在运行,Black Widow正在轮询,并且没有其他任何React。 但是,这在现实生活中不太可能发生。 由于各种事件,Black Widow和Spawn节点将不时发生变化。
对于“黑寡妇”,在以下情况下会有所变化:
- 订户命中限制
- 找到新的订户
- 现有用户已被用户禁用
- 订阅者轮询失败
- Spawn节点的轮询失败
为了处理更改,Black Widow监视工具提供了两项服务:硬重装和软重装。 硬重载发生在节点更改时,而软重载发生在订户更改时。 硬重载过程会收回所有正在运行的作业,并从头开始重新分配可用节点。 软重装过程将删除作废的作业,分配新作业,然后重新分配失败的作业。
与Black Widow相比,Spawn节点的监视更为简单。 两个主要问题是保持与Black Widow的连接以及删除孤儿订户。
部署策略
部署策略很简单。 我们需要启动Black Widow和至少一个Spawn节点。 Spawn节点应该知道Black Widow的URL。 从那时起,运行状况检查API将使用每个节点的订户数量。 如果现有节点超载,我们可以将Health Check与AWS API集成以自动启动新的Spawn节点。 Spawn节点映像将需要让Spawn应用程序作为服务运行。 同样,当不使用节点时,我们可以关闭冗余Spawn节点。
黑寡妇由于其重要性而需要特殊对待。 如果Black Widow失败,我们可以重新启动应用程序。 这将导致Spawn节点上的所有现有作业变为孤立状态,并且所有Spawn节点都进入分离模式。 慢慢地,所有节点将自行清理并尝试再次注册。 在默认配置下,整个重新启动过程将在15分钟内完成。
威胁和可能的改进
选择集中式体系结构时,我们知道Black Widow是系统的最大风险。 尽管Spawn节点故障只会在受影响的订户中造成较小的中断,但是Black Widow故障最终会导致Spawn节点重新启动,这将需要更长的时间才能恢复。
而且,即使系统可以部分恢复,恢复过程中也会出现服务中断的情况。 因此,如果由于不稳定的基础结构而使轮询请求经常失败,则将极大地阻碍操作。
可伸缩性是集中式体系结构的另一个问题。 黑寡妇可以处理的最大Spawn节点数量尚未确定。 从理论上讲,这应该很高,因为“黑寡妇”只做较小的处理,它的大部分工作都在发出HTTP请求上。 网络可能是此体系结构的主要限制因素。 因此,我们让Black Widow轮询节点,而不是节点轮询Black Widow(其他人这样做,例如Hadoop)。 通过这种方法,Black Widow可以按自己的速度工作,而不会在Spawn节点的压力下工作。
我们得到的第一个问题是它是否是Map Reduce问题,答案是否定的。我们的分布式搜寻系统中的每个订户都处理自己的评论,并且不将结果报告给Black Widow。 这就是为什么我们不使用任何像Hadoop这样的Map Reduce产品的原因。 我们的监控程序了解业务逻辑,而不仅仅是纯粹的基础架构监控,这就是我们选择使用Zoo Keeper或AKKA等监控工具进行构建的原因 。
为了将来进行改进,最好让多个集线器相互协作,以摆脱集中式体系结构。 如果Black Widow唯一一次访问数据库正在加载订户,这应该不太困难。 因此,我们可以对数据进行切片,然后让每个“黑寡妇”加载其中的一部分。
让我感到非常不满意的另一点是检查全局计数器的用户限制。 随着对每条评论爬网的检查的进行,这大大增加了内部网络流量,并限制了系统的可伸缩性。 更好的策略应该是根据处理速度分配配额。 Black Widow可以为每个订户(在不同节点上)调节和重新分配配额。
翻译自: https://www.javacodegeeks.com/2014/09/distributed-crawling.html
分布式数据抓取系统