一、Spark运行原理
Spark-submit提交一个Spark作业之后,这个作业就会启动一个对应的Driver进程。根据部署模式(deploy-mode)不同,Driver进程可能在本地启动,也可能在集群中某个工作节点上启动(参见 client和cluster的区别)。Driver进程最开始会向集群管理器(可以是Spark Standalone集群,也可以是其他的资源管理集群,比如YARN资源管理集群)申请运行Spark作业需要使用的资源,这里的资源指的就是Executor进程。集群管理器会根据Spark作业提交的资源参数,在各个工作节点上,启动一定数量的Executor进程,每个Executor进程都占有一定数量的内存和CPU core。
在申请到了作业所需的资源之后,Driver进程就会开始调度和执行我们的作业代码了。Driver进程会将我们编写的Spark作业代码分拆为多个job,Spark会对每个job检查 RDD 的DAG再计算出执行 plan (plan 以最远端的 RDD 为起点(最远端指的是对外没有依赖的 RDD 或者 数据已经缓存下来的 RDD),产生结果 RDD 的 action 为结束),每个job由action(比如count/write/take/collect)触发,每个job的执行plan由多个stage组成,每个stage执行一部分代码片段,并为每个stage创建一批task,然后将这些task分配到各个Executor进程中执行。task是最小的计算单元,负责执行一模一样的计算逻辑(也就是我们自己编写的某个代码片段),只是每个task处理的数据不同而已。一个stage的所有task都执行完毕之后,会在各个节点本地的磁盘文件中写入计算中间结果,然后Driver就会调度运行下一个stage。下一个stage的task的输入数据就是上一个stage输出的中间结果。如此循环往复,直到将我们编写的代码逻辑全部执行完,并且计算完所有的数据得到结果。
Spark是根据shuffle类算子来进行stage的划分。如果我们的代码中执行了某个shuffle类算子(比如reduceByKey、join等),那么就会在该算子处,划分出一个stage界限来。可以大致理解为,shuffle算子执行之前的代码会被划分为一个stage,shuffle算子执行以及之后的代码会被划分为下一个stage。因此一个stage刚开始执行的时候,它的每个task可能都会从上一个stage的task所在的节点,去通过网络传输拉取需要自己处理的所有key,然后对拉取到的所有相同的key使用我们自己编写的算子函数执行聚合操作(比如join算子接收的函数)。这个过程就是shuffle。
当我们在代码中执行了cache/persist等持久化操作时,根据我们选择的持久化级别的不同,每个task计算出来的数据也会保存到Executor进程的内存或者所在节点的磁盘文件中。
因此Executor的内存主要分为三块:第一块是让task执行我们自己编写的代码时使用,默认是占Executor总内存的20%;第二块是让task通过shuffle过程拉取了上一个stage的task的输出后,进行聚合等操作时使用,默认也是占Executor总内存的20%;第三块是让RDD持久化时使用,默认占Executor总内存的60%。
task的执行速度是跟每个Executor进程的CPU core数量有直接关系的。一个CPU core同一时间只能执行一个线程。而每个Executor进程上分配到的多个task,都是以每个task一条线程的方式,多线程并发运行的。如果CPU core数量比较充足,而且分配到的task数量比较合理,那么通常来说,可以比较快速和高效地执行完这些task线程。
以上就是Spark作业的基本运行原理的说明,大家可以结合上图来理解。理解Spark作业基本原理,是我们进行资源参数调优的基本前提。
例子:spark执行参数
nohup spark-submit --class com.ad.feature.featureengineering.Application \
--master yarn --deploy-mode client \
--driver-memory 4G --executor-memory 10G \
--num-executors 200 --executor-cores 4 \
--conf spark.sql.shuffle.partitions=2000 \
--conf spark.default.parallelism=2000 \
--conf spark.shuffle.memoryFraction=0.5 \
--conf spark.storage.memoryFraction=0.4 \
--conf spark.driver.maxResultSize=4g \
--conf spark.yarn.executor.memoryOverhead=4096 \
--conf spark.network.timeout=1800s \
--conf spark.shuffle.service.enabled=true \
--conf spark.speculation=true \
--conf spark.speculation.multiplier=2 \
--conf spark.speculation.quantile=0.9 \
--conf spark.speculation.interval=1000ms \
jars/feature-model-1.0-SNAPSHOT-jar-with-dependencies.jar 2> logs/test.log 1>&2 &
上述例子中,Driver进程(client模式在Master节点资源,4G)会根据提交Spark作业的参数先向YARN申请资源,YARN会根据参数分配多个Work节点资源(10Gx200),Driver进程会在分配到的节点资源上启动Executor进程,然后Driver进程将Jar包通过Netty HTTP分发到Executor,给Executor分配任务(将代码中每个stage的task分发到Executor进程执行),其余具体spark参数含义参见 Spark常用参数解释及建议值
二、Spark调优
1. Spark 开发调优
-避免创建重复的RDD, 多复用同一个RDD, 多次使用的RDD进行持久化
-少使用shuffle类算子, 使用map-side预聚合的shuffle操作, 使用高性能的算子
-广播大变量
-使用Kryo优化序列化性能 (未使用过)
-优化数据结构(使用字符串替代对象,使用原始类型(比如Int/Long)替代字符串,使用数组替代集合类型)
2. Spark 资源调优
根据数据的计算设置适当的任务参数,常见参数:num-executors,executor-memory,executor-cores,spark.default.parallelism,spark.storage.memoryFraction,spark.shuffle.memoryFraction,driver-memory,具体参加 Spark常用参数解释及建议值
3. 数据倾斜调优
数据倾斜的原理:在进行shuffle的时候,必须将各个节点上相同的key拉取到某个节点上的一个task来进行处理,比如按照key进行聚合或join等操作。此时如果某个key对应的数据量特别大的话,就会发生数据倾斜。比如大部分key对应10条数据,但是个别key却对应了100万条数据,那么大部分task可能就只会分配到10条数据,然后1秒钟就运行完了;但是个别task可能分配到了100万数据,要运行一两个小时。因此,整个Spark作业的运行进度是由运行时间最长的那个task决定的。
首先找到数据倾斜的stage对应的代码,然后针对倾斜的key作处理。
-将数据倾斜转移到HiveETL中预处理数据中
-过滤少数导致倾斜的key
-提高shuffle操作的并行度,调大 spark.sql.shuffle.partitions,不过效果有限
-将key分散后再聚合处理,把(hello, 1) (hello, 1) (hello, 1) (world, 1)将key加随机数变多变成((1,hello), 1) ((1, hello), 1)((3, hello), 1)((1, world), 1) ,再聚合得到((1, hello), 2)((3, hello), 1)((1, world), 1) ,再去掉key中的随机数聚合得到(hello, 3)(world, 1) 。不过仅适用于聚合shuffle
-将reduce join转为map join
-分散倾斜key并分拆join操作,若join两方数据都过大无法进行map join,数据将join两方的数据中倾斜的key及数据从数据中分离,将倾斜key加入随机数然后join,将未倾斜key直接进行join,两个jion后的结果union起来
-使用随机前缀和扩容RDD进行join,将join两方的key都插入随机数扩容N倍后进行join
-改用类似倒排的方式换key
4. shuffle调优
没用过,参见 Spark性能优化:shuffle调优
Reference: