共计 7747 个字符,预计需要花费 20 分钟才能阅读完成。
前言
我在大约六年前的一个较为巧合的时机加入了领英。当时我们正面临着单机应用,集中式数据库带来的挑战,并开始将其迁移成一组定制的分布式系统。这是一段很有趣的经历:我们构建,编译并运行了一套分布式图形数据库,一个分布式的搜索后台应用,一套 Hadoop 和一代与二代键值数据库。
在此期间,我体验最深的一件事情是,这些工具的本质是日志。这些日志可以是预写式日志(write-ahead logs),可以是提交日志或是事物日志。日志基本是所有分布式数据系统和实时应用架构的核心。
你很难脱离日志体系来透彻的了解关系型数据库,非关系型数据库,键值数据库,备份,paxos,hadoop,版本控制或是任何软件系统。但是,仍然有不少软件工程师对日志并不熟悉。我希望能够改变这一点。在这篇文章中,我会带你学习日志所必须知道的一切,包括什么是日志以及如何使用日志进行数据集成,实时数据处理和系统构建。
第一部分:什么是日志
日志可能是最简单的存储抽象形式。它只支持添加式写入,完全时间有序。
日志被添加到图片的末尾,并且按照从左往右的顺序读取。每一条日志有唯一的顺序的日志编号。
日志记录的顺序隐藏了时间的属性,因为左边的日志默认要“老于”右边的日志。每条日志的编号可以视作日志的时间戳。从时间的角度来形容日志编号乍一看有点奇怪,但是它使得日志和任何物理时钟节耦。在分布式系统中,该属性是至关重要的。
日志条目的内容和格式在本文汇总并不重要。同样,我们也不能无限的往日志中新增内容,因为它会占据所有的空间。稍后我会回来讨论这一点。
所以,日志并非完全不同与文件或是表格。文件是一组二进制编码的,而表格是一组记录,而日志就是一种内容按照时间排序的表格或是文件。
至此你可能会想这么简单的东西有啥值得说的?一个只支持添加式的记录和文件系统有关系?答案是日志有一个特殊的目的:它们记录发生的事件及其发生的时间。从各个角度看来,这都是分布式文件系统中很多问题的本质所在。
但在深入解释之前,让我先将一些容易混淆的概念解释一下。每个开发者都熟悉日志的另一个维度的定义:即记录一个应用的无结构的错误信息或是错误路径,并使用 log4j 或是 syslog 写入本地文件系统中。应用日志是本文中描述的日志概念的降级形式。二者之间最大的区别在于文本日志主要是为了开发者来阅读,而本文描述的系统 / 数据日志主要是由程序来访问并解析。
(事实上,如果仔细想一想,人肉阅读单机上的日志的想法并不高明。当很多服务参与进来,这种方式很快变得难以管理,而日志就变成了打印入参出参以了解跨机器调用行为的工具。此时文本式的日志并不是最合适的形式)
数据库中的日志
我不知道日志的概念起源于何处 – 也许就和二分查找法一样,因为过于简单而并没有被发明者视为是一个创举。早在 IBM 的 R 系统中它就已经存在了。数据库中使用日志实现在应用崩溃时能够同步各类数据和索引。为了保证操作的原子性和持久性,数据库会在执行具体的变更之前,将需要修改的数据先写入日志中。日志记录了发生的事件,而每个表格 / 索引将这些历史变更映射为当前的数据结构或索引。因为日志的实时持久化的,因此它被视为在系统崩溃时对其它持久化数据结构进行恢复的权威标准。
随着时间流失,日志从最开始 ACID 的事务实现方式,进化成了数据库之间数据备饭的工具。事实证明,对数据库执行的一系列变更记录,正好可以用于同步远程的备份数据库。Oracle,MySQL 和 PostgreSQL 使用日志传输协议将部分日志发送到备份数据库。
息传输,数据流和实时数据处理场景是非常合适的。
分布式系统中的日志
日志解决的两个问题:顺序变化和分布式数据,在分布式数据系统中更为重要。对更新操作的顺序达成一致是这些系统进行设计时的核心问题。
这种在分布式系统中以日志为中心的思想来源于一个简单的观察,我将其称为状态机备份理论(State Machine Replication Principle):
如果两个相同的,确定的进程以同一个状态开始,并以同样的顺序传入相同的输入,那么它们会输出相同的结果并以相同的状态结束。
这看上去有点晦涩,让我们深入理解其含义。
确定性 是指该过程和时间无关,并且不会让任何外带输入影响其结果。比如一个程序的输出,受特定的线程执行顺序,或是调用获取当前时间 API 或是其它非可重复的操作的影响,则将其视为非确定性的。
进程的 状态 是指在程序执行完毕后,任何在内存中或是磁盘上存留下来的数据。
需要注意按照相同的顺序传入相同的输入 – 这是日志参与的部分。这是一个非常启发性的理念:如果你向两段确定性的代码片段传入相同的输入日志,它们会产生相同的输出。
分布式计算的应用显而易见。你可以将原先使用多台机器做同一件事情,转化为实现一个分布式一致性日志来给这些进程相同的输入流。这里使用日志的目的是将输入流中的所有不确定性排除掉,来确保每个备份可以使用这个输入流进行同步。
一旦你理解了这一点,这个理念就不再复杂了:它等于是说确定性的处理得出的结论也是确定的。尽管如此,我认为它是一个更加通用的分布式系统设计的工具。
这个方法的优点之一是用来索引日志的时间戳同样可以用来描述备份的状态 – 你可以用该备份处理过的最大日志编号来描述该备份的状态。这个时间戳结合日志可以唯一的描述整个备份当前所处的状态。
在系统中使用这个思想的方式有很多,具体采用哪种方式取决于将什么内容写入了日志。比如,我们可以将调用服务的请求,或是服务在响应请求后状态的变化,或是它执行的转换指令写入日志。理论上来说,我们甚至可以将每个备份执行的机器指令以及或是方法与参数名写入日志中。只要两个进程用同样的方式处理同一组输入,这些进程就可以在不同的备份中保持一致的状态。
不同的人群会用不同的方式描述日志。数据库开发者会区分物理日志和逻辑日志。物理日志是指记录每一行内容变更的日志。而逻辑日志是指不仅记录内容的变更,还会记录导致内容变更的 SQL 日志(插入,更新和删除语句)。
分布式系统语义下,通常区分两种处理和备份的通用方法。状态机模型 通常指我们将一组输入请求写入日志,而每个备份处理这些请求。对于这个模型进行略微修改的一个模型称为 基本备份模型,它将一个备份选为主机器,并且允许这个主机器按照请求到达的顺序逐个处理请求,并将处理后的状态变更写入日志中。其它的备份同步主机器的状态变更,这样当主机器崩溃后,其它机器可以重新选举出新的主机器并替代它。
要了解二者之间的区别,先看一个简单问题。假设现在有一个多备份的“计算服务”,它保存了一个数字作为其状态(该数字初始化为 0),并且对该值执行加减乘除操作。状态机模型会将所有的变化操作写入日志,比如“+1”,“*2”等。而基本备份模型则会让一个主节点执行计算操作,并记录计算后的结果,如“1”,“3”,“6“。这个例子也证明了为什么顺序性是在备份之间保持一致性的关键:加法和乘法操作的乱序会产生不同的结果。
其实我觉得,由于历史原因,我们关于日志产生了一些偏见,也许是因为最近几年分布式计算的理论超越了其实际应用。在现实世界中,一致性问题被看的简单了。计算机系统很少会对单个值执行一致性计算,它们通常需要处理一系列请求。因此日志,相比于简单的单值寄存器,是更常见的抽象。
不仅如此,这些分布式一致性算法,如 Paxos,Raft 等,并不太关注底层的日志抽象系统。因此,我们会将日志视为一个商业化的构建模块,而不会关心其具体的实现细节。就像是当我们讨论哈希表时,不会关心我们是通过线性探测法或是其它的方式来进行冲突解决的这种细节。。日志就像是一个通用的商业化接口,它底下隐藏了很多的算法和实现,来确保提供最优性能。
变更日志 101: 表格和事件的互补性
再回到数据库。关于变更日志和表格有一个很特别的互补性。日志就像是银行处理的所有的借款和还款的记录,而表格则记录了当前账户的余额。如果你记录了变更日志,就可以通过执行这些变更来创造出捕获当前状态的表格。这个表格会记录每个健的最终状态(或者说日志的一个时刻)。因此日志是更基础的数据结构,除了来创造最终表格,还可以通过它创造各种衍生的表格。
这个过程也可以反过来执行:如果你有一个需要执行更新操作的表格,你可记录这些变更并发布一个包含所有变更的变更日志给当前的表格。这个变更日志就是需要提供给需要提供给近实时同步的备份的数据信息。因此,从这个角度来看,二者是互补的:表格捕获数据的当前状态,而日志捕获变更。日志的魅力在于,如果它捕获了完整的变更日志,它不仅能够生成最终版本的表格,还能够生成任意其它版本的表格。它是对表格过去的每一个状态的有效备份。
这可能会让你想起源代码版本控制。版本控制和数据库之间存在紧密的关联。版本控制和分布式数据库系统解决的问题很类似:管理状态上分布的,并发的变化。版本控制系统通常将一组修补变更进行建模,本质上也是一条日志。开发者与代码的一个快照进行交互,而快照就类似于表格。和别的分布式有状态的系统一样,版本控制中的备份通过日志来实现:当你更新代码仓库时,其实是将所有代码变更拉取下来,并将它们依次执行到当前的快照上。
接下来的内容
在本文接下来的内容中,我会试着以超出分布式计算或抽象的分布式计算模型之外的领域来说明日志的好处。包括:
- 数据集成:使各个组织的数据可以在各个存储介质和处理系统中快速访问
- 实时数据处理:基于数据流的计算
- 分布式系统设计:如何通过日志中心的设计思想简化系统设计
这些应用都围绕着将日志作为独立服务的思想来实现。
在每个场景下,日志的易用性均来自于其提供的简单功能:提供一个持久的,可重现的历史记录。令人惊讶的是,上述这些问题的核心都在于支持每台机器需要能够按照自己的处理速度来处理确定的历史记录。
第二部分:数据集成
数据集成是指支持所有的服务和系统均能够访问该组织机构的所有数据。
数据集成并非通用概念,但是我找不到更加合适的术语。而另一个相对而言更加具有识别度的概念 ETL 只包含数据集成中的一部分操作 – 生成一个关联型的数据仓库。对于数据集成,你并没有听到太多关于大数据概念的令人窒息的炒作,但尽管如此,我相信“使数据可用”这个平凡的问题是任何一个组织机构可以关注的更有价值的事情之一。
数据的有效使用某种程度上遵循了马斯洛的需求层级模型。在金字塔的底端涉及了捕获所有相关的数据,并能够将其放在一个可用的访问环境中(比如一个实时访问系统或是文本文件或是 python 脚本)。这个数据需要用通用的方式建模,使其易于读取和处理。当这些通用的数据捕获能力建设好之后,才能够通过工具用各种方式处理这些数据 – 如 MapReduce,实时查询系统等。
需要注意一点:没有一个可靠且完整的数据流的话,Hadoop 集群无异于一个昂贵且难以使用的工具。只有当数据和处理可用后,开发者才能将关注点移动到如何建好数据模型和易于理解的语境。最后,关注点可以移动到更复杂的处理,比如可视化,报表和使用算法进行处理与预测。
从我的过往经验看来,很多公司在基础的数据收集上存在很大的缺陷。它们缺乏可靠的完整的数据流,却想要直接实施高端的数据建模技巧。这完全是一种倒退。
所以问题在于,我们如何能够横跨公司的所有数据系统,构建一个可靠的数据流?
数据集成的两个复杂点
事件数据井喷
数据的第一个趋势是时间数据的增加。事件数据记录了发生了什么事情,而非事情本分。在 web 系统中,这可以是用户的行为日志,也可使机器级别的统计数据,用来监控数据中心的可靠性。人们倾向于将这些称为日志数据,因为它们通常被写入应用日志中,但它会混淆形式和功能。其实这些数据在现代互联网处于核心地位。毕竟谷歌的财富,就是通过点击和事件之间的关联创造的。
而这并不只局限于互联网公司,只是因为互联网公司已经电子化,因此这些数据更好管理。金融数据一直以来也是事件为中心的。RFID 将这种跟踪添加到物理对象中。我认为,随着传统商业的数字化,这一趋势将继续下去。
这种类型的事件数据记录发生了什么,并且通常比传统的数据库的数据量大几个量级。这意味着处理上带来了巨大的挑战。
特殊的数据系统的爆发式产生
第二个趋势是面向特殊场景的数据系统逐渐流行起来并且近五年来被广泛使用。这些数据系统包括 OLAP,Elastic Search,批处理系统 Hadoop,图形系统 graphlab 等等。
各种类型数据的组合以及将这些数据流入各种类型的系统给数据集成带来了巨大的问题。
日志结构的数据流
日志是处理系统间数据流的通用数据结构。它的思路很简单:将公司的所有数据收集起来放入日志中心,并提供实时的数据订阅。
每一个逻辑数据源可以视为独立的日志。数据源可以是应用将事件(如点击或页面浏览)打印的日志,或是数据库表格的变更操作日志。每一个订阅系统尽可能快速的阅读这些日志,将这些日志应用于自己的数据上,并继续推进日志的阅读。订阅者可以是任何数据系统 – 缓存,Hadoop,其他地址的数据库,搜索引擎等。
比如,日志的概念使得每一个订阅者执行的变更进度可以通过逻辑时钟度量。它通过每个订阅者当前订阅的时间节点进度,简化了不同订阅者彼此之间的状态比较。
想象一个简单的场景,现在有一个数据库和一组缓存服务器。日志提供了一种同步更新到所有这些系统的方式,并能够告知当前系统的同步进度。假设我们写了一条日志 X 并且需要从缓存执行一个读操作。假设我们想要确保不会读到过期数据,只需要确保不要从任何还未同步日志 X 的缓存服务器读取即可。
日志还可以充当实现数据异步消费的缓存。这一点很重要,尤其是在存在多个消费速率各不相同的消费者的场景下。这意味着订阅系统崩溃或是停服维护后,重启时可以立刻恢复状态。批处理系统如 Haddop 或是数据仓库可能会以小时级或是天级来进行消费,而实时查询系统可能需要以秒级消费。数据源和日志都无需感知各种类型的目标数据系统,因此各个消费系统可以透明的从流水线上插入或移除。
最重要的是,每一个目标系统都只需要了解日志而无需感知日志源系统的任何细节。消费系统不需要知道谁究竟来自关系型数据库,一个新型的键值数据库,还是实时数据流。这一点看上去微不足道,实则至关重要。
这里我使用日志的概念而非消息系统或是消费订阅,是因为它相对而言在语义上更加具体,并且更详细地描述了在实际实现中支持数据复制所需的内容。我发现发布订阅这个词只能表达出非直接的消息路由。如果比较任意两个发布订阅的消息系统,会发现它们有完全不同的实现机制,而且大多数的模型在这个领域中并不适用。你可以将日志视为一种消息系统,它实现了持久性和强顺序性。在分布式系统中,这种通信模型有时被称为原子广播。
身在领英
使用日志作为数据流的结构已经在领英存在很久了。早期我们开发的一个基础设施就是 databus,它能够在 Oracle 数据表之上建立日志缓存抽象,从而将这些数据库变更同步给社交图库和搜索引擎。
对此我再提供一些上下文。我最初参与这个项目中是在 2008 年去构建键值数据库,我的下一个项目是试着去搭建一个 Hadoop 集群,并将一些推荐进程迁移至其上。对此我们都没什么经验,因此我们先用了几周时间来实现数据的写入和输出,再用剩余的时间实现各种高贵的预测算法。
最初我们的设想是将数据从现有的 Oracle 数据仓库中剥离出来。接着就发现快速将数据从 Oracle 中提取出来是一门黑科技。更糟糕的是,Oracle 的数据仓库的处理过程并不适合我们为 Hadoop 预设的批处理过程 – 它们大多是不可逆且特定于某种报表。于是我们不再使用数据仓库,而是直接从源数据库和日志文件来获取数据。最终,我们实现了其它的数据流水线将数据导入我们的键值数据库。
这种平平无奇的数据复制最终成为项目开发的主要工作。更糟糕的是,任何一个流水线中出现问题,Hadoop 系统很大程度上就无用了,在脏数据上运行高端的算法只会产生更脏的数据。
虽然我们已经尽可能用通用的方式来构建,但是每当一个新的数据源出现时,都需要配置自定义的配置。它还成了大量错误和异常的来源。我们基于 Hadoop 实现的特性逐渐流行起来,并吸引了很多人的兴趣。每个业务方都有一些想要与这个系统集成的系统,以及一组关心的数据流。
慢慢的一些事情渐渐清晰起来。
首先,我们之前构建的流水线,虽然有一点凌乱,却是非常重要的。仅仅是将数据在另一个数据处理系统中可见就解锁了无限的可能。很多新的产品和分析都是基于将原来分散在多个特殊系统中的数据片段归拢在一起。
其次,可靠的数据流需要数据管道的更多支持。如果我们能够捕获所需的所有数据结构,就能让 Hadoop 数据流彻底自动化,从而在新增数据源或是处理结构变化时无需手动执行这些操作,它们会自动为新的数据源创建合适的列。
第三点,我们的数据覆盖率很低。也就是说,如果你观察在 Hadoop 上保存的 LinkedIn 数据比例,它还是相当不完整的。而要想完善数据并不容易,考虑到为每一个新的数据源执行定制化所需要的成本。
我们为每个数据源和数据目标构建自定义数据加载的方式显然是不可行的。我们有几十个数据系统和数据仓库。连接所有这些将导致在每对系统之间建立自定义管道,如下所示:
注意,数据通常是双向流动的,因为许多系统(数据库、Hadoop)都是数据传传输源和目的地。这意味着我们最终将为每个系统构建两个管道:一个用于获取数据,另一个用于获取数据。
很明显,这需要一大堆人来建设,而且永远无法运作。当我们接近完全连通时,我们最终会得到类似于 O(N2)的管道。
相反,我们需要这样的通用工具:
As much as possible, we needed to isolate each consumer from the source of the data. They should ideally integrate with just a single data repository that would give them access to everything.
我们需要尽可能地将每个消费者与数据源隔离开来。理想情况下,它们应该只与一个单一的数据存储库集成,支持它们访问所有内容。
其思想是,添加一个新的数据系统(无论是数据源还是数据目的地)只负责集成工作,将每个数据源连接到单个管道,而不是每个数据使用者。
这一经验使我专注于构建 Kafka,将我们在消息传递系统中看到的内容与数据库和分布式系统内部流行的日志概念结合起来。我们希望它首先作为所有活动数据的中心管道,并最终用于许多其他用途,包括从 Hadoop 部署数据、监视数据等。
很长一段时间以来,Kafka 作为一种基础设施产品(既不是数据库,也不是日志文件收集系统,也不是传统的消息传递系统)有点独特(有些人会说很奇怪)。但最近亚马逊提供了一种与 Kafka 非常相似的服务,叫做 kinisis。这种相似点可以体现在分区的处理方式、数据的保留,以及 Kafka API 中高层和底层消费者之间相当奇怪的分离。我对此很高兴。创建了一个良好的基础架构抽象的标志是,AWS 将其作为服务提供!他们对此的设想似乎与我所描述的完全相似:它是连接所有分布式系统 DynamoDB、RedShift、S3 等的管道,也是使用 EC2 进行分布式流处理的基础。