PowerData胥朝辉 PowerData

本文由PowerData钻石王老五贡献 姓名:胥朝辉 花名:钻石王老五 微信:wxid\_i8mczvku170k22 年龄:00后 工作经验:0年 工作内容:学生 自我介绍:我是一名来自武汉的大学生,对实时计算和数据仓库比较感兴趣 希望加入社区和大家探讨技术问题和交流前沿解决方案!


全文共 13898 个字,建议阅读 38 分钟

Feacebook的实时数据处理

摘要

实时计算为Facebook的许多服务提供支持,包括Feacebook用户的匿名数据报告、移动应用的数据分析以及web页面的数据处理。Feacebook有一个实时数据处理生态系统,每秒可以在数百个数据管道中同时处理数百个GB的数据。

在设计实时流处理系统时,必须做出许多决定,在本文中,我们确定了几个重要的决策,这些决策会影响系统的易用性、性能、容错性、可扩展性和正确性,我们比较了几个备选方案,并将Feacebook建立的系统和其它已经发布的系统进行了对比。

我们主要决定是针对秒级延迟而非毫秒级。对于所有的服务秒都足够快,我们允许并支持我们使用持久消息队列进行数据传输。这种数据传输方案为我们的流处理系统Puma、Swift和Stylus的容错、可扩展性和正确性的多种选择铺平了道路。

1.简介

实时数据处理系统被广泛用于洞察事件的发生。许多公司已经开发了自己的系统:Twitter的Storm和Heron,google的Millwheel和Linkedln的Samza。我们在此介绍Facebook的Puma,Swift,和Stylus流处理系统。

以下特性在实时数据系统的设计中都很重要:

  • 易用性:处理要求多复杂,SQL是否足够,是否需要用通用程序语言(如C++或Java),用户编写、测试和部署一个新的应用程序的速度如何。
  • 可用性:延迟大小、吞吐量、集群总量
  • 容错性:容忍的故障类型,对于数据被处理或输出的次数有什么语义保障,系统是如何存储和恢复记忆中的状态
  • 扩展性:数据能否被切片和重新分片,能否通过切片和重分片来并行处理数据,系统如何适应容量变化,能否重新处理旧数据
  • 正确性:是否需要ACID保证,是否所有数据都要被处理并输出

在本文中我们介绍了在设计实时流处理系统时必须做出的一些决定。我们比较了替代方案,确定了文献中不同系统的选择,并讨论了选择的依据。

我们主要决定是,秒级延迟(每秒数百GB的吞吐量)符合我们的性能要求。因此我们可以用一个持久消息总线(persistent message bus)来连接我们系统的所有处理组件进行数据传输,以便数据传输与处理解耦,使我们能够实现容错性、可扩展性和易用性。

我们在生产中运行数百条实时数据管道。目前主要的四种类型生产数据管道说明了流媒体系统在Facebook的使用情况。

  • Chorus:是一个用于聚合Feacebook上的匿名数据的管道:如当日讨论最多的五个话题、世界杯球迷的统计(年龄,性别,国家)
  • 移动分析管道(Mobile analytics piplines):为Facebook移动应用程序开发人员提供实时反馈,他们使用这些数据诊断性能和可用性问题:如冷启动时间和崩溃率。
  • 页面洞察管道:从Facebook页面收集信息:如每个页面帖子的喜欢、覆盖率和参与度的实时信息
  • 实时流媒体管道:从我们的交互式数据存储中卸载了 CPU 密集型Dashboard queries以节省全局 CPU 资源。

💡 Figure 1:实时数据处理所涉及的系统概述:从左边的移动平台和web的发送到记录到中间的Scribe和实时流处理器,再到用于分析的数据存储集群。

我们回顾了在过去四年中我们建立和重建这些系统时学到地教训。其中一个教训是强调使用的方便性:不仅仅是编写应用程序的方便性,还包括测试、调试、部署以及最后监控数百个生产中的应用程序。

2.系统概述

Facebook 有多个系统参与实时数据处理。我们在本节中概述了我们的生态系统。

Figure 1说明了数据通过我们的系统的情况。在左边,数据来自于移动和网络产品。它们记录的数据被送入Scribe,它是一个分布式数据传输系统。所有的实心(黄色)箭头代表通过Scribe的数据。

实时流处理系统Puma、Stylus和Swift从Scribe读取数据,也向Scribe写入数据。根据需要,Puma、Stylus和Swift应用程序可以通过Scribe连接成一个复杂的DAG(有向无环图)。

在右边,Laser、Scuba和Hive是从Scribe进行读取的数据存储,为不同类型的查询提供服务。Laser也可以为产品和流媒体系统提供数据,如虚线(蓝色)箭头所示。

2.1 Scribe

Scribe是一个持久的、分布式的信息传递系统,用于收集、聚合和传递大量的日志数据,其延迟只有几秒钟,吞吐量却很高。Scribe是向Facebook的批量和实时系统发送数据的传输管道。在Scribe中,数据是按类别组织的。一个类别是一个独特的数据流:所有数据都被写入或从一个特定的类别中读取。通常情况下,一个流式应用会消耗一个Scribe类别作为输入。一个Scribe类别有多个桶。一个Scribe桶是流处理系统的基本处理单元。流处理系统的基本处理单元:应用程序通过向不同的进程发送不同的Scribe桶来并行化。Scribe通过将数据存储在HDFS中,提供了数据的耐久性。Scribe消息被存储起来,在数天内流可以由相同或不同的接收者重放。

2.2 Puma

Puma是一个流处理系统,其应用程序是用类似于SQL的语言编写的,其UDFs(用户自定义函数)是用Java编写的。Puma应用程序的编写速度很快:编写、测试和部署一个新的应用程序可能需要不到一个小时。

Puma应用程序有两个目的:

首先,Puma为简单的聚合查询提供预先计算的查询结果。对于这些有状态的单体应用,延迟等于查询结果的时间窗口大小。查询结果通过查询Puma应用获得.

💡 Figure 2:一个完整的Puma应用程序,可以计算出每个5分钟时间窗口的 "top N"。这个应用程序可用于Fig 3中的排名器。

通过Thrift API。图2显示了一个简单的Puma聚合应用的代码,有5分钟的时间窗口。

第二,Puma提供对Scribe流的过滤和处理(有几秒钟的延迟)。例如,一个Puma应用程序可以将所有Facebook行动的流减少到只有帖子,或者只有与某个谓词相匹配的帖子,例如包含标签/#superbowl"。这些无状态的Puma应用程序的输出是另一个Scribe流,然后它可以成为另一个Puma应用程序或任何其他实时流处理器或数据存储的输入。

与传统的关系型数据库不同,Puma是为编译查询而优化的,而不是为特定的分析而优化。工程师在部署应用程序时,希望它们能运行几个月或几年。这种期望允许Puma生成一个高效的查询计算和存储计划。Puma 聚合应用在共享的HBase集群中存储状态。

2.3 Swift

Swift是一个基本的流处理引擎,为Scribe提供检查点功能。它提供了一个非常简单的API:你可以从Scribe流中每隔N个字符串或B个字节就读一个检查点。如果应用程序崩溃了,你可以从最新的检查点重新开始;因此,所有数据都至少从Scribe读取一次。Swift通过系统级管道与客户端应用程序进行通信。因此,系统的性能和容错由客户端决定。Swift主要适用于低吞吐量、无状态处理。大多数Swift客户端应用程序是用Python等脚本语言编写的。

2.4 Stylus

Stylus是一个用C++编写的低层次流处理框架。Stylus的基本组件是一个流处理器。处理器的输入是一个Scribe流,输出可以是另一个Scribe流或一个用于提供数据的数据存储。Stylus处理器可以是无状态或有状态的。处理器可以被组合成一个复杂的处理DAG。我们在下一节的图3中介绍了这样一个DAG实例。

Stylus的处理API与其他程序流处理系统相似。与它们一样,Stylus必须处理其输入流中的不完美排序。因此,Stylus要求应用程序编写者识别流中的事件时间数据。作为回报,Stylus提供了一个函数来估计事件时间的低水位和给定的置信区间。

2.5 Laser

Laser是一个建立在RocksDB之上的高查询吞吐量、低(毫秒)时效的键值存储服务。Laser可以实时地从任何Scribe类别中读取,或者每天从任何Hive表中读取一次。键和值可以是(序列化的)输入流中的任何列的组合。然后,存储在Laser中的数据可以被Facebook产品代码以及Puma和Stylus应用程序访问。

Laser有两种常见的使用情况。Laser可以使Puma或Stylus应用程序的输出Scribe流可用于Facebook产品。Laser还可以将复杂的Hive查询或Scribe流的结果提供给Puma或Stylus应用,通常用于查找连接,例如确定某个标签的主题。

2.6 Scuba

Scuba是Facebook的快速切片分析数据存储(fast slice-and-dice analysis data store),最常用于发生问题时的故障排除。Scuba每秒钟将数百万条新行摄入数千张表中。数据通常从产品通过Scribe进入Scuba,延迟时间不到1分钟。Scuba还可以摄取任何Puma、Stylus或Swift应用程序的输出。

Scuba提供临时查询,大多数响应时间在1秒以内。Scuba用户界面以各种可视化格式显示查询结果,包括表格、时间序列、柱状图和世界地图。

2.7 Hive data warehouse

Hive是Facebook的EB级数据仓库。Facebook每天产生多个新的PB级数据,其中大约一半是从Scribe摄取的原始事件数据(另一半数据来自原始数据,例如,通过日常查询管道)。Hive中的大多数事件表都是按天划分的:每个分区都是在一天的午夜结束后才可用。对这些数据的任何实时处理都必须发生在Puma、Stylus或Swift应用程序中。Presto为存储在Hive中的数据提供完整的ANSI SQL查询。查询结果每天只改变一次,因为新数据被加载。然后,它们可以被发送到Laser,供产品和实时流处理器访问。

3. 应用实例

我们使用Figure 3中的应用实例来展示下一节中的设计选择。这个应用程序在输入的事件流中识别趋势事件。事件包含一个事件类型,一个维度ID(用于获取关于事件的维度信息,如事件的语言)和文本(用于分析事件主题的分类,如电影或电影)。该应用程序的输出是每个5分钟时间桶的主题排名列表(按事件计数排序)。

💡 图Figure 3:一个有4个节点的流媒体应用实例:这个应用计算的是 "趋势 "事件。

  1. Filterer(过滤器)根据事件类型对输入流进行分流,然后根据维度ID对其输出进行分流,这样下一个节点的处理就可以在维度ID不相连的分流上并行进行。
  2. Joiner查询一个或多个外部系统,(a)根据维度ID检索信息,(b)根据事件的文本,按主题进行分类。由于每个Joiner进程接收分片输入,它更有可能在缓存中获得它所需要的维度信息,这就减少了对外部服务的网络调用。然后,输出被按(事件;主题)对重新分片,以便Scorer可以并行地聚合它们。
  3. Scorer(计数器)保留了一个最近历史上每个主题的事件计数的滑动窗口。它还跟踪这些计数器的长期趋势。基于长期趋势和当前的计数,它为每个(事件,主题)对计算一个总和,并将结果作为其输出给排名器,按主题进行重新分流。
  4. Ranker(排名器)计算出每个主题在每N分钟时间桶中的前K个事件。

4. 设计决定

在本节中,我们提出了一些设计决定。这些决定在Table 4中进行了总结,其中我们显示了每个决定所影响的能力。对于每一个决定,我们都对备选方案进行分类,并解释它们是如何影响相关的能力的。然后,我们讨论了我们的决定对Facebook系统的利弊。

Table 5总结了在Facebook和相关文献中,各种实时系统选择了哪些替代方案。

4.1 语言范式

第一个设计决定是人们在系统中编写应用程序所使用的语言类型。这个决定决定了编写应用程序的难易程度以及应用程序编写者对其性能的控制程度。

4.1.1 选择

有三种常见的选择:

  • 声明性的:SQL是陈述性的而且是最简单和最快的编写方式。很多人已经知道SQL,所以他们的上升速度很快。然而,SQL的缺点是它的表达能力有限。许多系统在SQL中加入了诸如散列和字符串运算符等操作的函数。例如,Streambase、S-Store和STREAM提供了基于SQL的流处理。
  • 函数式:函数式编程模型将一个应用程序表示为一连串预先设定好的运算符。编写应用程序仍然很简单,但用户对操作顺序有更多的控制,而且通常有更多的操作可以使用。
  • 程序性的:C++、Java和Python都是常见的程序性语言。它们具有最强的执行力和(通常)最高的性能。应用程序编写者可以完全控制数据结构和执行。然而,它们也需要花费最多的时间来编写和测试,并且需要最多的语言专业知识。S4、Storm、Heron和Samza处理器都是过程性流处理系统的例子。

4.1.2  Facebook的语言

在Facebook的环境中,没有一种语言可以满足所有的使用情况。需要不同的语言(以及它们所提供的不同程度的易用性和性能)是我们有三个不同的流处理系统的主要原因。

Swift应用程序大多使用Python。它很容易原形化,对于低吞吐量(每秒几十兆字节)的流处理应用程序非常有用。尽管用Swift写一个高性能的处理器是可能的,但它需要大量的时间。

Stylus应用程序是用C++编写的,一个Stylus 处理需要多个类。虽然脚本会生成模板代码,但编写一个应用程序仍然需要几天时间。Stylus应用程序对复杂的流处理应用具有最大的灵活性

💡 Figure 4:每个设计决策都会影响一些数据质量属性。

💡 Figure 5:不同的流媒体系统做出的设计决定。

4.2 数据传输

一个典型的流处理应用是由多个处理节点组成的,以DAG的形式排列。第二个设计决定是在处理节点之间传输数据的机制。这个决定对流处理系统的容错性、性能和可扩展性有很大的影响。它还会影响到系统的易用性,特别是在调试方面。

4.2.1 选择

数据传输的典型选择包括:

  • 直接消息传输:通常,RPC或内存中的消息队列被用来将数据从一个进程直接传递给另一个进程。例如,MillWheel、Flink和Spark Streaming使用RPC,Storm使用ZeroMQ,这是消息队列的一种形式。这种方法的优势之一是速度:可以实现几十毫秒的端到端延迟。
  • 基于中间人的消息传输:在这种情况下,有一个单独的中介进程连接流处理节点,并在它们之间转发消息。使用一个中介进程会增加开销,但也允许系统更好地扩展。中间人可以将一个给定的输入流复用到多个输出处理器上。当输出处理器落后时,它还可以对输入处理器施加反压力。Heron在Heron实例之间使用一个流管理器来解决这两个问题。
  • 基于持久性存储的消息传输:在这种情况下,处理器通过一个持久的信息总线连接起来。一个处理器的输出流被写入一个持久的存储,下一个处理器从该存储中读取其输入。除了多路复用,持久性存储允许输入和输出处理器在不同的时间点以不同的速度写入和读取,并多次读取相同的数据,例如,从处理器故障中恢复。处理节点之间是相互解耦的,所以一个节点的故障不会影响到其他节点。Samza使用Kafka,一个持久性存储,来连接处理节点。

所有这三种类型的数据传输机制都是连接DAG中的连续节点。

连续节点之间有两种类型的连接。狭窄的依存关系连接了从发送节点到接收节点的固定数量的分区。这种连接通常是一对一的,其节点可以被折叠。广义的依赖性连接将发送方的每个分区与再接收方的每个分区联系起来。这些连接必须用一个数据传输机制来实现。

4.2.2 Facebook中的数据传输

在Facebook,我们使用Scribe,一个持久的消息总线,来连接处理节点。使用Scribe使每个流的最小延迟为一秒钟。在Facebook,实时流处理的典型要求是秒。

Scribe的第二个限制是它会写到磁盘。在实践中,写是异步的(不是阻塞的),读来自于缓存,因为流式应用会读取最近的数据。最后,一个持久性存储需要额外的硬件和网络带宽。

接受这些限制使我们在容错性、易用性、可扩展性和性能方面具有多种优势。

💡 Figure 6:这个计数器节点处理器从一个(时间戳;事件)输入流中对事件进行计数。每隔几秒钟,它就向一个(timewindow;counter)输出流发出计数器的值。

  • 容错性:当我们部署成千上万的作业来处理流时,流处理节点故障的独立性是一个非常理想的属性。
  • 容错性:从故障中恢复的速度更快,因为我们只需要更换发生故障的节点。
  • 容错性:自动复用允许我们运行重复的下游节点。例如,我们可以运行多个Scuba或Laser层,每个层都读取其所有输入流的数据,这样我们就有了用于灾难恢复的冗余度。
  • 性能:如果一个处理节点速度慢(或死亡),前一个节点的速度就不会受到影响。例如,如果一台机器被过多的作业所占用,我们只需将一些作业转移到新的机器上,它们就会从它们离开的地方继续处理输入流。在一个紧密耦合的系统中,back pressure在上游传播,峰值处理量由DAG中最慢的节点决定。
  • 易用性:调试更容易。当观察到某一处理节点出现问题时,我们可以通过从一个新的节点读取相同的输入流来重新产生这个问题。
  • 易用性:监测和警报的实现比较简单。每个节点的主要责任是消耗其输入。在处理来自持久性存储的数据流时,设置监控和警报是足够的。
  • 易用性:我们在如何编写每个应用程序方面有更多的灵活性。我们可以在同一个DAG中连接任何读取或写入数据的系统的组件。我们可以将Puma应用的输出作为Stylus处理器的输入,然后将Stylus的输出作为输入读取到我们的数据存储Scuba或Hive。
  • 可扩展性:我们可以通过改变配置中每个Scribe类别的桶的数量,轻松地扩大或减少分区的数量。
back pressure:指的是系统为“推回”下行力量采取的行动。也就是系统在受到胁迫,或在总调用模式表现出过多峰值,或过于突发时,单方面采取的一种防御性行动,如处理速度比摄取速度慢

鉴于上述优势,Scribe作为Facebook的数据传输机制运行良好。Kafka或其他持久性存储会有类似的优势。我们使用Scribe是因为我们在Facebook开发它。

4.3 处理语义

每个节点的处理语义决定了它的正确性和容错性。

💡 Figure 7:具有不同状态语义的有状态处理器的输出。

4.3.1 选择

一个流处理器做三种类型的活动:

  1. 处理输入事件:例如,它可以对输入事件进行反序列化,查询一个外部系统,并更新其内存状态。这些活动可以重新运行,没有副作用。
  2. 产生输出:基于输入事件和内存状态,它为下游系统生成输出,以便进一步处理或服务。这可以随着输入事件的处理而发生,也可以在检查点之前或之后同步进行。
  3. 将检查点保存到数据库:用于故障恢复。可以保存三个独立的项目:
  4. 处理节点的内存中状态。
  5. 输入流中的当前偏移量。
  6. 输出值。

不是所有的处理器都会保存所有这些项目。保存什么以及何时保存决定了处理器的语义。

这些活动的实现,特别是检查点,控制着处理器的语义。有两种相关的语义:

  • 状态语义:每个输入事件可以至少算一次,最多算一次,还是完全算一次?
  • 输出语义:一个给定的输出值可以在输出流中出现至少一次、最多一次,还是完全一次?

无状态处理器只具有输出语义。有状态处理器有两种。

不同的状态语义只取决于保存偏移量和内存中的状态。

  • 至少一次的状态语义。首先保存内存中的状态,然后保存偏移量。
  • 最多只有一次的状态语义。先保存偏移量,再保存内存中的状态。
  • 完全一次的状态语义。原子地保存内存状态和偏移量,例如在一个事务中。

💡 Figure 8:状态和输出处理语义的常见组合。

输出语义取决于在检查点中保存输出值,此外还有内存状态和偏移量。

  • 至少一次的输出语义:向输出流发出输出,然后保存一个偏移量和内存状态的检查点。
  • 最多只有一次的输出语义:保存一个偏移量和内存状态的检查点,然后发出输出。
  • 完全一次输出语义:保存偏移量和内存状态的检查点,并在同一事务中原子式地发出输出值。

Figure 6显示了一个有状态的流处理节点,即计数器节点(Counter Node),它对其输入的事件进行计数并定期输出计数。我们用计数器节点来说明具有至少一次输出语义的不同的状态处理语义。Figure 7显示了不同的语义如何影响机器或处理器故障后可能的计数器输出值。

最少一次性输出语义允许计数器节点在接收事件时发出输出。对于需要低延迟处理并能处理少量输入数据的系统来说,最少一次的输出语义是可取的。

为了实现最多一次的输出语义,计数器节点必须在生成输出之前保存其检查点。如果处理器可以对事件A1-A4进行无副作用的处理,然后保存检查点A,然后生成输出,它就不需要在保存检查点A之前对事件A1-A4进行处理。这种优化也减少了丢失数据的机会,因为只有在检查点和输出之间发生的故障会导致数据丢失。我们在第4.3.2节中说明了在检查点之间进行无副作用处理的性能优势。Photon也提供了减少数据丢失的选项,即最多一次输出语义。当数据丢失比数据重复更可取时,最多一次的状态和输出语义是可取的。

为了获得完全一次性的输出语义,计数器节点必须在处理完事件A1-A4后保存检查点,但要与输出的原子化。一次性输出语义要求输出的接收方提供交易支持。在实践中,这意味着接收器必须是一个数据存储,而不是像Scribe那样的数据传输机制。由于处理器需要等待交易行为的完成,因此完全一次性语义通常会造成性能上的损失。

表8显示了状态和输出语义的常见组合。

💡 Figure 9:该程序的Stylus实现在检查点之间进行了无副作用的处理,实现的吞吐量几乎是Swift实现的4倍。

4.3.2 在Facebook使用的处理语义

在Facebook的环境中,不同的应用往往有不同的状态和输出语义要求。我们举几个不同的例子。

在Figure 3的趋势事件例子中,Ranker将其结果发送到一个闲置服务系统。发送两次输出并不是一个问题。因此,我们可以使用至少一次的状态和输出语义。

Scuba的数据摄取管道是无状态的。只有输出语义适用。发送给Scuba的大部分数据都是抽样的,Scuba是一个最好的查询系统,这意味着查询结果可能是基于部分数据。因此,相对于任何数据的重复,少量的数据损失是首选。因为Scuba不支持交易,所以不可能有完全一次的语义,所以最多一次的输出语义是最佳选择。

事实上,我们的大多数分析数据存储,包括Laser、Scuba和Hive,都不支持事务。我们需要使用其他的数据存储来获得交易和完全一次性的状态语义。

当下游数据存储是一个分布式数据库,如HBase或ZippyDB时,获得完全一次的状态语义也是一个挑战。(ZippyDB是Facebook的分布式键值存储,具有Paxos风格的复制,建立在RocksDB之上)。状态必须被保存到多个分片上,需要一个高延迟的分布式交易。为了避免产生这种延迟,大多数用户选择最多一次或最少一次的语义。

Puma通过对HBase的检查点保证了一次性的状态和输出语义。Stylus为其应用程序编写者提供了图8中的所有选项。

现在我们来看看将无副作用的处理与接收输入的最多一次输出语义重叠的好处。图9显示了Scuba数据摄取处理器的两种不同实现的吞吐量。两种实现都有最多一次输出语义。Swift实现在检查点之间构建所有的输入事件,检查点大约每2秒出现一次。然后它处理这些事件并将其输出发送到Scuba服务器。当它在等待检查点的时候,处理器的CPU是没有被充分利用的。

Stylus实现在检查点之间尽可能多地进行无副作用的工作,包括对其输入事件进行反序列化。由于反序列化是性能瓶颈,Stylus处理器实现了更高的CPU利用率。因此,Stylus处理器的吞吐量几乎是Swift处理器的四倍:在图9中,我们看到的是135 MB/s与35 MB/s的对比

一般来说,如果开发者对处理语义有很好的理解,可以在任何处理器的自定义代码中实现分离出无副作用的处理并在检查点之间进行处理。Stylus开箱即提供这种优化

4.4 状态保存机制

有状态处理器的状态保存机制决定了其容错性。例如,在图3中,Scorer同时维护着事件的长期计数器和短期计数器,以便计算出趋势得分。在机器发生故障后,我们需要恢复计数器的值。

4.4.1 选择

有多种方法可以在处理过程中保存状态,并在失败后恢复状态。这些解决方案包括:

  • 复制。在基于复制的方法中,有状态的节点被复制了两个或更多的副本。这种方法需要两倍的硬件,因为许多节点被复制了。
  • 本地数据库持久化。Samza将状态存储到本地数据库,同时将突变写入Kafka。失败后,本地状态会从Kafka日志中恢复出来。日志是经过压缩的,以保持日志大小的约束。由于Kafka不支持事务,Samza可以支持至少一次,但不支持完全一次的状态和输出模式
  • 远程数据库持久化。在这种方法中,检查点和状态被持久化到一个远程数据库中。MillWheel将检查点保存到一个远程数据库中,并支持完全一次性状态和输出语义。
  • 上游备份。在这些系统中,事件被保存在上游节点中,并在故障后重新播放。
  • 全局一致的快照。Flink使用分布式快照算法来维护全局一致的快照。在发生故障后,必须将多台机器恢复到一致状态。

4.4.2 Facebook中的状态保存机制

在Facebook,我们对流处理系统的容错有不同的要求。Puma为有状态聚合提供容错。Stylus为有状态处理提供了多种开箱即用的容错解决方案。

将状态保存到本地的RocksDB数据库对Facebook的许多用户来说是有吸引力的。它很容易设置。当使用闪存或基于内存的文件系统(如tmpfs)时,本地数据库的写入速度很快,因为没有网络成本。

💡 Figure 10:使用RocksDB本地数据库和HDFS进行远程备份保存状态。

它还支持有状态处理的完全一次性状态和输出语义。

Figure 3中的Scorer节点是将其状态保存到本地数据库的一个很好的候选者:整体的状态很小,将进入一台机器的闪存或磁盘。

Figure 10展示了RocksDB嵌入到与流处理器相同的程序中以及它的状态。内存中的状态以固定的时间间隔被保存到这个本地数据库中。然后使用RocksDB的备份引擎,以更大的间隔将本地数据库异步复制到HDFS。当一个进程崩溃并在同一台机器上重新启动时,本地数据库被用来恢复状态并从检查点恢复处理。如果机器死亡,则使用HDFS上的副本来代替。

HDFS是为批处理工作负载而设计的,并不是要成为一个永远可用的系统。如果HDFS不能被写入,在没有远程备份副本的情况下继续处理。如果出现了故障,那么恢复就会使用较早的快照。我们可以在恢复时从HDFS中并行读取,这样HDFS的读取带宽就不会成为瓶颈。

💡 Figure 11:图11:使用远程数据库保存状态。

一个远程数据库可以保存不在内存中的状态。远程数据库解决方案还提供了更快的机器故障切换时间,因为我们不需要在重新启动时将完整的状态加载到机器上。通常情况下,分布式数据库可以作为流处理系统输出的存储空间。同样的远程数据库也可以用来保存状态。

Figure 11说明了在远程数据库中保存状态。当从输入流中收到一个事件时,状态被更新。如果需要的状态不在内存中,就从远程数据库中读取,修改,然后再保存到数据库中。如果状态被限制在单体处理器中,这种读-修改-写的模式可以被优化。

向量[1, 14]是一种代数结构,它有一个身份元素,并且是关联的。当单体处理器的应用需要访问不在内存中的状态时,对空的状态(身份元素)进行修改。定期地,现有的数据库状态被加载到内存中,与内存中的部分状态合并,然后异步地写出到数据库中。这种读-合并-写的模式可以比读-修改-写的模式更少地进行。

当远程数据库支持自定义合并操作时,合并操作可以在数据库中进行。读-修改-写的模式被优化为只附加的模式,从而提高性能。

💡 Figure 12:保存状态:读-修改-写与只附加的远程数据库写入吞吐量对比。

RocksDB和ZippyDB支持自定义合并运算符。我们在Figure 12中说明了性能的提高,图中比较了同一Stylus流处理器应用程序的吞吐量,其中包含和不包含仅附加的优化。该应用将其输入事件聚集在许多方面,这意味着一个输入事件会改变应用状态中的许多不同的值。远程数据库是一个三台机器的ZippyDB集群。由于不同的应用程序可能对远程保存状态的频率有不同的要求,我们改变了向远程数据库提交数据的间隔时间。图12显示,使用仅附加的优化方法,应用程序的吞吐量高出25%到200%。

Puma中的聚合函数都是单数。在Stylus中,一个用户应用程序声明它是一个单数流处理器。应用程序将部分状态附加到框架中,Stylus决定何时将部分状态合并为一个完整的状态。这种何时合并的灵活性以及由此带来的性能提升是我们的远程数据库方法与Millwheel的主要区别。

4.5 回顾处理

我们经常需要重新处理旧的数据,有几个原因。

  • 当用户开发一个新的流处理应用程序时,最好用旧的数据来测试该应用程序。例如,在Figure 3中,在一个已知的事件流上运行趋势算法,看看它是否能识别出预期的趋势事件,这是非常有用的。
  • 对于许多应用来说,当我们添加一个新的指标时,我们希望对照旧的数据运行它,以生成历史指标数据。
  • 当我们发现一个记录或处理错误时,我们需要重新处理有该错误的时期的数据。

4.5.1 选择

有几种方法可以对数据进行再处理:

  • 只有流:在这种方法中,数据传输机制的保留时间必须足够长,以重新播放输入流进行再处理。
  • 维护两个独立的系统:一个用于批处理,一个用于流处理。例如,为了引导Figure 3中的长期计数器,我们可以开发一个单独的批处理管道来计算它们。这种方法很有挑战性:很难在两个不同的系统之间保持一致性。Summingbird使用一个高水平的DSL将一个处理器规格自动转换到每个系统,但仍然需要在两个不同的处理系统实现之间保持一致性。
  • 开发也能在批处理环境中运行的流处理系统。Spark Streaming和Flink是这种方法的好例子,这也是我们采取的方法。

4.5.2 在Facebook重新处理数据

我们让Scribe在短时间内存储数据。对于长时间的保留,我们将输入和输出流存储在我们的数据仓库Hive中。我们的方法与Spark Streaming和Flink是不同的。它们的批处理和流处理都使用类似的容错和数据传输机制。

为了重新处理旧数据,我们使用标准的MapReduce框架从Hive读取数据,并在我们的批处理环境中运行流处理应用程序。Puma应用程序可以作为Hive UDFs(用户自定义函数)和UDAFs(用户自定义聚合函数)在Hive的环境中运行。无论在流式数据还是在批处理数据上运行,Puma应用程序的代码都不会改变。

Stylus提供三种类型的处理器:无状态处理器、一般有状态处理器和单体流处理器。当用户创建一个Stylus应用程序时,会同时生成两个二进制文件:一个用于流,一个用于批处理。无状态处理器的批处理二进制文件作为自定义映射器在Hive中运行。一般有状态处理器的批处理二进制文件作为一个自定义的还原器运行,还原键是聚合键和事件时间戳。单体处理器的批处理二进制可以被优化为在映射阶段做部分聚合。

5. FACEBOOK应用

在这一节中,我们描述了在Facebook生产中使用的几个不同的实时应用。我们展示了这些应用如何使用现有的不同组件来实现其特定目标。

5.1 Chorus

Chorus数据管道将单个Facebook帖子流转化为聚合的、匿名的、有注释的视觉摘要(不透露任何私人信息)。这个管道使Facebook的通信和洞察力团队能够向公众报告重新发生的对话,而不需要知道底层数据存储或查询。新的帖子在几秒钟内就会出现在查询结果中:例如,在2015年的超级碗比赛中,我们看到在电视广告之后的2分钟内,含有”#likeagirl "的帖子出现了巨大的峰值。

这条管道有许多步骤。我们想提请注意两件事。

首先,这个管道包含了Puma和Stylus应用程序的混合,在Laser中进行查找连接,并将Hive和Scuba作为结果的汇数据存储。所有的数据传输都是通过Scribe。数据流类似于Figure 3中的例子。

第二,这个管道在过去两年里有了很大的发展。最初的管道只有一个Puma应用程序,用于过滤帖子。laser连接是用定制的Python代码来执行连接的。后来,一个Stylus应用程序取代了自定义代码。在管道发展的每一步,我们可以一次增加或替换一个组件,并逐步进行测试和部署。

5.2 Dashboard queries

Dashboard在观察趋势和发现异常方面很受欢迎,一目了然。它们在一个滑动的时间窗口内重复运行相同的查询。一旦查询被嵌入到Dashboard中,汇总和指标就会被改变。流处理应用程序在数据到达时计算其查询结果。它们是Dashboard queries的理想选择。

Scuba是为交互式、切片式查询而设计的。它在查询时通过读取所有的原始事件数据进行聚合。当我们意识到来自Dashboard的查询消耗了很多Scuba的CPU时,我们建立了一个框架,将Dashboard 查询从Scuba转移到Puma。

这方面有几个挑战。

首先,Puma应用程序可以从Scribe读取Scuba的输入,但需要新的代码来编写Scuba的UDF,这些UDF是在Scuba的可视化层中定义的。

第二,Puma是为在其结果中有数百万时间序列的应用程序设计的。那些Puma应用程序按时间序列来分割他们的数据。大多数Scuba查询有一个7的限制:在一个图表中最多只有7条线的可视化才有意义。它们不能按时间序列分片到N>7个进程中:这些进程必须使用不同的分片并计算部分聚合。然后,一个进程将这些部分聚集结合起来。

第三,在将查询导出到Dashboard 时,人们会对不同的查询进行试验。我们需要检测死的Dashboard 查询,以避免浪费CPU。总的来说,迁移项目是非常成功的。Puma应用程序消耗的CPU大约是在Scuba中运行相同查询所需的14%。

一般来说,在读时处理(如Scuba)和写时处理(如流处理器Puma和Stylus)之间有一个权衡。读取时的处理更可行你不需要提前选择查询,但一般来说更耗费CPU。

5.3 混合实时-批处理管道

在Facebook的数据仓库Hive上的所有查询中,有一半以上是日常查询管道的一部分。这些管道可以在午夜后随时开始处理。由于依赖性,其中一些在12小时或更长时间后才完成。我们现在正在努力将这些管道中的一些早期查询转换为实时流应用,以便管道能够提前完成。

这些转换中有趣的挑战包括:

  • 验证实时管道的结果是正确的。正确的实时结果可能与批处理结果不完全相同。
  • 在Puma和Stylus中添加足够多的普通Hive UDFs,以支持大多数查询。
  • 使转换过程成为自助服务,这样团队就可以在没有Puma和Stylus团队的帮助下进行转换、测试和部署。

在多个案例中,我们已经将管道的速度提高了10到24小时。例如,我们能够将过去在下午2点左右完成的一部分管道转换为一组实时流处理应用程序,在凌晨1点之前在Hive中交付相同的数据。因此,这条管道的最终结果可以提前13个小时提供。

6. 经验教训

我们在建立我们的实时数据系统的生态系统时学到了很多东西,就像从比较它们和建立任何单一系统一样。下面的许多经验都是关于服务管理的:仅仅提供一个框架让用户编写应用程序是不够的。易用性也包括调试、部署和监控。使操作更容易的工具的价值被低估了。根据我们的经验,每当我们增加一个新的工具时,我们都会惊讶于没有它的情况下我们的管理。

6.1 多个系统让我们 "快速行动”

同样,进化我们自己的系统也很重要。Puma的每一个主要部分都至少被重写过一次。使用高级语言使我们可以在不需要改变Puma应用程序的情况下修改下面的实现。

最后,用Scribe流连接节点,不仅可以很容易地用更快或更强的节点来代替一个节点,而且还可以在另一个数据管道(DAG)中重新使用该节点的输出。通过将同一个处理器的输出用于不同的目的,我们已经能够节省开发人员的时间和系统资源。

6.2 调试的便利性

数据库通常存储数据并使用户能够对其运行查询。这样的环境非常有利于迭代的开发过程。用户可以对一个处理阶段进行编程并运行它。如果它没有产生正确的结果,他们可以修改处理逻辑并在相同的数据上重新运行。

当使用流处理系统时,在开发过程中更难迭代,因为数据没有被存储。当你更新一个流处理操作者时,它开始在新的流数据上运行,而不是之前的相同数据,所以结果可能是不一样的。此外,你还必须处理延迟的数据。有了持久的Scribe流,我们可以从给定的(最近的)时间段重放一个流,这使得调试变得更加容易。

6.3 易于部署

编写一个新的流媒体应用程序需要的不仅仅是编写应用程序代码。部署和维护应用程序的方便或麻烦也同样重要。

Laser和Puma应用程序是作为一种服务部署的。Stylus应用程序由编写它们的各个团队拥有,但我们提供了一个标准框架来监控它们。

Laser应用的设置、部署和删除都非常容易。有一个UI来设置应用:只要从输入的Scribe流中为每个键和值选择一组有序的列,为每个键值对选择一个寿命,以及一组数据中心来运行服务。然后,UI给你提供了一个部署应用的命令和另一个删除应用的命令。

Puma应用的部署和删除几乎和Laser应用一样简单,但需要第二个工程师:UI产生的代码必须被审查。在代码被接受并提交后,应用程序会自动部署或删除。

Puma应用程序并不总是这么容易部署的。最初,Puma团队的一名成员必须部署每个新的应用程序,这对Puma团队和编写应用程序的团队都造成了瓶颈。让Puma的部署成为自助服务,使我们能够扩展到今天使用Puma的数百条数据管道。

6.4 易于监测和操作

一旦应用程序被部署,我们需要对其进行监控。它是否使用了适量的并行性?对于Scribe,改变并行性通常只是改变Scribe桶的数量,并重新启动输出和消费Scribe类别的节点。然而,在部署前猜测正确的并行量是一门黑科技。我们可以通过轻松改变它来节省时间和机器资源;我们可以从一些初始水平开始,然后迅速适应。

然后,我们使用警报来检测一个应用程序处理其Scribe输入的速度比输入产生的速度慢。我们把这种情况称为 "处理滞后"(processing lag)。Puma团队为所有Puma应用程序运行处理滞后警报,因为Puma服务是由Puma团队维护的。Stylus为其应用程序开发人员提供了处理滞后的警报。这些警报通知我们,使我们的应用程序适应随时间变化的数量。

在未来,我们希望提供仪表板和警报,以自动监测使用Puma和Stylus应用程序的团队。我们还希望能自动扩展这些应用程序。

6.5 流处理和批处理

流处理和批处理并不是一个非此即彼的决定。最初,Facebook的所有数据仓库处理都是批处理。我们在大约五年前开始开发Puma和Swift。正如我们在第5.3节中所展示的,混合使用流式处理和批处理可以将长的管道速度提高几个小时。

此外,纯流媒体系统可以是权威性的。我们不需要把实时系统的结果当作近似值,而把批处理的结果当作真理。"有可能创建不遗漏数据的流式系统(因此计数和总和是精确的)。好的近似唯一计数(用HyperLogLog计算)往往和精确的数字一样具有可操作性。

7. 结论

在过去的几年里,实时处理在Facebook大量涌现。我们已经开发了多个独立但可组合的系统。它们共同构成了一个全面的平台,以满足我们不同的需求。

在本文中,我们讨论了我们所做的多个设计决定以及它们对易用性、性能、容错性、可扩展性和正确性的影响。我们想用三点来总结。

首先,以秒为目标的延迟,而不是以毫秒为目标,是一个重要的设计决定。对于我们支持的所有用例来说,几秒钟已经足够快了;Facebook的其他系统在我们的产品中提供毫秒或微秒级的延迟。几秒钟的延迟使我们能够使用一个持久的消息总线进行数据传输。这种数据传输机制为我们的流处理系统Puma、Swift和Stylus的容错性、可扩展性和多选项的正确性铺平了道路。

第二,易用性和其他条件一样重要。在我们的黑客文化中,"快速 "被印在海报上,拥有具有正确学习曲线的系统可以让我们在几小时或几天内启动和运行原型应用程序,而不是几周。然后我们可以进行测试和迭代。此外,使调试、部署和运行监测变得容易,大大增加了我们系统的采用率,我们计划继续使其变得更加容易。

第三,有一个正确性的spectrum。不是所有的用例都需要ACID语义。通过在这个spectrum上提供选择,我们让应用程序构建者决定他们需要什么。如果他们需要交易和精确到一的语义,他们可以用额外的延迟和硬件来支付。但当他们不需要时许多用例是测量相对比例或方向的变化,而不是绝对值,我们可以使应用更快、更简单。

我们的系统不断发展,以更好地服务于我们的用户。在未来,我们计划致力于扩展我们的系统基础结构。例如,我们希望改善我们的流处理作业的动态负载平衡;负载平衡器应该协调一台机器上的数百个作业,并尽量减少落后作业的恢复时间。我们也在考虑运行流处理工作的其他运行环境。今天,它们在Hive中运行。我们计划评估Spark和Flink。我们希望弥合Facebook的实时处理和批处理之间的差距。


想要加入社区或对本文有任何疑问,可直接添加作者微信交流。

图:作者微信


我们是由一群数据从业人员,因为热爱凝聚在一起,以开源精神为基础,组成的PowerData数据之力社区。

可关注下方公众号后点击“加入我们”,与PowerData一起成长


PowerData
1 声望2 粉丝

PowerData社区官方思否账号