This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
<audio id="audio" title="12 | 我们为什么需要Spark" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8d/e8/8d9f4b613775dd6db5634650012602e8.mp3"></audio>
你好,我是蔡元楠。
今天我要与你分享的主题是“我们为什么需要Spark”。
也许你之前没有做过大规模数据处理的项目但是Spark这个词我相信你一定有所耳闻。
Spark是当今最流行的分布式大规模数据处理引擎被广泛应用在各类大数据处理场景。
2009年美国加州大学伯克利分校的AMP实验室开发了Spark。2013年Spark成为Apache软件基金会旗下的孵化项目。
而现在Spark已经成为了该基金会管理的项目中最活跃的一个。Spark社区也是成长迅速不仅有数以千计的个人贡献者在不断地开发维护还有很多大公司也加入了这个开源项目如Databricks、IBM和华为。
在技术不断高速更迭的程序圈,一个新工具的出现与流行,必然是因为它满足了很大一部分人长期未被满足的需求,或是解决了一个长期让很多人难受的痛点。
所以,在学一个新技术之前,你有必要先了解这门技术出现的意义。这样,你才能更好地理解:它是应用到什么场景的?与同类工具相比,它的优缺点是什么?什么时候用它比其它工具好(或差)?……
至少理解了这些,你才好说自己是真正掌握了这个工具,否则只能说是浅尝辄止,半生不熟。
学习Spark同样是如此。
我们首先要问自己既然已经有了看似很成熟的Hadoop和MapReduce为什么我们还需要Spark它能帮我们解决什么实际问题相比于MapReduce它的优势又是什么
## MapReduce的缺陷
MapReduce通过简单的Map和Reduce的抽象提供了一个编程模型可以在一个由上百台机器组成的集群上并发处理大量的数据集而把计算细节隐藏起来。各种各样的复杂数据处理都可以分解为Map或Reduce的基本元素。
这样复杂的数据处理可以分解为由多个Job包含一个Mapper和一个Reducer组成的有向无环图DAG然后每个Mapper和Reducer放到Hadoop集群上执行就可以得出结果。
我们在第一讲中讲到过MapReduce被硅谷一线公司淘汰的两大主要原因高昂的维护成本、时间性能“达不到”用户的期待。不过除此之外MapReduce还存在诸多局限。
第一MapReduce模型的抽象层次低大量的底层逻辑都需要开发者手工完成。
打个比方写MapReduce的应用就好比用汇编语言去编写一个复杂的游戏。如果你是开发者你会习惯用汇编语言还是使用各种高级语言如Java、C++的现有框架呢?
第二只提供Map和Reduce两个操作。
很多现实的数据处理场景并不适合用这个模型来描述。实现复杂的操作很有技巧性,也会让整个工程变得庞大以及难以维护。
举个例子两个数据集的Join是很基本而且常用的功能但是在MapReduce的世界中需要对这两个数据集做一次Map和Reduce才能得到结果。这样框架对于开发者非常不友好。正如第一讲中提到的维护一个多任务协调的状态机成本很高而且可扩展性非常差。
第三在Hadoop中每一个Job的计算结果都会存储在HDFS文件存储系统中所以每一步计算都要进行硬盘的读取和写入大大增加了系统的延迟。
由于这一原因MapReduce对于迭代算法的处理性能很差而且很耗资源。因为迭代的每一步都要对HDFS进行读写所以每一步都需要差不多的等待时间。
第四,只支持批数据处理,欠缺对流数据处理的支持。
因此在Hadoop推出后有很多人想办法对Hadoop进行优化其中发展到现在最成熟的就是Spark。
接下来就让我们看一下Spark是如何对上述问题进行优化的。
## Spark的优势
Spark最基本的数据抽象叫作弹性分布式数据集Resilient Distributed Dataset, RDD它代表一个可以被分区partition的只读数据集它内部可以有很多分区每个分区又有大量的数据记录record
RDD是Spark最基本的数据结构。Spark定义了很多对RDD的操作。对RDD的任何操作都可以像函数式编程中操作内存中的集合一样直观、简便使得实现数据处理的代码非常简短高效。这些我们会在这一模块中的后续文章中仔细阐述。
Spark提供了很多对RDD的操作如Map、Filter、flatMap、groupByKey和Union等等极大地提升了对各种复杂场景的支持。开发者既不用再绞尽脑汁挖掘MapReduce模型的潜力也不用维护复杂的MapReduce状态机。
相对于Hadoop的MapReduce会将中间数据存放到硬盘中Spark会把中间数据缓存在内存中从而减少了很多由于硬盘读写而导致的延迟大大加快了处理速度。
Databricks团队曾经做过一个实验他们用Spark排序一个100TB的静态数据集仅仅用时23分钟。而之前用Hadoop做到的最快记录也用了高达72分钟。此外Spark还只用了Hadoop所用的计算资源的1/10耗时只有Hadoop的1/3。
这个例子充分体现出Spark数据处理的最大优势——速度。
在某些需要交互式查询内存数据的场景中Spark的性能优势更加明显。
根据Databricks团队的结果显示Spark的处理速度是Hadoop的100倍。即使是对硬盘上的数据进行处理Spark的性能也达到了Hadoop的10倍。
由于Spark可以把迭代过程中每一步的计算结果都缓存在内存中所以非常适用于各类迭代算法。
Spark第一次启动时需要把数据载入到内存之后的迭代可以直接在内存里利用中间结果做不落地的运算。所以后期的迭代速度快到可以忽略不计。在当今机器学习和人工智能大热的环境下Spark无疑是更好的数据处理引擎。
下图是在Spark和Hadoop上运行逻辑回归算法的运行时间对比。
<img src="https://static001.geekbang.org/resource/image/54/3d/54e4df946206a4a2168a25af8814843d.png" alt="">
可以看出Hadoop做每一次迭代运算的时间基本相同而Spark除了第一次载入数据到内存以外别的迭代时间基本可以忽略。
在任务task级别上Spark的并行机制是多线程模型而MapReduce是多进程模型。
多进程模型便于细粒度控制每个任务占用的资源,但会消耗较多的启动时间。
而Spark同一节点上的任务以多线程的方式运行在一个JVM进程中可以带来更快的启动速度、更高的CPU利用率以及更好的内存共享。
从前文中你可以看出Spark作为新的分布式数据处理引擎对MapReduce进行了很多改进使得性能大大提升并且更加适用于新时代的数据处理场景。
但是Spark并不是一个完全替代Hadoop的全新工具。
因为Hadoop还包含了很多组件
- 数据存储层分布式文件存储系统HDFS分布式数据库存储的HBase
- 数据处理层进行数据处理的MapReduce负责集群和资源管理的YARN
- 数据访问层Hive、Pig、Mahout……
从狭义上来看Spark只是MapReduce的替代方案大部分应用场景中它还要依赖于HDFS和HBase来存储数据依赖于YARN来管理集群和资源。
当然Spark并不是一定要依附于Hadoop才能生存它还可以运行在Apache Mesos、Kubernetes、standalone等其他云平台上。
<img src="https://static001.geekbang.org/resource/image/bc/0c/bc01239280bb853ca1d00c0fb3a8150c.jpg" alt="">
此外作为通用的数据处理平台Spark有五个主要的扩展库分别是支持结构化数据的Spark SQL、处理实时数据的Spark Streaming、用于机器学习的MLlib、用于图计算的GraphX、用于统计分析的SparkR。
这些扩展库与Spark核心API高度整合在一起使得Spark平台可以广泛地应用在不同数据处理场景中。
## 小结
通过今天的学习我们了解了Spark相较于MapReduce的主要优势那就是快、易于开发及维护和更高的适用性。我们还初步掌握了Spark系统的架构。
MapReduce作为分布式数据处理的开山鼻祖虽然有很多缺陷但是它的设计思想不仅没有过时而且还影响了新的数据处理系统的设计如Spark、Storm、Presto、Impala等。
Spark并没有全新的理论基础它是一点点地在工程和学术的结合基础上做出来的。可以说它站在了Hadoop和MapReduce两个巨人的肩膀上。在这一模块中我们会对Spark的架构、核心概念、API以及各个扩展库进行深入的讨论并且结合常见的应用例子进行实战演练从而帮助你彻底掌握这一当今最流行的数据处理平台。
## 思考题
你认为有哪些MapReduce的缺点是在Spark框架中依然存在的用什么思路可以解决
欢迎你把答案写在留言区,与我和其他同学一起讨论。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,129 @@
<audio id="audio" title="13 | 弹性分布式数据集Spark大厦的地基" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/47/79/4761378fe34794f16fede8d1dca6f779.mp3"></audio>
你好,我是蔡元楠。
今天我要与你分享的主题是“弹性分布式数据集”。
上一讲中提到Spark最基本的数据抽象是弹性分布式数据集Resilient Distributed Dataset, 下文用RDD代指
Spark基于RDD定义了很多数据操作从而使得数据处理的代码十分简洁、高效。所以要想深入学习Spark我们必须首先理解RDD的设计思想和特性。
## 为什么需要新的数据抽象模型?
传统的MapReduce框架之所以运行速度缓慢很重要的原因就是有向无环图的中间计算结果需要写入硬盘这样的稳定存储介质中来防止运行结果丢失。
而每次调用中间计算结果都需要要进行一次硬盘的读取,反复对硬盘进行读写操作以及潜在的数据复制和序列化操作大大提高了计算的延迟。
因此很多研究人员试图提出一个新的分布式存储方案不仅保持之前系统的稳定性、错误恢复和可扩展性还要尽可能地减少硬盘I/O操作。
一个可行的设想就是在分布式内存中存储中间计算的结果因为对内存的读写操作速度远快于硬盘。而RDD就是一个基于分布式内存的数据抽象它不仅支持基于工作集的应用同时具有数据流模型的特点。
## RDD的定义
弹性分布式数据集是英文直译的名字乍一看这个名字相信你会不知所云。如果你去Google或者百度搜索它的定义你会得到如下结果
**RDD表示已被分区、不可变的并能够被并行操作的数据集合。**
这个定义很不直观我认识的很多Spark初学者在查阅了很多资料后还是对RDD一头雾水很难理解这个抽象的概念。接下来让我们一起来对这个晦涩的概念抽丝剥茧见其真义。
在上述定义以及RDD的中文译名中我们不难发现RDD有以下基本特性分区、不可变和并行操作。接下来让我分别讲解这些特点。
### 分区
顾名思义分区代表同一个RDD包含的数据被存储在系统的不同节点中这也是它可以被并行处理的前提。
逻辑上我们可以认为RDD是一个大的数组。数组中的每个元素代表一个分区Partition
在物理存储中每个分区指向一个存放在内存或者硬盘中的数据块Block而这些数据块是独立的它们可以被存放在系统中的不同节点。
所以RDD只是抽象意义的数据集合分区内部并不会存储具体的数据。下图很好地展示了RDD的分区逻辑结构
<img src="https://static001.geekbang.org/resource/image/2f/9e/2f9ec57cdedf65be382a8ec09826029e.jpg" alt="">
RDD中的每个分区存有它在该RDD中的index。通过RDD的ID和分区的index可以唯一确定对应数据块的编号从而通过底层存储层的接口中提取到数据进行处理。
在集群中,各个节点上的数据块会尽可能地存放在内存中,只有当内存没有空间时才会存入硬盘。这样可以最大化地减少硬盘读写的开销。
虽然 RDD 内部存储的数据是只读的,但是,我们可以去修改(例如通过 repartition 转换操作)并行计算单元的划分结构,也就是分区的数量。
### 不可变性
不可变性代表每一个RDD都是只读的它所包含的分区信息不可以被改变。既然已有的RDD不可以被改变我们只可以对现有的RDD进行**转换**Transformation操作得到新的RDD作为中间计算的结果。从某种程度上讲RDD与函数式编程的Collection很相似。
```
lines = sc.textFile(&quot;data.txt&quot;)
lineLengths = lines.map(lambda s: len(s))
totalLength = lineLengths.reduce(lambda a, b: a + b)
```
在上述的简单例子中我们首先读入文本文件data.txt创建了第一个RDD lines它的每一个元素是一行文本。然后调用map函数去映射产生第二个RDD lineLengths每个元素代表每一行简单文本的字数。最后调用reduce函数去得到第三个RDD totalLength它只有一个元素代表整个文本的总字数。
那么这样会带来什么好处呢显然对于代表中间结果的RDD我们需要记录它是通过哪个RDD进行哪些转换操作得来即**依赖关系**,而不用立刻去具体存储计算出的数据本身。
这样做有助于提升Spark的计算效率并且使错误恢复更加容易。
试想在一个有N步的计算模型中如果记载第N步输出RDD的节点发生故障数据丢失我们可以从第N-1步的RDD出发再次计算而无需重复整个N步计算过程。这样的容错特性也是RDD为什么是一个“弹性”的数据集的原因之一。后边我们会提到RDD如何存储这样的依赖关系。
### 并行操作
由于单个RDD的分区特性使得它天然支持并行操作即不同节点上的数据可以被分别处理然后产生一个新的RDD。
## RDD的结构
通过上述讲解我们了解了RDD的基本特性——分区、不可变和并行计算。而且我们还提到每一个RDD里都会包括分区信息、所依赖的父RDD以及通过怎样的转换操作才能由父RDD得来等信息。
实际上RDD的结构远比你想象的要复杂让我们来看一个RDD的简易结构示意图
<img src="https://static001.geekbang.org/resource/image/8c/1c/8cae25f4d16a34be77fd3e84133d6a1c.png" alt="">
SparkContext是所有Spark功能的入口它代表了与Spark节点的连接可以用来创建RDD对象以及在节点中的广播变量等。一个线程只有一个SparkContext。SparkConf则是一些参数配置信息。感兴趣的同学可以去阅读官方的技术文档一些相对不重要的概念我就不再赘述了。
Partitions前文中我已经提到过它代表RDD中数据的逻辑结构每个Partition会映射到某个节点内存或硬盘的一个数据块。
Partitioner决定了RDD的分区方式目前有两种主流的分区方式Hash partitioner和Range partitioner。Hash顾名思义就是对数据的Key进行散列分区Range则是按照Key的排序进行均匀分区。此外我们还可以创建自定义的Partitioner。
### 依赖关系
Dependencies是RDD中最重要的组件之一。如前文所说Spark不需要将每个中间计算结果进行数据复制以防数据丢失因为每一步产生的RDD里都会存储它的依赖关系即它是通过哪个RDD经过哪个转换操作得到的。
细心的读者会问这样一个问题父RDD的分区和子RDD的分区之间是否是一对一的对应关系呢Spark支持两种依赖关系窄依赖Narrow Dependency和宽依赖Wide Dependency
<img src="https://static001.geekbang.org/resource/image/5e/e1/5eed459f5f1960e2526484dc014ed5e1.jpg" alt="">
窄依赖就是父RDD的分区可以一一对应到子RDD的分区宽依赖就是父RDD的每个分区可以被多个子RDD的分区使用。
<img src="https://static001.geekbang.org/resource/image/98/f9/989682681b344d31c61b02368ca227f9.jpg" alt="">
显然窄依赖允许子RDD的每个分区可以被并行处理产生而宽依赖则必须等父RDD的所有分区都被计算好之后才能开始处理。
如上图所示一些转换操作如map、filter会产生窄依赖关系而Join、groupBy则会生成宽依赖关系。
这很容易理解因为map是将分区里的每一个元素通过计算转化为另一个元素一个分区里的数据不会跑到两个不同的分区。而groupBy则要将拥有所有分区里有相同Key的元素放到同一个目标分区而每一个父分区都可能包含各种Key的元素所以它可能被任意一个子分区所依赖。
Spark之所以要区分宽依赖和窄依赖是出于以下两点考虑
<li>
窄依赖可以支持在同一个节点上链式执行多条命令,例如在执行了 map 后,紧接着执行 filter。相反宽依赖需要所有的父分区都是可用的可能还需要调用类似 MapReduce 之类的操作进行跨节点传递。
</li>
<li>
从失败恢复的角度考虑,窄依赖的失败恢复更有效,因为它只需要重新计算丢失的父分区即可,而宽依赖牵涉到 RDD 各级的多个父分区。
</li>
## 小结
弹性分布式数据集作为Spark的基本数据抽象相较于Hadoop/MapReduce的数据模型而言各方面都有很大的提升。
首先它的数据可以尽可能地存在内存中从而大大提高的数据处理的效率其次它是分区存储所以天然支持并行处理而且它还存储了每一步骤计算结果之间的依赖关系从而大大提升了数据容错性和错误恢复的正确率使Spark更加可靠。
下一讲我们会继续深入研究RDD的容错机制、任务执行机制以及Spark定义在RDD上的各种转换与动作操作。
## 思考题
窄依赖是指父RDD的每一个分区都可以唯一对应子RDD中的分区那么是否意味着子RDD中的一个分区只可以对应父RDD中的一个分区呢如果子RDD的一个分区需要由父RDD中若干个分区计算得来是否还算窄依赖
最后,欢迎你把对弹性分布式数据集的疑问写在留言区,与我和其他同学一起讨论。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,214 @@
<audio id="audio" title="14 | 弹性分布式数据集Spark大厦的地基" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b3/91/b30d9e8a1bd4d680e9724e5530535c91.mp3"></audio>
你好,我是蔡元楠。
上一讲我们介绍了弹性分布式数据集RDD的定义、特性以及结构并且深入讨论了依赖关系Dependencies
今天让我们一起来继续学习RDD的其他特性。
## RDD的结构
首先我来介绍一下RDD结构中其他的几个知识点检查点Checkpoint、存储级别 Storage Level和迭代函数Iterator
<img src="https://static001.geekbang.org/resource/image/8c/1c/8cae25f4d16a34be77fd3e84133d6a1c.png" alt="">
通过上一讲你应该已经知道了基于RDD的依赖关系如果任意一个RDD在相应的节点丢失你只需要从上一步的RDD出发再次计算便可恢复该RDD。
但是如果一个RDD的依赖链比较长而且中间又有多个RDD出现故障的话进行恢复可能会非常耗费时间和计算资源。
而检查点Checkpoint的引入就是为了优化这些情况下的数据恢复。
很多数据库系统都有检查点机制在连续的transaction列表中记录某几个transaction后数据的内容从而加快错误恢复。
RDD中的检查点的思想与之类似。
在计算过程中对于一些计算过程比较耗时的RDD我们可以将它缓存至硬盘或HDFS中标记这个RDD有被检查点处理过并且清空它的所有依赖关系。同时给它新建一个依赖于CheckpointRDD的依赖关系CheckpointRDD可以用来从硬盘中读取RDD和生成新的分区信息。
这样当某个子RDD需要错误恢复时回溯至该RDD发现它被检查点记录过就可以直接去硬盘中读取这个RDD而无需再向前回溯计算。
存储级别Storage Level是一个枚举类型用来记录RDD持久化时的存储级别常用的有以下几个
<li>
MEMORY_ONLY只缓存在内存中如果内存空间不够则不缓存多出来的部分。这是RDD存储级别的默认值。
</li>
<li>
MEMORY_AND_DISK缓存在内存中如果空间不够则缓存在硬盘中。
</li>
<li>
DISK_ONLY只缓存在硬盘中。
</li>
<li>
MEMORY_ONLY_2和MEMORY_AND_DISK_2等与上面的级别功能相同只不过每个分区在集群中两个节点上建立副本。
</li>
这就是我们在前文提到过的Spark相比于Hadoop在性能上的提升。我们可以随时把计算好的RDD缓存在内存中以便下次计算时使用这大幅度减小了硬盘读写的开销。
迭代函数Iterator和计算函数Compute是用来表示RDD怎样通过父RDD计算得到的。
迭代函数会首先判断缓存中是否有想要计算的RDD如果有就直接读取如果没有就查找想要计算的RDD是否被检查点处理过。如果有就直接读取如果没有就调用计算函数向上递归查找父RDD进行计算。
到现在相信你已经对弹性分布式数据集的基本结构有了初步了解。但是光理解RDD的结构是远远不够的我们的终极目标是使用RDD进行数据处理。
要使用RDD进行数据处理你需要先了解一些RDD的数据操作。
在[第12讲](http://time.geekbang.org/column/article/94410)中我曾经提过相比起MapReduce只支持两种数据操作Spark支持大量的基本操作从而减轻了程序员的负担。
接下来让我们进一步了解基于RDD的各种数据操作。
## RDD的转换操作
RDD的数据操作分为两种转换Transformation和动作Action
顾名思义转换是用来把一个RDD转换成另一个RDD而动作则是通过计算返回一个结果。
不难想到之前举例的map、filter、groupByKey等都属于转换操作。
### Map
map是最基本的转换操作。
与MapReduce中的map一样它把一个RDD中的所有数据通过一个函数映射成一个新的RDD任何原RDD中的元素在新RDD中都有且只有一个元素与之对应。
在这一讲中提到的所有的操作,我都会使用代码举例,帮助你更好地理解。
```
rdd = sc.parallelize([&quot;b&quot;, &quot;a&quot;, &quot;c&quot;])
rdd2 = rdd.map(lambda x: (x, 1)) // [('b', 1), ('a', 1), ('c', 1)]
```
### Filter
filter这个操作是选择原RDD里所有数据中满足某个特定条件的数据去返回一个新的RDD。如下例所示通过filter只选出了所有的偶数。
```
rdd = sc.parallelize([1, 2, 3, 4, 5])
rdd2 = rdd.filter(lambda x: x % 2 == 0) // [2, 4]
```
### mapPartitions
mapPartitions是map的变种。不同于map的输入函数是应用于RDD中每个元素mapPartitions的输入函数是应用于RDD的每个分区也就是把每个分区中的内容作为整体来处理的所以输入函数的类型是Iterator[T] =&gt; Iterator[U]。
```
rdd = sc.parallelize([1, 2, 3, 4], 2)
def f(iterator): yield sum(iterator)
rdd2 = rdd.mapPartitions(f) // [3, 7]
```
在mapPartitions的例子中我们首先创建了一个有两个分区的RDD。mapPartitions的输入函数是对每个分区内的元素求和所以返回的RDD包含两个元素1+2=3 和3+4=7。
### groupByKey
groupByKey和SQL中的groupBy类似是把对象的集合按某个Key来归类返回的RDD中每个Key对应一个序列。
```
rdd = sc.parallelize([(&quot;a&quot;, 1), (&quot;b&quot;, 1), (&quot;a&quot;, 2)])
rdd.groupByKey().collect()
//&quot;a&quot; [1, 2]
//&quot;b&quot; [1]
```
在此我们只列举这几个常用的、有代表性的操作对其他转换操作感兴趣的同学可以去自行查阅官方的API文档。
## RDD的动作操作
让我们再来看几个常用的动作操作。
### Collect
RDD中的动作操作collect与函数式编程中的collect类似它会以数组的形式返回RDD的所有元素。需要注意的是collect操作只有在输出数组所含的数据数量较小时使用因为所有的数据都会载入到程序的内存中如果数据量较大会占用大量JVM内存导致内存溢出。
```
rdd = sc.parallelize([&quot;b&quot;, &quot;a&quot;, &quot;c&quot;])
rdd.map(lambda x: (x, 1)).collect() // [('b', 1), ('a', 1), ('c', 1)]
```
实际上上述转换操作中所有的例子最后都需要将RDD的元素collect成数组才能得到标记好的输出。
### Reduce
与MapReduce中的reduce类似它会把RDD中的元素根据一个输入函数聚合起来。
```
from operator import add
sc.parallelize([1, 2, 3, 4, 5]).reduce(add) // 15
```
### Count
Count会返回RDD中元素的个数。
sc.parallelize([2, 3, 4]).count() // 3
### CountByKey
仅适用于Key-Value pair类型的 RDD返回具有每个 key 的计数的&lt;Key, Count&gt;的map。
```
rdd = sc.parallelize([(&quot;a&quot;, 1), (&quot;b&quot;, 1), (&quot;a&quot;, 1)])
sorted(rdd.countByKey().items()) // [('a', 2), ('b', 1)]
```
讲到这你可能会问了为什么要区分转换和动作呢虽然转换是生成新的RDD动作是把RDD进行计算生成一个结果它们本质上不都是计算吗
这是因为所有转换操作都很懒它只是生成新的RDD并且记录依赖关系。
但是Spark并不会立刻计算出新RDD中各个分区的数值。直到遇到一个动作时数据才会被计算并且输出结果给Driver。
比如在之前的例子中你先对RDD进行map转换再进行collect动作这时map后生成的RDD不会立即被计算。只有当执行到collect操作时map才会被计算。而且map之后得到的较大的数据量并不会传给Driver只有collect动作的结果才会传递给Driver。
这种惰性求值的设计优势是什么呢?让我们来看这样一个例子。
假设你要从一个很大的文本文件中筛选出包含某个词语的行然后返回第一个这样的文本行。你需要先读取文件textFile()生成rdd1然后使用filter()方法生成rdd2最后是行动操作first(),返回第一个元素。
读取文件的时候会把所有的行都存储起来,但我们马上就要筛选出只具有特定词组的行了,等筛选出来之后又要求只输出第一个。这样是不是太浪费存储空间了呢?确实。
所以实际上Spark是在行动操作first()的时候开始真正的运算只扫描第一个匹配的行不需要读取整个文件。所以惰性求值的设计可以让Spark的运算更加高效和快速。
让我们总结一下Spark执行操作的流程吧。
Spark在每次转换操作的时候使用了新产生的 RDD 来记录计算逻辑,这样就把作用在 RDD 上的所有计算逻辑串起来,形成了一个链条。当对 RDD 进行动作时Spark 会从计算链的最后一个RDD开始依次从上一个RDD获取数据并执行计算逻辑最后输出结果。
## RDD的持久化缓存
每当我们对RDD调用一个新的action操作时整个RDD都会从头开始运算。因此如果某个RDD会被反复重用的话每次都从头计算非常低效我们应该对多次使用的RDD进行一个持久化操作。
Spark的persist()和cache()方法支持将RDD的数据缓存至内存或硬盘中这样当下次对同一RDD进行Action操作时可以直接读取RDD的结果大幅提高了Spark的计算效率。
```
rdd = sc.parallelize([1, 2, 3, 4, 5])
rdd1 = rdd.map(lambda x: x+5)
rdd2 = rdd1.filter(lambda x: x % 2 == 0)
rdd2.persist()
count = rdd2.count() // 3
first = rdd2.first() // 6
rdd2.unpersist()
```
在文中的代码例子中你可以看到我们对RDD2进行了多个不同的action操作。由于在第四行我把RDD2的结果缓存在内存中所以Spark无需从一开始的rdd开始算起了持久化处理过的RDD只有第一次有action操作时才会从源头计算之后就把结果存储下来所以在这个例子中count需要从源头开始计算而first不需要
在缓存RDD的时候它所有的依赖关系也会被一并存下来。所以持久化的RDD有自动的容错机制。如果RDD的任一分区丢失了通过使用原先创建它的转换操作它将会被自动重算。
持久化可以选择不同的存储级别。正如我们讲RDD的结构时提到的一样有MEMORY_ONLYMEMORY_AND_DISKDISK_ONLY等。cache()方法会默认取MEMORY_ONLY这一级别。
## 小结
Spark在每次转换操作的时候使用了新产生的 RDD 来记录计算逻辑,这样就把作用在 RDD 上的所有计算逻辑串起来形成了一个链条,但是并不会真的去计算结果。当对 RDD 进行动作Action时Spark 会从计算链的最后一个RDD开始利用迭代函数Iterator和计算函数Compute依次从上一个RDD获取数据并执行计算逻辑最后输出结果。
此外我们可以通过将一些需要复杂计算和经常调用的RDD进行持久化处理从而提升计算效率。
## 思考题
对RDD进行持久化操作和记录Checkpoint有什么区别呢
欢迎你把对弹性分布式数据集的疑问写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,163 @@
<audio id="audio" title="15 | Spark SQLSpark数据查询的利器" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/83/48/837681afb98c553e15107a4483e1df48.mp3"></audio>
你好,我是蔡元楠。
上一讲中,我介绍了弹性分布式数据集的特性和它支持的各种数据操作。
不过在实际的开发过程中我们并不是总需要在RDD的层次进行编程。
就好比编程刚发明的年代工程师只能用汇编语言到后来才慢慢发展出高级语言如Basic、C、Java等。使用高级语言大大提升了开发者的效率。
同样的Spark生态系统也提供很多库让我们在不同的场景中使用。
今天让我们来一起探讨Spark最常用的数据查询模块——Spark SQL。
## Spark SQL 发展历史
几年前Hadoop/MapReduce在企业生产中的大量使用HDFS上积累了大量数据。
由于MapReduce对于开发者而言使用难度较大大部分开发人员最熟悉的还是传统的关系型数据库。
为了方便大多数开发人员使用HadoopHive应运而生。
Hive提供类似SQL的编程接口HQL语句经过语法解析、逻辑计划、物理计划转化成MapReduce程序执行使得开发人员很容易对HDFS上存储的数据进行查询和分析。
在Spark刚问世的时候Spark团队也开发了一个Shark来支持用SQL语言来查询Spark的数据。
Shark的本质就是Hive它修改了Hive的内存管理模块大幅优化了运行速度是Hive的10倍到100倍之多。
但是Shark对于Hive的依赖严重影响了Spark的发展。Spark想要定义的是一个统一的技术栈和完整的生态不可能允许有这样的外在依赖。
试想如果Spark想发布新的功能还需要等Hive的更新那么势必会很难执行。此外依赖于Hive还制约了Spark各个组件的相互集成Shark也无法利用Spark的特性进行深度优化。
<img src="https://static001.geekbang.org/resource/image/68/75/68a739ff869d714a32c7760d1a439a75.png" alt="">
所以2014年7月1日Spark团队就将Shark交给Hive进行管理转而开发了SparkSQL。
SparkSQL摒弃了Shark的将SQL语句转化为Spark RDD的执行引擎换成自己团队重新开发的执行引擎。
Spark SQL不仅将关系型数据库的处理模式和Spark的函数式编程相结合还兼容多种数据格式包括Hive、RDD、JSON文件、CSV文件等。
可以说Spark SQL的问世大大加快了Spark生态的发展。
## Spark SQL的架构
Spark SQL本质上是一个库。它运行在Spark的核心执行引擎之上。
<img src="https://static001.geekbang.org/resource/image/3b/13/3bdb29b1d697e3530d1efbd05e694e13.png" alt="">
如上图所示它提供类似于SQL的操作接口允许数据仓库应用程序直接获取数据允许使用者通过命令行操作来交互地查询数据还提供两个APIDataFrame API和DataSet API。
Java、Python和Scala的应用程序可以通过这两个API来读取和写入RDD。
此外正如我们在上一讲介绍的应用程序还可以直接操作RDD。
使用Spark SQL会让开发者觉得好像是在操作一个关系型数据库一样而不是在操作RDD。这是它优于原生的RDD API的地方。
与基本的Spark RDD API不同Spark SQL提供的接口为Spark提供了关于数据结构和正在执行的计算的更多信息。
在内部Spark SQL使用这些额外的信息来执行额外的优化。虽然Spark SQL支持多种交互方式但是在计算结果时均使用相同的执行引擎。
这种统一意味着开发人员可以轻松地在不同的API之间来回切换基于这些API提供了表达给定转换的最自然的方式。
接下来让我们进一步了解DataSet和DataFrame。
## DataSet
DataSet顾名思义就是数据集的意思它是Spark 1.6新引入的接口。
同弹性分布式数据集类似DataSet也是不可变分布式的数据单元它既有与RDD类似的各种转换和动作函数定义而且还享受Spark SQL优化过的执行引擎使得数据搜索效率更高。
DataSet支持的转换和动作也和RDD类似比如map、filter、select、count、show及把数据写入文件系统中。
同样地DataSet上的转换操作也不会被立刻执行只是先生成新的DataSet只有当遇到动作操作才会把之前的转换操作一并执行生成结果。
所以DataSet的内部结构包含了逻辑计划即生成该数据集所需要的运算。
当动作操作执行时Spark SQL的查询优化器会优化这个逻辑计划并生成一个可以分布式执行的、包含分区信息的物理计划。
那么DataSet和RDD的区别是什么呢
通过之前的叙述我们知道DataSet API是Spark SQL的一个组件。那么你应该能很容易地联想到DataSet也具有关系型数据库中表的特性。
是的DataSet所描述的数据都被组织到有名字的列中就像关系型数据库中的表一样。
<img src="https://static001.geekbang.org/resource/image/5a/e3/5a6fd6e91c92c166d279711bf9c761e3.png" alt="">
如上图所示左侧的RDD虽然以People为类型参数但Spark框架本身不了解People类的内部结构。所有的操作都以People为单位执行。
而右侧的DataSet却提供了详细的结构信息与每列的数据类型。
这让Spark SQL可以清楚地知道该数据集中包含哪些列每列的名称和类型各是什么。也就是说DataSet提供数据表的schema信息。这样的结构使得DataSet API的执行效率更高。
试想如果我们要查询People的年龄信息Spark SQL执行的时候可以依靠查询优化器仅仅把需要的那一列取出来其他列的信息根本就不需要去读取了。所以有了这些信息以后在编译的时候能够做更多的优化。
其次由于DataSet存储了每列的数据类型。所以在程序编译时可以执行类型检测。
## DataFrame
DataFrame可以被看作是一种特殊的DataSet。它也是关系型数据库中表一样的结构化存储机制也是分布式不可变的数据结构。
但是它的每一列并不存储类型信息所以在编译时并不能发现类型错误。DataFrame每一行的类型固定为Row他可以被当作DataSet[Row]来处理,我们必须要通过解析才能获取各列的值。
所以对于DataSet我们可以用类似people.name来访问一个人的名字而对于DataFrame我们一定要用类似people.get As [String] (“name”)来访问。
## RDD、DataFrame、DataSet对比
学习Spark到现在我们已经接触了三种基本的数据结构RDD、DataFrame和DataSet。接下来你的表格中你可以看到它们的异同点思考一下怎样在实际工程中选择。
<img src="https://static001.geekbang.org/resource/image/40/ef/40691757146e1b480e08969e676644ef.png" alt="">
### 发展历史
从发展历史上来看RDD API在第一代Spark中就存在是整个Spark框架的基石。
接下来为了方便熟悉关系型数据库和SQL的开发人员使用在RDD的基础上Spark创建了DataFrame API。依靠它我们可以方便地对数据的列进行操作。
DataSet最早被加入Spark SQL是在Spark 1.6它在DataFrame的基础上添加了对数据的每一列的类型的限制。
在Spark 2.0中DataFrame和DataSet被统一。DataFrame作为DataSet[Row]存在。在弱类型的语言如Python中DataFrame API依然存在但是在Java中DataFrame API已经不复存在了。
### 不变性与分区
由于DataSet和DataFrame都是基于RDD的所以它们都拥有RDD的基本特性在此不做赘述。而且我们可以通过简单的 API在 DataFrame或 Dataset与RDD之间进行无缝切换。
### 性能
DataFrame和DataSet的性能要比RDD更好。
Spark程序运行时Spark SQL中的查询优化器会对语句进行分析并生成优化过的RDD在底层执行。
举个例子如果我们想先对一堆数据进行GroupBy再进行Filter操作这无疑是低效的因为我们并不需要对所有数据都GroupBy。
如果用RDD API实现这一语句在执行时它只会机械地按顺序执行。而如果用DataFrame/DataSet APISpark SQL的Catalyst优化器会将Filter操作和GroupBy操作调换顺序从而提高执行效率。
下图反映了这一优化过程。
<img src="https://static001.geekbang.org/resource/image/d6/42/d6162126bae14517aa163b3885c13a42.png" alt="">
### 错误检测
RDD和DataSet都是类型安全的而DataFrame并不是类型安全的。这是因为它不存储每一列的信息如名字和类型。
使用DataFrame API时我们可以选择一个并不存在的列这个错误只有在代码被执行时才会抛出。如果使用DataSet API在编译时就会检测到这个错误。
## 小结
DataFrame和DataSet是Spark SQL提供的基于RDD的结构化数据抽象。
它既有RDD不可变、分区、存储依赖关系等特性又拥有类似于关系型数据库的结构化信息。
所以基于DataFrame和DataSet API开发出的程序会被自动优化使得开发人员不需要操作底层的RDD API来进行手动优化大大提升开发效率。
但是RDD API对于非结构化的数据处理有独特的优势比如文本流数据而且更方便我们做底层的操作。所以在开发中我们还是需要根据实际情况来选择使用哪种API。
## 思考题
什么场景适合使用DataFrame API什么场景适合使用DataSet API
欢迎你把答案写在留言区,与我和其他同学一起讨论。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,137 @@
<audio id="audio" title="16 | Spark StreamingSpark的实时流计算API" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ad/bd/ad03ca21670e43c66a1f2808c29dfdbd.mp3"></audio>
你好,我是蔡元楠。
今天我要与你分享的内容是“Spark Streaming”。
通过上一讲的内容我们深入了解了Spark SQL API。通过它我们可以像查询关系型数据库一样查询Spark的数据并且对原生数据做相应的转换和动作。
但是无论是DataFrame API还是DataSet API都是基于批处理模式对静态数据进行处理的。比如在每天某个特定的时间对一天的日志进行处理分析。
在第二章中你已经知道了批处理和流处理是大数据处理最常见的两个场景。那么作为当下最流行的大数据处理平台之一Spark是否支持流处理呢
答案是肯定的。
早在2013年Spark的流处理组件Spark Streaming就发布了。之后经过好几年的迭代与改进现在的Spark Streaming已经非常成熟在业界应用十分广泛。
今天就让我们一起揭开Spark Streaming的神秘面纱让它成为我们手中的利器。
## Spark Streaming的原理
Spark Streaming的原理与微积分的思想很类似。
在大学的微积分课上,你的老师一定说过,微分就是无限细分,积分就是对无限细分的每一段进行求和。它本质上把一个连续的问题转换成了无限个离散的问题。
比如用微积分思想求下图中阴影部分S的面积。
<img src="https://static001.geekbang.org/resource/image/1c/eb/1cef18cc51ef652c90d05c170c04e7eb.png" alt="">
我们可以把S无限细分成无数个小矩形因为矩形的宽足够短所以它顶端的边近似是一个直线。这样把容易计算的矩形面积相加就得到不容易直接计算的不规则图形面积。
你知道流处理的数据是一系列连续不断变化且无边界的。我们永远无法预测下一秒的数据是什么样。Spark Streaming用时间片拆分了无限的数据流然后对每一个数据片用类似于批处理的方法进行处理输出的数据也是一块一块的。如下图所示。
<img src="https://static001.geekbang.org/resource/image/2e/a2/2e5d3fdbe0bb09a7f2cf219df1d41ca2.png" alt="">
Spark Streaming提供一个对于流数据的抽象DStream。DStream可以由来自Apache Kafka、Flume或者HDFS的流数据生成也可以由别的DStream经过各种转换操作得来。讲到这里你是不是觉得内容似曾相识
没错底层DStream也是由很多个序列化的RDD构成按时间片比如一秒切分成的每个数据单位都是一个RDD。然后Spark核心引擎将对DStream的Transformation操作变为针对Spark中对 RDD的Transformation操作将RDD经过操作变成中间结果保存在内存中。
之前的DataFrame和DataSet也是同样基于RDD所以说RDD是Spark最基本的数据抽象。就像Java里的基本数据类型Primitive Type一样所有的数据都可以用基本数据类型描述。
也正是因为这样无论是DataFrame还是DStream都具有RDD的不可变性、分区性和容错性等特质。
所以Spark是一个高度统一的平台所有的高级API都有相同的性质它们之间可以很容易地相互转化。Spark的野心就是用这一套工具统一所有数据处理的场景。
由于Spark Streaming将底层的细节封装起来了所以对于开发者来说只需要操作DStream就行。接下来让我们一起学习DStream的结构以及它支持的转换操作。
## DStream
下图就是DStream的内部形式即一个连续的RDD序列每一个RDD代表一个时间窗口的输入数据流。
<img src="https://static001.geekbang.org/resource/image/66/ac/66b4562bcbd4772160f0f5766b59b5ac.png" alt="">
对DStream的转换操作意味着对它包含的每一个RDD进行同样的转换操作。比如下边的例子。
```
sc = SparkContext(master, appName)
ssc = StreamingContext(sc, 1)
lines = sc.socketTextStream(&quot;localhost&quot;, 9999)
words = lines.flatMap(lambda line: line.split(&quot; &quot;))
```
<img src="https://static001.geekbang.org/resource/image/72/b4/72d05c02bf547f5c993fb0b3349343b4.png" alt="">
首先我们创建了一个lines的DStream去监听来自本机9999端口的数据流每一个数据代表一行文本。然后对lines进行flatMap的转换操作把每一个文本行拆分成词语。
本质上对一个DStream进行flatMap操作就是对它里边的每一个RDD进行flatMap操作生成了一系列新的RDD构成了一个新的代表词语的DStream。
正因为DStream和RDD的关系RDD支持的所有转换操作DStream都支持比如map、flatMap、filter、union等。这些操作我们在前边学习RDD时都详细介绍过在此不做赘述。
此外DStream还有一些特有操作如滑动窗口操作我们可以一起探讨。
### 滑动窗口操作
任何Spark Streaming的程序都要首先创建一个**StreamingContext**的对象它是所有Streaming操作的入口。
比如我们可以通过StreamingContext来创建DStream。前边提到的例子中lines这个DStream就是由名为sc的StreamingContext创建的。
StreamingContext中最重要的参数是批处理的**时间间隔**,即把流数据细分成数据块的粒度。
这个时间间隔决定了流处理的延迟性所以需要我们根据需求和资源来权衡间隔的长度。上边的例子中我们把输入的数据流以秒为单位划分每一秒的数据会生成一个RDD进行运算。
有些场景中我们需要每隔一段时间统计过去某个时间段内的数据。比如对热点搜索词语进行统计每隔10秒钟输出过去60秒内排名前十位的热点词。这是流处理的一个基本应用场景很多流处理框架如Apache Flink都有原生的支持。所以Spark也同样支持滑动窗口操作。
从统计热点词这个例子,你可以看出滑动窗口操作有两个基本参数:
- 窗口长度window length每次统计的数据的时间跨度在例子中是60秒
- 滑动间隔sliding interval每次统计的时间间隔在例子中是10秒。
显然由于Spark Streaming流处理的最小时间单位就是StreamingContext的时间间隔所以这两个参数一定是它的整数倍。
<img src="https://static001.geekbang.org/resource/image/93/e3/933bd108299c65a3eb00329f345119e3.png" alt="">
最基本的滑动窗口操作是window它可以返回一个新的DStream这个DStream中每个RDD代表一段时间窗口内的数据如下例所示。
```
windowed_words = words.window(60, 10)
```
windowed_words代表的就是热词统计例子中我们所需的DStream即它里边每一个数据块都包含过去60秒内的词语而且这样的块每10秒钟就会生成一个。
此外Spark Streaming还支持一些“进阶”窗口操作。如countByWindow、reduceByWindow、reduceByKeyAndWindow和countByValueAndWindow在此不做深入讨论。
## Spark Streaming的优缺点
讲了这么多Spark Streaming不管内部实现也好支持的API也好我们还并不明白它的优势是什么相比起其他流处理框架的缺点是什么。只有明白了这些才能帮助我们在实际工作中决定是否使用Spark Streaming。
首先Spark Streaming的优点很明显由于它的底层是基于RDD实现的所以RDD的优良特性在它这里都有体现。
比如数据容错性如果RDD 的某些分区丢失了,可以通过依赖信息重新计算恢复。
再比如运行速度DStream同样也能通过persist()方法将数据流存放在内存中。这样做的好处是遇到需要多次迭代计算的程序时,速度优势十分明显。
而且Spark Streaming是Spark生态的一部分。所以它可以和Spark的核心引擎、Spark SQL、MLlib等无缝衔接。换句话说对实时处理出来的中间数据我们可以立即在程序中无缝进行批处理、交互式查询等操作。这个特点大大增强了Spark Streaming的优势和功能使得基于Spark Streaming的应用程序很容易扩展。
而Spark Streaming的主要缺点是实时计算延迟较高一般在秒的级别。这是由于Spark Streaming不支持太小的批处理的时间间隔。
在第二章中我们讲过准实时和实时系统无疑Spark Streaming是一个准实时系统。别的流处理框架如Storm的延迟性就好很多可以做到毫秒级。
## 小结
Spark Streaming作为Spark中的流处理组件把连续的流数据按时间间隔划分为一个个数据块然后对每个数据块分别进行批处理。
在内部每个数据块就是一个RDD所以Spark Streaming有RDD的所有优点处理速度快数据容错性好支持高度并行计算。
但是,它的实时延迟相比起别的流处理框架比较高。在实际工作中,我们还是要具体情况具体分析,选择正确的处理框架。
## 思考题
如果想要优化一个Spark Streaming程序你会从哪些角度入手
欢迎你把答案写在留言区,与我和其他同学一起讨论。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,177 @@
<audio id="audio" title="17 | Structured Streaming如何用DataFrame API进行实时数据分析?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ab/95/ab8e8b0950a7caa0fa58710a0d077395.mp3"></audio>
你好,我是蔡元楠。
上一讲中我们介绍了Spark中的流处理库Spark Streaming。它将无边界的流数据抽象成DStream按特定的时间间隔把数据流分割成一个个RDD进行批处理。所以DStream API与RDD API高度相似也拥有RDD的各种性质。
在第15讲中我们比较过RDD和DataSet/DataFrame。你还记得DataSet/DataFrame的优点吗你有没有想过既然已经有了RDD API我们为什么还要引入DataSet/DataFrame呢
让我们来回顾一下DataSet/DataFrame的优点为了方便描述下文中我们统一用DataFrame来代指DataSet和DataFrame
- DataFrame 是**高级API**,提供类似于**SQL**的query接口方便熟悉关系型数据库的开发人员使用
- **Spark SQL执行引擎会自动优化DataFrame程序**而用RDD API开发的程序本质上需要工程师自己构造RDD的DAG执行图所以依赖于工程师自己去优化。
那么我们自然会想到如果可以拥有一个基于DataFrame API的流处理模块作为工程师的我们就不需要去用相对low level的DStream API去处理无边界数据这样会大大提升我们的开发效率。
基于这个思想2016年Spark在其2.0版本中推出了结构化流数据处理的模块Structured Streaming。
Structured Streaming是基于Spark SQL引擎实现的依靠Structured Streaming在开发者眼里流数据和静态数据没有区别。我们完全可以像批处理静态数据那样去处理流数据。随着流数据的持续输入Spark SQL引擎会帮助我们持续地处理新数据并且更新计算结果。
今天就让我们来一起学习Structured Streaming的原理以及应用。
## Structured Streaming模型
流数据处理最基本的问题就是如何对不断更新的无边界数据建模。
之前讲的Spark Streaming就是把流数据按一定的时间间隔分割成许多个小的数据块进行批处理。在Structured Streaming的模型中我们要把数据看成一个无边界的关系型的数据表。每一个数据都是表中的一行不断会有新的数据行被添加到表里来。我们可以对这个表做任何类似批处理的查询Spark会帮我们不断对新加入的数据进行处理并更新计算结果。
<img src="https://static001.geekbang.org/resource/image/bb/37/bb1845be9f34ef7d232a509f90ae0337.jpg" alt="">
与Spark Streaming类似Structured Streaming也是将输入的数据流按照时间间隔以一秒为例划分成数据段。每一秒都会把新输入的数据添加到表中Spark也会每秒更新输出结果。输出结果也是表的形式输出表可以写入硬盘或者HDFS。
这里我要介绍一下Structured Streaming的三种输出模式。
1. 完全模式Complete Mode整个更新过的输出表都被写入外部存储
1. 附加模式Append Mode上一次触发之后新增加的行才会被写入外部存储。如果老数据有改动则不适合这个模式
1. 更新模式Update Mode上一次触发之后被更新的行才会被写入外部存储。
需要注意的是Structured Streaming并不会完全存储输入数据。每个时间间隔它都会读取最新的输入进行处理更新输出表然后把这次的输入删除。Structured Streaming只会存储更新输出表所需要的信息。
Structured Streaming的模型在根据事件时间Event Time处理数据时十分方便。
我们在第六讲中曾经讲过事件时间和处理时间Processing Time的区别。这里我再简单说一下。事件时间指的是事件发生的时间是数据本身的属性而处理时间指的是Spark接收到数据的时间。
很多情况下我们需要基于事件时间来处理数据。比如说统计每个小时接到的订单数量一个订单很有可能在12:59被创建但是到了13:01才被处理。
在Structured Streaming的模型中由于每个数据都是输入数据表中的一行那么事件时间就是行中的一列。依靠DataFrame API提供的类似于SQL的接口我们可以很方便地执行基于时间窗口的查询。
## Streaming DataFrame API
在Structured Streaming发布以后DataFrame既可以代表静态的有边界数据也可以代表无边界数据。之前对静态DataFrame的各种操作同样也适用于流式DataFrame。接下来让我们看几个例子。
### 创建DataFrame
SparkSession.readStream()返回的DataStreamReader可以用于创建流DataFrame。它支持多种类型的数据流作为输入比如文件、Kafka、socket等。
```
socketDataFrame = spark
.readStream
.format(&quot;socket&quot;
.option(&quot;host&quot;, &quot;localhost&quot;)
.option(&quot;port&quot;, 9999)
.load()
```
上边的代码例子创建了一个DataFrame用来监听来自localhost:9999的数据流。
### 基本的查询操作
流DataFrame同静态DataFrame一样不仅支持类似SQL的查询操作如select和where等还支持RDD的转换操作如map和filter。 让我们一起来看下面的例子。
假设我们已经有一个DataFrame代表一个学生的数据流即每个数据是一个学生每个学生有名字name、年龄age、身高height和年级grade四个属性我们可以用DataFrame API去做类似于SQL的Query。
```
df = … // 这个DataFrame代表学校学生的数据流schema是{name: string, age: number, height: number, grade: string}
df.select(&quot;name&quot;).where(&quot;age &gt; 10&quot;) // 返回年龄大于10岁的学生名字列表
df.groupBy(&quot;grade&quot;).count() // 返回每个年级学生的人数
df.sort_values([age], ascending=False).head(100) //返回100个年龄最大的学生
```
在这个例子中通过第二行我们可以得到所有年龄在10岁以上的学生名字第三行可以得到每个年级学生的人数第四行得到100个年龄最大的学生信息。此外DataFrame还支持很多基本的查询操作在此不做赘述。
我们还可以通过isStreaming函数来判断一个DataFrame是否代表流数据。
```
df.isStreaming()
```
###
基于事件时间的时间窗口操作
在学习Spark Streaming的时间窗口操作时我们举过一个例子是每隔10秒钟输出过去60秒的前十热点词。这个例子是基于处理时间而非事件时间的。
现在让我们设想一下如果数据流中的每个词语都有一个时间戳代表词语产生的时间那么要怎样实现每隔10秒钟输出过去60秒内产生的前十热点词呢你可以看看下边的代码。
```
words = ... #这个DataFrame代表词语的数据流schema是 { timestamp: Timestamp, word: String}
windowedCounts = words.groupBy(
window(words.timestamp, &quot;1 minute&quot;, &quot;10 seconds&quot;),
words.word
).count()
.sort(desc(&quot;count&quot;))
.limit(10)
```
基于词语的生成时间我们创建了一个窗口长度为1分钟滑动间隔为10秒的window。然后把输入的词语表根据window和词语本身聚合起来并统计每个window内每个词语的数量。之后再根据词语的数量进行排序只返回前10的词语。
在Structured Streaming基于时间窗口的聚合操作中groupBy是非常常用的。
### 输出结果流
当经过各种SQL查询操作之后我们创建好了代表最终结果的DataFrame。下一步就是开始对输入数据流的处理并且持续输出结果。
我们可以用Dataset.writeStream()返回的DataStreamWriter对象去输出结果。它支持多种写入位置如硬盘文件、Kafka、console和内存等。
```
query = wordCounts
.writeStream
.outputMode(&quot;complete&quot;)
.format(&quot;csv&quot;)
.option(&quot;path&quot;, &quot;path/to/destination/dir&quot;)
.start()
query.awaitTermination()
```
在上面这个代码例子中我们选择了完全模式把输出结果流写入了CSV文件。
## Structured Streaming与Spark Streaming对比
接下来让我们对比一下Structured Streaming和上一讲学过的Spark Streaming。看看同为流处理的组件的它们各有什么优缺点。
### 简易度和性能
Spark Streaming提供的DStream API与RDD API很类似相对比较低level。
当我们编写 Spark Streaming 程序的时候本质上就是要去构造RDD的DAG执行图然后通过 Spark Engine 运行。这样开发者身上的担子就很重很多时候要自己想办法去提高程序的处理效率。这不是Spark作为一个数据处理框架想看到的。对于好的框架来说开发者只需要专注在业务逻辑上而不用操心别的配置、优化等繁杂事项。
Structured Streaming提供的DataFrame API就是这么一个相对高level的API大部分开发者都很熟悉关系型数据库和SQL。**这样的数据抽象可以让他们用一套统一的方案去处理批处理和流处理**,不用去关心具体的执行细节。
而且DataFrame API是在Spark SQL的引擎上执行的Spark SQL有非常多的优化功能比如执行计划优化和内存管理等所以Structured Streaming的应用程序性能很好。
### 实时性
在上一讲中我们了解到Spark Streaming是准实时的它能做到的最小延迟在一秒左右。
虽然Structured Streaming用的也是类似的微批处理思想每过一个时间间隔就去拿来最新的数据加入到输入数据表中并更新结果但是相比起Spark Streaming来说它更像是实时处理能做到用更小的时间间隔最小延迟在100毫秒左右。
而且在Spark 2.3版本中Structured Streaming引入了连续处理的模式可以做到真正的毫秒级延迟这无疑大大拓展了Structured Streaming的应用广度。不过现在连续处理模式还有很多限制让我们期待它的未来吧。
### 对事件时间的支持
就像我们在前边讲过的Structured Streaming对基于事件时间的处理有很好的支持。
由于Spark Streaming是把数据按接收到的时间切分成一个个RDD来进行批处理所以它很难基于数据本身的产生时间来进行处理。如果某个数据的处理时间和事件时间不一致的话就容易出问题。比如统计每秒的词语数量有的数据先产生但是在下一个时间间隔才被处理这样几乎不可能输出正确的结果。
Structured Streaming还有很多其他优点。比如它有更好的容错性保证了端到端exactly once的语义等等。所以综合来说Structured Streaming是比Spark Streaming更好的流处理工具。
## 思考题
在基于事件时间的窗口操作中Structured Streaming是怎样处理晚到达的数据并且返回正确结果的呢
比如在每十分钟统计词频的例子中一个词语在1:09被生成在1:11被处理程序在1:10和1:20都输出了对应的结果在1:20输出时为什么可以把这个词语统计在内这样的机制有没有限制
欢迎你把自己的答案写在留言区,与我和其他同学一起讨论。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,231 @@
<audio id="audio" title="18 | Word Count从零开始运行你的第一个Spark应用" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1c/49/1cac8efe5213fc3a8d73395841dba749.mp3"></audio>
你好,我是蔡元楠。
今天我们来从零开始运行你的第一个Spark应用。
我们先来回顾一下模块三的学习路径。
首先我们由浅入深地学习了Spark的基本数据结构RDD了解了它这样设计的原因以及它所支持的API。
之后我们又学习了Spark SQL的DataSet/DataFrame API了解到它不仅提供类似于SQL query的接口大大提高了开发者的工作效率还集成了Catalyst优化器可以提升程序的性能。
这些API应对的都是批处理的场景。
再之后我们学习了Spark的流处理模块Spark Streaming和Structured Streaming。两者都是基于微批处理Micro batch processing的思想将流数据按时间间隔分割成小的数据块进行批处理实时更新计算结果。
其中Structured Streaming也是使用DataSet/DataFrame API这套API在某种程度上统一了批处理和流处理是当前Spark最流行的工具我们必需要好好掌握。
虽然学习了这么多API以及它们的应用但是大部分同学还没有从零开始写一个完整的Spark程序可能更没有运行Spark程序的经历。纸上谈兵并不能帮助我们在工作生活中用Spark解决实际问题。所以今天我就和你一起做个小练习从在本地安装Spark、配置环境开始为你示范怎样一步步解决之前提到数次的统计词频Word Count的问题。
通过今天的学习,你可以收获:
- 怎样安装Spark以及其他相关的模块
- 知道什么是SparkContext、SparkSession
- 一个完整的Spark程序应该包含哪些东西
- 用RDD、DataFrame、Spark Streaming如何实现统计词频。
这一讲中我们使用的编程语言是Python操作系统是Mac OS X。
在这一讲以及之前文章的例子中我们都是用Python作为开发语言。虽然原生的Spark是用Scala实现但是在大数据处理领域中我个人最喜欢的语言是Python。因为它非常简单易用应用非常广泛有很多的库可以方便我们开发。
当然Scala也很棒作为一个函数式编程语言它很容易用链式表达对数据集进行各种处理而且它的运行速度是最快的感兴趣的同学可以去学习一下。
虽然Spark还支持Java和R但是我个人不推荐你使用。用Java写程序实在有些冗长而且速度上没有优势。
操作系统选Mac OS X是因为我个人喜欢使用Macbook当然Linux/Ubuntu也很棒。
## 安装Spark
首先我们来简单介绍一下如何在本地安装Spark以及用Python实现的Spark库——PySpark。
在前面的文章中我们了解过Spark的job都是JVMJava Virtual Machine的进程所以在安装运行Spark之前我们需要确保已经安装Java Developer KitJDK。在命令行终端中输入
```
java -version
```
如果命令行输出了某个Java的版本那么说明你已经有JDK或者JRE在本地。如果显示无法识别这个命令那么说明你还没有安装JDK。这时你可以去[Oracle的官网](https://www.oracle.com/technetwork/java/javase/downloads/index.html)去下载安装JDK然后配置好环境变量。
同样我们需要确保Python也已经被安装在本地了。在命令行输入“Python”或者“Python3”如果可以成功进入交互式的Python Shell就说明已经安装了Python。否则需要去[Python官网](https://www.python.org/downloads/)下载安装Python。这里我推荐你使用Python3而不是Python2。
我们同样可以在本地预装好Hadoop。Spark可以脱离Hadoop运行不过有时我们也需要依赖于HDFS和YARN。所以这一步并不是必须的你可以自行选择。
接下来我们就可以安装Spark。首先去[Spark官网](https://spark.apache.org/downloads.html)的下载界面。在第一个下拉菜单里选择最新的发布第二个菜单最好选择与Hadoop 2.7兼容的版本。因为有时我们的Spark程序会依赖于HDFS和YARN所以选择最新的Hadoop版本比较好。
<img src="https://static001.geekbang.org/resource/image/e9/28/e934ae8f6f3f2394e1d14153953f4328.png" alt="">
下载好之后解压缩Spark安装包并且把它移动到/usr/local目录下在终端中输入下面的代码。
```
$ tar -xzf ~/Dowmloads/spark-2.4.3-bin-hadoop2.7.tg
$ mv spark-2.4.3-bin-hadoop2.7.tgz /usr/local/spark
```
经过上述步骤从官网下载并安装Spark的文件这样我们便完成了Spark的安装。但是Spark也是要进行相应的环境变量配置的。你需要打开环境变量配置文件。
```
vim ~/.bash_profile
```
并在最后添加一段代码。
```
export SPARK_HOME=/usr/local/spark
export PATH=$PATH:$SPARK_HOME/bin
```
这样所需的步骤都做完之后我们在命令行控制台输入PySpark查看安装情况。如果出现下面的欢迎标志就说明安装完毕了。
```
Welcome to
____ __
/ __/__ ___ _____/ /__
_\ \/ _ \/ _ `/ __/ '_/
/__ / .__/\_,_/_/ /_/\_\ version 2.4.3
/_/
Using Python version 2.7.10 (default, Oct 6 2017 22:29:07)
SparkSession available as 'spark'.
&gt;&gt;&gt;
```
## 基于RDD API的Word Count程序
配置好所需的开发环境之后下一步就是写一个Python程序去统计词语频率。我们都知道这个程序的逻辑应该是如下图所示的。
<img src="https://static001.geekbang.org/resource/image/b0/2e/b0b16243323bb871959e9a86b803992e.jpg" alt="">
对于中间的先map再reduce的处理我相信通过前面的学习所有同学都可以用RDD或者DataFrame实现。
但是我们对于Spark程序的入口是什么、如何用它读取和写入文件可能并没有了解太多。所以接下来让我们先接触一下Spark程序的入口。
在Spark 2.0之前,**SparkContext**是所有Spark任务的入口它包含了Spark程序的基本设置比如程序的名字、内存大小、并行处理的粒度等Spark的驱动程序需要利用它来连接到集群。
无论Spark集群有多少个节点做并行处理每个程序只可以有唯一的SparkContext它可以被SparkConf对象初始化。
```
conf = SparkConf().setAppName(appName).setMaster(master)
sc = SparkContext(conf=conf)
```
这个appName参数是一个在集群UI上展示应用程序的名称master参数是一个Spark、Mesos 或YARN的集群URL对于本地运行它可以被指定为“local”。
在统计词频的例子中我们需要通过SparkContext对象来读取输入文件创建一个RDD如下面的代码所示。
```
text_file = sc.textFile(&quot;file://…...&quot;) //替换成实际的本地文件路径。
```
这里的text_file是一个RDD它里面的每一个数据代表原文本文件中的一行。
在这些版本中如果要使用Spark提供的其他库比如SQL或Streaming我们就需要为它们分别创建相应的context对象才能调用相应的API比如的DataFrame和DStream。
```
hc = HiveContext(sc)
ssc = StreamingContext(sc)
```
在Spark 2.0之后随着新的DataFrame/DataSet API的普及化Spark引入了新的**SparkSession**对象作为所有Spark任务的入口。
SparkSession不仅有SparkContext的所有功能它还集成了所有Spark提供的API比如DataFrame、Spark Streaming和Structured Streaming我们再也不用为不同的功能分别定义Context。
在统计词频的例子中我们可以这样初始化SparkSession以及创建初始RDD。
```
spark = SparkSession
.builder
.appName(appName)
.getOrCreate()
text_file = spark.read.text(&quot;file://….&quot;).rdd.map(lambda r: r[0])
```
由于SparkSession的普适性我推荐你尽量使用它作为你们Spark程序的入口。随后的学习中我们会逐渐了解怎样通过它调用DataFrame和Streaming API。
让我们回到统计词频的例子。在创建好代表每一行文本的RDD之后接下来我们便需要两个步骤。
1. 把每行的文本拆分成一个个词语;
1. 统计每个词语的频率。
对于第一步我们可以用flatMap去把行转换成词语。对于第二步我们可以先把每个词语转换成word, 1的形式然后用reduceByKey去把相同词语的次数相加起来。这样就很容易写出下面的代码了。
```
counts = lines.flatMap(lambda x: x.split(' '))
.map(lambda x: (x, 1))
.reduceByKey(add)
```
这里counts就是一个包含每个词语的wordcountpair的RDD。
相信你还记得只有当碰到action操作后这些转换动作才会被执行。所以接下来我们可以用collect操作把结果按数组的形式返回并输出。
```
output = counts.collect()
for (word, count) in output:
print(&quot;%s: %i&quot; % (word, count))
spark.stop() // 停止SparkSession
```
## 基于DataFrame API的Word Count程序
讲完基于RDD API的Word Count程序接下来让我们学习下怎样用DataFrame API来实现相同的效果。
在DataFrame的世界中我们可以把所有的词语放入一张表表中的每一行代表一个词语当然这个表只有一列。我们可以对这个表用一个groupBy()操作把所有相同的词语聚合起来然后用count()来统计出每个group的数量。
但是问题来了虽然Scala和Java支持对DataFrame进行flatMap操作但是Python并不支持。那么要怎样把包含多个词语的句子进行分割和拆分呢这就要用到两个新的操作——explode和split。split是pyspark.sql.functions库提供的一个函数它作用于DataFrame的某一列可以把列中的字符串按某个分隔符分割成一个字符串数组。
explode同样是pyspark.sql.functions库提供的一个函数通俗点的翻译是“爆炸”它也作用于DataFrame的某一列可以为列中的数组或者map中每一个元素创建一个新的Row。
由于之前代码中创建的df_lines这个DataFrame中每一行只有一列每一列都是一个包含很多词语的句子我们可以先对这一列做split生成一个新的列列中每个元素是一个词语的数组再对这个列做explode可以把数组中的每个元素都生成一个新的Row。这样就实现了类似的flatMap功能。这个过程可以用下面的三个表格说明。
<img src="https://static001.geekbang.org/resource/image/c9/55/c9ebf1f324a73539a2a57ce8151e4455.png" alt="">
接下来我们只需要对Word这一列做groupBy就可以统计出每个词语出现的频率代码如下。
```
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
if __name__ == &quot;__main__&quot;:
spark = SparkSession
.builder
.appName(WordCount)
.getOrCreate()
lines = spark.read.text(&quot;sample.txt&quot;)
wordCounts = lines
.select(explode(split(lines.value, &quot; &quot;))
.alias(&quot;word&quot;))
.groupBy(&quot;word&quot;)
.count()
wordCounts.show()
spark.stop()
```
从这个例子你可以很容易看出使用DataSet/DataFrame API的便利性——我们不需要创建word, count的pair来作为中间值可以直接对数据做类似SQL的查询。
## 小结
通过今天的学习我们掌握了如何从零开始创建一个简单的Spark的应用程序包括如何安装Spark、如何配置环境、Spark程序的基本结构等等。
## 实践题
希望你可以自己动手操作一下,这整个过程只需要跑通一次,以后就可以脱离纸上谈兵,真正去解决实际问题。
欢迎你在留言中反馈自己动手操作的效果。
如果你跑通了,可以在留言中打个卡。如果遇到了问题,也请你在文章中留言,与我和其他同学一起讨论。

View File

@@ -0,0 +1,293 @@
<audio id="audio" title="19 | 综合案例实战:处理加州房屋信息,构建线性回归模型" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/48/70/484a40fdfd3932021002db99892e6f70.mp3"></audio>
你好,我是蔡元楠。
今天我要与你分享的主题是“综合案例实战:处理加州房屋信息,构建线性回归模型”。
通过之前的学习我们对Spark各种API的基本用法有了一定的了解还通过统计词频的实例掌握了如何从零开始写一个Spark程序。那么现在让我们从一个真实的数据集出发看看如何用Spark解决实际问题。
## 数据集介绍
为了完成今天的综合案例实战我使用的是美国加州1990年房屋普查的数据集。
<img src="https://static001.geekbang.org/resource/image/a9/5c/a9c1d749f2d1c43261a043aa77056f5c.png" alt="">
数据集中的每一个数据都代表着一块区域内房屋和人口的基本信息总共包括9项
1. 该地区中心的纬度latitude
1. 该地区中心的经度longitude
1. 区域内所有房屋屋龄的中位数housingMedianAge
1. 区域内总房间数totalRooms
1. 区域内总卧室数totalBedrooms
1. 区域内总人口数population
1. 区域内总家庭数households
1. 区域内人均收入中位数medianIncome
1. 该区域房价的中位数medianHouseValue
也就是说我们可以把每一个数据看作一个地区它含有9项我们关心的信息也就是上面提到的9个指标。比如下面这个数据
```
-122.230000,37.880000,41.000000,880.000000,129.000000,322.000000,126.000000,8.325200,452600.000000'
```
这个数据代表该地区的经纬度是(-122.230000,37.880000这个地区房屋历史的中位数是41年所有房屋总共有880个房间其中有129个卧室。这个地区内共有126个家庭和322位居民人均收入中位数是8.3252万房价中位数是45.26万。
这里的地域单位是美国做人口普查的最小地域单位平均一个地域单位中有1400多人。在这个数据集中共有两万多个这样的数据。显然这样小的数据量我们并“不需要”用Spark来处理但是它可以起到一个很好的示例作用。这个数据集可以从[网上](http://www.dcc.fc.up.pt/~ltorgo/Regression/cal_housing.html)下载到。这个数据集是在1997年的一篇学术论文中创建的感兴趣的同学可以去亲自下载并加以实践。
那么我们今天的目标是什么呢?就是用已有的数据,构建一个**线性回归模型**,来预测房价。
我们可以看到前8个属性都可能对房价有影响。这里我们假设这种影响是线性的我们就可以找到一个类似**A=b**B+c**C+d**D+…+i**I**的公式A代表房价B到I分别代表另外八个属性。这样对于不在数据集中的房子我们可以套用这个公式来计算出一个近似的房价。由于专栏的定位是大规模数据处理专栏所以我们不会细讲统计学的知识。如果你对统计学知识感兴趣或者还不理解什么是线性回归的话可以去自行学习一下。
## 进一步了解数据集
每当我们需要对某个数据集进行处理时不要急着写代码。你一定要先观察数据集了解它的特性并尝试对它做一些简单的预处理让数据的可读性更好。这些工作我们最好在Spark的交互式Shell上完成而不是创建python的源文件并执行。因为在Shell上我们可以非常直观而简便地看到每一步的输出。
首先让我们把数据集读入Spark。
```
from pyspark.sql import SparkSession
# 初始化SparkSession和SparkContext
spark = SparkSession.builder
.master(&quot;local&quot;)
.appName(&quot;California Housing &quot;)
.config(&quot;spark.executor.memory&quot;, &quot;1gb&quot;)
.getOrCreate()
sc = spark.sparkContext
# 读取数据并创建RDD
rdd = sc.textFile('/Users/yourName/Downloads/CaliforniaHousing/cal_housing.data')
# 读取数据每个属性的定义并创建RDD
header = sc.textFile('/Users/yourName/Downloads/CaliforniaHousing/cal_housing.domain')
```
这样我们就把房屋信息数据和每个属性的定义读入了Spark并创建了两个相应的RDD。你还记得吧RDD是有一个惰性求值的特性的所以我们可以用collect()函数来把数据输出在Shell上。
```
header.collect()
[u'longitude: continuous.', u'latitude: continuous.', u'housingMedianAge: continuous. ', u'totalRooms: continuous. ', u'totalBedrooms: continuous. ', u'population: continuous. ', u'households: continuous. ', u'medianIncome: continuous. ', u'medianHouseValue: continuous. ']
```
这样我们就得到了每个数据所包含的信息这和我们前面提到的9个属性的顺序是一致的而且它们都是连续的值而不是离散的。你需要注意的是collect()函数会把所有数据都加载到内存中如果数据很大的话有可能会造成内存泄漏所以要小心使用。平时比较常见的方法是用take()函数去只读取RDD中的某几个元素。
由于RDD中的数据可能会比较大所以接下来让我们读取它的前两个数据。
```
rdd.take(2)
[u'-122.230000,37.880000,41.000000,880.000000,129.000000,322.000000,126.000000,8.325200,452600.000000', u'-122.220000,37.860000,21.000000,7099.000000,1106.000000,2401.000000,1138.000000,8.301400,358500.000000']
```
由于我们是用SparkContext的textFile函数去创建RDD所以每个数据其实是一个大的字符串各个属性之间用逗号分隔开来。这不利于我们之后的处理因为我们可能会需要分别读取每个对象的各个属性。所以让我们用map函数把大字符串分隔成数组这会方便我们的后续操作。
```
rdd = rdd.map(lambda line: line.split(&quot;,&quot;))
rdd.take(2)
[[u'-122.230000', u'37.880000', u'41.000000', u'880.000000', u'129.000000', u'322.000000', u'126.000000', u'8.325200', u'452600.000000'], [u'-122.220000', u'37.860000', u'21.000000', u'7099.000000', u'1106.000000', u'2401.000000', u'1138.000000', u'8.301400', u'358500.000000']]
```
我们在前面学过Spark SQL的DataFrame API在查询结构化数据时更方便使用而且性能更好。在这个例子中你可以看到数据的schema是定义好的我们需要去查询各个列所以DataFrame API显然更加适用。所以我们需要先把RDD转换为DataFrame。
具体来说就是需要把之前用数组代表的对象转换成为Row对象再用toDF()函数转换成DataFrame。
```
from pyspark.sql import Row
df = rdd.map(lambda line: Row(longitude=line[0],
latitude=line[1],
housingMedianAge=line[2],
totalRooms=line[3],
totalBedRooms=line[4],
population=line[5],
households=line[6],
medianIncome=line[7],
medianHouseValue=line[8])).toDF()
```
现在我们可以用show()函数打印出这个DataFrame所含的数据表。
```
df.show()
```
<img src="https://static001.geekbang.org/resource/image/de/24/de91764e7e7cc3d143a8217400ec0524.png" alt="">
这里每一列的数据格式都是string但是它们其实都是数字所以我们可以通过cast()函数把每一列的类型转换成float。
```
def convertColumn(df, names, newType)
for name in names:
df = df.withColumn(name, df[name].cast(newType))
return df
columns = ['households', 'housingMedianAge', 'latitude', 'longitude', 'medianHouseValue', 'medianIncome', 'population', 'totalBedRooms', 'totalRooms']
df = convertColumn(df, columns, FloatType())
```
转换成数字有很多优势。比如,我们可以按某一列,对所有对象进行排序,也可以计算平均值等。比如,下面这段代码就可以统计出所有建造年限各有多少个房子。
```
df.groupBy(&quot;housingMedianAge&quot;).count().sort(&quot;housingMedianAge&quot;,ascending=False).show()
```
## 预处理
通过上面的数据分析,你可能会发现这些数据还是不够直观。具体的问题有:
1. 房价的值普遍都很大,我们可以把它调整成相对较小的数字;
1. 有的属性没什么意义,比如所有房子的总房间数和总卧室数,我们更加关心的是平均房间数;
1. 在我们想要构建的线性模型中,房价是结果,其他属性是输入参数。所以我们需要把它们分离处理;
1. 有的属性最小值和最大值范围很大,我们可以把它们标准化处理。
对于第一点我们观察到大多数房价都是十万起的所以可以用withColumn()函数把所有房价都除以100000。
```
df = df.withColumn(&quot;medianHouseValue&quot;, col(&quot;medianHouseValue&quot;)/100000)
```
对于第二点,我们可以添加如下三个新的列:
- 每个家庭的平均房间数roomsPerHousehold
- 每个家庭的平均人数populationPerHousehold
- 卧室在总房间的占比bedroomsPerRoom
当然你们可以自由添加你们觉得有意义的列这里的三个是我觉得比较典型的。同样用withColumn()函数可以容易地新建列。
```
df = df.withColumn(&quot;roomsPerHousehold&quot;, col(&quot;totalRooms&quot;)/col(&quot;households&quot;))
.withColumn(&quot;populationPerHousehold&quot;, col(&quot;population&quot;)/col(&quot;households&quot;))
.withColumn(&quot;bedroomsPerRoom&quot;, col(&quot;totalBedRooms&quot;)/col(&quot;totalRooms&quot;))
```
同样,有的列是我们并不关心的,比如经纬度,这个数值很难有线性的意义。所以我们可以只留下重要的信息列。
```
df = df.select(&quot;medianHouseValue&quot;,
&quot;totalBedRooms&quot;,
&quot;population&quot;,
&quot;households&quot;,
&quot;medianIncome&quot;,
&quot;roomsPerHousehold&quot;,
&quot;populationPerHousehold&quot;,
&quot;bedroomsPerRoom&quot;)
```
对于第三点最简单的办法就是把DataFrame转换成RDD然后用map()函数把每个对象分成两部分房价和一个包含其余属性的列表然后在转换回DataFrame。
```
from pyspark.ml.linalg import DenseVector
input_data = df.rdd.map(lambda x: (x[0], DenseVector(x[1:])))
df = spark.createDataFrame(input_data, [&quot;label&quot;, &quot;features&quot;])
```
我们重新把两部分重新标记为“label”和“features”label代表的是房价features代表包括其余参数的列表。
对于第四点数据的标准化我们可以借助Spark的机器学习库Spark ML来完成。Spark ML也是基于DataFrame它提供了大量机器学习的算法实现、数据流水线pipeline相关工具和很多常用功能。由于本专栏的重点是大数据处理所以我们并没有介绍Spark ML但是我强烈推荐同学们有空去了解一下它。
在这个AI和机器学习的时代我们不能落伍。
```
from pyspark.ml.feature import StandardScaler
standardScaler = StandardScaler(inputCol=&quot;features&quot;, outputCol=&quot;features_scaled&quot;)
scaler = standardScaler.fit(df)
scaled_df = scaler.transform(df)
```
在第二行我们创建了一个StandardScaler它的输入是features列输出被我们命名为features_scaled。第三、第四行我们把这个scaler对已有的DataFrame进行处理让我们看下代码块里显示的输出结果。
```
scaled_df.take(1)
[Row(label=4.526, features=DenseVector([129.0, 322.0, 126.0, 8.3252, 6.9841, 2.5556, 0.1466]), features_scaled=DenseVector([0.3062, 0.2843, 0.3296, 4.3821, 2.8228, 0.2461, 2.5264]))]
```
我们可以清楚地看到这一行新增了一个features_scaled的列它里面每个数据都是标准化过的我们应该用它而非features来训练模型。
## 创建模型
上面的预处理都做完后,我们终于可以开始构建线性回归模型了。
首先我们需要把数据集分为训练集和测试集训练集用来训练模型测试集用来评估模型的正确性。DataFrame的randomSplit()函数可以很容易的随机分割数据这里我们将80%的数据用于训练剩下20%作为测试集。
```
train_data, test_data = scaled_df.randomSplit([.8,.2],seed=123)
```
用Spark ML提供的LinearRegression功能我们可以很容易得构建一个线性回归模型如下所示。
```
from pyspark.ml.regression import LinearRegression
lr = LinearRegression(featuresCol='features_scaled', labelCol=&quot;label&quot;, maxIter=10, regParam=0.3, elasticNetParam=0.8)
linearModel = lr.fit(train_data)
```
LinearRegression可以调节的参数还有很多你可以去[官方API文档](https://spark.apache.org/docs/latest/api/python/pyspark.ml.html#pyspark.ml.regression.LinearRegression)查阅,这里我们只是示范一下。
## 模型评估
现在有了模型我们终于可以用linearModel的transform()函数来预测测试集中的房价,并与真实情况进行对比。代码如下所示。
```
predicted = linearModel.transform(test_data)
predictions = predicted.select(&quot;prediction&quot;).rdd.map(lambda x: x[0])
labels = predicted.select(&quot;label&quot;).rdd.map(lambda x: x[0])
predictionAndLabel = predictions.zip(labels).collect()
```
我们用RDD的zip()函数把预测值和真实值放在一起,这样可以方便地进行比较。比如让我们看一下前两个对比结果。
```
predictionAndLabel.take(2)
[(1.4491508524918457, 1.14999), (1.5831547768979277, 0.964)]
```
这里可以看出,我们的模型预测的结果有些偏小,这可能有多个因素造成。最直接的原因就是房价与我们挑选的列并没有强线性关系,而且我们使用的参数也可能不够准确。
这一讲我只是想带着你一起体验下处理真实数据集和解决实际问题的感觉想要告诉你的是这种通用的思想并帮助你继续熟悉Spark各种库的用法并不是说房价一定就是由这些参数线性决定了。感兴趣的同学可以去继续优化或者尝试别的模型。
## 小结
这一讲我们通过一个真实的数据集,通过以下步骤解决了一个实际的数据处理问题:
1. 观察并了解数据集
1. 数据清洗
1. 数据的预处理
1. 训练模型
1. 评估模型
其实这里还可以有与“优化与改进”相关的内容这里没有去阐述是因为我们的首要目的依然是熟悉与使用Spark各类API。相信通过今天的学习你初步了解了数据处理问题的一般思路并强化了对RDD、DataFrame和机器学习API的使用。
## 实践与思考题
今天请你下载这个数据集,按文章的介绍去动手实践一次。如果有时间的话,还可以对这个过程的优化和改进提出问题并加以解决。
欢迎你在留言板贴出自己的idea。如果你觉得有所收获也欢迎你把文章分享给朋友。

View File

@@ -0,0 +1,228 @@
<audio id="audio" title="20 | 流处理案例实战:分析纽约市出租车载客信息" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/88/95/8881f9078fc69c0f39af9f0558f86495.mp3"></audio>
你好,我是蔡元楠。
今天我要与你分享的主题是“流处理案例实战:分析纽约市出租车载客信息”。
在上一讲中我们结合加州房屋信息的真实数据集构建了一个基本的预测房价的线性回归模型。通过这个实例我们不仅学习了处理大数据问题的基本流程而且还进一步熟练了对RDD和DataFrame API的使用。
你应该已经发现上一讲的实例是一个典型的批处理问题因为处理的数据是静态而有边界的。今天让我们来一起通过实例更加深入地学习用Spark去解决实际的流处理问题。
相信你还记得在前面的章节中我们介绍过Spark两个用于流处理的组件——Spark Streaming和Structured Streaming。其中Spark Streaming是Spark 2.0版本前的的流处理库在Spark 2.0之后集成了DataFrame/DataSet API的Structured Streaming成为Spark流处理的主力。
今天就让我们一起用Structured Streaming对纽约市出租车的载客信息进行处理建立一个实时流处理的pipeline实时输出各个区域内乘客小费的平均数来帮助司机决定要去哪里接单。
## 数据集介绍
今天的数据集是纽约市20092015年出租车载客的信息。每一次出行包含了两个事件一个事件代表出发另一个事件代表到达。每个事件都有11个属性它的schema如下所示
<img src="https://static001.geekbang.org/resource/image/4a/90/4ae9c7d353925f84d36bf7280f2b5b90.jpg" alt="">
这部分数据有个不太直观的地方,那就是同一次出行会有两个记录,而且代表出发的事件没有任何意义,因为到达事件已经涵盖了所有必要的信息。现实世界中的数据都是这样复杂,不可能像学校的测试数据一样简单直观,所以处理之前,我们要先对数据进行清洗,只留下必要的信息。
这个数据还包含有另外一部分信息就是所有出租车的付费信息它有8个属性schema如下所示。
<img src="https://static001.geekbang.org/resource/image/8e/de/8ef443617788243f4546116fffb40ede.jpg" alt="">
这个数据集可以从[网上](https://training.ververica.com/setup/taxiData.html)下载到数据集的规模在100MB左右它只是节选了一部分出租车的载客信息所以在本机运行就可以了。详细的纽约出租车数据集超过了500GB同样在[网上](https://www1.nyc.gov/site/tlc/about/tlc-trip-record-data.page)可以下载,感兴趣的同学可以下载来实践一下。
## 流数据输入
你可能要问,这个数据同样是静态、有边界的,为什么要用流处理?
因为我们手里没有实时更新的流数据源。我也没有权限去公开世界上任何一个上线产品的数据流。所以这里只能将有限的数据经过Kafka处理输出为一个伪流数据作为我们要构建的pipeline的输入。
在模块二中我们曾经初步了解过Apache Kafka知道它是基于Pub/Sub模式的流数据处理平台。由于我们的专栏并不涉及Apache Kafka的具体内容所以我在这里就不讲如何把这个数据输入到Kafka并输出的细节了。你只要知道在这个例子中Consumer是之后要写的Spark流处理程序这个消息队列有两个Topic一个包含出行的地理位置信息一个包含出行的收费信息。Kafka会**按照时间顺序**向这两个Topic中发布事件从而模拟一个实时的流数据源。
相信你还记得写Spark程序的第一步就是创建SparkSession对象并根据输入数据创建对应的RDD或者DataFrame。你可以看下面的代码。
```
from pyspark.sql import SparkSession
spark = SparkSession.builder
.appName(&quot;Spark Structured Streaming for taxi ride info&quot;)
.getOrCreate()
rides = spark
.readStream
.format(&quot;kafka&quot;)
.option(&quot;kafka.bootstrap.servers&quot;, &quot;localhost:xxxx&quot;) //取决于Kafka的配置
.option(&quot;subscribe&quot;, &quot;taxirides&quot;)
.option(&quot;startingOffsets&quot;, &quot;latest&quot;)
.load()
.selectExpr(&quot;CAST(value AS STRING)&quot;)
fares = spark
.readStream
.format(&quot;kafka&quot;)
.option(&quot;kafka.bootstrap.servers&quot;, &quot;localhost:xxxx&quot;)
.option(&quot;subscribe&quot;, &quot;taxifares&quot;)
.option(&quot;startingOffsets&quot;, &quot;latest&quot;)
.load()
.selectExpr(&quot;CAST(value AS STRING)
```
在这段代码里我们创建了两个Streaming DataFrame并订阅了对应的Kafka topic一个代表出行位置信息另一个代表收费信息。Kafka对数据没有做任何修改所以流中的每一个数据都是一个长String属性之间是用逗号分割的。
```
417986,END,2013-01-02 00:43:52,2013-01-02 00:39:56,-73.984528,40.745377,-73.975967,40.765533,1,2013007646,2013007642
```
## 数据清洗
现在我们要开始做数据清洗了。要想分离出我们需要的位置和付费信息我们首先要把数据分割成一个个属性并创建对应的DataFrame中的列。为此我们首先要根据数据类型创建对应的schema。
```
ridesSchema = StructType([
StructField(&quot;rideId&quot;, LongType()), StructField(&quot;isStart&quot;, StringType()),
StructField(&quot;endTime&quot;, TimestampType()), StructField(&quot;startTime&quot;, TimestampType()),
StructField(&quot;startLon&quot;, FloatType()), StructField(&quot;startLat&quot;, FloatType()),
StructField(&quot;endLon&quot;, FloatType()), StructField(&quot;endLat&quot;, FloatType()),
StructField(&quot;passengerCnt&quot;, ShortType()), StructField(&quot;taxiId&quot;, LongType()),
StructField(&quot;driverId&quot;, LongType())])
faresSchema = StructType([
StructField(&quot;rideId&quot;, LongType()), StructField(&quot;taxiId&quot;, LongType()),
StructField(&quot;driverId&quot;, LongType()), StructField(&quot;startTime&quot;, TimestampType()),
StructField(&quot;paymentType&quot;, StringType()), StructField(&quot;tip&quot;, FloatType()),
StructField(&quot;tolls&quot;, FloatType()), StructField(&quot;totalFare&quot;, FloatType())])
```
接下来,我们将每个数据都用逗号分割,并加入相应的列。
```
def parse_data_from_kafka_message(sdf, schema):
from pyspark.sql.functions import split
assert sdf.isStreaming == True, &quot;DataFrame doesn't receive streaming data&quot;
col = split(sdf['value'], ',')
for idx, field in enumerate(schema):
sdf = sdf.withColumn(field.name, col.getItem(idx).cast(field.dataType))
return sdf.select([field.name for field in schema])
rides = parse_data_from_kafka_message(rides, ridesSchema)
fares = parse_data_from_kafka_message(fares, faresSchema)
```
在上面的代码中我们定义了函数parse_data_from_kafka_message用来把Kafka发来的message根据schema拆成对应的属性转换类型并加入到DataFrame的表中。
正如我们之前提到的,读入的数据包含了一些无用信息。
首先,所有代表出发的事件都已被删除,因为到达事件已经包含了出发事件的所有信息,而且只有到达之后才会付费。
其次出发地点和目的地在纽约范围外的数据也可以被删除。因为我们的目标是找出纽约市内小费较高的地点。DataFrame的filter函数可以很容易地做到这些。
```
MIN_LON, MAX_LON, MIN_LAT, MAX_LAT = -73.7, -74.05, 41.0, 40.5
rides = rides.filter(
rides[&quot;startLon&quot;].between(MIN_LON, MAX_LON) &amp;
rides[&quot;startLat&quot;].between(MIN_LAT, MAX_LAT) &amp;
rides[&quot;endLon&quot;].between(MIN_LON, MAX_LON) &amp;
rides[&quot;endLat&quot;].between(MIN_LAT, MAX_LAT))
rides = rides.filter(rides[&quot;isStart&quot;] == &quot;END&quot;)
```
上面的代码中首先定义了纽约市的经纬度范围,然后把所有起点和终点在这个范围之外的数据都过滤掉了。最后,把所有代表出发事件的数据也移除掉。
当然,除了前面提到的清洗方案,可能还会有别的可以改进的地方,比如把不重要的信息去掉,例如乘客数量、过路费等,你可以自己思考一下。
## Stream-stream Join
我们的目标是找出小费较高的地理区域而现在收费信息和地理位置信息还在两个DataFrame中无法放在一起分析。那么要用怎样的方式把它们联合起来呢
你应该还记得DataFrame本质上是把数据当成一张关系型的表。在我们这个例子中rides所对应的表的键值Key是rideId其他列里我们关心的就是起点和终点的位置fares所对应的表键值也是rideId其他列里我们关心的就是小费信息tips
说到这里你可能会自然而然地想到如果可以像关系型数据表一样根据共同的键值rideId把两个表inner join起来就可以同时分析这两部分信息了。但是这里的DataFrame其实是两个数据流Spark可以把两个流Join起来吗
答案是肯定的。在Spark 2.3中流与流的JoinStream-stream join被正式支持。这样的Join难点就在于在任意一个时刻流数据都不是完整的流A中后面还没到的数据有可能要和流B中已经有的数据Join起来再输出。为了解决这个问题我们就要引入**数据水印**Watermark的概念。
数据水印定义了我们可以对数据延迟的最大容忍限度。
比如说如果定义水印是10分钟数据A的事件时间是1:00数据B的事件时间是1:10由于数据传输发生了延迟我们在1:15才收到了A和B那么我们将只处理数据B并更新结果A会被无视。在Join操作中好好利用水印我们就知道什么时候可以不用再考虑旧数据什么时候必须把旧数据保留在内存中。不然我们就必须把所有旧数据一直存在内存里导致数据不断增大最终可能会内存泄漏。
在这个例子中为什么我们做这样的Join操作需要水印呢
这是因为两个数据流并不保证会同时收到同一次出行的数据,因为收费系统需要额外的时间去处理,而且这两个数据流是独立的,每个都有可能产生数据延迟。所以要对时间加水印,以免出现内存中数据无限增长的情况。
那么下一个问题就是,究竟要对哪个时间加水印,出发时间还是到达时间?
前面说过了我们其实只关心到达时间所以对rides而言我们只需要对到达时间加水印。但是在fares这个DataFrame里并没有到达时间的任何信息所以我们没法选择只能对出发时间加水印。因此我们还需要额外定义一个时间间隔的限制出发时间和到达时间的间隔要在一定的范围内。具体内容你可以看下面的代码。
```
faresWithWatermark = fares
.selectExpr(&quot;rideId AS rideId_fares&quot;, &quot;startTime&quot;, &quot;totalFare&quot;, &quot;tip&quot;)
.withWatermark(&quot;startTime&quot;, &quot;30 minutes&quot;)
ridesWithWatermark = rides
.selectExpr(&quot;rideId&quot;, &quot;endTime&quot;, &quot;driverId&quot;, &quot;taxiId&quot;, &quot;startLon&quot;, &quot;startLat&quot;, &quot;endLon&quot;, &quot;endLat&quot;)
.withWatermark(&quot;endTime&quot;, &quot;30 minutes&quot;)
joinDF = faresWithWatermark
.join(ridesWithWatermark,
expr(&quot;&quot;&quot;
rideId_fares = rideId AND
endTime &gt; startTime AND
endTime &lt;= startTime + interval 2 hours
&quot;&quot;&quot;)
```
在这段代码中我们对fares和rides分别加了半小时的水印然后把两个DataFrame根据rideId和时间间隔的限制Join起来。这样joinDF就同时包含了地理位置和付费信息。
接下来,就让我们开始计算实时的小费最高区域。
## 计算结果并输出
到现在为止,我们还没有处理地点信息。原生的经纬度信息显然并没有很大用处。我们需要做的是把纽约市分割成几个区域,把数据中所有地点的经纬度信息转化成区域信息,这样司机们才可以知道大概哪个地区的乘客比较可能给高点的小费。
纽约市的区域信息以及坐标可以从网上找到这部分处理比较容易。每个接收到的数据我们都可以判定它在哪个区域内然后对joinDF增加一个列“area”来代表终点的区域。现在让我们假设area已经加到现有的DataFrame里。接下来我们需要把得到的信息告诉司机了。
还记得第16讲和第17讲中提到的滑动窗口操作吗这是流处理中常见的输出形式即输出每隔一段时间内特定时间窗口的特征值。在这个例子中我们可以每隔10分钟输出过去半小时内每个区域内的平均小费。这样的话司机可以每隔10分钟查看一下数据决定下一步去哪里接单。这个查询Query可以由以下代码产生。
```
tips = joinDF
.groupBy(
window(&quot;endTime&quot;, &quot;30 minutes&quot;, &quot;10 minutes&quot;),
&quot;area&quot;)
.agg(avg(&quot;tip&quot;))
```
最后我们把tips这个流式DataFrame输出。
```
query.writeStream
.outputMode(&quot;append&quot;)
.format(&quot;console&quot;)
.option(&quot;truncate&quot;, False
.start()
.awaitTermination()
```
你可能会问,为什么我们不可以把输出结果按小费多少进行排序呢?
这是因为两个流的inner-join只支持附加输出模式Append Mode而现在Structured Streaming不支持在附加模式下进行排序操作。希望将来Structured Streaming可以提供这个功能但是现在司机们只能扫一眼所有的输出数据来大概判断哪个地方的小费最高了。
## 小结
流处理和批处理都是非常常见的应用场景而且相较而言流处理更加复杂对延迟性要求更高。今天我们再次通过一个实例帮助你了解要如何利用Structured Streaming对真实数据集进行流处理。Spark最大的好处之一就是它拥有统一的批流处理框架和API希望你在课下要进一步加深对DataSet/DataFrame的熟练程度。
## 思考题
今天的主题是“案例实战”不过我留的是思考题而不是实践题。因为我不确定你是否会使用Kafka。如果你的工作中会接触到流数据那么你可以参考今天这个案例的思路和步骤来解决问题多加练习以便熟悉Spark的使用。如果你还没有接触过流数据但却想往这方面发展的话我就真的建议你去学习一下Kafka这是个能帮助我们更好地做流处理应用开发和部署的利器。
现在,来说一下今天的思考题吧。
1. 为什么流的Inner-Join不支持完全输出模式?
1. 对于Inner-Join而言加水印是否是必须的 Outer-Join呢
欢迎你把答案写在留言区,与我和其他同学一起讨论。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,167 @@
<audio id="audio" title="21 | 深入对比Spark与Flink帮你系统设计两开花" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fe/ab/fe113271d8348a35f9157255f5abc8ab.mp3"></audio>
你好,我是蔡元楠。
今天我要与你分享的主题是“深入对比Spark与Flink”。
相信通过这一模块前9讲的学习你对Spark已经有了基本的认识。现在我们先来回顾整个模块理清一下思路。
首先从MapReduce框架存在的问题入手我们知道了Spark的主要优点比如用内存运算来提高性能提供很多High-level API开发者无需用map和reduce两个操作实现复杂逻辑支持流处理等等。
接下来我们学习了Spark的数据抽象——RDD。RDD是整个Spark的核心概念所有的新API在底层都是基于RDD实现的。但是RDD是否就是完美无缺的呢显然不是它还是很底层不方便开发者使用而且用RDD API写的应用程序需要大量的人工调优来提高性能。
Spark SQL提供的DataFrame/DataSet API就解决了这个问题它提供类似SQL的查询接口把数据看成关系型数据库的表提升了熟悉关系型数据库的开发者的工作效率。这部分内容都是专注于数据的批处理那么我们很自然地就过渡到下一个问题Spark是怎样支持流处理的呢
那就讲到了Spark Streaming和新的Structured Streaming这是Spark的流处理组件其中Structured Streaming也可以使用DataSet/DataFrame API这就实现了Spark批流处理的统一。
通过这个简单的回顾我们发现Spark的发布和之后各个版本新功能的发布并不是开发人员拍脑袋的决定每个新版本发布的功能都是在解决旧功能的问题。在如此多的开源工作者的努力下Spark生态系统才有今天的规模成为了当前最流行的大数据处理框架之一。
在开篇词中我就提到过,我希望你能通过这个专栏建立自己的批判性思维,遇到一个新的技术,多问为什么,而不是盲目的接受和学习。只有这样我们才能不随波逐流,成为这个百花齐放的技术时代的弄潮儿。
所以这里我想问你一个问题Spark有什么缺点
这个缺点我们之前已经提到过一个——无论是Spark Streaming还是Structured StreamingSpark流处理的实时性还不够所以无法用在一些对实时性要求很高的流处理场景中。
这是因为Spark的流处理是基于所谓微批处理Micro-batch processing的思想即它把流处理看作是批处理的一种特殊形式每次接收到一个时间间隔的数据才会去处理所以天生很难在实时性上有所提升。
虽然在Spark 2.3中提出了连续处理模型Continuous Processing Model但是现在只支持很有限的功能并不能在大的项目中使用。Spark还需要做出很大的努力才能改进现有的流处理模型。
想要在流处理的实时性上提升,就不能继续用微批处理的模式,而要想办法实现真正的流处理,即每当有一条数据输入就立刻处理,不做等待。那么当今时代有没有这样的流处理框架呢?
Apache Flink就是其中的翘楚。它采用了基于操作符Operator的连续流模型可以做到微秒级别的延迟。今天我就带你一起了解一下这个流行的数据处理平台并将Flink与Spark做深入对比方便你在今后的实际项目中做出选择。
## Flink核心模型简介
Flink中最核心的数据结构是Stream它代表一个运行在多个分区上的并行流。
在Stream上同样可以进行各种转换操作Transformation。与Spark的RDD不同的是Stream代表一个数据流而不是静态数据的集合。所以它包含的数据是随着时间增长而变化的。而且Stream上的转换操作都是逐条进行的即每当有新的数据进来整个流程都会被执行并更新结果。这样的基本处理模式决定了Flink会比Spark Streaming有更低的流处理延迟性。
当一个Flink程序被执行的时候它会被映射为Streaming Dataflow下图就是一个Streaming Dataflow的示意图。
<img src="https://static001.geekbang.org/resource/image/c4/b5/c49f4155d91c58050d8c7a2896bbc9b5.jpg" alt="">
在图中你可以看出Streaming Dataflow包括Stream和Operator操作符。转换操作符把一个或多个Stream转换成多个Stream。每个Dataflow都有一个输入数据源Source和输出数据源Sink。与Spark的RDD转换图类似Streaming Dataflow也会被组合成一个有向无环图去执行。
在Flink中程序天生是并行和分布式的。一个Stream可以包含多个分区Stream Partitions一个操作符可以被分成多个操作符子任务每一个子任务是在不同的线程或者不同的机器节点中独立执行的。如下图所示
<img src="https://static001.geekbang.org/resource/image/e9/58/e90778ee8f3cf092d80b73dca59a8658.jpg" alt="">
从上图你可以看出Stream在操作符之间传输数据的形式有两种一对一和重新分布。
- 一对一One-to-oneStream维护着分区以及元素的顺序比如上图从输入数据源到map间。这意味着map操作符的子任务处理的数据和输入数据源的子任务生产的元素的数据相同。你有没有发现它与RDD的窄依赖类似。
- 重新分布RedistributingStream中数据的分区会发生改变比如上图中map与keyBy之间。操作符的每一个子任务把数据发送到不同的目标子任务。
## Flink的架构
当前版本Flink的架构如下图所示。
<img src="https://static001.geekbang.org/resource/image/72/8a/7279dcfede45e83e1f8a9ff28cca178a.png" alt="">
我们可以看到,这个架构和[第12讲](https://time.geekbang.org/column/article/94410)中介绍的Spark架构比较类似都分为四层存储层、部署层、核心处理引擎、high-level的API和库。
从存储层来看Flink同样兼容多种主流文件系统如HDFS、Amazon S3多种数据库如HBase和多种数据流如Kafka和Flume。
从部署层来看Flink不仅支持本地运行还能在独立集群或者在被 YARN 或 Mesos 管理的集群上运行,也能部署在云端。
核心处理引擎就是我们刚才提到的分布式Streaming Dataflow所有的高级API及应用库都会被翻译成包含Stream和Operator的Dataflow来执行。
Flink提供的两个核心API就是DataSet API和DataStream API。你没看错名字和Spark的DataSet、DataFrame非常相似。顾名思义DataSet代表有界的数据集而DataStream代表流数据。所以DataSet API是用来做批处理的而DataStream API是做流处理的。
也许你会问Flink这样基于流的模型是怎样支持批处理的在内部DataSet其实也用Stream表示静态的有界数据也可以被看作是特殊的流数据而且DataSet与DataStream可以无缝切换。所以Flink的核心是DataStream。
DataSet和DataStream都支持各种基本的转换操作如map、filter、count、groupBy等让我们来看一个用DataStream实现的统计词频例子。
```
public class WindowWordCount {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream&lt;Tuple2&lt;String, Integer&gt;&gt; dataStream = env
  .socketTextStream(&quot;localhost&quot;, 9999)
  .flatMap(new Splitter())
  .keyBy(0)
  .timeWindow(Time.seconds(5))
  .sum(1);
dataStream.print();
env.execute(&quot;Window WordCount&quot;);
}
public static class Splitter implements FlatMapFunction&lt;String, Tuple2&lt;String, Integer&gt;&gt; {
@Override
public void flatMap(String sentence, Collector&lt;Tuple2&lt;String, Integer&gt;&gt; out) {
for (String word: sentence.split(&quot; &quot;)) {
out.collect(new Tuple2&lt;String, Integer&gt;(word, 1));
}
}
}
```
这里我是用Java来示范的因为Flink就是用Java开发的所以它对Java有原生的支持。此外也可以用Scala来开发Flink程序在1.0版本后更是支持了Python。
在这个例子中我们首先创建了一个Splitter类来把输入的句子拆分成词语1的对。在主程序中用StreamExecutionEnvironment创建DataStream来接收本地Web Socket的文本流并进行了4步操作。
1. 用flatMap把输入文本拆分成词语1的对
1. 用keyBy把相同的词语分配到相同的分区
1. 设好5秒的时间窗口
1. 对词语的出现频率用sum求和。
可以看出DataStream的使用方法和RDD比较相似都是把程序拆分成一系列的转换操作并分布式地执行。
在DataSet和DataStream之上有更高层次的Table API。Table API和Spark SQL的思想类似是关系型的API用户可以像操作SQL数据库表一样的操作数据而不需要通过写Java代码、操作DataStream/DataSet的方式进行数据处理更不需要手动优化代码的执行逻辑。
此外Table API同样统一了Flink的批处理和流处理。
## Flink和Spark对比
通过前面的学习我们了解到Spark和Flink都支持批处理和流处理接下来让我们对这两种流行的数据处理框架在各方面进行对比。
首先,这两个数据处理框架有很多相同点。
- 都基于内存计算;
- 都有统一的批处理和流处理API都支持类似SQL的编程接口
- 都支持很多相同的转换操作编程都是用类似于Scala Collection API的函数式编程模式
- 都有完善的错误恢复机制;
- 都支持Exactly once的语义一致性。
当然它们的不同点也是相当明显我们可以从4个不同的角度来看。
**从流处理的角度来讲**Spark基于微批量处理把流数据看成是一个个小的批处理数据块分别处理所以延迟性只能做到秒级。而Flink基于每个事件处理每当有新的数据输入都会立刻处理是真正的流式计算支持毫秒级计算。由于相同的原因Spark只支持基于时间的窗口操作处理时间或者事件时间而Flink支持的窗口操作则非常灵活不仅支持时间窗口还支持基于数据本身的窗口开发者可以自由定义想要的窗口操作。
**从SQL功能的角度来讲**Spark和Flink分别提供SparkSQL和Table API提供SQL交互支持。两者相比较Spark对SQL支持更好相应的优化、扩展和性能更好而 Flink 在 SQL 支持方面还有很大提升空间。
**从迭代计算的角度来讲**Spark对机器学习的支持很好因为可以在内存中缓存中间计算结果来加速机器学习算法的运行。但是大部分机器学习算法其实是一个有环的数据流在Spark中却是用无环图来表示。而Flink支持在运行时间中的有环数据流从而可以更有效的对机器学习算法进行运算。
**从相应的生态系统角度来讲**Spark的社区无疑更加活跃。Spark可以说有着Apache旗下最多的开源贡献者而且有很多不同的库来用在不同场景。而Flink由于较新现阶段的开源社区不如Spark活跃各种库的功能也不如Spark全面。但是Flink还在不断发展各种功能也在逐渐完善。
## 小结
今天我们从Spark存在的一个缺点——无法高效应对低延迟的流处理场景入手一起学习了另一个主流流数据处理框架Flink还对比了这两个框架的异同相信现在你对两个框架肯定有了更多的认识。
我经常被问到的一个问题是Spark和Flink到底该选哪一个对于这个问题我们还是要分一下场景。
对于以下场景你可以选择Spark。
1. 数据量非常大而且逻辑复杂的批数据处理,并且对计算效率有较高要求(比如用大数据分析来构建推荐系统进行个性化推荐、广告定点投放等);
1. 基于历史数据的交互式查询,要求响应较快;
1. 基于实时数据流的数据处理,延迟性要求在在数百毫秒到数秒之间。
Spark完美满足这些场景的需求 而且它可以一站式解决这些问题,无需用别的数据处理平台。
由于Flink是为了提升流处理而创建的平台所以它适用于各种需要非常低延迟微秒到毫秒级的实时数据处理场景比如实时日志报表分析。
而且Flink用流处理去模拟批处理的思想比Spark用批处理去模拟流处理的思想扩展性更好所以我相信将来Flink会发展的越来越好生态和社区各方面追上Spark。比如阿里巴巴就基于Flink构建了公司范围内全平台使用的数据处理平台Blink美团、饿了么等公司也都接受Flink作为数据处理解决方案。
可以说Spark和Flink都在某种程度上统一了批处理和流处理但也都有一些不足。下一模块中让我们来一起学习一个全新的、完全统一批流处理的数据处理平台——Apache Beam到时候我们会对Spark的优缺点有更加深入的认识。
## 思考题
除了高延迟的流处理这一缺点外你认为Spark还有什么不足可以怎样改进
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。