推文中经常包含指向网络上各种内容的URL或链接,包括图片、视频、新闻文章和博客帖子。SpiderDuck 是 Twitter 上的一个服务,实时获取推文中分享的所有URL,解析下载的内容以提取感兴趣的元数据,并使这些元数据在几秒钟内可供其他 Twitter 服务使用。
Twitter的几个团队需要访问链接内容,通常是实时的,以改进Twitter产品。例如:
- 搜索以索引已解析的 URL 并提高相关性
- 客户端显示特定类型的媒体,如照片,与推文并列
- 推文按钮计算每个 URL 在 Twitter 上被分享的次数
- 信任与安全帮助检测恶意软件和垃圾邮件
- 分析显示有关在 Twitter 上分享的链接的各种汇总统计数据
背景
在SpiderDuck之前,Twitter提供了一个通过发出HEAD请求并跟踪重定向来解析推文中共享的所有URL的服务。尽管这项服务简单且满足了公司当时的需求,但它也存在一些局限性:
- 它解析了URL,但实际上没有下载内容。解析信息存储在内存缓存中,但没有持久地存储到磁盘。这意味着如果内存缓存实例重新启动,数据将丢失。
- 它没有实现现代机器人典型的礼貌规则,例如速率限制和遵循robots.txt指令。
显然,我们需要构建一个真正的URL获取器,克服上述限制,并能长期满足公司的需求。我们最初考虑使用或在现有开源URL爬虫的基础上构建。然而,我们意识到几乎所有可用的爬虫都具有两个我们不需要的属性:
- 它们是递归爬虫。也就是说,它们被设计为获取页面,然后递归地爬取从这些页面中提取的链接。递归爬取涉及爬取调度和长期排队的复杂性,这与我们的用例无关。
- 它们针对大批量爬取进行了优化。我们需要的是一个快速、实时的URL获取器。
因此,我们决定设计一个新系统,以满足 Twitter 的实时需求,并随着其增长而水平扩展。我们没有重复造轮子,而是在开源构建块的基础上大部分构建了新系统,从而仍然利用了开源社区的贡献。
这是 Twitter 许多工程问题的典型特征 - 尽管它们类似于其他大型互联网公司的问题,但一切都要实时运行的要求带来了独特且有趣的挑战。
系统概述
这里是 SpiderDuck 工作原理的概述。以下图示了其主要组件。
Kestrel: 这是Twitter广泛使用的消息队列系统,用于排队传入的推文。
Schedulers: 这些作业确定是否获取 URL、安排获取操作、跟踪重定向跳转(如果有的话)。在获取操作之后,它们解析已下载内容,提取元数据,并将元数据写入元数据存储库,将原始内容写入内容存储库。每个调度程序独立于其他调度程序执行其工作;也就是说,可以添加任意数量的调度程序以横向扩展系统,以适应推文和 URL 量的增长。
Fetchers: 这些是Thrift服务器,维护短期的URL获取队列,发出实际的HTTP获取请求,并实现速率限制和robots.txt处理。与Schedulers一样,Fetchers可以根据获取速率进行水平扩展。
Memcached: 这是由抓取程序用来临时存储 robots.txt 文件的分布式缓存。
Metadata Store: 这是一个基于Cassandra的分布式哈希表,用于存储按URL键入的页面元数据和分辨率信息,以及系统最近遇到的每个URL的获取状态。该存储为Twitter上需要实时访问URL元数据的客户端提供服务。
内容存储: 这是一个HDFS集群,用于存档已下载的内容和所有获取信息。
我们现在将更详细地描述SpiderDuck的两个主要组件——URL调度器和URL获取器。
URL调度器
以下图示了SpiderDuck Scheduler中处理过程的各个阶段。
与SpiderDuck的大部分组件一样,调度器是建立在Twitter开发的名为Finagle的开源异步RPC框架之上的。(事实上,这是最早利用Finagle的项目之一。)上图中的每个方框,除了Kestrel Reader之外,都是Finagle Filter - 一种允许将一系列处理阶段轻松组合成完全异步管道的抽象。完全异步使SpiderDuck能够使用少量固定线程处理高吞吐量。
Kestrel Reader 不断轮询新的 Tweets。当有 Tweets 进来时,它们会被发送到 Tweet Processor,该处理器从中提取 URL。然后每个 URL 被发送到 Crawl Decider 阶段。该阶段从 Metadata Store 读取 URL 的抓取状态,以检查 SpiderDuck 是否之前已经看过该 URL。Crawl Decider 然后根据预定义的抓取策略(即,如果 SpiderDuck 在过去 X 天内已经抓取过,则不再抓取)决定是否应该抓取该 URL。如果 Decider 决定不抓取该 URL,则记录状态以指示处理已完成。如果决定抓取该 URL,则将 URL 发送到 Fetcher Client 阶段。
Fetcher Client 阶段使用客户端库与 Fetchers 进行通信。客户端库实现了确定哪个 Fetcher 将获取给定 URL 的逻辑;它还处理重定向跳转的处理。(因为 Twitter 上发布的 URL 通常是缩短的,所以通常会有一系列重定向。)与通过 Scheduler 流动的每个 URL 相关联的上下文对象。Fetcher Client 将所有获取信息(包括状态、下载的标头和内容)添加到上下文对象中,并将其传递给 Post Processor。Post Processor 将提取的页面内容通过元数据提取库运行,该库检测页面编码并使用开源 HTML5 解析器解析页面。提取库实现了一组启发式规则来检索页面元数据,如标题、描述和代表图像。然后,Post Processor 将所有元数据和获取信息写入 Metadata Store。如果需要,Post Processor 还可以安排一组依赖获取。依赖获取的一个示例是嵌入式媒体,如图像。
在后处理完成后,URL 上下文对象被转发到下一个阶段,该阶段记录所有信息,包括完整内容,使用名为 Scribe 的开源日志聚合器将其存储到内容存储(HDFS)中。该阶段还通知感兴趣的监听器 URL 处理已完成。通知使用简单的发布-订阅模型,使用 Kestrel 的扇出队列来实现。
所有处理步骤都是异步执行的 - 没有线程会等待步骤完成。与每个正在处理的 URL 相关的所有状态都存储在与之关联的上下文对象中,这使得线程模型非常简单。异步实现还受益于 Finagle 和 Twitter Util libraries 提供的便捷抽象和构造。
URL获取器
让我们看看 Fetcher 如何处理 URL。
Fetcher 通过其 Thrift 接口接收 URL。经过基本验证后,Thrift 处理程序将 URL 传递给请求队列管理器,后者将其分配给适当的请求队列。定时任务以固定速率处理每个请求队列。一旦 URL 从其队列中取出,它将被发送到 HTTP 服务进行处理。HTTP 服务建立在 Finagle 之上,首先检查与 URL 关联的主机是否已存在于其缓存中。如果不存在,它将为其创建一个 Finagle 客户端并安排 robots.txt 获取。下载 robots.txt 后,HTTP 服务获取允许的 URL。robots.txt 文件本身被缓存在进程内主机缓存以及 Memcached 中,以防止每次 Fetcher 遇到来自该主机的新 URL 时重新获取。
Vultures定期检查请求队列和主机缓存,以查找一段时间未被使用的队列和主机;一旦找到,它们将被删除。Vultures还通过日志和Twitter Commons统计导出库报告有用的统计信息。
Fetcher's Request Queue 有一个重要作用:速率限制。SpiderDuck 对每个域名的传出 HTTP 获取请求进行速率限制,以避免超载接收请求的 Web 服务器。为了准确的速率限制,SpiderDuck 确保每个请求队列在任何时间点都只分配给一个 Fetcher,如果分配的 Fetcher 失败,则自动切换到另一个 Fetcher。一个名为 Pacemaker 的集群套件分配请求队列给 Fetchers 并管理故障转移。基于域名,Fetcher 客户端库将 URL 分配给请求队列。可以根据需要在每个域名上覆盖所有网站使用的默认速率限制。Fetchers 还实现了队列退避逻辑。也就是说,如果 URL 的到达速度快于它们被处理的速度,它们会拒绝请求,以指示客户端退避或采取其他适当的操作。
出于安全考虑,Fetchers 部署在 Twitter 数据中心的一个特殊区域,称为 DMZ. 这意味着 Fetchers 无法访问 Twitter 的生产集群和服务。因此,保持它们轻量且自包含变得更加重要,这一原则指导了设计的许多方面。
Twitter 如何使用 SpiderDuck
Twitter 服务以多种方式使用 SpiderDuck 数据。大多数直接查询 Metadata Store 以检索 URL 元数据(例如,页面标题)和解析信息(即,在重定向后的规范 URL)。Metadata Store 在实时填充,通常在 URL 被推文后几秒钟内。这些服务不直接与 Cassandra 通信,而是与代理请求的 SpiderDuck Thrift 服务器通信。这个中间层为 SpiderDuck 提供了灵活性,可以在必要时透明地切换存储系统。它还支持比服务直接与 Cassandra 交互时可能实现的更高级别 API 抽象的途径。
其他服务定期处理HDFS中的SpiderDuck日志,以生成Twitter内部指标仪表板的聚合统计数据,或进行其他类型的批量分析。这些仪表板帮助我们回答诸如“Twitter每天分享多少张图片?”、“Twitter用户最常链接到哪些新闻网站?”以及“昨天我们从这个特定网站获取了多少个URL?”等问题。
请注意,服务通常不会告诉SpiderDuck要获取什么内容;SpiderDuck会从传入的推文中获取所有的URL。相反,服务在URL可用后会查询与其相关的信息。SpiderDuck还允许服务直接向获取器发出请求,通过HTTP获取任意内容(从而受益于我们的数据中心设置、速率限制、robots.txt支持等),但这种用例并不常见。
性能数据
SpiderDuck 每秒处理数百个 URL。在 SpiderDuck 的抓取策略定义的时间窗口内,大多数 URL 都是唯一的,因此会被抓取。对于被抓取的 URL,SpiderDuck 的中位数处理延迟不到两秒,第 99 百分位处理延迟不到五秒。这种延迟是从推文创建时间开始计算的,这意味着用户点击“推文”后不到五秒,推文中的 URL 就被提取出来,准备好进行抓取,所有重定向跳转都被检索,内容被下载和解析,元数据被提取并通过元数据存储库提供给客户端。大部分时间要么花在抓取请求队列中(由于速率限制),要么实际从外部 Web 服务器抓取。SpiderDuck 本身的处理开销不超过几百毫秒,其中大部分时间花在 HTML 解析上。
SpiderDuck的基于Cassandra的元数据存储每秒处理近10,000个请求。每个请求通常是针对单个URL或小批量(大约20个URL),但它也处理大批量请求(200-300个URL)。该存储的读取中位延迟为4-5毫秒,第99百分位延迟为50-60毫秒。
致谢
SpiderDuck核心团队由以下成员组成:Abhi Khune、Michael Busch、Paul Burstein、Raghavendra Prabhu、Tian Wang和Yi Zhuang。此外,我们还要感谢跨公司多个团队的以下成员,他们直接为项目做出了贡献,或者通过帮助SpiderDuck依赖的组件(例如Cassandra、Finagle、Pacemaker和Scribe)或其独特的数据中心设置:Alan Liang、Brady Catherman、Chris Goffinet、Dmitriy Ryaboy、Gilad Mishne、John Corwin、John Sirois、Jonathan Boulle、Jonathan Reichhold、Marius Eriksen、Nick Kallen、Ryan King、Samuel Luckenbill、Steve Jiang、Stu Hood和Travis Crawford。还要感谢整个Twitter搜索团队提供宝贵的设计反馈和支持。如果您想参与类似项目,请加入我们!
- Raghavendra Prabhu (@omrvp), 软件工程师