文章目录
前言
Hadoop丰富了大数据处理思想,将分而治之形式散布出去,但由于MR繁琐的shuffle过程、频繁的I/O操作,导致计算过程特别慢。基于此,hive实现sql编写代替mr程序编写,presto实现中间结果存储内存中,spark则全都具备,且将task任务在线程中处理。
一、Spark概念
1.MapReduce框架
分布式并行计算框架,处理数据,思想:分而治之(先分后合)
map:分
将处理大规模数据划分为一部分一部分数据,每部分数据启动任务MapTask处理
10GB -> 100 份 -> 100 MapTask 处理
reduce:合
合并所有map处理结果,合并数据启动任务:ReduceTask,可以有多个合并任务
2.Spark框架
Apache 顶级项目,类似Hadoop、Hive、Zookeeper
http://spark.apache.org
官方定义:Unified engine for large-scale data analytics
第1点:类似MapReduce大数据计算引擎,处理计算数据分布式计算引擎
第2点:统一分析引擎,表示Spark 框架可以针对任意类型数据分析需求进行,如:
离线分析
实时计算
图形计算,比如A地方 -> B 地方,最短最优路径,类似高德导航
机器学习,比如推荐系统
科学计算,比如Pandas,R语言
第3点:处理大规模数据,也可以分析小数据(比如pandas分析数据,mysql数据库分析数据)
第4点:并行计算引擎,也可以单机分析数据
RDD:封装数据结构,将其当做集合,类似Python中列表list或字典dic,处理HDFS上数据,首先将HDFS上数据封装到RDD集合中,默认情况下:
1个block对应1partition
logs.data: 100 block -> RDD: 100 个partition -> 100 Task任务
3.两者之间的比较
1、计算的中间结果,MR写入磁盘,spark储存在内存中。
2、job的调度方式,spark以DAG图进行job的调度,每个job有多个stage,每个stage有多个task,task是以线程执行,省略了进程的申请和销毁过程。
3、mr直接读取文件,spark将文件封装成结构化数据集RDD形式,便于计算。
比较方面 | MapRedue 计算引擎 | Spark 计算引擎 |
---|---|---|
1、Job 程序结构 | 1 个Map Stage + 1个 Reduce Stage | 构架DAG图,多个Stage 多个Map Stage + 多个Redue Stage |
2、中间结果存储 | 本地磁盘Disk | 没有Shuffle时,存储内存Memory |
3、Task 运行方式 | 进程Process:MapTask 和Reduce Task | 线程Thread:Task,无需频繁启动和销毁 |
4、程序编程模型 | 直接读取文件数据,map + reduce | 文件数据封装:RDD,调用函数处理 |
4.spark应用执行组成
包含两部分
1、driver program:申请资源运行executor调度job执行,相当于yarn中的appMaster,运行jvm process,运行程序的main函数,必须创建sparkcontext对象
2、executor:缓存RDD数据,执行task任务,相当于线程池,运行jvm process每个任务对应一个线程,最少需要1core,线程数等于cpu core数
5、spark standalone集群
类似于YARN集群,分布式有两个节点
1、master,集群的资源管理和调度,类似于RM
2、workers,集群的真正资源,类似于NM
整个集群存在主节点的单点故障,可利用zookeeper的注册机制实现高可用。
二、词频统计与spark应用运行
1.Hive实现词频统计
1、加载文件数据到数据库
LOAD DATA LOCAL INPATH '/root/words.txt' INTO TABLE db_test.tbl_lines;
2、格式化字段
select split(line,' ') from tbl_lines;
3、表生成函数,单行转多行
select explode(spliit(line,' ')) from tb_lines;
4、子查询、group by 、order by,完整sql
select word,count(word)as num from (select explode(split(line,' '))as word from tbl_lines)t group by word order by num desc;
2.spark实现词频统计
1、构建context对象
sc = Sparkcontext(conf=conf)
2、加载数据源
sc.textFiile('../datas/words.txt')
3、数据格式化、转化成元组、根据key聚合
output_rdd = input_rdd \
.flatMap(lambda line: str(line).split(' ')) \
.map(lambda word: (word, 1)) \
.reduceByKey(lambda tmp, item: tmp + item)
spark RDD模式,也可以使用spark sql模式
1、创建context对象
2、注册临时视图
3、编写sql
createOrReplaceTempView('view_tmp_lines')
spark.sql(
WITH tmp AS (
select explode(split(value, ' ')) AS word from view_tmp_lines
)
SELECT word, COUNT(1) AS total FROM tmp GROUP BY word ORDER BY total DESC LIMIT 10
)
在spark sql使用过程中最主要的是dataframe的构建,有三种方式,后续介绍。dataframe类似于pandas中的dataframe。
3.spark-submit提交应用执行
spark-submit提交应用执行主要有三种参数:
1、基本参数
–master 【】 此参数代表程序运行在哪里,包括本地:local[N] 集群:standalone yarn mesos
–driver-mode 此参数代表driver program 运行在哪里,默认client,包括提交应用的客户端:client 集群的从节点:cluster
–conf 【】 此参数可设置其他可选项,如task任务数
2、Driver Program 参数
–driver-memory 512M 此参数设置driver启动内存,默认1G
–driver-cores 1 此参数设置运行核数
–supervise 此参数设置运行在standalone中的driver是否自动失败重启
3、Executor 参数配置
–executor-memory 512M 此参数设置启动executor线程池内存大小
–executor-cores 此参数设置启动executor线程池的核数默认1
–num-executors 此参数设置启动的executor个数,默认2
–queue 此参数设置executor队列,在standalone集群中,默认调度策略为先进先出
具体参数设置,参考官方文档
4.spark-submit提交应用运行模式
–deploymode 参数最大的设置就是driver programe运行在哪里,是在client即提交客户端上,还是cluster集群从节点中。这里又分为standalone集群和yarn集群,通常都是部署在yarn集群上,一方面是yarn集群更好的契合hadoop资源和HDFS,另一方面是yarn集群的资源调度策略更为成熟。
当运行在standalone上时,driver program 运行在worker节点中完成job调度和executor启动、注册,当运行在yarn集群中,driver program与appMaster合为一,完成任务的调度和资源申请。
5.Spark程序运行YARN集群流程
yarn-client:
1、Driver在任务提交的本地机器上运行,Driver启动后会和ResourceManager通讯申请启动ApplicationMaster;
2、随后ResourceManager分配Container,在合适的NodeManager上启动ApplicationMaster,此时的ApplicationMaster的功能相当于一个ExecutorLaucher,只负责向ResourceManager申请Executor内存;
3、ResourceManager接到ApplicationMaster的资源申请后会分配Container,然后ApplicationMaster在资源分配指定的NodeManager上启动Executor进程;
4、Executor进程启动后会向Driver反向注册,Executor全部注册完成后,Driver开始执行main函数;
5、之后执行到Action算子时,触发一个Job,并根据宽依赖开始划Stage,每个Stage生成对应的TaskSet,之后将Task分发到各个Executor上执行。
yarn-cluster:
1、任务提交后会和ResourceManager通讯申请启动ApplicationMaster;
2、随后ResourceManager分配Container,在合适的NodeManager上启动ApplicationMaster,此时的ApplicationMaster就是Driver;
3、Driver启动后向ResourceManager申请Executor内存,ResourceManager接到ApplicationMaster的资源申请后会分配Container,然后在合适的NodeManager上启动Executor进程;
4、Executor进程启动后会向Driver反向注册;
5、Executor全部注册完成后Driver开始执行main函数,之后执行到Action算子时,触发一个job,并根据宽依赖开始划分stage,每个stage生成对应的taskSet,之后将task分发到各个Executor上执行
三、RDD算子
1、概念和特性
RDD:弹性分布式数据集,数据的抽象封装,可调用转换算子和触发算子进行数据处理。
主要特质:
不可变:immutable,相当于元组
分区的:partitioned,大数据分而治之思想,类似数据逻辑切片
并行计算:parallel,并行计算
主要特性:
1、a list of partitions
数据为分区形式的列表
2、A function for computing each split
分区计算,分而治之
3、A list of dependencies on other RDDs
RDD之间具有依赖性
4、Optionally, a Partitioner for key-value RDDs
可选,key/value形式分区计算
5、Optionally, a list of preferred locations to compute each split
可选,程序移动,数据不动
2、RDD创建方式
1、并行化集合
2、读取储存系统文件
# 创建SparkConf实例,设置应用属性,比如名称和master
spark_conf = SparkConf().setAppName("SparkContext Test").setMaster("local[2]")
# 创建SparkContext对象,传递SparkConf实例
sc = SparkContext(conf=spark_conf)
# 读取数据源
# 1、并行化集合,设置分区数
input_rdd = sc.parallel([1,2,3,4,5,6],numSlices=2)
# 2、读取存储系统数据,设置分区
input_rdd_text = sc.textFile('../datas/words.txt', minPartitions=2)
3、小文件处理
①、在文件系统中合并小文件
②、利用wholeTextFiles方法加载小文件
input_rdd = sc.wholeTextFiles('../datas/rating', minPartitions=2)
3、RDD常用算子
算子即封装的特定方法,RDD算子主要分为两类,一种是转换算子,其返回值是一个RDD;一种是触发算子,其返回值,触发job执行。
常用的算子有:
1、常用转换算子:map、filter、flatMap
# TODO: map转化算子操作
result_map = map_rdd.map(lambda items: items * items)
# TODO: filter 算子,对集合中每条数据进行过滤,返回值为true保存,否则删除
result_filter = map_rdd.filter(lambda a: a % 2 != 0)
# TODO: flatMap 算子,对集合中每条数据进行处理,类似map算子,但是要求处理每条数据结果为集合,将自动将集合数据扁平化(explode)
rdd = sc.parallelize(['爱情 / 犯罪', '剧情 / 灾难 / 动作'])
result_flatmap = rdd.flatMap(lambda a: str(a).split(' / '))
2、常用触发算子:count、foreach、saveAsTextFile
# TODO: count算子统计集合中元素的个数
print(input_rdd.count())
# TODO: foreach 算子,遍历集合中每个元素,进行输出操作
input_rdd.foreach(lambda a: print(a))
# TODO: saveAsTextFiles 算子,将集合数据保存到文本文件,一个分区数据保存一个文件
input_rdd.saveAsTextFile('../datas/output')
3、基本转换算子:union、distinct、groupByKey、reduceByKey
# TODO: union算子,合并两个rdd,相当于union all
rdd_union = rdd1.union(rdd2)
# TODO: distinct对rdd进行去重
rdd_distinct = rdd_union.distinct()
# TODO: groupByKey对rdd进行分组,相同key的value放在list
rdd_groupByKey = rdd3.groupByKey()
print(rdd_groupByKey.collect())
rdd_groupByKey.foreach(lambda tuple: print(tuple[0], list(tuple[1])))
# TODO: reduceByKey算子,将集合中数据,先按照Key分组,再使用定义reduce函数进行组内聚合
rdd_reduceByKey = rdd3.reduceByKey(lambda tmp, a: tmp + a)
print(rdd_reduceByKey.collect())
rdd_reduceByKey.foreach(lambda a: print(a))
4、基本触发算子:first、take、collect、reduce
# TODO: first算子获取集合中第一个元素
print(input_rdd.first())
# TODO: take获取集合中前几个元素,返回一个列表
print(input_rdd.take(4))
# TODO: collect获取集合中所有的元素,存储在内存中数据量大时不建议用
print(input_rdd.collect())
# TODO: reduce对集合中的元素进行聚合操作
print(input_rdd.reduce(lambda a, b: a + b))
5、数据排序算子:sortBy、sortByKey、top、takeOrdered
input_rdd = sc.parallelize(
[("spark", 10), ("mapreduce", 3), ("hive", 6), ("flink", 4), ("python", 5)],
numSlices=2
)
# TODO: sortBy 分区排序
rdd1 = input_rdd.sortBy(keyfunc=(lambda a: a[1]), ascending=False)
rdd1.foreach(lambda a: print(a))
# TODO: sortByKey 分区排序,对象为dict形式
rdd2 = input_rdd.map(lambda a: (a[1], a[0])).sortByKey(ascending=False)
rdd2.foreach(lambda a: print(a))
# TODO: top 获得集合中最大的前N个数据,触发算子返回的是list
rdd3 = input_rdd.top(3)
print(rdd3)
for i in rdd3:
print(i)
# TODO: takeOrdered 获得集合中最小的前N个数据,触发算子返回的是list
rdd4 = input_rdd.takeOrdered(4)
print(rdd4)
for i in rdd4:
print(i)
6、调整分区算子:repartition、coalesce
# TODO: 加载数据源
input_rdd = sc.textFile('../datas/words.txt', minPartitions=2)
print(input_rdd.getNumPartitions())
# TODO: repartition 增加partition会产生shuffle
rdd1 = input_rdd.repartition(4)
print(rdd1.getNumPartitions())
# TODO: coalesce 减少partition 不会产生shuffle
rdd2 = input_rdd.coalesce(1)
print(rdd2.getNumPartitions())
4、RDD其他算子
RDD算子运算除了常用的算子还有些针对特定处理的算子,包括聚合、join、优化等等。
1、数据聚合算子:reduce、fold、aggregate
分布式聚合的思想是先局部集合,再全局聚合。三者之间的区别是起始值的设置,fold和aggregate可设置起始值,并且aggregate可进一步设置全局聚合函数
# TODO: 加载并行化数据源
input_rdd = sc.parallelize([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], numSlices=2)
# TODO: reduce算子
rd_rdd = input_rdd.reduce(lambda tmp, a: tmp + a)
# TODO: fold算子计算
fd_rdd = input_rdd.fold(0, lambda tmp, a: tmp + a)
# TODO: aggregate算子计算
ag_rdd = input_rdd.aggregate(0, lambda tmp, a: tmp + a, lambda tmp, a: tmp + a)
ag_rdd2 = input_rdd.aggregate(0, lambda tmp, a: tmp + a, lambda tmp, a: tmp * a)
2、Key/Value类型算子:keys/values、mapValues、collectAsMap
相当于字典方法
input_rdd = sc.parallelize(
[("spark", 10), ("mapreduce", 3), ("hive", 6), ("flink", 4)],
numSlices=2
)
# TODO: keys/values 算子使用
input_rdd.keys().foreach(lambda a: print(a))
input_rdd.values().foreach(lambda a: print(a))
# TODO: mapValues 算子使用,对values值进行计算
input_rdd.mapValues(lambda a: a * a).foreach(lambda a: print(a))
# TODO: collectAsMap 将rdd转化成字典
print(input_rdd.collectAsMap())
3、join关联算子,join、left join、right join、 full outer join
类似sql中的join
emp_rdd = sc.parallelize(
[(1001, "zhangsan"), (1002, "lisi"), (1003, "wangwu"), (1004, "zhaoliu")]
)
dept_rdd = sc.parallelize(
[(1001, "sales"), (1002, "tech")]
)
# (1001, ('zhangsan', 'sales'))
# (1002, ('lisi', 'tech'))
# TODO:关联算子
result_rdd = emp_rdd.join(dept_rdd)
result_rdd.foreach(lambda a: print(a))
4、分区处理算子
针对分区数据与外部建立对象连接时,将整个分区创建一个对象,减少对象的创建和销毁过程
input_rdd = sc.parallelize(list(range(1, 11)), numSlices=2)
# TODO: map 算子处理rdd中的元素
result_rdd = input_rdd.map(lambda a: a * a)
# TODO: mapPartitions 算子处理分区迭代对象,返回迭代对象
def mp_func(iter):
for item in iter:
yield item * item
mp_rdd = input_rdd.mapPartitions(mp_func)
# TODO: foreachPartition 算子,表示对RDD集合中每个分区数据输出
def fp_func(iter):
for item in iter:
print(item)
input_rdd.foreachPartition(fp_func)
5、jieba分词模块
根据特定的方法将字符串切割
# 定义一个字符串
line = '我来到北京清华大学'
# TODO:全模式分词
seg_list = jieba.cut(line, cut_all=True)
print(",".join(seg_list))
# 我,来到,北京,清华,清华大学,华大,大学
# TODO: 精确模式
seg_list_2 = jieba.cut(line, cut_all=False)
print(",".join(seg_list_2))
# 我,来到,北京,清华大学
# TODO: 搜索引擎模式
seg_list_3 = jieba.cut_for_search(line)
print(",".join(seg_list_3))
# 我,来到,北京,清华,华大,大学,清华大学
4、Spark高级特性
1、RDD持久化
某些RDD的计算或转换可能会比较耗费时间,如果这些RDD后续还会频繁的被使用到,可以将这些RDD进行持久化/缓存,这样下次再使用到的时候就不用再重新计算了,提高了程序运行的效率。
但由于内存的大小是有限的,所以应选择合适的缓存级别,常用MEMORY_AND_DISK 和MEMORY_AND_DISK2,和转换算子一样时lazy操作需要action算子触发,常用count()
缓存函数:
cache、persist
使用情景:某个RDD被多次使用、某个RDD计算困难
input_rdd = sc.textFile('../datas/words.txt', minPartitions=2)
# TODO: 可以直接将数据放到内存中
# input_rdd.cache()
# input_rdd.persist()
# TODO: 默认缓存级别MEMORY_ONLY
input_rdd.cache()
# TODO: 缓存RDD数据,设置缓存级别,先放内存不足放磁盘
input_rdd.persist(storageLevel=StorageLevel.MEMORY_AND_DISK)
# 使用count()算子触发
input_rdd.count()
# 当再次使用缓存数据时,直接从缓存读取
print(input_rdd.count())
# TODO: 当缓存RDD数据不在被使用时,一定记住需要释放资源
input_rdd.unpersist()
2、RDD checkpoint
将RDD保存至可靠储存系统如:HDFS,与持久化的区别时程序结束时不丢失;不保存RDD之间依赖关系
# TODO: step1、设置Checkpoint保存目录
sc.setCheckpointDir('../datas/ckpt')
input_rdd = sc.textFile('../datas/words.txt', minPartitions=2)
# TODO: step2、将RDD进行Checkpoint
input_rdd.checkpoint()
input_rdd.count()
# TODO: 当RDD进行Checkpoint以后,再次使用RDD数据时,直接从Checkpoint读取数据
print(input_rdd.count())
3、Spark 累加器
spark提供两种共享变量,广播变量和累加器
1)、广播变量Broadcast Variables:将变量在所有节点的内存之间进行共享,在每个机器上缓存一个只读的变量,而不是为机器上每个任务都生成一个副本。
2)累加器 Accamulators:支持在所有不同节点之间进行累加计算(比如技术或者求和)
Spark内置三种类型Accumulator:LongAccumulator
累加整数型,DoubleAccumulator
累加浮点型,CollectionAccumulator
累加集合元素,SparkCore提供接口,允许用户自定义累加器Accamulator
# TODO: 第1步、定义累加器
counter = sc.accumulator(0)
# 2. 加载数据源-source
input_rdd = sc.textFile('../datas/words.txt', minPartitions=2)
# 3. 数据转换处理-transformation
def map_func(line):
# TODO:第2步、使用累加器
counter.add(1)
return line
output_rdd = input_rdd.map(map_func)
# 4. 处理结果输出-sink
print(output_rdd.count())
# TODO: 第3步、获取累加器值
print("counter =", counter.value)
4、广播变量
广播变量(Broadcast Variables)允许开发人员在每个节点(Worker or Executor)缓存只读变量
,而不是在Task之间传递这些变量
。
# 字典信息
dic = {1: "北京", 2: "上海", 3: "深圳"}
# TODO:step1、将小表数据(变量)进行广播
broadcast_dic = sc.broadcast(dic)
# 2. 加载数据源-source
input_rdd = sc.parallelize([
("张三", 1), ("李四", 2), ("王五", 3), ("赵六", 1), ("田七", 2)
])
# 3. 数据转换处理-transformation
def map_func(tuple):
# TODO: step2、使用广播变量
city_dic = broadcast_dic.value
# 依据城市ID获取城市名称
city_id = tuple[1]
city_name = city_dic[city_id]
# 返回
return (tuple[0], city_name)
output_rdd = input_rdd.map(map_func)
四、Spark内核调度
1、spark任务调度
1、任务提交后会和ResourceManager通讯申请启动ApplicationMaster;
2、随后ResourceManager分配Container,在合适的NodeManager上启动ApplicationMaster,此时的ApplicationMaster就是Driver;
3、Driver启动后向ResourceManager申请Executor内存,ResourceManager接到ApplicationMaster的资源申请后会分配Container,然后在合适的NodeManager上启动Executor进程;
4、Executor进程启动后会向Driver反向注册;
5、Executor全部注册完成后Driver开始执行main函数,之后执行到Action算子时,触发一个job
6、(DAGScheduler)首先构建Job中RDD依赖对应DAG图,划分为Stage阶段:利用回溯法,RDD之间的依赖类型为宽依赖进行划分(Shuffle依赖)划分
7、(TaskScheduler)确定每个Stage阶段的任务数,依次调度到executor中执行,每个Stage中的Task数目=Stage中最后一个RDD的分区数目,shuffle阶段根据stage调度进行shuffle write和shuffleread。一个job中有多个stage,按照向后顺序执行stage中的task任务,只有前面的stage的task任务完成,才能执行后面的stage中task任务
2、spark RDD依赖
宽依赖:
1、shuffle依赖,父RDD一个分区数据给子RDD多个分区
2、产生shuffle,类似MR shuffle,如果子RDD的某个分区数据丢失,必须重构父RDD的所有分区
3、划分DAG图stage的依据
4、2个RDD之间依赖,使用S曲线有向箭头表示
窄依赖:
1、一个父RDD分区数据直给子RDD一个分区
2、都是转换算子,lazy执行
3、不产生Shuffle,如果子RDD的某个分区数据丢失,重构父RDD的对应分区
4、两个RDD之间用有向箭头表示
产生原因:
1、从数据血脉恢复角度来说:
如果宽依赖:
子RDD某个分区的数据丢失,必须重新计算整个父RDD的所有分区
如果窄依赖:
子RDD某个分区的数据丢失,只需要计算父RDD对应分区的数据即可
2、从性能的角度来考虑:
需要经过shuffle:使用宽依赖
不需要经过shuffle:使用窄依赖
3、spark shuffle
Shuffle过程:
-
Shuffle Write:
- Shuffle 的前半部分输出叫做 Shuffle Write,类似MapReduce Shuffle中Map Shuffle;
- 将处理数据先写入内存,后写入磁盘。
-
Shuffle Read:
- Shuffle 的后半部分输出叫做 Shuffle Read,类似MapReduce Shuffle中Reduce Shuffle;
- 拉取ShuffleWrite写入磁盘的数据,进行处理
Shuffle 机制:
-
第1种:SortShuffleWriter,普通机制
- 第1步、先将数据写入内存,达到一定大小,进行排序
- 第2步、再次写入内存缓冲,最后写入磁盘文件
- 第3步、最终合并一个文件和生成索引文件
-
第2种:BypassMergeSortShuffleWriter,bypass机制
- 当map端不用聚合,并且partition分区数目小于200时,采用该机制
- 第1步、直接将数据写入内存缓冲,再写入磁盘文件
- 第2步、最后合并一个文件和生成索引文件
-
第3种:UnsafeShuffleWriter,钨丝优化机制
- 当map端不用聚合,分区数目小于16777215,并且支持relocation序列化
- 第1步、利用Tungsten的内存作为缓存,将数据写入到缓存,达到一定大小写入磁盘
- 第2步、最后合并一个文件和生成索引文件
总结
spark核心与hadoop中的MR类似,又因为解决MR慢的问题使用了内存储存中间结果、线程并行运算,所以在程序调度过程与MR产生了区别,同时,弹性分布式数据集和dataframe的使用,极大降低了数据分析的开发过程。
时光如水,人生逆旅矣。