spark2.x-jvm调优实战(以tomcat访问日志分析为例)

背景

如果在持久化RDD的时候,持久化了大量的数据,那么Java虚拟机的垃圾回收就可能成为一个性能瓶颈。因为Java虚拟机会定期进行垃圾回收,此时就会追踪所有的java对象,并且在垃圾回收时,找到那些已经不在使用的对象,然后清理旧的对象,来给新的对象腾出内存空间。

垃圾回收的性能开销,是跟内存中的对象的数量,成正比的。所以,对于垃圾回收的性能问题,首先要做的就是,使用更高效的数据结构,比如array和string;其次就是在持久化rdd时,使用序列化的持久化级别,而且用Kryo序列化类库,这样,每个partition就只是一个对象——一个字节数组。

集群环境

节点
xyz01.aiso.com
硬件环境(VMware 10 虚拟机)(三台)
内存 6G
CPU 4个1核处理器
硬盘SCSI 500GB
网络 NAT

软件环境
CentOS release 6.4 (Final) x64
java version “1.8.0_112”
Scala 2.11.8
Hadoop 2.5.0-cdh5.3.6
spark-2.3.0-bin-hadoop2.7
apache-maven-3.3.9

xyz02.aiso.com
硬件环境(VMware 10 虚拟机)(三台)
内存 4G
CPU 1核处理器
硬盘SCSI 500GB
网络 NAT

软件环境
CentOS release 6.4 (Final) x64
java version “1.8.0_112”
Scala 2.11.8
Hadoop 2.5.0-cdh5.3.6
spark-2.3.0-bin-hadoop2.7
apache-maven-3.3.9

xyz03.aiso.com
硬件环境(VMware 10 虚拟机)(三台)
内存 4G
CPU 1核处理器
硬盘SCSI 500GB
网络 NAT

软件环境
CentOS release 6.4 (Final) x64
java version “1.8.0_112”
Scala 2.11.8
Hadoop 2.5.0-cdh5.3.6
spark-2.3.0-bin-hadoop2.7
apache-maven-3.3.9

集群规划
xyz01.aiso.com NameNode DataNode ResourceManager NodeManager QuorumPeerMain SPARK HistoryServer HMaster HRegionServer
xyz02.aiso.com SecondaryNameNode DataNode NodeManager QuorumPeerMain HRegionServer
xyz03.aiso.com DataNode JobHistoryServer NodeManager QuorumPeerMain HRegionServer

yarn资源状况

spark2.x-jvm调优实战(以tomcat访问日志分析为例)_第1张图片

日志格式

123.66.144.115 - - [15/Apr/2018:22:42:25 +0800] “GET /docs/manager-howto.html HTTP/1.1” 200 72733

优化前

spark job代码

val logsRDD = initRDD.union(initRDD).union(initRDD)
      .map(line => parseLog(line))
      .filter(x => x != null)
      .cache()

    /**
      * 统计web服务器所有响应中的最大、最小及平均字节数
      */
    val contextSize = logsRDD.map { log => log.bytes }
    val maxSize = contextSize.max()
    val minSize = contextSize.min()
    val averageSize = contextSize.reduce(_ + _) / contextSize.count()
    println("响应最大值:" + maxSize + "  最小值:" + minSize + "   平均值:" + averageSize);

    /**
      * 统计各种响应状态的出现次数
      */
    logsRDD.map(log => (log.status, 1))
      .reduceByKey(_ + _)
      .foreach(result => println(" 响应状态:" + result._1 + "  计数:" + result._2))

    /**
      * 统计访问总次数超过1000的前3名的ip
      */
    val result = logsRDD.map(log => (log.ip, 1))
      .reduceByKey(_ + _)
      .filter(result => result._2 > 1000)
      .map(m => (m._2, m._1))
      .top(3)
    for (tuple <- result) {
      println("ip : " + tuple._2 + "  请求次数:" + tuple._1);
    }

    /**
      * 统计请求URI的TopN
      */
    val topN = logsRDD.map { log => (log.request.split(" ")(1), 1) }
      .reduceByKey(_ + _)
      .map(result => (result._2, result._1))
      .sortByKey(false)
      .take(3)
    for (tuple <- topN) {
      println("URI : " + tuple._2 + "  请求频次:" + tuple._1);
    }

提交脚本

  /opt/module/apache-2.7-ha/spark-2.3.0-bin-hadoop2.7/bin/spark-submit \
  --master yarn \
  --deploy-mode client \
  --executor-memory 500M \
  --num-executors 2 \
  --executor-cores 2 \
  --driver-memory 1G \
   --conf "spark.executor.extraJavaOptions=-XX:+PrintGCDetails  -verbose:gc -XX:+PrintGCTimeStamps " \
  --queue root.xiaoyuzhou \
   --jars ${SPARK_REPORT_EXPORT_JARS} \
   --class com.spark.learn.job.TomcatLogStat \
  $JOB_HOME/sparkexamples-1.0.jar \
   env=prd  \
   log.level=error \ 
   input.data.path=/data/tomcat_localhost_access_log

运行结果

spark2.x-jvm调优实战(以tomcat访问日志分析为例)_第2张图片

优化分析

1、使用高效的垃圾回收器

此任务使用 -XX:+UseG1GC前后变化:

GC日志

使用前

使用后

执行时间的变化
App ID App Name Started Completed Duration Spark User Last Updated Event Log
application_1526440328204_0005 TomcatLogStat 2018-05-16 15:21:54 2018-05-16 15:31:42 9.8 min xiaoyuzhou 2018-05-16 15:31:42 Download
application_1526440328204_0004 TomcatLogStat 2018-05-16 14:41:10 2018-05-16 14:57:42 17 min xiaoyuzhou 2018-05-16 14:57:42

调整JVM内存空间大小

降低spark.storage.memoryFraction

Spark中,垃圾回收调优的目标就是,只有真正长时间存活的对象,才能进入老年代,短时间存活的对象,只能呆在年轻代。不能因为某个Survivor区域空间不够,在Minor GC时,就进入了老年代。从而造成短时间存活的对象,长期呆在老年代中占据了空间,而且Full GC时要回收大量的短时间存活的对象,导致Full GC速度缓慢。

如果发现,在task执行期间,大量full gc发生了,那么说明,年轻代的Eden区域,给的空间不够大。

此时可以执行一些操作来优化垃圾回收行为:

1、包括降低spark.storage.memoryFraction的比例,给年轻代更多的空间,来存放短时间存活的对象;

使用前

使用后

jvm eden区调节

使用前

使用后

1、给Eden区域分配更大的空间,使用-Xmn即可,通常建议给Eden区域,预计大小的4/3;
如果使用的是HDFS文件,那么很好估计Eden区域大小,如果每个executor有4个task,然后每个hdfs压缩块解压缩后大小是3倍,此外每个hdfs块的大小是64M,那么Eden区域的预计大小就是:4 * 3 * 64MB,然后呢,再通过-Xmn参数,将Eden区域大小设置为4 * 3 * 64 * 4/3。

-XX:SurvivorRatio=4:如果值为4,那么就是两个Survivor跟Eden的比例是2:4,也就是说每个Survivor占据的年轻代的比例是1/6,所以,你其实也可以尝试调大Survivor区域的大小。
-XX:NewRatio=4:调节新生代和老年代的比例, 表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
Xms=Xmx 并且设置了Xmn的情况下,该参数不需要进行设置。

调节executor的堆外内存

当spark作业中,是不是的报错,shuffle file cannot find,executro、task lost,out of memory等,可能是堆外内存不足,导致executor挂掉,task拉取该executor的数据是无法获取到,导致以上错误,甚至spark作业崩溃。

在spark作业的提交脚本中,修改spark.yarn.executor.memoryOverhead参数(默认为256M),在脚本中添加参数

--conf spark.yarn.executor.memoryOverhead=2048 \        调节堆外内存
--conf spark.core.connection.ack.wait.timeout=300 \       调节连接时间

调节spark 内存管理相关参数

使用前

使用后

了解 Spark Shuffle 中的 JVM 内存使用空间对一个Spark应用程序的内存调优是至关重要的。跟据不同的内存控制原理分别对存储和执行空间进行参数调优:spark.executor.memory, spark.storage.safetyFraction, spark.storage.memoryFraction, spark.storage.unrollFraction, spark.shuffle.memoryFraction, spark.shuffle.safteyFraction。

Spark 1.6 以前的版本是使用固定的内存分配策略,把 JVM Heap 中的 90% 分配为安全空间,然后从这90%的安全空间中的 60% 作为存储空间,例如进行 Persist、Unroll 以及 Broadcast 的数据。然后再把这60%的20%作为支持一些序列化和反序列化的数据工作。其次当程序运行时,JVM Heap 会把其中的 80% 作为运行过程中的安全空间,这80%的其中20%是用来负责 Shuffle 数据传输的空间。

Spark 2.0 中推出了联合内存的概念,最主要的改变是存储和运行的空间可以动态移动。需要注意的是执行比存储有更大的优先值,当空间不够时,可以向对方借空间,但前提是对方有足够的空间或者是 Execution 可以强制把 Storage 一部份空间挤掉。Excution 向 Storage 借空间有两种方式:第一种方式是 Storage 曾经向 Execution 借了空间,它缓存的数据可能是非常的多,当 Execution 需要空间时可以强制拿回来;第二种方式是 Storage Memory 不足 50% 的情况下,Storgae Memory 会很乐意地把剩馀空间借给 Execution。

如果是你的计算比较复杂的情况,使用新型的内存管理 (Unified Memory Management) 会取得更好的效率,但是如果说计算的业务逻辑需要更大的缓存空间,此时使用老版本的固定内存管理 (StaticMemoryManagement) 效果会更好。

总结

jvm的调优是非常复杂和敏感的,尽量调节executor内存的比例就可以了。真的到了万不得已的地方,并且对jvm相关的技术很了解,那么再进行eden区域的调节。总之,JVM调优没有“银弹”,结合系统现状和多尝试不同的调优策略是找到合适调优方法的唯一途径。

你可能感兴趣的:(spark)