上一次这样高强度的读一篇英文文献还是写小论文的时候,英文版断断续续读了近两个礼拜,收获良多。恰好最近复习找工作云笔记记了一堆也从不看,还是自己动手写写博客记忆更深点吧。
ps.感觉Jay Kreps老大也太能(hua)侃(lao)了吧
附英文版链接:The Log: What every software engineer should know about real-time data’s unifying abstraction
序
当庞大的集中式数据库遇到瓶颈,有效的方法是向专业的分布式系统过渡。而在这个过程中,需要完成构建、部署并运行分布式图形数据库、分布式搜索引擎、Hadoop平台、以及键值数据库等步骤。以上所有的概念都离不开日志(the log,也称之为写前日志或提交日志或事务日志),它是分布式数据系统和实时应用程序架构的核心。如果不了解日志,就不能完全理解数据库、NoSQL存储、键值存储、备份、paxos、hadoop、版本控制、以及几乎所有的软件系统。
下文将介绍有关日志的所有内容,包括日志是什么、如何使用日志进行数据集成、实时处理和系统构建。
第一章 什么是日志?
日志是最简单的存储抽象,它是严格按时间排序的一串记录。如下图:
记录每次被添加在日志的末尾,读取从左至右进行。每条记录被分配一个唯一的日志记录编号(编号是连续的)。记录的顺序表明了一个“时间”的概念,理解为日志左边的记录比右边的记录要老一些,日志记录编号可以视为“时间戳”。将记录的顺序描述为时间概念有助于将记录与任意特定的物理时钟解耦,这对于分布式系统非常重要。另外需要注意,不能只添加记录,因为空间终将耗尽。
日志和文件、表没有太大的不同:
- 文件是一组字节,表是一组记录;
- 日志可以视为一个文件或表,只是其中的记录按时间排序。
值得讨论这样一个简单的事情吗?以何种方式将一组记录与数据系统关联起来呢?答案是:日志记录何事在何时发生。这是分布式数据系统中许多问题的核心。
在深入讲解之前,需要阐明一些概念。
- 应用程序日志(“application logs”):非结构化的错误信息或跟踪信息,一般供程序员看的日志,应用程序可以使用syslog或log4j将其写入本地文件。
- 本文讨论的日志(“journal”、”data logs”)主要是供编程访问的。
- 两者是不同的,应用程序日志记录是日志的退化版本。
数据库中的日志
日志的出现可能像二分查找一样太过简单以至于发明者都没有意识到这是一项发明。日志的概念早在IBM的System R中就出现了,在数据库中的用途是在系统崩溃时维持不同数据结构和索引之间的同步,为了使之具有原子性和持久性,数据库需要在修改数据结构之前,记录将要修改的信息。日志记录发生的所有事,所有的表或索引都是这些历史信息新的映射。由于日志被立即持久化,因此在系统崩溃时,它被视为恢复所有其他持久化结构的权威源。
日志的用途从一种ACID的实现细节发展为一种数据库之间的数据拷贝方法。事实证明,一系列发生在数据库中的变化信息是维护远程备份数据库同步所必需的。Oracle、MySQL、PostgreSQL,都包括日志传输协议,将log的一部分发送到用于备份的从数据库(Slave)。在Oracle的XStream和GoldenGate系统中已经将日志作为一种通用日志订阅机制,用以面向非Oracle的数据订阅者。相似的组件也是MySQL和PostgreSQL许多数据架构的核心。
由于上述原因,机器可读日志的概念曾一度局限于数据库内部。将日志作为一种数据订阅机制的用法是偶然产生的,但是这种抽象很快就用于消息传送、数据流以及实时数据处理中。
分布式系统中的日志
日志解决了分布式数据系统中两个非常重要的问题
- 将改变按时间排序
- 分发数据
在分布式系统中,核心的设计问题是要不要同意一个更新操作的有序化(不同意就要应对相应的副作用)。
以日志为中心的分布式系统处理方式来自于一个简单的观察,称之为状态机复制原则:如果有两个确定的、相同的进程,从相同的状态开始执行,以相同的顺序获得相同的输入,这两个进程将产生相同的输出,并以相同的状态结束。
确定性是指:进程的执行是不依赖时间的,也不会让其他输入影响最终的结果。
通过非确定性的例子来理解:
- 程序的输出受线程执行的特定顺序的影响
- 调用getTimeOfDay()方法
- 其他不能重复的步骤
进程的状态是在处理过程结束时,无论是内存还是硬盘上保留的数据。以相同的顺序获得相同的输入,将会产生相同的结果,这是日志的特性,如果对两个确定性的代码段输入同一日志,它们将产生相同的输出。在分布式计算的应用中,可以将问题 “让所有机器做相同的事”规约为问题“实现分布式的、一致性的日志系统,作为处理进程的输入”。此日志的目的是将所有非确定性排除在输入流之外,以确保每个副本处理同一输入时保持同步。这一原则并无深奥之处,可以理解为“确定性的进程的结果是确定性的”。同时,它是分布式系统设计中非常重要的通用原则之一。
这种方法的一个优点是,索引日志的时间戳现在作为副本状态的时钟。可以通过副本中处理的最大日志记录的时间戳描述该副本,结合时间戳和日志,可以捕获副本中的所有状态。
在系统中应用该原则的方法很多,主要取决于日志中的内容。例如,可以在日志中记录:
- 服务请求信息
- 回应服务请求的状态改变信息
- 服务执行的转换指令等
理论上,可以在每个副本中记录一系列机器指令,或调用的方法名称及参数,只要两个进程以相同的方式执行,进程就能保证副本之间的一致性。
数据库工程师通常将日志区分为物理日志和逻辑日志。物理日志记录发生变化的内容,逻辑日志记录记录导致变化发生的SQL命令(插入、更新、删除语句)。
分布式系统通常将处理和复制方法分为两种:
- State machine mdel(active-active)
- Primary-back model (active – passive)
状态机模型中,日志记录所有请求和副本对请求的处理;
主-从模型中,选择一个副本作为领导者,允许领导者按照请求到达的顺序处理请求,并在日志中记录其状态的改变,另一个副本作为从节点,应用领导者日志中的状态改变记录,保证了副本间的同步,同时准备在领导者崩溃时接替成为领导者。
一个栗子:
现在副本提供一个简单的算术服务,将初始化一个数字(为0),并对该数字进行加法和乘法运算。active-active方法可能会在日志中记录所有的运算,比如“+1”、“*2”等。每个副本都应用这些转换,因此得到相同的结果集。active – passive方法将会有一个领导者副本执行所有运算,并将每次运算得到的结果记录在日志中,比如“1,3,6”。乘法和加法执行顺序不同的话结果也将不同,这清楚地说明了为什么顺序是确保副本之间一致性的关键。
日志记录了一系列关于“下一个值是什么”的决定。分布式日志可以视为以一致性问题建模的数据结构,广泛应用于Paxos算法家族,以及ZAB、RAFT、Viewstamped Replication等协议中。
Changelog 101:Tables and Events are Dual
从数据库角度讲,一组记录更改的日志和表是对偶的。日志类似于所有信用、借贷和银行处理过程的列表;表是当前账户的余额。如果有一个更改日志,可以应用其中记录的更改来创建当前状态的表,此表将记录每个键的最新状态。可以认为,日志是更基础的数据结构,除了创建原始表之外,还可以通过转换创建许多派生表(也可以是非关系型存储系统中有key的记录)。相反,如果有一些表执行更新操作,可以将这些更改记录到日志中。这个日志就是支持实时拷贝的关键。在这个意义上可以看出表和事件是对偶的,表存储更改后的数据,日志捕捉更改的操作。日志的神奇之处在于,一个完整的日志,不仅保存表的最终版本的内容,而且允许重建表可能存在的其他版本,这实际上是对表的每次状态改变的备份。
从而想到了源代码版本控制,数据库与其有着密不可分的联系,版本控制需要解决一个分布式数据系统已经解决的问题-管理分布式的、并发的状态变化。版本控制系统通常对一系列补丁进行建模, 这实际上就是日志。可以和一个被签出的当前代码的快照进行交互,这份快照就相当于数据库中的表。可以注意到,在版本控制系统中,就像在其他分布式状态系统一样,拷贝是基于日志完成的。当更新时,只是将反映版本变化的补丁拉取下来,并应用于当前的快照中。
What’s next
在剩下的部分中,将尝试给出一个日志优于分布式计算或抽象分布式计算模型内部组件的特点。包括:
- 数据集成-使整个组织的数据在所有的存储和处理系统中都很容易获得;
- 实时数据处理-计算派生的数据流;
- 分布式系统设计-如何通过简化以逻辑为中心的设计来简化实用系统。
通过将日志作为独立服务的思想解决了这些问题。
在每种情况下,日志的可用性来自于它提供的简单函数:生产持久的、可重复播放的历史记录。这些问题的核心是让众多机器拥有以确定性的方式和各自的速率重播历史的能力。
第二章 数据集成
数据集成的含义是将组织所有服务和系统中的数据变得可用。更常见的术语ETL通常只涵盖数据集成的有限部分,包括装载一个关系型数据仓库。但是本文中的数据集成涵盖实时系统和处理流,可以认为是ETL泛化而来的。
“让数据可用”这个平凡的问题是一个组织可以关注的最有价值的事情之一。
数据流的有效使用,恰好遵循了马斯洛的需求层次理论。金字塔的底层是获取相关数据,将其放进应用系统中(无论是实时查询系统、文本文件或python脚本)。获取的数据需要以统一的格式建模,以便于阅读和处理。一旦这些这些要求得到满足,就可以用多种方式处理这些数据,例如Map-Reduce、实时查询系统等。需要注意:如果没有可靠的、完整的数据流,Hadoop集群仅仅是一个昂贵且难以组装的空间加热器。相反,一旦数据和处理可用,就可以将关注点转移到更好的数据模型和一致性的、更易被理解的语义等更精细的问题上。最后,注意力就可以转移到更为复杂的可视化、报表、算法和预测上去。大多数组织在获取数据上都有很大的漏洞(缺乏完整可靠的数据流),但是他们想直接跃进到高级数据建模技术上,这是行不通的。
所以问题是,我们如何在一个组织的数据系统中建立可靠的数据流?
数据集成的两个复杂性
两个趋势使得数据集成变得困难
- 事件
- 专业数据系统的增长
第一个趋势是事件数据的增长,事件数据记录事件是怎么发生的,而不是事件本身。在web系统中,这意味着记录用户的活动日志,同时还要机器级别的事件和统计数据,用以可靠的操作、监控数据中心。因为它总被写入到应用程序日志中,人们把这种记录称为“日志数据”,但这其实混淆了它的功能。这些数据是现代网络的核心,毕竟谷歌的财富是由事件(用户鼠标点击和体验之上的相关管道)创造的。这种类型的事件数据记录发生了什么,往往要比传统数据库的使用空间大几个量级,这给数据处理带来个极大的挑战。
第二个趋势是专业数据系统的爆炸式增长,这些系统变得流行,并且通常是免费的,这些专业的数据系统存在于OLAP、搜索、简单在线存储、批处理和图形分析系统中。
显然,如何将全部更多更多样的数据集成到一起提供给更多的系统成了一个大问题。
日志结构化数据流
日志是一个处理系统间数据流的自然数据结构,秘诀是:把所有组织的数据放在一个中心日志中,实时订阅。每个逻辑上的数据源可以根据自己的日志建模。一个数据源可以是记录事件的应用程序,比如点击或浏览页面,或是一个接受修改信息的数据库表。每个订阅的系统尽可能快地从该日志中读取数据,将每个新纪录的修改应用到系统的存储中,并在日志中提升它的偏移量。订阅者可以是任何的数据系统,例如缓存、Hadoop、另一个站点的数据库、或者搜索引擎。
日志实际上为每个用户可以测量的更改提供了一种逻辑时钟,因为每个用户都有一个“时间点”,这使得不同用户系统之间的状态推理变得简单。这些订阅者在日志中的读取偏移不同且相互独立,这种偏移就像一个时间意义上的“时刻”一样。
举个栗子:有一个数据库和一个缓存服务器集合,日志提供了一种方法来同步所有系统的更新和推理每个系统现在所处的时间点。假设我们写入了一条日志记录X,然后需要从缓存服务器中读取数据,如果想保证不看到过时的数据,只需要确保不从任何没有到达记录X的缓存服务器中读取数据即可。
日志还充当了一个缓存区,使数据生产和数据消耗异步,在有多个订阅者以不同的速度消费时,订阅系统可以崩溃或停机维护,并在恢复时赶上进度:订阅系统使用自己可以控制的速度消费。像Hadoop或数据仓库这样的批处理系统只能按小时或按天消费数据的,实时查询系统通常需要秒级消费数据。而数据源和日志,对消费数据的订阅者一无所知,所以需要在管道中做到添加/移除订阅者。更重要的是订阅系统只需要知道日志,而不需要了解其所消费的数据来源。消费者不需要关心数据来自于RDBMS还是一种新型的键值存储仓库。
之所以讨论“日志”而不是“消息传递系统”或“pub sub”,是因为日志更具体的描述了语义,更详细的描述了在支持数据备份的实际应用中需要的是什么,“消息系统”更重要的在于重定向消息(简介寻址)。可以将日志视为一种具有持久性保证和强排序语义的消息传递系统。在分布式系统中,在通信系统称之为原子广播。
在领英(At Linkedin)
当Linkedin从一个集中式的关系数据库迁移到分布式系统的集合上时,有许多的数据集成问题出现。
主要的数据系统每一个都是专业的分布式系统,在其领域内提供高级服务,包括:
- Search(搜索)
- Social Graph(社交网络)
- Voldemort(键值存储)
- Espresso(文件存储)
- Recommendation engine(推荐引擎)
- OLAP query engine(查询引擎)
- Hadoop
- Terradata
- Ingraphs(监视和度量服务)
(这段都是Jay Kreps老大在Linkedin的工作经历,大致介绍下)
- 最早开发的基础设施是一个名为databus的服务,为早期的Oracle表提供一个日志缓存抽象,用于订阅数据库更改,并可以提供社交图谱和搜索索引。
- 2008年,Linkedin已经发布了自己的键值存储系统,接下来的项目是一个Hadoop平台的设置,并将我们的一些推荐进程迁移到那里。因为在这个领域缺乏经验,通过几周的时间获取和输出数据,计划剩下的时间用于实现各种花哨的预测算法,然而,一段艰苦跋涉开始了。
- 最初计划从现有的Oracle数据仓库中提取数据,经过一段时间有两个发现。第一个发现是从Oracle中快速获取数据是一个噩梦;第二个发现更糟糕,数据库仓的处理不适用于Hadoop的批处理,导致不能输出预期的结果,而大部分的Hadoop批处理执行后数据是不可逆的,特别是在报告完成之后。最终,采用回避数据仓库,直接访问源数据库和日志的方法,实现了一条管道用于键值存储中。
- 这种普通的数据复制最终成为原始开发的主要内容之一,Hadoop系统在任何一个管道出现问题的时候基本都是无用的,因为在糟糕的数据上运行花哨的算法只会产生更多糟糕的数据。
- 尽管团队构建的东西通用性很好,但是每个新的数据源都需要自定义配置,这是大量错误和失败的根源。
- 在Hadoop上实现的站点特性变得流行起来,大量的工程师想要加入进来,每个用户都有一个想要集成的系统列表,以及一长串的数据提要。
- Jay哥开始认识到,创建的管道虽然有点粗糙,但实际上有非常高的价值。即使是解决了数据在hadoop中的问题,也解锁了很多的可能性。以前难做的计算开始变为可能。新的产品和分析,都需要将其它系统中的数据块组合在一起。第二,可靠的数据加载需要数据管道的深度支持,如果捕获了需要的结构,可以让Hadoop数据装载完全自动化,不需要手动添加新数据源或人工修改数据的模式,数据就会神奇地出现在HDFS中,新的数据源添加后,Hive的表就会用合适的列自动化地、自使用地生成。第三,数据覆盖率仍然很低,完成每个新数据源所需要的工作量,不是很容易完成。
- 为每个数据源和目的地构建自定义数据负载的方式显然是不可行的,我们有几十个数据系统和数据仓库,链接这些将会导致在每一对系统之间建立自定义管道之类的东西:
这种方法中的数据会在两个方向上流动,许多数据库既是数据传输源又是传输目的地,最终会建立两个系统,一个获取数据,一个输出数据,最终的管道数量会变为O(n^2)。所以,需要这样的模型:
需要尽可能的将每个消费者与数据源隔离,理想的情况下,这些消费者只和单一数据存储仓库交互,而这个数据存储仓库可以提供访问任意数据的能力。 - 将数据库和日志结合起来就是构建Kafka的基础。
日志与ETL、数据仓库的关系
数据仓库应该是干净的、集成的、结构化的数据存储库,用以支持分析。这是个好主意。对于那些不知情的人来说,数据仓库方法需要定期地从源数据库中提取数据,将其转换成某种可以理解的形式,并加载到一个中央数据仓库中。一个包含所有数据的干净副本对于数据密集型的分析和处理来说是非常宝贵的资产,但是获得这些数据的机制有点过时,关键是将干净集成的数据耦合到数据仓库可能就会出现问题。
数据仓库是一个批处理查询结构,它非常适合服务于报表和专业分析,特别是当查询涉及计数、聚合和过滤操作时。但这意味着实时处理、搜索索引、实时监控等系统的数据是不可用的。
ETL实际上做了两件事。
- 数据提取和清理,删除数据与特定系统的联系
- 重构数据,供数据仓库查询使用
例如,为了适应关系数据库,数据被迫重构成星型或雪花结构,或被分解成高性能的列格式等。
把这两件事混淆会出现问题,因为对于低延迟处理、索引存储和其他实时系统,干净、集成的数据存储库应该实时可用。
数据仓库团队负责收集和清理其他团队生成的所有数据。数据生产者往往不清楚数据仓库中的数据处理方式,而创建出大量难以提取和转换的数据。同时,各个团队的步调很难保持一致,因此数据覆盖率总是很低,数据流很脆弱,难以应对快速变化。
所以,更好的解决办法是,中心管道(即日志),并使用定义良好的API来添加数据。
提供干净、结构良好的数据源的责任在其生产者。这意味着,作为系统设计和实现的一部分,必须考虑如何将数据输出到一个结构良好的中心管道。数据仓库团队只从中心管道,即日志,加载结构化的数据源,并对系统进行特定的转换。
当考虑在传统数据仓库之外使用其他数据系统时,可扩展性变得尤为重要。例如,假设希望提供搜索功能,或希望提供实时趋势图,或实时警报,在这种情况下,传统的数据仓库,甚至是Hadoop集群都不合适。更糟的是,为支持数据库负载而构建的ETL管道很可能无法为扩展系统提供服务。相反,如果组织构建了统一的、结构化的数据源,那么让附加系统访问所有数据只需要一个集成管道连接到中心日志上。
最好的模型,是在数据发布之前数据发布者就对其进行了清理。这确保数据以规范的形式保存,并且不会在生成它的特定代码或维护它的存储系统中出现问题。这些细节最好由创建数据的团队来处理,因为只有他们最了解自己的数据,并且在此阶段执行的任何操作都应该是无损和可逆的。
任何一种可以实时完成的增值操作,都应该在原始日志发布后进行处理。这将对事件数据进行会话化,或者添加其他通用派生字段。原始日志仍然可用,但是这个实时处理产生一个包含增加数据的派生日志。
最后,只有特定于目标系统的聚合操作可以作为数据装载过程的一部分执行,比如数据转换为特定的星型或雪花模型,以便在数据仓库中进行分析和报告。因为这个阶段,就像传统的ETL过程一样,现在已经完成了一组更干净、更均匀的数据流,数据应该被简化。
日志文件与事件
以日志为核心的架构有附带好处:它支持解耦的、事件驱动的系统。
在web行业中,捕获活动数据的方法是将其记录到文本文件中,然后将其抽取到数据仓库或Hadoop集群中进行聚合和查询处理。这与之前讨论的数据仓库和ETL的问题相似:数据流与数据仓库高度耦合。
LinkedIn,以日志为中心的方式(Kafka)构建了事件数据处理系统。该系统已经定义了数百个事件类型,每个事件都捕获特定操作的独特属性。涵盖了从浏览页面、用户对广告的印象、搜索服务调用和应用程序异常等内容。
为了理解Kafka的优势,可以想象一个简单的事件——在页面上显示招聘信息。这个页面应该只负责显示信息所需的逻辑。然而,在动态网站中,这很容易被附加的与显示工作无关的逻辑所占据。例如,假设需要集成以下系统功能:
- 将这些数据发送到Hadoop和数据仓库以实现离线处理;
- 需要统计页面浏览次数,以确保浏览器没有尝试某种内容抓取;
- 将此页面的浏览信息聚合到招聘发布者的分析页面中;
- 记录下用户对该页面的浏览记录,以确保我们对用户的工作建议都是有价值的,用户当然不希望一次又一次地看到同一招聘信息。
- 推荐系统可能需要记录对此页面的浏览记录,以正确地计算该页面的流行度。
很快,显示一份招聘信息的简单逻辑变得相当复杂。当我们在移动端也增加了页面时,这个逻辑必须被迁移到移动端,复杂性再度增加。更糟的是,负责处理该页面的工程师需要了解许多其他系统及其特性,以确保它们被正确集成。
这只是问题的简化版本,在实际环境下复杂程度只高不低。
“事件驱动”提供了一种简化方法:
显示招聘信息的页面现在只显示招聘信息,并记录与之相关的因素,比如招聘的相关属性、浏览的用户以及其他有用的信息。其他的系统——推荐系统、安全系统、招聘信息分析系统和数据仓库——都只订阅该事件并独自进行处理。显示页面甚至不需要知道这些其他系统是什么,也不必因为添加新的数据使用者而更改原有逻辑。
构建可扩展的日志
隔离发布者和订阅者并不新鲜,但是要保证多个订阅者能够实时处理消息,同时保证扩展能力,对于日志系统来说,是一个挑战。如果不能构建一个快速、便宜和可扩展的结构,那么使用日志作为通用集成机制永远不可能是最好的选择。人们通常认为分布式日志系统是一种缓慢、开销巨大的抽象,通常只能处理一些类似Zookeeper可能适合处理的“元数据”。但是,在LinkedIn,现在每天有超过600亿个独特的信息通过Kafka写入数据仓库(如果算上数据中心的镜像的话,那就有几千亿条写入)。
在卡夫卡中使用了一些技术来支持大数据:
- 对日志进行分区
- 通过批量读取和写入来优化吞吐量
- 避免不必要的数据拷贝
为了提高日志的可扩展性,将日志分割成多个分区:
每个分区都是一个完全有序的日志,但是在分区之间没有全局的排序(除了一些在消息中可能包含的时钟之外)。将消息写入哪个分区是由写入者控制的,大多数用户选择按关键字(例如用户id)进行分区。日志允许在分区不协调的情况下进行扩展,并保证系统的吞吐量与Kafka集群的规模呈线性关系。
分区被复制到数量可修改的副本中,每个副本都有相同的分区日志副本。在任何时候,其中一个副本将作为领导者;如果领导者宕机,会有一个副本将接管成为领导者。
分区缺乏全局排序,但它不是一个主要的限制。实际上,与日志交互的通常是成百上千个不同的进程,因此,讨论它们的全局顺序没有意义。Kafka保证,发送者的数据发送顺序和分区的数据输出顺序保持一致。
日志,就像文件系统一样,可以优化线性读、写模式,将小型读写组合成更大、更高吞吐量的操作。Kafka积极地追求这种优化,在发送数据时,从客户端到服务器,服务器之间的复制、数据传输到消费者以及确认提交的数据时,都发生了批处理。
最后,Kafka使用一种简单的二进制格式,在内存日志、硬盘日志和网络数据传输之间进行维护和优化操作,比如零拷贝数据传输技术(zero-copy data transfer)。
这些优化的累积效应是,即使在内存不足时,也可以用硬盘或网络能提供的速率来写入和读取数据。
第三章 日志以及实时流处理
到目前为止,只描述了复制数据的高级方法,但是存储系统之间的交换字节并不是故事的结局。事实证明“日志”是“数据流”的另一个说法,日志是流处理的核心。
到底什么是流处理呢?
如果了解90年代末和21世纪初数据库文献或数据库产品,那么可能会将流处理与构建SQL引擎以实现事件驱动处理的工作相关联。
如果关注开源数据系统的爆炸式增长,可能会将流处理与这个领域内的一些系统相关联,例如Strom、Akka、S4和Samza。但大多数人将这些系统看作是异步消息处理系统,而不是与集群感知的RPC层不同的东西(实际上,在该领域,有些东西正是如此)。
这两种观点都是有局限的,流处理与SQL没有任何关系,也不仅仅局限于实时处理。没有内联关系系统是无法处理昨天或一个月前使用各种不同语言来计算的数据流的。
流处理应是更广泛的概念:用于持续数据处理的基础结构。其计算模型可以像MapReduce或者其他分布式处理框架一样,只要有能力保证结果是低延迟的。
真正驱动数据处理进行的是数据收集方法,批量收集的数据自然要进行批量处理。美国人口普查为批量数据收集提供了一个很好的例子,人口普查会周期性地启动,并通过让人们挨家挨户地走动来计数美国公民。这在1790年的人口普查刚开始时有一定意义,当时的数据收集本质上是面向批量的,它涉及计数执行和纸上的记录,将这批记录传输到一个中心位置,在那里人们把所有的计数都加起来。当你描述人口普查的过程时,人们马上就会问,为什么我们不存储出生和死亡记录,人口增长的数量是持续或按一定粒度增长的,通过基数减去每年出生和死亡的人数可以得到最新的人口数量。
这是一个极端的例子,但许多数据的传输过程仍然依赖于定期转储和批量传输。处理批量转储的唯一自然方法是批处理,但随着这些过程被连续的数据源所取代,会开始朝着持续处理转变,平滑地处理所需的处理资源并减少延迟。
例如,LinkedIn几乎没有批量数据,大部分数据要么是活动数据(不够多),要么是数据库的变化,两者都是持续发生的。实际上,任何业务,潜在的机制总是个连续的过程——正如杰克·鲍尔(美剧24小时男主- -)告诉我们的那样,事件是实时发生的。当数据被批量收集时,总会出现一些人工操作、缺乏数字化以及非数字过程的自动化遗留下来的历史遗留问题。当人类和机器进行数据传输时,传送和反应数据的速度非常慢,自动化的初始传递总是保持原始进程的形式,常常会持续很长时间。
在处理数据时,带了一个时间的概念,不需要对数据保持一个静态的快照,所以可以在用户自定义的频率之下,输出结果,而不必等数据集到达某种“结束”的状态。
在处理数据时,增加一个时间的概念,就不再需要保存静态的数据快照,这样就能以用户的步调而不是等待数据集的“结束”的方式处理数据。从这个意义上说,流处理是批处理的一种泛化,考虑到实时数据的流行,这是一个非常重要的泛化。
为什么传统的流处理视图是一个利基(大概是一片蓝海?)应用程序呢?最大的原因是缺乏实时的数据收集,这很可能导致商业流处理系统的失败。他们的客户仍然在为ETL和数据集成进行面向文件的、每天的批处理。构建流处理系统的公司想要向处理引擎提供实时数据流,但事实证明,很难真正拥有实时数据流。实际上,在LinkedIn,很早就有公司出售一个非常酷的流处理系统,但由于当时所有的数据都收集在按小时处理的文件中,能想到最好的解决方案是每小时向流系统传输数据,而这是一个相当普遍的问题。
事实证明,日志解决了流处理中一些最关键的技术问题,其中最重要的成果是解决了向实时多订阅用户服务器提供可用数据的问题。
数据流图
流处理中最有趣的地方在于,它扩展了关于数据源(feeds)这一概念。数据源、日志、事件以及数据记录,都是在执行各种应用程序时产生的。流处理允许我们处理从来自其他源的数据,这些数据和源数据对于消费者来说没有什么不同,但是这些数据可以封装任意的复杂性(对源数据进行一定处理后的派生数据)。
流处理任务从日志中读取内容,将处理后的内容写入日志或其他系统。用于输入和输出的日志将这些处理流程合并了到一个图中,即数据流图。实际上,可以将所有数据提取、转换和处理视为日志上写入的一系列记录。
一个流处理程序不需要有奇特的框架:它可以是任何从日志中读取和写入的进程,但是为了帮助管理代码,还需要提供额外的基础设施和支持。
引入日志的目的有两个:
- 保证数据集支持多订阅用户及有序;
- 日志可以作为应用程序的缓冲区。
如果以不同步的方式进行处理,数据生成任务可能比处理任务更快地产生数据。当这种情况发生时,处理必须阻塞、缓冲或删除数据。删除数据可能不是一个好的选择;阻塞可能导致整个数据流图停止工作;而日志作为一个非常大的缓冲区,允许进程重新启动或挂掉,同时不会影响数据流图的其他进程。当将数据流扩展到一个庞大的机构时,这种缓冲就显得尤为重要。在这种情况下,由许多不同的团队所做的工作正在进行处理,不能因为一个错误导致全局停止。
Strom和Samza都以这种方式构建,同时作为流处理引擎,还可以使用Kafka(或其他类似的)作为其日志系统。
基于状态的实时处理
很多实时流处理只是无状态的记录-时间的转换,但许多用例都需要在一定大小的窗口时间里进行复杂的计数、聚合或连接操作。例如,用户可能想要丰富一个事件流(比如单击鼠标流),将信息流加入到帐户数据库中。不可避免的是,这种处理需要维护数据的状态。问题在于,如果处理系统本身可以挂掉,那么如何能够正确地维护数据的状态呢?
最简单的方法是将状态保存在内存中,但是无法解决进程崩溃问题。如果只在窗口时间内维护状态,那么当进程挂掉,就可以返回到窗口的起点来重放。但是,如果窗口大小为一个小时,那么系统的开销会难以承受。
另一种方法是简单地将所有状态存在远程存储系统或数据库中,并通过网络连接到该存储区,这种方法的问题在于丢失了数据的局部性并产生大量的网络通信开销。
最好的方法是:采用一个流处理组件,将数据流转换为表以及处理这些表的容错机制。
流处理组件可以存储在本地的“表”或“索引”中维护状态,即bdb、leveldb、Lucene和fastbit。存储的内容是其输入流,它可以对该本地索引进行更改,以使其在发生崩溃和重新启动时恢复状态。这种机制,展示了一种通用的,可以存储为任意索引类型的,与输入流同时被分割(co-partitioned)的状态。
当进程失败时,可以从changelog中恢复索引。在这里,日志存储了一系列从本地状态转换的基于时间的增量记录。
这种方法有一种优雅的特性:处理组件的状态也可以保存为日志。由于这个状态本身是一个日志,其他处理进程可以订阅它,这一点非常有用。当将日志从数据库中取出来进行数据集成时,可以从数据库中提取一个变更日志,并通过不同的流处理组件以不同的形式对其进行索引,以加入到事件流中,实现了不同事件的关联。
日志合并
除非拥有无限空间,不然显然不可以存储全局状态数据,这意味着日志必须被清理。在Kafka中,有两种方式(日志合并和日志垃圾回收技术)处理,选择哪一个取决于数据是否包含键控更新或事件数据。
- 对于事件数据,Kafka仅保留一个窗口。通常可以被配置为几天,或根据空间来定义;
- 对于键控更新,Kafka依靠重放压缩日志来到达系统的原状态。
第四章 系统构建
日志在分布式数据库的数据流系统与数据集成系统中的作用是一致的:
1. 抽象化数据流
2. 维护数据一致性
3. 数据恢复
Unbundling(拆分?)
可以将整个组织的系统和数据流视为一个独立的分布式数据库,将所有面向查询的系统,Redis、SOLR、Hive tables等,视为数据上的特定索引。将Storm或Samza这样的流处理系统视为完善的触发器和物化视图机制。人们在所有这些不同的数据系统中所做的事情只是不同的索引类型,其中的复杂性其实一直存在,即使是在关系数据库的鼎盛时期。所以也许真正的数据集成并不存在,因为事实上所有的数据都存储在一个地方。将数据分离到多个系统的动机有很多:规模、地理、安全性和性能隔离。对于已经迁移到分布式系统的数据,最好的解决方案是,将由许多的小组件构成一个大的系统,其中每个组件可以是一个小型的集群。许多组件或许不能保证安全性、性能隔离或者只是不能很好地扩展,但这些问题都能得到专业的解决。
各种系统的爆炸式增长是因为,建立优秀的分布式数据系统是无比困难的。可以限制每个组件的复杂度,但是当多个组件集成后,再运行这些系统会产生几何倍数的复杂度。
未来三个可能的方向:
- 维持现状,数据集成问题仍然是最核心的问题,日志非常重要。;
- 出现一个强大的系统重新整合所有组件(就像辉煌时期的关系型数据库一样,不是很可能出现);
- 基础的数据处理可以被拆分为一组服务和面向应用程序的系统api(因为最新一代的数据系统都是开源的,使得这种拆分成为可能)。
每个服务或api都不是完整的,却能各司其职的解决一个问题。通过Java现有的技术栈就能看出:
- Zookeeper:处理分布式系统的同步、协调问题(可能需要一些高级抽象组件的帮助,比如Helix或curator);
- Mesos、YARN:处理虚拟化和资源管理问题;
- 嵌入式的Lucene、LevelDB组件:处理索引问题;
- Netty、Jetty和更高级抽象的Finagle和rest组件:处理远程通信问题;
- Avro、Protocol Buffers、Thrift和umpteen zlin:处理序列化问题;
- Kafka、bookeeper:提供日志备份。
就像搭建乐高积木一样的构建分布式系统,可以创建大量有用的系统。这显然与更关心API的终端用户无关,但它可能是通向更加多样化、模块化完善化并保持简单性的系统的道路。如果一个分布式系统的实现时间从数年降到几周,那一定是因为出现了更可靠、灵活的构建块,同时构建一个庞大分布式系统的复杂性就会消失。
日志在系统构建中的地位(The place of the log in system architecture)
一个拥有外部日志的分布式系统允许每个组件通过关联共享日志来降低自身的复杂性。日志可以做到:
- 通过对节点的并发更新来处理数据一致性问题(无论是最终的还是实时的);
- 提供节点之间的数据复制;
- 向作者提供“提交”语义(仅当写操作保证不丢失时才进行操作);
- 提供外部数据可订阅的数据源(feeds);
- 提供恢复挂掉的副本的能力,通过重放日志、重建新的副本;
- 处理节点间数据的负载均衡。
以上是分布式数据系统中的很大一部分功能了,实际上,剩下的内容就是终端客户的API和构建索引之类的事了。例如,全文搜索查询可能需要查询所有的分区,而主键的查询可能只需要查询该键所在的分区。
以下是分布式数据系统的工作原理,系统分为两个逻辑部分:
- 日志层
- 服务层
日志层顺序地捕获状态变化,而服务层存储外部查询所需要的索引(例如,一个键值存储可能有一个b-tree或sstable索引,而一个搜索服务需要反向索引)。写操作可以直接访问日志,也可以通过服务层代理。写操作会产生一个逻辑上的时间戳(即日志的索引)。如果系统是分区的,那么日志层和服务层的分区数量应该相同,尽管二者可能有不同的机器数量。
服务层订阅日志层,并尽可能快地按日志中的顺序将数据和状态变化同步到本地索引中。
客户端可以从任何节点获得read-your-write语义:通过提供写操作的时间戳作为查询的一部分,服务层可以比较本地的时间戳,为了避免读取过期数据,可以延迟请求的执行,直到服务层的时间戳同步到最新。
服务层的节点可能需要,也可能不需要有“领导者”或“领导人选举”的概念。对于许多简单的用例,服务层可以完全没有领导者,因为日志就是事实。
分布式系统必须处理的一个比较棘手的问题:如何处理崩溃节点的恢复问题。一个典型的方法是,日志保留一个固定大小的时间窗口,并与存储在分区中的数据快照相结合。另一种方法是,日志保持完整的数据副本和垃圾收集日志,这种方法将复杂性从服务层迁移到日志层,因为服务层是系统相关的(system-specific),而日志层是共享的。
基于日志系统,可以获得一组完善的、供开发用的、可作为其他系统ETL数据源的、可供其他系统订阅的API。例如:
注意,这种以日志为中心的分布式系统本身就是数据流的提供者,也可以处理和加载到其他系统。同样,流处理系统可以消耗多个数据流,然后通过其他可以索引该输出的系统来为其他系统服务。将系统的视图分解为日志层和服务层,非常具有启发性,可以将服务和系统的可用性、一致性解耦。
在日志中维护单独的数据副本会使许多人感到浪费(特别是对一个完整的副本来说)。实际上,有一些因素使这个问题变得不那么重要。
- 日志可以是一个特别有效的存储机制,LinkedIn在卡夫卡服务器上存储了超过75tb的数据(2013年),而应用集群需要的存储空间和存储条件(SSD+内存)比Kafka集群要高;
- 全文搜索的索引,需要全部装入内存,而logs因为都是线性读写,所以可以利用廉价的大容量硬盘;
- kafka集群实际运作在多个订阅者模式下,多个系统消费数据,所以日志系统的开销被多个索引分摊了;
以上原因,导致基于外部日志系统的开销变得非常小。
而这正是LinkedIn用来构建实时查询系统的模式,这些系统以数据库为基础(使用Databus作为日志抽象,或从Kafka中提取专用日志),并在该数据流之上提供特定的分区、索引和查询功能,实现了搜索、社交图谱和OLAP查询系统。这些系统不需要有外部可访问的写api,卡夫卡和数据库被用作记录系统,并通过该日志将数据流向适当的查询系统,写操作是由存储特定分区的节点在本地处理的,这些节点盲目地将日志提供的记录同步给自己的存储数据,可以通过重放日志恢复挂掉的节点。
这些系统对日志的依赖程度是不同的,一个完全依赖日志的系统可以利用日志进行数据分区、节点恢复、负载均衡以及一致性和数据传播的所有方面。在这个意义上,服务层实际上是一种“缓存”结构,以使特定类型的处理能够直接写入日志。
The End