51.性能调优之广播大变量

来源:互联网 发布:网络优化与维护 编辑:程序博客网 时间:2024/05/29 18:04

本文为《Spark大型电商项目实战》 系列文章之一,主要介绍在实际项目中广播大变量的原因、原理和具体在代码中实现的方法。

问题分析

Spark Application的Driver进程,其实就是我们写的Spark作业打成的jar运行起来的进程,以随机抽取map步骤为例,其工作时过程大致为:
这里写图片描述

这种默认的情况下,task执行的算子中使用了外部的变量,每个task都会获取一份变量的副本,有什么缺点呢?在什么情况下会出现性能上的恶劣的影响呢?

因为map本身是不小,存放数据的一个单位是Entry,还有可能会用链表的格式的来存放Entry链条,所以map是比较消耗内存的数据格式。比如,map总共是1M。你前面调优都调的特好,资源给的到位,配合着资源并行度调节的绝对到位,设置1000个task,大量task的确都在并行运行。

第一点,这些task里面都用到了占用1M内存的map,那么首先,map会拷贝1000份副本,通过网络传输到各个task中去,给task使用。总计有1G的数据,会通过网络传输。网络传输的开销不容乐观啊!网络传输也许就会消耗掉你的spark作业运行的总时间的一小部分。

第二点,map副本传输到了各个task上之后是要占用内存的。也许1个map的确不大,也就1M,但1000个map分布在你的集群中,一下子就耗费掉1G的内存。这对性能会有什么影响呢?
首先不必要的内存的消耗和占用,就导致了你在进行RDD持久化到内存,也许就没法完全在内存中放下,就只能写入磁盘,最后导致后续的操作在磁盘IO上消耗性能;还有可能你的task在创建对象的时候,也许会发现堆内存放不下所有对象,也许就会导致频繁的垃圾回收器的回收(GC)。GC的时候一定是会导致工作线程停止,也就是导致Spark暂停工作那么一点时间。频繁GC的话对Spark作业的运行的速度会有相当可观的影响。

这种举例的随机抽取的map为1M还算小的,如果你是从哪个表里面读取了一些维度数据,比方说,所有商品品类的信息,在某个算子函数中要使用到,也许会达到100M,如果有1000个task,就会有100G的数据进行网络传输,集群瞬间因为这个原因消耗掉100G的内存。

广播大变量

如果task使用大变量(1m~100m),明知道会导致性能出现恶劣的影响。那么我们怎么来解决呢?
此时就需要用到广播(Broadcast),将大变量广播出去,而不是直接使用。
这里写图片描述
如上图所示,每个Executor会对应自己的BlockManager,BlockManager是负责管理某个Executor对应的内存和磁盘上的数据。

广播变量初始的时候就在Drvier上有一份副本,task在运行的时候,想要使用广播变量中的数据,此时首先会在自己本地的Executor对应的BlockManager中,尝试获取变量副本。如果本地没有,那么就从Driver远程拉取变量副本,并保存在本地的BlockManager中,此后这个executor上的task都会直接使用本地的BlockManager中的副本。executor的BlockManager除了从driver上拉取,也可能从其他节点的BlockManager上拉取变量副本,距离越近越好。

广播变量的优点:不是每个task一份变量副本,而是变成每个节点的executor才一份副本。这样的话,就可以让变量产生的副本大大减少。

根据在实际企业中的生产环境举例来说:总共有50个executor,1000个task,一个map大小为10M。
默认情况下,1000个task,1000份副本,共有10G的数据进行网络传输,在集群中,耗费10G的内存资源。
如果使用了广播变量,50个execurtor就只有50个副本,有500M的数据进行网络传输,而且不一定都是从Driver传输到每个节点,还可能是就近从最近的节点的executor的bockmanager上拉取变量副本,网络传输速度大大增加,只有500M的内存消耗。
之前是10000M,现在是500M,大约20倍以上的网络传输性能消耗的降低,20倍的内存消耗的减少。对性能的提升和影响,还是很客观的。
虽然说,不一定会对性能产生决定性的作用。比如运行30分钟的spark作业,可能做了广播变量以后,速度快了2分钟,或者5分钟。但是一点一滴的调优,积少成多,最后还是会有效果的。

代码优化

以代码中的 session 随机抽取功能为例,因为 session 随机抽取功能使用到了随机抽取索引map这一比较大的变量,之前是直接在算子里使用了这个map,每个task都会拷贝一份map副本,比较消耗内存和网络传输性能,现在将其改为广播变量。
首先传入一个变量sc,将代码

private static void randomExtractSession(            final long taskid,            JavaPairRDD<String, String> sessionid2AggrInfoRDD,            JavaPairRDD<String, Row> sessionid2actionRDD)

变为

private static void randomExtractSession(            JavaSparkContext sc,            final long taskid,            JavaPairRDD<String, String> sessionid2AggrInfoRDD,            JavaPairRDD<String, Row> sessionid2actionRDD) 

然后在“使用按是按比例随机抽取算法,计算出每小时要抽取session的索引”方法后设置广播变量(原先map之前的final去掉)

final Broadcast<Map<String, Map<String, List<Integer>>>> dateHourExtractMapBroadcast =                 sc.broadcast(dateHourExtractMap);

接着就是使用广播变量, 直接调用广播变量(Broadcast类型)的value()或getvalue()函数就可以获取到之前封装的广播变量,在代码

List<Integer> extractIndexList = dateHourExtractMap.get(date).get(hour);

前加上

Map<String, Map<String, List<Integer>>> dateHourExtractMap =                                 dateHourExtractMapBroadcast.value();

最后不要忘了在randomExtractSession处添加sc参数,即由

randomExtractSession(task.getTaskid(),filteredSessionid2AggrInfoRDD, sessionid2actionRDD);

变为

randomExtractSession(sc, task.getTaskid(),filteredSessionid2AggrInfoRDD, sessionid2actionRDD);

此时广播变量方法就完成了。

《Spark 大型电商项目实战》源码:https://github.com/Erik-ly/SprakProject

本文为《Spark大型电商项目实战》系列文章之一,
更多文章:Spark大型电商项目实战:http://blog.csdn.net/u012318074/article/category/6744423

0 0