设计数据密集型应用——批处理(10)

1. 写在最前面

带有太强个人色彩的系统无法成功。当最初的设计完成并且相对稳定时,不同的人们以自己的方式进行测试,真正的考验才开始。

—高德纳

构建系统的三种不同类型:

  • 服务(在线系统):服务等待客户的请求或指令到达。每收到一个,服务会试图尽快处理它,并发回一个响应。响应时间通常是服务性能的主要衡量指标,可用性通常非常重要
  • 批处理系统(离线系统):一个批处理系统有大量的输入数据,跑一个作业(job )来处理它,并生成一些数据,这往往需要一段时间,通常不会有用户等待作业完成。批处理作业的主要性能衡量标准通常是吞吐量
  • 流处理系统(准实时系统):流处理介于在线和离线之间,所以有时候被称为准实时或准在线。像批处理系统一样,流处理输入并产生输出。但是流处理系统比批处理系统具有更低的延迟。

批处理是构建可靠、可伸缩和可维护应用程序的重要组成部分。例如,2004 年发布的批处理算法 Map-Reduce 被称为「造就 Google 大规模可伸缩性的算法」。

2. 使用 Unix 工具的批处理

以下是 Nginx 的日志实例,采集的模板如下:

 $remote_addr - $remote_user [$time_local] "$request"
 $status $body_bytes_sent "$http_referer" "$http_user_agent"

采集的样例如下:

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"

2.1 简单的日志分析

需要分析出网站五个最受欢迎的网页。

使用Unix Shell 的方式:

cat /var/log/nginx/access.log | #1 读取日志
	awk '{print $7}' | #2 按照空格分割日志,输出其第七字段,此处为 URL
	sort             | #3 按照字母序列排序 URL
	uniq -c          | #4 过滤掉重复的行,并统计其出现的此处
	sort -r -n       | #5 按照每行起始处的数字 -n 排序,然后逆序返回结果
	head -n 5          #6 只输出前五行

注:使用 awk、sed 、grep、sort、uniq 和 xargs 的组合,可以在几分钟内完成许多数据的分析,并且它们的性能相当的好。

2.1.1 命令链与自定义程序

使用简单的程序来完成分析网络最受欢迎的网页。

counts = Hash.new(0)         # 1 counts 是一个哈希表,保存每个 URL 被浏览的次数,默认值为0
File.open('/var/log/nginx/access.log') do |file| 
    file.each do |line|
        url = line.split[6]  # 2 逐行读取日志,抽取每行第七个字段
        counts[url] += 1     # 3 将日志当前行的 URL 对应的计数器值加 1
    end
end

top5 = counts.map{|url, count| [count, url] }.sort.reverse[0...5] # 4 按计数器降序对哈希表排序,并取前五位
top5.each{|count, url| puts "#{count} #{url}" }                   # 5 打印前五个条目

注:上述程序并不像 Unix 管道那样简洁,但它的可读性很强,喜欢那种属于口味的问题

2.1.2 排序 VS 内存中的聚合

上述两种方式的那种方式更好?

这取决于有多少个不同的 URL ,即工作集的大小。

  • 当工作集足够小时,内存散列表现良好。
  • 如果工作集大于可用内存,则排序方法的优点可以高效地使用磁盘。

注:sort 程序通过溢出至磁盘的方式来自动应对大于内存的数据集,并能同时使用多个 CPU 核进行排序。这意味着之前看到的简单的 Unix 命令链很容易伸缩至大数据集。

2.2 Unix 哲学

Unix 的哲学,该哲学的表述:

  • 让每个程序都做好一件事。要做一件新的工作,写一个新程序,而不是通过添加「功能」让老程序复杂化。
  • 期待每个程序的输出成为另一个程序的输入。不要将无关信息混入输出。
  • 设计和构建软件时,即使是操作系统,也让它们能够尽早地被试用,最好在几周内完成。
  • 优先试用工具来减轻编排任务,即使必须曲线救国编写工具,且在用完后很可能要扔掉大部分。

2.2.1 统一的接口

在 Unix 中,统一的接口是文件(file)(更准确地说,是一个文件描述符)。一个文件只是一串有序的字节序列。因为这是一个非常简单的接口,所以可以使用相同的接口来表示许多不同的东西:从文件系统上的真实文件,到另一个进程(Unix 套接字,stdin,stdout)的通信通道,设备驱动程序(比如/dev/audio或/dev/lp0),表示 TCP 连接的套接字等等。很容易将这些设计视为理所当然的,但实际上能让这些差异巨大的东西共享一个统一的接口是非常厉害的,这使得它们可以很容易地连接在一起。

注:统一接口的另一个例子是 URL 和 HTTP,这是 Web 的基石。一个 URL 标识一个网站上的一个特定的资源,可以链接到任何其他网站的任何网址。具有网络浏览器的用户因此可以通过跟随链接在网站之间无缝跳转,即使服务器可能由完全不相关的组织维护。

2.2.2 逻辑与布线相分离

Unix 工具的另一个特点是使用标准输入和标准输出。如果运行一个程序,而不指定任何其他的东西,标准输入来自键盘,标准输出指向屏幕。但是,也可以从文件输入或将输出重定向到文件。管道允许你将一个进程的标准输出附加到另一个进程的标准输入。

注:此处存才一个小内存缓冲区,而不需要将整个中间数据流写入磁盘

如果需要,程序仍然可以直接读取和写入文件,但 Unix 方法在程序中不关心特定的文件路径、只使用标准输入和标准输出时效果最好,这允许 Shell 用户以任何他们想要的方式连接输入和输出;该程序不知道也无需关系输入来自哪里以及输出到哪里。

注:这是一种松耦合,将输入/输出布线与程序逻辑分开,可以将小工具组合成更大的系统。

2.2.3 透明度和实验

使 Unix 工具如此成功的部分原因是,它们使查看正在发生的事情变得非常容易:

  • Unix 命令的输入文件被视为不可变的。这意味着你可以随意运行命令,尝试各种命令行选项,而不会损坏输入文件。
  • 你可以在任何时候结束管道,将管道输出到 less,然后查看它是否具有预期的形式。这种检查能力对调试非常有用。
  • 你可以将一个流水线阶段的输出写入文件,并将该文件用作下一阶段的输入。这使你可以重新启动后面的阶段,而无需重新运行整个管道。

注:Unix 工具的最大局限在于它们只能在一台机器上运行。

3. MapReduce 和分布式文件系统

3.1 MapReduce 作业执行

MapReduce 是一个编程框架,你可以使用它编写代码来处理 HDFS 等分布式文件系统中的大型数据集。用「简单日志分析」的例子来理解 MapReduce。

  • 读取一组输入文件,并将其分解成记录。在 Web 服务器日志示例中,每条记录都是日志中的一行(即 \n 是记录分隔符)
  • 调用 Mapper 函数,从每条输入记录只能够提取一对键值对。在前面的例子中,Mapper 函数是 awk '{print $7}',它用于提取 URL $7 作为键,并将值留空。
  • 按键排序所有键值对。在日志的例子中,这由第一个 sort 命令完成。
  • 调用 Reducer 函数遍历排序后的键值对。如果同一个键出现多次,排序使它们在列表中相邻,所以很容易组合这些值而不必在内存中保留很多状态。在前面的例子中,Reducer 是由 uniq -c 命令实现的,该命令使用相同的键来统计相邻记录的数量。

3.1.1 分布式执行 MapReduce

MapReduce 与 Unix 命令管道的主要区别在于,MapReduce 可以在多台机器上并行执行计算,而无需编写代码来显示处理并行问题。Mapper 和 Reducer 一次只能处理一条记录;它们不需要知道它们的输入来自哪里,或者输出去往什么地方,所以框架可以处理在机器之间移动数据的复杂性。

具有三个 Mapper 和 三个 Reducer 的 MapReduce 任务:

在这里插入图片描述

注:隐藏的 MapReduce 调度器试图在存储输入文件副本的机器上运行每个 Mapper,只要该机器有足够的备用 RAM 和 CPU 资源来运行 Mapper 任务。这个原则称为将计算放在数据附近:它节省了通过网络复制输入文件的开销,减少网络负载的并增加局部性。

3.1.2 MapReduce 工作流

将 MapReduce 作业链接称为工作流式极为常见的。例如,一个作业的输出成为下一个作业的输入。Hadoop MapReduce 框架对工作流没有特殊支持,所以这个链是通过目录名隐式实现的:第一个作业必须将其输出配置为 HDFS 中的指定目录,第二个作业必须将输入配置为同一个目录。从 MapReduce 框架的角度来看,这是两个独立的作业。

注:Hadoop 的各种高级工具,如 Pig、Hive等也能自动布线组装多个 MapReduce 阶段,生成合适的工作流。

3.2 Reduce 侧连接与分组

在数据库中,如果执行只涉及少量记录的查询,数据库通常会使用索引来快速定位感兴趣的记录。如果查询涉及到连接,则可能涉及到查询多个索引。

当我们在批处理的语境中讨论连接时,我们指的是在数据集中解析某种关联的全量存在。

3.2.1 示例:用户活动事件分析

批处理作业中连接的典型例子。用户行为日志与用户档案的连接。

在这里插入图片描述

分析任务可能需要将用户活动与用户档案信息相关联:例如,如果档案包含用户的年龄或出生日期,系统可以确定哪些页面更受哪些年龄段的用户欢迎。但是两张表的数据均不全,因此活动事件需要与用户档案数据库相连接。

为了在批处理过程中实现良好的吞吐量,计算必须(尽可能)限于单台机器上运行。为待处理的每条记录发起随机访问的网络请求是在是太慢了。而且查询远程数据库意味着批处理作业变为非确定的,因为远程数据库中的数据可能会改变。

因此更好的方法是获取用户数据库的副本,并将它和用户行为日志放入同一个分布式系统中。这样 MapReduce 将所有相关记录集中到用一个地方进行高效处理。

3.2.2 排序合并连接

Mapper 的目的是从每个记录中提取一对键值。下图所示:

  • 包含两组 Mapper,其中的一组会扫过活动事件(提取用户 ID 作为键,活动事件作为值)
  • 另一组 Mapper 将会扫过用户数据库(提取用户 ID 作为键,用户的出生日期作为值)

在这里插入图片描述

当 MapReduce 框架通过键对 Mapper 输出进行分区,然后对键值对进行排序是,效果是具有相同 ID 的所有活动事件和用户记录在 Reducer 输入中彼此相邻。

由于 Reducer 一次处理一个特定用户 ID 的所有记录,因此一次只需要将一条用户记录保存在内存中,而不需要通过网络发出任何请求。这个算法称为排序合并连接(sort-merge join),因为 Mapper 的输出时按键排序的,然后 Reducer 将来自连接两侧的有序记录列表合并在一起。

3.2.3 把相关数据放在一起

在排序合并连接中,Mapper 和排序过程确保了所有对特定用户 ID 执行连接操作的必须数据都被放在同一个地方:单词调用 Reducer 的地方。预先排好所有需要的数据,Reducer 可以是相当简单的单线程代码,能够以高吞吐量和低内存开销扫过这些记录。

这种架构可以看做,Mapper 将消息发送给Reducer,当一个 Mapper 发出一个键值对是,这个键的作用就像值应该传递到的目标地址,即所有具有相同键的键值对将被传递到相同的目标。

3.2.4 分组

除了连接之外,「把相关数据放在一起」的另一种常见的模式是,按某个键对记录分组(如 SQL 中的 Group by 子句)。所有带有相同键的记录构成一个组,而下一步往往是在每个组内进行某种聚合操作,例如:

  • 统计每个组中记录的数量(count(*) 聚合)
  • 对某个特定字段求和(sum(fieldname))
  • 按某种分级函数取出排名前 K 条记录

使用 MapReduce 实现这种分组操作的简单方法是设置 Mapper,以便它们生成的键值对使用所需的分组键。然后分区和排序过程将所有具有相同分区键的记录导向同一个 Reducer。因此在 MapReduce 之上实现分组和连接看上去非常相似。

3.2.5 处理偏斜

如果连接的输入存在热键,可以使用一些算法进行补偿。例如,Pig 中的偏斜连接方法首先运行一个抽样作业来确定哪些是热键。连接实际执行时,Mapper 会将热键的关联记录随机发送到几个 Reducer 之一。对于另外一侧的连接输入,与热键相关的记录需要被复制到所有处理该键的 Reducer 上。

这种技术将处理热键的工作分散到多个 Reducer 上,这样可以使其更好地并行化,代价是需要连接另一侧的输入记录复制到多个 Reducer 上。

3.3 Map 侧连接

上一节描述的连接算法在 Reducer 中执行实际的连接逻辑,因此被称为 Reduce 侧连接。

Reduce 侧方法的优点是不需要对输入数据做任何假设:无论其属性和结构如何,Mapper 都可以对其预处理以备连接。然而不利的一面是,排序,复制到 Reducer,以及合并 Reducer 输入,所有这些操作可能开销巨大。

注:如果能对输入数据作出某种假设,则通过 Map 侧连接来加快连接速度是可行的。

3.3.1 广播散列连接

适用于执行 Map 端连接的最简单场景是大数据集与小数据集连接的情况。要点在于小数据集需要足够小,以便可以将其全部加载到每个 Mapper 的内存中。

参与连接的较大输入的每个文件块各有一个 Mapper 。每个 Mapper 都会将较小输入整个加载到内存中。

3.3.2 分区散列连接

如果 Map 侧连接的输入以相同的方式进行分区,则散列连接方式可以独立应用于每个分区。

根据用户 ID 的最后一位十进制数字来对活动事件和用户数据库进行分区。例如, Mapper3 首先将所有具有以 3 结尾的 ID 的用户加载到散列表中,然后扫描 ID 为 3 的每个用户的所有活动事件。

注:如果分区准确无误,可以确定的是,所有可能需要连接的记录都落在同一个编号的分区中。

3.3.3 Map 侧合并连接

如果输入数据集不仅以相同的方式进行分区,而且还基于相同的键进行排序,则可适用另一种 Map 侧连接的变体。在这种情况下,输入是否小到能放入内存并不重要,因为这时候 Mapper 同样可以执行归并操作:按键递增的顺序依次读取两个输入文件,将具有相同键的记录配对。

3.3.4 MapReduce 工作流与 Map 侧连接

当下游作业使用 MapReduce 连接的输出时,选择 Map 侧连接或 Reduce 侧连接都会影响输出的结构。

  • Reduce 侧连接的输出时按照连接键进行分区和排序。
  • Map 端连接的输出则按照与较大输入相同的方式进行分区和排序

4. 批处理工作流的输出

在数据库查询的场景中,将事务处理与分析两种目的区分开来。

  • 事务处理通常是根据键查找少量记录,使用索引,并将其呈现给用户。
  • 分析通常会扫描大量记录,执行分组与聚合,输出通常有报告形式。可以供分析师或经理做决策

注:批处理与分析类似,通常会扫入输入数据集的绝大部分,但是输出通常不是报表

4.1 建立搜索索引

如果需要对一组固定文档执行全文索引,则批处理是一种构建索引的高效方法:Mapper 根据需要对文档集合进行分区,每个 Reducer 构建该分区的索引,并将索引文件写入分布式文件系统。

由于按关键字查询搜索索引是只读操作,因而这些索引文件一旦创建就不可变。

注:如果索引的文档集合发生更改,一种选择是定期重跑整个索引工作流,并在完成后用新索引文件批量替换以前的索引文件。

4.2 键值存储作为批处理输出

搜索索引只是批处理工作流可能输出的一个例子。批处理的另一个常见用途是构建机器学习系统,例如分类器(比如垃圾邮件过滤器,异常检查,图像识别)与推荐系统(比如可能认识的人)。

注:这些批处理作业的输出通常是某种数据库。

思考:批处理过程的输出如何回到 Web 应用可以查询的数据库中?

答:

  • 直接在 web 应用数据库客户端运行 Mapper 或 Reducer 作业
  • 创建一个全新的的数据库,同步更新给 web 应用数据库

4.3 批处理输出的哲学

批处理作业拥有良好的性能且更容易维护:

  • 如果在代码中引入一个错误,而输出错误或损坏了,则可以简单地回滚到代码的先前版本,然后重新运行该作业,输出将重新被纠正。
  • 由于回滚很容易,这种原则有利于敏捷软件开发
  • 如果 Map 或 Reduce 任务失败,MapReduce 框架将自动重新调度,并在同样的输入上再次运行它。如果是由代码中的错误造成的,那么它会不断崩溃,并最终导致作业在多次尝试后失败。如果是由于临时问题导致,那么故障就会被容忍。(ps:重试的意义)
  • 同一组文件可用作各种不同作业的输入,包括计算指标的监控作业并且评估作业的输出是否具有预期的性质。
  • 与 Unix 工具类似, MapReduce 作业将逻辑与布线(配置输入和输出目录)分离,这使得关注点分离,可以重用代码:一个团队可以专注实现一个做好事情的作业;而其他团队可以决定何时何地运行这项作业。

5. Hadoop 与分布式数据库对比

  • 大规模并行处理数据库专注于在一组机器上并行执行分析 SQL 查询。
  • MapReduce 和分布式文件系统的组合更像一个可以运行任意程序的通用操作系统。

5.1 存储多样性

数据库要求根据特定的模型来构造数据,而分布式文件系统中的文件只是字节序列,可以使用任何数据模型和编码来编写,它们可能是数据库记录的集合,但同样可以使文本、图像、视频、传感器读数、稀疏矩阵、特征向量,基因组序列或任何其他类型的数据。

5.2 处理模型的多样性

并非所有类型的处理都可以合理地表达为 SQL 查询。例如,如果要构建机器学习和推荐系统,或者使用相关性排名模型的全文搜索引擎,或者执行图像分析,则很可能需要更一般的数据处理模型。

MapReduce 使工程师能够轻松地在大型数据集上运行自己的代码。有 HDFS 和 MapReduce,则可以轻松构建一个 SQL 查询执行引擎,这正是 Hive 所做的事情。

5.3 针对频繁故障设计

批处理对故障不太敏感,因为就算失败也不会立即影响到用户,而且它们总是能再次运行。MapReduce 可以容忍单个 Map 或 Reduce 任务失败,而不会影响整体作业。因为 MapReduce 作业一般较大,整体重试的策略开销较大。

6. MapReduce 之后

MapReduce 在 2000 年代后期变得非常流行,并受到大量的炒作,但它只是分布式系统的许多可能的编程模型之一。对于不同的数据量,数据结构和处理类型,其他工具可能更适合表示计算。

MapReduce 非常稳健,可以使用它在任务频繁中止的多租户系统上处理几乎任何大量级的数据,并且仍然可以完成工作。

MapReduce 执行模型本身也存在一些问题,这些问题并不能通过增加另一个抽象层次而解决,同时对于某些类型的处理并行比较差。

6.1 物化中间状态

本章开头的日志分析示例使用 Unix 管道将一个命令的输出与另一个命令的输入连接起来。管道并没有完全物化中间状态,而只使用一个小的内存缓存区,将输出增量地流向输入。

在 MapReduce 中,经中间状态写入文件的过程称为物化。其有如下不足:

  • MapReduce 作业只有在前驱作业中的所有任务都完成时才能启动,而由 Unix 管道连接的进程会同时启动,输出一旦生成就会被消费。
  • Mapper 通常是多余的:它们仅仅是读取刚刚由 Reducer 写入的同样文件,为下一个阶段的分区和排序做准备。
  • 将中间状态存储在分布式文件系统中意味着这些文件被复制到多个节点,对这些临时数据这么搞就比较过分了

6.1.1 数据流引擎

将工作流显示建模为数据从几个处理阶段穿过,所以这些系统被称为数据流引擎。像 MapReduce 一样,它们在一条线上通过反复调用用户定义的函数来一次处理一条记录,它们通过输入分区来并行化负载,它们通过网络将一个函数的输出复制到另一个函数的输出。

6.1.2 容错

完全物化中间状态至分布式系统的一个优点是,它具有持久性,这使得 MapReduce 中的容错相当容易:如果一个任务失败,它可以在另一台机器上重新启动,并从文件系统重新读取相同的输入。

注:通过重算数据来从故障中恢复并不总是正确的答案:如果中间状态比源数据小得多,或者如果计算量非常大,那么将中间数据物化为文件可能更为简单。

6.1.3 关于物化的讨论

回到 Unix 的类比,MapReduce 就像是将每个命令的输出写入临时文件,而数据流引擎看起来更像是 Unix 管道。尤其是 Flink 是基于管道执行的思想而建立的:将算子的输出增量地传递给其他算子,不待输入完成便开始处理。

排序算子不可避免地需要消费全部的输入后才能生成输出。因为输入中最后一条输入记录可能具有最小的键,因此需要作为第一条记录输出。因为,需要排序的算子需要至少暂时地累积状态。

6.2 图与迭代处理

批处理上下文中的图非常有趣,其目标是在整个图上执行某种离线处理或分析。这种需求经常出现在机器学习应用或排序系统中。例如,最著名的图形分析算法之一是 PageRank,它试图链接到某个网页的其他网页来估计网页的流行度。

6.2.1 Pregel 处理模型

针对图批处理的优化——批量同步并行计算模型。在 Pregel 模型中,顶点在一次迭代到下一次迭代的过程中会记录它的状态,所以这个函数只需要处理新的传入消息。如果图的某个部分没有被发送消息,那里就不需要做任何工作。

6.2.2 容错

容错是通过在迭代结束时,定期存档所有顶点的状态来实现的,即将其全部状态写入持久化存储。如果某个节点发生故障并且其内存中的状态丢失,则最简单的解决方法是将整个图计算回滚到上一个存档点,然后重启计算。

注:如果算法是确定性的,且消息记录在日志中,那么也可以选择性地只恢复丢失的分区。

6.2.3 并行执行

顶点不需要知道它在哪台物理机器上执行;当它向其他顶点发送消息时,它只是简单地将消息发往某个顶点 ID。图的分区取决于框架——即,确定哪个顶点运行在哪台机器上,以及如何通过网络路由消息,以便它们到达正确的地方。

在实践中,图经常按照分配的顶点 ID 分区,而不会尝试将相关的顶点分组在一起,因此,图算法通常会有很多跨机器通信的额外开销,而中间状态往往比原始图大。通过网络发送消息的开销会显著拖慢分布式图算法的速度。

6.3 高级 API 和语言

自 MapReduce 开始流行的这几年以来,分布式批处理的执行引擎已经很成熟了。到目前为止,基础设施已经足够强大,能够存储和处理超过 PB 的数据。由于在这种规模下的物理执行批处理的问题已经被认为或多或少解决了,所以关注点已经转向其他领域:改进编程模型,提高处理效率,扩大这些技术可以解决的问题集。

高级接口不仅提高了人类的工作效率,也提高了机器层面的作业执行效率。

6.3.1 向声明式查询语言的转变

与硬写执行连接的代码相比,指定连接关系算子的优点是,框架可以分析连接输入的属性,并自动决定哪种上述连接算法最适合当前任务。

通过在高级 API 中引入声明式的部分,并使查询优化器可以在执行期间利用这些来做优化,批处理框架看起来更像并行处理数据库。同时,通过拥有运行任意代码和以任意格式读取数据的可扩展性,它们保持了灵活性的优势。

6.3.2 专用化的不同领域

尽管能够运行任意代码的可扩展性是很重要的,但是也有很多常见的例子,不断重复着标准的处理模式。因而这些模式值得拥有自己的可重用通用构建模型实现。

另一个越来越重要的领域是统计和数值算法,它们是机器学习应用所需要的。

批处理引擎正被用于分布式执行日益广泛的各种领域算法。随着批处理系统获得各种内置功能以及高级声明式算子,且随着并行处理数据库变得更加灵活和易于编程,两者开始看起来相似了:最终,它们都只是存储和处理数据的系统。

7. 碎碎念

如期摘抄完,就在末尾碎碎念几句吧。

  • 你要储蓄你的可爱,眷顾你的善良,变得勇敢,当这个世界越来越坏时,只希望你能越来越好。

  • 如果一听到一种与你相左的意见就要发怒,这表明,你已经下意识地感觉到你那种看法没有充分理由。如果某个人硬要说二加二等于五,你只会感到怜悯而不是愤怒。

  • 喜欢自己是个很好的开始。

猜你喜欢

转载自blog.csdn.net/phantom_111/article/details/120770771