下面是基于官方优化建议,加上自己的一些理解整理。官方地址:https://spark.apache.org/docs/2.4.8/tuning.html
Spark会根据每个文件的大小自动设置运行“map”任务的数量,而对于分布式的“reduce”操作,例如groupByKey和reduceByKey,它使用最大的父RDD分区数,我们也可以为这些算子提供其分区数的参数值或者设置spark.default.parallelism参数,推荐是CPU的2-3倍任务数。增加Reduce任务的并行度(sortByKey, groupByKey, reduceByKey, join, etc),减少每一个任务处理的数据集的规模,Spark可以200ms内完成一个任务的计算,因为spark支持重用executor的JVM,并且每个任务的消耗也很低,所以可以放心增加任务数。
默认的并行度:spark.default.parallelism
在SQL中动态修改分区数:SET spark.sql.shuffle.partitions = 2;
当需要把结果收集到driver端时,先filter多余的行->再去除不需要的列->如果有必要再distinct->再collect
whether to use memory, or ExternalBlockStore,
whether to drop the RDD to disk if it falls out of memory or ExternalBlockStore,
whether to keep the data in memory in a serialized format,
and whether to replicate the RDD partitions on multiple nodes.
class StorageLevel private(private var _useDisk: Boolean,private var _useMemory: Boolean,private var _useOffHeap: Boolean,private var _deserialized: Boolean,private var _replication: Int = 1) extends Externalizableobject StorageLevel {val NONE = new StorageLevel(false, false, false, false)val DISK_ONLY = new StorageLevel(true, false, false, false)val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)val MEMORY_ONLY = new StorageLevel(false, true, false, true)val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)val OFF_HEAP = new StorageLevel(true, true, true, false, 1)
普通的cache等同于persist(Storage.MEMORY_ONLY),在内存不足时数据会被驱逐,下次使用时需要重算,所以建议使用persist(Storage.MEMORY_AND_DISK_SER)。
SparkSQL使用spark.catalog.cacheTable(“tableName”)或者dataFrame.cache()可以使用内存中的列存储格式。SparkSQL只扫描请求的列并自动调节最小压缩和GC压力之间的平衡。
spark.sql.inMemoryColumnarStorage.compressed | 默认true | When set to true Spark SQL will automatically select a compression codec for each column based on statistics of the data. |
spark.sql.inMemoryColumnarStorage.batchSize | 默认10000 | Controls the size of batches for columnar caching. Larger batch sizes can improve memory utilization and compression, but risk OOMs when caching data. |
checkpoint两个应用:
在RDD上做checkpoint和在DF或者DS上做checkpoint有些区别,后两者会返回一个新的数据集。默认checkpoint是lazy的,但我们默认在DF或者DS上使用的是checkpoint(eager = true, reliableCheckpoint = true),会立即执行(其内部是通过调用ds的rdd的count方法实现)。checkpoint的过程:首先是拷贝现有的RDD,对新的RDD进行checkpoint(也就是存储到本地),然后生成一个新的DS返回,这样就切断了依赖。所以后续算子都会基于新的RDD计算,那么还有必要先对原有RDD缓存吗?另外注意,checkpoint到本地的rdd文件只能用于spark恢复,不能直接被后续的算子利用。
A RDD can be recovered from a checkpoint files using SparkContext.checkpointFile. You can use SparkSession.internalCreateDataFrame method to (re)create the DataFrame from the RDD of internal binary rows.
数据存储和计算分离可以方便系统的横向扩展,但当计算数据的时候往往需要把数据网路传输到计算节点带来网络耗时。所以Spark更喜欢存算不分离的方式。这就是数据本地化,有以下几个级别:
通常,为了高效,需要将序列化代码从一个地方传送到另一个地方比将数据块传送到另一个地方,因为代码的大小比数据小得多,这都是spark的任务调度来完成的。当数据和代码在不同的节点时,Spark通常所做的是等待一段时间,希望数据所处的executor有任务结束腾出资源从而调度新的任务。一旦超时到期,它就开始将数据从远处移动到空闲CPU,比如在同节点的不同executor间移动数据。每个级别之间的回退等待超时可以单独配置。
在spark1.6版本之前就是用的静态内存模型。静态模型就是把一个Executor分成三个部分,一部分是Storage内存区域,一部分是Execution区域,还有一部分是其他区域。在spark的configuration中默认的有以下参数控制。
(旧版)spark.storage.memoryFraction: 默认0.6,用于缓存和广播变量。
(旧版)spark.shuffle.memoryFraction: 默认0.2,用于Execution。
在spark2.0版本之后,spark新增加一种模型,就是统一动态模型。Spark内存结构分为:默认是(JVM的堆空间 - 300MB)*60%作为Spark的内存,40%用于存储Spark的用户数据结构和Spark的内部元数据。该比例由spark.memory.fraction参数控制。
Spark内存又默认五五分为execution内存和storage内存,execution内存用于Shuffle\joins\sorts\aggregations算子使用,而storage内存用于在集群间缓存和传播内部数据,storage内存是不会被占用的,通过spark.memory.storageFraction参数控制。
这种设计确保了几个理想的性能。 首先,不使用缓存的应用程序可以使用整个执行空间,从而避免不必要的磁盘溢出。 其次,使用缓存的应用程序可以保留最小的存储空间®,使其数据块不会被移除。
需要注意,因为动态占用机制,Spark UI上的storage memory是execution+storage的内存,另外还包含了堆外内存,即其值等于 (spark.executor.memory - 300M) * spark.memory.fraction + 堆外内存
为了进一步优化内存的使用以及提高 Shuffle 时排序的效率,Spark 1.6 引入了堆外(Off-heap)内存,即JVM之外,使之可以直接在工作节点的系统内存中开辟空间,存储经过序列化的二进制数据。 这种模式不在 JVM 内申请内存,而是调用 Java 的 unsafe 相关 API 进行诸如 C 语言里面的 malloc() 直接向操作系统申请内存,由于这种方式不经过 JVM 内存管理,所以可以避免频繁的 GC,这种内存申请的缺点是必须自己编写内存申请和释放的逻辑。如果堆外内存被启动,堆外内存也会存在execution和storage内容,和On-heap中的execution和storage内存不同的是,前者不会被JVM的GC回收。
spark.driver.memoryOverhead | driverMemory * 0.10, with minimum of 384 | The amount of off-heap memory to be allocated per driver in cluster mode, in MiB unless otherwise specified. This is memory that accounts for things like VM overheads, interned strings, other native overheads, etc. This tends to grow with the container size (typically 6-10%). This option is currently supported on YARN and Kubernetes. 相当于spark.memory.offHeap.enabled+spark.memory.offHeap.size,只是该参数用于告知YARN和K8S来分配内存,和spark.memory.offHeap.size同时使用时需要比其大。 |
spark.executor.memoryOverhead | executorMemory * 0.10, with minimum of 384 | The amount of off-heap memory to be allocated per executor, in MiB unless otherwise specified. This is memory that accounts for things like VM overheads, interned strings, other native overheads, etc. This tends to grow with the executor size (typically 6-10%). This option is currently supported on YARN and Kubernetes. |
spark.memory.offHeap.enabled | false | If true, Spark will attempt to use off-heap memory for certain operations. If off-heap memory use is enabled, then spark.memory.offHeap.size must be positive. |
spark.memory.offHeap.size | 0 | The absolute amount of memory in bytes which can be used for off-heap allocation. This setting has no impact on heap memory usage, so if your executors’ total memory consumption must fit within some hard limit then be sure to shrink your JVM heap size accordingly. This must be set to a positive value when spark.memory.offHeap.enabled=true. |
BROADCAST, MERGE, SHUFFLE_HASH and SHUFFLE_REPLICATE_NL,
当使用BROADCAST hint在表t1上时,即使t1表的大小超过了spark.sql.autoBroadcastJoinThreshold,Spark也会也会优先考虑把t1作为build side。至于是broadcast hash join 或者broadcast nested loop join要取决于是否有等值连接条件。
当不同的join策略hint用于join两边时,Spark使用hint的优先级是BROADCAST>MERGE>SHUFFLE_HASH>SHUFFLE_REPLICATE_NL。当两端都指定BROADCAST hint或者SHUFFLE_HASH hint时,Spark将根据join的类型和表的大小来选择build side。注意,Spark不保证一定会使用指定的hint,因为某些join策略hint可能不支持所有join类型。
spark.table("src").join(spark.table("records").hint("broadcast"), "key").show()
Join Hints也可以用于SparkSQL
COALESCE, REPARTITION, and REPARTITION_BY_RANGE
SELECT /*+ COALESCE(3) */ * FROM t;
SELECT /*+ REPARTITION(3) */ * FROM t;
SELECT /*+ REPARTITION(c) */ * FROM t;
SELECT /*+ REPARTITION(3, c) */ * FROM t;
SELECT /*+ REPARTITION_BY_RANGE(c) */ * FROM t;
SELECT /*+ REPARTITION_BY_RANGE(3, c) */ * FROM t;
EXPLAIN EXTENDED SELECT /*+ REPARTITION(100), COALESCE(500), REPARTITION_BY_RANGE(3, c) */ * FROM t;
堆和栈都是Java用来在RAM中存放数据的地方。
堆:
JVM的GC日志的主要参数包括如下几个:
- -XX:+PrintGC 输出GC日志
- -XX:+PrintGCDetails 输出GC的详细日志
- -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
- -XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
- -XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
- -Xloggc:…/logs/gc.log 日志文件的输出路径
在较高的层次上,管理全GC发生的频率可以帮助减少开销,可以通过在作业的配置中设置spark.executor.extraJavaOptions来指定执行器的GC调优标志。通过在Java选项中添加-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps来实现,Spark会在executor端控制台日志中打印的消息。当您的程序存储的rdd有很大的“变动”时,JVM垃圾收集可能会成为一个问题。 (对于只读取一次RDD,然后在其上运行多次操作的程序来说,这通常不是问题。) 当Java需要清除旧对象来为新对象腾出空间时,它将需要跟踪所有Java对象并找到未使用的对象。 这里需要记住的要点是,垃圾收集的成本与Java对象的数量成比例,因此使用对象较少的数据结构(例如,使用int数组而不是LinkedList)会大大降低此成本。 一个更好的方法是以序列化的形式持久化对象,如上所述:现在每个RDD分区只有一个对象(一个字节数组)。 在尝试其他技术之前,如果GC存在问题,首先要尝试的是使用序列化缓存。
JAVA堆内部划分为年轻代和老年代,年轻代存储短生命周期的对象,而老年代存储长生命周期的对象。年轻一代被进一步划分为三个区域: Eden, Survivor1, Survivor2。GC的过程:当Eden满时,会在Eden上运行一个minor GC,并将来自Eden和Survivor1的活对象复制到Survivor2。交换Survivor区域。如果一个对象足够老或Survivor2已满,则将其移动到老年代。 最后,当老年代接近满时将调用一个完整的GC。
Runtime.getRuntime.maxMemory 是程序能够使用的最大内存,其值会比实际配置的执行器内存的值小。这是因为内存分配池的堆部分分为 Eden,Survivor 和 Tenured 三部分空间,而这里面一共包含了两个 Survivor 区域,而这两个 Survivor 区域在任何时候我们只能用到其中一个,所以我们可以使用下面的公式进行描述:
ExecutorMemory = Eden + 2 * Survivor + Tenured
Runtime.getRuntime.maxMemory = Eden + Survivor + Tenured
Spark中GC调优的目标是确保只有长寿命的rdd存储在Old代中,而Young代的大小足以存储短寿命的对象。 这将有助于避免完整的gc收集任务执行期间创建的临时对象。 一些可能有用的步骤是:
可以重复利用变量,减少重复定义变量对资源的消耗