本文被誉为程序员必读文章,学习消息队列的同学都应该看看。原文: The Log: What every software engineer should know about real-time data's unifying abstraction | LinkedIn Engineering
杰伊·克雷普斯
大约六年前,我在一个特别有趣的时候加入了 LinkedIn。我们刚刚开始遇到我们的单一集中式数据库的限制,需要开始过渡到专门的分布式系统组合。这是一次有趣的经历:我们构建、部署和运行了一个分布式图形数据库、一个分布式搜索后端、一个 Hadoop 安装以及第一代和第二代键值存储。
在这一切中我学到的最有用的东西之一是,我们正在构建的许多东西的核心都有一个非常简单的概念:日志。有时称为预写日志或提交日志或事务日志,日志几乎与计算机一样长,并且是许多分布式数据系统和实时应用程序架构的核心。
不了解日志就无法完全了解数据库、NoSQL 存储、键值存储、复制、paxos、hadoop、版本控制或几乎任何软件系统;然而,大多数软件工程师并不熟悉它们。我想改变它。在这篇文章中,我将带您了解有关日志的所有知识,包括什么是日志以及如何使用日志进行数据集成、实时处理和系统构建。
记录附加到日志的末尾,读取从左到右进行。每个条目都分配有唯一的顺序日志条目编号。
记录的顺序定义了“时间”的概念,因为左侧的条目被定义为比右侧的条目更旧。日志条目号可以被认为是条目的“时间戳”。将这种排序描述为时间的概念起初似乎有点奇怪,但它具有方便的特性,即它与任何特定的物理时钟解耦。当我们进入分布式系统时,这个属性将变得至关重要。
就本次讨论而言,记录的内容和格式并不重要。此外,我们不能只向日志中添加记录,因为我们最终会用完空间。稍后我会回到这个问题。
因此,日志与文件或表并没有什么不同。文件是字节数组,表是记录数组,而日志实际上只是一种表或文件,其中记录按时间排序。
在这一点上,您可能想知道为什么值得谈论如此简单的事情?仅追加的记录序列与数据系统有什么关系?答案是日志有一个特定的目的:它们记录发生的事情和时间。对于分布式数据系统,这在很多方面都是问题的核心。
但在我们走得太远之前,让我澄清一些有点令人困惑的事情。每个程序员都熟悉日志的另一种定义——应用程序可能使用 syslog 或 log4j 写入本地文件的非结构化错误消息或跟踪信息。为清楚起见,我将其称为“应用程序日志记录”。应用程序日志是我所描述的日志概念的退化形式。最大的区别是文本日志主要是供人类阅读的,而我所描述的“日志”或“数据日志”是为编程访问而构建的。
(实际上,如果你仔细想想,人类阅读单个机器上的日志的想法有点不合时宜。当涉及到许多服务和服务器时,这种方法很快就会成为一种难以管理的策略,并且日志的目的很快就会成为查询和图表来理解许多机器上的行为——文件中的英文文本并不像这里描述的那种结构化日志那样合适。)
我不知道日志的概念起源于哪里——可能它是像二进制搜索这样的东西之一,它对于发明者来说太简单了,以至于无法意识到它是一项发明。它早在 IBM 的System R就出现了。数据库中的使用与在出现崩溃时保持各种数据结构和索引的同步有关。为了使这种原子性和持久性,数据库使用日志写出有关他们将修改的记录的信息,然后再将更改应用于它维护的所有各种数据结构。日志是所发生的事情的记录,每个表或索引都是将这段历史投影到一些有用的数据结构或索引中。由于日志会立即持久化,因此它被用作在发生崩溃时恢复所有其他持久性结构的权威来源。
随着时间的推移,日志的使用从 ACID 的实现细节发展为在数据库之间复制数据的方法。事实证明,数据库上发生的一系列更改正是保持远程副本数据库同步所需要的。Oracle、MySQL 和 PostgreSQL 包含日志传送协议,用于将部分日志传输到充当从属的副本数据库。Oracle 已将日志产品化为非 Oracle 数据订阅者的通用数据订阅机制,其XStreams和GoldenGate以及 MySQL 和 PostgreSQL 中的类似设施是许多数据架构的关键组件。
由于这个起源,机器可读日志的概念在很大程度上仅限于数据库内部。使用日志作为数据订阅机制似乎几乎是偶然出现的。但这种抽象非常适合支持各种消息传递、数据流和实时数据处理。
日志解决的两个问题——更改排序和分发数据——在分布式数据系统中更为重要。同意更新订单(或同意不同意并应对副作用)是这些系统的核心设计问题之一。
分布式系统的以日志为中心的方法源于一个简单的观察,我将其称为状态机复制原则:
这可能看起来有点迟钝,所以让我们深入了解它的含义。
确定性意味着处理不依赖于时间,并且不会让任何其他“带外”输入影响其结果。例如,一个程序的输出受线程的特定执行顺序或调用gettimeofday
或一些其他不可重复的事物的影响,通常最好将其视为非确定性的。
进程的状态是处理结束时机器上保留的任何数据,无论是在内存中还是在磁盘上。
以相同的顺序获得相同的输入应该敲响警钟——这就是日志的来源。这是一个非常直观的概念:如果你将两个确定性的代码段提供给相同的输入日志,它们将产生相同的输出.
分布式计算的应用非常明显。您可以将让多台机器都做同样事情的问题减少到实现分布式一致日志以提供这些进程输入的问题。此处日志的目的是从输入流中挤出所有不确定性,以确保处理此输入的每个副本保持同步。
当您理解它时,这个原理并没有什么复杂或深刻的:它或多或少等于说“确定性处理是确定性的”。尽管如此,我认为它是分布式系统设计更通用的工具之一。
这种方法的优点之一是索引日志的时间戳现在充当副本状态的时钟——您可以用一个数字来描述每个副本,即它已处理的最大日志条目的时间戳。这个时间戳与日志相结合,唯一地捕获了副本的整个状态。
根据日志中的内容,有多种方法可以在系统中应用此原则。例如,我们可以记录对服务的传入请求,或者服务响应请求所经历的状态变化,或者它执行的转换命令。从理论上讲,我们甚至可以为每个副本记录一系列机器指令以执行,或者在每个副本上调用方法名称和参数。只要两个进程以相同的方式处理这些输入,这些进程将在副本之间保持一致。
不同的人群似乎对日志的用途有不同的描述。数据库人员通常会区分物理日志和逻辑日志。物理日志记录意味着记录更改的每一行的内容。逻辑日志记录不是记录更改的行,而是记录导致行更改的 SQL 命令(插入、更新和删除语句)。
分布式系统文献通常区分处理和复制的两种广泛方法。“状态机模型”通常是指一个主动-主动模型,我们记录传入请求的日志,每个副本处理每个请求。对此稍作修改,称为“主备份模型”,即选举一个副本作为领导者,并允许该领导者按照请求到达的顺序处理请求,并在处理请求时注销对其状态的更改。其他副本按照领导者所做的状态更改的顺序应用,以便它们同步并准备好在领导者失败时接管领导者。
要了解这两种方法之间的区别,让我们看一个玩具问题。考虑一个复制的“算术服务”,它维护一个数字作为其状态(初始化为零)并对这个值应用加法和乘法。主动-主动方法可能会注销要应用的转换,例如“+1”、“*2”等。每个副本都会应用这些转换,因此会经历相同的一组值。“主动-被动”方法将让单个主服务器执行转换并注销结果,例如“1”、“3”、“6”等。这个例子也清楚地说明了为什么排序是确保两者之间一致性的关键副本:重新排序加法和乘法将产生不同的结果。
分布式日志可以看作是对共识问题建模的数据结构。毕竟,日志代表了一系列关于要附加的“下一个”值的决定。你必须眯着眼睛才能看到 Paxos 算法家族中的日志,尽管日志构建是它们最常见的实际应用。对于 Paxos,这通常是使用称为“multi-paxos”的协议的扩展来完成的,它将日志建模为一系列共识问题,日志中的每个槽都有一个。日志在ZAB 、RAFT和Viewstamped Replication等其他协议中更为突出,这些协议直接对维护分布式、一致日志的问题进行建模。
我的怀疑是,我们对此的看法有点偏向于历史的轨迹,这可能是由于分布式计算理论在过去的几十年里超过了它的实际应用。实际上,共识问题有点太简单了。计算机系统很少需要决定单个值,它们几乎总是处理一系列请求。所以日志,而不是简单的单值寄存器,是更自然的抽象。
此外,对算法的关注掩盖了底层日志抽象系统的需求。我怀疑我们最终将更多地关注日志作为商品化的构建块,而不管它的实现方式,就像我们经常谈论哈希表而不费心去了解我们是指线性探测的杂音哈希还是一些细节其他变体。日志将成为某种商品化接口,许多算法和实现都在竞争以提供最佳保证和最佳性能。
让我们回到数据库。更改日志和表格之间存在着令人着迷的二元性。该日志类似于所有贷方和借方以及银行流程的列表;一张表是所有经常账户余额。如果您有更改日志,则可以应用这些更改以创建捕获当前状态的表。该表将记录每个键的最新状态(截至特定日志时间)。从某种意义上说,日志是更基础的数据结构:除了创建原始表之外,您还可以对其进行转换以创建各种派生表。(是的,表可能意味着非关系人员的键控数据存储。)
这个过程也反向进行:如果您有一个表进行更新,您可以记录这些更改并将所有更新的“更改日志”发布到表的状态。此更改日志正是您支持近实时副本所需要的。因此,从这个意义上说,您可以将表和事件视为双重的:表支持静态数据,而日志则捕获变化。日志的神奇之处在于,如果它是完整的更改日志,它不仅包含表的最终版本的内容,还允许重新创建可能已经存在的所有其他版本。实际上,它是表的每个先前状态的一种备份。
这可能会让您想起源代码版本控制。源代码控制和数据库之间有着密切的关系。版本控制解决了一个与分布式数据系统必须解决的问题非常相似的问题——管理分布式的并发状态更改。版本控制系统通常对补丁序列进行建模,这实际上是一个日志。您可以直接与当前代码的已签出“快照”进行交互,该“快照”类似于表格。您会注意到,在版本控制系统中,与在其他分布式有状态系统中一样,复制是通过日志进行的:当您更新时,您只需下载补丁并将它们应用到您当前的快照中。
最近,有些人从销售以日志为中心的数据库的公司Datomic看到了其中的一些想法。本演示文稿很好地概述了他们如何在他们的系统中应用这个想法。当然,这些想法并不是这个系统所独有的,因为十多年来它们一直是分布式系统和数据库文献的一部分。
这似乎有点理论化。不要灰心!我们很快就会得到实用的东西。
在本文的其余部分中,我将尝试说明日志的好处超出了分布式计算或抽象分布式计算模型的内部结构。这包括:
这些使用都解决了将日志作为独立服务的想法。
在每种情况下,日志的用处都来自于日志提供的简单功能:生成持久的、可重复播放的历史记录。令人惊讶的是,这些问题的核心是能够以确定的方式以自己的速率让许多机器播放历史记录。
让我先说一下我所说的“数据集成”是什么意思以及为什么我认为它很重要,然后我们将看看它与日志的关系。
“数据集成”这个短语并不常见,但我不知道更好的短语。更容易识别的术语ETL通常只涵盖数据集成的有限部分——填充关系数据仓库。但我所描述的大部分内容可以被认为是 ETL 泛化以涵盖实时系统和处理流程。
在围绕大数据概念的所有令人窒息的兴趣和炒作中,您并没有听到太多关于数据集成的信息,但尽管如此,我相信“使数据可用”这个平凡的问题是组织可以关注的更有价值的事情之一.
数据的有效使用遵循一种马斯洛的需求层次。金字塔的基础涉及捕获所有相关数据,能够将它们放在适用的处理环境中(无论是花哨的实时查询系统还是只是文本文件和 python 脚本)。这些数据需要以统一的方式建模,以便于读取和处理。一旦满足了以统一方式捕获数据的这些基本需求,就可以在基础设施上工作以各种方式处理这些数据——MapReduce、实时查询系统等。
值得注意的一点是:没有可靠和完整的数据流,Hadoop 集群只不过是一个非常昂贵且难以组装的空间加热器。一旦数据和处理可用,人们就可以将注意力转移到更好的数据模型和一致且易于理解的语义等更精细的问题上。最后,注意力可以转向更复杂的处理——更好的可视化、报告以及算法处理和预测。
根据我的经验,大多数组织在这个金字塔的底部都有巨大的漏洞——他们缺乏可靠的完整数据流——但希望直接跳到高级数据建模技术。这完全是倒退。
所以问题是,我们如何才能在组织中的所有数据系统中建立可靠的数据流?
两个趋势使数据集成变得更加困难。
事件数据流水线
第一个趋势是事件数据的兴起。事件数据记录发生的事情,而不是已经发生的事情。在 Web 系统中,这意味着用户活动记录,以及可靠操作和监控数据中心机器价值所需的机器级事件和统计信息。人们倾向于称其为“日志数据”,因为它通常被写入应用程序日志,但这将形式与功能混淆了。这些数据是现代网络的核心:毕竟,谷歌的财富是由建立在点击和印象(即事件)之上的相关管道产生的。
而且这些东西不仅限于网络公司,只是网络公司已经完全数字化,所以它们更容易检测。长期以来,财务数据一直以事件为中心。RFID将这种跟踪添加到物理对象。我认为随着传统业务和活动的数字化,这种趋势将继续下去。
这种类型的事件数据记录了发生的事情,并且往往比传统数据库使用大几个数量级。这对处理提出了重大挑战。
专业数据系统的爆炸式增长
第二个趋势来自专业数据系统的爆炸式增长,这些系统在过去五年中变得流行并且通常免费提供。存在用于OLAP、搜索、简单 在线 存储、批处理、图形分析等的专门系统 。
更多种类的更多数据以及将这些数据输入更多系统的愿望的结合导致了巨大的数据集成问题。
每个逻辑数据源都可以建模为自己的日志。数据源可以是记录事件(例如点击或页面查看)的应用程序,也可以是接受修改的数据库表。每个订阅系统都尽可能快地从该日志中读取数据,将每条新记录应用到自己的存储中,并提升其在日志中的位置。订阅者可以是任何类型的数据系统——缓存、Hadoop、另一个站点中的另一个数据库、搜索系统等。
例如,日志概念为每个更改提供了一个逻辑时钟,所有订阅者都可以根据该时钟进行测量。这使得推理不同用户系统相对于彼此的状态要简单得多,因为每个用户系统都有一个他们已经读取到的“时间点”。
为了使这一点更具体,考虑一个简单的情况,其中有一个数据库和一组缓存服务器。该日志提供了一种方法来同步所有这些系统的更新并推断每个系统的时间点。假设我们用日志条目 X 写了一条记录,然后需要从缓存中读取。如果我们想保证我们看不到陈旧的数据,我们只需要确保我们不会从任何尚未复制到 X 的缓存中读取。
日志还充当缓冲区,使数据生产与数据消费异步。这很重要,原因有很多,尤其是当有多个订阅者可能以不同的速率消费时。这意味着订阅系统可能会崩溃或停机进行维护,并在它恢复时赶上:订阅者以它控制的速度消费。像 Hadoop 或数据仓库这样的批处理系统可能只需要每小时或每天消耗一次,而实时查询系统可能需要实时更新。原始数据源和日志都不了解各种数据目标系统,因此可以在不改变管道的情况下添加和删除消费者系统。
特别重要的是:目标系统只知道日志,而不知道源系统的任何细节。消费者系统不需要关心数据是来自关系数据库管理系统、新奇的键值存储,还是在没有任何类型的实时查询系统的情况下生成的。这似乎是一个小问题,但实际上很关键。
我在这里使用术语“日志”而不是“消息系统”或“发布订阅”,因为它更具体地描述了语义,并且更详细地描述了在实际实现中支持数据复制所需的内容。我发现“发布订阅”不仅仅意味着消息的间接寻址——如果你比较任何两个承诺发布-订阅的消息传递系统,你会发现它们保证的东西非常不同,而且大多数模型在这个领域没有用处. 您可以将日志视为一种具有持久性保证和强排序语义的消息传递系统。在分布式系统中,这种通信模型有时被称为原子广播(有点可怕) 。
值得强调的是,日志仍然只是基础设施。掌握数据流的故事还没有结束:故事的其余部分围绕元数据、模式、兼容性以及处理数据结构和演变的所有细节。但是,除非有一种可靠的、通用的方法来处理数据流的机制,否则语义细节是次要的。
如今,我们的主要数据系统包括:
其中每一个都是一个专门的分布式系统,在其专业领域提供高级功能。
甚至在我来到这里之前,这种使用日志进行数据流的想法就已经在 LinkedIn 上流传。我们开发的最早的基础设施之一是一项名为databus的服务,它在我们早期的 Oracle 表之上提供了一个日志缓存抽象,以扩展对数据库更改的订阅,以便我们可以提供我们的社交图和搜索索引。
我将提供一点历史来提供背景。我自己在 2008 年左右开始参与这件事,当时我们已经发布了我们的键值存储。我的下一个项目是尝试启动一个有效的 Hadoop 设置,并将我们的一些推荐流程移到那里。由于在这方面经验不足,我们自然而然地预算了几周的时间来输入和输出数据,剩下的时间用于实现花哨的预测算法。于是开始了漫长的征程。
我们最初计划只是从我们现有的 Oracle 数据仓库中抓取数据。第一个发现是,快速从 Oracle 中获取数据是一种黑魔法。更糟糕的是,数据仓库处理不适用于我们为 Hadoop 计划的生产批处理——大部分处理是不可逆转的,并且特定于正在完成的报告。我们最终避开了数据仓库,直接访问了源数据库和日志文件。最后,我们实现了另一个管道将数据加载到我们的键值存储中以提供结果。
这种平凡的数据复制最终成为原始开发的主要项目之一。更糟糕的是,任何时候任何管道出现问题,Hadoop 系统基本上都是无用的——在坏数据上运行花哨的算法只会产生更多的坏数据。
尽管我们以相当通用的方式构建了东西,但每个新数据源都需要自定义配置来设置。它也被证明是大量错误和失败的根源。我们在 Hadoop 上实现的站点功能变得流行起来,我们发现自己拥有一长串感兴趣的工程师。每个用户都有一个他们想要集成的系统列表和一长串他们想要的新数据馈送。
我慢慢明白了一些事情。
首先,我们建造的管道虽然有点乱,但实际上非常有价值。仅仅在新的处理系统 (Hadoop) 中提供数据的过程就开启了许多可能性。以前很难对数据进行新的计算。许多新产品和分析只是将以前锁定在专门系统中的多条数据组合在一起。
其次,很明显,可靠的数据加载需要数据管道的深入支持。如果我们捕获了我们需要的所有结构,我们就可以让 Hadoop 数据加载完全自动化,这样就不会增加手动工作来添加新数据源或处理模式更改——数据会神奇地出现在 HDFS 中,并且会自动为新生成 Hive 表具有适当列的数据源。
第三,我们的数据覆盖率仍然很低。也就是说,如果您查看 LinkedIn 拥有的在 Hadoop 中可用的数据的总体百分比,它仍然非常不完整。考虑到操作每个新数据源所需的工作量,完成工作并非易事。
我们一直在为每个数据源和目标构建自定义数据加载的方式显然是不可行的。我们有几十个数据系统和数据存储库。连接所有这些将导致在每对系统之间构建自定义管道,如下所示:
请注意,数据通常双向流动,因为许多系统(数据库、Hadoop)既是数据传输的源又是目标。这意味着我们最终将为每个系统构建两条管道:一条用于获取数据,一条用于获取数据。
这显然需要一支军队来建造,而且永远无法运作。当我们接近完全连通性时,我们最终会得到类似 O(N 2 ) 的管道。
相反,我们需要这样的通用内容:
我们需要尽可能地将每个消费者与数据源隔离开来。理想情况下,他们应该与一个单一的数据存储库集成,让他们可以访问所有内容。
这个想法是,添加一个新的数据系统——无论是数据源还是数据目标——应该创建集成工作,仅将其连接到单个管道而不是每个数据消费者。
这段经历使我专注于构建Kafka,将我们在消息传递系统中看到的内容与数据库和分布式系统内部流行的日志概念相结合。我们希望首先充当所有活动数据的中央管道,最终用于许多其他用途,包括 Hadoop 外的数据部署、监控数据等。
长期以来,Kafka 作为一种基础设施产品有点独特(有些人会说很奇怪)——既不是数据库,也不是日志文件收集系统,也不是传统的消息传递系统。但最近亚马逊提供了一项与 Kafka 非常相似的服务,称为Kinesis。相似之处在于处理分区、保留数据的方式,以及 Kafka API 中高级和低级消费者之间相当奇怪的划分。我对此感到非常高兴。您创建了良好的基础设施抽象的标志是 AWS 将其作为服务提供!他们对此的看法似乎与我所描述的完全相似:它是连接他们所有分布式系统(DynamoDB、RedShift、S3 等)的管道,也是使用 EC2 进行分布式流处理的基础。
让我们稍微谈谈数据仓库。数据仓库旨在成为支持分析的干净、集成数据的存储库。这是一个好主意。对于那些不知道的人,数据仓库方法包括定期从源数据库中提取数据,将其转换成某种可理解的形式,并将其加载到中央数据仓库中。拥有这个包含所有数据的干净副本的中心位置对于数据密集型分析和处理来说是一项非常宝贵的资产。在高层次上,无论您使用 Oracle、Teradata 还是 Hadoop 等传统数据仓库,这种方法都不会发生太大变化,尽管您可能会改变加载和修改的顺序。
包含干净、集成数据的数据仓库是一项非凡的资产,但获得它的机制有点过时了。
以数据为中心的组织的关键问题是将干净的集成数据耦合到数据仓库。数据仓库是一种批量查询基础设施,非常适合多种报告和即席分析,尤其是当查询涉及简单的计数、聚合和过滤时。但是,将批处理系统作为唯一干净完整数据的存储库意味着数据无法用于需要实时馈送的系统——实时处理、搜索索引、监控系统等。
在我看来,ETL 真的是两件事。首先,它是一个提取和数据清理过程——本质上是释放锁定在组织中各种系统中的数据,并删除特定于系统的废话。其次,为数据仓库查询重新构建数据(即,使其适合关系数据库的类型系统,强制为星形或雪花模式,可能分解为高性能列 格式等)。把这两件事混为一谈是个问题。干净、集成的数据存储库应实时可用,并可用于低延迟处理以及其他实时存储系统中的索引。
我认为这具有使数据仓库 ETL 在组织上更具可扩展性的额外好处。数据仓库团队的经典问题是他们负责收集和清理组织中其他团队生成的所有数据。激励措施不一致:数据生产者通常不太了解数据仓库中数据的使用情况,最终创建的数据难以提取或需要大量、难以扩展的转换才能进入可用形式。当然,中央团队永远无法完全按照组织其他部门的步伐进行扩展,因此数据覆盖范围总是参差不齐,数据流很脆弱,而且变化很慢。
更好的方法是拥有一个中央管道,即日志,以及一个定义明确的 API 用于添加数据。与此管道集成并提供干净、结构良好的数据馈送的责任在于此数据馈送的生产者。这意味着,作为系统设计和实施的一部分,他们必须考虑将数据取出并以结构良好的形式传递到中央管道的问题。添加新的存储系统对数据仓库团队没有影响,因为它们有一个中心集成点。数据仓库团队只处理从中央日志加载结构化数据馈送和执行特定于其系统的转换的简单问题。
当考虑采用传统数据仓库之外的其他数据系统时,关于组织可扩展性的这一点变得尤为重要。例如,假设一个人希望提供对组织的完整数据集的搜索功能。或者,假设您希望通过实时趋势图和警报提供对数据流的亚秒级监控。在这两种情况下,传统数据仓库甚至 Hadoop 集群的基础架构都不合适。更糟糕的是,为支持数据库负载而构建的 ETL 处理管道可能无法为这些其他系统提供数据,这使得引导这些基础设施的工作与采用数据仓库一样大。这可能不是 这是可行的,并且可能有助于解释为什么大多数组织没有这些功能可以轻松地用于其所有数据。相比之下,如果组织已经构建了统一、结构良好的数据源,那么让任何新系统完全访问所有数据只需要一点集成管道连接到管道。
此架构还针对特定清理或转换可以驻留的位置提出了一组不同的选项:
最好的模型是在数据发布者将数据发布到日志之前进行清理。这意味着确保数据采用规范形式,并且不会保留来自生成它的特定代码或可能维护它的存储系统的任何保留。这些细节最好由创建数据的团队处理,因为他们最了解自己的数据。在这个阶段应用的任何逻辑都应该是无损和可逆的。
任何可以实时完成的增值转换都应该作为对生成的原始日志提要的后处理来完成。这将包括事件数据的会话化,或添加其他普遍感兴趣的派生字段。原始日志仍然可用,但这种实时处理会生成包含增强数据的派生日志。
最后,只有特定于目标系统的聚合应该作为加载过程的一部分执行。这可能包括将数据转换为特定的星形或雪花模式,以便在数据仓库中进行分析和报告。因为这个最自然地映射到传统 ETL 过程的阶段,现在是在一组更干净、更统一的流上完成的,所以应该大大简化。
让我们稍微谈谈这种架构的一个附带好处:它支持解耦的、事件驱动的系统。
Web 行业中获取活动数据的典型方法是将其记录到文本文件中,然后可以将其提取到数据仓库或 Hadoop 中以进行聚合和查询。这样做的问题与所有批处理 ETL 的问题相同:它将数据流与数据仓库的能力和处理计划相结合。
在 LinkedIn,我们以日志为中心的方式构建了我们的事件数据处理。我们使用 Kafka 作为中央的多订阅者事件日志。我们已经定义了数百个事件类型,每个事件类型都捕获关于特定类型动作的独特属性。这涵盖了从页面浏览量、广告印象和搜索到服务调用和应用程序异常的所有内容。
为了理解这样做的好处,想象一个简单的事件——在工作页面上显示一个职位发布。作业页面应仅包含显示作业所需的逻辑。但是,在一个相当动态的站点中,这很容易被与显示工作无关的附加逻辑所掩盖。例如,假设我们需要集成以下系统:
很快,展示工作的简单行为变得相当复杂。当我们添加其他显示工作的地方时——移动应用程序等等——这个逻辑必须被延续,复杂性也会增加。更糟糕的是,我们需要与之交互的系统现在有些交织在一起——工作显示工作的人需要了解许多其他系统和功能,并确保它们被正确集成。这只是问题的玩具版本,任何真正的应用程序都会更复杂,而不是更少。
“事件驱动”风格提供了一种简化这一点的方法。作业显示页面现在只显示作业并记录显示作业的事实以及作业的相关属性、查看器以及关于作业显示的任何其他有用的事实。其他每个感兴趣的系统——推荐系统、安全系统、工作海报分析系统和数据仓库——都只是订阅提要并进行处理。显示代码不需要知道这些其他系统,并且如果添加了新的数据消费者也不需要更改。
当然,将发布者与订阅者分开并不是什么新鲜事。但是,如果您想保留一个提交日志,作为一个多订阅者实时日志记录消费者规模网站上发生的所有事情,那么可扩展性将是一个主要挑战。如果我们不能构建一个足够快速、廉价和可扩展的日志以使其大规模实用,那么使用日志作为通用集成机制将永远只是一个优雅的幻想。
系统人员通常认为分布式日志是一种缓慢、重量级的抽象(通常只将其与 Zookeeper 可能适用的“元数据”用途相关联)。但是,对于专注于记录大数据流的深思熟虑的实施,这不一定是真的。在 LinkedIn,我们目前每天通过 Kafka 运行超过 600 亿条独特的消息写入(如果算上数据中心之间镜像的写入量,则为数千亿条)。
我们在 Kafka 中使用了一些技巧来支持这种规模:
为了允许水平扩展,我们将日志分成多个分区:
每个分区都是一个完全有序的日志,但分区之间没有全局排序(除了可能包含在消息中的一些挂钟时间)。消息到特定分区的分配由作者控制,大多数用户选择通过某种键(例如用户 ID)进行分区。分区允许在没有分片之间协调的情况下发生日志追加,并允许系统的吞吐量与 Kafka 集群大小线性扩展。
每个分区都通过可配置数量的副本进行复制,每个副本都有分区日志的相同副本。在任何时候,其中一个人将充当领导者;如果领导者失败,其中一个副本将接管领导者。
缺乏跨分区的全局顺序是一个限制,但我们还没有发现它是一个主要限制。事实上,与日志的交互通常来自成百上千个不同的进程,因此谈论它们的行为的总顺序是没有意义的。相反,我们提供的保证是每个分区都保持顺序,并且 Kafka 保证来自单个发送者的附加到特定分区将按照它们发送的顺序传递。
像文件系统一样,日志很容易针对线性读写模式进行优化。日志可以将小的读取和写入组合成更大的高吞吐量操作。Kafka 积极地追求这种优化。在发送数据、写入磁盘、服务器之间的复制、向消费者传输数据以及确认已提交的数据时,会发生从客户端到服务器的批处理。
最后,Kafka 使用一种简单的二进制格式,在内存日志、磁盘日志和网络数据传输之间进行维护。这使我们能够利用许多优化,包括零拷贝数据传输。
这些优化的累积效应是您通常可以以磁盘或网络支持的速率写入和读取数据,即使在维护大大超出内存的数据集时也是如此。
这篇文章主要不是关于 Kafka 的,所以我不会详细介绍。您可以在此处阅读有关 LinkedIn 方法的更详细概述,并在此处阅读对 Kafka 设计的全面概述。
到目前为止,我只描述了一种从一个地方到另一个地方复制数据的奇特方法。但是在存储系统之间交换字节并不是故事的结局。事实证明,“日志”是“流”的另一个词,日志是流处理的核心。
但是,等等,流处理到底是什么?
如果您是 90 年代末和 2000 年代初数据库 文献或半成功的数据 基础架构 产品的粉丝,您可能会将流处理与构建 SQL 引擎或用于事件驱动处理的“方框和箭头”接口的努力联系起来。
如果您关注开源数据系统的爆炸式增长,您可能会将流处理与该领域的一些系统联系起来——例如,Storm、Akka、S4和 Samza。但是大多数人认为这些是一种异步消息处理系统,与集群感知的 RPC 层没有什么不同(事实上,这个领域的一些东西正是如此)。
这两种观点都有些局限。流处理与 SQL 无关。它也不限于实时处理。没有内在的原因您不能使用各种不同的语言来处理昨天或一个月前的数据流来表达计算。
我将流处理视为更广泛的东西:用于连续数据处理的基础设施。我认为计算模型可以像 MapReduce 或其他分布式处理框架一样通用,但具有产生低延迟结果的能力。
处理模型的真正驱动力是数据收集的方法。批量收集的数据自然是批量处理的。当数据被连续收集时,它自然会被连续处理。
美国人口普查提供了批量数据收集的一个很好的例子。人口普查会定期启动,并通过让人们挨家挨户走动,对美国公民进行强力发现和枚举。这在 1790 年首次开始人口普查时很有意义。当时的数据收集本质上是面向批处理的,它涉及骑马四处走动并在纸上写下记录,然后将这批记录传输到人类汇总所有计数的中心位置。这些天来,当您描述人口普查过程时,您会立即想知道为什么我们不记录出生和死亡的日志并连续或以任何所需的粒度生成人口计数。
这是一个极端的例子,但许多数据传输过程仍然依赖于定期转储以及批量传输和集成。处理批量转储的唯一自然方法是使用批处理。但随着这些过程被连续馈送所取代,人们自然会开始转向连续处理,以理顺所需的处理资源并减少延迟。
例如,LinkedIn 几乎没有批量数据收集。我们的大部分数据要么是活动数据,要么是数据库更改,这两者都是连续发生的。事实上,当您考虑任何业务时,基本机制几乎总是一个连续的过程——事件是实时发生的,正如 Jack Bauer 告诉我们的那样。当数据批量收集时,几乎总是由于一些手动步骤或缺乏数字化,或者是一些非数字化过程自动化遗留下来的历史遗迹。当机械师是邮件并且人类进行处理时,传输和对数据做出反应过去非常缓慢。自动化的第一次通过始终保留原始流程的形式,因此这通常会持续很长时间。
每天运行的生产“批处理”作业通常有效地模仿一种窗口大小为一天的连续计算。当然,基础数据总是在变化。这些实际上在 LinkedIn 很常见(以及使它们在 Hadoop 中工作的机制非常棘手),以至于我们实现了一个完整的框架来管理增量 Hadoop 工作流。
从这个角度来看,很容易对流处理有不同的看法:它只是在处理的基础数据中包含时间概念的处理,不需要数据的静态快照,因此它可以在一定时间内产生输出用户控制频率,而不是等待到达数据集的“结束”。从这个意义上说,流处理是批处理的泛化,并且鉴于实时数据的普遍性,这是一个非常重要的泛化。
那么为什么流处理的传统观点一直是一个小众应用呢?我认为最大的原因是缺乏实时数据收集使得连续处理成为学术问题。
我认为缺乏实时数据收集可能是商业流处理系统注定要失败的原因。他们的客户仍在为 ETL 和数据集成进行面向文件的日常批处理。构建流处理系统的公司专注于提供附加到实时数据流的处理引擎,但事实证明,当时很少有人真正拥有实时数据流。实际上,在我在 LinkedIn 的职业生涯早期,一家公司试图向我们推销一个非常酷的流处理系统,但由于当时我们所有的数据都是以每小时文件的形式收集的,所以我们能想出的最好的应用程序是每小时文件在小时结束时进入流系统!他们指出,这是一个相当普遍的问题。这个例外实际上证明了这里的规则:金融,
即使存在一个健康的批处理生态系统,我认为流处理作为一种基础架构样式的实际适用性是相当广泛的。我认为它涵盖了实时请求/响应服务和离线批处理之间的基础设施差距。对于现代互联网公司,我认为他们大约 25% 的代码属于这一类。
事实证明,日志解决了流处理中的一些最关键的技术问题,我将对此进行描述,但它解决的最大问题只是让数据在实时多订阅者数据馈送中可用。对于那些对更多细节感兴趣的人,我们开源了Samza,这是一个明确地建立在许多这些想法之上的流处理系统。我们在此处的文档中更详细地描述了许多这些应用程序。
流处理最有趣的方面与流处理系统的内部无关,而是与它如何扩展我们从早期数据集成讨论中对数据馈送的概念有关。我们主要讨论了原始数据的提要或日志——在执行各种应用程序时产生的事件和数据行。但是流处理允许我们还包括根据其他提要计算的提要。这些派生的提要对消费者而言与计算它们的原始数据提要没有什么不同。这些派生的提要可以封装任意复杂性。
让我们深入研究一下。就我们的目的而言,流处理作业将是从日志中读取并将输出写入日志或其他系统的任何内容。他们用于输入和输出的日志将这些过程加入到处理阶段的图表中。实际上,以这种方式使用集中式日志,您可以将组织的所有数据捕获、转换和流视为只是写入它们的一系列日志和流程。
流处理器根本不需要花哨的框架:它可以是从日志读取和写入的任何进程或一组进程,但可以提供额外的基础设施和支持来帮助管理处理代码。
集成中日志的目的有两个。
首先,它使每个数据集都是多订阅者和有序的。回想一下我们的“状态复制”原则,记住顺序的重要性。为了使这一点更具体,考虑一个来自数据库的更新流——如果我们在处理过程中对同一记录重新排序两次更新,我们可能会产生错误的最终输出。这种顺序比 TCP 等提供的顺序更持久,因为它不限于单个点对点链接,并且在进程失败和重新连接之后仍然存在。
其次,日志为进程提供缓冲。这是非常根本的。如果处理以非同步方式进行,则上游数据生成作业生成数据的速度可能比另一个下游作业消耗数据的速度更快。发生这种情况时,处理必须阻塞、缓冲或丢弃数据。删除数据可能不是一种选择;阻塞可能会导致整个处理图停止运行。日志充当一个非常非常大的缓冲区,允许进程重新启动或失败,而不会减慢处理图的其他部分。当将此数据流扩展到更大的组织时,这种隔离尤其重要,在该组织中,处理由许多不同团队的工作进行。我们不能有一个错误的工作导致停止整个处理流程的背压。
Storm和Samza都是以这种方式构建的,并且可以使用 Kafka 或其他类似系统作为它们的日志。
一些实时流处理只是无状态的一次记录转换,但许多用途是更复杂的计数、聚合或流中窗口的连接。例如,一个人可能希望使用有关用户进行点击的信息来丰富事件流(例如点击流)——实际上是将点击流加入到用户帐户数据库中。总是,这种处理最终需要处理器维护某种状态:例如,在计算计数时,您需要维护到目前为止的计数。如果处理器本身可能发生故障,如何正确维护这种状态?
最简单的选择是将状态保存在内存中。但是,如果进程崩溃,它将失去其中间状态。如果仅在一个窗口上维护状态,则该过程可能会回退到日志中窗口开始的位置。但是,如果一个人在一个多小时内进行计数,这可能是不可行的。
另一种方法是简单地将所有状态存储在远程存储系统中,并通过网络加入该存储。这样做的问题是没有数据的局部性和大量的网络往返。
我们如何支持像“表”这样与我们的处理分开的东西?
回想一下关于表和日志的二元性的讨论。这为我们提供了能够将流转换为与我们的处理位于同一位置的表的工具,以及一种处理这些表的容错机制。
流处理器可以将其状态保存在本地“表”或“索引”中——bdb、leveldb,甚至更不寻常的东西,例如Lucene或fastbit索引。这个存储的内容是从它的输入流中提供的(可能在第一次应用任意转换之后)。它可以为它保留的这个本地索引记录一个更改日志,以允许它在发生崩溃和重新启动时恢复其状态。这种机制允许一种通用机制,用于将任意索引类型中的共同分区状态与传入的流数据保持在本地。
当进程失败时,它会从更改日志中恢复其索引。日志是将本地状态转换为一次备份的一种增量记录。
这种状态管理方法具有优雅的特性,即处理器的状态也作为日志进行维护。我们可以认为这个日志就像我们对数据库表的更改日志一样。事实上,处理器有一些非常像与它们一起维护的共同分区表。由于这个状态本身就是一个日志,其他处理器可以订阅它。在处理的目标是更新最终状态并且该状态是处理的自然输出的情况下,这实际上非常有用。
当与出于数据集成目的而从数据库中出来的日志结合使用时,日志/表二元性的力量就变得清晰起来。可以从数据库中提取更改日志,并由各种流处理器以不同的形式进行索引,以结合事件流。
我们在 Samza 中详细介绍了这种管理状态处理的方式,并在此处提供了更多实际示例。
当然,我们不能希望始终为所有状态更改保留完整的日志。除非想使用无限空间,否则必须以某种方式清理日志。我将稍微谈谈在 Kafka 中的实现,以使其更加具体。在 Kafka 中,清理有两个选项,具体取决于数据是否包含键控更新或事件数据。对于事件数据,Kafka 支持只保留一个数据窗口。通常,这被配置为几天,但可以根据时间或空间来定义窗口。但是,对于键控数据,完整日志的一个很好的特性是您可以重放它以重新创建源系统的状态(可能在另一个系统中重新创建它)。
但是,保留完整的日志会随着时间的推移使用越来越多的空间,并且回放的时间会越来越长。因此,在 Kafka 中,我们支持不同类型的保留。我们不是简单地丢弃旧日志,而是删除过时的记录——即主键有更新的记录。通过这样做,我们仍然保证日志包含源系统的完整备份,但现在我们不能再重新创建源系统的所有先前状态,只能重新创建最近的状态。我们将此功能称为日志压缩。
我要讨论的最后一个主题是日志在在线数据系统的数据系统设计中的作用。
日志在分布式数据库中用于数据流的角色与它在更大组织中用于数据集成的角色之间存在类比。在这两种情况下,它都负责数据流、一致性和恢复。毕竟,如果不是一个非常复杂的分布式数据系统,那么组织是什么?
因此,如果您稍微眯起眼睛,您可以将整个组织的系统和数据流视为单个分布式数据库。您可以将所有单独的面向查询的系统(Redis、SOLR、Hive 表等)视为数据上的特定索引。您可以将 Storm 或 Samza 等流处理系统视为非常完善的触发器和视图物化机制。我注意到,古典数据库人员非常喜欢这种观点,因为它最终向他们解释了人们究竟在用所有这些不同的数据系统做什么——它们只是不同的索引类型!
不可否认,现在数据系统类型呈爆炸式增长,但实际上,这种复杂性一直存在。即使在关系数据库的鼎盛时期,组织也拥有大量的关系数据库!因此,当所有数据真正都在一个地方时,也许自从大型机以来就没有真正的集成。将数据隔离到多个系统中的动机有很多:规模、地理、安全性和性能隔离是最常见的。但是这些问题可以通过一个好的系统来解决:例如,一个组织可以拥有一个单一的 Hadoop 集群,它包含所有数据并服务于大量不同的选区。
因此,在迁移到分布式系统时,数据处理已经有了一种可能的简化:将每个系统的大量小实例合并到几个大集群中。许多系统还不够好,无法做到这一点:它们没有安全性,或者不能保证性能隔离,或者只是扩展性不够好。但是这些问题中的每一个都是可以解决的。
我的看法是,不同系统的爆炸式增长是由构建分布式数据系统的困难造成的。通过削减到单个查询类型或用例,每个系统都能够将其范围缩小到可以构建的一组事物中。但是运行所有这些系统会产生太多的复杂性。
我看到了未来可能遵循的三个可能方向。
第一种可能性是维持现状:系统分离或多或少地保持原样,持续时间更长。这可能是因为分发难度太难以克服,或者因为这种专业化为每个系统提供了新的便利性和功能。只要这仍然是正确的,数据集成问题将仍然是成功使用数据的最重要的事情之一。在这种情况下,集成数据的外部日志将非常重要。
第二种可能性是重新整合,一个具有足够通用性的单一系统开始将所有不同的功能重新合并到一个单一的超级系统中。这个超级系统表面上可能类似于关系数据库,但它在组织中的使用会大不相同,因为您只需要一个大系统而不是无数小系统。在这个世界上,除了这个系统内部解决的问题之外,没有真正的数据集成问题。我认为建立这样一个系统的实际困难使得这不太可能。
不过,还有另一种可能的结果,作为一名工程师,我实际上觉得这很有吸引力。新一代数据系统的一个有趣方面是它们几乎都是开源的。开源允许另一种可能性:可以将数据基础设施拆分为一组服务和面向应用程序的系统 API。您已经在 Java 堆栈中看到了一定程度的这种情况:
如果你把这些东西堆成一堆,眯着眼睛看,它开始看起来有点像分布式数据系统工程的乐高版本。您可以将这些成分拼凑在一起,以创建大量可能的系统。这显然与最终用户无关,他们可能更关心 API 而不是它的实现方式,但它可能是在不断发展的更加多样化和模块化的世界中获得单一系统简单性的途径. 如果分布式系统的实施时间因为可靠、灵活的构建块的出现而从数年缩短到数周,那么合并成单个单体系统的压力就会消失。
假设存在外部日志的系统允许各个系统放弃许多自身的复杂性并依赖于共享日志。以下是我认为日志可以做的事情:
这实际上是分布式数据系统所做工作的重要部分。事实上,剩下的大部分内容都与最终的面向客户端的查询 API 和索引策略有关。这正是因系统而异的部分:例如,全文搜索查询可能需要查询所有分区,而按主键查询可能只需要查询负责该键数据的单个节点。
这是它的工作原理。系统分为两个逻辑部分:日志和服务层。日志按顺序捕获状态更改。服务节点存储服务查询所需的任何索引(例如,键值存储可能具有 btree 或 sstable 之类的东西,搜索系统将具有倒排索引)。写入可以直接进入日志,尽管它们可能由服务层代理。写入日志会产生一个逻辑时间戳(比如日志中的索引)。如果系统是分区的,我假设它是分区的,那么日志和服务节点将具有相同数量的分区,尽管它们可能具有非常不同的机器数量。
服务节点订阅日志并按照日志存储它们的顺序尽快将写入应用到其本地索引。
客户端可以通过提供写入的时间戳作为其查询的一部分,从任何节点获取 read-your-write 语义——接收此类查询的服务节点会将所需的时间戳与其自己的索引点进行比较,并在必要时延迟请求直到它至少索引到那个时间以避免提供过时的数据。
服务节点可能需要也可能不需要任何“主控”或“领导选举”的概念。对于许多简单的用例,服务节点可以完全没有领导者,因为日志是事实的来源。
分布式系统必须做的一件棘手的事情是处理恢复故障节点或在节点之间移动分区。一种典型的方法是让日志只保留一个固定的数据窗口,并将其与存储在分区中的数据的快照相结合。日志保留完整的数据副本并垃圾收集日志本身同样是可能的。这将大量复杂性从特定于系统的服务层移到日志中,这可以是通用的。
通过拥有这个日志系统,您可以获得一个完全开发的订阅 API,用于将 ETL 馈送到其他系统的数据存储内容。事实上,许多系统可以共享相同的日志,同时提供不同的索引,如下所示:
请注意,这样一个以日志为中心的系统本身如何立即成为数据流的提供者,以便在其他系统中进行处理和加载。同样,流处理器可以使用多个输入流,然后通过另一个索引该输出的系统为它们提供服务。
我发现将系统视图作为日志和查询 API 的因素非常具有启发性,因为它可以让您将查询特征与系统的可用性和一致性方面分开。实际上,我认为这甚至是一种有用的方法,可以在心理上考虑不是以这种方式构建的系统以更好地理解它。
值得注意的是,虽然 Kafka 和 Bookeeper 是一致的日志,但这并不是必须的。您可以轻松地将类似Dynamo的数据库分解为最终一致的AP日志和键值服务层。这样的日志使用起来有点棘手,因为它会重新传递旧消息并且依赖于订阅者来处理这个(很像 Dynamo 本身)。
在日志中拥有一个单独的数据副本(特别是如果它是一个完整的副本)的想法让许多人觉得浪费。实际上,尽管有一些因素使这个问题变得不那么重要。首先,日志可以是一种特别有效的存储机制。我们在生产 Kafka 服务器上为每个数据中心存储超过 75TB 的数据。同时,许多服务系统需要更多的内存来有效地服务数据(例如,文本搜索通常都在内存中)。服务系统也可以使用优化的硬件。例如,我们的大多数实时数据系统要么内存不足,要么使用 SSD。相比之下,日志系统只做线性读写,所以使用多TB的大硬盘还是蛮开心的。最后,如上图,在数据由多个系统提供服务的情况下,日志的成本分摊到多个索引上。这种组合使得外部日志的开销非常小。
这正是 LinkedIn 用来构建许多自己的实时查询系统的模式。这些系统提供数据库(使用 Databus 作为日志抽象或来自 Kafka 的专用日志)并在该数据流之上提供特定的分区、索引和查询功能。这就是我们实现搜索、社交图和 OLAP 查询系统的方式。事实上,将单个数据馈送(无论是实时馈送还是来自 Hadoop 的派生馈送)复制到多个服务系统以进行实时服务是很常见的。这已被证明是一个巨大的简化假设。这些系统都不需要具有外部可访问的写入 API,Kafka 和数据库被用作记录系统,并且更改通过该日志流向适当的查询系统。写入由托管特定分区的节点在本地处理。这些节点盲目地将日志提供的提要转录到自己的存储中。可以通过重播上游日志来恢复失败的节点。
这些系统对日志的依赖程度各不相同。完全依赖的系统可以利用日志进行数据分区、节点恢复、重新平衡以及一致性和数据传播的所有方面。在这个设置中,实际的服务层实际上就是一种“缓存”,其结构是为了支持特定类型的处理,直接写入日志。
如果你做到了这一步,你就会知道我对日志的大部分了解。
以下是您可能想要查看的一些有趣的参考资料。
每个人似乎对相同的事物使用不同的术语,因此将数据库文献与分布式系统的东西、各种企业软件阵营和开源世界联系起来有点令人费解。尽管如此,这里有一些大方向的指示。
学术论文、系统、演讲和博客:
我试图跟上这个领域,所以如果你知道我遗漏的一些事情,请告诉我。