《Designing Data Intensive Applications》读书笔记 - 批处理
Unix 工具批处理
这一章先以 Unix 命令处理日志分析为例,介绍 Unix 设计的哲学,之后介绍 MapReduce 时会比较它们的相似之处。
下面这个命令是一个简单的日志分析,统计访问量最多的前 5 个 URL:
1 | cat /var/log/nginx/access.log | |
同时与 Ruby 脚本的实现代码对比,Unix 链式管道更为简洁。另外一个大的不同时,Ruby 脚本使用了一个哈希表,而 Unix 命令使用了排序。
如果数据量较小,所有的数据都能够放入内存,哈希表的性能更好。如果数据量非常大,不能完全放入内容,排序的方法就更合适。sort
命令可以自动处理大量数据,超出内存会使用磁盘并且利用多核并行排序。
Unix 哲学:每个程序只做一件事,但是做的很好;程序之间通过管道连接;统一的文本接口;逻辑和 I/O 分离。
MapReduce 和分布式文件系统
MapReduce 有点像 Unix 工具,只不过可能分布在上千台机器上。每个 MapReduce 任务就像单个 Unix 命令,接收一个或多个输入,产生一个或多个输出,通常不会修改输入。
MapReduce 读写分布式文件系统上的文件,Hadoop 的 MapReduce 文件系统是 HDFS,基于无共享原则。 每台机器上运行一个守护进程,对外暴露网络服务,允许其它节点访问存储其上的文件。一个中心化的 NameNode 记录文件的位置。
考虑故障,每个文件会有多个副本。
MapReduce 作业执行
以上面的日志分析为例,一个 MapReduce 作业步骤:
- 读取输入文件,将其分成记录,每个记录对应一行日志。
- 调用 mapper 函数提取记录中的 URL 作为 key, value 为空
- 排序所有的键值对
- 调用 reducer 函数处理排序后的键值对
分布式执行
和 Unix 命令行最大的不同是 MapReduce 可以在多个节点上并行执行。map 任务的数量通常由输入文件块的数量决定,而 reduce 任务的数量由用户指定。框架会确保相同 key 的记录会被发送到同一个 reduce 任务。
键值对需要进行排序,但是数据量可能非常大,无法在单台机器上排序,需要分阶段排序。首先,map 任务会对输出进行排序,基于 key 的哈希将输出分为多个分区(对应 reducer),每个分区写入到单独的文件中。
在 mapper 完成任务之后, reducer 开始下载排序后的文件并进行合并同时保留了原来的顺序。
MapReduce 工作流
单个 MapReduce 可以解决的问题有限,像上面的日志分析例子,单个任务只能决定每个 URL 的访问次数,如果需要找到最热门的 URL,需要另外一个 MapReduce 任务。所以,将多个 MapReduce 任务组合成一个工作流是很常见的。
有点像 Unix 中的管道了。
Reduce-Side Join and Grouping
数据库的 join 操作非常常见,比如一个数据集是用户信息,另一个数据集是用户的日志,需要将两个数据集合并到一起。
Sort-merge joins
两个 mapper 任务,一个处理用户信息,一个处理日志,输出的键值对都是用户 ID 和信息。在 reduce 阶段,将两个数据集合并到一起,这个过程叫做 sort-merge join。
mapper 就像发送信息到 reducer,而 key 就像地址。
处理倾斜
如果存在热点,比如一个用户有大量的日志,这个用户的数据会集中在一个 reducer 上,就会导致这个 reducer 的负载很高。
一种方法是,将热点分散到多个 reducer 上。 Hive 使用的是另一种方法,需要先标识出热点,然后在执行 join 时,使用 map-side join。
Map-Side Join
Broadcast hash join
简单来说就是如果用户数据量比较小,直接在 map 阶段将用户数据加载到内存中,然后在 map 阶段就可以进行 join 操作。
Partitioned hash join
如果数据量比较大,不能放入内存,可以将用户数据分区,每个分区对应一个 reducer,然后在 map 阶段将用户数据发送到对应的 reducer 上。
应用
一是建立索引,二是机器学习分类或推荐系统。
与分布式数据库的比较
可以说 MapReduce 更为灵活,存储可以是任意格式数据。另外,MapReduce 是针对频繁故障设计,倒不是硬件太不可靠,而是需要任意终止任务。
MapReduce 之上
MapReduce 很容易理解,但是使用起来非常麻烦,所以有了很多的抽象实现。
中间状态的物化
MapReduce 任务之间的数据传递是通过文件系统,每个任务都会将输出写入到文件系统,下一个任务再读取。和开头的 Unix 例子相比,这样的不足:
- 作业只有在所有先行作业完成之后才能开始
- 不必要的 Mapper
- 存储这些中间状态意味着在节点之间复制
保存这些中间状态的一个好处是容错,如果一个任务失败,可以重新执行。 Flink 和 Spark 避免将中间状态写入文件系统,在机器出现故障时,需要重新计算,这就需要框架追踪数据时如何计算的。
高级 API
高级语言像 Pig 和 Hive,可以将 SQL 转换为 MapReduce 任务。这些语言提供了更高级的抽象,比如 join 操作,分组,过滤等。
声明式查询语言
查询优化器可以利用列式存储只读取需要的列。
总结
这一章感觉内容并不深,尤其是已经对 MapReduce 有所了解的情况。最有意思或许是将其与 Unix 命令行做比较,让人更有启发。