1 spark性能调优
常规性能调节
1.1 分配资源
资源包括:executor、cpu per executor、memory per executor、drivermemory
提交作业的时候采取如下方式
/usr/local/spark/bin/spark-submit\
--class cn.spark.sparktest.core.WordCountCluster \
--num-executors 3 \ 配置executor的数量
--driver-memory 100m \ 配置driver的内存(影响不大)
--executor-memory 100m \ 配置每个executor的内存大小
--executor-cores 3 \ 配置每个executor的cpu core数量
/path/to/SparkTest-0.0.1-SNAPSHOT-jar-with-dependencies.jar\
分配资源的一个原则:你能使用的资源有多大,就调节到多大
①增加executor
增加executor的数量之后,意味着能够并行执行的task数量也就增大
②增加executor per core
增加每个executor的cpu core,也意味着能够并行执行的task数量也就增大
③增加memory per executor
增加每个executor的内存,可以缓存更多RDD,将更少的数据写入磁盘甚至不写,减少磁盘IO
增加每个executor的内存,对于shuffle操作,reduce端需要内存存放拉取的数据,如果内存不够,将存入磁盘
对于task的执行,会创建很多对象,如果内存过小,会频繁导致JVM堆内存满溢,频繁GC(minor gc、full gc)
1.2 调节并行度
并行度:spark作业中,各个stage的task数量,也就代表了spark作业在各个阶段(stage)的并行度。
当你的资源充足时,例如有100个executor,每个executor有2个cpu core,每个executor 的内存10G,但是,你现在只有150个task,平均分配之后,还剩下20个cpu core处于空闲状态。
task的数量推荐设置:总cpu core数量的2-3倍,因为实际运行情况,一些task运行速度较快,另外一些task运行速度较慢,如果你的task数量设置刚好与cpu core一致,运行快的task所在节点资源处于空闲状态。
SparkConfconf = new SparkConf().set("spark.default.parallelism", "500")
1.3 抽取公共的RDD
默认情况下,多次对一个RDD执行算子,会对这个RDD以及之前的父RDD重新计算一次(这种情况要避免)持久化
从一个RDD到几个不用的RDD,算子和逻辑都一致,因为人为的疏忽,导致计算多次(这种情况要避免)抽取一个大的共用RDD
持久化,是可以进行序列化的,如果正常将数据持久化到内存了能会导致内存满溢,这个时候可以将数据序列化,序列化唯一缺点,获取数据需要反序列化,消耗一定的性能;持久化还支持双副本的形式(这种方式仅仅在你的内存资源足够充分的情况下使用)。
公共的RDD一定要持久化,避免重读计算。
1.4 广播大变量
默认情况下,task执行的算子中,使用了外部变量,每个task都会获取一份变量的副本,比如map,本身数据结构就占用很多内存,有1000个task,这种情况下,map会拷贝1000份,通过网络传输到各个task中,网络传输就会消耗你的spark作业性能;map副本,传输到各个task中会占用内存,不必要的内存消耗和占用可能会引起JVM GC,可能会引起RDD持久化到磁盘。
将大变量广播出去,不是每一个task一个副本,而是每一个executor一个副本
广播变量初始的时候,只有在driver上有一份副本,task运行时,要想使用广播变量中的数据,首先会在自己本地的executor对应的BlockManager中,尝试获取变量值,如果本地没有,就会从Driver远程拉取变量副本,并保存在本地BlockManager中;此后,该节点上的task都会从本地BlockManager中获取广播变量的值。
BlockManager负责管理对应自己的executor上的内存和磁盘上的数据,它可能会从Driver上面去获取变量副本值,也有可能从距离较近的其它节点上拉取数据。
1.5 使用kyro序列化
set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
SparkConf.registerKryoClasses()
默认情况下,spark内部使用的是java序列化机制,ObjectOutputStream/ ObjectInputStream这种序列化机制处理比较方便,不需要我们手动去做什么事情,但是序列化机制效率不高,占用内存空间相对较大
使用kyro序列化机制,速度要快,占用内存较少。
使用kyro序列化机制之后,生效的地方有以下几个几点:
①算子函数中使用到的外部变量
②持久化RDD是进行序列化
StorageLevel.MEMORY_ONLY_SER
③shuffle过程,节点与节点之间的task会互相进行大量的网络拉取和传输文件,这些数据既然是网络传输,也会进行序列化,序列化之后数据量少,也会提升网络传输的性能1.6 数据本地化等待时长
spark将一个application切分成多个基于stage的task分配到executor节点上时,会计算出每个task要计算的哪个分片数据,Spark的task分配算法,会优先将每个task分配到它要计算的数据所在的节点,这样的话,就不需要数据的网络传输了;但是,有些时候,task没有机会分配到数据所在的节点,因为那个节点的计算资源和计算能力都满了,通常在这个时候,spark会等待一段时间,默认是3s,如果在等待一段时间后,还是不行,就选择一种较差的本地化级别将task分配其它节点上。
spark提供了以下几种数据本地化级别
①PROCESS_LOCAL:进程本地化,代码和数据在同一个进程中,也就是说在同一个executor中,数据在executor的BlockManager中,性能最好;
②NODE_LOCAL:节点本地化,代码和数据在同一个节点中,即数据和task在一个节点的不同executor中,数据需要进程间通信;
③NO_PREF : 对于task,在哪个节点上获取数据都一样,比如从mysql中获取数据源
④RACK_LOCAL:机架本地化,数据和task处于同一个机架的不用节点上,数据需要网络传输
⑤ANY : 数据和task不在同一个机架上,性能最差
对于应用来说,最好的,当然是进程本地化,直接从本地executor的BlockManager中获取数据,纯内存,或者少量IO操作;如果需要通过网络传输数据的话,性能肯定会下降。
通过调节spark.locality.wait这个参数,可以调节数据本地化等待时长,默认是3s
查看spark的日记,starting task ...... PROCESS_LOCAL,就不需要调节该参数,如果是其它本地化数据等待策略,可以考虑调节该值。
newSparkConf().set("spark.locality.wait", "10")
2 JVM调优
该调优对spark性能提升并没有太大影响,不过对线上spark的OOM异常有直接影响
Spark在一个Executor中的内存分为三块,一块是execution内存,一块是storage内存,一块是other内存。
2.1 降低cache内存占比
spark.storage.memoryFraction :该参数用于分配Executor内存中storage内存占比;
我们在执行spark任务的时候,创建的对象都存放在Eden区域;当Eden区域满溢后(spark运行是产生的对象及其多),就会触发minor gc,把不在使用的对象从内存中删除,存活下来的对象放入Survivor(From)区域,但是如果存活下来的对象占比为1.5,一个Survivor区域放不下,此时会将对象直接存放到老年代中;
在minor gc开始的时候,对象只会存在Eden和Survivor(From)区域,Survivor(To)区域是空的;紧接着进行minor gc,Eden区域中存活的对象会被复制到Survivor(To)区域,而Survivor(From)区域中存活下来的对象,会根据他们的年龄决定去向,年龄达到阈值后将会存入到老年代中,没有达到阈值的对象将会被复制到Survivor(To)区域;经过这次gc之后,Eden区域和From区域已经被清空,这个时候from区域和to区域交换角色,新的from就是上一次的to,不管怎么样,都会保证to区域是空闲的。minor gc一直会重复这个过程。
如果你的堆内存较小,可能会导致频繁minor gc,会导致短时间内,有些短生命周期对象,年龄过大,进入老年代中。
老年代中可能会因为堆积一堆短生命周期对象而导致内存不足,进行full gc,该过程很慢,很消费性能;
不管是minorgc,还是full gc,都会导致stop the world.
而spark堆内存又被划分成两块,一个用于RDD的cache、persist操作缓存数据使用,另外一部分用来给spark算子函数使用,存放算子中创建的对象等;默认情况下,RDD cache的占比为0.6。可以适当调整该比例。
2.2 堆外内存
spark引入堆外内存,使之可以直接在节点上的系统内存中开辟空间,存储经过序列化之后的数据。即spark可以直接操作系统的堆外内存,相比堆内内存,堆外内存不受JVM管理,也就没有gc操作,一定程度上提升了性能。
如果spark作业运行时不时的报错,shufflefile cannot find,executor、tasklost,outof memory(内存溢出);有可能是因为堆外内存不够用导致executor挂掉,对应的BlockManager也就不存在了,远程通信拉取不到数据,spark任务崩溃;可以适当提高该参数。
--confspark.yarn.executor.memoryOverhead=2048
2.3 连接等待时长
spark的task会优先在同一个节点的executor中的BlockManager中获取计算数据,但如果此时数据不在同一个节点上,就需要去网络传输拉取数据,但如果那个节点正在进行gc操作,就会没有响应,application卡住,spark默认超时时间为60s,超过这个时间,spark任务宣告失败。
--conf spark.core.connection.ack.wait.timeout=300
3 shuffle调优
spark在执行groupbykey、reducebykey等操作时,shuffle环节的调优很关键,shuffle对spark的作业性能影响很高。
shuffle分为shuffle write与shuffle fetch
shuffle write: executor的map端,第一批并行任务的每个task会在map端输出一定数量(与下一个stage的task数量相等)的文件,下一批并行任务的每一个task不会在输出文件,而是复用第一批task创建出的文件。而map端在写数据的时候,会将数据写入内存缓冲区中,当内存缓冲区满溢之后,在spill写入到磁盘落地。(优化后的HashShuffleManager)
shuffle fetch : executor的reduce端,每一个task会从map端的输出文件拉取属于自己的文件并进行聚合运算,计算数据也是先在执行内存(堆内或堆外),该区域满溢,才将reduce结果刷盘。
3.1 合并map端输出文件
基本上spark的性能都消耗在shuflle,而map端输出文件越多,影响性能越大
默认情况下不开启map端文件合并的,这个时候map端输出文件个数为M*R,可以通过参数将该功能开启
map端磁盘文件IO减少,reduce端拉取文件数量也减少,可以减少网络传输。
newSparkConf().set("spark.shuffle.consolidateFiles", "true")
3.2 map端缓冲区与reduce端执行内存占比
默认map端缓冲区的大小为32k,如果不够大,缓冲区内存满溢之后,会进行数据刷盘,多次数据刷盘会导致性能降低,可以适当调大该参数,减少刷盘IO次数
默认reduce端的executor的内存的0.2划分给shuffle聚合计算,该区域是执行内存,如果内存占比小,刷盘次数也较高,可以适当调大该参数,减少刷盘IO次数
map端内存缓冲 spark.shuffle.file.buffer 32
reduce端执行内存占比 spark.shuffle.memoryFraction 0.2
3.3 HashShuffleManager与SortShuffleManager
不同点:SortShuffleManager会对每个reduce task处理的数据进行排序(默认)
SortShuffleManager一个task,只会写入一个磁盘文件,不同task的数据,使用offset来划分界定。
SortShuffleManager的运行机制有两种:普通运行机制和bypass机制
①普通运行机制:数据会先写入到内存中,当达到阈值后,会将内存中的数据按照ket进行排序,排序之后将数据溢写到磁盘并清空内存中数据;一个task将数据写入磁盘,会产生多个临时文件,最后会将所有的临时文件merge为一个文件;
②bypass机制:当shuffle map task的数量小于spark.shuffle.sort.bypassMergeThreshold对应的阈值时,将触发bypass机制,该过程与HashShuffleManager过程一致,只不过在最后会merge归并。而该机制与普通SortShuffleManager运行机制的不同在于:不会排序,会减少一部分性能开销。
③总结:需不需要进行数据排序,如果不需要建议使用HashShuffleManager;当你的数据需要排序,将使用SortShuffleManager,一定要让你的redude task大于200,否则sort、merge不生效,一定要考量一下,这个过程很耗性能;如果你不需要排序,但希望一个task的文件合并为一个,就使用bypass机制。
newSparkConf().set("spark.shuffle.manager", "hash")
4 算子调优
4.1 map与mapPartitions
普通的map算子一次只计算一条数据,而mapPartitions一次处理一个分区的数据
普通的map操作不会导致OOM,而mapPartitions可能会出现OOM
将一些普通map错做转换为mapPartitions
4.2 filter之后数据平衡
数据经过了filter算子之后,RDD中的每个partition数据量可能就不太一样了
①每个partition数据量变少了,但是在后面进行处理的task处理不变,有点浪费资源
②每个partition的数据量不一样,会导致后面进行处理的时候,每个task处理的数据量不同,会导致数据倾斜
③解决办法: 针对filter之后的RDD调用方法coalesce(int numPartitions) ,减少partition的数量
4.3 foreach与foreachPartition
使用foreach,会对每一条数据进行一次算子操作,比如将数据写入数据库中,每执行一条数据,就要创建一个数据库连接
而使用foreachPartition之后,会将一个partition数据进行一次算子操作
4.4 sparkSQL的并行度
设置了spark.default.parallelism,使用sparkSQL进行数据源读取的时候,不会有效;sparkSQL默认根据Hive表或者HBase中数据对应的HDFS上文件block,自动设备sparkSQL的并行度,自己定义的并行度之后在没有sparkSQL的stage中生效。
这个就产生了一个问题?你的第一个stage的并行度只有20个task,每个task处理的数据量太大而导致spark运行缓慢。
可以对sparkSQL生成的RDD进行重新分区,例如dataFrame.javaRDD().repartition(1000)
4.5 reducebykey本地聚合使用了reducebykey算子,会在map端进行本地聚合之后,再将数据拉取到reduce端进行聚合,这样的好处:
①本地聚合之后,map端的数据量减少,减少磁盘IO,减少磁盘占用空间
②下一个stage拉取数据量也变少,减少网络传输性能消耗
③在reduce端进行数据缓存内存占用变少
④reduce端要进行聚合的数据也减少
5 报错解决方案
5.1 reduce端缓冲导致OOM
map端的task是不断的输出数据,数据量可能很大,reduce端的task并不是等到map端task将属于自己的那份数据全部写入磁盘之后才去拉取。而是map端写一点数据,reduce端task就会去拉取一小部分数据,之后立即进行聚合、算子函数的执行。
每次reduce端拉取数据的大小由reduce端缓冲区buffer决定,拉取过来的数据首先放在buffer区,然后才用executor分配的执行内存去进行后续聚合计算。
spark.reducer.maxSizeInFlight 48,可以将该参数调小一点,这样每次拉取的数据量少,就不会导致OOM
但是如果map端的输出的数据量不是特别大,可以将该参数调大一点,可以减少reduce网络传输次数,减少reduce端聚合操作次数(资源足够的情况下)
5.2 shuffle拉取文件失败
当map端的JVM进程正在执行minor gc/full gc,就会导致executor进程内的工作线程全部停止,而此时reduce端的task去拉取数据时,等待一段时间后,没有拉取到,可能就会报出:shuffle file not found.
spark.shuffle.io.maxRetries 3 shuffle拉取数据失败,最多重试几次(默认3次)
spark.shuffle.io.retryWait 5 每一次重试拉取数据的时间间隔(默认是5s)
默认情况下,reduce端的task拉取数据,会反复重试3次,每次时间间隔为5s,也就是会等待15s,就会报出shuffle file not found.
5.3 yarn-client模式导致网卡流量激增
spark-submit提交作业后, driver进程在本地启动之后,将向ResourceManager申请启动ApplicationMaster,ResourceManager选择其中一个NodeManager节点启动ApplicationMaster进程,ApplicationMaster向ResourceManager申请资源,在NodeManager上启动executor进程,executor反向注册到driver,driver接收到属于自己的executor进程之后,就可以进行task作业的分配与调度进行计算。
在yarn-client模式下,资源的申请(ApplicationManager)与作业的调度(driver)是分离的。driver是在本地启动,负责任务的调度,需要跟yarn集群中运行的executor进行频繁通信,此时你的本地机器可能会引起网卡流量激增。
改用yarn-cluster模式。
5.4 yarn-cluster模式JVM内存溢出
spark-submit提交作业后,将向ResourceManager申请启动ApplicationMaster,ResourceManager选择其中一个NodeManager节点启动ApplicationMaster进程,Driver进程在ApplicationMaster进程中启动,之后ApplicationMaster向ResourceManager申请资源,在NodeManager上启动executor进程,executor进程向ApplicationMaster进行反向注册,之后ApplicationMaster进程进行任务的分配与调度进行计算。
在yarn-cluster模式下,资源的申请(ApplicationManager)与作业的调度(driver)是在一个节点上的。性能上较yarn-client模式佳。
driver进程运行在JVM的PermGen(方法区)中,在yarn-client模式下,会默认配置128M,如果在yarn-cluster模式下,默认就是82M,可能会PermGen out of Memory error。
在spark-submit 脚本提交时添加 --conf spark.driver.extraJavaOptions="-XX:PermSize=128M-XX:MaxPermSize=256M"
而在spark sql中,如果有很多or语句等,会报出jvm stack overflow,JVM栈内存溢出,建议不要搞这么复杂的sql语句,将复杂的sql语句拆分开进行运算
持久化RDD大部分正常,但是有时候会出现内存中某一部分数据丢失,或者存储在磁盘上的文件丢失,这个时候就会出现问题;这种情况下就可以选择对这个RDD进行checkpoint操作,进行checkpoint操作,会将RDD的数据,持久化一份到指定的文件系统上(HDFS)
6 数据倾斜
同一个stage中各个task处理的数据差距较大,比如其中一个task处理的数据有100万,另外一个task处理1万条数据,造成数据倾斜。
出现数据倾斜的原因只可能发生了shuffle操作,因为某个key,或者某些key对应的数据量远远高于其他的key。
6.1 聚合源数据
spark的源数据来源于hive(HDFS)表,而这些hive表中的数据一般也是经过hive etl生成的。如果hive etl生成的数据倾斜,可以直接在etl过程中,对数据进行聚合,比如按照key进行分组,并将value采用一种特殊的格式拼接到一个字符串中。这样一个key就对应一条数据,在spark作业中就不需要对key进行groupbykey操作了。可能就不需要shuffle操作了,也就没有数据倾斜问题。
你可能没有办法就一个key的数据聚合成一条数据,可以将聚合粒度放粗,比如数据中包括city字段,可以先按照city进行聚合,将原先的数据量减少,减轻数据倾斜的现象和问题。
6.2 过滤导致倾斜的key
如果你能够接受某些数据直接丢弃,就可以将数据倾斜的key直接在spark SQL中使用where条件过滤掉
6.3 shuffle reduce端并行度
提高shuffle reduce端task数量,就可以让每一个task计算的数据量变少,可能缓解甚至基本解决掉数据倾斜。
在进行shuffle算子的时候,后面在传一个参数,该参数代表task的数量。这个没有从根本上解决数据倾斜,只是尽可能缓解数据倾斜问题
6.4 随机key实现双重聚合
将原始数据的key加上一个10以内的随机数作为前缀,将数据按照新的key进行聚合计算,之后在将结果中的key的前缀去掉,再次进行聚合计算。这样做的目的就是将原来一样的key转换成不一样的key。
使用场景:groupbykey、reducebykey
/**
* 第一步,给每个key打上一个随机数
*/
JavaPairRDD mappedClickCategoryIdRDD = clickCategoryIdRDD.mapToPair(
new PairFunction, String, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2 call(Tuple2 tuple)
throws Exception {
Random random = new Random();
int prefix = random.nextInt(10);
return new Tuple2(prefix + "_" + tuple._1, tuple._2);
}
});
/**
* 第二步,执行第一轮局部聚合
*/
JavaPairRDD firstAggrRDD = mappedClickCategoryIdRDD.reduceByKey(
new Function2() {
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2) throws Exception {
return v1 + v2;
}
});
/**
* 第三步,去除掉每个key的前缀
*/
JavaPairRDD restoredRDD = firstAggrRDD.mapToPair(
new PairFunction, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2 call(Tuple2 tuple)
throws Exception {
long categoryId = Long.valueOf(tuple._1.split("_")[1]);
return new Tuple2(categoryId, tuple._2);
}
});
/**
* 第四步,第二轮全局的聚合
*/
JavaPairRDD clickCategoryId2CountRDD = restoredRDD.reduceByKey(
new Function2() {
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2) throws Exception {
return v1 + v2;
}
});
6.5 将reduce join转换为map join
使用场景:join
将其中一个RDD的作为广播变量,之后在mapPartitions的时候,手工进行join。未走shuffle,就不会导致数据倾斜。
/**
* reduce join转换为map join
*/
List> userInfos = userid2InfoRDD.collect();
final Broadcast>> userInfosBroadcast = sc.broadcast(userInfos);
JavaPairRDD sessionid2FullAggrInfoRDD = userid2PartAggrInfoRDD.mapToPair(
new PairFunction, String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2 call(Tuple2 tuple)
throws Exception {
// 得到用户信息map
List> userInfos = userInfosBroadcast.value();
Map userInfoMap = new HashMap();
for(Tuple2 userInfo : userInfos) {
userInfoMap.put(userInfo._1, userInfo._2);
}
// 获取到当前用户对应的信息
String partAggrInfo = tuple._2;
Row userInfoRow = userInfoMap.get(tuple._1);
String sessionid = StringUtils.getFieldFromConcatString(
partAggrInfo, "\\|", Constants.FIELD_SESSION_ID);
int age = userInfoRow.getInt(3);
String professional = userInfoRow.getString(4);
String city = userInfoRow.getString(5);
String sex = userInfoRow.getString(6);
String fullAggrInfo = partAggrInfo + "|"
+ Constants.FIELD_AGE + "=" + age + "|"
+ Constants.FIELD_PROFESSIONAL + "=" + professional + "|"
+ Constants.FIELD_CITY + "=" + city + "|"
+ Constants.FIELD_SEX + "=" + sex;
return new Tuple2(sessionid, fullAggrInfo);
}
});
6.6 sample采样将数据倾斜的key单独处理
通过数据采样,将出现次数最多的一个或者几个key的数据单独抽取出来,原来的一个RDD就变成两个RDD,这两个RDD在分别进行join操作,join操作之后生成两个RDD,在将这两个RDD进行join合并为最终RDD。如下图:
使用场景:优先使用6.5节;如果两个RDD中数据差不多,就使用6.6节。案例代码如下:
不适用场景:导致数据倾斜的RDD的key特别多。
JavaPairRDD sampledRDD = userid2PartAggrInfoRDD.sample(false, 0.1, 9);
JavaPairRDD mappedSampledRDD = sampledRDD.mapToPair(
new PairFunction, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2 call(Tuple2 tuple)
throws Exception {
return new Tuple2(tuple._1, 1L);
}
});
JavaPairRDD computedSampledRDD = mappedSampledRDD.reduceByKey(
new Function2() {
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2) throws Exception {
return v1 + v2;
}
});
JavaPairRDD reversedSampledRDD = computedSampledRDD.mapToPair(
new PairFunction, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2 call(Tuple2 tuple)
throws Exception {
return new Tuple2(tuple._2, tuple._1);
}
});
final Long skewedUserid = reversedSampledRDD.sortByKey(false).take(1).get(0)._2;
JavaPairRDD skewedRDD = userid2PartAggrInfoRDD.filter(
new Function, Boolean>() {
private static final long serialVersionUID = 1L;
@Override
public Boolean call(Tuple2 tuple) throws Exception {
return tuple._1.equals(skewedUserid);
}
});
JavaPairRDD commonRDD = userid2PartAggrInfoRDD.filter(
new Function, Boolean>() {
private static final long serialVersionUID = 1L;
@Override
public Boolean call(Tuple2 tuple) throws Exception {
return !tuple._1.equals(skewedUserid);
}
});
JavaPairRDD skewedUserid2infoRDD = userid2InfoRDD.filter(
new Function, Boolean>() {
private static final long serialVersionUID = 1L;
@Override
public Boolean call(Tuple2 tuple) throws Exception {
return tuple._1.equals(skewedUserid);
}
}).flatMapToPair(new PairFlatMapFunction, String, Row>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable> call(
Tuple2 tuple) throws Exception {
Random random = new Random();
List> list = new ArrayList>();
for(int i = 0; i < 100; i++) {
int prefix = random.nextInt(100);
list.add(new Tuple2(prefix + "_" + tuple._1, tuple._2));
}
return list;
}
});
JavaPairRDD> joinedRDD1 = skewedRDD.mapToPair(
new PairFunction, String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2 call(Tuple2 tuple)
throws Exception {
Random random = new Random();
int prefix = random.nextInt(100);
return new Tuple2(prefix + "_" + tuple._1, tuple._2);
}
}).join(skewedUserid2infoRDD).mapToPair(
new PairFunction>, Long, Tuple2>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2> call(
Tuple2> tuple)
throws Exception {
long userid = Long.valueOf(tuple._1.split("_")[1]);
return new Tuple2>(userid, tuple._2);
}
});
JavaPairRDD> joinedRDD2 = commonRDD.join(userid2InfoRDD);
JavaPairRDD> joinedRDD = joinedRDD1.union(joinedRDD2);
JavaPairRDD sessionid2FullAggrInfoRDD = joinedRDD.mapToPair(
new PairFunction>, String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2 call(
Tuple2> tuple)
throws Exception {
String partAggrInfo = tuple._2._1;
Row userInfoRow = tuple._2._2;
String sessionid = StringUtils.getFieldFromConcatString(
partAggrInfo, "\\|", Constants.FIELD_SESSION_ID);
int age = userInfoRow.getInt(3);
String professional = userInfoRow.getString(4);
String city = userInfoRow.getString(5);
String sex = userInfoRow.getString(6);
String fullAggrInfo = partAggrInfo + "|"
+ Constants.FIELD_AGE + "=" + age + "|"
+ Constants.FIELD_PROFESSIONAL + "=" + professional + "|"
+ Constants.FIELD_CITY + "=" + city + "|"
+ Constants.FIELD_SEX + "=" + sex;
return new Tuple2(sessionid, fullAggrInfo);
}
});
6.7 采用随机数和扩容表进行join
选取一个RDD,用flatMap将RDD扩容,将该RDD中每一条数据扩为10条;
将两外一个RDD,做map映射,key加上一个10以内的随机数;
最后将两个RDD进行join操作;
局限性:你的两个RDD都很大,无法将RDD扩容很大,10倍扩容只能缓解数据倾斜
JavaPairRDD expandedRDD = userid2InfoRDD.flatMapToPair(
new PairFlatMapFunction, String, Row>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable> call(Tuple2 tuple)
throws Exception {
List> list = new ArrayList>();
for(int i = 0; i < 10; i++) {
list.add(new Tuple2(0 + "_" + tuple._1, tuple._2));
}
return list;
}
});
JavaPairRDD mappedRDD = userid2PartAggrInfoRDD.mapToPair(
new PairFunction, String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2 call(Tuple2 tuple)
throws Exception {
Random random = new Random();
int prefix = random.nextInt(10);
return new Tuple2(prefix + "_" + tuple._1, tuple._2);
}
});
JavaPairRDD> joinedRDD = mappedRDD.join(expandedRDD);
JavaPairRDD finalRDD = joinedRDD.mapToPair(
new PairFunction>, String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2 call(
Tuple2> tuple)
throws Exception {
String partAggrInfo = tuple._2._1;
Row userInfoRow = tuple._2._2;
String sessionid = StringUtils.getFieldFromConcatString(
partAggrInfo, "\\|", Constants.FIELD_SESSION_ID);
int age = userInfoRow.getInt(3);
String professional = userInfoRow.getString(4);
String city = userInfoRow.getString(5);
String sex = userInfoRow.getString(6);
String fullAggrInfo = partAggrInfo + "|"
+ Constants.FIELD_AGE + "=" + age + "|"
+ Constants.FIELD_PROFESSIONAL + "=" + professional + "|"
+ Constants.FIELD_CITY + "=" + city + "|"
+ Constants.FIELD_SEX + "=" + sex;
return new Tuple2(sessionid, fullAggrInfo);
}
});
7 spark sql中数据倾斜
7.1 聚合源数据
7.2 过滤倾斜key
在SQL中使用where条件过滤
7.3 提供shuffle并行度
sqlContext.setConf("spark.sql.shuffle.partitions", "1000"); 默认200
7.4 随机key实现双重聚合
import java.util.Random;
import org.apache.spark.sql.api.java.UDF2;
public class RandomPrefixUDF implements UDF2 {
private static final long serialVersionUID = 1L;
@Override
public String call(String val, Integer num) throws Exception {
Random random = new Random();
int randNum = random.nextInt(10);
return randNum + "_" + val;
}
}
public class RemoveRandomPrefixUDF implements UDF1 {
private static final long serialVersionUID = 1L;
@Override
public String call(String val) throws Exception {
String[] valSplited = val.split("_");
return valSplited[1];
}
}
将上诉自定义的UDF注册
sqlContext.udf().register("random_prefix", new RandomPrefixUDF(), DataTypes.StringType);
sqlContext.udf().register("remove_random_prefix", new RemoveRandomPrefixUDF(), DataTypes.StringType);
SQL如下
String _sql =
"SELECT "
+ "product_id_area,"
+ "count(click_count) click_count,"
+ "group_concat_distinct(city_infos) city_infos "
+ "FROM ( "
+ "SELECT "
+ "remove_random_prefix(product_id_area) product_id_area,"
+ "click_count,"
+ "city_infos "
+ "FROM ( "
+ "SELECT "
+ "product_id_area,"
+ "count(*) click_count,"
+ "group_concat_distinct(concat_long_string(city_id,city_name,':')) city_infos "
+ "FROM ( "
+ "SELECT "
+ "random_prefix(concat_long_string(product_id,area,':'), 10) product_id_area,"
+ "city_id,"
+ "city_name "
+ "FROM tmp_click_product_basic "
+ ") t1 "
+ "GROUP BY product_id_area "
+ ") t2 "
+ ") t3 "
+ "GROUP BY product_id_area ";
7.5 reduce join 转换成map join
① 可以将表转换成RDD,进行手动map join
②spark sql 内置的map join,默认如果有一张表在10M以内,就会将该表进行broadcast,然后执行map join
③自己可以去调节阈值: sqlContext.setConf("spark.sql.autoBroadcastJoinThreshold", "20971520");
7.6 采样倾斜key
spark sql 不适用,因为是spark core算子
7.7 随机key与扩容表
spark sql + spark core
JavaRDD rdd = sqlContext.sql("select * from product_info").javaRDD();
JavaRDD flattedRDD = rdd.flatMap(new FlatMapFunction() {
private static final long serialVersionUID = 1L;
@Override
public Iterable call(Row row) throws Exception {
List list = new ArrayList();
for(int i = 0; i < 10; i ++) {
long productid = row.getLong(0);
String _productid = i + "_" + productid;
Row _row = RowFactory.create(_productid, row.get(1), row.get(2));
list.add(_row);
}
return list;
}
});
StructType _schema = DataTypes.createStructType(Arrays.asList(
DataTypes.createStructField("product_id", DataTypes.StringType, true),
DataTypes.createStructField("product_name", DataTypes.StringType, true),
DataTypes.createStructField("product_status", DataTypes.StringType, true)));
DataFrame _df = sqlContext.createDataFrame(flattedRDD, _schema);
_df.registerTempTable("tmp_product_info");
String _sql =
"SELECT "
+ "tapcc.area,"
+ "remove_random_prefix(tapcc.product_id) product_id,"
+ "tapcc.click_count,"
+ "tapcc.city_infos,"
+ "pi.product_name,"
+ "if(get_json_object(pi.extend_info,'product_status')=0,'自营商品','第三方商品') product_status "
+ "FROM ("
+ "SELECT "
+ "area,"
+ "random_prefix(product_id, 10) product_id,"
+ "click_count,"
+ "city_infos "
+ "FROM tmp_area_product_click_count "
+ ") tapcc "
+ "JOIN tmp_product_info pi ON tapcc.product_id=pi.product_id ";