Spark的运行模式
local,standalone,yarn,mesos。yarn还分为yarn-client 和 yarn-master学习过程中主要用到local和yarn
Spark名词
Standalone模式下:
Master:集群中含有Master进程的节点。Master是整个集群的控制器,负责整个集群的正常运行。
Slave:集群中含有Worker进程的节点。Worker是工作节点,接收主节点的命令并进行状态汇报。
Yarn模式下:
ResourceManager:相当于Master,负责整个集群资源的调度。
NodeManager:相当于Worker,负责工作。
ApplicationMaster:相当于Driver。
Container:在NM上启动,负责执行任务。
SparkApplication:包含SparkContext和Executor。
Driver:在client提交Spark程序的时候产生的进程,运行Application中的main函数,初始化SparkContext。
SparkContext:整个Spark应用的上下文,控制应用的生命周期。
Executor:执行器,在NodeManager上执行任务的组件,启动线程池运行任务,每个Application都有一组的Executors。
RDD:Spark中的数据集。
Job:当程序碰到action算子的时候会提交一个job。job之前的操作都是延迟执行的。
Stage:每个job都会被切割成一个或者多个stage。
Task:RDD中的每一个分区都是一个task,task是RDD中最小的处理单元了。
TaskSet:一组task组成taskSet。每组TaskSet会被分发到各个节点上执行。
DAGScheduler:根据每个作业job,切分成一个个的stage。负责切分job
TaskScheduler:将任务taskset分发到每个节点上执行。
RDD
RDD的五大特性:
(1)抽象的概念,由一系列的partitions组成,partition是具体的概念,节点中某个磁盘上的数据集。
(2)算子可以作用在RDD上。比如map,filter。
(3)一个RDD可以转化成另一个RDD
(4)RDD可以重新定义分区。
(5)RDD遵循数据本地性的原则。
RDD的两种创建方式:
(1)从Hadoop文件系统(或者一些其他的持久化存储系统,Hive,HBase)等创建而来。
(2)从父RDD转化成子RDD
RDD的两种操作算子:
(1)Transformation:该算子是延迟执行的,只有遇到action算子的时候,才会触发执行。执行效果是从RDD转化为另一个RDD。
常见的算子:map,flatMap,filter,reduceByKey,mapValues,join,groupBy等。
(2)Action:该算子会触发Spark提交一个job,执行效果是从RDD转化为结果集。
常见的算子:count,collect,saveAsTextFile,top,reduce,top,fold,aggregate等。
注:
RDD在内存中都是瞬时转换的过程,如果没有做持久化的话,那么当需要前一个RDD多次执行的时候只能重新算,因为在第一次执行RDD的算子之后这个RDD已经不存在了。
val lines = sc.textFile(args(0), 5)
val maleHeight = lines.filter(line => line.contains("M")).map(line => line.split("\t")(1) + " " + line.split("\t")(2))
val femaleHeight = lines.filter(line => line.contains("F")).map(line => line.split("\t")(1) + " " + line.split("\t")(2))
femaleHeight中需要lines的过滤,这个时候需要重新去读文件。所以最好在lines做一个持久化的动作。
lines.persist() 和 lines.cache()
测试:
读取一个1000000行的数据,未持久化:需要22秒 持久化:需要13秒
读取一个10000000行的数据,未持久化:需要98秒 持久化:需要72秒
(未进行多次测试,数据有偏差)
尽量避免DiskIO,优化策略。
宽窄依赖
宽依赖:父RDD中的每个partition去向子RDD中的 多个partition。
窄依赖:父RDD中的每个partition去向子RDD中的一个partition。
DAGScheduler根据宽窄依赖将job切分成多个stage。
同一机器的内存上的瞬时状态),速度快。将一条直线封装成一个task,这样多个算子作用实际上只需要发一个task任务。
注:这也是Spark延迟执行的原因,将多个窄依赖封装在一个task中,只需要发一个task任务就行。task任务是Spark的最小计算单元。
Spark运行过程
1.Spark客户端提交程序到集群中的某一台机器上。
2.在机器上会通过反射的方式创建一个Driver进程,Driver进程运行Application中的main函数,初始化SparkContext,在初始化SparkContext过程中最重要的是构造出一个DAGScheduler和TaskScheduler。同时Driver进程会向集群中的主节点申请资源,Standalone模式下是Master,Yarn模式下是ResourceManager。
3.Master或者ResourceManager接收到Driver的请求后,会在Spark集群的Worker上为Driver启动多个Executor进程。并且在Executor启动之后会反向注册到TaskScheduler上。
4.所有的Executor都注册到Driver上之后,Driver结束SparkContext的初始化。
5.之后开始切分job,通过DAGScheduler和TaskScheduler将job分成一个个的TaskSet。
6.TaskScheduler会把TaskSet里面的每一个Task提交到集群中的没一个Executor中执行。
7.最后,整个Spark程序就是被切分为多个job,每个job切分为多个Stage,stage中的每个task分批次地被提交到集群中的Executor中执行。
注:1.对于每个Task都是从Driver端像Worker端发送的。
2.Driver端发送的数据都是不可更改了。比如val或者final。
集群中提交Spark应用程序
./bin/spark-submit --class org.apache.spark.examples.SparkPi --master yarn-cluster ./lib/spark-examples-1.6.0-hadoop2.6.0.jar 10
./bin/spark-submit --class com.b.MySpark --master yarn-client /root/myScala-1.0-SNAPSHOT.jar
--class:Spark主程序路径
--master:运行模式
jar包路径
注:
(1)如果是yarn-client 或者 yarn-cluster模式,那么整个集群的资源调度由yarn来执行,不需要启动spark中sbin目录下的start-all.sh命令,就是不需要Master进程。
(2)对于yarn-client模式:Driver运行在提交程序的机器上。
(3)对于yarn-cluster模式:Driver运行在集群中的某台机器上
(4)对于standalone模式:--master spark://node:7077
(5)一般来说如果在window下写spark程序,需要把程序打包到集群中运行。本地测试可以setMaster("local"),但是这个时候无法使用yarn提供的资源调度服务。测试的时候使用local方便测试。
一些算子操作
map:对每个数据进行操作。
collect:汇总数据到Driver进程所在的机器上,将RDD转化为数组,测试时候使用,正式开发慎用,可能造成内存泄露。
coalesce(数目,true)重新指定分区数,data.partitions.size。如果重分区数大于原来的分区数,必须指定shuffle为true。
repartition:coelesce函数中第二个参数为true的实现。
makeRDD:创建RDD
union:连接两个RDD,不去重。
intersection:返回两个RDD的交集,并且去重
subtract:返回在rdd1中出现,不在rdd2中出现的元素 相当于rdd1-rdd2
mapPartitions:适用于一个partition中数据不多的时候。一次以迭代器的形式返回一个partition中的所有数据
filter:过滤。返回值为true的保留。通常在filter之后会接上coalesce算子。将数据重新分区。防止产生数据倾斜。
zip:zip算子用于将两个rdd关联起来 rdd1.zip(rdd2).collect 输出Array((1,"a"),(2,"b"),(3,"c"))
mapValues:对每个以key, value形式的map中的value操作。rdd.mapValues(x => x + "_")
partitionBy:根据partitioner函数生成新的ShuffleRDD,将原来的RDD重新分区。通过新的分区策略将原来在不同分区的数据都合并到一个分区。
rdd1.partitionBy(new org.apache.spark.HashPartitioner(2))
flatMapValues:同基本转换操作中的flatMap,flatMapValues是针对[K,V] 中的V进行flatMap操作。
sample:抽样 三个参数
sc.parallelize(datas)
.sample(withReplacement = false, 0.5, System.currentTimeMillis)
.foreach(println)
groupByKey():对<key,value>结构的RDD进行类似RMDB的group by聚合操作,具有相同的key的RDD成员的value会被聚合在一起,返回的RDD的结构是(key,Iterator<value>)
reduceByKey:对<key,value>结构的RDD进行聚合,对具有相同Key的value调用func来进行reduce操作,func的类型必须是<value,value> => v 对相同key的value进行操作。rdd2.reduceByKey((a,b) => (a + b)/4).collect().foreach(println)
sortByKey:对<key, value>结构的RDD进行升序或者降序排列。两个参数
comp:排序时的比较运算方式。
ascending:false降序;true升序。
join操作:对<K, V>和<K, W>进行join操作,返回(K, (V, W))外连接函数为leftOuterJoin、rightOuterJoin和fullOuterJoin
cogroup:对多个RDD中的KV元素,每个RDD中相同key中的元素分别聚合成一个集合。与reduceByKey不同的是针对两个RDD中相同的key的元素进行合并。
cartesian:两个RDD进行笛卡尔积合并
action操作:
reduce:对RDD成员使用func进行reduce操作,func接收两个参数,合并之后返回一个值,reduce操作的返回结果只有一个值,需要注意的是,reduce会并发执行。
collect:将RDD读取到Driver所在的内存,类型是Array,一般要求RDD不要太大
count:返回RDD的成员数量
first:返回RDD的第一个成员。等价于take(1)
take(n):返回RDD的前n个成员。sc.parallelize( 1 to 10).take(2).foreach(println)
takeSample:和sample用法相同,第二个参数换成了个数,返回也不是RDD,而是collect
saveAsTextFile:将RDD转化为文本内容并保存到路径path下,可能有多个文件(和Partition数量有关),路径path可以是本地路径,也可以是HDFS地址,转换方法时对RDD成员调用toString函数
saveAsSequenceFile:与saveAsTextFile类似,但以SequenceFile格式保存,成员类型必须实现Writeable接口或可以被隐式转换为Writable类型(比如基本Scala类型Int、String等)
saveAsObjectFile:用于将RDD中的元素序列化成对象,存储到文件中。对于HDFS,默认采用SequenceFile保存。
countByKey:适用于<K,V>类型,对key计数,返回(K,Int)
println(sc.parallelize(Array(("A", 1), ("B", 6), ("A", 2), ("C", 1), ("A", 7), ("A", 8))) .countByKey())
Map(B -> 1, A -> 4, C -> 1)
glom:glom函数将每个分区形成一个数组,
union:返回的RDD的数据类型和被合并的RDD的元素数据类型一样,通过符号++相当于union操作
combineByKey:相当于将元素(Int,Int)的RDD转变为了(Int,Seq[Int])类型的RDD 。将 (V1,2), (V1,1)数据合并为( V1,Seq(2,1))。
reduceByKey:reduceByKey是比combineByKey更简单的一种情况,知识两个值合并成一个值将 (V1,2), (V1,1)数据合并为( V1,3)。
reduce:f:(A,B) => (A._1 + "@" + B._1,A._2 + B._2)A和B可以代表<key, value>形式的数据,而不仅仅是单个数据
fold:折叠
reduceByKye = groupByKey + reduce 。map端和reduce端执行的逻辑一样。
shuffle = map端 + reduce端
spark的reduceByKey在map端自带combine。可以用来计算sum。不能计算average。
aggregateByKey:当map端和reduce端执行逻辑不一样的时候,可以用aggregateByKey。
unit:不会发生shuffle。只是逻辑上的抽象。
distince:有shuffle动作。
countByKey:会产生shuffle。其他的ByKey都会产生shuffle,join也会 cogroup也会。
cogroup:join相当于把cogroup中的value值进行排列组合。
zip:rdd1=:a b c d e rdd2= 1 2 3 4 5 rdd1.zip(rdd2) (a,1)(b,2)...两个rdd的分区数必须相同。
Spark读取文件
HDFS数据源:val lines = sc.textFile("hdfs://node:9000/user/sc/*.txt")
本地数据源:val lines = sc.textFile("D://")
./bin/spark-submit \
--class com.ibm.spark.exercise.basic.SparkWordCount \
--master spark://hadoop036166:7077 \
--num-executors 3 \
--driver-memory 6g --executor-memory 2g \
--executor-cores 2 \
/home/fams/sparkexercise.jar \
hdfs://hadoop036166:9000/user/fams/*.txt
Spark的缓存策略
Memory_ONLY:默认的缓存策略,保存在内存中。
Memory_AND_DISK:尽量存在内存中,如果内存中存不下,那么就存到磁盘中。
区别:
1.都是对RDD的缓存
2.前者是往内存总存,存不下就不存,需要的话重新计算。
3.后者是先往内存中存,存不下,其余的落地到磁盘中。用到的时候需要从磁盘中读取。DISKIO
选择:主要看前一个的数据的中间结果是否值得保留,因为存到磁盘要进行磁盘的IO,如果计算量不是很大的话可以重新计算。
尽量使用MEMORY_ONLY,持久化需要时间,读取DISK也需要时间。不存中间结果。不占存取空间。
如果内存比较吃紧,选择MEMORY_ONLY_SER:序列化。
还可以使用checkpoint,所以基本上不会用到Memory_AND_DISK
RDD是在内存中瞬时转换的状态,不持久化的话RDD1直接变成RDD2,RDD1不存在了。
数据大概:一万行一mb。
Spark-env.sh
xport JAVA_HOME=JAVA_HOME
export SCALA_HOME=scala地址
export HADOOP_HOME=hadoop地址
export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop
export SPARK_MASTER_IP=node
export SPARK_WORKER_CORES=1 cpu资源
export SPARK_WORKER_MEMORY=512m 使用多大内存
export SPARK_WORKER_INSTANCES=1 每个节点有几个worker进程
export SPARK_HOME=spark地址
export PATH=$SPARK_HOME/bin:$PATH
export YARN_CONF_DIR=$HADOOP_HOME/etc/hadoop
Spark调优
spark调优:内存,cpu,磁盘,网络io,序列化机制。
Yarn会根据Spark作业设置的资源参数,在每个工作节点上,启动一定数量的Executor进程,每个进程都占有一定数量的CPU和内存。
Spark的资源参数调优:
num-executors:设置整个Spark作业总共需要多少个Executor进程来执行。如果不设置的话,系统只会启动少量的Executor进程。一般设置50-100个Executor进程。
executor-memory:设置每个Executor进程的内存。设置4G-8G比较合适,需要看集群的整体资源。
executor-cores:设置每个Executor进程的CPU core数量。决定了Executor进程并行执行task线程的能力。设置2-4个比较合适。
driver-memory:设置Driver进程的内存。一般1g左右就足够,但是如果代码中有collect操作的话,需要考虑内存溢出的情况。
spark.default.parallelism:设置每个stage默认的task数量。Spark根据底层HDFS的block数量来设置task的数量,默认是一个HDFS block对应一个task,这样的task数量是偏少的。如果task数量偏少的话,那么之前设置的那些资源可能得不到很好的利用。很多Executor进程可能空闲。根据Spark官网的设置原则,设置参数为num-executors*executor-cores的2-3倍。比如Executor的总cpu core的数量是300个那么设置1000个task是可以的。(其实就是看Executor总得占有的CPU core的数量的2-3倍)
spark.storage.memoryFraction 该参数用于设置RDD持久化数据在Executor内存中能占的比例,默认是0.6。
spark.shuffle.memoryFraction:该参数用于设置shuffle过程中一个task拉取到上个stage的task的输出后,进行聚合操作时能够使用的Executor内存的比例,默认是0.2。
./bin/spark-submit \
--master yarn-cluster \
--num-executors 100 \
--executor-memory 6G \
--executor-cores 4 \
--driver-memory 1G \
--conf spark.default.parallelism=1000 \
--conf spark.storage.memoryFraction=0.5 \
--conf spark.shuffle.memoryFraction=0.3 \
./bin/spark-submit \
--master yarn-cluster \
--num-executors 100 \
--executor-memory 6G \
--executor-cores 4 \
--driver-memory 1G \
--conf spark.default.parallelism=1000 \
--conf spark.storage.memoryFraction=0.5 \
--conf spark.shuffle.memoryFraction=0.3 \
优化:使用mapPartitions代替map
使用foreachPartitions代替foreach:这两个需要考虑内存溢出的问题。一次处理一个partition的数据
使用filter之后进行coalesce操作。
repartitionAndSortWithinPartitons代替repartition和sort:如果需要在repartition重分区之后,还要进行排序,建议直接使用repartitionAndSortWithinPartitions算子。因为该算子可以一边进行重分区的shuffle操作,一边进行排序。shuffle与sort两个操作同时进行,比先shuffle再sort来说,性能可能是要高的。
广播变量:广播后的变量,保证每个Executor的内存中,只有一个变量副本,每个Executor的task公用这个副本,这样可以大大减少副本的数量,减少网络的开销。
可能发生数据倾斜的算子:distinct、groupByKey、reduceByKey、aggregateByKey、join、cogroup、repartition等
解决数据倾斜:
1.hive预处理。
2.过滤少数的key
3.提高shuffle并行度
4.两阶段聚合(局部聚合 全局聚合)给每个key加上随机数,接着如果进行reduceByKey等聚合操作,不同的key分配到不同的task中,然后再进行全局聚合,得到最后结果。适用于聚合类的shuffle操作,不适合join类的shuffle操作。
5.扩容:采样倾斜key并拆分join操作:对于join导致的数据倾斜,如果只是某几个key导致了倾斜,可以将少数几个key拆分成独立RDD,并附加随机前缀打散成n份进行join。过滤出来那几个倾斜key对应的数据并形成一个单独的RDD,将每条数据膨胀成n条数据,这n条数据都按顺序附加一个0~n的前缀,不会导致倾斜的大部分key也形成另外一个RDD。
Spark中的共享变量
广播变量:不可修改,在sc中创建,发送到各个节点。
var broadcastVar = sc.broadcast(Array(1,2,3))
broadcastVar.value
累加器:在从节点中不可读,只能进行累加。
val accum = sc.accumulator(0,"My Accumulator")
accum.value
容错性保证
Spark通过lineage和Checkpoint机制进行容错性保证。
优化
数据倾斜问题:Map端和Reduce端,尽量让shuffle在map端。前一个rdd的输出就是后一个rdd的输入。尽量让shuffle在rdd内,先在rdd内shuffle,再多个rdd进行shuffle。例如reduceByKey和groupByKey。reduceByKey更好,先在每个rdd中就进行shuffle动作。
问题
1.DAG图太长,导致内存溢出
page rank : 第一个action打印输出a, 第二个action打印输出b,如果没有缓存第一个action,那么执行第二个action的时候也会打印a,因为第二个action是基于第一个action操作的。所以要在第一个action操作后增加缓存。比缓存更好的是checkpoint()
在执行算子map操作的时候,如果不是一个简单的匿名函数表达式,则应该用{},不是()
如果不进行checkpoint的话可能DAG图太长了,导致栈溢出。
object SparkPageRank {
def main(args: Array[String]) {
val conf = new SparkConf().setAppName("PageRank").setMaster("local[2]")
val item = 200
val sc = new SparkContext(conf)
val lines = sc.textFile("D:\\MYFile\\IDEAFiles\\MyScala\\src\\main\\resource\\page_rank.txt")
sc.setCheckpointDir(".")
//根据边关系生成(1,(2,3,4,5)), (2,(1,4)), (3,(2,4))
val links = lines.map{
s => val pairs = s.split("\\s+")
(pairs(0), pairs(1))
}.distinct().groupByKey().cache()
//生成(1,1.0) (2,1.0)..
var ranks = links.mapValues(v => 1.0)
for(i <- 1 to item) {
//join操作:(1,((2,3,4,5),1.0))
val contribs = links.join(ranks).values.flatMap{
case (urls, rank) =>
val size = urls.size
urls.map(urls => (urls, rank/size))
}
ranks= contribs.reduceByKey(_ + _).mapValues(0.15 + 0.85 * _)
//如果不进行checkpoint的话DAG太长了
ranks.checkpoint()
}
ranks.foreach(tup => println(tup._1 + "has rank : " + tup._2 + "."))
}
}
注:目录下的checkpoint文件只有一个,并没有200个,Spark会自动把之前没用的rdd删除,更加节省磁盘空间。
2.Yarn资源管理页面跑了两次ApplicationMasters
说明第一次没执行成功,App被再次提交。yarn默认的尝试次数为2.
3.数据倾斜
Map端和Reduce端,尽量让shuffle在map端。前一个rdd的输出就是后一个rdd的输入。尽量让shuffle在rdd内,先在rdd内shuffle,再多个rdd进行shuffle。例如reduceByKey和groupByKey。reduceByKey更好,先在每个rdd中就进行shuffle动作。join的时候容易发生数据倾斜。
对于两个RDD数据量都很大且倾斜的Key特别多如何解决:数据扩容
数据集RDD1和RDD2中存在数据量很大且倾斜的key,目的RDD1.join(RDD2):
RDD1中的key加前缀Random(10)生成rdd11,RDD2扩容10倍生成rdd22。rdd11.join(rdd22)
伪代码:
val rdd11 = RDD1.flatMap(key => Random(10)_key)
val rdd22 = RDD2.flatMap
{key =>
for(i <- 0 to 9) {
i_key
}
}
(未完)
./bin/spark-submit \
--master yarn-cluster \
--num-executors 100 \
--executor-memory 6G \
--executor-cores 4 \
--driver-memory 1G \
--conf spark.default.parallelism=1000 \
--conf spark.storage.memoryFraction=0.5 \
--conf spark.shuffle.memoryFraction=0.3 \