衍生数据
存储和处理数据的系统分为两大类:记录系统和衍生系统:
- 记录系统(System of record):也称真相源(source of truth) ,持有数据的权威版本。当新数据进入时首先会记录在此,若数据与其他系统发生冲突那么判记录系统的值是正确的。
- 衍生数据系统(Derived data systems):通常是另一个系统中的现有数据以某种方式进行转换或处理的结果。若丢失衍生数据,可以从原始来源重新创建。典型的例子是缓存(cache):如果数据在缓存中,就可以由缓存提供服务;如果缓存不包含所需数据,则降级由底层数据库提供。非规范化的值,索引和物化视图亦属此类。在推荐系统中,预测汇总数据通常衍生自用户日志。
从技术上讲,衍生数据是冗余的(redundant),和已有的信息是重复的。但是衍生数据能获得良好的只读查询性能。
大多数数据库,存储引擎和查询语言,本质上既不是记录系统也不是衍生系统。数据库只是一个工具:如何使用它取决于你自己。记录系统和衍生数据系统之间的区别不在于工具,而在于应用程序中的使用方式。
批处理
用于调用远程API的在线系统不是构建系统的唯一方式
- 服务(在线系统):服务等待客户请求或指令到达,收到后尽快返回响应。响应时间是服务性能的主要衡量指标,可用性非常重要。
- 批处理系统(离线系统):输入大量数据使用作业(job) 来处理它并生成输出数据,不会有用户等待作业完成因为时间较长。批处理作业通常定期完成,主要性能衡量指标是吞吐量。
- 流处理系统(准实时系统):介于在线和离线之间,有时称为准实时或准在线处理。流处理像批处理系统一样消费输入并产生输出,但是流式作业在事件发生后不久就会对事件进行操作,而批处理作业则需等待固定的一组输入数据。即流处理比批处理延迟低,且基于批处理。
批处理是构建可靠、可扩展和可维护应用程序的重要组成部分。如Google的Map-Reduce,还有后来的Hadoop、CouchDB和MongoDB。
批处理实际上是一种古老的计算方式,MapReduce是一个相当低级别的编程模型。本章中我们将了解MapReduce和其他一些批处理算法和框架。首先我们看看使用标准Unix工具的数据处理。
1 使用Unix工具的批处理
我们从一个简单的例子开始。假设您有一台Web服务器,每次处理请求时都会在日志文件中附加一行。例如,使用nginx默认访问日志格式,日志的一行可能如下所示:
arduino
复制代码
216.58.210.78 - - [27/Feb/2015:17:55:11 +0000] "GET /css/typography.css HTTP/1.1" 200 3377 "http://martin.kleppmann.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36"
(实际上这只是一行,分成多行只是为了便于阅读。)这一行中有很多信息。为了解释它,你需要了解日志格式的定义,如下所示:
bash
复制代码
$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"
日志的这一行表明在2015年2月27日17:55:11 UTC,服务器从客户端IP地址216.58.210.78接收到对文件/css/typography.css的请求。用户没有被认证,所以$remote_user被设置为连字符(- )。响应状态是200(即请求成功),响应的大小是3377字节。网页浏览器是Chrome 40,URL http://martin.kleppmann.com/ 的页面中的引用导致该文件被加载。
1.1 分析简单日志
使用基本的Unix功能创建自己的工具。假设想在网站上找到五个最受欢迎的网页,我们可以在Unix shell中这样做:
bash
复制代码
cat /var/log/nginx/access.log | #1
awk '{print $7}' | #2
sort | #3
uniq -c | #4
sort -r -n | #5
head -n 5 #6
- 读取日志文件
- 将每一行按空格分割成不同字段,每行只输出第七个字段,即URL。
- 按字母顺序排列URL,若某个URL被请求n次,那么排序后将连续出现n次该URL。
uniq命令通过检查两个相邻行是否相同来过滤掉输入中的重复行。-c表示还输出一个计数器。- 第二种排序按每行起始处的数字(
-n)排序,这是URL的请求次数。然后逆序(-r)返回结果,大的数字在前。 - 最后,只输出前五行(
-n 5),并丢弃其余的。该系列命令的输出如下所示:
bash
复制代码
4189 /favicon.ico
3631 /2013/05/24/improving-security-of-ssh-private-keys.html
2124 /2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html
1369 /
915 /css/typography.css
Unix工具的命令非常强大,它能在几秒钟内处理几GB的日志文件,且可以根据需要轻松修改命令。
1.2 Unix哲学
管道是Unix的关键设计思想之一,即通过管道连接程序,它的设计哲学如下:
- 让每个程序都做好一件事。要做一件新的工作,写一个新程序,而不是通过添加“功能”让老程序复杂化。
- 期待每个程序的输出成为另一个程序的输入。不要将无关信息混入输出。避免使用严格的列数据或二进制输入格式。不要坚持交互式输入。
- 设计和构建软件,甚至是操作系统,要尽早尝试,最好在几周内完成。不要犹豫,扔掉笨拙的部分,重建它们。
- 优先使用工具来减轻编程任务,即使必须曲线救国编写工具,且在用完后很可能要扔掉大部分。
这种方法的目的是:自动化,快速原型设计,增量式迭代,对实验友好,将大型项目分解成可管理的块。
这些程序由不同人编写,却可以灵活的组成在一起,例如sort和uniq的结合。Unix是如何实现这种可组合性的?
统一的接口
Unix使用相同的数据格式,相同的I/O接口,这让一个程序的输出能成为另一个程序的输入。统一接口的另一个例子是URL和HTTP。
在Unix中,这种接口是一个文件(file) 。一个文件是一串有序的字节序列,可以使用相同的接口来表示许多不同的东西:文件系统上的真实文件、到另一个进程的通信通道、设备驱动程序、表示TCP连接的套接字等等。
许多Unix程序将这个字节序列视为ASCII文本,这样所有的程序才能使用相同的分隔符记录列表,它们才能相互操作。处理每行的输入时,Unix工具通常通过空白或制表符将行分割成字段。
逻辑与布线分离
Unix工具的另一个特点是使用标准输入(stdin)和标准输出(stdout)。默认情况下,标准输入来自键盘,标准输出指向屏幕。我们也可以将输入或输出重定向到文件。
Unix方法允许shell用户以任何他们想要的方式连接输入和输出,程序不关心或不知道输入的来源和输出的指向。将输入/输出布线与程序逻辑分开,可以将小工具组合成更大的系统。
透明度和实验
Unix工具成功的原因之一是,更容易查看正在发生的事:
- Unix命令的输入文件通常不会被程序修改,这样我们才可以随意运行尝试命令,而不会损坏输入文件。
- 我们可以在任何时候结束管道,将管道输出到
less。这种检查能力对调试非常有用。 - 我们可以将一个流水线阶段的输出写入文件,并作为下一阶段的输入。这使得可以重启后面的阶段而不是整个管道。
与关系数据库查询优化器相比,Unix工具简单且有效。但是却只能在一台机器上运行,所以Hadoop这样的工具应运而生。
2 MapReduce和分布式文件系统
MapReduce有点像Unix工具,但分布在数千台机器上。MapReduce作业在分布式文件系统上读写文件,且不会修改输入,输出文件以连续方式一次性写入。
在Hadoop的Map-Reduce实现中,该文件系统被称为HDFS(Hadoop分布式文件系统) 。 除了HDFS外,还有其他分布式文件系统,本章主要用HDFS作为示例。
与网络连接存储(NAS)和存储区域网络(SAN)架构的共享磁盘方法相比,HDFS基于无共享原则(参见第二部分前言)。共享磁盘需要集中式存储设备和定制的硬件和专用的网络设施(如光纤)。而无共享只需要计算机联网。
HDFS包含在每台机器上运行的守护进程,对外暴露网络服务,允许其他节点访问存储在该机器上的文件。名为NameNode的中央服务器会跟踪哪个文件块存储在哪台机器上。因此,HDFS在概念上创建了一个大型文件系统,可以使用所有运行有守护进程的机器的磁盘。且文件块被复制到多台机器上以支持容灾。与RAID的区别在于,文件访问通过网络而非硬件。
2.1 MapReduce作业执行
MapReduce是一个编程框架,可以使用它编写代码处理HDFS中的大型数据集。数据处理模式与上一节的“简单日志分析”中的示例相似。
创建MapReduce作业,需要实现两个回调函数,Mapper和Reducer
- Mapper:会在每条输入记录上调用一次,对每个记录进度独立处理。其工作是从输入记录中提取键值,然后生成任意数量的键值对。
- Reducer:框架拉取Mapper生成的键值对,手机属于同一个键的所有值,在这组值的列表上迭代调用Reducer。Reducer可以产生输出记录。
简单来说,Mapper的作用是将数据放入一个适合排序的表单中,而Reducer的作用是处理已排序的数据。如果要用MapReduce完成上一节中的例子,我们可以写两个作业,第一个作业统计数量,第二个作业排序。
分布式执行MapReduce
下图显示了Hadoop MapReduce作业中的数据流。其并行化基于分区:作业的输入通常是HDFS中的一个目录,输入目录中的每个文件或文件块都被认为是一个单独的分区,可以单独处理map任务(图中的m1、m2和m3)。
调度器会试图在存储着输入文件副本的机器上运行每个Mapper(如果该机器的资源够的话),这个原则叫将计算放在数据附近,可以节省网络负载并增加局部性。
开始时,框架会将代码复制到适当的机器,然后启动Map任务并开始读取输入文件,一次将一条记录传入Mapper回调函数。Mapper的输出由键值对组成。
计算的Reduce端也被分区,虽然Map任务的数量由输入文件块的数量决定,但Reducer的任务的数量是由作业作者配置的。框架使用键的散列值来确定哪个Reduce任务应该接收到特定的键值对。
键值对必须进行排序,但数据集可能大得无法在单台机器上排序。分类是分阶段进行的,每个Map任务都按照Reducer对输出进行分区然后写入Mapper的本地磁盘。
当Mapper写完排序后的输出文件,MapReduce调度器通知Reducer,随后Reducer连接到每个Mapper,并下载自己相应分区的键值对文件且保留其有序特性。按Reducer分区、排序、复制分区数据的这个过程称为混洗(shuffle) 。如果不同的Mapper生成了相同的记录,那么在Reducer输入中他们会相邻。
Reducer调用时会收到一个键,和一个迭代器作为参数,迭代器会顺序地扫过所有具有该键的记录。Reducer的输出会写入分布式文件系统上的文件中,通常是有备份的。
MapReduce工作流
单个MapReduce作业能做的事有限,所以经常将作业链接成为工作流(workflow) ,即一个作业的输出是下一个的输入。
Hadoop对工作流没有特殊支持,所以链是通过目录名隐式实现的:手动设置第二个作业的输入目录为第一个的输出目录。从框架的角度来看,这是俩独立作业。因此,工作流没那么像Unix命令管道,更像一系列命令。
只有当批处理作业成功完成后,输出才能被视为有效。因此,工作流中只有前一个作业做完后面的作业才能开始,为此出现了很多针对工作流的调度器,包括Oozie,Azkaban,Luigi,Airflow和Pinball。
这些调度程序还具有管理功能,在维护大量批处理作业时非常有用。 Hadoop的各种高级工具也能自动布线组装多个MapReduce阶段,生成合适的工作流。
2.2 Reduce端连接与分组
MapReduce会读取输入文件的全部内容,这被称为全表扫描,在分析查询中需要用到。与索引查询相比效率低,但是hadoop可以在多台机器上并行读取输入。
当我们在批处理的语境中讨论连接时,我们指的是在数据集中解析某种关联的全量存在。
示例:分析用户活动事件
如图,左侧是事件日志,右侧是用户数据库。Mapper从每个输入记录中提取一对键值,在下图中,一组Mapper扫过User activity events(键是user ID,值是活动事件);另一组Mapper扫过User database(user_id为键,用户信息是值)。
分析任务可能需要将用户活动与用户档案相关联,这样才可以进行“大数据推荐”。然而活动事件仅包含用户ID,而没有包含完整的用户档案信息,且如果要在事件中嵌入信息会非常浪费,因此需要将活动事件和用户档案数据库相连接。
该怎样实现连接?1. 遍历活动然后查询用户数据库——性能太差。2. 获取用户数据库副本将它与用户行为日志放入同一个分布式文件系统中,然后用MapReduce将所有相关记录集中到同一个地方进行高效处理——这是更好的办法。
排序合并连接
上一张图中的Mapper是如何提取键值对的呢?如图所示:
Mapper输出后,相同ID则相邻。MapReduce甚至能二次排序(secondary sort) ,以该例子为例,即先以用户ID排序,然后同ID的时间戳再排序。
随后,每个用户ID都会被调用一次Reducer函数,因此一次只需要将一条用户记录保存在内存中, 而不需要通过网络发出请求。这个算法被称为排序合并连接(sort-merge join) 。因为Mapper的输出是按键排序的,然后Reducer将来自连接两侧的有序记录列表合并在一起。
把相关数据放在一起
排序合并连接中,Mapper和排序过程确保了数据都被放在调用Reducer的地方。这种架构可以看作Mapper将”消息“发送给Reducer——所有相同键的键值对被发送到相同的目标(一次Reduce调用)。
使用MapReduce编程模型,能将计算的物理网络通信层面从应用逻辑中剥离出来。而传统数据库的请求在代码内部,这样MapReduce就不用用代码来处理故障了,它在不影响应用逻辑的情况下能透明地重试失败的任务。
2.3 GROUP BY
Mapper做到的和SQL中的GROUP BY差不多,能按某个键对记录分组,然后就能对每个组内进行聚合操作。
会话化(sessionization) 是分组的另一个常见用途:整理特定用户会话的所有活动事件,以找出用户进行的一系列操作。
处理倾斜
单个键有大量数据被称为关键对象(linchpin object) 或热键(hot key) 。比如大V的几百万粉丝。单个Reducer中收集到热键相关的所有活动可能导致严重倾斜,这称为热点(hot spot) 。
可以用算法进行补偿。例如Pig中的倾斜连接(skewed join) ,它会先运行一个抽样作业确定热键,连接的时候Mapper将热键的关联记录随机发送到几个Reducer。代价是需要将连接另一侧的输入记录复制到多个Reducer上。Crunch中的分片连接(sharded join) 方法与之类似,但需要显式指定热键而不是使用采样作业。
按照热键进行分组并聚合时,可以将分组分两个作业进行。第一个阶段将记录发送到随机Reducer,然后第二个作业将所有来自第一阶段Reducer的中间聚合结果合并为每个键一个值。
2.4 Map端连接
复制至Reducer、合并Reducer输入这些操作可能开销巨大。内存缓冲区不够大的话,数据通过作业时可能落盘好几次。如果我们能对输入数据做出某些假设,就能通过Map端连接来加快连接速度。这种方法去掉了Reduce和排序,Mapper读取输入后输出直接写入文件。
广播散列连接(broadcast hash join)
广播散列连接连接适用于大数据集和小数据集连接的情况。小数据集需要足够小,才能全部加载到每个Mapper的内存中。Mapper启动时,会将用户数据库从分布式文件系统中读到内存中的散列中。这样程序就可以扫描用户活动事件,并简单地在散列表中查找每个事件的用户ID
广播:每个连接较大输入端分区的Mapper都会将较小输入端数据集整个读入内存中。
分区散列连接
如果我们将Map端连接进行分区,那么可以在每个分区进行散列。比如我们可以根据用户ID最后一位10进制数字来对活动事件和用户数据库进行分区,例如Mapper3把以3结尾的ID加载到散列表中。分区散列连接在Hive中称为Map端桶连接(bucketed map joins)。
这种方法只有当连接两端输入有相同的分区数,且两侧的记录都是使用相同的键与相同的哈希函数做分区时才适用。
Map端合并连接
若输入数据集不仅以相同方式进行分区,而且还基于相同的键进行排序,就可以使用Map端合并连接。输入是否小得内存放得下不重要,因为Mapper同样可以执行归并操作:按键递增的顺序依次读取两个输入文件,将具有相同键的记录配对。‘
能用Map端合并连接意味着前一个作业可能一开始就把输入数据做了分区进行排序。也就是说这个连接其实上一个作业的Reduce阶段就可以进行。
MapReduce工作流与Map端连接
当下游作业使用MapReduce连接的输出时,选择Map端连接或Reduce端连接会影响输出的结构。Reduce端连接的输出是按照连接键进行分区和排序的,而Map端连接的输出则按照与较大输入相同的方式进行分区和排序。
如前所述,Map端连接对输入数据集的大小,有序性和分区方式做出了更多假设。在优化连接策略时,了解分布式文件系统中数据集的物理布局变得非常重要:仅仅知道编码格式和数据存储目录的名称是不够的;你还必须知道数据是按哪些键做的分区和排序,以及分区的数量。
在Hadoop生态系统中,这种关于数据集分区的元数据通常在HCatalog和Hive Metastore中维护。
2.5 批处理工作流的输出
作业完成后的最终结果是什么?我们最开始为什么要跑这些作业?
数据库查询的场景中分为事务处理(OLTP)与分析查询。OLTP查询通常根据键查找少量记录,使用索引呈现给用户。分析查询通常扫描大量记录,执行分组与聚合,输出通常有着报告的形式。
批处理离分析比较近,因为批处理通常会扫过输入数据集的绝大部分。但是输出不是报表,而是其他类型的结构。
建立搜索索引
Google最初使用MapReduce是为其搜索引擎建立索引,用了由5到10个MapReduce作业组成的工作流实现。直至今日,Hadoop MapReduce仍然是为Lucene/Solr构建索引的好方法。
若需要对一组固定文档执行全文搜索,那么批处理是高效方法。但如果索引文件修改的话就不准确了。可以定期重跑整个索引工作流,或者是增量建立索引:例如Lucene,它会写新的段文件,并在后台异步合并压缩段文件。
键值存储作为批处理输出
数据库是独立于Hadoop的,那么如何输出到数据库中呢?
-
错误的做法:直接在Reducer中使用客户端库,让作业直接写入数据库,一次写入一条记录。这会带来几个问题:
- 每条记录一个网络请求,吞吐量小。
- 作业经常并行,数据库可能被批处理压垮。
- 对于MapReduce来说一次批处理中有一次失败全部都会回滚,但如果Reducer直接写入数据库的话,数据已经保存到数据库里了,失败了也回滚不了。
-
正确的做法:在批处理作业内创建一个新的数据库,将其作为文件写入分布式系统中作业的输出目录。这样就可以批量加载到处理只读查询的服务器中
批处理输出的哲学
Unix哲学鼓励以显示指明数据流的方式进行实验:程序读取输入并写入输出。在这一过程中,输入保持不变,任何先前的输出都被新输出完全替换,且没有其他副作用。这意味着你可以随心所欲地重新运行一个命令,略做改动或进行调试,而不会搅乱系统的状态。
MapReduce作业的输出处理遵循同样的原理。通过将输出视为不可变(如写入外部数据库),批处理作业不仅实现了良好的性能,而且比数据库更容易维护:
- 数据库中我们可以用事务回滚代码,但是如果你代码就运行错了那就无法回滚错误的数据。(能从错误代码中恢复称为人类容错(human fault tolerance) )’
- 如果不能回滚,功能很难开发,回滚使得功能开发更快。这种最小化不可逆性(minimizing irreversibility) 的原则有利于敏捷软件开发。
- 若Map或Reduce任务失败,MapReduce框架将自动重新调度,并在同样的输入上再次运行它。因为输入不可变,这种自动重试是安全的。
- 同一组文件可用作各种不同作业的输入,包括计算指标的监控作业就可以评估作业的输出是否具有预期的性质。
- 与Unix工具类似,MapReduce作业将逻辑与布线(配置输入和输出目录)分离,这使得关注点分离,可以重用代码。
Unix需要做大量的输入解析工作。Hadoop上就可以用更结构化的文件格式:比如Avro和Parquet。
2.6 Hadoop与分布式数据库的对比
Hadoop有点像Unix的分布式版本,其中HDFS是文件系统,而MapReduce是Unix进程的怪异实现。
且MapReduce论文发表的时候其实早已有了大规模并行处理(MPP) 。最大的区别是,MPP数据库专注于在一组机器上并行执行分析SQL查询,而MapReduce和分布式文件系统的组合则更像是一个可以运行任意程序的通用操作系统。
存储多样性
- 数据库:根据特定模型(关系或文档)来构造数据
- 分布式文件系统:字节序列,可以用任何数据模型和编码来编写。
Hadoop让数据能不加区分地转储到HDFS,允许后续在研究如何进一步处理。实践经验证明,使用原始格式通常比先决定数据模型更有价值。这个想法与数据仓库类似:将大型组织的所有数据集中在一起,使得可以跨越以前相分离的数据集进行连接。以原始形式收集数据,稍后再操心模式的设计,能使数据收集速度加快(有时被称为“数据湖(data lake) ”或“企业数据中心(enterprise data hub) ”)
数据存储不加区分,那么解释数据的负担就落到了消费者身上:数据集的生产者不需要强制将其转化为标准格式。以原始形式简单地转储数据,可以允许数据向多种数据模型转换。这种方法被称为寿司原则(sushi principle) :“原始数据更好”。
因此,Hadoop经常被用于实现ETL过程(参阅“数据仓库”):事务处理系统中的数据以原始形式转储到分布式文件系统中,然后用MapReduce将其转化为关系形式,然后导到MMP数据仓库加以分析。数据建模在一个单独的步骤中进行,与数据收集相解耦。
处理模型多样性
MMP数据库是单体的,紧密集成的软件,负责磁盘上的存储布局、查询计划、调度和执行。这些组件都可以针对数据库的特定需求进行调整和优化,且SQL让使业务分析师可以用可视化工具访问。
有些类型的处理用SQL查询不合适。例如机器学习和推荐系统、图像分析、全文搜索索引等。这些类型的处理通常针对特定应用,因此需要写代码。MapReduce使工程师能在大数据集上运行自己的代码。HDFS和MapReduce上可以建立一个SQL查询执行引擎(Hive项目)。
MapReduce对某些类型的处理效率不高。因此出现了Hadoop之上的其他处理模型。最重要的是,处理模型都可以在共享的单个机器集群上运行,这些机器都可以访问分布式文件系统上的相同文件。Hadoop方法中,不需要将数据导入到几个不同的专用系统中进行不同类型的处理:系统足够灵活,可以支持同一个群集内不同的工作负载。不需要移动数据,使得从数据中挖掘价值变得容易得多,也使采用新的处理模型容易的多。
针对频繁故障设计
当比较MapReduce和MPP数据库时,两种不同的设计思路出现了:
-
MMP倾向于在内存中保留尽可能多的数据以避免读盘开销。若一个节点在查询时崩溃,MMP会中止整个查询,并让用户重新提交查询或自动重新运行。
-
MapReduce会直接将数据写入磁盘,一方面为了容错,另一方面是它假设了数据集太大了放不进内存。
- MapReduce可以容忍单个Map或Reduce任务的失败,而不会影响作业的整体,它会以单个任务的粒度重试工作。这更适用于较大的作业,因为数据量大时全部重试非常浪费。
- 以单个任务的粒度恢复,虽然无故障时处理更慢,但如果失败率够高就有收益。
为什么MapReduce会节约使用内存,而在任务的层次进行恢复呢 :一开始,Google有混用的数据中心,在线生产服务和离线批处理作业在同样机器上运行。每个任务有各自的优先级与资源配给,优先级高的任务可以抢占低优先级任务的资源。同时,团队必须为他们使用的资源付费,而优先级高的进程花费更多。
这种架构允许非生产(低优先级)计算资源被过量使用(overcommitted) ,因为系统知道必要时它可以回收资源,这可以更好利用机器并提高效率。但由于MapReduce作业以低优先级运行,容易被抢占资源而终止。所以MapReduce需要能任意终止进程的自由,以提高计算集群中的资源利用率。
在开源的集群调度器中,抢占的使用较少,支持通用优先级抢占的更是没有,所以MapReduce这一设计决策就没什么意义了。下一节中,我们将研究一些与MapReduce设计决策相异的替代方案。
3 MapReduce之后
MapReduce是一种有用的学习工具,它是分布式文件系统的简单抽象,简单的意思是它易于理解。但其实它使用困难,于是在MapReduce上出现很多高级编程模型。
但是它的一些问题无法通过增加另一个抽象层次而解决:一方面,它非常稳健,另一方面,它处理某些类型远不如其他工具。对于不同的数据量、数据结构和处理类型,其他工具可能更适合标识计算。
3.1 物化中间状态
前面说过,MapReduce作业独立,要连接的话要把第一个作业的输出目录作为第二个作业的输入。将数据发布到分布式文件众所周知的位置可以带来松耦合,这样作业就不需要知道谁在提供输入。
很多情况下,一个作业的输出只能作为另一个作业的输入,这种情况被称为中间状态(intermediate state) 。将这个中间状态写入文件的过程称为物化(materialization) 。
而在Unix中,例如开头的日志分析,管道没有完全物化中间状态,而只是用一个小的内存缓冲区将输出流(stream) 向输入。与Unix管道相比,MapReduce完全物化中间状态的方法存在不足:
- MapReduce作业只有当全部前驱作业完成才能启动,而Unix管道连接的进程会同时启动,输出一旦生成就会被消费。必须等前驱作业完成拖慢了整个工作流程的进度。
- Mapper通常是多余的,只是用来刚又Reducer写入的文件,为下一阶段的分区和排序做准备。这种情况下Reducer直接串在一起更好。
- 将中间状态存储在分布式文件系统,意味着这些文件要被复制到多个节点,大量的临时文件复制很占资源。
数据流引擎
为解决以上问题,几种新的分布式批处理执行引擎被开发出来。最著名的是Spark、Tez、Flink。它们有一个共同点:把整个工作流作为单个作业来处理,而不是分解为多个独立子作业。
数据流引擎(dataflow engines):将工作流显式建模为数据从几个处理阶段穿过。它们在一条线上反复调用用户定义的函数来一次处理一条记录,通过输入分区来并行化载荷,通过网络将一个函数的输出复制到另一个函数的输入。
这些函数称为算子(operators) ,用算子代替了严格的Map和Reduce。数据流引擎提供了几种不同的选项来将一个算子的输出连接到另一个算子的输入:
- 对记录按键重新分区并排序,就像在MapReduce的混洗阶段一样,用于实现排序合并连接和分组。
- 接受多个输入,并以相同方式进行分区,但跳过排序。如果记录的顺序不重要的话,这种方法省去了分区散列连接的工作。
- 对于广播散列连接,可以将一个算子的输出,发送到连接算子的所有分区。
这种类型的处理引擎是基于像Dryad和Nephele这样的研究系统,与MapReduce模型相比有几个优点:
- 排序等昂贵工作只有需要才进行。
- 去掉了不必要的Map任务,因为Mapper工作通常可以合并到Reduce算子中。
- 工作流中所有连接和数据依赖是透明的,因此调度程序能总揽全局,知道哪里需要哪些数据。这使得能够利用局部性进行优化,比如将产生和消费数据的任务放在同一台机器上,从而直接通过共享内存缓冲区而不是通过网络复制。
- 算子的中间状态通常保存到内存或写入本地磁盘,这比复制到多台机器要好。
- 算子能在输入就绪后立即执行,不用整个前驱阶段完成后再开始。
- MapReduce会为每个任务启动一个JVM,启动开销很大。而数据流引擎会将现有JVM进程重新用来运行新算子。
相同的处理代码在数据流引擎和MapReduce工作流上都能运行,因为算子是Map和Reduce的泛化。所以我们可以通过修改配置,简单的从MapReduce切换到Tez或Spark。Spark和Flink则是包含了独立网络通信层,调度器,及用户向API的大型框架。我们将简要讨论这些高级API。
容错
完全物化中间状态到分布式文件系统有个优点:具有持久性。这能提高容错,一台机器上失败了,另一台机器上可以重启。为实现这种重新计算,框架必须跟踪一个给定的数据的计算过程,例如使用了哪些分区或算子。实践中,Spark使用弹性分布式数据集(RDD) 的抽象来跟踪数据的谱系,而Flink对算子状态存档,允许恢复运行在执行过程中遇到错误的算子。
重新计算时,重要的是知到计算是否是确定性的:给定相同输入数据,算子是否始终产生相同输出?若一些丢失的数据已经发送给下游算子,这个问题就很重要——重启后,重新计算的数据和丢失的数据不一致,下游算子不知道得用哪个。这种情况得杀死下游算子,然后重新跑数据。
为了避免这种级联故障,最好让算子具有确定性。需要注意,非确定性行为很容易悄悄溜进来:例如哈希、概率、统计等算法可能用到随机数。因此不能使用系统时钟,且得用固定种子生成伪随机数。如果中间状态的数据比源数据小得多,那么为应对容错,直接将中间数据物化或许更好。
关于物化的讨论
数据流引擎更像Unix管道:将算子输出增量地传递给其他算子,不待输入完成便开始处理。大部分工作流的算子以流水线方式执行,但是排序算子不可以——排序需要消费所有输入才能生成任何输出。
作业完成后,他的输出持久化到分布式文件系统,所以,使用数据流引擎时,HDFS上的物化数据集通常仍是作业的输入和输出。和MapReduce一样,输入不可变,输出被完全替换。
3.2 图与迭代处理
之前讨论过图:快速执行查询来查找少量符合特定条件的顶点。而批处理中的图,作用是在整个图上执行某种离线处理或分析,这可以用于机器学习应用(如推荐引擎)或排序系统中。例如,最著名的图形分析算法之一是PageRank,它根据指向该网页的链接数来估算网页流行度,用于确定网络搜索引擎结果的呈现顺序。
许多图算法将一个顶点与近邻的顶点连接以传播一些信息,不断重复直到满足某些条件——例如直到没有更多边要跟进。可用在分布式文件系统中存储图(包含顶点和边的列表文件),但是这种“重复至完成”的操作普通MapReduce无法表示,因为它只扫过一趟数据。
这种算法通常以迭代风格实现:
- 外部调度程序运行批处理来计算算法的一个步骤。
- 批处理完成后,调度器检查它是否完成。
- 若尚未完成,则调度程序返回到步骤1并运行另一轮批处理。
Pregel处理模型
批量同步并行(BSP) 计算模型是针对图批处理的优化,它也被称为Pregel模型。Pregel让一个顶点向另一个顶点发送消息,通常这些消息沿着图的边发送。
Pregel在每次迭代中,为每个顶点调用一个函数,将所有发送给它的消息传递给他(就像调用Reducer一样)。顶点在一次迭代到下一次迭代的过程中会记住它的状态,所以这个函数只需要处理新的传入消息。如果图的某个部分没有被发送消息,那里就不需要做任何工作。
这与Actor模型有些相似:顶点状态和顶点之间的消息具有容错性和耐久性,且通信以固定方式进行,即每次迭代中,框架递送上次迭代中发送的所有消息。
并行执行
顶点不需要知道它在哪台机器上执行,发送消息时只是将消息发到对应顶点ID。图的分区取决于框架——即确定哪个顶点运行在哪台机器上,以及如何通过网络路由消息让它们到达正确的地方。
由于编程模型一次只处理一个顶点,所以框架可以以任意方式对图分区。若顶点需要进行大量通信,那么它们最好能在同一台机器上,然而找到这样一种优化的分区方法是困难的——实践中,图一般不把顶点分组到一起,通常用ID分区。
因此,图算法通常由很多跨机器通信的额外开销,而中间状态(节点之间发送的消息)往往比原始图大。通过网络发消息会大大降低效率。因此,图可以放入一台计算机内存中,这样会比分布式快很多,例如使用GraphChi等框架。图比内存大也没关系,可以放到磁盘里,但是如果图大到不适合单机,那么Pregel这样的分布式是不可避免的。
3.3 高级API和语言
分布式批处理执行引擎现在已经很成熟了,现在关注点已经转向其他领域:改进编程模型、提高处理效率、扩大这些技术等。
Hive,Pig,Cascading和Crunch等高级语言和API变得越来越流行,随着Tez的出现,这些高级语言还可以迁移到新的数据流执行引擎,而无需重写作业代码。而Spark和Flink也有它们自己的高级数据流API。
这些数据流API通常使用关系型构建块来表达一个计算:按某个字段连接数据集;按键对元组做分组;按某些条件过滤;并通过计数求和或其他函数来聚合元组。这些操作使用本章前面说过的各种连接和分组算法实现。
高级接口还支持交互式用法。我们可以在Shell中增量式编写分析代码,频繁运行来观察它做了什么。高级接口同时提高了人类和机器层面的作业执行效率。
向声明式查询语言的转变
声明式有许多优点。连接算法会大幅影响批处理作业的性能,而查询优化器会决定如何最好的执行连接。如果连接是以声明式方式指定的,我们就不需要知道算法的原理。但是与SQL的完全声明式查询模型有很大区别,MapReduce是围绕着回调函数的概念建立的,在函数里用户可以自由调用代码来决定输出什么,这样可以基于大量已有库的生态系统进行创作。
声明式特性还有其他优势,例如,如果回调函数做的事很少或者只是从一条记录中选择了一些字段,那么为每一条记录调用函数就会浪费大量资源。若以声明方式表示这些简单的过滤和映射操作,那么查询优化器可以用列存储以提高读取效率(参与“列存储”)。
能否自由运行任意代码,可以看作是MapReduce批处理系统和MMP数据库的区别所在。(参见“比较Hadoop和分布式数据库”)。数据库写函数通常比较麻烦,且与很多语言的依赖管理器兼容不好(比如Maven)。如果在高级API中引入声明式部分和查询优化器,批处理框架看着就像个MMP数据库。同时,通过拥有运行任意代码和以任意格式读取数据的可扩展性,这让它们十分灵活。
Hive,Spark DataFrames和Impala还使用了向量化执行(参阅“内存带宽和向量处理”):在对CPU缓存友好的内部循环中迭代数据,避免函数调用。Spark生成JVM字节码,Impala使用LLVM为这些内部循环生成本机代码。
专业化的不同领域
MPP数据库满足了商业智能分析和业务报表的需求。统计和数值分析算法是机器学习所需要的,现在逐渐变得重要。例如,Mahout在MapReduce,Spark和Flink之上实现了用于机器学习的各种算法,而MADlib在关系型MPP数据库(Apache HAWQ)中实现了类似的功能。
空间算法能在一些多维空间中搜索与给定最近的项目,例如最近邻搜索(k-nearest neghbors, kNN)。近似搜索对于基因组分析算法很重要,它们需要找到相似但不相同的字符串。
批处理引擎正被用于分布式执行日益广泛的各领域算法。批处理系统和MMP数据库在互相汲取对方的优点,它们都是用于存储和处理数据的系统。