==========第一篇:Scala编程语言=========
一、Scala语言基础
1、Scala语言简介
Scala是一种多范式的编程语言,其设计的初衷是要集成面向对象编程和函数式编程的各种特性。Scala运行于Java平台(Java虚拟机),并兼容现有的Java程序。它也能运行于CLDC配置的Java ME中。目前还有另一.NET平台的实现,不过该版本更新有些滞后。Scala的编译模型(独立编译,动态类加载)与Java和C#一样,所以Scala代码可以调用Java类库(对于.NET实现则可调用.NET类库)。Scala包括编译器和类库,以及BSD许可证发布。
学习Scala编程语言,为后续学习Spark奠定基础。
2、下载和安装Scala
l 安装JDK
l 下载Scala:http://www.scala-lang.org/download/
l 安装Scala:
n 设置环境变量:SCALA_HOME和PATH路径
l 验证Scala
3、Scala的运行环境
l REPL(Read Evaluate Print Loop):命令行
l IDE:图形开发工具
n The Scala IDE (Based on Eclipse):http://scala-ide.org/
n IntelliJ IDEA with Scala plugin:http://www.jetbrains.com/idea/download/
n Netbeans IDE with the Scala plugin
4、Scala的常用数据类型
注意:在Scala中,任何数据都是对象。例如:
① 数值类型:Byte,Short,Int,Long,Float,Double
l Byte: 8位有符号数字,从-128 到 127
l Short: 16位有符号数据,从-32768 到 32767
l Int: 32位有符号数据
l Long: 64位有符号数据
例如:
val a:Byte = 10
a+10
得到:res9: Int = 20
这里的res9是新生成变量的名字
val b:Short = 20
a+b
注意:在Scala中,定义变量可以不指定类型,因为Scala会进行类型的自动推导。
② 字符类型和字符串类型:Char和String
对于字符串,在Scala中可以进行插值操作。
注意:前面有个s;相当于执行:"My Name is " + s1
③ Unit类型:相当于Java中的void类型
④ Nothing类型:一般表示在执行过程中,产生了Exception
例如,我们定义一个函数如下:
5、Scala变量的申明和使用
l 使用val和var申明变量
例如:scala> val answer = 8 * 3 + 2
可以在后续表达式中使用这些名称
l val:定义的值实际是一个常量
要申明其值可变的变量:var
注意:可以不用显式指定变量的类型,Scala会进行自动的类型推到
6、Scala的函数和方法的使用
l 可以使用Scala的预定义函数
例如:求两个值的最大值
l 也可以使用def关键字自定义函数
语法:
示例:
7、Scala的条件表达式
Scala的if/else语法结构和Java或C++一样。
不过,在Scala中,if/else是表达式,有值,这个值就是跟在if或else之后的表达式的值。
8、Scala的循环
Scala拥有与Java和C++相同的while和do循环
Scala中,可以使用for和foreach进行迭代
- 使用for循环案例:
注意:
(*) <- 表示Scala中的generator,即:提取符
(*)第三种写法是第二种写法的简写
- 在for循环中,还可以使用yield关键字来产生一个新的集合
在上面的案例中,我们将list集合中的每个元素转换成了大写,并且使用yield关键字生成了一个新的集合。
- 使用while循环:注意使用小括号,不是中括号
- 使用do ... while循环
- 使用foreach进行迭代
注意:在上面的例子中,foreach接收了另一个函数(println)作为值
9、Scala函数的参数
- Scala中,有两种函数参数的求值策略
l Call By Value:对函数实参求值,且仅求一次
l Call By Name:函数实参每次在函数体内被用到时都会求值
我们来分析一下,上面两个调用执行的过程:
一份复杂一点的例子:
- Scala中的函数参数
l 默认参数
l 代名参数
l 可变参数
10、Scala的Lazy值(懒值)
当val被申明为lazy时,它的初始化将被推迟,直到我们首次对它取值。
一个更为复杂一点的例子:读取文件:
11、异常的处理
Scala异常的工作机制和Java或者C++一样。直接使用throw关键字抛出异常。
使用try...catch...finally来捕获和处理异常:
12、Scala中的数组
Scala数组的类型:
l 定长数组:使用关键字Array
l 变长数组:使用关键字ArrayBuffer
l 遍历数组
l Scala数组的常用操作
l Scala的多维数组
l 和Java一样,多维数组是通过数组的数组来实现的。
l 也可以创建不规则的数组,每一行的长度各不相同。
13、映射
映射就是Map集合,由一个(key,value)组成。
-> 操作符用来创建
例如:
val scores = Map(“Alice” -> 10,”Bob” -> 3,”Cindy” -> 8)
映射的类型分为:不可变Map和可变Map
映射的操作
l 获取映射中的值
l 更新映射中的值(必须是可变Map)
l 迭代映射
14、元组(Tuple)
元组是不同类型的值的聚集。
例如:val t = (1, 3.14, "Fred") // 类型为Tuple3[Int, Double, java.lang.String]
这里:Tuple是类型,3是表示元组中有三个元素。
元组的访问和遍历:
注意:要遍历Tuple中的元素,需要首先生成对应的迭代器。不能直接使用for或者foreach。
二、Scala语言的面向对象
1、面向对象的基本概念
把数据及对数据的操作方法放在一起,作为一个相互依存的整体——对象
面向对象的三大特征:
u 封装
u 继承
u 多态
2、类的定义
简单类和无参方法:
案例:注意没有class前面没有public关键字修饰。
如果要开发main方法,需要将main方法定义在该类的伴生对象中,即:object对象中,(后续做详细的讨论)。
3、属性的getter和setter方法
l 当定义属性是private时候,scala会自动为其生成对应的get和set方法
private var stuName:String = "Tom"
- get方法: stuName ----> s2.stuName() 由于stuName是方法的名字,所以可以加上一个括号
- set方法: stuName_= ----> stuName_= 是方法的名字
l 定义属性:private var money:Int = 1000 希望money只有get方法,没有set方法??
- 办法:将其定义为常量private val money:Int = 1000
l private[this]的用法:该属性只属于该对象私有,就不会生成对应的set和get方法。如果这样,就不能直接调用,例如:s1.stuName ---> 错误
4、内部类(嵌套类)
我们可以在一个类的内部在定义一个类,如下:我们在Student类中,再定义了一个Course类用于保存学生选修的课程。
开发一个测试程序进行测试:
5、类的构造器
类的构造器分为:主构造器、辅助构造器
l 主构造器:和类的声明结合在一起,只能有一个主构造器
Student4(val stuName:String,val stuAge:Int)
(1) 定义类的主构造器:两个参数
(2) 声明了两个属性:stuName和stuAge 和对应的get和set方法
l 辅助构造器:可以有多个辅助构造器,通过关键字this来实现
6、Scala中的Object对象
Scala没有静态的修饰符,但Object对象下的成员都是静态的 ,若有同名的class,这其作为它的伴生类。在Object中一般可以为伴生类做一些初始化等操作。
下面是Java中的静态块的例子。在这个例子中,我们对JDBC进行了初始化。
而Scala中的Object就相当于Java中静态块。
Object对象的应用
u 单例对象
u 使用应用程序对象:可以省略main方法;需要从父类App继承。
7、Scala中的apply方法
遇到如下形式的表达式时,apply方法就会被调用:
Object(参数1,参数2,......,参数N)
通常,这样一个apply方法返回的是伴生类的对象;其作用是为了省略new关键字
Object的apply方法举例:
8、Scala中的继承
Scala和Java一样,使用extends关键字扩展类。
l 案例一:Employee类继承Person类
l 案例二:在子类中重写父类的方法
l 案例三:使用匿名子类
l 案例四:使用抽象类。抽象类中包含抽象方法,抽象类只能用来继承。
l 案例五:使用抽象字段。抽象字段就是一个没有初始值的字段
9、Scala中的trait(特质)
trait就是抽象类。trait跟抽象类最大的区别:trait支持多重继承
10、包的使用
Scala的包和Java中的包或者C++中的命名空间的目的是相同的:管理大型程序中的名称。
Scala中包的定义和使用:
u 包的定义
u 包的引入:Scala中依然使用import作为引用包的关键字,例如
u 而且Scala中的import可以写在任意地方(Java中,import写在最前面)
11、包对象
包可以包含类、对象和特质,但不能包含函数或者变量的定义。很不幸,这是Java虚拟机的局限。
把工具函数或者常量添加到包而不是某个Utils对象,这是更加合理的做法。Scala中,包对象的出现正是为了解决这个局限。
Scala中的包对象:常量,变量,方法,类,对象,trait(特质),包
12、Scala中的文件访问
l 读取行
l 读取字符
其实这里的source就指向了这个文件中的每个字符。
l 从URL或其他源读取:注意指定字符集UTF-8
l 读取二进制文件:Scala中并不支持直接读取二进制,但可以通过调用Java的InputStream来进行读入。
l 写入文本文件
三、Scala语言的函数式编程
1、Scala中的函数
在Scala中,函数是“头等公民”,就和数字一样。可以在变量中存放函数,即:将函数作为变量的值(值函数)。
2、匿名函数
3、带函数参数的函数,即:高阶函数
示例1:
(*)首先,定义一个最普通的函数
(*)再定义一个高阶函数
(*)分析这个高阶函数调用的过程
示例2:
在这个例子中,首先定义了一个普通的函数mytest,然后定义了一个高阶函数myFunction;myFunction接收三个参数:第一个f是一个函数参数,第二个是x,第三个是y。而f是一个函数参数,本身接收两个Int的参数,返回一个Int的值。
4、闭包
就是函数的嵌套,即:在一个函数定义中,包含另外一个函数的定义;并且在内函数中可以访问外函数中的变量。
测试上面的函数:
5、柯里化:Currying
柯里化函数(Curried Function)是把具有多个参数的函数转换为一条函数链,每个节点上是单一参数。
一个简单的例子:
6、高阶函数示例
示例1:
示例2:
示例3:
示例4:
示例5:
在这个例子中,可以被2整除的被分到一个分区;不能被2整除的被分到另一个分区。
示例6:
示例7:
示例8:
在这个例子中,分为两步:
(1)将(1,2,3)和(4,5,6)这两个集合合并成一个集合
(2)再对每个元素乘以2
四、Scala中的集合
1、可变集合和不可变集合
l 可变集合
l 不可变集合:
n 集合从不改变,因此可以安全地共享其引用。
n 甚至是在一个多线程的应用程序当中也没问题。
集合的操作:
2、列表
l 不可变列表(List)
不可变列表的相关操作:
l 可变列表(LinkedList):scala.collection.mutable
3、序列
常用的序列有:Vector和Range
u Vector是ArrayBuffer的不可变版本,是一个带下标的序列
u Range表示一个整数序列
4、集(Set)和集的操作
l 集Set是不重复元素的集合
l 和列表不同,集并不保留元素插入的顺序。默认以Hash集实现
示例1:创建集
示例2:集的操作
5、模式匹配
Scala有一个强大的模式匹配机制,可以应用在很多场合:
u switch语句
u 类型检查
Scala还提供了样本类(case class),对模式匹配进行了优化
模式匹配示例:
l 更好的switch
l Scala的守卫
l 模式匹配中的变量
l 类型模式
l 匹配数组和列表
6、样本类(CaseClass)
简单的来说,Scala的case class就是在普通的类定义前加case这个关键字,然后你可以对这些类来模式匹配。
case class带来的最大的好处是它们支持模式识别。
首先,回顾一下前面的模式匹配:
其次,如果我们想判断一个对象是否是某个类的对象,跟Java一样可以使用isInstanceOf
下面这个好像有点问题
最后,在Scala中有一种更简单的方式来判断,就是case class
注意:需要在class前面使用case关键字。
五、Scala语言的高级特性
1、什么是泛型类
和Java或者C++一样,类和特质可以带类型参数。在Scala中,使用方括号来定义类型参数
测试程序:
2、什么是泛型函数
函数和方法也可以带类型参数。和泛型类一样,我们需要把类型参数放在方法名之后。
注意:这里的ClassTag是必须的,表示运行时的一些信息,比如类型。
3、Upper Bounds 与 Lower Bounds
类型的上界和下界,是用来定义类型变量的范围。它们的含义如下:
l S <: T
这是类型上界的定义。也就是S必须是类型T的子类(或本身,自己也可以认为是自己的子类。
u U >: T
这是类型下界的定义。也就是U必须是类型T的父类(或本身,自己也可以认为是自己的父类)。
l 一个简单的例子:
l 一个复杂一点的例子(上界):
l 再来看一个例子:
4、视图界定(View bounds)
它比 <: 适用的范围更广,除了所有的子类型,还允许隐式转换过去的类型。用 <% 表示。尽量使用视图界定,来取代泛型的上界,因为适用的范围更加广泛。
示例:
l 上面写过的一个列子。这里由于T的上界是String,当我们传递100和200的时候,就会出现类型不匹配。
l 但是100和200是可以转成字符串的,所以我们可以使用视图界定让addTwoString方法接收更广泛的数据类型,即:字符串及其子类、可以转换成字符串的类型。
注意:使用的是 <%
l 但实际运行的时候,会出现错误:
这是因为:Scala并没有定义如何将Int转换成String的规则,所以要使用视图界定,我们就必须创建转换的规则。
l 创建转换规则
l 运行成功
5、协变和逆变
l 协变:
Scala的类或特征的范型定义中,如果在类型参数前面加入+符号,就可以使类或特征变为协变了。
u 逆变:
在类或特征的定义中,在类型参数之前加上一个-符号,就可定义逆变范型类和特征了。
总结一下:Scala的协变:泛型变量的值可以是本身类型或者其子类的类型
Scala的逆变:泛型变量的值可以是本身类型或者其父类的类型
6、隐式转换函数
所谓隐式转换函数指的是以implicit关键字申明的带有单个参数的函数。
l 前面讲视图界定时候的一个例子:
l 再举一个例子:我们把Fruit对象转换成了Monkey对象
7、隐式参数
使用implicit申明的函数参数叫做隐式参数。我们也可以使用隐式参数实现隐式的转换
8、隐式类
所谓隐式类: 就是对类增加implicit 限定的类,其作用主要是对类的功能加强!
==========第二篇:Spark Core==========
一、什么是Spark?(官网:http://spark.apache.org)
1、什么是Spark?
翻译:Spark是一个针对大规模数据处理的快速通用引擎。
Spark是一种快速、通用、可扩展的大数据分析引擎,2009年诞生于加州大学伯克利分校AMPLab,2010年开源,2013年6月成为Apache孵化项目,2014年2月成为Apache顶级项目。目前,Spark生态系统已经发展成为一个包含多个子项目的集合,其中包含SparkSQL、Spark Streaming、GraphX、MLlib等子项目,Spark是基于内存计算的大数据并行计算框架。Spark基于内存计算,提高了在大数据环境下数据处理的实时性,同时保证了高容错性和高可伸缩性,允许用户将Spark部署在大量廉价硬件之上,形成集群。Spark得到了众多大数据公司的支持,这些公司包括Hortonworks、IBM、Intel、Cloudera、MapR、Pivotal、百度、阿里、腾讯、京东、携程、优酷土豆。当前百度的Spark已应用于凤巢、大搜索、直达号、百度大数据等业务;阿里利用GraphX构建了大规模的图计算和图挖掘系统,实现了很多生产系统的推荐算法;腾讯Spark集群达到8000台的规模,是当前已知的世界上最大的Spark集群。
2、为什么要学习Spark?
(*)Hadoop的MapReduce计算模型存在的问题:
学习过Hadoop的MapReduce的学员都知道,MapReduce的核心是Shuffle(洗牌)。在整个Shuffle的过程中,至少会产生6次的I/O。下图是我们在讲MapReduce的时候,画的Shuffle的过程。
中间结果输出:基于MapReduce的计算引擎通常会将中间结果输出到磁盘上,进行存储和容错。另外,当一些查询(如:Hive)翻译到MapReduce任务时,往往会产生多个Stage(阶段),而这些串联的Stage又依赖于底层文件系统(如HDFS)来存储每一个Stage的输出结果,而I/O的效率往往较低,从而影响了MapReduce的运行速度。
(*)Spark的最大特点:基于内存
Spark是MapReduce的替代方案,而且兼容HDFS、Hive,可融入Hadoop的生态系统,以弥补MapReduce的不足。
3、Spark的特点:快、易用、通用、兼容性
(*)快
与Hadoop的MapReduce相比,Spark基于内存的运算速度要快100倍以上,即使,Spark基于硬盘的运算也要快10倍。Spark实现了高效的DAG执行引擎,从而可以通过内存来高效处理数据流。
(*)易用
Spark支持Java、Python和Scala的API,还支持超过80种高级算法,使用户可以快速构建不同的应用。而且Spark支持交互式的Python和Scala的shell,可以非常方便地在这些shell中使用Spark集群来验证解决问题的方法。
(*)通用
Spark提供了统一的解决方案。Spark可以用于批处理、交互式查询(Spark SQL)、实时流处理(Spark Streaming)、机器学习(Spark MLlib)和图计算(GraphX)。这些不同类型的处理都可以在同一个应用中无缝使用。Spark统一的解决方案非常具有吸引力,毕竟任何公司都想用统一的平台去处理遇到的问题,减少开发和维护的人力成本和部署平台的物力成本。
另外Spark还可以很好的融入Hadoop的体系结构中可以直接操作HDFS,并提供Hive on Spark、Pig on Spark的框架集成Hadoop。
(*)兼容性
Spark可以非常方便地与其他的开源产品进行融合。比如,Spark可以使用Hadoop的YARN和Apache Mesos作为它的资源管理和调度器,器,并且可以处理所有Hadoop支持的数据,包括HDFS、HBase和Cassandra等。这对于已经部署Hadoop集群的用户特别重要,因为不需要做任何数据迁移就可以使用Spark的强大处理能力。Spark也可以不依赖于第三方的资源管理和调度器,它实现了Standalone作为其内置的资源管理和调度框架,这样进一步降低了Spark的使用门槛,使得所有人都可以非常容易地部署和使用Spark。此外,Spark还提供了在EC2上部署Standalone的Spark集群的工具。
二、Spark的体系结构与安装部署
1、Spark集群的体系结构
官方的一张图:
自己的一张图:
2、Spark的安装与部署
Spark的安装部署方式有以下几种模式:
l Standalone
l YARN
l Mesos
l Amazon EC2
(*)Spark Standalone伪分布的部署
l 配置文件:conf/spark-env.sh
n export JAVA_HOME=/root/training/jdk1.7.0_75
n export SPARK_MASTER_HOST=spark81
n export SPARK_MASTER_PORT=7077
n 下面的可以不写,默认
n export SPARK_WORKER_CORES=1
n export SPARK_WORKER_MEMORY=1024m
l 配置文件:conf/slave
n spark81
(*)Spark Standalone全分布的部署
l 配置文件:conf/spark-env.sh
n export JAVA_HOME=/root/training/jdk1.7.0_75
n export SPARK_MASTER_HOST=spark82
n export SPARK_MASTER_PORT=7077
n 下面的可以不写,默认
n export SPARK_WORKER_CORES=1
n export SPARK_WORKER_MEMORY=1024m
l 配置文件:conf/slave
n spark83
n spark84
(*)启动Spark集群:start-all.sh
3、Spark HA的实现
(*)基于文件系统的单点恢复
主要用于开发或测试环境。当spark提供目录保存spark Application和worker的注册信息,并将他们的恢复状态写入该目录中,这时,一旦Master发生故障,就可以通过重新启动Master进程(sbin/start-master.sh),恢复已运行的spark Application和worker的注册信息。
基于文件系统的单点恢复,主要是在spark-en.sh里对SPARK_DAEMON_JAVA_OPTS设置
配置参数 |
参考值 |
spark.deploy.recoveryMode |
设置为FILESYSTEM开启单点恢复功能,默认值:NONE |
spark.deploy.recoveryDirectory |
Spark 保存恢复状态的目录 |
参考:
export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=FILESYSTEM -Dspark.deploy.recoveryDirectory=/root/training/spark-2.1.0-bin-hadoop2.7/recovery"
测试:
1、在spark82上启动Spark集群
2、在spark83上启动spark shell
MASTER=spark://spark82:7077 spark-shell
3、在spark82上停止master
stop-master.sh
4、观察spark83上的输出:
5、在spark82上重启master
start-master.sh
(*)基于Zookeeper的Standby Masters
ZooKeeper提供了一个Leader Election机制,利用这个机制可以保证虽然集群存在多个Master,但是只有一个是Active的,其他的都是Standby。当Active的Master出现故障时,另外的一个Standby Master会被选举出来。由于集群的信息,包括Worker, Driver和Application的信息都已经持久化到ZooKeeper,因此在切换的过程中只会影响新Job的提交,对于正在进行的Job没有任何的影响。加入ZooKeeper的集群整体架构如下图所示。
配置参数 |
参考值 |
spark.deploy.recoveryMode |
设置为ZOOKEEPER开启单点恢复功能,默认值:NONE |
spark.deploy.zookeeper.url |
ZooKeeper集群的地址 |
spark.deploy.zookeeper.dir |
Spark信息在ZK中的保存目录,默认:/spark |
l 参考:
export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=ZOOKEEPER -Dspark.deploy.zookeeper.url=bigdata12:2181,bigdata13:2181,bigdata14:2181 -Dspark.deploy.zookeeper.dir=/spark"
l 另外:每个节点上,需要将以下两行注释掉。
l ZooKeeper中保存的信息
三、执行Spark Demo程序
1、执行Spark Example程序
(*)示例程序:$SPARK_HOME/examples/jars/spark-examples_2.11-2.1.0.jar
(*)所有的示例程序:$EXAMPLE_HOME/examples/src/main有Java、Scala等等
(*)Demo:蒙特卡罗求PI
命令:
spark-submit --master spark://spark81:7077 --class org.apache.spark.examples.SparkPi examples/jars/spark-examples_2.11-2.1.0.jar 100
2、使用Spark Shell
spark-shell是Spark自带的交互式Shell程序,方便用户进行交互式编程,用户可以在该命令行下用scala编写spark程序。
(*)启动Spark Shell:spark-shell
也可以使用以下参数:
参数说明:
--master spark://spark81:7077 指定Master的地址
--executor-memory 2g 指定每个worker可用内存为2G
--total-executor-cores 2 指定整个集群使用的cup核数为2个
例如:
spark-shell --master spark://spark81:7077 --executor-memory 2g --total-executor-cores 2
(*)注意:
如果启动spark shell时没有指定master地址,但是也可以正常启动spark shell和执行spark shell中的程序,其实是启动了spark的local模式,该模式仅在本机启动一个进程,没有与集群建立联系。
请注意local模式和集群模式的日志区别:
(*)在Spark Shell中编写WordCount程序
程序如下:
sc.textFile("hdfs://192.168.88.111:9000/data/data.txt").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).saveAsTextFile("hdfs://192.168.88.111:9000/output/spark/wc")
说明:
l sc是SparkContext对象,该对象时提交spark程序的入口
l textFile("hdfs://192.168.88.111:9000/data/data.txt")是hdfs中读取数据
l flatMap(_.split(" "))先map在压平
l map((_,1))将单词和1构成元组
l reduceByKey(_+_)按照key进行reduce,并将value累加
l saveAsTextFile("hdfs://192.168.88.111:9000/output/spark/wc")将结果写入到hdfs中
3、在IDEA中编写WordCount程序
(*)需要的jar包:$SPARK_HOME/jars/*.jar
(*)创建Scala Project,并创建Scala Object、或者Java Class
(*)书写源代码,并打成jar包,上传到Linux
==========================Scala版本==========================
(*)运行程序:
spark-submit --master spark://spark81:7077 --class mydemo.WordCount jars/wc.jar hdfs://192.168.88.111:9000/data/data.txt hdfs://192.168.88.111:9000/output/spark/wc1
====================Java版本(直接输出在屏幕)====================
(*)运行程序:
spark-submit --master spark://spark81:7077 --class
mydemo.JavaWordCount jars/wc.jar hdfs://192.168.88.111:9000/data/data.txt
====================Python版本(直接输出在屏幕)====================
(*)运行程序:
等等
四、Spark运行机制及原理分析
1、WordCount执行的流程分析
需要看源码一步步看。
2、Spark提交任务的流程
五、Spark的算子
1、RDD基础
- 什么是RDD?
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据抽象,它代表一个不可变、可分区、里面的元素可并行计算的集合。RDD具有数据流模型的特点:自动容错、位置感知性调度和可伸缩性。RDD允许用户在执行多个查询时显式地将工作集缓存在内存中,后续的查询能够重用工作集,这极大地提升了查询速度。
- RDD的属性(源码中的一段话)
² 一组分片(Partition),即数据集的基本组成单位。对于RDD来说,每个分片都会被一个计算任务处理,并决定并行计算的粒度。用户可以在创建RDD时指定RDD的分片个数,如果没有指定,那么就会采用默认值。默认值就是程序所分配到的CPU Core的数目。
² 一个计算每个分区的函数。Spark中RDD的计算是以分片为单位的,每个RDD都会实现compute函数以达到这个目的。compute函数会对迭代器进行复合,不需要保存每次计算的结果。
² RDD之间的依赖关系。RDD的每次转换都会生成一个新的RDD,所以RDD之间就会形成类似于流水线一样的前后依赖关系。在部分分区数据丢失时,Spark可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算。
² 一个Partitioner,即RDD的分片函数。当前Spark中实现了两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。只有对于于key-value的RDD,才会有Partitioner,非key-value的RDD的Parititioner的值是None。Partitioner函数不但决定了RDD本身的分片数量,也决定了parent RDD Shuffle输出时的分片数量。
² 一个列表,存储存取每个Partition的优先位置(preferred location)。对于一个HDFS文件来说,这个列表保存的就是每个Partition所在的块的位置。按照“移动数据不如移动计算”的理念,Spark在进行任务调度的时候,会尽可能地将计算任务分配到其所要处理数据块的存储位置。
u RDD的创建方式
- 通过外部的数据文件创建,如HDFS
val rdd1 = sc.textFile(“hdfs://192.168.88.111:9000/data/data.txt”)
- 通过sc.parallelize进行创建
val rdd1 = sc.parallelize(Array(1,2,3,4,5,6,7,8))
- RDD的类型:Transformation和Action
u RDD的基本原理
2、Transformation
RDD中的所有转换都是延迟加载的,也就是说,它们并不会直接计算结果。相反的,它们只是记住这些应用到基础数据集(例如一个文件)上的转换动作。只有当发生一个要求返回结果给Driver的动作时,这些转换才会真正运行。这种设计让Spark更加有效率地运行。
转换 |
含义 |
map(func) |
返回一个新的RDD,该RDD由每一个输入元素经过func函数转换后组成 |
filter(func) |
返回一个新的RDD,该RDD由经过func函数计算后返回值为true的输入元素组成 |
flatMap(func) |
类似于map,但是每一个输入元素可以被映射为0或多个输出元素(所以func应该返回一个序列,而不是单一元素) |
mapPartitions(func) |
类似于map,但独立地在RDD的每一个分片上运行,因此在类型为T的RDD上运行时,func的函数类型必须是Iterator[T] => Iterator[U] |
mapPartitionsWithIndex(func) |
类似于mapPartitions,但func带有一个整数参数表示分片的索引值,因此在类型为T的RDD上运行时,func的函数类型必须是(Int, Interator[T]) => Iterator[U] |
sample(withReplacement, fraction, seed) |
根据fraction指定的比例对数据进行采样,可以选择是否使用随机数进行替换,seed用于指定随机数生成器种子 |
union(otherDataset) |
对源RDD和参数RDD求并集后返回一个新的RDD |
intersection(otherDataset) |
对源RDD和参数RDD求交集后返回一个新的RDD |
distinct([numTasks])) |
对源RDD进行去重后返回一个新的RDD |
groupByKey([numTasks]) |
在一个(K,V)的RDD上调用,返回一个(K, Iterator[V])的RDD |
reduceByKey(func, [numTasks]) |
在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,与groupByKey类似,reduce任务的个数可以通过第二个可选的参数来设置 |
aggregateByKey(zeroValue)(seqOp,combOp,[numTasks]) |
|
sortByKey([ascending], [numTasks]) |
在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD |
sortBy(func,[ascending], [numTasks]) |
与sortByKey类似,但是更灵活 |
join(otherDataset, [numTasks]) |
在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD |
cogroup(otherDataset, [numTasks]) |
在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable<V>,Iterable<W>))类型的RDD |
cartesian(otherDataset) |
笛卡尔积 |
pipe(command, [envVars]) |
|
coalesce(numPartitions) |
|
repartition(numPartitions) |
|
repartitionAndSortWithinPartitions(partitioner) |
3、Action
动作 |
含义 |
reduce(func) |
通过func函数聚集RDD中的所有元素,这个功能必须是课交换且可并联的 |
collect() |
在驱动程序中,以数组的形式返回数据集的所有元素 |
count() |
返回RDD的元素个数 |
first() |
返回RDD的第一个元素(类似于take(1)) |
take(n) |
返回一个由数据集的前n个元素组成的数组 |
takeSample(withReplacement,num, [seed]) |
返回一个数组,该数组由从数据集中随机采样的num个元素组成,可以选择是否用随机数替换不足的部分,seed用于指定随机数生成器种子 |
takeOrdered(n, [ordering]) |
|
saveAsTextFile(path) |
将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本 |
saveAsSequenceFile(path) |
将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统。 |
saveAsObjectFile(path) |
|
countByKey() |
针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。 |
foreach(func) |
在数据集的每一个元素上,运行函数func进行更新。 |
Spark算子示例:
1、RDD的创建方式
通过外部的数据文件创建,如HDFS
val rdd1 = sc.textFile(“hdfs://192.168.88.111:9000/data/data.txt”)
通过sc.parallelize进行创建
val rdd1 = sc.parallelize(Array(1,2,3,4,5,6,7,8))
2、RDD的算子
(1)Transformation算子:
(*)map(func)算子: 将输入的每个元素重写组合成一个元组
val rdd2 = rdd1.map((_,"*"))
乘以10
val rdd2 = rdd1.map((_ * 10))
val rdd2 = rdd1.map((x:Int) = x + 10)
(*)filter(func):返回一个新的RDD,该RDD是经过func运算后返回true的元素
val rdd3 = rdd1.filter(_ > 5)
(*)flatMap(func) 压平操作
val books = sc.parallelize(List("Hadoop","Hive","HDFS"))
books.flatMap(_.toList).collect
结果:res18: Array[Char] = Array(H, a, d, o, o, p, H, i, v, e, H, D, F, S)
val sen = sc.parallelize(List("I love Beijing","I love China","Beijing is the capital of China"))
(*)union(otherDataset):并集运算,注意类型要一致
val rdd4 = sc.parallelize(List(5,6,4,7))
val rdd5 = sc.parallelize(List(1,2,3,4))
val rdd6 = rdd4.union(rdd5)
(*)intersection(otherDataset):交集
val rdd7 = rdd5.intersection(rdd4)
(*)distinct([numTasks])):去掉重复数据
val rdd8 = sc.parallelize(List(5,6,4,7,5,5,5))
rdd8.distinct.collect
(*)groupByKey([numTasks]) :对于一个<k,v>的RDD,按照Key进行分组
val rdd = sc.parallelize(Array(("I",1),("love",2),("I",3)))
rdd.groupByKey.collect
结果:res38: Array[(String, Iterable[Int])] = Array((love,CompactBuffer(2)), (I,CompactBuffer(1, 3)))
复杂一点的例子:
val sen = sc.parallelize(List("I love Beijing","I love China","Beijing is the capital of China"))
sen.flatMap(_.split(" ")).map((_,1)).groupByKey.collect
(*)reduceByKey(func, [numTasks]):类似于groupByKey,区别是reduceByKey会有一个combiner的过程对每个分区上的数据先做一次合并
画图说明,所以效率更高
(*)cartesian笛卡尔积
val rdd1 = sc.parallelize(List("tom", "jerry"))
val rdd2 = sc.parallelize(List("tom", "kitty", "shuke"))
val rdd3 = rdd1.cartesian(rdd2)
(2)Action算子:
val rdd1 = sc.parallelize(List(1,2,3,4,5), 2)
(*)collect
rdd1.collect
(*)reduce
val rdd2 = rdd1.reduce(_+_)
(*)count
rdd1.count
(*)top
rdd1.top(2)
(*)take
rdd1.take(2)
(*)first(similer to take(1))
rdd1.first
(*)takeOrdered
rdd1.takeOrdered(3)
4、RDD的缓存机制
RDD通过persist方法或cache方法可以将前面的计算结果缓存,但是并不是这两个方法被调用时立即缓存,而是触发后面的action时,该RDD将会被缓存在计算节点的内存中,并供后面重用。
通过查看源码发现cache最终也是调用了persist方法,默认的存储级别都是仅在内存存储一份,Spark的存储级别还有好多种,存储级别在object StorageLevel中定义的。
缓存有可能丢失,或者存储存储于内存的数据由于内存不足而被删除,RDD的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于RDD的一系列转换,丢失的数据会被重算,由于RDD的各个Partition是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部Partition。
l Demo示例:
l 通过UI进行监控:
5、RDD的Checkpoint(检查点)机制:容错机制
检查点(本质是通过将RDD写入Disk做检查点)是为了通过lineage(血统)做容错的辅助,lineage过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点的RDD开始重做Lineage,就会减少开销。
设置checkpoint的目录,可以是本地的文件夹、也可以是HDFS。一般是在具有容错能力,高可靠的文件系统上(比如HDFS, S3等)设置一个检查点路径,用于保存检查点数据。
分别举例说明:
l 本地目录
注意:这种模式,需要将spark-shell运行在本地模式上
l HDFS的目录
注意:这种模式,需要将spark-shell运行在集群模式上
l 源码中的一段话
6、RDD的依赖关系和Spark任务中的Stage
l RDD的依赖关系
RDD和它依赖的父RDD(s)的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)。
- 窄依赖指的是每一个父RDD的Partition最多被子RDD的一个Partition使用
总结:窄依赖我们形象的比喻为独生子女
- 宽依赖指的是多个子RDD的Partition会依赖同一个父RDD的Partition
总结:窄依赖我们形象的比喻为超生
l Spark任务中的Stage
DAG(Directed Acyclic Graph)叫做有向无环图,原始的RDD通过一系列的转换就就形成了DAG,根据RDD之间的依赖关系的不同将DAG划分成不同的Stage,对于窄依赖,partition的转换处理在Stage中完成计算。对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算,因此宽依赖是划分Stage的依据。
7、RDD基础练习
- 练习1:
//通过并行化生成rdd
val rdd1 = sc.parallelize(List(5, 6, 4, 7, 3, 8, 2, 9, 1, 10))
//对rdd1里的每一个元素乘2然后排序
val rdd2 = rdd1.map(_ * 2).sortBy(x => x, true)
//过滤出大于等于十的元素
val rdd3 = rdd2.filter(_ >= 10)
//将元素以数组的方式在客户端显示
rdd3.collect
- 练习2:
val rdd1 = sc.parallelize(Array("a b c", "d e f", "h i j"))
//将rdd1里面的每一个元素先切分在压平
val rdd2 = rdd1.flatMap(_.split(' '))
rdd2.collect
- 练习3:
val rdd1 = sc.parallelize(List(5, 6, 4, 3))
val rdd2 = sc.parallelize(List(1, 2, 3, 4))
//求并集
val rdd3 = rdd1.union(rdd2)
//求交集
val rdd4 = rdd1.intersection(rdd2)
//去重
rdd3.distinct.collect
rdd4.collect
- 练习4:
val rdd1 = sc.parallelize(List(("tom", 1), ("jerry", 3), ("kitty", 2)))
val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 1), ("shuke", 2)))
//求jion
val rdd3 = rdd1.join(rdd2)
rdd3.collect
//求并集
val rdd4 = rdd1 union rdd2
//按key进行分组
rdd4.groupByKey
rdd4.collect
- 练习5:
val rdd1 = sc.parallelize(List(("tom", 1), ("tom", 2), ("jerry", 3), ("kitty", 2)))
val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 1), ("shuke", 2)))
//cogroup
val rdd3 = rdd1.cogroup(rdd2)
//注意cogroup与groupByKey的区别
rdd3.collect
- 练习6:
val rdd1 = sc.parallelize(List(1, 2, 3, 4, 5))
//reduce聚合
val rdd2 = rdd1.reduce(_ + _)
rdd2.collect
- 练习7:
val rdd1 = sc.parallelize(List(("tom", 1), ("jerry", 3), ("kitty", 2), ("shuke", 1)))
val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 3), ("shuke", 2), ("kitty", 5)))
val rdd3 = rdd1.union(rdd2)
//按key进行聚合
val rdd4 = rdd3.reduceByKey(_ + _)
rdd4.collect
//按value的降序排序
val rdd5 = rdd4.map(t => (t._2, t._1)).sortByKey(false).map(t => (t._2, t._1))
rdd5.collect
六、Spark RDD的高级算子
1、mapPartitionsWithIndex
把每个partition中的分区号和对应的值拿出来
u 接收一个函数参数:
l 第一个参数:分区号
l 第二个参数:分区中的元素
u 示例:将每个分区中的元素和分区号打印出来。
l val rdd1 = sc.parallelize(List(1,2,3,4,5,6,7,8,9), 2)
l 创建一个函数返回RDD中的每个分区号和元素:
def func1(index:Int, iter:Iterator[Int]):Iterator[String] ={
iter.toList.map( x => "[PartID:" + index + ", value=" + x + "]" ).iterator
}
l 调用:rdd1.mapPartitionsWithIndex(func1).collect
2、aggregate
先对局部聚合,再对全局聚合
示例:val rdd1 = sc.parallelize(List(1,2,3,4,5), 2)
u 查看每个分区中的元素:
u 将每个分区中的最大值求和,注意:初始值是0;
如果初始值时候10,则结果为:30
u 如果是求和,注意:初始值是0:
如果初始值是10,则结果是:45
u 一个字符串的例子:
val rdd2 = sc.parallelize(List("a","b","c","d","e","f"),2)
修改一下刚才的查看分区元素的函数
def func2(index: Int, iter: Iterator[(String)]) : Iterator[String] = {
iter.toList.map(x => "[partID:" + index + ", val: " + x + "]").iterator
}
两个分区中的元素:
[partID:0, val: a], [partID:0, val: b], [partID:0, val: c],
[partID:1, val: d], [partID:1, val: e], [partID:1, val: f]
运行结果:
u 更复杂一点的例子
val rdd3 = sc.parallelize(List("12","23","345","4567"),2)
rdd3.aggregate("")((x,y) => math.max(x.length, y.length).toString, (x,y) => x + y)
结果可能是:”24”,也可能是:”42”
val rdd4 = sc.parallelize(List("12","23","345",""),2)
rdd4.aggregate("")((x,y) => math.min(x.length, y.length).toString, (x,y) => x + y)
结果是:”10”,也可能是”01”,
原因:注意有个初始值””,其长度0,然后0.toString变成字符串
val rdd5 = sc.parallelize(List("12","23","","345"),2)
rdd5.aggregate("")((x,y) => math.min(x.length, y.length).toString, (x,y) => x + y)
结果是:”11”,原因同上。
3、aggregateByKey
n 准备数据:
val pairRDD = sc.parallelize(List( ("cat",2), ("cat", 5), ("mouse", 4),("cat", 12), ("dog", 12), ("mouse", 2)), 2)
def func3(index: Int, iter: Iterator[(String, Int)]) : Iterator[String] = {
iter.toList.map(x => "[partID:" + index + ", val: " + x + "]").iterator
}
n 两个分区中的元素:
n 示例:
l 将每个分区中的动物最多的个数求和
scala> pairRDD.aggregateByKey(0)(math.max(_, _), _ + _).collect
res69: Array[(String, Int)] = Array((dog,12), (cat,17), (mouse,6))
l 将每种动物个数求和
scala> pairRDD.aggregateByKey(0)(_+_, _ + _).collect
res71: Array[(String, Int)] = Array((dog,12), (cat,19), (mouse,6))
这个例子也可以使用:reduceByKey
scala> pairRDD.reduceByKey(_+_).collect
res73: Array[(String, Int)] = Array((dog,12), (cat,19), (mouse,6))
4、coalesce与repartition
n 都是将RDD中的分区进行重分区。
n 区别是:coalesce默认不会进行shuffle(false);而repartition会进行shuffle(true),即:会将数据真正通过网络进行重分区。
n 示例:
def func4(index: Int, iter: Iterator[(Int)]) : Iterator[String] = {
iter.toList.map(x => "[partID:" + index + ", val: " + x + "]").iterator
}
val rdd1 = sc.parallelize(List(1,2,3,4,5,6,7,8,9), 2)
下面两句话是等价的:
val rdd2 = rdd1.repartition(3)
val rdd3 = rdd1.coalesce(3,true) --->如果是false,查看RDD的length依然是2
5、其他高级算子
参考:http://homepage.cs.latrobe.edu.au/zhe/ZhenHeSparkRDDAPIExamples.html
七、Spark基础编程案例
1、案例一:求网站的访问量
l Tomcat的访问日志
l 求出访问量最高的两个网页
n 要求显示:网页名称、访问量
2、案例二:创建自定义分区
l 根据jsp文件的名字,将各自的访问日志放入到不同的分区文件中,如下:
n 生成的分区文件
n 例如:part-00000文件中的内容:只包含了web.jsp的访问日志
3、案例三:访问数据库
l 将RDD的数据保存到Oracle数据库中
调用:
l 使用JdbcRDD:执行SQL语句
JdbcRDD参数说明:
参数名称 |
类型 |
说明 |
sc |
org.apache.spark.SparkContext |
Spark Context对象 |
getConnection |
scala.Function0[java.sql.Connection] |
得到一个数据库Connection |
sql |
scala.Predef.String |
执行的SQL语句 |
lowerBound |
scala.Long |
下边界值,即:SQL的第一个参数 |
upperBound |
scala.Long |
上边界值,即:SQL的第二个参数 |
numPartitions |
scala.Int |
分区的个数,即:启动多少个Executor |
mapRow |
scala.Function1[java.sql.ResultSet, T] |
得到的结果集 |
JdbcRDD的缺点:从上面的参数说明可以看出,JdbcRDD有以下两个缺点:
1.执行的SQL必须有两个参数,并类型都是Long
2.得到的结果是ResultSet,即:只支持select操作
===========第三篇:Spark SQL===========
一、Spark SQL基础
1、Spark SQL简介
Spark SQL是Spark用来处理结构化数据的一个模块,它提供了一个编程抽象叫做DataFrame并且作为分布式SQL查询引擎的作用。
为什么要学习Spark SQL?我们已经学习了Hive,它是将Hive SQL转换成MapReduce然后提交到集群上执行,大大简化了编写MapReduce的程序的复杂性,由于MapReduce这种计算模型执行效率比较慢。所以Spark SQL的应运而生,它是将Spark SQL转换成RDD,然后提交到集群执行,执行效率非常快!同时Spark SQL也支持从Hive中读取数据。
Spark SQL的特点:
l 容易整合(集成)
l 统一的数据访问方式
l 兼容Hive
l 标准的数据连接
2、基本概念:Datasets和DataFrames
u DataFrame
DataFrame是组织成命名列的数据集。它在概念上等同于关系数据库中的表,但在底层具有更丰富的优化。DataFrames可以从各种来源构建,
例如:
l 结构化数据文件
l hive中的表
l 外部数据库或现有RDDs
DataFrame API支持的语言有Scala,Java,Python和R。
从上图可以看出,DataFrame多了数据的结构信息,即schema。RDD是分布式的 Java对象的集合。DataFrame是分布式的Row对象的集合。DataFrame除了提供了比RDD更丰富的算子以外,更重要的特点是提升执行效率、减少数据读取以及执行计划的优化
u Datasets
Dataset是数据的分布式集合。Dataset是在Spark 1.6中添加的一个新接口,是DataFrame之上更高一级的抽象。它提供了RDD的优点(强类型化,使用强大的lambda函数的能力)以及Spark SQL优化后的执行引擎的优点。一个Dataset 可以从JVM对象构造,然后使用函数转换(map, flatMap,filter等)去操作。 Dataset API 支持Scala和Java。 Python不支持Dataset API。
3、测试数据
使用员工表的数据,并已经将其保存到了HDFS上。
4、创建DataFrames
(*)通过Case Class创建DataFrames
① 定义case class(相当于表的结构:Schema)
注意:由于mgr和comm列中包含null值,简单起见,将对应的case class类型定义为String
② 将HDFS上的数据读入RDD,并将RDD与case Class关联
③ 将RDD转换成DataFrames
④ 通过DataFrames查询数据
(*)使用SparkSession
① 什么是SparkSession
Apache Spark 2.0引入了SparkSession,其为用户提供了一个统一的切入点来使用Spark的各项功能,并且允许用户通过它调用DataFrame和Dataset相关API来编写Spark程序。最重要的是,它减少了用户需要了解的一些概念,使得我们可以很容易地与Spark交互。
在2.0版本之前,与Spark交互之前必须先创建SparkConf和SparkContext。然而在Spark 2.0中,我们可以通过SparkSession来实现同样的功能,而不需要显式地创建SparkConf, SparkContext 以及 SQLContext,因为这些对象已经封装在SparkSession中。
② 创建StructType,来定义Schema结构信息
注意,需要:import org.apache.spark.sql.types._
③ 读入数据并且切分数据
④ 将RDD中的数据映射成Row
注意,需要:import org.apache.spark.sql.Row
⑤ 创建DataFrames
val df = spark.createDataFrame(rowRDD,myschema)
再举一个例子,使用JSon文件来创建DataFame
① 源文件:$SPARK_HOME/examples/src/main/resources/people.json
② val df = spark.read.json("源文件")
③ 查看数据和Schema信息
5、DataFrame操作
DataFrame操作也称为无类型的Dataset操作
(*)查询所有的员工姓名
(*)查询所有的员工姓名和薪水,并给薪水加100块钱
(*)查询工资大于2000的员工
(*)求每个部门的员工人数
完整的例子,请参考:
http://spark.apache.org/docs/2.1.0/api/scala/index.html#org.apache.spark.sql.Dataset
(*)在DataFrame中使用SQL语句
① 将DataFrame注册成表(视图):df.createOrReplaceTempView("emp")
② 执行查询:spark.sql("select * from emp").show
spark.sql("select * from emp where deptno=10").show
spark.sql("select deptno,sum(sal) from emp group by deptno").show
6、Global Temporary View
上面使用的是一个在Session生命周期中的临时views。在Spark SQL中,如果你想拥有一个临时的view,并想在不同的Session中共享,而且在application的运行周期内可用,那么就需要创建一个全局的临时view。并记得使用的时候加上global_temp作为前缀来引用它,因为全局的临时view是绑定到系统保留的数据库global_temp上。
① 创建一个普通的view和一个全局的view
df.createOrReplaceTempView("emp1")
df.createGlobalTempView("emp2")
② 在当前会话中执行查询,均可查询出结果。
spark.sql("select * from emp1").show
spark.sql("select * from global_temp.emp2").show
③ 开启一个新的会话,执行同样的查询
spark.newSession.sql("select * from emp1").show (运行出错)
spark.newSession.sql("select * from global_temp.emp2").show
7、创建Datasets
DataFrame的引入,可以让Spark更好的处理结构数据的计算,但其中一个主要的问题是:缺乏编译时类型安全。为了解决这个问题,Spark采用新的Dataset API (DataFrame API的类型扩展)。
Dataset是一个分布式的数据收集器。这是在Spark1.6之后新加的一个接口,兼顾了RDD的优点(强类型,可以使用功能强大的lambda)以及Spark SQL的执行器高效性的优点。所以可以把DataFrames看成是一种特殊的Datasets,即:Dataset(Row)
(*)创建DataSet,方式一:使用序列
1、定义case class
case class MyData(a:Int,b:String)
2、生成序列,并创建DataSet
val ds = Seq(MyData(1,"Tom"),MyData(2,"Mary")).toDS
3、查看结果
(*)创建DataSet,方式二:使用JSON数据
1、定义case class
case class Person(name: String, gender: String)
2、通过JSON数据生成DataFrame
val df = spark.read.json(sc.parallelize("""{"gender": "Male", "name": "Tom"}""" :: Nil))
3、将DataFrame转成DataSet
df.as[Person].show
df.as[Person].collect
(*)创建DataSet,方式三:使用HDFS数据
1、读取HDFS数据,并创建DataSet
val linesDS = spark.read.text("hdfs://hadoop111:9000/data/data.txt").as[String]
2、对DataSet进行操作:分词后,查询长度大于3的单词
val words = linesDS.flatMap(_.split(" ")).filter(_.length > 3)
words.show
words.collect
3、执行WordCount程序
val result = linesDS.flatMap(_.split(" ")).map((_,1)).groupByKey(x => x._1).count
result.show
排序:result.orderBy($"value").show
8、Datasets的操作案例
(*)使用emp.json 生成DataFrame
val empDF = spark.read.json("/root/resources/emp.json")
查询工资大于3000的员工
empDF.where($"sal" >= 3000).show
(*)创建case class
case class Emp(empno:Long,ename:String,job:String,hiredate:String,mgr:String,sal:Long,comm:String,deptno:Long)
(*)生成DataSets,并查询数据
val empDS = empDF.as[Emp]
查询工资大于3000的员工
empDS.filter(_.sal > 3000).show
查看10号部门的员工
empDS.filter(_.deptno == 10).show
(*)多表查询
1、创建部门表
val deptRDD=sc.textFile("/root/temp/dept.csv").map(_.split(","))
case class Dept(deptno:Int,dname:String,loc:String)
val deptDS = deptRDD.map(x=>Dept(x(0).toInt,x(1),x(2))).toDS
2、创建员工表
case class Emp(empno:Int,ename:String,job:String,mgr:String,hiredate:String,sal:Int,comm:String,deptno:Int)
val empRDD = sc.textFile("/root/temp/emp.csv").map(_.split(","))
val empDS = empRDD.map(x => Emp(x(0).toInt,x(1),x(2),x(3),x(4),x(5).toInt,x(6),x(7).toInt)).toDS
3、执行多表查询:等值链接
val result = deptDS.join(empDS,"deptno")
另一种写法:注意有三个等号
val result = deptDS.joinWith(empDS,deptDS("deptno")=== empDS("deptno"))
joinWith和join的区别是连接后的新Dataset的schema会不一样
(*)查看执行计划:result.explain
二、使用数据源
1、通用的Load/Save函数
(*)什么是parquet文件?
Parquet是列式存储格式的一种文件类型,列式存储有以下的核心:
l 可以跳过不符合条件的数据,只读取需要的数据,降低IO数据量。
l 压缩编码可以降低磁盘存储空间。由于同一列的数据类型是一样的,可以使用更高效的压缩编码(例如Run Length Encoding和Delta Encoding)进一步节约存储空间。
l 只读取需要的列,支持向量运算,能够获取更好的扫描性能。
l Parquet格式是Spark SQL的默认数据源,可通过spark.sql.sources.default配置
(*)通用的Load/Save函数
l 读取Parquet文件
val usersDF = spark.read.load("/root/resources/users.parquet")
l 查询Schema和数据
l 查询用户的name和喜爱颜色,并保存
usersDF.select($"name",$"favorite_color").write.save("/root/result/parquet")
l 验证结果
(*)显式指定文件格式:加载json格式
l 直接加载:val usersDF = spark.read.load("/root/resources/people.json") 会出错
l val usersDF = spark.read.format("json").load("/root/resources/people.json")
(*)存储模式(Save Modes)
可以采用SaveMode执行存储操作,SaveMode定义了对数据的处理模式。需要注意的是,这些保存模式不使用任何锁定,不是原子操作。此外,当使用Overwrite方式执行时,在输出新数据之前原数据就已经被删除。SaveMode详细介绍如下表:
Demo:
l usersDF.select($"name").write.save("/root/result/parquet1")
--> 出错:因为/root/result/parquet1已经存在
l usersDF.select($"name").write.mode("overwrite").save("/root/result/parquet1")
(*)将结果保存为表
l usersDF.select($"name").write.saveAsTable("table1")
也可以进行分区、分桶等操作:partitionBy、bucketBy
2、Parquet文件
Parquet是一个列格式而且用于多个数据处理系统中。Spark SQL提供支持对于Parquet文件的读写,也就是自动保存原始数据的schema。当写Parquet文件时,所有的列被自动转化为nullable,因为兼容性的缘故。
(*)案例:
读入json格式的数据,将其转换成parquet格式,并创建相应的表来使用SQL进行查询。
(*)Schema的合并:
Parquet支持Schema evolution(Schema演变,即:合并)。用户可以先定义一个简单的Schema,然后逐渐的向Schema中增加列描述。通过这种方式,用户可以获取多个有不同Schema但相互兼容的Parquet文件。
Demo:
Schema的合并
val df1 = sc.makeRDD(1 to 5).map(i => (i, i * 2)).toDF("single", "double")
df1.write.parquet("/root/myresult/test_table/key=1")
val df2 = sc.makeRDD(6 to 10).map(i => (i, i * 3)).toDF("single", "triple")
df2.write.parquet("/root/myresult/test_table/key=2")
val df3 = spark.read.option("mergeSchema", "true").parquet("/root/myresult/test_table/")
df3.printSchema()
3、JSON Datasets
Spark SQL能自动解析JSON数据集的Schema,读取JSON数据集为DataFrame格式。读取JSON数据集方法为SQLContext.read().json()。该方法将String格式的RDD或JSON文件转换为DataFrame。
需要注意的是,这里的JSON文件不是常规的JSON格式。JSON文件每一行必须包含一个独立的、自满足有效的JSON对象。如果用多行描述一个JSON对象,会导致读取出错。读取JSON数据集示例如下:
(*)Demo1:使用Spark自带的示例文件 --> people.json 文件
定义路径:
val path ="/root/resources/people.json"
读取Json文件,生成DataFrame:
val peopleDF = spark.read.json(path)
打印Schema结构信息:
peopleDF.printSchema()
创建临时视图:
peopleDF.createOrReplaceTempView("people")
执行查询
spark.sql("SELECT name FROM people WHERE age=19").show
4、使用JDBC
Spark SQL同样支持通过JDBC读取其他数据库的数据作为数据源。
Demo演示:使用Spark SQL读取Oracle数据库中的表。
l 启动Spark Shell的时候,指定Oracle数据库的驱动
spark-shell --master spark://spark81:7077 \
--jars /root/temp/ojdbc6.jar \
--driver-class-path /root/temp/ojdbc6.jar
l 读取Oracle数据库中的数据
(*)方式一:
val oracleDF = spark.read.format("jdbc").
option("url","jdbc:oracle:thin:@192.168.88.101:1521/orcl.example.com").
option("dbtable","scott.emp").
option("user","scott").
option("password","tiger").
load
(*)方式二:
导入需要的类:
import java.util.Properties
定义属性:
val oracleprops = new Properties()
oracleprops.setProperty("user","scott")
oracleprops.setProperty("password","tiger")
读取数据:
val oracleEmpDF =spark.read.jdbc("jdbc:oracle:thin:@192.168.88.101:1521/orcl.example.com",
"scott.emp",oracleprops)
注意:下面是读取Oracle 10g(Windows上)的步骤
5、使用Hive Table
l 首先,搭建好Hive的环境(需要Hadoop)
l 配置Spark SQL支持Hive
n 只需要将以下文件拷贝到$SPARK_HOME/conf的目录下,即可
u $HIVE_HOME/conf/hive-site.xml
u $HADOOP_CONF_DIR/core-site.xml
u $HADOOP_CONF_DIR/hdfs-site.xml
l 使用Spark Shell操作Hive
n 启动Spark Shell的时候,需要使用--jars指定mysql的驱动程序
n 创建表
spark.sql("create table src (key INT, value STRING) row format delimited fields terminated by ','")
n 导入数据
spark.sql("load data local inpath '/root/temp/data.txt' into table src")
n 查询数据
spark.sql("select * from src").show
l 使用spark-sql操作Hive
n 启动spark-sql的时候,需要使用--jars指定mysql的驱动程序
n 操作Hive
u spark.sql("show tables").show
u spark.sql("select * from emp1").show
三、性能优化
1、在内存中缓存数据
性能调优主要是将数据放入内存中操作。通过spark.cacheTable("tableName")或者dataFrame.cache()。使用spark.uncacheTable("tableName")来从内存中去除table。
u Demo案例:
(*)从Oracle数据库中读取数据,生成DataFrame
val oracleDF = spark.read.format("jdbc")
.option("url","jdbc:oracle:thin:@192.168.88.101:1521/orcl.example.com")
.option("dbtable","scott.emp")
.option("user","scott")
.option("password","tiger").load
(*)将DataFrame注册成表: oracleDF.registerTempTable("emp")
(*)执行查询,并通过Web Console监控执行的时间
spark.sql("select * from emp").show
(*)将表进行缓存,并查询两次,并通过Web Console监控执行的时间
spark.sqlContext.cacheTable("emp")
(*)清空缓存:
spark.sqlContext.cacheTable("emp")
spark.sqlContext.clearCache
2、性能优化相关参数
l 将数据缓存到内存中的相关优化参数
n spark.sql.inMemoryColumnarStorage.compressed
u 默认为 true
u Spark SQL 将会基于统计信息自动地为每一列选择一种压缩编码方式。
n spark.sql.inMemoryColumnarStorage.batchSize
u 默认值:10000
u 缓存批处理大小。缓存数据时, 较大的批处理大小可以提高内存利用率和压缩率,但同时也会带来 OOM(Out Of Memory)的风险。
l 其他性能相关的配置选项(不过不推荐手动修改,可能在后续版本自动的自适应修改)
n spark.sql.files.maxPartitionBytes
u 默认值:128 MB
u 读取文件时单个分区可容纳的最大字节数
n spark.sql.files.openCostInBytes
u 默认值:4M
u 打开文件的估算成本, 按照同一时间能够扫描的字节数来测量。当往一个分区写入多个文件的时候会使用。高估更好, 这样的话小文件分区将比大文件分区更快 (先被调度)。
l spark.sql.autoBroadcastJoinThreshold
n 默认值:10M
n 用于配置一个表在执行 join 操作时能够广播给所有 worker 节点的最大字节大小。通过将这个值设置为 -1 可以禁用广播。注意,当前数据统计仅支持已经运行了 ANALYZE TABLE <tableName> COMPUTE STATISTICS noscan 命令的 Hive Metastore 表。
l spark.sql.shuffle.partitions
n 默认值:200
n 用于配置 join 或聚合操作混洗(shuffle)数据时使用的分区数。
四、在IDEA中开发Spark SQL程序
1、指定Schema格式
2、使用case class
3、数据保存到数据库
=======第四篇:Spark Streaming=======
一、Spark Streaming基础
1、Spark Streaming简介
Spark Streaming是核心Spark API的扩展,可实现可扩展、高吞吐量、可容错的实时数据流处理。数据可以从诸如Kafka,Flume,Kinesis或TCP套接字等众多来源获取,并且可以使用由高级函数(如map,reduce,join和window)开发的复杂算法进行流数据处理。最后,处理后的数据可以被推送到文件系统,数据库和实时仪表板。而且,您还可以在数据流上应用Spark提供的机器学习和图处理算法。
2、Spark Streaming的特点
3、Spark Streaming的内部结构
在内部,它的工作原理如下。Spark Streaming接收实时输入数据流,并将数据切分成批,然后由Spark引擎对其进行处理,最后生成“批”形式的结果流。
Spark Streaming将连续的数据流抽象为discretizedstream或DStream。在内部,DStream 由一个RDD序列表示。
4、第一个小案例:NetworkWordCount
(1)由于在本案例中需要使用netcat网络工具,所以需要先安装。
rpm -iUv ~/netcat-0.6.1-1.i386.rpm
(2)启动netcat数据流服务器,并监听端口:1234
命令:nc -l -p 9999
服务器端:
(3)启动客户端
bin/run-example streaming.NetworkWordCount localhost 1234
客户端:
(一定注意):如果要执行本例,必须确保机器cpu核数大于2
5、开发自己的NetworkWordCount
(一定注意):
val sparkConf = new SparkConf().setAppName("NetworkWordCount").setMaster("local[2]")
官方的解释:
二、Spark Streaming进阶
1、StreamingContext对象详解
l 初始化StreamingContext
n 方式一:从SparkConf对象中创建
n 从一个现有的SparkContext实例中创建
l 程序中的几点说明:
n appName参数是应用程序在集群UI上显示的名称。
n master是Spark,Mesos或YARN集群的URL,或者一个特殊的“local [*]”字符串来让程序以本地模式运行。
n 当在集群上运行程序时,不需要在程序中硬编码master参数,而是使用spark-submit提交应用程序并将master的URL以脚本参数的形式传入。但是,对于本地测试和单元测试,您可以通过“local[*]”来运行Spark Streaming程序(请确保本地系统中的cpu核心数够用)。
n StreamingContext会内在的创建一个SparkContext的实例(所有Spark功能的起始点),你可以通过ssc.sparkContext访问到这个实例。
n 批处理的时间窗口长度必须根据应用程序的延迟要求和可用的集群资源进行设置。
l 请务必记住以下几点:
n 一旦一个StreamingContextt开始运作,就不能设置或添加新的流计算。
n 一旦一个上下文被停止,它将无法重新启动。
n 同一时刻,一个JVM中只能有一个StreamingContext处于活动状态。
n StreamingContext上的stop()方法也会停止SparkContext。 要仅停止StreamingContext(保持SparkContext活跃),请将stop() 方法的可选参数stopSparkContext设置为false。
n 只要前一个StreamingContext在下一个StreamingContext被创建之前停止(不停止SparkContext),SparkContext就可以被重用来创建多个StreamingContext。
2、离散流(DStreams):Discretized Streams
l DiscretizedStream或DStream 是Spark Streaming对流式数据的基本抽象。它表示连续的数据流,这些连续的数据流可以是从数据源接收的输入数据流,也可以是通过对输入数据流执行转换操作而生成的经处理的数据流。在内部,DStream由一系列连续的RDD表示,如下图:
l 举例分析:在之前的NetworkWordCount的例子中,我们将一行行文本组成的流转换为单词流,具体做法为:将flatMap操作应用于名为lines的 DStream中的每个RDD上,以生成words DStream的RDD。如下图所示:
但是DStream和RDD也有区别,下面画图说明:
3、DStream中的转换操作(transformation)
最后两个transformation算子需要重点介绍一下:
n transform(func)
u 通过RDD-to-RDD函数作用于源DStream中的各个RDD,可以是任意的RDD操作,从而返回一个新的RDD
u 举例:在NetworkWordCount中,也可以使用transform来生成元组对
n updateStateByKey(func)
u 操作允许不断用新信息更新它的同时保持任意状态。
l 定义状态-状态可以是任何的数据类型
l 定义状态更新函数-怎样利用更新前的状态和从输入流里面获取的新值更新状态
l 注意:需要设置检查点
u 重写NetworkWordCount程序,累计每个单词出现的频率(注意:累计)
u 输出结果:
n 注意:如果在IDEA中,不想输出log4j的日志信息,可以将log4j.properties文件(放在src的目录下)的第一行改为:
log4j.rootCategory=ERROR, console
4、窗口操作
Spark Streaming还提供了窗口计算功能,允许您在数据的滑动窗口上应用转换操作。下图说明了滑动窗口的工作方式:
如图所示,每当窗口滑过originalDStream时,落在窗口内的源RDD被组合并被执行操作以产生windowed DStream的RDD。在上面的例子中,操作应用于最近3个时间单位的数据,并以2个时间单位滑动。这表明任何窗口操作都需要指定两个参数。
l 窗口长度(windowlength) - 窗口的时间长度(上图的示例中为:3)。
l 滑动间隔(slidinginterval) - 两次相邻的窗口操作的间隔(即每次滑动的时间长度)(上图示例中为:2)。
这两个参数必须是源DStream的批间隔的倍数(上图示例中为:1)。
我们以一个例子来说明窗口操作。 假设您希望对之前的单词计数的示例进行扩展,每10秒钟对过去30秒的数据进行wordcount。为此,我们必须在最近30秒的pairs DStream数据中对(word, 1)键值对应用reduceByKey操作。这是通过使用reduceByKeyAndWindow操作完成的。
需要注意:滑动的距离必须是采样时间的整数倍。
一些常见的窗口操作如下表所示。所有这些操作都用到了上述两个参数 - windowLength和slideInterval。
u window(windowLength, slideInterval)
l 基于源DStream产生的窗口化的批数据计算一个新的DStream
u countByWindow(windowLength, slideInterval)
l 返回流中元素的一个滑动窗口数
u reduceByWindow(func, windowLength, slideInterval)
l 返回一个单元素流。利用函数func聚集滑动时间间隔的流的元素创建这个单元素流。函数必须是相关联的以使计算能够正确的并行计算。
u reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks])
l 应用到一个(K,V)对组成的DStream上,返回一个由(K,V)对组成的新的DStream。每一个key的值均由给定的reduce函数聚集起来。注意:在默认情况下,这个算子利用了Spark默认的并发任务数去分组。你可以用numTasks参数设置不同的任务数
u reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks])
l 上述reduceByKeyAndWindow() 的更高效的版本,其中使用前一窗口的reduce计算结果递增地计算每个窗口的reduce值。这是通过对进入滑动窗口的新数据进行reduce操作,以及“逆减(inverse reducing)”离开窗口的旧数据来完成的。一个例子是当窗口滑动时对键对应的值进行“一加一减”操作。但是,它仅适用于“可逆减函数(invertible reduce functions)”,即具有相应“反减”功能的减函数(作为参数invFunc)。 像reduceByKeyAndWindow一样,通过可选参数可以配置reduce任务的数量。 请注意,使用此操作必须启用检查点。
u countByValueAndWindow(windowLength, slideInterval, [numTasks])
l 应用到一个(K,V)对组成的DStream上,返回一个由(K,V)对组成的新的DStream。每个key的值都是它们在滑动窗口中出现的频率。
5、输入DStreams和接收器
输入DStreams表示从数据源获取输入数据流的DStreams。在NetworkWordCount例子中,lines表示输入DStream,它代表从netcat服务器获取的数据流。每一个输入流DStream和一个Receiver对象相关联,这个Receiver从源中获取数据,并将数据存入内存中用于处理。
输入DStreams表示从数据源获取的原始数据流。Spark Streaming拥有两类数据源:
l 基本源(Basic sources):这些源在StreamingContext API中直接可用。例如文件系统、套接字连接、Akka的actor等
l 高级源(Advanced sources):这些源包括Kafka,Flume,Kinesis,Twitter等等。
下面通过具体的案例,详细说明:
- 文件流:通过监控文件系统的变化,若有新文件添加,则将它读入并作为数据流
需要注意的是:
① 这些文件具有相同的格式
② 这些文件通过原子移动或重命名文件的方式在dataDirectory创建
③ 如果在文件中追加内容,这些追加的新数据也不会被读取。
注意:要演示成功,需要在原文件中编辑,然后拷贝一份。
- RDD队列流
使用streamingContext.queueStream(queueOfRDD)创建基于RDD队列的DStream,用于调试Spark Streaming应用程序。
- 套接字流:通过监听Socket端口来接收数据
6、DStreams的输出操作
输出操作允许DStream的操作推到如数据库、文件系统等外部系统中。因为输出操作实际上是允许外部系统消费转换后的数据,它们触发的实际操作是DStream转换。目前,定义了下面几种输出操作:
l foreachRDD的设计模式
DStream.foreachRDD是一个强大的原语,发送数据到外部系统中。
- 第一步:创建连接,将数据写入外部数据库(使用之前的NetworkWordCount,改写之前输出结果的部分,如下)
出现以下Exception:
原因是:Connection对象不是一个可被序列化的对象,不能RDD的每个Worker上运行;即:Connection不能在RDD分布式环境中的每个分区上运行,因为不同的分区可能运行在不同的Worker上。所以需要在每个RDD分区上单独创建Connection对象。
- 第二步:在每个RDD分区上单独创建Connection对象,如下:
7、DataFrame和SQL操作
我们可以很方便地使用DataFrames和SQL操作来处理流数据。您必须使用当前的StreamingContext对应的SparkContext创建一个SparkSession。此外,必须这样做的另一个原因是使得应用可以在driver程序故障时得以重新启动,这是通过创建一个可以延迟实例化的单例SparkSession来实现的。
在下面的示例中,我们使用DataFrames和SQL来修改之前的wordcount示例并对单词进行计数。我们将每个RDD转换为DataFrame,并注册为临时表,然后在这张表上执行SQL查询。
8、缓存/持久化
与RDD类似,DStreams还允许开发人员将流数据保留在内存中。也就是说,在DStream上调用persist() 方法会自动将该DStream的每个RDD保留在内存中。如果DStream中的数据将被多次计算(例如,相同数据上执行多个操作),这个操作就会很有用。对于基于窗口的操作,如reduceByWindow和reduceByKeyAndWindow以及基于状态的操作,如updateStateByKey,数据会默认进行持久化。 因此,基于窗口的操作生成的DStream会自动保存在内存中,而不需要开发人员调用persist()。
对于通过网络接收数据(例如Kafka,Flume,sockets等)的输入流,默认持久化级别被设置为将数据复制到两个节点进行容错。
请注意,与RDD不同,DStreams的默认持久化级别将数据序列化保存在内存中。
9、检查点支持
流数据处理程序通常都是全天候运行,因此必须对应用中逻辑无关的故障(例如,系统故障,JVM崩溃等)具有弹性。为了实现这一特性,Spark Streaming需要checkpoint足够的信息到容错存储系统,以便可以从故障中恢复。
① 一般会对两种类型的数据使用检查点:
1) 元数据检查点(Metadatacheckpointing) - 将定义流计算的信息保存到容错存储中(如HDFS)。这用于从运行streaming程序的driver程序的节点的故障中恢复。元数据包括以下几种:
l 配置(Configuration) - 用于创建streaming应用程序的配置信息。
l DStream操作(DStream operations) - 定义streaming应用程序的DStream操作集合。
l 不完整的batch(Incomplete batches) - jobs还在队列中但尚未完成的batch。
2) 数据检查点(Datacheckpointing) - 将生成的RDD保存到可靠的存储层。对于一些需要将多个批次之间的数据进行组合的stateful变换操作,设置数据检查点是必需的。在这些转换操作中,当前生成的RDD依赖于先前批次的RDD,这导致依赖链的长度随时间而不断增加,由此也会导致基于血统机制的恢复时间无限增加。为了避免这种情况,stateful转换的中间RDD将定期设置检查点并保存到到可靠的存储层(例如HDFS)以切断依赖关系链。
总而言之,元数据检查点主要用于从driver程序故障中恢复,而数据或RDD检查点在任何使用stateful转换时是必须要有的。
② 何时启用检查点:
对于具有以下任一要求的应用程序,必须启用检查点:
1) 使用状态转:如果在应用程序中使用updateStateByKey或reduceByKeyAndWindow(具有逆函数),则必须提供检查点目录以允许定期保存RDD检查点。
2) 从运行应用程序的driver程序的故障中恢复:元数据检查点用于使用进度信息进行恢复。
③ 如何配置检查点:
可以通过在一些可容错、高可靠的文件系统(例如,HDFS,S3等)中设置保存检查点信息的目录来启用检查点。这是通过使用streamingContext.checkpoint(checkpointDirectory)完成的。设置检查点后,您就可以使用上述的有状态转换操作。此外,如果要使应用程序从驱动程序故障中恢复,您应该重写streaming应用程序以使程序具有以下行为:
1) 当程序第一次启动时,它将创建一个新的StreamingContext,设置好所有流数据源,然后调用start()方法。
2) 当程序在失败后重新启动时,它将从checkpoint目录中的检查点数据重新创建一个StreamingContext。
使用StreamingContext.getOrCreate可以简化此行为
④ 改写之前的WordCount程序,使得每次计算的结果和状态都保存到检查点目录下
通过查看HDFS中的信息,可以看到相关的检查点信息,如下:
三、高级数据源
1、Spark Streaming接收Flume数据
需要注意:如果使用Scala IDE(eclipse)开发程序集成Flume的话,直接加入Flume的jar包,会存在以下两个问题
(1)问题1:
存在两个scala-library(scala-library-2.11.8.jar和scala-library-2.10.5.jar),删除第二个。
(2)问题2:
spark-streaming-flume_2.10-2.1.0.jar of SparkProject build path is cross-compiled with an incompatible version of Scala (2.10.0). In case this report is mistaken, this check can be disabled in the compiler preference page.SparkProject Unknown Scala Version Problem
l 基于Flume的Push模式
Flume被用于在Flume agents之间推送数据.在这种方式下,Spark Streaming可以很方便的建立一个receiver,起到一个Avro agent的作用.Flume可以将数据推送到改receiver.
- 第一步:Flume的配置文件
- 第二步:Spark Streaming程序
- 第三步:注意除了需要使用Flume的lib的jar包以外,还需要以下jar包:
- 第四步:测试
n 启动Spark Streaming程序
n 启动Flume
n 拷贝日志文件到/root/training/logs目录
n 观察输出,采集到数据
l 基于Custom Sink的Pull模式
不同于Flume直接将数据推送到Spark Streaming中,第二种模式通过以下条件运行一个正常的Flume sink。Flume将数据推送到sink中,并且数据保持buffered状态。Spark Streaming使用一个可靠的Flume接收器和转换器从sink拉取数据。只要当数据被接收并且被Spark Streaming备份后,转换器才运行成功。
这样,与第一种模式相比,保证了很好的健壮性和容错能力。然而,这种模式需要为Flume配置一个正常的sink。
以下为配置步骤:
- 第一步:Flume的配置文件
- 第二步:Spark Streaming程序
- 第三步:需要的jar包
n 将Spark的jar包拷贝到Flume的lib目录下
n 下面的这个jar包也需要拷贝到Flume的lib目录下,同时加入IDEA工程的classpath
- 第四步:测试
n 启动Flume
n 在IDEA中启动FlumeLogPull
n 将测试数据拷贝到/root/training/logs
n 观察IDEA中的输出
2、Spark Streaming接收Kafka数据
Apache Kafka是一种高吞吐量的分布式发布订阅消息系统。
搭建ZooKeeper(Standalone):
(*)配置/root/training/zookeeper-3.4.10/conf/zoo.cfg文件
dataDir=/root/training/zookeeper-3.4.10/tmp
server.1=spark81:2888:3888
(*)在/root/training/zookeeper-3.4.10/tmp目录下创建一个myid的空文件
echo 1 > /root/training/zookeeper-3.4.6/tmp/myid
搭建Kafka环境(单机单broker):
(*)修改server.properties文件
(*)启动Kafka
bin/kafka-server-start.sh config/server.properties &
出现以下错误:
需要修改bin/kafka-run-class.sh文件,将这个选项注释掉。
(*)测试Kafka
l 创建Topic
bin/kafka-topics.sh --create --zookeeper spark81:2181 -replication-factor 1 --partitions 3 --topic mydemo1
l 发送消息
bin/kafka-console-producer.sh --broker-list spark81:9092 --topic mydemo1
l 接收消息
bin/kafka-console-consumer.sh --zookeeper spark81:2181 --topic mydemo1
l 搭建Spark Streaming和Kafka的集成开发环境
由于Spark Streaming和Kafka集成的时候,依赖的jar包比较多,而且还会产生冲突。强烈建议使用Maven的方式来搭建项目工程。
下面是依赖的pom.xml文件:
l 基于Receiver的方式
这个方法使用了Receivers来接收数据。Receivers的实现使用到Kafka高层次的消费者API。对于所有的Receivers,接收到的数据将会保存在Spark executors中,然后由Spark Streaming启动的Job来处理这些数据。
- 开发Spark Streaming的Kafka Receivers
- 测试
n 启动Kafka消息的生产者
bin/kafka-console-producer.sh --broker-list spark81:9092 --topic mydemo1
n 在IDEA中启动任务,接收Kafka消息
l 直接读取方式
和基于Receiver接收数据不一样,这种方式定期地从Kafka的topic+partition中查询最新的偏移量,再根据定义的偏移量范围在每个batch里面处理数据。当作业需要处理的数据来临时,spark通过调用Kafka的简单消费者API读取一定范围的数据。
- 开发Spark Streaming的程序
- 测试
n 启动Kafka消息的生产者
bin/kafka-console-producer.sh --broker-list spark81:9092 --topic mydemo1
n 在IDEA中启动任务,接收Kafka消息
四、性能优化
1、减少批数据的执行时间
在Spark中有几个优化可以减少批处理的时间:
① 数据接收的并行水平
通过网络(如kafka,flume,socket等)接收数据需要这些数据反序列化并被保存到Spark中。如果数据接收成为系统的瓶颈,就要考虑并行地接收数据。注意,每个输入DStream创建一个receiver(运行在worker机器上)接收单个数据流。创建多个输入DStream并配置它们可以从源中接收不同分区的数据流,从而实现多数据流接收。例如,接收两个topic数据的单个输入DStream可以被切分为两个kafka输入流,每个接收一个topic。这将在两个worker上运行两个receiver,因此允许数据并行接收,提高整体的吞吐量。多个DStream可以被合并生成单个DStream,这样运用在单个输入DStream的transformation操作可以运用在合并的DStream上。
② 数据处理的并行水平
如果运行在计算stage上的并发任务数不足够大,就不会充分利用集群的资源。默认的并发任务数通过配置属性来确定spark.default.parallelism。
③ 数据序列化
可以通过改变序列化格式来减少数据序列化的开销。在流式传输的情况下,有两种类型的数据会被序列化:
l 输入数据
l 由流操作生成的持久RDD
在上述两种情况下,使用Kryo序列化格式可以减少CPU和内存开销。
2、设置正确的批容量
为了Spark Streaming应用程序能够在集群中稳定运行,系统应该能够以足够的速度处理接收的数据(即处理速度应该大于或等于接收数据的速度)。这可以通过流的网络UI观察得到。批处理时间应该小于批间隔时间。
根据流计算的性质,批间隔时间可能显著的影响数据处理速率,这个速率可以通过应用程序维持。可以考虑WordCountNetwork这个例子,对于一个特定的数据处理速率,系统可能可以每2秒打印一次单词计数(批间隔时间为2秒),但无法每500毫秒打印一次单词计数。所以,为了在生产环境中维持期望的数据处理速率,就应该设置合适的批间隔时间(即批数据的容量)。
找出正确的批容量的一个好的办法是用一个保守的批间隔时间(5-10,秒)和低数据速率来测试你的应用程序。
3、内存调优
在这一节,我们重点介绍几个强烈推荐的自定义选项,它们可以减少Spark Streaming应用程序垃圾回收的相关暂停,获得更稳定的批处理时间。
l Default persistence level of DStreams:和RDDs不同的是,默认的持久化级别是序列化数据到内存中(DStream是StorageLevel.MEMORY_ONLY_SER,RDD是StorageLevel.MEMORY_ONLY)。即使保存数据为序列化形态会增加序列化/反序列化的开销,但是可以明显的减少垃圾回收的暂停。
l Clearing persistent RDDs:默认情况下,通过Spark内置策略(LUR),Spark Streaming生成的持久化RDD将会从内存中清理掉。如果spark.cleaner.ttl已经设置了,比这个时间存在更老的持久化RDD将会被定时的清理掉。正如前面提到的那样,这个值需要根据Spark Streaming应用程序的操作小心设置。然而,可以设置配置选项spark.streaming.unpersist为true来更智能的去持久化(unpersist)RDD。这个配置使系统找出那些不需要经常保有的RDD,然后去持久化它们。这可以减少Spark RDD的内存使用,也可能改善垃圾回收的行为。
l Concurrent garbage collector:使用并发的标记-清除垃圾回收可以进一步减少垃圾回收的暂停时间。尽管并发的垃圾回收会减少系统的整体吞吐量,但是仍然推荐使用它以获得更稳定的批处理时间。