Spark性能调优

  • SparkJob WebUI 工具
  • 页签
  • Spark性能调优
  • RDD/Dataset Cache缓存
  • 缓存语法
  • RDD Kryo序列化
  • RDD.MEMORY_ONLY_SER
  • Dataset.MEMORY_ONLY_SER
  • 内存调优
  • 内存管理概述
  • 确定内存消耗
  • ※ 内存调整措施 ※
  • 其他调优参数
  • 并行度
  • ReduceTask内存使用
  • 广播大变量
  • 数据本地化


SparkJob WebUI 工具

页签

Jobs => 由行动算子决定(1 action = 1 job)
	Stages => 由 Shuffle 算子决定(Shuffle算子 + 1 = 总Stages)
	Storage => RDD缓存(StorageLevel)
	Environment => 环境上下文 Jar Jdk ...
	Executor => 执行节点详情及各个节点上Task统计Active Dead Fail
	            最重要的是GC Time,如果过长一定是有问题;ThreadDump多线程信息
	SQL => 完成的查询的DAG图(主要是用于优化SQL Join过程,查看表大小)

Spark性能调优

RDD/Dataset Cache缓存

缓存语法
sparkSession.catalog.cacheTable("tableName")
	sparkSession.catalog.uncacheTable("tableName")
	
	dataFrame.cache() 
	unpersist()
	
	//default (`MEMORY_ONLY`)
	RDD.persist(StorageLevel.MEMORY_ONLY) == RDD.cache(){ persist(...) } 
	//default (`MEMORY_AND_DISK`)
	Dataset.persist(StorageLevel.MEMORY_AND_DISK) == Dataset.cache(){ persist(...) } 

	//缓存类别
	[DISK_ONLY、MEMORY_ONLY、MEMORY_ONLY_SER、MEMORY_AND_DISK、MEMORY_AND_DISK_SER、OFF_HEAP]
	只硬盘,只内存,只内存并序列化,内存和硬盘,内存和硬盘序列化,堆外内存
RDD Kryo序列化
val conf = new SparkConf().setMaster(...).setAppName(...)
	  .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
	conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]))
	val sc = new SparkContext(conf)

Java序列化的默认值适用于任何可序列化的Java对象,但速度很慢,因此建议使用Kryo序列化。
也可配置为org.apache.spark.Serializer的任何子类。【通常情况下我们不使用RDD,性能很差

RDD.MEMORY_ONLY_SER

内存空间资源 / CPU时间片资源 只能选一个

Store RDD as serialized Java objects (one byte array per partition). This is generally more space-efficient than deserialized objects, especially when using a fast serializer, but more CPU-intensive to read.

Dataset.MEMORY_ONLY_SER

拥有专用Encoder

Datasets are similar to RDDs, however, instead of using Java serialization or Kryo they use a specialized Encoder to serialize the objects for processing or transmitting over the network.

内存调优

内存管理概述
主要分为2类:Execution执行内存 Storage存储内存
			执行内存:用于洗牌Shuffle,联接join,排序sort和聚合agg中的计算内存
			存储内存:用于在集群中缓存和传播内部数据的内存
			
			二者关系:
					动态分配,当不使用执行内存时,存储可以获取所有可用内存,反之亦然。
					如果有必要,执行可能会驱逐存储,但只有在总存储内存使用率下降到某个阈值(R)以下时,才可以执行。
			设计优点:
					1.不使用缓存的应用程序可以将整个空间用于执行内存
					2.确实使用缓存的应用程序可以保留最小的存储空间,而不必担心被强制丢弃
					3.屏蔽内存使用的复杂性,无需用户了解如何在内部划分内存
			配置:
				spark.memory.fraction M 代表JVM堆空间占比(默认0.6),剩余0.4给UserDataStructure (JVMHeapSpace-300MB)
				spark.memory.storageFraction R 表示R的大小为M的一部分(默认为0.5) R是M中的存储空间,其中的缓存不会被执行逐出
			
			可配置spark.memory.fraction的值,以便契合JVM堆实际使用大小.Worker端的内存 需要考虑数据倾斜问题。
确定内存消耗

①确定数据集所需的内存消耗量的最佳方法是创建一个RDD,将其放入缓存中,然后查看Web UI中的“ Storage”页面。该页面将告诉您RDD占用了多少内存。
②估算特定对象的内存消耗,请使用SizeEstimator的estimate方法。这对于尝试使用不同的数据布局以减少内存使用量以及确定广播变量将在每个执行程序堆上占用的空间量很有用。
[不准] println(SizeEstimator.estimate(joinDWSMemberDS))

※ 内存调整措施 ※

第一类

①调整数据结构
		1.尽量使用primaryType原始数据类型及[],而不是Java标准库、Scala集合类。
		  http://fastutil.di.unimi.it/ fastutil库为与Java标准库兼容的基本类型提供了方便的集合类。
		2.尽可能避免使用带有许多小对象和指针的嵌套结构
		3.考虑使用数字ID或枚举对象代替Key的字符串
		4.如果RAM少于32GB,则设置JVM选项 -XX:+UseCompressedOops 以使指针为四个字节而不是八个字节。在spark-env.sh中添加。

第二类

②序列化RDD存储
		见下方标题 Cahching 缓存至内存

第三类

③垃圾收集优化(FullGC会导致所有线程停止)
		GC衡量
			通过JMap命令 输出完整的JVM日志,使用IBM HeapAnalyzer分析内容
			或添加JVM选项,将GC信息收集至 worker node(-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps)
		GC调优目标
			确保在老年代中只存储长期存在的RDDs,新生代的大小足以存储短期存在的对象.避免FullGC
		GC调优效果
			取决于应用程序和可用的内存量。
		情况及解决方法
			1.如果在一个Task完成之前多次调用一个Full GC,则意味着没有足够的内存来执行任务
			2.如果Minor GC(轻量)过多,则考虑分配更多内存给Eden.若Eden.size=E,Young.size=4/3*E 通过JVM选项 -Xmn=4/3*E
			3.如果打印日志中显示 老年代近满 则降低 spark.memory.fraction JVM内存占比,
			  与减慢任务执行速度相比,缓存较少的对象效果更好。
			  或者,减少新生代Young内存空间通过JVM选项 -Xmn.最后,也可以尝试 将 老年代与新生代的比例调整
			  通过 JVM 的 NewRatio 参数,默认为2 => 老年代 占 堆内存的 2/3
			  它应该足够大以至于这个比分fraction超过spark.memory.fraction.
			4.尝试使用G1GC垃圾收集器-XX:+UseG1GC。在垃圾收集成为瓶颈的某些情况下,它可以提高性能。
			  注意,对于较大的executor堆大小,使用-XX:G1HeapRegionSize来增加G1区域大小可能很重要(划分堆的基本单位)
			5.更加高维度的JVM调优策略
			  https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/index.html
		实现手段
			对于executors调优可以通过 spark.executor.extraJavaOptions 属性配置(SparkSession.config()).
		内存估算
			如,从HDFS读取数据块,块大小可估计Task所使用的内存量。
			解压缩后块大小通常为2到3倍,且希望有3或4个Task执行该Job(块大小128MB):可估计Eden大小为 4Task*3倍*128 MB

其他调优参数

并行度

结论:并行度设置为ExecutorCPU核数的2-3倍

spark.default.parallelism 属性(默认 集群CPU核数)
	通常建议集群中每个CPU内核执行2-3个任务:parallelism=totalCores*3
ReduceTask内存使用

结论:对于Shuffle算子通常需要大并行度

有时OOM Error原因不是因为RDD不能容纳在内存中,而是某一个Task(如reduceTask)groupByKey的working set太大
	Spark的Shuffle算子(sortByKey,groupByKey,reduceByKey,join...)会建立HashTable(很大)在每一个Task中来执行分组
	
	这里最简单的修复方法是增加并行度,这样每个Task的输入集就会更小。
	Spark可以有效地支持短至200 ms的Task => 可跨多个Task重用一个Executor JVM,这样的Task启动成本很低,
	故可安全地将并行度提高到比集群中的内核数量更多的水平。
广播大变量

结论:Driver中大于20KB大变量使用请设为广播变量

使用SparkContext中的broadcast功能可以极大地减少每个序列化Task的大小,以及通过集群启动Job的成本。
	如果 Task 使用了 Driver 中的任何大对象(20KB以上),请考虑将其转换为广播变量。
数据本地化

结论:默认设置通常效果很好

本地化等级
		PROCESS_LOCAL:数据与正在运行的代码位于同一JVM中[最好的位置]
		NODE_LOCAL:数据与Executor在同一节点上[数据在进程之间传输,要慢一些]
		NO_PREF:没有位置偏好preference[都可以不受位置限制]
		RACK_LOCAL:数据与Executor在同一服务器机架上[通过单个交换机网络IO]
		ANY:数据在网络上的其他位置,与Executor不在同一机架中[通过复杂网络IO]
	本地化策略
		Spark倾向于在最佳位置级别安排所有任务,但这并不是总是可以的.
		在任何空闲Executor上没有未处理数据的情况下,Spark会在等待一段时间后,选择切换到较低的本地级别启动Task。
			a)等待忙碌的CPU释放以在与数据同一服务器上启动Task
			↓↓↓等待超时↓↓↓
			b)启动新Task在需要移动数据且较远的地方
	本地化调优
		每个级别之间的等待超时可以单独配置,也可以一起配置在一个参数中。有关详细信息,参见spark.locality参数。
		如果任务很长并且位置不佳,则应该增加这些设置,但是默认设置通常效果很好。