学习笔记:The Log(我所读过的最好的一篇分布式技术文章)

前言

这是一篇学习笔记。
学习的材料来自Jay Kreps的一篇讲Log的博文。
原文很长,但是我坚持看完了,收获颇多,也深深为Jay哥的技术能力、架构能力和对于分布式系统的理解之深刻所折服。同时也因为某些理解和Jay哥观点吻合而略沾沾自喜。

Jay Kreps是前Linkedin的Principal Staff Engineer,现任Confluent公司的联合创始人和CEO,Kafka和Samza的主要作者。

所谓笔记,就是看了文章,提笔就记,因为Jay哥本身本章组织的太好,而其本身的科学素养及哲学素养也很高,所以私以为出彩的东西就不省略了。

一、资料来源

The Log: What every software engineer should know about real-time data's unifying abstraction

二、笔记

2.1 Log的价值

1) Log是如下系统的核心:

  • 分布式图数据库
  • 分布式搜索引擎
  • Hadoop
  • 第一代和第二代K-V数据库

2) Log可能跟计算机的历史一样长,并且是分布式数据系统和实时计算系统的核心。
3) Log的名字很多:

  • Commit log
  • Transaction log
  • Write-ahead log

4) 不理解Log,你就不可能充分理解

  • 数据库
  • NoSQL存储
  • K-V存储
  • 复制
  • Paxos算法
  • Hadoop
  • Version Control
  • 或者,任何软件系统

2.2 什么是Log?

2.2.1 概述

这里写图片描述

  • 记录会附加到log的尾部。
  • 从左到右读取记录。
  • 每个entry都有唯一且有序的log entry 序号。

记录的顺序定义了这样的一个概念:时间。
因为越靠左的记录越早。
Entry的序号可以当作一种时间戳,将记录的顺序当作时间这一概念看起来很奇怪,但是很快你就会发现,这样做:可以方便地将“时间”与任一特定的物理时钟解耦。
Log和常见的文件、表(table)没有那么大的差别。

  • 文件是一组字节
  • 表是一组记录
  • Log可以说是某种将记录按时间排序的文件或者表

这样说,可能你会觉得log如此简单,还有讨论的必要吗?
其实,log的核心意义在于:

Log记录了何时发生了什么(they record what happened and when.)。

而这一条,通常是分布式系统最最最核心的东西。
注意,这里有必要澄清几个概念:

  • 本篇所讨论的Log和程序员通常接触的应用日志(application logs)不同
  • 应用日志通常是一种非结构化的,记录错误信息、调试信息,用于追踪应用的运行的,给人看的日志,比如通过log4j或者 syslog来写入本地文件的日志。
  • 而本篇所讨论的log是通过编程方式访问的,不是给人看的,比如“journal”、“data logs”。
  • 应用日志是本篇所讨论的log的一种特化。

2.2.2 数据库中的Logs

Log的起源不得而知,就像发明二分查找的人,难以意识到这种发明是一种发明。
Log的出现和IBM的System R 一样早。
在数据库中,需要在数据库崩溃时,保持多种多样的数据结构和索引保持同步。
为保证原子性和持久性,数据库需要在对数据结构和索引进行修改提交之前,记录其要修改的内容。
所以log记录了何时发生了什么,而每一张表和索引本身,都是这种历史信息的映射。
因为log是立即持久化的,所以当crash发生时,其成为恢复其它持久化结构的可靠来源。

Log从保证ACID特性的一种实现,发展成了一种数据库之间数据复制的手段。

很显然,数据库中发生的一系列的数据变更,成为数据库之间 保持同步最需要的信息。
Oracle、MySQL、PostgreSQL,都包含了log传输协议,将log的一部分发送到用于保持复制的从数据库(Slave)。
Oracle的XStreams和GoldenState,将log当作一种通用的数据订阅机制,以提供给非Oracle的数据库订阅数据。
MySQL和PostgreSQL也提供了类似的组件,这些组件是数据系统架构的核心。
面向机器的Log,不仅仅可被用在数据库中,也可以用在:

  • 消息系统
  • 数据流(data flow)
  • 实时计算

2.2.3 分布式系统中的logs

Log解决了两个很重要的分布式数据系统中的问题:
1) 有序的数据变化
2) 数据分布式化

所谓的状态机复制原理(State Machine Replication Principle):

如果两个确定的处理过程,从相同的状态开始,按照相同的顺序,接收相同的输入,那么它们将会产生相同的输出,并以 相同的状态结束。

所谓确定的(deterministic),是指处理过程是时间无关的,其处理结果亦不受额外输入的影响。
可以通过非确定的例子来理解:

  • 多线程的执行顺序不同导致不同的结果
  • 执行getTimeOfDay()方法
  • 其它的不能重复的处理过程

所谓状态,可以是机器上的任意数据,无论在处理结束后,是在机器的内存中还是磁盘上。
相同的输入按照相同的顺序,产生相同的结果,这一点值得引起你的注意,这也是为什么log会如此重要,这是一个直觉性的概念:如果你将同一个log输入两个确定性的程序,它们将产生相同的输出。
在分布式系统的构建中,意识到这一点,可以使得:
让所有的机器做同样的事,规约为:
构建分布式的、满足一致性的log系统,以为所有处理系统提供输入。

Log系统的作用,就是将所有的输入流之上的不确定性驱散,确保所有的处理相同输入的复制节点保持同步。

这种方法的最妙之处在于,你可以将索引日志的时间戳,作为所有复制节点的时钟来对待:

通过将复制节点所处理过的log中最大的时间戳,作为复制节点的唯一ID,这样,时间戳结合log,就可以唯一地表达此节点的整个状态。

应用这种方法的方式也很多:

  • 在log中记录对一个服务的请求
  • 在回复请求的前后,记录服务状态的变化
  • 或者,服务所执行的一系列转换命令,等等。

理论上来讲,我们可以记录一系列的机器指令,或者所调用方法的名称及参数,只要数据处理进程的行为相同,这些进程就可以保证跨节点的一致性。
常玩儿数据库的人,会将逻辑日志和物理日志区分对待:

  • 物理日志:记录了所有的行内容的变化。
  • 逻辑日志:不是记录内容的变化,而是Insert , update , delete等导致行内容变化的SQL语句。

对分布式系统,通常有两种方式来处理复制和数据处理:
1) State machine model(active - active)
2) Primary-back model (active - passive)

如下图所示:
这里写图片描述

为了理解上述两种方式的不同,来看个简单的例子:
现在,集群需要提供一个简单的服务,来做加法、乘法等算术运算。初始,维护一个数字,比如0。

  • Active – active :在日志记录这样的一些操作,如“+1”、“*2”等,这样,每个复制节点需要执行这些操作,以保证最后的数据状态是一致的。
  • Active – passive:一个单独的master节点,执行“+1”、“*2”等操作,并且在日志中记录操作的结果,如“1”、“3”、“6”等。

上面的例子也揭示了,为什么顺序是复制节点之间保持一致性的关键因素,如果打乱了这些操作的顺序,就会得到不同的运算结果。
分布式log,可以当做某些一致性算法的数据结构:

  • Paxos
  • ZAB
  • RAFT
  • Viewstamped Replication

一条log,表征了一系列的关于下一个值是什么的决定。

2.2.4 Changelog

从数据库的角度来看,一组记录数据变化的changelog和表,是对偶和互通的。
1) 依据记录了数据变化的log,可以重构某一状态的表(也可以是非关系型存储系统中有key的记录)
2) 相反,表如果发生了变化,可以将变化计入log。

这正是你想要的准实时复制的秘籍所在!

这一点和版本控制所做的事情极为类似:管理分布式的、并发的、对状态进行的修改。

版本控制工具,维护了反映修改的补丁,这其实就是log,你和一个被签出(checked out)的分支快照进行交互,这份快照就相当于数据库中的表。你会发现,版本控制与分布式系统中,复制都是基于log的:当你更新版本时,你只是拉取了反映了版本变化的补丁,并应用于当前的分支快照。

2.3 数据集成(Data integration)

2.3.1 数据集成的含义

所谓数据集成,就是将一个组织中的所有服务和系统的数据,变得可用。

实际上,对数据进行有效利用,很符合马斯洛的层次需求理论。
金字塔的最底层,是收集数据,将其整合进应用系统中(无论是实时计算引擎,还是文本文件,还是python脚本)。
而这些数据,需要经过转换,保持一个统一、规范、整洁的格式,以易于被读取和处理。
当上面的要求被满足后,就可以开始考虑多种多样的数据处理方式,比如map – reduce 或者实时查询系统。
很显然,如果没有一个可靠的、完备的数据流,Hadoop就仅仅是一个昂贵的、难以整合的加热器(集群很费电么?)。
相反,如果能保证数据流可靠、可用且完备,就可以考虑更高级的玩法、更好的数据模型和一致的、更易被理解的语义。
接着,注意力就可以转移到可视化、报表、算法和预测上来(挖啊机啊深度啊)。

2.3.2 数据集成的两个复杂性

事件

事件数据,记录了事件是怎么发生的,而不仅仅是发生了什么,这一类log通常被当做应用日志,因为一般是由应用系统写入的。但这一点,其实混淆了log的功能。
Google的财富,其实,是由一个建立在(用户)点击流和好恶印象(体验)之上的相关性pipeline产生的,而点击流和印象,就是事件。

各种各样的专业数据系统的爆发

这些系统存在的原因:

  • 联机分析(OLAP)
  • 搜索
  • 简单的在线存储
  • 批处理
  • 图谱分析
  • 等等(如spark)

显然,要将数据整合进这样的系统中,对于数据集成来讲,极为困难。

2.3.3 基于日志结构的数据流

每种逻辑意义上的数据源,都可以依据log进行建模。

数据源可以是记录了事件(点击和PV)的应用程序,可以是接受更改的数据库表。

每个订阅者,都尽可能快地从这些数据源产生的log中获取新的记录,应用于本地的存储系统,并且提升其在log中的读取偏移(offset)。订阅者可以是任何数据系统,比如缓存、Hadoop、另一个站点的数据库,或者搜索引擎。

Log,实际上提供了一种逻辑时钟,针对数据变化,可以测量不同的订阅者所处的状态,因为这些订阅者在log中的读取偏移不同且相互独立,这种偏移就像一个时间意义上的“时刻”一样。

这里写图片描述

考虑这样一个例子,一个数据库,和一些缓存服务器:
Log提供了这样一种能力,可以使得所有的缓存服务器得到同步,并推出它们所处的“时刻”。

假设我们写入了一个编号为X的log,要从某个缓存服务器读取数据,为了不读到老数据,只需要保证:在缓存服务器将数据(同步)复制到X这个位置前,我们不从这个缓存中读取任何东西即可。

此外,log还提供了作为缓冲区的能力,以支持生产者和消费者的行为以异步的方式进行。

最关键的一个支持异步的原因,是订阅系统可能会发生崩溃、因维护而下线,接着恢复上线,而在这种情况下,每个订阅者都以自己的步调消费数据。

一个批处理系统,比如Hadoop,或者一个数据仓库,是以小时或天为单位消费数据,而一个实时系统,通常在秒级消费数据。
而数据源或者log,对消费数据的订阅者一无所知,所以,需要在pipeline中做到无缝的添加订阅者和移除订阅者。

更重要的是,订阅者,只需要知道log,而不需要对其所消费的数据的来源有任何了解,无论这个数据源是RDBMS、Hadoop,还是一个最新流行的K-V数据库,等等。

之所以讨论log,而不是消息系统,是因为不同的消息系统所保证的特性不同,并且用消息系统这个词,难以全面和精确表达某种语义,因为消息系统,更重要的在于重定向消息。

但是,可以将log理解为这样一种消息系统,其提供了持久性保证及强有序的语义,在通讯系统中,这称作原子广播。

2.4 在Linkedin

Linkedin目前的主要系统包括(注:2013年):

  • Search
  • Social Graph
  • Voldemort (K-V存储)
  • Espresso (文档存储)
  • Recommendation engine
  • OLAP query engine
  • Hadoop
  • Terradata
  • Ingraphs (监控图谱及metrics服务)

每个系统,都在其专业的领域提供专门的高级功能。

(这一段太长太长了,Jay兄十分能侃啊,所以挑重点的来记吧!)

1) 之所以引入数据流这个概念,是因为要在oracle数据库的表之上,建立一个抽象的缓存层,为搜索引擎的索引构建和社交图谱更新,提供拓展能力。

2) 为了更好的处理linkedin的一些推荐算法,开始搭Hadoop集群,但团队在此块的经验尚浅,所以走了很多弯路。

3) 开始时,简单粗暴地认为只要将数据从oracle数据仓库中拉出来,丢进hadoop就可以了。结果发现:第一,将数据从oracle数据仓库快速导出是个噩梦;第二,也是更糟糕的一点,数据仓库中某些数据的处理不对,导致了hadoop的批处理任务不能按预期输出结果,且通过hadoop批处理执行任务,通常不可逆,特别是在出了报表之后。

4) 最后,团队抛弃了从数据仓库中出数据的方式,直接以数据库和logs为数据源。接着,造出了一个轮子:K-V 存储(Voldemort)。

5) 即使是数据拷贝这样不高大上的活儿,也占据了团队大量的时间去处理,更糟的是,一旦数据处理的pipeline中有个点出错,hadoop立马变得废柴,因为再牛逼的算法跑在错误的数据上,只有一个后果,就是产生更多的错误数据。

6) 即使团队构建的东西抽象层次很高,针对每种数据源还是需要特定的配置,而这也是很多错误和失败的根源。

7) 一大批程序员想跟进,每个程序员都有一大批的想法,集成这个系统,添加这个功能,整合这个特色,或者想要自定义的数据源。

8) Jay哥开始意识到:
第一, 虽然他们构建的pipelines还很糙,但是却极其有价值。即使是解决了数据在新的系统(如hadoop)中可用的问题,也解锁了一大批可能性。以前难做的计算开始变为可能。新的产品和分析,仅需要解锁其它系统中的数据,并且进行整合,就可以容易地做出来。

第二, 很明显,可靠地数据装载需要更坚实的支撑,如果能够捕获所有的结构,就可以让hadoop数据装载完全自动化,不需要加入新的数据源或人工修改数据的模式。数据会神奇地出现在HDFS中,而新的数据源加入后,Hive的表会用合适的列自动化地、自适应地生成。

第三,数据覆盖度远远不足。因为要处理很多新的数据源,很难。

9) 为了解决新数据源加入后的数据装载问题,团队开始了这样的尝试:

这里写图片描述

很快,他们发现这样搞行不通,因为发布和订阅、生产和消费,数据流通常还是双向的,这成了一个O(n^2)的问题。
所以,他们需要的是这样的模型:

这里写图片描述

需要将每个消费者从数据源隔离,理想的情况下,这些消费者只和一个data repository进行交互,而这个repository可以提供它们访问任意数据的能力。

10)消息系统 + log = Kafka,kafka横空出世。

2.5 Log和ETL、数据仓库的关系

2.5.1 数据仓库

1) 一个装有干净的、结构化的、集成的数据repository,用于分析。
2) 虽然想法很美好,但是获取数据的方式有点过时了:周期性地从数据库获取数据,将其转换为某种可读性更佳的格式。
3) 之前的数据仓库问题在于:将干净的数据和数据仓库高度耦合

数据仓库,应该是一组查询功能的集合,这些功能服务于报表、搜索、ad hot 分析,包含了计数(counting)、聚合(aggregation)、过滤(filtering)等操作,所以更应该是一个批处理系统。

但是将干净的数据和这样的一种批处理系统高度耦合在一起,意味着这些数据不能被实时系统消费,比如搜索引擎的索引构建、实时计算和实时监控系统,等等。

2.5.2 ETL

Jay哥认为,ETL无非做两件事:

1) 对数据进行抽取和清洗,将数据从特定的系统中解锁
2) 重构数据,使其能通过数据仓库进行查询。比如将数据类型变为适配某个关系型数据库的类型,将模式转换为星型或者雪花模式,或者将其分解为某种面向列的存储格式。

但是,将这两件事耦合在一起,问题很大,因为集成后的、干净的数据,本应能被其它实时系统、索引构建系统、低延时的处理系统消费。

数据仓库团队,负责收集和清洗数据,但是,这些数据的生产者往往因为不明确数据仓库团队的数据处理需求,导致输出很难被抽取和清洗的数据。
同时,因为核心业务团队对和公司的其它团队保持步调一致这件事儿不敏感,所以真正能处理的数据覆盖度很低,数据流很脆弱,很难快速应对变化。

所以,更好的方式是:

这里写图片描述

如果想在一个干净的数据集上做点搜索、实时监控趋势图、实时报警的事儿,以原有的数据仓库或者hadoop集群来作为基础设施,都是不合适的。更糟的是,ETL所构建的针对数据仓库的数据加载系统,对其它(实时)系统点儿用没有。

最好的模型,就是在数据发布者发布数据之前,就已经完成了数据的清洗过程,因为只有发布者最清楚它们的数据是什么样的。而所有在这个阶段所做的操作,都应该满足无损和可逆

所有丰富语义、或添加值的实时转换,都应在原始的log发布后处理(post-processing),包括为事件数据建立会话,或者添加某些感兴趣的字段。原始的log依旧可被单独使用,但是此类实时应用也派生了新的参数化的log。

最后,只有对应于具体的目标系统的数据聚合操作,应作为数据装载的一部分,比如转换为星型或雪花型模式,以在数据仓库中进行分析和出报表。因为这个阶段,就像传统的ETL所做的那样,因为有了非常干净和规范的数据流,(有了log后)现在变得非常简单。

2.6 Log文件和事件

以log为核心的架构,还有个额外的好处,就是易于实现无耦合的、事件驱动的系统。

传统的 捕获用户活动和系统变化的方式,是将此类信息写入文本日志,然后抽取到数据仓库或者hadoop集群中进行聚合和处理,这个问题和前面所述的数据仓库和ETL问题类似:数据与数据仓库的高度耦合。

在Linkedin,其基于kafka构建了事件数据处理系统。为各种各样的action定义了成百上