DAC spark优化性能实战
背景:
dac应对分页场景情况添加limit限制,结果集不能显示总条数,如果需要运算总条数,需要count() IQ/impala: count(1) over() as countNum 但是大数据量场景下spark上性能极差,优化2sqls分别查询结果集和order by的子查询获得总条数 小数据量场景下,count cover比2sqls的方式性能高一倍以上,对业务做出了挑战
性能低原因:
现有的SparkJdbcServer count over时使用了窗口函数,并且是single partition,所以没有并行读的支持在大数据下性能表现极差
ps:什么是分区以及为什么要分区?
Spark RDD 是一种分布式的数据集,由于数据量很大,因此要它被切分并存储在各个结点的分区当中。从而当我们对RDD进行操作时,实际上是对每个分区中的数据并行操作。
Ation(执行):触发Spark作业的运行,真正触发转换算子的计算
分析:
是否可以小数据量采用countover,大数据量采用2sqls呢?答案是不可以,1.数据量无法预测, 2.性能交点不确定,无法动态选择3.通用的数据访问平台,强行适配做法不可取
目标:
1条sql,耗时最短
解决思路:
数据库专家介绍,spark中有缓存(持久化),可以将RDDs或者DataFrame做缓存,多个操作间共享持久化数据,每个节点的每个分区都可以使用RDD在内存中计算,改数据上的其他action操作将直接使用内存的数据,可以让之后的acation操作速度加快十倍。
原生的Spark JDBCServer无法干预,可以使用之前的Marttix将RDDCache原型验证
原型验证:
分别采用dataSet和RDDs在不同数据量做Cache的原型验证,需要注意,必须在创建一个RDD或者DataSet之后直接连续调用Cache()或者persist(),而不是单独另起执行cache或者persist
dataSet Cache源码示例
DataSet<Row> result=SqlContext.sql(sql).persist();
long count=result.count();
result.orderBy(Function.desc('900')).limit(5000).withColumn("countNum",function.lit(count);
List<Row> rows=result.collectAsList();
result.unpersist();
RDDs Cache源码示例
JavaRDD<Row> rdd = result.toJavaRDD().persist(StorageLevel.MEMORY_AND_DISK());
long count = rdd.count();
int index = result.schema().fieldIndex("900");
SerializableComparator scc = new SerializableComparator();
scc.setIndex(index);
rows = rdd.takeOrdered(5000, scc);
rdd.unpersist();
原型验证结果:
万级别 Cache方式耗时和countOver相当,均好于2sqls,
千万级别 cache优势明显
亿级别 Rdd Cache出现预期之外的情况,耗时严重,查看SparkUI发现,GC耗时很严重
研究发现,RDD的Api是函数式的,强调不变性,大部分场景下倾向于创建新对象而不是修改老对象,这一特点可以带来干净整洁的API,却创建了大量临时对象,对GC造成压力,DataFrame和DataSet API都是基于Sparksql引擎构建的,所有R JAVA scala Python语言的这俩API的sql型数据查询,底层都是相同的代码优化器,所有会获得时间和空间的效率提升,无类型的DataSet运行较快,适合做交互式分析,另一方面,sparksql在框架内部已经在各种可能的情况下尽量重用对象,这样做虽然内部打破了不变形,但数据返回给用户是,还会转换为不可变数据,利用这俩API开发,可以免费享受这些优化效果,官方也建议使用DataSet。
详细的继续验证DataSet分析:
验证环境:8spark实例,10核心,20G内存/instance
继续采用三种不同的Dataset Cache方案:2sqls counterover dataSet cache的Spark executor配置,都是用相同的jdbc链接,连续查询11次,取后10次的平均值
亿级以下,cache方案优于其他方案
十亿级别以上,cache不如2sqls,怀疑是内存分配导致。
查看SparkUI,有两点需要关注,1.二者GC时间差异很大,第二 input的数据量差异很大,
启动时的加入jVM参数--conf "spark.executor.extraJavaOptions=-verbose:gc -XX:+PrintGCDetails"
或者
conf.set("spark.executor.extraJavaOptions","-XX:+PrintGCDetails -XX:+PrintGCTimeStamps") 设置一下这个,在excetors端会输出GC的信息。
查看日志输出后发现,DataSet cache的方式比2sqls多发生了两次Full GC,其他新生代minor GC也多了若干次,符合情况
总的来说,可以满足亿级以下查询,十亿级以上有待继续分析验证 pS: 启动SparkGC日志: 有一种较为简单的做法。Spark平台可能是长期运行的,可在启动Spark时在其JVM选项里加上【命令2】,然后通过【命令3】开始一个记录,接着运行你的Spark程序。命令3中的duration参数要注意,要大于你的Spark程序运行时间。运行完后你就可通过【命令4】转储出记录文件,复制这个文件到客户端,最后用JMC打开此文件分析即可。下面就是我的操作命令(蓝色字体)的过程
实战命令:
jcmd 13364 JFR.start name=MyRecording settings=profile delay=10s duration=60s
jcmd 13364 JFR.check
jcmd 13364 JFR.dump name=MyRecording filename=myrecording.jfr
13364:
Dumped recording "MyRecording", 2.4 MB written to:
D:\study\spark-workshop\myrecording.jfr
D:\study\spark-workshop>
就可以在一个Spark应用程序运行前开始记录,运行完后再dump出记录文件,这是一个loop 。下一个/下一次Spark应用程序运行,按此loop,不断的评估改进效果。
打开ftl文件点,点击“代码”->“热点方法”,Spark程序WordCount2SortByWord中调用的Java String.split方法最耗时。还可以一层层展开,它是被哪个方法调用的。
“代码”->“调用树”,也可以一层层展开调用关系,例如从WordCount2SortByWord.main开始,调用了SparkContex,RDD等类的一些方法。
从图中可以看出,在SparkStreaming8Main这个程序中,最耗时的热点方法是org.apache.spark.util.collection.AppendOnlyMap.changeValue(Object, Function2):