Spark简介

  1. Spark安装+RDD
    1.1. Spark安装+RDD
    1.1.1. Spark安装+RDD

思考点:
序号 知识点 类型 难度系数 掌握程度

  1.  为什么需要RDD?为何每次操作后都创建新的RDD,而不是修改现有的RDD呢?	概念	1	熟练
    

知识点:
序号 知识点 类型 难度系数 掌握程度
2. 数量安装配置hadoop,启动伪分布式,启动HDFS服务 操作 1 熟练
1.2. 搭建HDFS环境
1.3. Spark介绍和安装
1.3.1. 为什么会有Spark
因为传统的并行计算模型无法有效的解决迭代计算(iterative)和交互式计算(interactive);而Spark的使命便是解决这两个问题,这也是他存在的价值和理由。
Spark如何解决迭代计算?
其主要实现思想就是RDD,把所有计算的数据保存在分布式的内存中。迭代计算通常情况下都是对同一个数据集做反复的迭代计算,数据在内存中将大大提升IO操作。这也是Spark涉及的核心:内存计算。同时往往计算的量太大内存放不下,分布式内存就极大的扩展了内存的大小,同时可以进行动态扩充。
Spark如何实现交互式计算?
因为Spark是用scala语言实现的,Spark和scala能够紧密的集成,所以Spark可以完美的运用scala的解释器,使得其中的scala可以向操作本地集合对象一样轻松操作分布式数据集。
Spark和RDD的关系?
可以理解为:RDD是一种具有容错性基于内存的集群计算抽象方法,Spark则是这个抽象方法的实现。
1.3.2. 介绍
Spark是内存计算系统,2009年它出品于UCBerkeley AMPLab伯克利下属实验室,它最早是一篇博士论文,论述如何提高map和reduce的效率。后来就针对这篇论文写出了相应的实现。2013年6月捐赠了给Apache,2014年2月成为Apache的顶级项目。最大节点的集群来自腾讯,8000个节点,单个job最大分别是阿里巴巴和Databricks,处理到1PB的数据。宣传其MapReduce方式比Hadoop方式快,快10到100倍。这就相当可观了。
1B (Byte 字节)=8b (bit 位)
1KB (Kilobyte 千字节)=1024B,
1MB (Megabyte 兆字节 简称“兆”)=1024KB,
1GB (Gigabyte 吉字节 又称“千兆”)=1024MB,
1TB (Trillionbyte 万亿字节 太字节)=1024GB,
1PB(Petabyte 千万亿字节 拍字节)=1024TB,
1EB(Exabyte 百亿亿字节 艾字节)=1024PB,
1ZB (Zettabyte 十万亿亿字节 泽字节)= 1024 EB,
1YB (Yottabyte 一亿亿亿字节 尧字节)= 1024 ZB,
1PB等于1024x1024 GB
如果数据量都能放到内存它就快100倍,如果数据量太大,中间需要磁盘交互,写中间内容到文件,那就快10倍。
官网地址:http://spark.apache.org/

特点:
1、 这是它最大的优点,处理速度快。
2、 支持的开发语句多。Spark源码是用scala写的,但开发时不一定要使用scala。目前支持Scala、Java、Python、R语言。
3、 可以运行于多种集群环境下,有自己的集群模式StandAlone。也支持Hadoop中的Yarn集群。多少内存,多少CPU,由yarn去管理调度。还支持Apache的mesos集群。
4、 支持多种数据源。HDFS数据量过大还是需要分布式存储。传统的关系型数据库。Hbase非关系型数据库。
5、 涉及也非常广
SparkCore Hadoop适合离线分析,而Spark接近实时分析。
SparkStreaming 实时的数据在线分析,如storm
SparkSQL Hive(hive封装了MR,提供了类SQL,spark中也有类似功能spark SQL,它模仿hive,但比hive快。因为hive是基于hadoop中传统的MR API,而sparkSQL是封装了spark中的方式,自然也就比hive性能高)
还支持机器学习,图计算GraphX等
它是一个大而全的一站式框架。比使用多个技术学习成本要低些。

1.3.3. *Spark的结构

Spark架构采用了分布式计算中的Master-Slave模型。Master是对应集群中的含有Master进程的节点,Slave是集群中含有Worker进程的节点。Master作为整个集群的控制器,负责整个集群的正常运行;Worker相当于是计算节点,接收主节点命令与进行状态汇报;Executor负责任务的执行;Client作为用户的客户端负责提交应用,Driver负责控制一个应用的执行。
整体流程:Spark的Driver负责接收读取用户的数据,完成task的解析和生成,并向Cluster Manager(集群资源管理器)申请运行task需要的资源。集群资源管理器为task分配满足要求的节点,并在节点创建Executor。创建的Executor向Driver注册。Driver将spark应用程序的代码和数据传送给分配的Executor。Executor运行task,运行完之后将结果返回给Driver或者写入HDFS或者其他介质。
1.3.4. 运行组件

Spark的核心组件包括RDD、Scheduler、Storage、Shuffle四部分。RDD是Spark最核心最精髓的部分,Spark将所有数据都抽象为RDD。Scheduler是Spark的调度机制,分为DAGScheduler和TaskScheduler。Storage管理缓存后的RDD、Shuffle中间结果数据和broadcast数据。Shuffle分为hash方式和sort方式,两种方式的shuffle中间数据都写本地磁盘。
1.4. 搭建Spark环境-单机模式
1.5. *RDD弹性分布式数据集
1.5.1. RDD介绍
Spark的核心概念是RDD (resilient distributed dataset),指的是一个只读的,可分区的分布式数据集,这个数据集的全部或部分可以缓存在内存中,在多次计算间重用。它是spark提供的一个特殊集合类。普通的集合数据作为一个整体,但RDD中的数据进行了分区Partition处理。这样做的目的就是为了分布式。如:传统List(1,2,3,4)是一个整体,RDD可能就是RDD(1,2) (3,4)。这样如需计算时,就把1和2发送给一个worker,把3和4发送给另一个worker。按分区完成数据的分发。

RDD的结构

分为三部分组成:数据分片、计算函数、RDD依赖(lineage血缘关系)
lineage让RDD有了生命,可以进行向前的追溯,这点对懒执行和容错特别有意义。

把普通集合变成RDD集合

scala> sc.parallelize(List(1,2,3)) #将普通数组转换为RDD集合
注意:第一种方式没有设定变量名,spark会自动起变量名resn,n自动是个序号,从0一直往下。第二种就是自己指定变量名。也可以使用makeRDD命令,它和parallelize等价。
分区

scala> sc.parallelize(List(1,2,3),n) #n代表分区的个数

查看分区长度

scala> rdd1.partitions.length #如果是单机返回1

调用工具类查看分区情况

scala> val rdd1 = sc.parallelize(List(1,2,3,4),2) #强制分2个区
scala> su.debug(rdd1) #查看rdd1分区情况
partition:[0]
1
partition:[1]
2
partition:[2]
3
4

追溯历史DAG=====

可以启用sbin/start-history-server.sh来记录日志,它会同时提供一个HistoryServer的进程来以Web方式展现日志:访问 http://主机:4040
在Web界面可以展现RDD之间的血缘关系,这张图称为DAG(有向无环图)

附录:SparkDebugUtils

1.5.2. RDD中的方法
map数据转换

进行映射转换
scala> val rdd2 = sc.makeRDD( List(1,2,3,4), 2 )
scala> rdd2.map{ x=> x+1 } #实现每个元素加1,执行完会创建新的rdd
scala> res8.collect #实现收集,执行完会创建新的rdd
执行流程是比较复杂的,每个分区都会创建一个Task。Task(1,2),(x=>x+1),Task(3,4) ,(x=>x+1)将每个Task将数据和函数交给worker来运行。worker1执行完结果(2,3)在worker1上存着;worker2执行完结果(4,5)在workd2上存着。collect
就把两个都拿过来形成一个新的集合(2,3)(4,5)如果集群环境下,它就是一个远程调用。

collect收集结果

scala> res8.collect #实现收集,执行完会创建新的rdd
结果返回给Driver。如果集群中数据量非常大,海量。在各个服务上还勉强可以承受,但如果收集到Driver所在的服务器,就会撑爆内存。那怎么办呢?实际项目中都将其结果存入到分布式的文件系统中,如HDFS中,这样即使数据海量,也无需担心。
filter过滤数据

过滤掉小于等于2的数据
scala> val rdd = sc.parallelize(List(1,2,3,4,5))
scala> rdd.filter{ x => x>2 }
scala> res1.collect #实现收集,执行完会创建新的rdd
res2: Array[Int] = Array(3, 4, 5)
仅计算包含Spark的行
scala> val rdd2 = rdd1.filter(x=>x.contains(“Spark”))
仅计算[ERROR]开头的行,用于日志分析特别适合
scala> val rdd2 = rdd1.filter(x=>x.startsWith(“[ERROR]”))
flatMap扁平化的map

在加工过程中原来的一维数组会变成二维数组,使用flatMap把二维集合转换为一维的集合。
scala> val rdd = sc.makeRDD(List(“hello world”,“hello count”,“world spark”),2)
rdd.flatMap{ x => x.split{" "}}

reduceByKey化简

先把相同的key分为一组,再对每个key的value进行累加。
要求集合必须是键值对(key,value)
var rdd3 = sc.makeRDD( List( (“hello”,1),(“hello”,1),(“hello”,1),(“world”,1) ) )
rdd3.reduceByKey{ (x,y) => x+y)}
res0.collect
Spark WordCount

从文件中读取数据,实现单词计数
textFile(“/root/words.txt”,n) //文件名,n几个分区
reduceByKey方式

步骤:
上传words.txt文件到root目录
val rdd5 = sc.textFile(“/root/words.txt”,2)
.flatMap{ x=> x.split(" ") }
.map{ x=> (x,1)}
.reduceByKey{(x,y)=>x+y}
rdd5.collect
结果:
res6: Array[(String, Int)] = Array((spark,3), (hive,1), (hadoop,2), (big,2), (scla,1), (data,1))
countByValue方式

val file = “/root/words.txt”
val lines = sc.textFile(file,2)
val words = lines.flatMap(_.split(“\s+”))
val wordCount = words.countByValue()
for(x<-wordCount){println(x)}
结果:
wordCount: scala.collection.Map[String,Long] = Map(scla -> 1, big -> 2, data -> 1, hadoop -> 2, spark -> 3, hive -> 1)
Spark会预估文件大小,然后进行拆分,数据量大,它就比较平均。split切分行时使用正则表达式,countByValue方法统计RDD中每个元素出现的次数,得到一个元素及其出现次数的Map。实际就是将结果存放到一个map集合中。
技巧:scala控制台提示

敲入命令后加上点sc.按下tab键就会提示这个下面有哪些方法

1.5.3. *小结
为什么会产生RDD?
(1)传统的MapReduce虽然具有自动容错、平衡负载和可拓展性的优点,但是其最大缺点是采用非循环式的数据流模型,使得在迭代计算式要进行大量的磁盘IO操作。RDD正是解决这一缺点的抽象方法。
(2)RDD的具体描述RDD(弹性数据集)是Spark提供的最重要的抽象的概念,它是一种有容错机制的特殊集合,可以分布在集群的节点上,以函数式操作集合的方式,进行各种并行操作。可以将RDD理解为一个具有容错机制的特殊集合,它提供了一种只读、只能有已存在的RDD变换而来的共享内存,然后将所有数据都加载到内存中,方便进行多次重用。a.他是分布式的,可以分布在多台机器上,进行计算。b.他是弹性的,计算过程中内存不够时它会和磁盘进行数据交换。c.这些限制可以极大的降低自动容错开销d.实质是一种更为通用的迭代并行计算框架,用户可以显示的控制计算的中间结果,然后将其自由运用于之后的计算。
(3)RDD的容错机制实现分布式数据集容错方法有两种:数据检查点和记录更新RDD采用记录更新的方式:记录所有更新点的成本很高。所以,RDD只支持粗颗粒变换,即只记录单个块上执行的单个操作,然后创建某个RDD的变换序列(血统)存储下来;变换序列指,每个RDD都包含了他是如何由其他RDD变换过来的以及如何重建某一块数据的信息。因此RDD的容错机制又称“血统”容错。 要实现这种“血统”容错机制,最大的难题就是如何表达父RDD和子RDD之间的依赖关系。
实际上依赖关系可以分两种,窄依赖和宽依赖:窄依赖Narrow:窄依赖是指每个父RDD的Partition最多被子RDD的一个Partition所使用,例如map、filter;宽依赖Wide:宽依赖是指一个父RDD的Partition会被多个子RDD的Partition所使用,例如groupByKey、reduceByKey。
例如:map变换,子RDD中的数据块只依赖于父RDD中对应的一个数据块;groupByKey变换,子RDD中的数据块会依赖于多个父RDD中的数据块,因为一个key可能导致数据在父RDD的任何一个数据块中。将依赖关系分类的两个特性:第一,窄依赖可以在某个计算节点上直接通过计算父RDD的某块数据计算得到子RDD对应的某块数据;宽依赖则要等到父RDD所有数据都计算完成之后,并且父RDD的计算结果进行hash并传到对应节点上之后才能计算子RDD。第二,数据丢失时,对于窄依赖只需要重新计算丢失的那一块数据来恢复;对于宽依赖则要将祖先RDD中的所有数据块全部重新计算来恢复。所以在长“血统”链特别是有宽依赖的时候,需要在适当的时机设置数据检查点。也是这两个特性要求对于不同依赖关系要采取不同的任务调度机制和容错恢复机制。

(4)RDD内部的设计每个RDD都需要包含以下四个部分:a.源数据分割后的数据块,源代码中的splits变量b.关于“血统”的信息,源码中的dependencies变量c.一个计算函数(该RDD如何通过父RDD计算得到),源码中的iterator(split)和compute函数d.一些关于如何分块和数据存放位置的元信息,如源码中的partitioner和preferredLocations例如:a.一个从分布式文件系统中的文件得到的RDD具有的数据块通过切分各个文件得到的,它是没有父RDD的,它的计算函数知识读取文件的每一行并作为一个元素返回给RDD;b.对与一个通过map函数得到的RDD,它会具有和父RDD相同的数据块,它的计算函数式对每个父RDD中的元素所执行的一个函数。

  1. Spark集群+流水线+RDD
    2.1. 独立集群方式
    2.1.1. 独立集群方式
    集群搭建分了三种方式:
    1) 独立集群方式StandAlone
    2) 和Yarn进行整合,由Yarn来完成资源的管理和调度,和hadoop无缝集成
    3) 和Apache Mesos进行整合,由Mesos进行管理和调度,轻松3万个节点

2.1.2. 修改每台的配置文件
vi conf/spark-env.sh
设置:SPARK_LOCAL_IP=192.168.1.106 #注意每台配置的是本机IP地址
还可以配置CPU和内存,根据实际情况进行配置,不配置时,默认读取当前服务器的实际情况。SPARK_WORKER_CORES、SPARK_WORKER_MEMORY。
2.1.3. 修改每台的hosts文件
vi /etc/hosts

192.168.1.106 spark1
192.168.1.108 spark2
192.168.1.105 spark3
192.168.1.107 spark4
192.168.1.103 hadoophdfs
配置集群中其它主机的IP地址和机器名,每台的服务器的hosts都要进行修改
2.1.4. 启动Master
任意选择一台服务器作为Master,其它就都作为worker
ssh 192.168.1.106
cd /usr/local/src/spark/spark-1.5.2-bin-hadoop2.6
sbin/start-master.sh –h 192.168.1.106 #启动master
sbin/stop-master.sh #停止master
注意:可以是IP地址也可以是主机名
启动后显示日志文件路径。要学会看错误日志。这样当出现错误,可以知道问题出在哪里了。

注意:1)akka访问地址work连接的地址2)WebUI访问地址,可以通过浏览器查看集群状态(单机是没有这个管理界面的)
2.1.5. 每台服务器防火墙打开端口
akka访问端口:7077和WebUI控制台Web服务端口8080
service iptables stop #简单测试时,直接关掉防火墙
web管理界面http://192.168.1.106:8080/

2.1.6. 启动Worker
在各个work上启动各自的worker
sbin/start-slave.sh spark://192.168.1.106:7077 #指向master的访问地址
#sbin/start-slave.sh spark://192.168.1.106:7077 –h spark1 指定IP地址或者主机名访问,连接HDFS按主机名会刚好些,IP可能出错。
sbin/stop-slave.sh #停止服务
有时第一次连接时不成功,重新连接次就好了。
同样也会有日志输出,刚开始你不熟悉时,最好看一下日志,看下启动时有没有错误,如果你配置有错,或者连接地址有错,或者防火墙没开,它都有可能连接不到master。所以必须先看下日志,养成一个好习惯。
tail –f 日志文件路径

可以看到它也会启动一个Web管理界面。看到“successful regiestered”搞定。

刷新Master,可以看到有worker就管理起来了。

 State状态ALIVE就表示活着,将来集群很多worker,master只会把任务分配给ALIVE活着的。如果DEAN,就不会被分配。
 Cores使用CPU的个数
 Memory是使用内存的大小
2.1.7. 修改配置自动刷新
sbin/stop-slave.sh #关闭服务
增加CPU的核数4,增加内存到2G
sbin/start-slave.sh #启动服务
刷新控制台,可以看到CPU和内存都加大了。

可以看到旧的已死,新的已启动。启动较慢,稍等几秒。
2.1.8. 客户端如何连接到集群中进行计算
bin/spark-shell --master=spark://192.168.1.106:7077 #master的IP地址

可以看到管理界面上出现一个RunningApplication,正在执行的应用。SparkShell就是一个特殊的应用程序,它可在集群环境中运行。
2.1.9. 运行任务
val rdd = sc.parallelize(List(1,2,3,4,5,6),2)
rdd.collect

可以看到进行了两个分区,上面刚好有两个worker是存活的。
2.1.10. 查看执行结果,必须点“Spark shell”进入

执行的collect操作

Job一次完整的计算称为一个job
Submitted提交任务的时间
Duration耗时:这个2s是很慢的,因为我们在虚拟机中。
Stages执行的阶段,现在的任务比较简单,执行很快,所以就一个阶段,复杂任务会有多个。
Tasks任务:成功执行的任务/总共的任务数。
2.1.11. 常见错误
failed to bind
如果配置了SPARK_LOCAL_IP, 但是并没有在slaves上修改为自己的IP,则会报错:
ERROR netty.NettyTransport: failed to bind to /192.168.6.52:0
java.net.BindException: Failed to bind to: /192.168.1.105:0: Service ‘sparkWorker’ failed
2.2. *流水线pipeline
2.2.1. 流水线pipeline
Spark为何比Hadoop快?
2.2.2. 懒惰式命令
一开始学习时容易产生困扰的地方

执行rdd.map,或者println都没有发现有动作,刷新job也看不到,实际上就没有立即执行或者说它的行为是懒惰式的。因为它并没有构成一个完整的任务。直到spark执行另一类方法时,才会触发计算。例如:rdd.count或者rdd.collect。这些方法才会触发计算,才把它看做是一次完整的计算。可以看出map不是一次完整的计算。
2.2.3. Spark中把方法分为了两类
一类transformations不会触发job的提交(没有立即计算),例如:map、filter、flatMap、reduceByKey等。
一类actions会触发job的提交,例如:collect、count等。
Spark在执行真正任务之前,都会做一些优化:会计算rdd之间的依赖关系,然后根据依赖关系划分阶段,完成优化。

2.2.4. *依赖关系
前面的步骤都没有触发job,它们这几步都在创建依赖关系,每一步都在生成新的RDD。那如何看这个关系呢?
找到最后一个RDD,它的toDebugString
scala> res12.toDebugString
res16: String =
(2) MapPartitionsRDD[6] at map at :22 []
| MapPartitionsRDD[4] at map at :20 []
| MapPartitionsRDD[1] at textFile at :16 []
| /root/letter.txt HadoopRDD[0] at textFile at :16 []
最后生成的RDD叫做HadoopRDD对象,它既可以读Hadoop文件也可以读普通文件。
这是spark为我们优化的第一步,它就是transformations类型,每一步都会生成新的RDD对象,每一个RDD不是独立的,它必须依赖上一个RDD。或者称为父RDD对象。它们之间都有父子关系。spark就会应用一种叫流水线的技术,它特别的节省内存,并且也让性能得到提升。
scala> res1.partitions.length
res19: Int = 2

scala> res7.partitions.length
res20: Int = 2

scala> res12.partitions.length
res21: Int = 2
每个RDD有两个分区。分区个数由父RDD决定。

上面的过程只是绘制了一个美好的蓝图,并未执行,那什么时候会真正执行运算呢?那就是执行一个actions类型命令时才会运算。
res12.collect
触发一次完整的计算,根据分区个数生成任务,根据2个分区生成了两个任务(Task)。spark就把它封装成任务对象,底层通过akka框架发给每个worker。Task对象中就包括了所有的RDD信息。Task最初在Driver上(SparkShell)。真正执行不是在SparkShell,而是在worker上执行。
怎么发?就是akka框架,先把它序列化为二进制的,再把它发给worker,worker收到二进制的,反序列化,就是Task。RDD都在Task里面,它们之间的关系就知道了。
具体计算是怎么计算的呢?它采用的是倒推的方式进行最后的计算。它先看最后一个RDD,res12去执行时,遇到变量x,它就去读res7,res7遇到变量x,就去读取res1,res1遇到textFile就按事先分区好的去读取前5行。(文件总共8行,前5行分给第一个分区,后3行分给第二个分区。)每次读一行,因为res7的x,每次只要一个。
2.2.5. 总结优缺点
 不会一次读很大量的数据,不会造成内存溢出
 中间结果流动方式无需暂存,内存消耗小
 命令批量提交,性能高

Spark首先采用了迭代思想,会不会用很大的内存?不会,数据是一条一条处理的。它边读边进行运算。它就不需要中间暂存结果。传统的Hadoop为什么慢,它每算一步,中间的结果都要暂存在HDFS上。为什么说spark效率好呢?就上面的例子,中间的结果用不用暂存?中间的处理是以流动的方式,一个RDD传到下一个RDD,就不需要暂存了。因此它能够充分的利用内存。这是它效果高的因素之一。这种方式就称为流水线(pipeline)。
从这也看出来,spark为何要把一些命令设置为transformation方式,先不执行,最后批量一次执行。你要立即执行,就得保存中间结果,供下一次执行使用。
缺点:每个分区的数据在一个worker上运行,万一有一个worker出现了故障,会导致出错的worker还需要重新计算,因为没有中间结果,一旦worker运算失败,spark需要找一个正常的worker来重新计算,而且从头算,这样就拉长了总的计算时间。
当然这个缺点可以配合一些缓存,例如一些中间结果非常重要,可以再加上缓存。以后出现故障不用重新算,从这些“保存点”上重新计算。有些类似有些的中间存盘。后面会详细讲到,加缓存也是spark很重要的一块,也是比hadoop性能高的原因之一。它的缓存是在内存,比缓存到HDFS上快的多。
2.3. 和HDFS结合
2.3.1. 和HDFS结合
上面的例子有个问题,就是需要处理的问题,必须保证每个worker中都有一个,那这种方式及其不方便,而且处理之前难以保证每个服务器都有。如何解决?可以使用HDFS分布式文件系统。
2.3.2. 准备:伪分布Hadoop单机

2.3.3. 虚拟机重启后执行步骤
1、 检查配置每个的主机名hosts
2、 启动HDFS服务
服务访问地址:http://192.168.1.103:50070/
3、 启动SparkMaster
4、 启动SparkSlave
5、 启动SparkShell
2.4. RDD的常用方法
RDD有两类
1) RDD是处理非键值对的
http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.rdd.RDD

2) PairRDDFunctions处理键值对的
http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.rdd.PairRDDFunctions

2.4.1. ++并集
等价于union将两个rdd做并集
例子:
val rdd1 = sc.parallelize(List(1,2,3,4,5))
rdd1.partitions.length #分区是3,两个worker的core总数
val rdd2 = sc.parallelize(List(5,6,7,8,9,10))
rdd2.partitions.length
val rdd3 = rdd1 ++ rdd2 # rdd1.union(rdd2)
rdd3.collect
结果:
res4:Array[Int] = Array(1,2,3,4,5,6,7,8,9,10)
它使每个分区不变,就是将分区合起来使用。

可以看出,它就没有动每个分区,就是把分区合起来。大家思考下如果动了什么结果?分区如果你的数据发生变了,就可能会发生网络传输了。
2.4.2. collect收集
等价于union将两个rdd做并集

2.4.3. take获取元素
take(n) 从分区中获取元素
scala> val rdd = sc.makeRDD(List(1,2,3,4,5))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[6] at makeRDD at :21

scala> rdd.take(2)
res12: Array[Int] = Array(1, 2)
2.4.4. takeOrdered(n)升序
从分区中获取头n个元素进行排序。先排序后获取。
scala> val list = sc.parallelize(List(4,5,6,3,2,11))
list: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[9] at parallelize at :21

scala> list.take(3)
res16: Array[Int] = Array(4, 5, 6)

scala> list.takeOrdered(3)
res17: Array[Int] = Array(2, 3, 4)
takeOrdered的内部实现是先分3个区,每个区里拿出三个最小的,没有那么多就都拿,再把这9个值比较下,在这9个里再取最小的。
2.4.5. top(n)降序
它的过程和上面相反,先分三个分区,三个分区中找出最大的,然后再比较,最后得到最大的。
scala> val list = sc.parallelize(List(4,5,6,3,2,11))
list: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[9] at parallelize at :21
scala> list.top(3)
res18: Array[Int] = Array(11, 6, 5)
2.4.6. *cache缓存
将RDD的内容缓存,这样就无需继续往父RDD找了,直接去缓存中获取值。
scala> var rdd1 = sc.textFile(“README.md”)
rdd1: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[1] at textFile at :21

scala> val rdd2 = rdd1.filter(x=>{println(Thread.currentThread);x.contains(“a”)})
rdd2: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[5] at filter at :23

scala> rdd2.cache

scala> rdd2.count
res0: Long = 18

scala> rdd2.count
res0: Long = 18
例如:观察运行结果,发现第一次执行count时,控制台输出了相关线程信息,第二次执行count时,不会再次出现线程信息,其实已经从缓存中进行读取。
怎么查看cathe呢?通过管理界面。
它存储可能分为两部分,一部分存在内存中,如果内存不够,会强制保存到磁盘。

可以看到执行完cathe,类型就是MapPartitionsRDD。
scala> val rdd2 = rdd1.filter(x=>{println(Thread.currentThread);x.contains(“a”)})
rdd2: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[5] at filter at :23
缓存也是分区的Cached Partitions。
cache很重要,因为它也是spark比hadoop快的一个原因之一。它把经常需要使用的内容放入缓存中,这样无需再次计算,就快了。
注意:给那些需要大量重复的RDD使用缓存。
2.4.7. *persist缓存
比cache多些选项,支持压缩,支持外部磁盘等。
如果缓存的内容非常大怎么办?可以使用很多的策略,一个就是压缩。比如有1万个对象,那内存中放不下,怎么办?就对这1万个对象进行压缩。压缩完就500M了,那它就可以放到内存中,这样也有助于性能的提升。这个压缩不要小看它,spark要充分的利用内存,如果内容很多,内存放不下,那一部分就需要放到磁盘上,那完了,效率立刻下来了。所以有时候即使压缩下,也比你放在硬盘上强。但压缩也必然需要解压缩,但它占用的是CPU的资源。牺牲了CPU,节省了内存。
如果还是大大呢,压缩也不行了,可以多构造几个partition分区,它运行完一个分区,才运行另一个分区。
注意先要导入包StorageLevel,才能调用这几个常量
scala> import org.apache.spark.storage.StorageLevel
import org.apache.spark.storage.StorageLevel

scala> val rdd = sc.textFile(“hdfs://hadoophdfs:9000/test/letter.txt”)
rdd: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[1] at textFile at :22

scala> rdd.persist(StorageLevel.MEMORY_ONLY_SER)
res2: rdd.type = MapPartitionsRDD[1] at textFile at :22

scala> rdd.collect
res3: Array[String] = Array(1,a, 2,b, 3,c, 4,d, 5,e, 3,f, 2,g, 1,h)

可以看到明显小多了,456B变成56B,尤其对文本压缩率相当高。

2.4.8. 扩展:spark中对内存如何划分?
多少作为缓存使用,多少作为计算使用?这个在spark中也有讲究。spark中有个configuration对象,它提供了很多配置spark.storage.memoryFraction内存因子,缓存占内存多大。默认0.6。
例如:worker 1g内存,60%是作为内存的,也就是600m作为缓存,剩下。这样可以事先计算好,让它别超过缓存。
配置文件spark-defaults.conf.template文件中配置。

可以自己加上,注意之间用空格区分,spark要重新启动才生效。
还需要复制配置文件为spark-defaults.conf,否则也不生效。
cp spark-defaults.conf.template spark-defaults.conf
2.4.9. cartesian笛卡尔积
男male女female发衣服,每人都有一件马甲jacket和裤子trousers
val rdd1 = sc.parallelize( List( (“male”,1), (“female”,1) ) )
val rdd2 = sc.parallelize( List( (“jacket”,1),(“trousers”,1) ) )
rdd1.cartesian(rdd2)
res12.collect

res14: Array[((String, Int), (String, Int))] = Array(
((male,1), (jacket,1)),
((male,1), (trousers,1)),
((female,1), (jacket,1)),
((female,1), (trousers,1))
)
2.4.10. coalesce(n,true/false)扩大或缩小分区
如果内存不够,可以重新分区,如果原来3个,可以重新分成6个。也可以减少分区,例如:运算过程中发现用不了那么多分区,也可以减少。可以从少变多,也可以从多变少。
scala> val rdd = sc.parallelize( List(1,2,3,4,5,6), 2)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[18] at parallelize at :22

scala> rdd.partitions.length
res25: Int = 2

scala> rdd.coalesce(5)
res27: org.apache.spark.rdd.RDD[Int] = CoalescedRDD[19] at coalesce at :25

scala> res27.partitions.length
res28: Int = 2

scala> rdd.coalesce(4,true)
res30: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[23] at coalesce at :25

scala> res30.partitions.length
res31: Int = 4
由少到多要多加一个参数shuffle为true,默认是false。由多到少,不需要。
val rdd = sc.parallelize( List(1,2,3,4,5,6), 4)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[24] at parallelize at :22

scala> rdd.partitions.length
res32: Int = 4

scala> rdd.coalesce(2)
res34: org.apache.spark.rdd.RDD[Int] = CoalescedRDD[26] at coalesce at :25

scala> res34.partitions.length
res35: Int = 2
2.4.11. repartition(n)分区扩大==================
它等价于于coalesce(n,true)由下面源码就可以看到,实际它内部就调用的coalesce。

2.4.12. count计算RDD中对象个数
scala> val rdd = sc.parallelize( List(1,2,3,4,5,6), 3 )
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[27] at parallelize at :22

scala> rdd.count
res36: Long = 6
2.4.13. countApprox计算一个近似值
当计算量非常大,不需要精确结果时,求一个近似值的情况下使用,设置超时时间毫秒数,超时时间越长,计算的越精确。

2.4.14. intersection交集
求两个RDD共有的元素

2.4.15. mapPartitionsWithIndex分别遍历分区做不同的处理

分区1加a,分区2加b。
2.4.16. saveAsTextFile保存分区内容到磁盘

注意aaa是目录不是文件,它会自动起文件名part-nnnn
保存本地文件有时候不成功

一般也都保存到HDFS上,有两个分区,就有两个文件。

2.4.17. sortBy排序
升序

降序

2.4.18. zip拉链操作

两个集合元素个数得相同,然后合并成tuple类型

  1. PairRDD+Shuffle
    3.1. PairRDD常见方法-键值对
    3.1.1. aggregateByKey根据key聚合
    aggregateByKey(zeroValue)(func1,func2)
    zeroValue表示初始值,初始值会参与func1的计算
    在分区内,按key分组,把每组的值进行fun1的计算
    再将每个分区每组的计算结果按fun2进行计算

注意max函数先到导入包import scala.math._
每个分区里先分组,每个分组求最大值,最后每个分区里最大值求和。
设置初始值为5,第一个分组内都没5大,所以最终选择了5,第二个分组内最大7,超过了5,所以选择7,5+7=12最后的12就是这么来的。

3.1.2. *cogourp结对
这个很重要,很多函数内部就使用它,如join。一般我们不直接使用它。
把两个RDD中key相同的结对

CompactBuffer就是一个特殊的数组,类似ArrayBuffer。

修改下rdd1

任务执行失败,测试机器不行,分区太多反而慢,但在实际中应该多加分区。
说明spark有容错机制,虽然出错依然执行成功,说明错误节点的内容被转到好的节点上重新计算了。spark4上执行失败,它就把任务交给现在这个worker继续执行。

它的重载方法很多,支持两个RDD,也支持3个RDD。
3.1.3. groupByKey根据key进行分组
、 groupByKey(1)缩减分区数量

groupByKey可以实现reduceByKey也可以实现,哪个好呢?

groupByKey要网络传输4次,而reduceByKey在map过程中先本地就累加了,然后才传输,这样网络传输就少多了。
所以以后做分组操作,最好别先用groupByKey,因为它会把所有数据都发给reducer。这样reducer的压力就比较大。最好能在map先运算运算,然后再发。这样对网络的压力,对reducer的压力越小。所以它们都可以时,优先选择reduceByKey。除非你想保留分组结果,那只能使用groupByKey。
3.1.4. *join把多个RDD连接运算
类似数据库中的inner join内连接操作,要求两个RDD必须是键值对的形式。键相同的放在一起。

会将不共有的数据去除,例如rdd2的tiger。

左外连接,不管RDD2中有没有wolf,都要留下来,就使用leftOutJoin

3.1.5. 扩展:Some和None

Scala中定义了Option对象,Some和None都是Option的子类
val s = Some(0)
s.get #提供获取的方法

None就相当于null
对返回结果又包装了一层

结果

使用None和Some包装后,我们写代码就简洁了,不用自己写if判断,直接使用getOrElse就可以,从这可以看到spark的代码非常方便。当然刚开始使用时比较难理解些。
3.1.6. *partitionBy
partitioner分区器
分区中的内容不在自动的选择,可以人为指定分区中的数据按什么样的规则进行分区。
Spark提供了两个默认实现HashPartitioner和RangePartitioner
需要导入包import org.apache.spark_

HashPartitioner

先按hashcode%分区数,然后看放入到哪个分区

RangePartitioner

两个参数第一个参数是分区数,第二个是调用的rdd传入

按字母顺序进行分区,先按字母顺序排序,
那如果取所有的数据进行排序,那效率不是太低了。它实际内部采用了水塘取样的算法,它是抽取样本。例如有1万条数据,那就随机抽一些数据,然后抽取的数据中给它排序。

3.2. Shuffle介绍
3.2.1. 概念
shuffle洗牌,效果就是把牌的顺序打乱。对应mapreduce什么时候需要shuffle。其实shuffle的应用场景很多,只是平时我们忽略了它。

数据没有打乱不叫shuffle,如下图

了解shuffle的意义在于,如果mr过程中发生了shuffle就无法实现流水线,它的中间结果就被驻留在内存中。一旦发生了shuffle,数据就可能被存储到磁盘上。这样效率肯定比流水线的思想效率会低了。因为落地了,存到本地磁盘了。
a到a不是那么简单,中间可能会产生临时文件,临时文件就需要写磁盘。

shuffle在实际中无法完全避免。
3.2.2. 怎么判断是发生了shuffle?
新RDD的分区的元素来自旧RDD的某个分区,但又不全来旧的RDD的这个分区。只要有一个部分依赖就发生了shuffle。如果完全依赖才未发生shuffle。
完全依赖:

完全依赖的好处是,那它们的task任务就可以放在一台服务器上执行。绿色部分一个task,红色部分一个task。它们之间就无需网络传输。
spark中已经总结了哪些方法是部分依赖,哪些方法是完全依赖,不用我们自己去总结,我们只要知道这个概念就可以。

每个RDD在产生时其实已经决定了它哪种依赖,通过继承实现。

说明数据的部分依赖。

底层经过两个过程:Shuffle写和shuffle读。
3.2.3. Shuffle写过程
把shuffle的中间结果要写本地的临时磁盘文件中。为什么要做这步呢?例如:从上面例子看,cat1要和cat2join。join完的内容存入到新的RDD2中,然后计算的结果抛弃掉行不行。当然不行,cat3也要和cat2结合。所以这步无法避免必须产生一个外部文件,换句话说就是中间结果落地。这种就无法像之前所说的,一直在内存中,因为它可能很大,所以不管hadoop的mr也好,还是spark的mr也好,遇到这种shuffle都得把这个结果落地。
那么这个临时文件产生几个呢?这个跟spark的版本有关,在最早的spark版本中,它会产生临时文件的个数等于map的个数*reduce的个数。上面绿色部分就是map,蓝色部分就是reduce。那就产生两个临时文件。这样就产生一个缺点,如果map和reduce比较多。那中间产生的临时文件个数也就非常多。临时文件越多,占用的缓冲区也就越多,这样当mr数量越来越多,这样占用的内存量就急剧上升,另外操作系统打开多个文件进行读写,它的性能也会受影响。必然要占用磁盘IO,并且操作系统打开文件的个数也是有上限的。

新版的spark1.5做了改进。也就是减少临时文件的个数,那怎么减少呢?刚开始采用的方案是有多少个reduce,就产生多少个临时文件。这样就减少了同时打开的文件个数,并且内存的压力也小了。它怎么做到的呢?
优化策略,将两个内容合2为1了,怎么区分哪部分数据给哪个reduce读呢?它会产生一个*.index的文件。它不干别的事情,就记录文件每个map演化出来的内容,起点到结束多少个字节。它就记录一个位置。将来读的时候就先找index。各个reduce就先读取这个索引。这样就实现了。它就通过加了一个索引文件,记录了哪个部分给哪个reduce用的。这样的好处是就减少了文件的个数。

这两种实现方式,可以用户自己配置。默认sort新版的,配置hash就是旧版的。

新版的缺点:记录索引信息前,所有的文件先要排序,不排序,怎么知道从0到10是给RDD3的呢?从11到15是给RDD4用的,不排序是不就无法知道。先的排序,排序完才知道哪些给哪些RDD用。
相对新版还是比较好,节约内存,打开文件数不会过多。但也要看应用的场景,如果产生的临时文件个数不是想象中那么多,可以用hash这种方式。hashcode相同的就放在 一起,它不需排序的过程。
在hadoop中显然用的第二种。它是先要排序,然后生成临时文件。刚开始spark也采用不排序,这样它比hadoop排序就少了一步,当然就快了。但发现内存的压力就过大,所以spark最终也加上排序。
总结:第一种方案占用内存多,打开文件多,但不需排序,速度快。第二种占用内存少,打开文件少,速度相对慢。实践证明使用第二种方案的应用场景更多些。
3.2.4. Shuffle读过程
shuffle读由reduce这边发起,它需要先知道到临时文件中读,一般这个临时文件和reduce不在一台服务器上,它需要跨网络去读。但也不排除在一台服务器。不论如何它需要知道临时文件的位置,这个是谁来告诉它的呢?它有一个BlockManager的类。这里就知道将来是从本地文件中读取,还是需要从远程服务器上读取。

读进来后再做join或者combine的运算。
这些临时文件的位置就在就记录在map中。
为什么要排序
比如说这有个索引文件,如果不排序,怎么知道从哪到哪是给第一个reduce用的?从哪到哪是给第二个reduce用的。不排序,它不知道啊。把第一个reduce的放到一块,形成一个连续的结果,这样将来reduce读的时候才方便。
可以这样理解partition是RDD存储数据的地方,实际是个逻辑单位,真正要取数据时,它就调用BlockManage去读,它是以数据块的方式来读。比如一次读取32k还是64k。它不是一条一条读,一条一条读肯定性能低。它读时首先是看本地还是远程,如果是本地就直接读这个文件了,如果是远程,它就是发起一次socket请求,创建一个socket链接。然后发起一次远程调用,告诉远程的读取程序,读取哪些数据。读到的内容再通过socket传过来。
3.2.5. 划分stage阶段
Spark会根据你是否执行了Shuffle来划分stage。
显示map操作,不需要shuffle用实线表示,join过程会发生shuffle,用红色箭头表示。先map,在join,join内部进行cogroup,最后flatMapValues。这就可以根据shuffle来划分stage。只要出现了shuffle就把shuffle之后的阶段单独的划分为一个stage。shuffle之前可以划分为一个或者多个stage。下面的例子join前依赖了两个RDD,所以又划分成两个阶段。
这有什么用呢?划分stage有什么用呢?划分后的每个stage可以应用流水线思想。在spark中通过DAGScheduler来实现的。它实际根据每个RDD有没有依赖关系,中间有没有shuffle过程来。它还有个web管理界面,可以把不同的stage行成一张图。由图可以清楚的知道,执行一个job时中间划分了多少个stage。这个图叫做DAG图。(有向无环图-有方向性)

看看它生成的图,例如:

其实这个图就可以看出有没有shuffle。只要分成不同阶段,就说明有shuffle发生。如就一个图,没有分多个stage,就说明中间没有shuffle。这当然就是最好的。
例如:map执行下,它就没有shuffle。

如果没有stage划分,spark也就无法知道哪些能使用流水线思想。它提前把依赖关系进行了stage划分,所以spark才知道,到哪一步都可以使用流水线思想来处理数据,这时中间结果就不落地。一旦碰到不同的stage,那就需要生成中间的临时文件。
可以看到有了stage,spark就有了一个是否可以使用流水线的判断依据,使用了流水线自然减少了中间结果的落地,充分利用了内存,提高了性能。
3.2.6. 小结
Shuffle这块spark并不比hadoop强!并没有明显的性能优势。早期的没有排序过程,还没hadoop做的好。当然在某些应用场景上比较好,没有排序,如wordcount。但相对排序的场景更多些。

  1. SparkSQL
    4.1. 介绍
    4.1.1. 历史

sparkSQL的前身是shark。在hadoop发展过程中,为了给熟悉RDBMS但又不理解MapReduce的技术人员提供快速上手的工具,hive应运而生,是当时唯一运行在hadoop上的SQL-on-Hadoop工具。但是,MapReduce计算过程中大量的中间磁盘落地过程消耗了大量的I/O,降低的运行效率,为了提高SQL-on-Hadoop的效率,大量的SQL-on-Hadoop工具开始产生,其中表现较为突出的是:
 MapR的Drill
 Cloudera的Impala
 Shark
其中Shark是伯克利实验室spark生态环境的组件之一,它修改了下图所示的右下角的内存管理、物理计划、执行三个模块,并使之能运行在spark引擎上,从而使得SQL查询的速度得到10-100倍的提升。

但是,随着Spark的发展,对于野心勃勃的Spark团队来说,Shark对于hive的太多依赖(如采用hive的语法解析器、查询优化器等等),制约了Spark的One Stack rule them all的既定方针,制约了spark各个组件的相互集成,所以提出了sparkSQL项目。SparkSQL抛弃原有Shark的代码,汲取了Shark的一些优点,如内存列存储(In-Memory Columnar Storage)、Hive兼容性等,重新开发了SparkSQL代码;由于摆脱了对hive的依赖性,SparkSQL无论在数据兼容、性能优化、组件扩展方面都得到了极大的方便,真可谓“退一步, 海阔天空”。
2014年6月1日,Shark项目和SparkSQL项目的主持人Reynold Xin宣布:停止对Shark的开发,团队将所有资源放sparkSQL项目上,至此,Shark的发展画上了句话,但也因此发展出两个直线:SparkSQL和hive on spark。

4.1.2. *为什么sparkSQL的性能会得到怎么大的提升呢?
主要sparkSQL在下面几点做了优化:

内存列存储(In-Memory Columnar Storage)

  sparkSQL的表数据在内存中存储不是采用原生态的JVM对象存储方式,而是采用内存列存储,如下图所示。

该存储方式无论在空间占用量和读取吞吐率上都占有很大优势。
对于原生态的JVM对象存储方式,每个对象通常要增加12-16字节的额外开销,对于一个270MB的TPC-H lineitem table数据,使用这种方式读入内存,要使用970MB左右的内存空间(通常是2~5倍于原生数据空间);另外,使用这种方式,每个数据记录产生一个JVM对象,如果是大小为200B的数据记录,32G的堆栈将产生1.6亿个对象,这么多的对象,对于GC来说,可能要消耗几分钟的时间来处理(JVM的垃圾收集时间与堆栈中的对象数量呈线性相关)。显然这种内存存储方式对于基于内存计算的spark来说,很昂贵也负担不起。
对于内存列存储来说,将所有原生数据类型的列采用原生数组来存储,将Hive支持的复杂数据类型(如array、map等)先序化后并接成一个字节数组来存储。这样,每个列创建一个JVM对象,从而导致可以快速的GC和紧凑的数据存储;额外的,还可以使用低廉CPU开销的高效压缩方法(如字典编码、行长度编码等压缩方法)降低内存开销;更有趣的是,对于分析查询中频繁使用的聚合特定列,性能会得到很大的提高,原因就是这些列的数据放在一起,更容易读入内存进行计算。

字节码生成技术(bytecode generation,即CG)

在数据库查询中有一个昂贵的操作是查询语句中的表达式,主要是由于JVM的内存模型引起的。比如如下一个查询:
SELECT a + b FROM table
在这个查询里,如果采用通用的SQL语法途径去处理,会先生成一个表达式树(有两个节点的Add树,参考后面章节),在物理处理这个表达式树的时候,将会如图所示的7个步骤:
1、 调用虚函数Add.eval(),需要确认Add两边的数据类型
2、 调用虚函数a.eval(),需要确认a的数据类型
3、 确定a的数据类型是Int,装箱
4、 调用虚函数b.eval(),需要确认b的数据类型
5、 确定b的数据类型是Int,装箱
6、 调用Int类型的Add
返回装箱后的计算结果,其中多次涉及到虚函数的调用,虚函数的调用会打断CPU的正常流水线处理,减缓执行。
Spark1.1.0在catalyst模块的expressions增加了codegen模块,如果使用动态字节码生成技术(配置spark.sql.codegen参数),sparkSQL在执行物理计划的时候,对匹配的表达式采用特定的代码,动态编译,然后运行。如上例子,匹配到Add方法:

然后,通过调用,最终调用:

对于Spark1.1.0,对SQL表达式都作了CG优化,具体可以参看codegen模块。CG优化的实现主要还是依靠scala2.10的运行时放射机制(runtime reflection)。对于SQL查询的CG优化,可以简单地用下图来表示

scala代码优化

另外,sparkSQL在使用Scala编写代码的时候,尽量避免低效的、容易GC的代码;尽管
增加了编写代码的难度,但对于用户来说,还是使用统一的接口,没受到使用上的困难。

4.2. 提交应用
4.2.1. 提交应用
利用EclipseIDE工具将scala程序打包部署到spark的集群环境。
4.2.2. 创建scala工程
4.2.3. 创建例子
package com.scala

import org.apache.spark.SparkConf
import org.apache.spark.SparkContext

object App {
def main(args: Array[String]): Unit = {
val conf = new SparkConf() //创建SparkConf对象
conf.setAppName(“first”) //创建应用程序first
conf.setMaster(“local[2]”) //创建单个spark
conf.set(“spark.shuffle.manager”,“hash”)
val sc = new SparkContext(conf)

sc.textFile("/root/letter.txt",2)
.flatMap{ x=>x.split(" ") }
.map{ x=>(x,1) }
.reduceByKey{ (x,y)=>x+y }
.saveAsTextFile("/root/result")

sc.stop()

}
}

4.2.4. 打jar包
4.2.5. 上传jar包到spark
cd /usr/local/src/spark/spark-1.5.2-bin-hadoop2.6
上传first.jar包
4.2.6. spark上执行jar包
bin/spark-submit.sh 用来提交一个应用程序
–master local|spark集群的地址,优先级比代码中的低
–jars 所引用的第三方的jar包(详细路径)jar包会被分发到所有worker节点上
–driver-class-path所引用的第三方的jar包(详细路径)仅在dirver节点上
–conf配置key=配置的值,优先级比代码中的低
如:–conf spark.shuffle.manager=hash
bin/spark-submit --class com.scala.App first.jar

正确执行,将会在/root产生result目录,目录下有多个文件
[root@spark1 ~]# cd /root/result
[root@spark1 result]# ll
total 8
-rw-r–r–. 1 root root 40 May 28 03:54 part-00000
-rw-r–r–. 1 root root 24 May 28 03:54 part-00001
-rw-r–r–. 1 root root 0 May 28 03:54 _SUCCESS
如果目录存在则报错
FileAlreadyExistsException: Output directory file:/root/result already exists
先删除,再执行。rm -rf result
使用spark-submit 默认不会记录日志,如想启用日志:
cp conf/spark-defaults.conf.template conf/spark-defaults.conf
vi conf/spark-defaults.conf

4.3. SparkSQL环境
4.3.1. SparkShell支持
将RDD再次封装成一个DataFrame对象,而这个对象更类似于关系型数据库中的表。借鉴hive的思想,创造了SparkSQL。
bin/spark-shell --master=local
bin/spark-shell --master=yarn-client
启动spark-shell,链接yarn集群。成功后会创建sqlContext对象。

4.3.2. 创建DataFrame对象
DataFrame就相当于数据库的一张表。它是个只读的表,不能运算过程再往里加元素。
RDD.toDF(“列名”)
scala> val rdd = sc.parallelize(List(1,2,3,4,5,6))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at :21

scala> rdd.toDF(“id”)
res0: org.apache.spark.sql.DataFrame = [id: int]

scala> res0.show #默认只显示20条数据
±–+
| id|
±–+
| 1|
| 2|
| 3|
| 4|
| 5|
| 6|
±–+
scala> res0.printSchema #查看列的类型等属性
root
|-- id: integer (nullable = true)
4.3.3. 创建多列DataFrame对象
DataFrame就相当于数据库的一张表。
scala> sc.parallelize(List( (1,“beijing”),(2,“shanghai”) ) )
res3: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[5] at parallelize at :22

scala> res3.toDF(“id”,“name”)
res4: org.apache.spark.sql.DataFrame = [id: int, name: string]

scala> res4.show
±–±-------+
| id| name|
±–±-------+
| 1| beijing|
| 2|shanghai|
±–±-------+

例如3列的
scala> sc.parallelize(List( (1,“beijing”,100780),(2,“shanghai”,560090),(3,“xi’an”,600329)))
res6: org.apache.spark.rdd.RDD[(Int, String, Int)] = ParallelCollectionRDD[10] at parallelize at :22

scala> res6.toDF(“id”,“name”,“postcode”)
res7: org.apache.spark.sql.DataFrame = [id: int, name: string, postcode: int]

scala> res7.show
±–±-------±-------+
| id| name|postcode|
±–±-------±-------+
| 1| beijing| 100780|
| 2|shanghai| 560090|
| 3| xi’an| 600329|
±–±-------±-------+
可以看出,需要构建几列,tuple就有几个内容。
4.3.4. 由外部文件构造DataFrame对象

parquet比json压缩率更高,hadoop生态圈中使用较多

4.4. SparkSQL的方法方式
4.4.1. 查询select
利用dataFrame 执行查询,底层仍然会被转换为mapreduce运算
select(“列名”,列名…) 返回的是一个新的DataFrame(子集)
scala> res3.select(“id”,“name”).show
select($“列名”, …)
是将普通字符串转换为 C o l u m n s c a l a > r e s 3. s e l e c t ( 是将普通字符串转换为 Column scala> res3.select( 是将普通字符串转换为Columnscala>res3.select(“id”, " n a m e " ) . s h o w 4.4.2. 带条件过滤 w h e r e 或 f i l t e r 例如: r e s 13. s e l e c t ( "name").show 4.4.2. 带条件过滤where或filter 例如: res13.select( "name").show4.4.2.带条件过滤wherefilter例如:res13.select(“name”).where( " n a m e " = = = " B e n " ) . s h o w 必须用 "name" === "Ben" ).show 必须用 "name"==="Ben").show必须用转成Column才能进行比较,比较使用===进行比较

4.4.3. 排序orderBy或sort
orderBy( " 列名 " ) 升序排列 o r d e r B y ( "列名") 升序排列 orderBy( "列名")升序排列orderBy(“列名”.desc) 降序排列
orderBy($“列名” , $“列2”.desc) 按两列排序

res3.select( " i d " , "id", "id",“name”).orderBy( " i d " . d e s c ) . s h o w r e s 3. s e l e c t ( "id".desc).show res3.select( "id".desc).showres3.select(“id”, " n a m e " ) . s o r t ( "name").sort( "name").sort(“id”.desc).show

例如用它求解二次排序
sc.makeRDD(List((1,88),(2,77),(2,87),(1,99),(2,60),(1,97)))
.toDF(“id”,“score”)
res26.orderBy($“id”, " s c o r e " . d e s c ) . s h o w 4.4.4. 分组 g r o u p B y g r o u p B y ( " 列名 " , . . . ) . m a x ( 列名 ) 求最大值 g r o u p B y ( " 列名 " , . . . ) . m i n ( 列名 ) 求最小值 g r o u p B y ( " 列名 " , . . . ) . a v g ( 列名 ) 求平均值 g r o u p B y ( " 列名 " , . . . ) . s u m ( 列名 ) 求和 g r o u p B y ( " 列名 " , . . . ) . c o u n t ( ) 求个数 g r o u p B y ( " 列名 " , . . . ) . a g g 可以将多个方法进行聚合:例如: r e s 26. g r o u p B y ( " i d " ) . a g g ( m a x ( "score".desc).show 4.4.4. 分组groupBy groupBy("列名", ...).max(列名) 求最大值 groupBy("列名", ...).min(列名) 求最小值 groupBy("列名", ...).avg(列名) 求平均值 groupBy("列名", ...).sum(列名) 求和 groupBy("列名", ...).count() 求个数 groupBy("列名", ...).agg 可以将多个方法进行聚合: 例如: res26.groupBy("id").agg(max( "score".desc).show4.4.4.分组groupBygroupBy("列名",...).max(列名)求最大值groupBy("列名",...).min(列名)求最小值groupBy("列名",...).avg(列名)求平均值groupBy("列名",...).sum(列名)求和groupBy("列名",...).count()求个数groupBy("列名",...).agg可以将多个方法进行聚合:例如:res26.groupBy("id").agg(max(“score”), min( " s c o r e " ) , c o u n t ( "score"), count( "score"),count(“*”)).show

4.4.5. 设置运行参数
默认情况下SparkSQL在执行join或聚合函数时,shuffle所使用的partition数量为200。

要是大家注意观察的话,可以看到在joion时,命令行提示的就能看到partition为200。如果想修改这个参数,可以使用:
sqlContext.setConf(“spark.sql.shuffle.partitions”,“2”)
观察发现设置2时,发现task只有一个。设置3个task有两个。如果shuffle产生的task过多了,要减少数量时,就可以配置这个参数。
4.5. *SparkSQL的SQL方式
4.5.1. SparkSQL的SQL方式
上面的形式都是调用类似hql面向对象的方式,where、groupby等,使用着别扭,没有SQL使起来顺。假如和SQL使用方式一模一样,那开发难度是不就降低了,只要会SQL语句,就可以很方便的使用spark了。于是spark发明了它,新的sql调用方式。
4.5.2. 查询所有记录
bin/spark-shell --master=local[3] --driver-class-path=mysql-connector-java-5.1.28-bin.jar

scala> val df = sc.parallelize(List((1,“tony”),(2,“hellen”))).toDF(“id”,“name”)
df: org.apache.spark.sql.DataFrame = [id: int, name: string]

scala> df.select(“id”,“name”).show
±–±-----+
| id| name|
±–±-----+
| 1| tony|
| 2|hellen|
±–±-----+

scala> df.registerTempTable(“people”)//将DF结果res2转换注册一个临时表
scala> sqlContext.sql(“select * from people”).show
±–±-----+
| id| name|
±–±-----+
| 1| tony|
| 2|hellen|
±–±-----+
4.5.3. 分组,获取学生表中每个学生的最高成绩
scala> sc.makeRDD(List( (1,88),(2,77),(2,87),(1,99),(2,60),(1,97))).toDF(“id”,“score”)
res19: org.apache.spark.sql.DataFrame = [id: int, score: int]

scala> res19.registerTempTable(“student”)

scala> sqlContext.sql(“select * from student”).show
±–±----+
| id|score|
±–±----+
| 1| 88|
| 2| 77|
| 2| 87|
| 1| 99|
| 2| 60|
| 1| 97|
±–±----+

scala> sqlContext.sql(“select id,max(score) from student group by id”).show
±–±–+
| id|_c1|
±–±–+
| 1| 99|
| 2| 87|
±–±–+
4.5.4. 排序,获取前3名
scala> sqlContext.sql(“select * from student order by score desc limit 3”).show
±–±----+
| id|score|
±–±----+
| 1| 99|
| 1| 97|
| 1| 88|
±–±----+
4.5.5. 链接join
scala> val dept=sc.parallelize(List((100,“财务部”),(200,“研发部”))).toDF(“deptid”,“deptname”)
dept: org.apache.spark.sql.DataFrame = [deptid: int, deptname: string]

scala> val emp=sc.parallelize(List((1,100,“张财务”),(2,100,“李会计”),(3,200,“王艳发”))).toDF(“id”,“did”,“name”)
emp: org.apache.spark.sql.DataFrame = [id: int, did: int, name: string]

scala> dept.registerTempTable(“dept”)

scala> emp.registerTempTable(“emp”)

scala> sqlContext.sql(“select * from emp e inner join dept d on e.did=d.deptid”).show
±–±–±—±-----±-------+
| id|did|name|deptid|deptname|
±–±–±—±-----±-------+
| 1|100| 张财务| 100| 财务部|
| 2|100| 李会计| 100| 财务部|
| 3|200| 王艳发| 200| 研发部|
±–±–±—±-----±-------+

scala> sqlContext.sql(“select * from emp e left join dept d on e.did=d.deptid”).show
±–±–±—±-----±-------+
| id|did|name|deptid|deptname|
±–±–±—±-----±-------+
| 1|100| 张财务| 100| 财务部|
| 2|100| 李会计| 100| 财务部|
| 3|200| 王艳发| 200| 研发部|
±–±–±—±-----±-------+
4.5.6. 查看有哪些表
scala> val teacher = sc.parallelize(List((1,“陈讲师”),(2,“朴讲师”),(3,“赵讲师”)))toDF(“id”,“name”)
teacher: org.apache.spark.sql.DataFrame = [id: int, name: string]

scala> teacher.saveAsTable(“teacher”)
warning: there were 1 deprecation warning(s); re-run with -deprecation for details
[Stage 31:> (0 + 5) / 5]SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder”.
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
16/05/28 21:59:59 WARN HiveMetaStore: Location: file:/user/hive/warehouse/teacher specified for non-external table:teacher

scala> sqlContext.sql(“show tables”).show
±--------±----------+
|tableName|isTemporary|
±--------±----------+
| dept| true|
| emp| true|
| people| true|
| student| true|
| teacher| false|
±--------±----------+
isTemporary表示是否是临时表。临时表的意思只要应用程序退出了这些表就消失了。如sparkshell退出,这些表就消失了。
4.5.7. 查看表结构
scala> sqlContext.sql(“desc teacher”).show
±-------±--------±------+
|col_name|data_type|comment|
±-------±--------±------+
| id| int| |
| name| string| |
±-------±--------±------+
4.5.8. 类似Hive方式插入表和查询============
val sc = new SparkContext(new SparkConf().setAppName(“HiveAPP”))
val sqlContext = new org.apache.spark.sql.hive.HiveContext(sc)
sqlContext.sql(“CREATE TABLE IF NOT EXISTS src (key INT, value STRING)”)
sqlContext.sql(“LOAD DATA LOCAL INPATH ‘examples/src/main/resources/kv1.txt’ INTO TABLE src”)

val df5 = sqlContext.sql(“select key,value from src”)
sql方法运行查询临时表或真实表的结果类型是dataFrame类型
4.5.9. *wordcount
scala> sc.textFile(“/root/words.txt”).flatMap{ _.split(" ") }.toDF(“word”).registerTempTable(“words”)

scala> sqlContext.sql(“select * from words”).show
±-----+
| word|
±-----+
| big|
| data|
| big|
| spark|
| scla|
| spark|
|hadoop|
|hadoop|
| hive|
| spark|
±-----+

scala> sqlContext.sql(“select word,count(*) c from words group by word”).show
±-----±–+
| word| c|
±-----±–+
| hive| 1|
| scla| 1|
| spark| 3|
| big| 2|
| data| 1|
|hadoop| 2|
±-----±–+

酷炫吧,2行代码搞定

sc.textFile(“/root/words.txt”).flatMap{_.split(“\s+”)}
.toDF(“word”).registerTempTable(“words”)
sqlContext.sql(“select word,count(*) c from words group by word”).show
注意,返回不是所有,show默认返回前20行

热点单词,前3个单词

scala> sqlContext.sql(“select word,count(*) c from words group by word order by c desc limit 3”).show
±-----±–+
| word| c|
±-----±–+
| spark| 3|
| big| 2|
|hadoop| 2|
±-----±–+

黑郁金香小说中出现最多的单词

scala> sc.textFile(“/root/Tulip.txt”).flatMap{ _.split(“\s”) }
.toDF(“word”).registerTempTable(“words”)

scala> sqlContext.sql(“select word,count(*) c from words group by word order by c desc limit 10”).show
±—±—+
|word| c|
±—±—+
| the|3813|
| of|2018|
| to|1722|
| and|1231|
| a| 983|
| his| 934|
| in| 799|
| he| 629|
| was| 586|
|with| 529|
±—±—+

对1G的小说进行单词频度统计

准备:1G的output.txt数据文件
java实现
package cn.tarena.utils;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

public class WordCount {
private static String path_src = “c:/output.txt”;
private static String path_result = “c:/result.txt”;
private static BufferedReader br = null;
private static BufferedWriter bw = null;
private static String line_current = null;
private static String[] words = null;
private static List word_list = new ArrayList();

public static void main(String[] args) {
	Long startTime = System.currentTimeMillis();
	
    File file = new File(path_src);
    if (!file.exists()) {
        System.out.println("file " + file + " is not existed, exit");
        return;
    }

    try {
        br = new BufferedReader(new FileReader(file.getPath()));
        line_current = br.readLine();
        while (line_current != null) {
            words = line_current.split(" |,|\\.");
            for (String s : words) {
                if (!s.equals(""))
                    word_list.add(s);
            }

            line_current = br.readLine();
        }

        for (String temp : word_list) {
            System.out.println(temp);
        }

        // HashSet
        Set<String> hashSet = new HashSet<String>(word_list);
        for (String str : hashSet) {
            System.out.println("word: " + str +
                    ", occur times: " + Collections.frequency(word_list, str));
        }

        // HashMap
        Map<String, Integer> hashMap = new HashMap<String, Integer>();
        for (String temp : word_list) {
            Integer count = hashMap.get(temp);
            hashMap.put(temp, (count == null) ? 1 : count + 1);
        }

        // TreeMap
        TreeMap<String, Integer> treeMap = new TreeMap<String, Integer>(
                hashMap);

        // Record result to another file
        printMap(treeMap);
        
        System.out.println("执行耗时:"+(System.currentTimeMillis()-startTime)/1000+" 秒");
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        closeInputStream(br);
        closeOutputStream(bw);
    }
}

public static void printMap(Map<String, Integer> map) throws IOException {

    bw = new BufferedWriter(new FileWriter(path_result));

    Set<String> keys = map.keySet();
    for (String s : keys) {
        System.out.println("word: " + s +
                ", times: " + map.get(s));
        writeResult("word: " + s +
                ", times: " + map.get(s));
    }
    for (Map.Entry<String, Integer> entry : map.entrySet()) {
        System.out.println("word: " + entry.getKey() + ", number : "
                + entry.getValue());
        writeResult("word: " + entry.getKey() + ", number : "
                + entry.getValue());
    }

}

public static void writeResult(String line) throws IOException {

    try {
        if (bw != null) {
            bw.write(line);
            bw.newLine();
            bw.flush();
        }
    } catch (IOException e) {
        e.printStackTrace();
        closeOutputStream(bw);
    }
}

public static void closeOutputStream(BufferedWriter writer) {
    try {
        if (writer != null) {
            writer.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

public static void closeInputStream(BufferedReader reader) {
    try {
        if (reader != null) {
            reader.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

}
结果:
Exception in thread “main” java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.Arrays.copyOfRange(Arrays.java:2694)
at java.lang.String.(String.java:203)
at java.lang.String.substring(String.java:1913)
at java.lang.String.subSequence(String.java:1946)
at java.util.regex.Pattern.split(Pattern.java:1202)
at java.lang.String.split(String.java:2313)
at java.lang.String.split(String.java:2355)

Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread “main”

StandAlone模式
启动时4个分区,花费5分钟
scala> sc.textFile(“/root/output.txt”).flatMap{ _.split(“\s+”) }.toDF(“word”).registerTempTable(“words”)

scala> sqlContext.sql(“select word,count(*) c from words group by word order by c desc limit 10”).show
±—±-------+
|word| c|
±—±-------+
| the|11439000|
| of| 6054000|
| to| 5166000|
| and| 3693000|
| a| 2949000|
| his| 2802000|
| in| 2397000|
| he| 1887000|
| was| 1758000|
|with| 1587000|
±—±-------+
4.5.10. 小结
可以看出SQL直接执行,一切又变得简单。但简单背后可不简单,它是分布式的,支持海量数据,这是关系型数据望尘莫及的,此招一处必然spark将一统江湖。

  1. SparkStreaming+kafka
    5.1. SparkStreaming+kafka
    5.1.1. SparkStreaming+Kafka
    随着大数据的发展,人们对大数据的处理要求也越来越高,原有的批处理框架MapReduce适合离线计算(在MapReduce中,由于其分布式特性——所有数据需要读写磁盘、启动job耗时较大,难以满足时效性要求。),却无法满足实时性要求较高的业务,如实时推荐、用户行为分析等。
    Spark是一个类似于MapReduce的分布式计算框架,其核心是弹性分布式数据集,提供了比MapReduce更丰富的模型,可以在快速在内存中对数据集进行多次迭代,以支持复杂的数据挖掘算法和图形计算算法。
    5.2. 介绍
    5.2.1. 介绍
    Spark Streaming是一种构建在Spark上的实时计算框架,它扩展了Spark处理大规模流式数据的能力,吞吐量高和容错能力强著称。
    容错性,RDD会记住创建自己的操作,每一批输入数据都会在内存中备份,如果由于某个结点故障导致该结点上的数据丢失,这时可以通过备份的数据在其它结点上重算得到最终的结果。
    Spark Streaming适合一些需要历史数据和实时数据结合分析的应用场合。当然,对于实时性要求不是特别高的应用也能完全胜任。另外通过RDD的数据重用机制可以得到更高效的容错处理。

5.2.2. 优势
 能运行在100+的结点上,并达到秒级延迟。
 使用基于内存的Spark作为执行引擎,具有高效和容错的特性。
 能集成Spark的批处理和交互查询。
 为实现复杂的算法提供和批处理类似的简单接口。
5.2.3. 架构
SparkStreaming是一个对实时数据流进行高通量、容错处理的流式处理系统,可以对多种数据源(如Kdfka、Flume、Twitter、Zero和TCP 套接字)进行类似Map、Reduce和Join等复杂操作,并将结果保存到外部文件系统、数据库或应用到实时仪表盘。

Spark Streaming是将流式计算分解成一系列短小的批处理作业。这里的批处理引擎是Spark Core,也就是把Spark Streaming的输入数据按照batch size(如1秒)分成一段一段的数据(Discretized Stream),每一段数据都转换成Spark中的RDD(Resilient Distributed Dataset),然后将Spark Streaming中对DStream的Transformation操作变为针对Spark中对RDD的Transformation操作,将RDD经过操作变成中间结果保存在内存中。整个流式计算根据业务的需求可以对中间的结果进行叠加或者存储到外部设备。
5.2.4. 工作原理
Spark Streaming的基本原理是将输入数据流以时间片(秒级)为单位进行拆分,然后以类似批处理的方式处理每个时间片数据。

首先,Spark Streaming把实时输入数据流以时间片Δt (如1秒)为单位切分成块。Spark Streaming会把每块数据作为一个RDD,并使用RDD操作处理每一小块数据。每个块都会生成一个Spark Job处理,最终结果也返回多块。
所以Streaming很容易很mlib,Spark SQL等进行结合,做到实时的数据分析处理。此外,Streaming也继承了RDD的容错特性。如果RDD 的某些 partition 丢失了 , 可以通过 lineage 信息重新计算恢复。
内部实现原理
使用Spark Streaming编写的程序与编写Spark程序非常相似,在Spark程序中,主要通过操作RDD(Resilient Distributed Datasets弹性分布式数据集)提供的接口,如map、reduce、filter等,实现数据的批处理。而在Spark Streaming中,则通过操作DStream(表示数据流的RDD序列)提供的接口,这些接口和RDD提供的接口类似。

5.2.5. SparkStream和Storm比较
时延
虽然两框架都提供了可扩展性(scalability)和可容错性(fault tolerance),但是它们的处理模型从根本上说是不一样的。Storm可以实现亚秒级时延的处理,而每次只处理一条event,而Spark Streaming可以在一个短暂的时间窗口里面处理多条(batches)Event。所以说Storm可以实现亚秒级时延的处理,而Spark Streaming则有一定的时延。
吞吐率
Spark目前在EC2上已能够线性扩展到100个节点(每个节点4Core),可以以数秒的延迟处理6GB/s的数据量(60M records/s),其吞吐量也比流行的Storm高2~5倍,图4是Berkeley利用WordCount和Grep两个用例所做的测试,在Grep这个测试中,Spark Streaming中的每个节点的吞吐量是670k records/s,而Storm是115k records/s。

5.3. 构建InputDStream
5.3.1. 监听Socket实现单词统计
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming._

val ssc = new StreamingContext(sc, Seconds(2))
ssc.checkpoint(“/root/checkpoint”)

// 通过Socket获取数据,该处需要提供Socket的主机名和端口号,数据保存在内存和硬盘中
val lines = ssc.socketTextStream(“192.168.1.106”,9999, StorageLevel.MEMORY_AND_DISK_SER)
val words = lines.flatMap(.split(“\s+”))
val wordCounts = words.map(x=>(x,1)).reduceByKey(
+_).print

ssc.start()
ssc.awaitTermination()
5.3.2. 监控文件夹只处理最新文件
当目录下文件有变动,就它当做新流入的数据进行处理。
ssc.textFileStream从文本文件获取流,检查一个文件夹,看是否有新的文件进入。
scala> import org.apache.spark.streaming._
import org.apache.spark.streaming._

// 创建Streaming的上下文,包括Spark的配置和时间间隔,这里时间为间隔1秒
scala> val ssc = new StreamingContext(sc, Seconds(1))
ssc: org.apache.spark.streaming.StreamingContext = org.apache.spark.streaming.StreamingContext@222630af

// 指定监控的目录,在这里为/root/ss
scala> val lines = ssc.textFileStream(“/root/ss”)
lines: org.apache.spark.streaming.dstream.DStream[String] = org.apache.spark.streaming.dstream.MappedDStream@68569eb0

// 对指定文件夹变化的数据进行单词统计并且打印
scala> val words = lines.flatMap(_.split(" "))
words: org.apache.spark.streaming.dstream.DStream[String] = org.apache.spark.streaming.dstream.FlatMappedDStream@18852a10

scala> val wordCounts = words.map(x=>(x,1)).reduceByKey(+)
wordCounts: org.apache.spark.streaming.dstream.DStream[(String, Int)] = org.apache.spark.streaming.dstream.ShuffledDStream@66a677ae

scala> wordCounts.print()

scala> ssc.start() // 启动Streaming
scala> ssc.awaitTermination() // 计算完毕退出

输出结果:

Time: 1464580183000 ms


Time: 1464580184000 ms

(hive,1)
(big,2)
(spark,3)
(hadoop,2)
(scla,1)
(data,1)
说明:每1秒监听一次,看文件夹下有没有新文件。
简写代码
import org.apache.spark.streaming._
val ssc = new StreamingContext(sc, Seconds(5))
val lines = ssc.textFileStream(“/root/ss”)
val words = lines.flatMap(.split(" "))
val wordCounts = words.map((
,1)).reduceByKey(+)
wordCounts.print()
ssc.start()
ssc.awaitTermination()
说明:每5秒监听一次,看文件夹下有没有新文件。简单点假设每行只有一条数据做一个wordcount。.print方法展示结果是ds独有的。
拷贝文件到目录中
cp words.txt ss/words2.txt
启动后可以看到每10秒做一次统计

再拷贝一个文件,可以看到又进行了统计。
注意:只针对新文件进行处理,旧的文件就不管了。

5.3.3. 历史结果累计updateStateByKey
上面的方法称为无状态,而要记录中间结果就称为有状态的。它通过检查点来实现。
ssc.chekcpoint(“目录路径”)
设置了检查点,历史的RDD结果就会存入到指定的目录,如果未设置检查点,旧的历史的结果就被抛弃掉了。
updateStateByKey接收一个函数作为参数
Seq代表当前RDD的值的数组,Option代表上次累计的结果
格式:(seq,Option) => {Some(本次的计算结果)}
Option代表可有可无的值,没有值就是None,有值就是Some,getOrElse(”默认值”)
重构代码
import org.apache.spark.streaming._
val ssc = new StreamingContext(sc, Seconds(5))
ssc.checkpoint(“/root/checkpoint”)

val lines = ssc.textFileStream(“/root/ss”)
val words = lines.flatMap(_.split(" "))
val wordCounts = words.map(x=>(x,1)).updateStateByKey{
(seq, op:Option[Int]) => { Some(seq.sum + op.getOrElse(0)) }
}
wordCounts.print()

ssc.start()
ssc.awaitTermination()
将之前的reduceByKey换成updateStateByKey。可以看到每次执行都会记录下最后一次的结果,有新的变化就在这个结果上累加。

5.3.4. 时间段结果累计reduceByKeyAndWindow
窗口DStream
 window(windowLength, slideInterval)
 countByWindow(windowLength, slideInterval)
 reduceByWindow(func, windowLength, slideInterval)
 reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks])
 reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks])
 countByValueAndWindow(windowLength, slideInterval, [numTasks])
如果需要更灵活记录累计值,可以使用窗口相关函数,窗口函数有两个重要参数:WindowLength和SlidingInterval,它们的含义参考下图说明:

绿色框就代表每10秒处理一次。
WindowLength如每5个绿色窗口作为一个阶段,这就叫窗口长度。

SlidingInterval滑动间隔。如每隔3个进行一次统计。它必须是绿色框处理时间的倍数。
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming._

val ssc = new StreamingContext(sc, Seconds(2))
ssc.checkpoint(“/root/checkpoint”)

// 通过Socket获取数据,该处需要提供Socket的主机名和端口号,数据保存在内存和硬盘中
val lines = ssc.socketTextStream(“192.168.1.106”,9999, StorageLevel.MEMORY_AND_DISK_SER)
val words = lines.flatMap(.split(“\s+”))
words.map(x=>(x,1)).reduceByKey(
+_).print
words.map(x=>(x,1)).reduceByKeyAndWindow( (x:Int,y:Int)=>x+y, Seconds(6), Seconds(2) ).print

ssc.start()
ssc.awaitTermination()
5.3.5. 补充:Seq集合
Seq就类似数组,sum将其所有值进行累加。
Microsoft Windows [版本 6.1.7601]
版权所有 © 2009 Microsoft Corporation。保留所有权利。

C:\Users\Administrator>scala
Welcome to Scala version 2.11.7 (Java HotSpot™ 64-Bit Server VM, Java 1.7.0_7
2).
Type in expressions to have them evaluated.
Type :help for more information.

scala> val seq = Seq(1,2,3,4)
seq: Seq[Int] = List(1, 2, 3, 4)

scala> seq.sum
res1: Int = 10

scala>
5.3.6. 问题:不输出就报错
ERROR StreamingContext: Error starting the context, marking it as stopped java.lang.IllegalArgumentException: requirement failed: No output operations registered, so nothing to execute.
上面结果如果未输出,就会报错,spark认为没有设置输出,处理无意义,所以报错。加个.print方法就可以。这也是一种输出。所以stream的api中必须设置输出。(有print、forEachRDD、saveAs* 等)
5.4. 输出相关的方法
5.4.1. 输出相关的方法
常用的输出方法有:
Output Operation Meaning
print() Prints the first ten elements of every batch of data in a DStream on the driver node running the streaming application. This is useful for development and debugging.
Python API This is called pprint() in the Python API.
saveAsTextFiles(prefix, [suffix]) Save this DStream’s contents as text files. The file name at each batch interval is generated based on prefix and suffix: “prefix-TIME_IN_MS[.suffix]”.
saveAsObjectFiles(prefix, [suffix]) Save this DStream’s contents as SequenceFiles of serialized Java objects. The file name at each batch interval is generated based on prefix and suffix: “prefix-TIME_IN_MS[.suffix]”.
Python API This is not available in the Python API.
saveAsHadoopFiles(prefix, [suffix]) Save this DStream’s contents as Hadoop files. The file name at each batch interval is generated based on prefix and suffix: “prefix-TIME_IN_MS[.suffix]”.
Python API This is not available in the Python API.
foreachRDD(func) The most generic output operator that applies a function, func, to each RDD generated from the stream. This function should push the data in each RDD to an external system, such as saving the RDD to files, or writing it over the network to a database. Note that the function func is executed in the driver process running the streaming application, and will usually have RDD actions in it that will force the computation of the streaming RDDs.

5.4.2. 控制台输出print
测试时使用,开发时比较少。
5.4.3. *文本文件输出saveAsTextFiles
格式:前缀_后缀[.后缀可选] #前缀将作为文件夹名称
import org.apache.spark.streaming._
val ssc = new StreamingContext(sc, Seconds(10))
val ds = ssc.socketTextStream(“192.168.1.106”,9999)
val rdd = sc.parallelize( List((“a”,1),(“b”,1)))
ds.map( x=>(x,1) ).reduceByKey( (x,y)=>x+y ).transform( r=>r.join(rdd)).saveAsTextFiles(“/root/output”)
ssc.start
ssc.awaitTermination()
通过对源DStream的每个RDD应用RDD-to-RDD函数,创建一个新的DStream。
生成结果

saveAsObjectFiles 序列为java对象后输出
saveAsHadoopFiles 作为hadoop文件输出
5.4.4. x-foreachRDD实时输出
forEach

准备:
1)http://192.168.35.1:8080/TestSocket/post #post请求地址接收json串
2)nc –lk 9999 #启动网络监控NetCat
3)执行命令需要添加第三方的2个jar:
上传bee-client_2.10-0.28.0.jar,slf4j-api-1.7.10.jar到下面目录
/usr/local/src/spark/spark-1.5.2-bin-hadoop2.6
4)启动SparkShell
bin/spark-shell --master=local[4] --jars=bee-client_2.10-0.28.0.jar,slf4j-api-1.7.10.jar
5)测试代码
import org.apache.spark.streaming._
import uk.co.bigbeeconsultants.http._
import java.net.URL
val ssc = new StreamingContext(sc, Seconds(10))
val ds = ssc.socketTextStream(“localhost”,9999)
// 遍历dstream所有的rdd
// 建立连接的代码如果在这里, 实际在driver上创建了连接 (错误)
ds.map{ (_,1) }.reduceByKey { + }.foreachRDD { (rdd, time) => {
// 遍历每个rdd里的结果数据
// 建立连接的代码写在这里比较合适
rdd.foreach {
x => {
val json = s"“”{“word”: “${x._1}”, “count”: x . 2 , " t i m e " : " {x._2}, "time":" x.2,"time":"{time}" }“”"
// 构造请求参数对象: json=json字符串
val requestBody = RequestBody(Map(“json” -> json))
val url = new URL(“http://192.168.35.1:8080/TestSocket/post”)
// 发送一个http请求
// 建立连接的代码如果在这里, 效率太低 (错误)
new HttpClient().post(url, Some(requestBody))
}
}
} }
要注意的是:发送http请求也好,或是将数据存入数据库也好,在建立连接时,不要将建立连接的代码写在rdd.foreach中,不然效率太低。

5.5. Spark优化
5.5.1. 更好的序列化实现
Spark用到序列化的地方
1) Shuffle时需要将对象写入到外部的临时文件。
2) 每个Partition中的数据要发送到worker上,spark先把RDD包装成task对象,将task通过网络发给worker。
3) RDD如果支持内存+硬盘,只要往硬盘中写数据也会涉及序列化。
官方帮助文档中提及

默认使用的是java的序列化。但java的序列化有两个问题,一个是性能相对比较低,另外它序列化完二进制的内容长度也比较大,造成网络传输时间拉长。业界现在有很多更好的实现,如kryo,比java的序列化快10倍以上,相当高啊。而且生成内容长度也短。时间快,空间小,自然选择它了。
方法一:修改spark-defaults.conf配置文件
设置:
spark.serializer org.apache.spark.serializer.KryoSerializer
注意:用空格隔开
方法二:启动spark-shell或者spark-submit时配置
–conf spark.serializer=org.apache.spark.serializer.KryoSerializer
方法三:在代码中
val conf = new SparkConf()
conf.set(“spark.serializer”,“org.apache.spark.serializer.KryoSerializer”)
三种方式实现效果相同,优先级越来越高。
5.5.2. 少用包装类型,多用基本类型

官网上就指出让尽量使用java的基础类型,因为javaString就比基本类型多出40个bytes。因为要对原始的字符串进行包装嘛。字符的下标啊,包括数组,还有些方法。这个都加上,每个字符串都比原始的数据多40个字节。又例如hashmap和LinkedList要保持数据结构,如hashmap实现时就要数组加链表来实现,它为了实现这个数据结构,就需要多些额外对象,那这些额外的对象必然占额外的数据空间。所以为什么spark没有使用java的hashmap,而用了个二次探测法实现的hashmap呢。就是为了节省内存。从好几个角度来论证应该选用基本的数据类型。尽量的节省内存空间。
5.5.3. 回顾:*hashmap的内部实现
这里我们使用的map还是很有问题的,大家都知道java中的map怎么实现的?
在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是引用(模拟指针),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap是基于哈希表的Map接口的实现。允许使用null值和null键。
hashmap由两部分组成数组和链表,一般数组大小是2的n次方。map的每个元素都是一个key,value。内部会把它先封装成一类型,java中叫Entry类型Entry(key,value)就和我们讲的tuple很像。就是一个键值对,先会算一下key的hashcode。算出来是一个整数值,很大。再根据这个很大的整数,再对整个数组的大小做取模操作。如:49823948 % 8。结果就对应数组的一个下标。接着处理Entry,假如刚好它的key和第一个Entry算完都在同一个下标,这时就会形成一个链表,第一个Entry指向第二个Entry,最后一个元素就指向空。新加入的放在链头,最先加入的放在链尾。当然如果hashcode做得好,它分散比较均匀,它的链表长度不会很长。如果map的元素个数超过了预设的值,默认0.75。整个hashmap就要扩容,它不会等放满了才进行扩容,扩容就翻倍。一翻倍问题来了,那元素再hashcode后取余的数量发生变更,下标值就不一样了,如变成16。那16就必须重新hash。把key/value重新打散,再重新分配。这是hashmap的内部实现。

这种结构主要目的为了快速查找,例如根据一个key找到一个value。先根据key计算hashcode然后取模以后,这样就缩小了比较的次数。再在链表中,再跟每个对象equals比较key,就找到要找到元素。如果直接放到一个list中,没有hashcode比较的过程,那就需要和list中每个元素进行比较。所以hashmap最终目标就是实现元素的快速查找。
但hashmap这种实现,不太适合大数据量的情况,不适合我们所做的combine(缩容)操作,一旦map扩容,或者map中有缩短,元素都需要重新计算位置,它的效率是相当低的,所以hashmap查找时效率高。
5.5.4. *哈希表二次探测
hashmap扩容或者删除元素时效率低,这样如果数据量大的情况下,这种操作肯定相当的频繁,怎么解决其性能问题呢?Spark自己实现了一个map,它叫二次探测map。

Spark中的AppendOnlyMap的内部实现,它是一个特殊的map实现,它采用了一种Quadratic Probing的算法。二次探测map算法。
创建一个数组,默认7个长度0到6。向里面存放元素,也是按key计算hashcode。如hashcode后算完是76。那放到哪里呢?它就会用探测的算法来计算位置。它有一个探测指针值probe,初始值是0,对hashcode值模7。(76 % 7 = 6)放入下标为6的数组中。这点和传统的hashmap是一样的。下面又要放入一个值hashcode后为40,模7后为5,和刚才没有冲突,继续放入。但接下来发生冲突它就不按原来的方式了。如48 % 7 = 6和76冲突了,它就称为hashcode取值发生碰撞。它就把probe加1,然后取1的平方,然后结果跟上次取余的结果相加。如上次是6,(6+1*1)%7计算完为0,然后就放在0的位置。这样做最大的好处就是最后的存储结构中避免了产生链表的结构。发生碰撞就放在另外的位置,而传统的hashmap就放在链表里了。思路已经明确。继续当key=5时,48 % 7 = 5和76冲突probe=1 (5+1*1)%7=6又和40冲突了,然后probe=2 (6+2*2)%7=2放入下标2的位置。

注意每次都是按第一次的余数做基准,而不是上一次的余数。它就用这种方式就把key,value放在了数组的不同位置,而不是采用链表的方式来实现。这种的优点在哪呢?如果你的map是只增加或者只修改值,不对map的元素删除时,用它效率比较高,因为它就是一维数组。它没有链表结构,占用的内存也比较少。这也就是spark中做combine合并时,不用传统hashmap方式而用它的理由。spark中AppendOnlyMap类就是利用二次探测法实现的。它里面是可变的,不是线程安全的。这里没有关系,因为它不可能同时往数组里放元素,收集的线程做成单线程就解决了。
5.5.5. 回顾:***java的垃圾回收机制
虽然spark是用scala编写,但最终还是要在jvm中运行,这就必然java的一些参数对其影响。
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干不同的数据区域,这些区域都有各自的用途以及创建和销毁的时间。根据《Java虚拟机规范(第2版)》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域。

Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例 。Java堆是垃圾收集器管理(GC: Garbage Collector)的主要区域。由于现在收集器基本采用分代回收算法,按对象访问的频繁不频繁,把它分成几个区域。就是堆内存又被划分为:

1) 永久区(这里的内容一般就一直存在,如类对象、静态变量。垃圾回收不会去回收它们。)
2) 老生代,缓存,引用老不被释放
3) 新生代,新对象
能不能回收看它有没有被引用,有引用就不能回收,没引用就可以回收。
最开始创建的对象都在新生代区域。如果GC回收时,发现它有引用,就将它放到老生代。这样老生代回收的频率比新生代就低些。新生代发生的垃圾回收就成为miniGC。如果经过几次垃圾回收后,内存占用率还是很高,GC就考虑对老生代也进行垃圾回收。如果老生代发生了垃圾回收就叫fullGC。一旦发生fullGC,就会影响程序执行,程序假死,甚至jvm都暂停,只做垃圾回收的事情。
新生代又划分为三个区:伊甸园、幸存区1、幸存区2
伊甸园,最早的对象new完就在这里,它刚生下来。一旦GC回收伊甸园的对象幸存下来,就将其放到“幸存区”,幸存区分为两个,幸存区1和幸存区2。同一时刻必然有一个幸存区是空的。
如:new对象被放在伊甸园,当伊甸园满了,就会触发miniGC,假设对象没有被回收掉,有人引用它,它就会被放在幸存区中去。假设第二个新的对象放入伊甸园,当伊甸园也满了,miniGC开始回收。它就会将第二个对象放入幸存区2,同时将幸存区1中的所有对象也都放到幸存区2中。这样幸存区1和伊甸园就都空下来了。假设第三个对象又来了,伊甸园又满了,它就会将第三个对象和幸存区2中的对象都放入到幸存区1中。这是就把伊甸园和幸存区2空出来了。
交替来空闲。这样做主要为了效率,只做移动,无需比较无需判断。反正总有一个幸存区是空的,为了在不同的内存区移动对象效率更高些。把对象都放在一块内存,读取是不就快些。只扫描一部分内存区域,不用跳着去扫。
那什么时候对象会跑到老生代去呢?每次对象被GC都有个计数,比如第一次活下来了,第二次活下来了,GC收集了很多次,达到一定的寿命,比如15次。那就把这个对象移到老生代。也就是基于习惯,如果对象老被引用,那肯定不应该被回收掉。那把它扔到哪里呢?扔到老生代中。其实很好理解,老生代应该放的就是经常驻留在内存中的对象。比如说缓存,缓存中的数据,在java中就表示成一个map。你说这个map能让GC轻易回收掉吗?所以缓存对象经过几次GC后就会放在老生代中。
或者说老生代就是寿命比较长的数据,新生代就是那些寿命比较短的对象。寿命最短的对象都在伊甸园中。
5.5.6. 经常发生fullGC就要减少缓存的大小

缓存一直占有着老生代,它不会轻易的被清除。这样就导致新生代内存偏少。那就减少缓存所占用的内存。缓存少了,老生代被占用的内存也少了,老生代所在内存减少了,就不会老被fullGC了。什么时候发生fullGC呢?老生代满了的时候。将更多的内存腾给新生代,经常用作计算的那些内存主要新生代在用。new出来就扔了,new出来就扔了,尤其是伊甸园的内容。所以把缓存内容减少一点,把更多内存留给计算。

所以设置值为0.5。

5.5.7. RDD的调优
和上面一样,只是从另一个角度论述。
spark.storage.memoryFraction如果很多rdd对象经常要重复使用,这时可以spark.storage.memoryFraction调大,如果没有什么需要重用的rdd需要缓存,可以把这个调小,把更多的内存留给计算使用。
5.5.8. 序列化RDD存储
rdd.persist(StorageLeve.MEMORY_ONLY) //只对缓存数据序列化
rdd.persist(StorageLevel.MEMORY_ONLY_SER) //对其中的对象做序列化
序列化后对象占用的内存就减少了,可以容纳更多的数据,另外内存减少了,fullGC就不会频繁的垃圾回收了。
5.5.9. 并行度调优
如果数据量特别大,内存中放不下。就意味着先要把中间结果存硬盘,但这样效率一下就低下来了。那怎么办呢?可以把并行度调大些。调大些就意味着每个任务占的内存就小了。内存中同时容纳的任务数就增多了。
例如:每个任务需要500M的内存,但内存只有400M。
之前按4个分区sc.repartition(),4500M=2G,一个分区需要500M算,需要2G。加大并发量,变成5个分区,每个分区400M,这样5400M=2G。这样内存就够用了。
5.5.10. 数据本地化
尽量让worker节点和hadoop的dataNode在同一台服务器上,那就不用跨网络访问,那效率自然读取的速率就提高了。内部由hadoop的api来具体实现。可以配置机架等。
5.5.11. 空任务小任务合并
如果多次filter操作后会产生多个空任务和小任务,partition数据量没了,或者非常小,在由RDD包装成一个task,是不是得不偿失啊。遇到这种情况则应该用coalesce和repartition合并。空的就会过滤掉,小的就被合并了。这样就减少了一些不必要的任务执行。因为有时不是task多了就好。很小的任务还得涉及网络的传输,又涉及IO,得不偿失。
5.5.12. 配置临时文件目录
spark.local.dir参数。当shuffle、归并排序(sort、merge)时都会产生临时文件。这些临时文件都在这么指定的目录下。那这个文件夹有很多临时文件,如果都发生读写操作,有的线程在读这个文件,有的线程在往这个文件里写,IO性能就非常低。
怎么解决呢?可以创建多个文件夹,每个文件夹都对应一个真实的硬盘。假如原来是3个程序同时读写一个硬盘,效率肯定低,现在让三个程序分别读取3个磁盘,这样冲突减少,效率就提高了。这样就有效提高外部文件读和写的效率。怎么配置呢?只需要在这个配置时配置多个路径就可以。中间用逗号分隔。
spark.local.dir=目录1,目录2,目录3
5.5.13. worker倾斜
有种情况,可能一共worker分配的任务很多,可其他的worker都闲着呢。其他worker上的任务都很快的处理完了,可一台worker上的任务很多一直跑,别的都在等它。这种情况叫做worker的倾斜。原因就是数据分配的不合理。原因是因为key划分的不合理。(redis就存在这个问题)这个就需要去观察key的分布,如果发现这种数据倾斜,就重新定义key,找到一个合适的key的算法。
还可以设置spark.speculation=true会把持续不给力的node去掉。如执行任务时,它老是最后执行完,下次给它还是这样。那再下次任务就不要它了。但这个配置判断依据不是那么准确,所以可以尝试看看。不能真正可靠的解决数据倾斜的问题。
5.5.14. collect速度慢
我们在讲的时候一直强调,collect只适合在测试时,因为把结果都收集到Driver服务器上,数据要跨网络传输,同时要求Driver服务器内存大,所以收集过程慢。解决办法就是直接输出到分布式文件系统中。
reducer数量不合适
当做reduceByKey(函数,partition的数量)groupByKey时,partition的数量就是reduce的数量,这个数量也需要根据实际情况调整,太多的reducer就造成task很小。看着任务多了,但任务不饱满,还不如较少的reducer的性能高。但如果reducer太少,并发数就不够,任务执行就慢。所以在reducer数量上的取一个平衡。这个只能根据实际情况来确定。
5.5.15. shuffle
shuffle的过程有两种,一种用hash实现,一种用sort实现。hash时,需要MR个临时文件。过多的临时文件,读写速度受影响,同时占用内存过多,整体性能下降。所以要减少文件个数。
spark.shuffle.consolidateFiles=true 但实际应用中虽然能减少些,但对性能没有明显提升。
sort每个产生两个临时文件,一个文件存数据,数据是分段存储的。比如1…10给reducer1用的,11…20是给reducer2用的。21…以后是给reducer3用的。另一个文件存储每个reducer读取的位置。所以需要的临时文件数量时M
2个。
5.5.16. RDD操作使用MapPartitions替代map
map方法对RDD的每一条记录逐一操作。mapPartition是对整个RDD,以迭代器的方式逐一操作,如下面代码
rdd map x=>conn=getDBConn.conn;write(x.toString);conn close;
这样频繁的链接、断开数据库,效率差。
rdd mapPartitions(record:=>conn.getDBConn;for(item<-recorders;write(item.toString);conn close;
这样就一次链接一次断开,中间批量操作,效率提升。

  • 11
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值