Spark性能优化

                         Spark性能优化篇

Spark性能调优主要包括以下手段:

  1. 对RDD使用高性能序列化类库
  2. 优化数据结构
  3. 对多次使用的RDD进行持久化 / Checkpoint
  4. 使用序列化的持久化级别
  5. Java虚拟机垃圾回收调优
  6. 提高并行度
  7. 广播共享数据
  8. 数据本地化
  9. reduceByKey和groupByKey的合理使用
  10. Shuffle调优
  11. 算子调优

上述这几项,综合起来就是:
1)配置参数调优
2)代码调优
Spark参数可参考Spark官网
http://spark.apache.org/docs/latest/configuration.html#viewing-spark-properties

阅读对象:
1)如果你只是使用Spark SQL,那么可以只需要阅读正文;
2)如果你在工作中需要写代码,那么整篇文章都会非常符合你;

Spark 提交命令中-properties-file FILE 设置应用程序属性的文件路径,默认是conf/spark-defaults.conf。
Client vs Cluster任务执行概览
在这里插入图片描述
Client 模式
在这里插入图片描述
Cluster模式
deploy-mode
默认值:client, Spark官方推荐使用cluster模式
1)Driver程序的运行需要一定的内存,如果客户端配置较低,这个时候应该就用cluster集群模式;
2)Client模式:
driver所在机器可能跟yarn集群不在一起,所以性能相对差一些;
Cluster模式
driver(AM)就在yarn集群中,进行复杂调度的时候,网络通信性能较好;
spark-submit关键属性参数
在介绍关键参数之前,我们来先熟悉一下Spark内部任务的主要流程和一些重要概念。
在这里插入图片描述

三个概念:
1)Job是以Action方法为界,遇到一个Action方法则触发一个Job;
2)Stage是Job的子集,以RDD宽依赖(Shuffle)为界,遇到一次Shuffle做一次划分;
3)Task:Task是Stage的子集,以并行度(分区数)来衡量,分区数是多少就有多少个Task;
在这里插入图片描述

Spark RDD通过Transaction操作形成了RDD血缘关系图,即DAG图,最后通过Action的调用,触发Job并调度执行。DAGScheduler负责Stage级的调度,主要是将DAG切分成若干个Stages,并将每个Stage打包成Taskset交给TaskScheduler调度。TaskScheduler负责Task
级的调度,将DAGScheduler给过来的Taskset按照指定的调度策略分发到Executor上执行,调度过程中,SchedulerBackend负责提供可用资源,其中SchedulerBackend有多种实现,分别对接不同的资源管理器。

Stage级的调度
Spark的任务调度是从DAG切割开始,主要是由DAGScheduler来完成。当遇到一个Action操作后就会触发一个Job的计算,并交给DAGScheduler来提交,上图是涉及到Job提交的相关方法调用流程图
在这里插入图片描述

Job由最终的RDD和Action方法封装而成,SparkContext将Job交给DAGScheduler提交,它会根据RDD的血缘关系构成的DAG进行切分,将一个Job划分为若干Stages,具体划分策略是,由最终的RDD不断通过依赖回溯判断父依赖是否是宽依赖,即以Shuffle为界,划分Stage,窄依赖的RDD之间被划分到同一个Stage中,可以进行pipeline式的计算,如上图紫色流程部分。划分的Stages分两类,一类叫做ResultStage,为DAG最下游的Stage,由Action方法决定,另一类叫做ShuffleMapStage,为下游Stage准备数据

例如:
在这里插入图片描述
Job由saveAsTextFile触发,该Job由RDD-3和saveAsTextFile方法组成,根据RDD之间的依赖关系从RDD-3开始回溯搜索,直到没有依赖的RDD-0,在回溯搜索过程中,RDD-3依赖RDD-2,并且是宽依赖,所以在RDD-2和RDD-3之间划分Stage,RDD-3被划到最后一个Stage,即ResultStage中,RDD-2依赖RDD-1,RDD-1依赖RDD-0,这些依赖都是窄依赖,所以将RDD-0、RDD-1和RDD-2划分到同一个Stage,即ShuffleMapStage中,实际执行的时候,数据记录会一气呵成地执行RDD-0到RDD-2的转化。不难看出,其本质上是一个深度优先搜索算法。
一个Stage是否被提交,需要判断它的父Stage是否执行,只有在父Stage执行完毕才能提交当前Stage,如果一个Stage没有父Stage,那么从该Stage开始提交。Stage提交时会将Task信息(分区信息以及方法等)序列化并被打包成TaskSet交给TaskScheduler,一个Partition对应一个Task,另一方面监控Stage的运行状态,只有Executor丢失或者Task由于Fetch失败才需要重新提交失败的Stage以调度运行失败的任务,其他类型的Task失败会在TaskScheduler的调度过程中重试。
相对来说DAGScheduler做的事情较为简单,仅仅是在Stage层面上划分DAG,提交Stage并监控相关状态信息。TaskScheduler则相对较为复杂
Task级的调度
在这里插入图片描述
Spark Task的调度是由TaskScheduler来完成的,DAGScheduler将stage打包成taskset提交给TaskScheduler,TaskScheduler会将其封装为TasksetManager加入调度队列中,TasksetManager负责监控管理同一个stage的Tasks,TaskScheduler就是以TasksetManager为单元来调度任务,TaskScheduler初始化后会启动SchedulerBackend,它负责与外界打交道,接收Executor的注册信息,并维护Executor的状态,所以说SchedulerBackend是管“粮食”的。同时它会在启动后定期去询问Taskscheduler又饿米有任务要运行,也就是说他会定期的问“我有什么余粮,你要不要啊”,TaskScheduler在SchedulerBackend问它的时候,会从调度队列中按照指定的调度策略选择TasksetManager去调度执行。

Queue
在这里插入图片描述

Used Resources: <memory:493568, vCores:29>
该queue已使用的资源占queue的分配的资源的百分比;

Demand Resources: <memory:493568, vCores:29>
AM Used Resources: <memory:6144, vCores:3>
AM Max Resources: <memory:3756156, vCores:557>
Num Active Applications: 3
已经分配资源,正在运行的任务数;

Num Pending Applications: 0
正在等待分配资源,还未运行的任务数;

Min Resources: <memory:0, vCores:0>
最少资源保证量
当一个队列的最少资源保证量未满足时,它将优先于其他同级队列获得资源,对于不同的调度策略(后面会详细介绍),最少资源保证量的含义不同;

Max Resources: <memory:24576000, vCores:6000>
最多可以使用的资源量
fair scheduler会保证每个队列使用的资源量不会超过该队列的最多可使用资源量。

Reserved Resources: <memory:0, vCores:0>
Steady Fair Share: <memory:6299494, vCores:935>
Instantaneous Fair Share: <memory:7413138, vCores:1101>
Preemptable: false
Used Resources: <memory:493568, vCores:29>
Demand Resources: <memory:493568, vCores:29>
AM Used Resources: <memory:6144, vCores:3>
AM Max Resources: <memory:3756156, vCores:557>
Num Active Applications: 3
Num Pending Applications: 0
Min Resources: <memory:0, vCores:0>
Max Resources: <memory:24576000, vCores:6000>
Reserved Resources: <memory:0, vCores:0>
Steady Fair Share: <memory:6299494, vCores:935>
Instantaneous Fair Share: <memory:7413138, vCores:1101>
Preemptable: false

Memory & Core
在这里插入图片描述

什么是Driver

用户提交的应用程序代码在spark中运行起来就是一个driver,用户提交的程序运行起来就是一个driver,他是一个一段特殊的excutor进程,这个进程除了一般excutor都具有的运行环境外,这个进程里面运行着DAGscheduler Tasksheduler Schedulerbackedn等组件;
在这里插入图片描述

总结起来Driver具有如下的功能:
1)运行应用程序的main函数
2)创建spark的上下文
3)划分RDD并生成有向无环图(DAGScheduler)
4)与spark中的其他组进行协调,协调资源等等(SchedulerBackend)
5)生成并发送task到executor(taskScheduler)

Driver的功能
1)一个Spark作业运行时包括一个Driver进程,也是作业的主进程,具有main函数,并且有SparkContext的实例,是程序的人口点;
2)功能:负责向集群申请资源,向master注册信息,负责了作业的调度,,负责作业的解析、生成Stage并调度Task到Executor上。包括DAGScheduler,TaskScheduler。
driver-memory
默认值:1g
1)在客户端模式下,这个配置不能在SparkConf中直接设置(因为驱动器JVM都启动完;了),客户端模式下,必须要在命令行里用 –driver-memory 或者在默认属性配置文件里设置;
2)通常情况,设置1g左右应该够了,但是如果使用collect等算子将当RDD的数据全部拉回到Driver上进行处理,那么这个值需要做调整,否则会造成OOM;
3)并非越大越好,最大不能超过Driver所在机器的物理内存;

综合:配置的值根据实际需要以及物理机内存大小。
driver-cores
默认值:1
Driver程序使用的内核数,通常情况下1个已经满足;
–conf spark.driver.maxResultSize
默认值:1g,最小1MB,设置0为无限
每个Action算子的所有分区计算返回给Driver端的序列化数据的总大小,
典型场景:
ERROR TaskSetManager: Total size of serialized results of 8113 tasks (1131.0 MB) is bigger than spark.driver.maxResultSize (1024.0 MB)
num-executors
默认值:只会启动很少量的executor;
表示启动多少个executor来运行该作业;
建议:每个Spark任务一般设置在50~100个左右的Executor进程比较合适,设置的太少会导致集群的资源无法充分利用;设置的太多会导致部分executor空转,也可能存在队列无法分配所需资源;
特殊情况:当数据量或者计算量特别大的时候可酌情调大,相反可酌情调小;
executor-memory
默认值: 1g ;
建议:每个Executor进程的内存设置4g~8g较为合适。用户设置该值的时候需要考虑如下影响因子:
1)自己使用的executor-memory * num-executor所使用的资源不能超过所提交队列的阈值;
2)在队列资源共用的模式下,所申请的资源还要更小,以免申请不到资源或者阻塞其他用户的任务;
3)用户申请的executor-momory不能超过yarn设置的最大值,Yarn当前设置的最大值为60g;
executor-cores
默认值:1
用于设置每个executor进程的cpu core数量,这个参数决定了每个executor进程并行执行task线程的能力,每个cpu core同一时间只能执行一个task线程,因此executor 的cpu core数量越多,那么并发执行的task数量越多。
建议:executor的cpu core数量设置为2 ~ 4个比较合适。
影响因素:
1)num-executors * executor-cores不能超过队列的cpu core
2)在队列有大量任务提交的情况下,还要更少,以免影响其他用户提交的任务因申请不到cpu资源而卡主。
–conf spark.default.parallelism
该参数用于设置每个stage的默认task数量,这个参数极为重要,如果不设置可能会直接影响你的任务性能。
建议:
500 ~ 1000较为合适。
如果不设置这个参数
For distributed shuffle operations like reduceByKey and join, the largest number of partitions in a parent RDD. For operations like parallelize with no parent RDDs, it depends on the cluster manager:
Local mode: number of cores on the local machine
Mesos fine grained mode: 8
Others: total number of cores on all executor nodes or 2, whichever is larger
没有父RDD的情况下,默认是Max(所有executors的vcore数量, 2),Spark默认设置的数量是偏少的(比如就几十个task),如果task数量偏少的话,就会导致你前面设置好的executor的参数前功尽弃。 Spark官网建议该参数的值为 num-executors * executor-cores的2 ~ 3倍较为合适,此时可充分利用集群的资源。
–spark.storage.memoryFraction
默认值:0.6
该参数用于设置RDD持久化数据在executor内存中能占的比例,默认值为0.6,也就是说,默认executor 60%的内存可以用来保存持久化的RDD数据。
建议:
如果Spark作业中,有较多的RDD持久化操作,该参数的值可以适当提高一些,保证持久化的数据能够容纳在内存中,避免内存不够缓存所有的数据,导致数据只能写入磁盘,降低了任务执行的性能。但是如果spark作业中的shuffle类操作比较多,而持久化操作比较少,或者通过Spark UI观察发现作业的gc耗时较长,意味着task执行的时候内存不够,这个时候建议调低这个参数的值。
–conf spark.yarn.am.memory
默认值:512M
在客户端client模式下,yarn应用master使用的内存数,在集群模式下,在集群cluster模式下,使用spark.driver.memory代替。
–conf spark.broadcast.compress
默认值:true
在发送广播变量前是否压缩
Broadcast机制是用来减少每个task时,所需要发送给Task的RDD所用到的相关数据的尺寸,一个Executor只需要在第一个Task启动时,获得一份Broadcast数据,之后的Task都从本地的BlockManager中获得相关数据。
因为Broadcast的数据需要通过网络发送,而在executor端又需要存储在本地BlockManager中,为了减少网络传输开销和内存占用,通常都是有利于提高整理性能的。
–conf spark.io.compression.codec
默认值:lz4;
压缩RDD数据、日志、shuffle输出等的压缩编解码格式,Spark提供了三种选择:
lz4, lzf, snappy。也可以通过完整的类名来指定。
org.apache.spark.io.LZ4CompressionCodec,
og.apache.spark.io.LZFCompressionCodec,
org.apache.spark.io.SnappyCompressionCodec
–conf spark.yarn.executor.memoryOverhead
在这里插入图片描述

Spark的Excutor的Container内存有两大部分组成:堆外内存和Excutor内存。
其中
堆外内存
由spark.yarn.executor.memoryOverhead参数设置。 主要用于JVM自身的开销。
默认:MAX(executorMemory* MEMORY_OVERHEAD_FACTOR,384MB)
这里384 的含义为,JVM自身开销,最小的需求是384MB

Excutor内存
由spark.executor.memory参数设置,分为两部分。
Execution:shuffle、排序、聚合等用于计算的内存
Storage:用于集群中缓冲和传播内部数据的内存(cache、广播变量)

1.每个 Executor 所申请的内存 = spark-executor-memory + spark.yarn.executor.memoryOverhead
2.spark.yarn.executor.memoryOverhead = Max( 384MB, MEMORY_OVERHEAD_FACTOR * spark.executor-memory )
其中MEMORY_OVERHEAD_FACTOR默认为0.1
也就是说,如果我们为每个 Executor 申请 20GB内存,AM 实际上将会申请 20GB + memoryOverhead = 20 + 20 * 10% ~= 22GB。
MemoryOverhead是JVM进程中除Java堆以外占用的空间大小,包括方法区(永久代)、Java虚拟机栈、本地方法栈、JVM进程本身所用的内存、直接内存(Direct Memory)等。通过spark.yarn.executor.memoryOverhead设置,单位MB。
如果用于存储RDD的空间不足,先存储的RDD的分区会被后存储的覆盖。当需要使用丢失分区的数据时,丢失的数据会被重新计算
如果Java堆或者永久代的内存不足,则会产生各种OOM异常,executor会被结束。spark会重新申请一个container运行executor。失败executor上的任务和存储的数据会在其他executor上重新计算。
如果实际运行过程中ExecutorMemory+MemoryOverhead之和(JVM进程总内存)超过container的容量。YARN会直接杀死container。executor日志中不会有异常记录。spark同样会重新申请container运行executor。
在Java堆以外的JVM进程内存占用较多的情况下,应该将MemoryOverhead设置为一个足够大的值,应该将MemoryOverhead设置为一个足够大的值,以防JVM进程因实际占用的内存超标而被kill。如果默认值(math.max((MEMORY_OVERHEAD_FACTOR *executorMemory).toInt,MEMORY_OVERHEAD_MIN)不够大,可以通过spark.yarn.executor.memoryOverhead手动设置一个更大的值。
–conf spark.yarn.maxAppAttempts
默认使用yarn资源管理器的配置,如果设置则会覆盖yarn的配置, 当前yarn默认值为6
用于任务执行失败,ApplicationMaster重试的次数.
重试带来的劣势:
导致过多的系统资源被无效占用

Yarn中的配置
yarn.resourcemanager.am.max-attempts

–conf spark.yarn.max.executor.failures
最大应用程序尝试次数。 它是所有AM的全局设置

–conf spark.task.maxFailures
在spark程序中,task有失败重试机制(根据 spark.task.maxFailures 配置,默认是4次),当task执行失败时,并不会直接导致整个应用程序down掉,只有在重试了 spark.task.maxFailures 次后任然失败的情况下才会使程序down掉。另外,spark on yarn模式还会受yarn的重试机制去重启这个spark程序,根据 yarn.resourcemanager.am.max-attempts 配置(默认是2次)。

Dynamic动态资源配置
在这里插入图片描述

基于工作负载通过后台线程定时来动态分配,移除executor资源以及取消多余的executor申请。当目前的executor 超过了工作负载的需求时则减少executor数量, 当前executor的数量总是被截断到可同时运行当前运行中及等待的task综合的executor数量上。作为对等待调度的积压的task的响应,ExecutorAllocationManager会增加目标executor的数量。移除策略很简单,如果一个executor已经空闲了指定的时间,则将他移除销毁。
如果设置了动态executor,也就是将spark.dynamicAllocation.enabled=true,那么spark将使用动态executor,覆盖指定的executor配置
–conf spark.dynamicAllocation.enabled
默认值:false
是否开启动态资源配置,根据工作负载来衡量是否应该增加或减少executor
–conf spark.dynamicAllocation.minExecutors
默认值:0
动态分配最小executor个数,在启动时就申请好的
–conf spark.dynamicAllocation.maxExecutors
默认值:infinity无穷大
动态分配最大executor个数
–conf spark.dynamicAllocation.initialExecutors
动态分配初始executor个数默认值=spark.dynamicAllocation.minExecutors
–conf spark.dynamicAllocation.executorIdleTimeout
默认值:60s
当某个executor空闲超过这个设定值,就会被kill
–conf spark.dynamicAllocation.cachedExecutorIdleTimeout
当某个缓存数据的executor空闲时间超过这个设定值,就会被kill,默认infinity无穷大;

Spark数据本地化策略
Spark在Driver上对Application的每个task任务进行分配之前,都会先计算出每个task要计算的对应的数据分片的位置。Spark的task分配算法优先考虑将task分配到分片数据所在的节点,以此来避免网络间数据传输带来的额性能消耗。但是在实际的生产环境中并不是我们所想象的那样,有可能某些task没有机会把它分配到所要计算的数据所在的节点;在这个时候,task会等待一段时间,默认情况下是3s,等待时间过了以后,还是没有轮到其执行,task就会选择一种比较差的本地化级别去进行,但是选择了其他的本地化级别就会出现网络数据传输的情况。

Spark中任务的处理需要考虑所涉及的数据的本地性的场合,基本就两种:
1)数据的来源是HadoopRDD
2)RDD的数据来源来自RDD Cache(即由CacheManager从BlockManager中读取,或者Streaming数据源RDD)
在这里插入图片描述
在这里插入图片描述

PROCESS_LOCAL
进程本地化, task要计算的数据在同一个Executor中。
在这里插入图片描述

NODE_LOCAL
节点本地化,速度比process_local稍慢,因为数据需要再不同进程之间传递或者从文件中读取:
1)task要计算的数据是在同一个Worker的不同executor进程中;
2)task要计算的数据是在同一个worker的磁盘上,或者在hdfs上,恰好有block在同一个节点上。
Spark计算数据来源于HDFS,那么最好的数据本地化级别就是NODE_LOCAL
在这里插入图片描述
NODE_PREF
没有最佳位置一说,数据从哪里访问都一样快,不需要位置优先,比如说Spark SQL读取MYSQL中的数据

RACK_LOCAL
机架本地化,数据在同一机架的不同节点上,需要通过网络传输数据及文件IO,比NODE_LOCAL慢。
情况1:task计算的数据在worker2的executor中
情况2:task计算的数据在worker2的磁盘上。

ANY 跨机架
数据在非同一机架的网络上,速度最慢。

–conf spark.locality.wait
默认值:3s
默认情况下,下边3个参数的值与spark.locality.wait 参数值一致
–conf spark.locality.wait.process
–conf spark.locality.wait.node
–conf spark.locality.wait.rack
Speculation 推测执行
在Hadoop中,当一个应用向Yarn提交应用程序后,此作业的多个任务由于负载不均衡,资源分布不均匀等各种原因,会导致各个任务执行时间的不同,甚至出现一个task明显慢于其他task的情况。
则这些task拖慢了作业的整体执行进度,为了避免这种情况发生,Hadoop会为该task启动备份任务,让该speculative task与原始task同时处理一份数据,哪个先运行完,则将谁的结果作为最终结果。
  推测执行优化机制采用了典型的以空间换时间的优化策略,它同时启动多个相同task(备份任务)处理相同的数据块,哪个完成的早,则采用哪个task的结果,这样可防止拖后腿Task任务出现,进而提高作业计算速度,但是,这样却会占用更多的资源,在集群资源紧缺的情况下,设计合理的推测执行机制可在多用少量资源情况下,减少大作业的计算时间。

执行原则:
先过滤掉已经成功执行的task,另外,推测执行task不在和正在执行的task同一Host执行,不在黑名单executor里执行,然后在延迟调度策略下根据task的优先位置来决定是否在该executor上以某种本地性级别被调度执行
在这里插入图片描述

–conf spark.speculation
默认值:false
是否推测执行
建议设置为true

–conf spark.speculation.interval
默认值:100ms
多长时间检查一次是否推测执行
建议调大

–conf spark.speculation.quantile
默认值:0.75
超过大部分task执行时长就需要推测执行

Schedule
为什么Spark Application在没有获得足够的资源,job就开始执行了,可能会导致什么什么问题发生?
答:会导致执行该job时候集群资源不足,导致执行job结束也没有分配足够的资源,分配了部分Executor,该job就开始执行task,应该是task的调度线程和Executor资源申请是异步的;如果想等待申请完所有的资源再执行job的:需要将spark.scheduler.maxRegisteredResourcesWaitingTime设置的很大;spark.scheduler.minRegisteredResourcesRatio 设置为1,但是应该结合实际考虑否则很容易出现长时间分配不到资源,job一直不能运行的情况

–conf spark.scheduler.maxRegisteredResourcesWaitingTime
在执行前最大等待申请资源的时间,默认30s

–conf spark.scheduler.minRegisteredResourcesRatio
实际注册的资源数占预期需要的资源数的比例,默认0.8

The minimum ratio of registered resources (registered resources / total expected resources) (resources are executors in yarn mode, CPU cores in standalone mode and Mesos coarsed-grained mode [‘spark.cores.max’ value is total expected resources for Mesos coarse-grained mode] ) to wait for before scheduling begins. Specified as a double between 0.0 and 1.0. Regardless of whether the minimum ratio of resources has been reached, the maximum amount of time it will wait before scheduling begins is controlled by config spark.scheduler.maxRegisteredResourcesWaitingTime.
default : 0.8 for YARN mode; 0.0 for standalone mode and Mesos coarse-grained mode

–conf spark.scheduler.listenerbus.eventqueue.capacity
spark事件监听队列容量,默认10000,必须为正值,增加可能会消耗更多内存

–conf spark.blacklist.enabled
是否列入黑名单,默认false。如果设成true,当一个executor失败好几次时,会被列入黑名单,防止后续task派发到这个executor。可以进一步调节spark.blacklist以下相关的参数

IO / NetWork
在这里插入图片描述
–conf spark.network.timeout
默认值:120s,
各种网络交互延时
不设置其值会覆盖如下几个参数的值
–conf spark.core.connection.ack.wait.timeout
–conf spark.akka.timeout
–conf spark.storage.blockManagerSlaveTimeoutMs
–conf spark.shuffle.io.connectionTimeout
–conf spark.rpc.askTimeout
–conf spark.rpc.lookupTimeout

网络或者GC引起Driver或者executor没有接收到executor或task的心跳反馈,可提高该参数值。
-conf spark.network.timeout=300
Shuffle
什么时候发生?
Spark Shuffle 是 spark job 中某些算子触发的操作。当 rdd 依赖中出现宽依赖的时候,就会触发 Shuffle 操作,Shuffle 操作通常会伴随着不同 executor/host 之间数据的传输。
Shuffle 操作可能涉及的过程包括数据的排序,聚合,溢写,合并,传输,磁盘IO,网络的 IO 等等。Shuffle 是连接 MapTask 和 ReduceTask 之间的桥梁,Map 的输出到 Reduce 中须经过 Shuffle 环节,Shuffle 的性能高低直接影响了整个程序的性能和吞吐量。
在这里插入图片描述
未优化的shuffle

在这里插入图片描述
优化过的shuffle
–conf spark.shuffle.consolidateFiles
为了解决产生大量文件的问题,我们可以在map端输出的位置,将文件进行合并操作;

–conf spark.shuffle.blockTransferService
默认值:在Spark 1.2.0,这个配置的默认值是netty,而之前是nio;
这个主要是用于在各个Executor之间传输Shuffle数据,Netty的实现更加简洁,但实际上用户不用太关心这个选项。除非是有特殊的需求,否则采用默认配置就可以
–conf spark.shuffle.io.maxRetries
默认值:3
参数说明:shuffle read task从shuffle write task所在节点拉取属于自己的数据时,如果因为网络异常导致拉取失败,是会自动进行重试的。该参数就代表了可以重试的最大次数。如果在指定次数之内拉取还是没有成功,就可能会导致作业执行失败

调优建议:对于那些包含了特别耗时的shuffle操作的作业,建议增加重试最大次数(比如60次),以避免由于JVM的full gc或者网络不稳定等因素导致的数据拉取失败。在实践中发现,对于针对超大数据量(数十亿~上百亿)的shuffle过程,调节该参数可以大幅度提升稳定性
–conf spark.shuffle.manager
–conf spark.shuffle.sort.bypassMergeThreshold
默认值:sort
Spark 1.5以后,有三个可选项:hash、sort和tungsten-sort。HashShuffleManager是Spark 1.2以前的默认选项,但是Spark 1.2以及之后的版本默认都是SortShuffleManager了。tungsten-sort与sort类似,但是使用了tungsten计划中的堆外内存管理机制,内存使用效率更高。

调优建议:由于SortShuffleManager默认会对数据进行排序,因此如果你的业务逻辑中需要该排序机制的话,则使用默认的SortShuffleManager就可以;而如果你的业务逻辑不需要对数据进行排序,那么建议参考后面的几个参数调优,通过bypass机制或优化的HashShuffleManager来避免排序操作,同时提供较好的磁盘读写性能。这里要注意的是,tungsten-sort要慎用,因为之前发现了一些相应的bug
–conf spark.shuffle.file.buffer
什么是map端内存缓冲区呢?默认情况下,每个map端的task 输出的一些中间结果在写入磁盘之前,会先被写入到一个临时的内存缓冲区,这个缓冲区的默认大小为32kb,当内存缓冲区满溢之后,才会将产生的中间结果spill到磁盘上作者

–conf spark.shuffle.memoryFraction
reduce端内存占比又是什么呢?reduce端的task在拉取到数据之后,会用一个hashmap的数据结构对各个key对应的value进行汇聚操作。在进行汇聚操作的时候,其使用的内存是由executor进程给分配的,默认将executor的内存的20%分配给reduce task 进行聚合操作使用。这里会有一个问题,当reduce task拉取的数据很多导致其分配的内存放不下的时候,这个时候会将放不下的数据全部spill到磁盘上去。
–conf spark.shuffle.compress
–conf spark.shuffle.spill.compress
默认值:true。
spark.shuffle.compress和spark.shuffle.spill.compress都是用来设置Shuffle过程中是否对Shuffle数据进行压缩;其中前者针对最终写入本地文件系统的输出文件,后者针对在处理过程需要spill到外部存储的中间数据,后者针对最终的shuffle输出文件

如何设置spark.shuffle.compress?

如果下游的Task通过网络获取上游Shuffle Map Task的结果的网络IO成为瓶颈,那么就需要考虑将它设置为true:通过压缩数据来减少网络IO。由于上游Shuffle Map Task和下游的Task现阶段是不会并行处理的,即上游Shuffle Map Task处理完成,然后下游的Task才会开始执行。因此如果需要压缩的时间消耗就是Shuffle MapTask压缩数据的时间 + 网络传输的时间 + 下游Task解压的时间;而不需要压缩的时间消耗仅仅是网络传输的时间。因此需要评估压缩解压时间带来的时间消耗和因为数据压缩带来的时间节省。如果网络成为瓶颈,比如集群普遍使用的是千兆网络,那么可能将这个选项设置为true是合理的;如果计算是CPU密集型的,那么可能将这个选项设置为false才更好。

如何设置spark.shuffle.spill.compress?

如果设置为true,代表处理的中间结果在spill到本地硬盘时都会进行压缩,在将中间结果取回进行merge的时候,要进行解压。因此要综合考虑CPU由于引入压缩解压的消耗时间和Disk IO因为压缩带来的节省时间的比较。在Disk IO成为瓶颈的场景下,这个被设置为true可能比较合适;如果本地硬盘是SSD,那么这个设置为false可能比较合适

–spark.sql.shuffle.partitions
在有JOIN或聚合等需要shuffle的操作时,从mapper端写出的partition个数,默认设置为2000。
如select a, avg© from test_table group by a语句,不考虑优化行为,如果一个map端的task中包含有3000个a,根据spark.sql.shuffle.partitions=2000,会将计算结果分成2000份partition(例如按2000取余),写到磁盘,启动2000个reducer,每个reducer从每个mapper端拉取对应索引的partition。
当作业数据较多时,适当调大该值,当作业数据较少时,适当调小以节省资源。

–conf spark.reducer.maxSizeInFlight

–conf spark.shuffle.file.buffer

–spark.sql.adaptive.enabled
是否开启调整partition功能,如果开启,spark.sql.shuffle.partitions设置的partition可能会被合并到一个reducer里运行。默认开启,同时强烈建议开启。理由:更好利用单个executor的性能,还能缓解小文件问题。

–spark.sql.adaptive.shuffle.targetPostShuffleInputSize
和spark.sql.adaptive.enabled配合使用,当开启调整partition功能后,当mapper端两个partition的数据合并后数据量小于targetPostShuffleInputSize时,Spark会将两个partition进行合并到一个reducer端进行处理。平台默认为67108864(64M),用户可根据自身作业的情况酌情调整该值。当调大该值时,一个reduce端task处理的数据量变大,最终产出的数据,存到HDFS上的文件也变大。当调小该值时,相反。

–spark.sql.adaptive.minNumPostShufflePartitions
当spark.sql.adaptive.enabled参数开启后,有时会导致很多分区被合并,为了防止分区过少,可以设置spark.sql.adaptive.minNumPostShufflePartitions参数,防止分区过少而影响性能。

在这里插入图片描述

(1)Driver的JVM参数:
1) 如果是yarn-cluster模式:
-Xmx,-Xms–读取的是spark-default.conf文件中的spark.driver.extraJavaOptions对应的JVM参数值。
PermSize-------读取的是spark-default.conf文件中的spark.driver.extraJavaOptions对应的JVM参数值。
GC方式--------读取的是spark-default.conf文件中的spark.driver.extraJavaOptions对应的参数值。

  2) 如果是yarn-client模式,
    -Xmx,-Xms--默认读取spark-env文件中的SPARK_DRIVER_MEMORY值,-Xmx,-Xms值一样大小;
    PermSize-----是默认读取spark-class文件中的JAVA_OPTS="-XX:MaxPermSize=256m $OUR_JAVA_OPTS"值;
    GC方式----默认读取的是spark-class文件中的JAVA_OPTS;

以上值最后均可被spark-submit工具中的–driver-java-options参数覆盖。

(2)Executor的JVM参数:
-Xmx,-Xms,
如果是yarn-cluster模式,则读取的是spark-default.conf文件中的spark.executor.extraJavaOptions对应的JVM参数值。
如果是yarn-client模式,则默认读取spark-env文件中的SPARK_EXECUTOR_MEMORY值,-Xmx,-Xms值一样大小;
PermSize,两种模式都是读取的是spark-default.conf文件中的spark.executor.extraJavaOptions对应的JVM参数值。
GC方式,两种模式都是读取的是spark-default.conf文件中的spark.executor.extraJavaOptions对应的JVM参数值;

EventLog日志
通过相应的配置,Spark应用程序在运行完应用程序之后,将应用程序的运行信息写入指定目录, 而Spark history server可以将这些运行信息装载并以web的方式供用户浏览
–conf spark.yarn.historyServer.address
Spark history server的地址
–conf spark.eventLog.enabled
默认值:false
是否记录Spark事件,用于应用程序在完成后重构webUI
–conf spark.eventLog.dir
该属性为记录spark事件的根目录。在此根目录中,Spark为每个应用程序创建分目录,并将应用程序的事件记录到在此目录中。用户可以将此属性设置为HDFS目录,以便history server读取该日志文件重构WEB UI
序列化
–conf spark.serializer
–conf spark.kryo.registrationRequired
默认值:org.apache.spark.serializer.JavaSerializer
数据在NetWork IO或者Disk IO传输过程中使用的序列化方式
也可以设置为org.apache.spark.serializer.KryoSerializer

该参数会同时作用于机器之间数据的shuffle操作以及序列化rdd到磁盘,内存

相较于 Java serialization 的方式,速度更快,空间占用更小,但并不支持所有的序列化格式,同时使用的时候需要注册class。spark-sql中默认使用的是kyro的序列化方式。

conf.set(“spark.serializer”, “org.apache.spark.serializer.KryoSerializer”)
conf.set(“spark.kryo.registrationRequired”, “true”)
conf.registerKryoClasses(Array(classOf[Info], classOf[scala.collection.mutable.WrappedArray.ofRef[_]]))

如果你没有注册需要序列化的class,Kyro依然可以照常工作,但会存储每个对象的全类名(full class name),这样的使用方式往往比默认的 Java serialization 还要浪费更多的空间

Spark ExtraLib
在这里插入图片描述

–conf spark.driver.extraClassPath
额外的classpath,将插入到到驱动器的classpath开头。
注意:驱动器如果运行客户端模式下,这个配置不能通过SparkConf 在程序里配置,因为这时候程序已经启动呀!而是应该用命令行参数(–driver-class-path)或者在 conf/spark-defaults.conf 配置
–conf spark.driver.extraLibraryPath
添加到执行器(executor)classpath开头的classpath。主要为了向后兼容老的spark版本,不推荐使用。

Client模式下只需要确保Driver所在节点存在该目录就行
Cluster模式需要集群中的所有节点都存在这个目录以及目录中的文件
–conf spark.files.userClassPathFirst
由于默认情况下,优先级SPARK_HOME/lib/jar包 > 用户程序中的jar包, 如果想让用户程序jar优先执行,那么要使用 spark.yarn.user.classpath.first (spark1.3以前)或者 spark.executor.userClassPathFirst 和spark.driver.userClassPathFirst 参数。
这些参数能让用户的jar覆盖SPARK_HOME/lib的jar。在spark conf中将他们设置为"true"即可;

Spark Streaming
Spark Streaming是将流式计算分解成一系列短小的批处理作业,也就是把Spark Streaming的输入数据按照batch size(如1s)分成一段一段的数据,每一段数据都转换成Spark中的RDD。

在这里插入图片描述
在这里插入图片描述

Discretized Streams或DStream是Spark Streaming提供的基本抽象。 它表示连续的数据流,即从源接收的输入数据流或通过转换输入流生成的已处理数据流。 在内部,DStream由连续的RDD系列表示,这是Spark对不可变的分布式数据集的抽象。 DStream中的每个RDD都包含来自特定时间间隔的数据;
Reciever模式
在这里插入图片描述
原理:
Spark Core在初始化时会生成一个SparkContext对象来对数据进行后续的处理,相对应的Spark Streaming会创建一个Streaming Context,它的底层是SparkContext,也就是说它会将任务提交给SparkContext来执行,这也很好的解释了DStream是一系列的RDD。当启动Spark Streaming应用的时候,首先会在一个节点的Executor上启动一个Receiver接受者,然后当从数据源写入数据的时候会被Receiver接收,接收到数据之后Receiver会将数据Split成很多个block,然后备份到各个节点(Replicate Blocks 容灾恢复),然后Receiver向StreamingContext进行块报告,说明数据在那几个节点的Executor上,接着在一定间隔时间内StreamingContext会将数据处理为RDD并且交给SparkContext划分到各个节点进行并行计算。

val kafkaStream = KafkaUtils.createStream(ssc, zookeepers, groupId, topicsMap, StorageLevel.MEMORY_AND_DISK_SER)

Reciever模式读取数据,为了解决数据丢失的问题,采用了WAL机制,Write Ahead Log预写日志机制,将数据备份到HDFS中一份,完成之后向zk中更新offset,发送task处理数据,开启WAL机制之后带来的新问题:1:数据处理变慢,延迟大 2:存在存储空间浪费。
–conf spark.streaming.receiver.maxRate
在Spark streaming中每秒的数据记录吞吐量
因为当streaming程序的数据源的数据量突然变得巨大,可能会导致streaming被撑住导致吞吐不过来,所以可以考虑对于最大吞吐做一下限制
–conf spark.streaming.blockInterval
默认: 200ms
对于大多数的reciever来说,会把接收到的数据合并成一个数据块然后存储到Spark的内存中,每一批次中数据块的数量决定了要使用多少个任务来处理这些数据,每一个receiver的每一个batch上的task数量约等于 batch interval / block interval
–conf spark.streaming.backpressure.enabled
–conf spark.streaming.kafka.maxRatePerPartition
如果在一个batch内接收到的消息比较多,这就需要executor分配更多的内存,可能会导致其他spark streaming应用程序资源分配不足,甚至出现OOM的风险,反压可以限制每个batch接收到的消息量,降低数据倾斜的风险;
spark.streaming.backpressure.enabled =true表示开启反压,spark.streaming.kafka.maxRatePerPartition表示每个batch从Kafka的每个topic的每个
partition获取消息的最大条数。
–conf spark.scheduler.mode
–conf spark.streaming.concurrentJobs
默认值:1
一个 batch 的数据对应一个 DStreamGraph,而一个DStreamGraph包含一个或多个DStream的输出操作,每一个输出对应于一个job,一个DStreamGraph对应一个JobSet,里面包含一个或多个job;
concurrentJobs参数初始化jobExecutor线程池,也就是这个参数直接影响了job executor线程池中的线程数目;

Direct模式
在这里插入图片描述
特点:
1.并不需要单独线程实时接收数据,而是每隔batch size时间抓取数据;

2.kafka相当于文件系统,每个分区相当于从hdfs中读取数据时的每个block块,Dstream的分区和kafka的分区是相同的,要改变并行度必须要重新分区重新分区会有shuffle;

3.读取kafka中的数据之后不需要更新offset到zookeeper而是spark自己管理,把offset存储到内存中,如果设置了checkpoint那么offset会保存到文件中一份保存在内存一份;

4.不需要设置WAL,减少了存储数据到hdfs的步骤增加了job的执行速度;

Spark会创建跟Kafka partition一样多的RDD partition,并且会并行从Kafka中读取数据,所以在Kafka partition和RDD partition之间,有一个一对一的映射关系
Spark RBO CBO原理
在这里插入图片描述

基于规则优化RBO
是指通过一系列预先定义好的规则Rule对逻辑计划进行等价转换,以提高查询效率。
RBO的两个主要思路是:
1)减少参与计算的数据量。
2)降低重复计算的代价。

RBO更多的基于经验的,也就是事先制定规则,这个规则在大部分查询场景都是有用并且高效的,但是其缺点就是不够灵活,毕竟这个阶段对物理上的特征,比如表的底层存储形式和真正的数据量都还没有感知。
在这里插入图片描述

上面的图中包含了3种最常见也是最有效的RBO方式:
1)常量折叠(ConstantFolding)
上述语句中有一个纯常量表达式,即100 – 99。如果行数很多的话,每行都需要计算一次该表达式的值,积少成多就浪费了很多时间,所以通过常量折叠可以将它预先转化为1,消除不必要的重复计算。

2)谓词下推(PushdownPredicate)
如果能够将SQL语句的谓词逻辑(where条件,join on中的谓词条件)都尽量提前执行,下游处理已经过滤完毕的数据,能够减少工作量。

3)列裁剪(ColumnPrunning)
在未优化的逻辑计划中,Join Inner与Filter操作符都会扫描多列,然后再由Project操作符选出结果列,但实际上,我们可以在初始单独扫描表时就只筛选出符合后续逻辑计划的最小列集合,同样能够节省很多资源,如果物理上使用Parquet, ORC等列式格式存储的,效率就会更高。

基于代价优化CBO
CBO是根据实际数据分布和组织情况,评估每个计划的执行代价,从而选择代价最小的执行计划。
目前Spark的优化器都是基于RBO的,已经有数十条优化规则,例如谓词下推,常量折叠,投影裁剪等,这些规则是有效的,但是它对数据是不敏感的。导致的一个问题就是数据表中数据分布发生变化时,RBO是不感知的,基于RBO生成的执行计划不能确保最优的。而CBO的重要作用就是根据实际数据分布情况估算出SQL语句,生成一组可能被使用的执行计划中代价最小的执行计划,从而提升性能。

目前CBO主要的优化点是Join算法的选择,举个简单例子,当两个表做Join操作,如果其中一张原本很大的表经过Filter操作之后结果集小于Broadcast的阈值,在没有CBO的情况下是无法感知大表过滤后变小的情况,采用SortMergeJoin算法,涉及到大量的Shuffle操作,很耗性能;在有CBO的情况下是可以感知到结果集的变化,采用BroadcastHashJoin 算法,会将过滤后的小表Broadcast到每个节点,转变为非Shuffle操作,从而大大提高性能。

操作步骤:
1)设置配置项
在spark-default.conf文件中增加配置项spark.sql.cbo true,默认为false
在客户端执行SQL语句 set spark.sql.cbo=true进行配置。
2)执行统计信息生成命令,得到统计信息。

注意 :
此步骤只需在运行所有SQL前执行一次。如果数据集发生了变化(插入、更新或删除),
为保证CBO的优化效果,需要对有变化的表或者列再次执行统计信息生成命令重新生成统计信息,以得到最新的数据分布情况。
● 表:
执行COMPUTE STATS FOR TABLE src命令计算表的统计信息,统计信息包括记录条数、文件数和物理存储总大小。
● 列:
1)执行COMPUTE STATS FOR TABLE src ON COLUMNS命令计算所有列的统计信息;
2)执行COMPUTE STATS FOR TABLE src ON COLUMNS name,age命令计算表中name和
age两个字段的统计信息。当前列的统计信息支持四种类型:数值类型、日期类型、时间类型和字符串类型。

对于数值类型、日期类型和时间类型,统计信息包括:Max、Min、不同值个数(Number of
Distinct Value,NDV)、空值个数(Number of Null)和Histogram(支持等宽、等高直方图);
对于字符串类型,统计信息包括:Max、Min、Max Length、Average Length、不同值个
数(Number of Distinct Value,NDV)、空值个数(Number of Null)和Histogram(支持等宽直方图)。

CBO调优
● 自动优化:用户根据自己的业务场景,输入SQL语句查询,程序会自动去判断输入
的SQL语句是否符合优化的场景,从而自动选择Join优化算法。
● 手动优化:用户可以通过DESC FORMATTED src命令查看统计信息,根据统计信息的
分布,人工优化SQL语句。

Spark SQL优化
Spark SQL提供对表的缓存功能,可以极大的改善程序的性能。Spark SQL采用内存中的列式存储来缓存数据,这样的结构不仅可以有效的利用缓存空间,还使得在缓存之后的查询可以仅依赖于数据的子集.

表的缓存
在这里插入图片描述
Coalesce 和 Repartition 暗示(Hint)
在 Spark 2.4 之前,我们是不能直接在 SQL 里面使用 coalesce 或 repartition的, 值得高兴的是,国内的开发者为 Spark SQL 开发了一个功能,使得我们在 Spark SQL 里面也能用这两个函数,详见 SPARK-24940。这个功能在 Spark 2.4 已经发布了,这样我们可以通过 COALESCE 或 REPARTITION 关键字暗示来设置程序的并行度

比如:
create table iteblog1 as select /*+ REPARTITION(4) / age,count() from person where age between 10 and 20 group by age

下面看一下spark sql的各种调优参数
–conf spark.sql.codegen
默认值:false, 动态字节码技术
SparkSQL在执行物理计划的时候,对匹配的表达式采用特定的代码,动态编译,然后运行
若设置为true,Spark SQL会将每个查询都编译为Java字节码;

动态字节码技术中不涉及虚函数的调用,其本质就是scala的反射机制,我们知道,虚函数的调用时是运行时多态,意思就是在编译期你是无法知道虚函数的具体调用。设想一下,如果说不是虚函数,那么在编译时期,其相对地址是确定的,编译器可以直接生成jmp/invoke指令; 如果是虚函数,多出来的一次查找vtable所带来的开销,倒是次要的,关键在于,这个函数地址是动态的,譬如 取到的地址在eax里,则在call eax之后的那些已经被预取进入流水线的所有指令都将失效。流水线越长,一次分支预测失败的代价也就越大;
–conf spark.sql.files.openCostInBytes
默认值:4M,
表示小于4M的小文件会合并到一个分区中,用于减小小文件,防止太多单个小文件占一个分区情况
–conf spark.sql.autoBroadcastJoinThreshold
默认值:10M
对于broadcast join模式,会将小于spark.sql.autoBroadcastJoinThreshold值(默认为10M)的表广播到其他计算节点,不走shuffle过程,所以会更加高效

–conf spark.sql.shuffle.partitions
默认值: 200
用于配置 join 或聚合操作混洗(shuffle)数据时使用的分区数
spark.default.parallelism只有在处理RDD时才会起作用,对Spark SQL的无效
spark.sql.shuffle.partitions则是对sparks SQL专用的设置
–conf spark.sql.files.maxPartitionBytes
默认值: 128M
读取文件时单个分区可容纳的最大字节数
–conf spark.sql.inMemoryColumnarStorage.compressed
默认值: true
Spark SQL 将会基于统计信息自动地为每一列选择一种压缩编码方式
–conf spark.sql.inMemoryColumnarStorage.batchSize
默认值: 10000
缓存批处理大小, 较大的批处理可以提高内存利用率和压缩率,但同时也会带来 OOM(Out Of Memory)的风险,在缓存 SchemaRDD 时,Spark SQL 会按照这个选项制定的大小(默认值是 1000)把记录分组,然后分批压缩。太小的批处理大小会导致压缩比过低,而批处理大小过大的话,也有可能引发内存问题。如果你表中的记录比较大,你就可能需要调低批处理大小来避免内存不够(OOM)的错误。如果不是在这样的场景下,默认的批处理大小是比较合适的,因为压缩超过 1000 条记录时也基本无法获得更高的压缩比了.

自动处理数据倾斜
对于那些存在大量数据量较小的partition,可以通过合并来解决问题(一个task处理多个partition的数据)。对于数据量特别大的partition,可以反其道而行,用多个task来处理这个partition。

开启自动处理数据倾斜后,在执行过程中,spark会自动找出那些出现倾斜的partition,然后用多个task来处理这个partition,这后再将这些task的处理结果进行union。

比如表A和表B做join,表A在shuffle write完,partition 0有4G的数据,其他partition都只有200M。这时候我们可以开启多个task,每个task读取几个上游mapper生成的partition 0的数据,然后和表B的partition 0做join,最后这几个task再进行union。这样虽然表B的partition 0要被多次读取,但是并行处理带来的收益还是要高过这些消耗的。
–conf spark.sql.adaptive.skewedJoin.enabled
–conf spark.sql.adaptive.skewedPartitionMaxSplits
控制处理一个倾斜Partition的Task个数上限,默认值为5
–conf spark.sql.adaptive.skewedPartitionRowCountThreshold
partition的数据条数如果少于这个值,数据量再大也不会被当成倾斜的partition。默认是1000W
–conf spark.sql.adaptive.skewedPartitionSizeThreshold
被认定为是倾斜partition的大小下限,默认是64M
–conf spark.sql.adaptive.skewedPartitionFactor
倾斜因子,如果一个Partition的大小大于spark.sql.adaptive.skewedPartitionSizeThreshold的同时大于各Partition大小中位数与该因子的乘积,或者行数大于spark.sql.adaptive.skewedPartitionRowCountThreshold的同时大于各Partition行数中位数与该因子的乘积,则它会被视倾斜的Partition

–spark.sql.windowExec.buffer.spill.threshold;
当用户的SQL中包含窗口函数时,并不会把一个窗口中的所有数据全部读进内存,而是维护一个缓存池,当池中的数据条数大于该参数表示的阈值时,spark将数据写到磁盘

–Sql中的数据倾斜处理
1)大表, 小表Broadcast
Select /*+BROADCAST (t1) */ * from t1, t2 where t1.key = t2.key

  1. 倾斜数据分离
    将数据倾斜的表t1拆分为t11,t12,然后使用 (其中t12只包含倾斜的key)
    Select * from (
    Select * from t11, t2 where t11.key=t2.key
    Union all
    Select /* + BROADCAST (t12) */ * from t12, t2 where t12.key=t2.key
    )
    PySpark

在这里插入图片描述
为了不破坏Spark已有的运行时架构,Spark在外围包装一层Python API,借助Py4j实现Python和Java的交互,进而实现通过Python编写Spark应用程序;
其中白色部分是新增的Python进程,在Driver端,通过Py4j实现在Python中调用Java的方法,即将用户写的PySpark程序”映射”到JVM中,例如,用户在PySpark中实例化一个Python的SparkContext对象,最终会在JVM中实例化Scala的SparkContext对象;在Executor端,则不需要借助Py4j,因为Executor端运行的Task逻辑是由Driver发过来的,那是序列化后的字节码,虽然里面可能包含有用户定义的Python函数或Lambda表达式,Py4j并不能实现在Java里调用Python的方法,为了能在Executor端运行用户定义的Python函数或Lambda表达式,则需要为每个Task单独启一个Python进程,通过socket通信方式将Python函数或Lambda表达式发给Python进程执行。语言层面的交互总体流程如下图所示,实线表示方法调用,虚线表示结果返回;
在这里插入图片描述
–conf spark.python.worker.memory
默认值512M
如果使用python开发spark应用程序,这个属性将为每个python worker进程分配内存总数
每一个python的worker进程的内存大小,在运行期间,如果数据大小超过这个限制,数据将会被分片并保存在磁盘上
–conf spark.python.profile
在Python worker中开启profiling。通过sc.show_profiles()展示分析结果。或者在driver退出前展示分析结果。可以通过sc.dump_profiles(path)将结果dump到磁盘中。如果一些分析结果已经手动展示,那么在driver退出前,它们再不会自动展示

–conf spark.python.profile.dump
driver退出前保存分析结果的dump文件的目录。每个RDD都会分别dump一个文件。可以通过ptats.Stats()加载这些文件。如果指定了这个属性,分析结果不会自动展示
–conf spark.python.worker.reuse
默认值:true
是否重用python worker。如果是,它将使用固定数量的Python workers,而不需要为每个任务fork()一个Python进程。如果有一个非常大的广播,这个设置将非常有用。因为,广播不需要为每个任务从JVM到Python worker传递一次

Spark + Python3问题
目前业务运维安装的地址是 /usr/local/anaconda2, Python版本为2.7,spark-env.sh文件中配置的也是这个路径;

解决方案:
链接地址:
https://blog.csdn.net/suzyu12345/article/details/82256277

另外附上引文版的解决方案,与之类似
步骤:
在创建虚拟环境之前,如果没有conda pack,请先安装
https://conda.github.io/conda-pack/#
安装命令:
conda install -c conda-forge conda-pack

             Create an environment:
             1:利用conda创建一个虚拟环境,在虚拟环境中安装需要的module,比如numpy, pandas, scikit-learn等等
                  $ conda create -y -n example python=3.5 numpy pandas scikit-learn
             Activate the environment:
                 $ conda activate example
             Package the environment into a tar.gz archive:
                 $ conda pack -o environment.tar.gz
             使用方式:(environment为environment.tar.gz虚拟环境报在executor上解压之后的目录)
                 $ PYSPARK_PYTHON=./environment/bin/python

任务提交
spark-submit
–conf spark.yarn.appMasterEnv.PYSPARK_PYTHON=./environment/bin/python
–master yarn
–deploy-mode cluster
–archives environment.tar.gz#environment
script.py(要执行的脚本)
SparkR
在这里插入图片描述

SparkR的架构类似于PySpark,Driver端除了一个JVM进程(包含一个SparkContext)外,还有起一个R的进程,这两个进程通过Socket进行通信,用户可以提交R语言代码,R的进程会执行这些R代码,当R代码调用Spark相关函数时,R进程会通过Socket触发JVM中的对应任务;

当R进程向JVM进程提交任务的时候,R会把子任务需要的环境(enclosing environment)进行打包,并发送到JVM的driver端;
通过R生成的RDD都会是RRDD类型,当触发RRDD的action时,Spark的执行器会开启一个R进程(worker.R),执行器和R进程通过Socket进行通信。执行器会把任务和所需的环境发送给R进程,R进程会加载对应的package,执行任务,并返回结果。

在这里插入图片描述
Spark Cache && Checkpoint
Spark计算框架的一大优势就是对迭代计算的强大支持。由于spark中的RDD都是只读不可变的对象,也就是RDD的每一个transformation操作都会产生一个新的RDD,所以Spark任务中的一个优化原则就是便面创建重复的RDD而尽可能复用同一个RDD。

Tathagata Das 有一段回答: There is a significant difference between cache and checkpoint. Cache materializes the RDD and keeps it in memory and/or disk. But the lineage of RDD (that is, seq of operations that generated the RDD) will be remembered, so that if there are node failures and parts of the cached RDDs are lost, they can be regenerated. However, checkpoint saves the RDD to an HDFS file and actually forgets the lineage completely. This is allows long lineages to be truncated and the data to be saved reliably in HDFS (which is naturally fault tolerant by replication). 上面这段话的大概意思就是:cache 和 checkpoint 之间有一个重大的区别,cache 将 RDD 以及 RDD 的血统(记录了这个RDD如何产生)缓存到内存中,当缓存的 RDD 失效的时候(如内存损坏),它们可以通过血统重新计算来进行恢复。但是 checkpoint 将 RDD 缓存到了 HDFS 中,同时忽略了它的血统(也就是RDD之前的那些依赖)。为什么要丢掉依赖?因为可以利用 HDFS 多副本特性保证容错!

def cache(): this.type = persist()
def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
cache的使用虽然非常简单,但是有几点需要注意。
1)cache之后一定不能直接去接算子。因为cache后有算子的话,它每次都会重新触发整个计算过程,从而导致cache失效。
2)cache操作需要当第一个使用到它的job执行后才会生效,而不是cache之后马上生效,这是spark框架的延迟计算导致的,另外不正确的使用unpersist操作,也可能导致cache失效。
在这里插入图片描述

3)如何释放cache缓存,unpersist,它是立即执行的,persist是lazy级别的(没有级别),unpersist是eager级别的。RDD cache的声明周期是application级别,也就是如果不显式unpersist释放缓存,RDD会一直存在(虽然当内存不够时按LRU算法进行清除),如果不正确的unpersist,让无用的RDD占用executor内存,会导致资源的浪费,影响任务的效率。

什么时候使用cache
最粗暴的方式是只要RDD有被复用就使用cache,但显然这不是最优的方式,总的来说在一下三种情况下应该使用cache
1)在迭代循环中重用RDD
2)在一个spark任务中多次重用RDD
3)当重新生成的RDD的分区数据成本很高时。

这3种情况说的还是很抽象,其实总的来说就是权衡cache与否的代价,不cache则多次使用RDD时会将RDD及其依赖的所有RDD都重新计算一次。Cache则重用的RDD只会计算一次,但是会占用executor的内存资源,那是否应该cache就是把计算RDD的时间资源与缓存RDD的内存资源之间进行权衡,这两者间的权衡没有一定的规则,不同的任务类型不同的业务场景的原则都不一样,在无法明确权衡时,最好也最直接的方式是用实际任务进行试验。
选用什么类型的cache级别
针对占用内存不是很大的中间结算结果优先采用MEMORY_ONLY, 它的性能最好(前提是内存足够),也是RDD的默认策略。如果中间结果数据量稍大,可以采用MEMORY_ONLY_SER策略进行补充,但是在实际的生产环境中,大多数情况下数据量都是超出内存容量的,这可能会导致JVM的OOM内存溢出异常。

如果内存中无法容纳中间数据,那么建议使用MEMORY_AND_DIS_SER策略,该策略会优先将数据缓存在内存中,只有当内存不够时才会把数据写入磁盘,另外对于分布式任务,IO资源往往比CPU资源更加紧张,序列化后的数据可以节省内存和磁盘的空间开销。

通常很少使用DISK_ONLY级别,它表示数据量已经非常大,远大于内存的容量,这个时候需要慎重权衡重新计算RDD的消耗和从磁盘加载RDD的消耗。

Spark的持久化级别有如下几种:
MEMORY_ONLY
使用未序列化的Java对象格式,将数据保存在内存中。如果内存不够存放所有的数据,则数据可能就不会进行持久化。那么下次对这个RDD执行算子操作时,那些没有被持久化的数据,需要从源头处重新计算一遍。如果RDD中数据量比较大时,会导致JVM的OOM内存溢出异常。这是RDD的默认持久化级别。

MEMORY_AND_DISK
使用未序列化的Java对象格式,优先尝试将数据保存在内存中。如果内存不够存放所有的数据,会将数据写入磁盘文件中,下次对这个RDD执行算子时,持久化在磁盘文件中的数据会被读取出来使用。这是DataFrame的默认持久化级别。

MEMORY_ONLY_SER/MEMORY_AND_DISK_SER
基本含义同MEMORY_ONLY/MEMORY_AND_DISK。唯一的区别是,会将RDD中的数据进行序列化,RDD的每个partition会被序列化成一个字节数组。这种方式更加节省内存,从而可以避免持久化的数据占用过多内存导致频繁GC。

这两种策略都是对MEMORY_ONLY/MEMORY_AND_DISK策略的补充。

DISK_ONLY
使用未序列化的Java对象格式,将数据全部写入磁盘文件中。

MEMORY_ONLY_2/MEMORY_AND_DISK_2 etc…
对于上述任意一种持久化策略,如果加上后缀_2,代表的是将每个持久化的数据,都复制一份副本,并将副本保存到其他节点上。这种基于副本的持久化机制主要用于进行容错。假如某个节点挂掉,节点的内存或磁盘中的持久化数据丢失了,那么后续对RDD计算时还可以使用该数据在其他节点上的副本。如果没有副本的话,就只能将这些数据从源头处重新计算一遍了。
Disk Cache是否有意义
在持久化级别中有将数据存储到磁盘中,粗看起来有些奇怪,因为大部分情况下可能源数据都是存在HDFS上的,如果把数据缓存到磁盘上,在再次使用的过程中还是需要重新读入,这是否有意义?对于DISK缓存的数据有两点需要注意:
1)是否Cache的权衡转变为重新计算RDD的计算代价与从磁盘加载RDD的代价两者之间的权衡。
2)RDD的cache是分区级别的,并且缓存是将数据写到executor对应的worker节点的本地目录中。

也就是对于DISK的缓存还是有意义的,首先当内存中无法容纳的分区数据才会被写入磁盘,其次写入的磁盘是worker节点的本地目录,当重新读取时相当于从本地磁盘读取数据,而且磁盘空间往往不是瓶颈,完全可以给worker节点配置足够的磁盘容量。

如果从这个思路触发,缓存数据的效率肯定比读取源数据的效率更高,那是否可以把所有输入数据进行缓存,从而将spark cache当做一个大的内存数据库呢?之所以有这个想法是因为实际的业务场景中,同一部门中使用的源数据是一致的,并且在不同业务团队不同的spark应用中会被反复使用到,那是否可以将源数据统一进行缓存,为同一部门不同业务不同任务统一提供缓存数据,从而大大提高任务的处理效率。查询资料发现现在已经有Apache Ignite(高性能,集成化和分布式的内存计算和事物平台)支持这种思路,但这种解决方案需要重新部署Apache Ignite集群进行支持。另一种是将spark任务设计成Server/Client模式,所有任务都提交到Server端进行,从而在application间进行数据复用。
Parquet
详情请阅读:
深入分析 Parquet 列式存储格式
https://www.infoq.cn/article/in-depth-analysis-of-parquet-column-storage-format

Parquet是一种列式存储格式,很多种处理引擎都支持这种存储格式,也是spark的默认存储格式。Spark SQL支持灵活的读和写Parquet文件,并且对parquet文件的schema可以自动解析。

Parquet是语言无关的,而且不与任何一种数据处理框架绑定在一起,适配多种语言和组件,能够与Parquet配合的组件有:
查询引擎: Hive, Impala, Pig, Presto, Drill, Tajo, HAWQ, IBM Big SQL
计算框架: MapReduce, Spark, Cascading, Crunch, Scalding, Kite
数据模型: Avro, Thrift, Protocol Buffers, POJOs
在这里插入图片描述

列式存储和行式相比有哪些优势?
1)可以跳过不符合条件的数据,只读取需要的数据,降低IO数据量。
2)压缩编码可以降低磁盘存储空间。由于同一列的数据类型是一样的,可以使用更高效的压缩编码(例如 Run Length Encoding 和 Delta Encoding)进一步解决存储空间。
3)只读取需要的列,支持向量运算,能够获取更好的扫描性能。

如果您在 HDFS 上拥有基于文本的数据文件或表,而且正在使用 Spark SQL 对它们执行查询,那么强烈推荐将文本数据文件转换为 Parquet 数据文件,以实现性能和存储收益。当然,转换需要时间,但查询性能的提升在某些情况下可能达到 30 倍或更高,存储的节省可高达 75%。

Spark 已经将 Parquet 设为默认的文件存储格式,Cloudera 投入了很多工程师到 Impala+Parquet 相关开发中,Hive/Pig 都原生支持 Parquet。Parquet 现在为 Twitter 至少节省了 1/3 的存储空间,同时节省了大量的表扫描和反序列化的时间。这两方面直接反应就是节约成本和提高性能。

如果说 HDFS 是大数据时代文件系统的事实标准的话,Parquet 就是大数据时代存储格式的事实标准。

Scala代码优化

取巧算法
SizeEstimator对象内存
SizeEstimator. Estimate(rdd)
获取RDD大小
高效算子
reduceByKey/aggregateByKey代替groupByKey
(1)groupByKey()是对RDD中的所有数据做shuffle,根据不同的Key映射到不同的partition中再进行aggregate;
(2)aggregateByKey()是先对每个partition中的数据根据不同的Key进行aggregate,然后将结果进行shuffle,完成各个partition之间的aggregate。因此,和groupByKey()相比,运算量小了很多。
(3) distinct()也是对RDD中的所有数据做shuffle进行aggregate后再去重。
(4)reduceByKey()也是先在单台机器中计算,再将结果进行shuffle,减小运算量
mapPartitions代替map
map(func)方法应用于rdd时,func()操作将应用于每一行,在这种情况下,func()操作将被调用1000次。即在一些时间关键的应用中会耗费时间;

如果我们在rdd上调用mapPartition(func)方法,则func()操作将在每个分区上而不是在每一行上调用。在这种特殊情况下,它将被称为10次(分区数)。通过这种方式,你可以在涉及时间关键的应用程序时阻止一些处理。

mapPartition的优势
(1)机器学习应用程序,特别是深度学习应用程序 - 使用矢量化时,执行比简单for循环要
好上百倍。mapPartitions将帮助您使用矢量化。一般来说,你的性能提高300倍+(这不是
百分比,是300倍)
(2)连接创建和清理任务很昂贵,每个元素都会使代码效率低下。这适用于数据库或其他连
接。但是使用mapPartitions,你可以只对整个分区执行一次init / cleanup循环。
(3)一般来说,JVM带有乱序执行(它将完全使用CPU并使你的代码运行得更快),JVM需
要分析你的代码,并且必须重写你的代码。使用mapPartitions,JVM可以更好地进行分析优化(与分析调用函数相比,它可以分析/优化简单代码)
(4)对于map (),CPU需要每次调用lambda函数(以arg形式传递以进行映射),这会带来10-15ns的开销,并导致CPU寄存器刷新并再次加载(堆栈指针,基址指针和指令指针)
filter之后coallease
先使用filter过滤掉部分不满足需求的数据,之后可以减少partition数量。
foreachPartitions替代foreach
原理类似于“使用mapPartitions替代map”,也是一次函数调用处理一个partition的所有
数据,而不是一次函数调用处理一条数据。
在实践中发现,foreachPartitions类的算子,对性能的提升还是很有帮助的。比如在foreach函数中,将RDD中所有数据写MySQL,那么如果是普通的foreach算子,就会一条数据一条数据地写,每次函数调用可能就会创建一个数据库连接,此时就势必会频繁地创建和销毁数据库连接,性能是非常低下;但是如果用foreachPartitions算子一次性处理一个partition的数据,那么对于每个partition,只要创建一个数据库连接即可,然后执行批量插入操作,此时性能是比较高的。实践中发现,对于1万条左右的数据量写mysql,性能可以提升30%以上。
RDD coalesce 与 repartition
有时候需要重新设置RDD的分区数量,比如RDD的分区中,RDD分区比较多,但是每个RDD的数量比较小,分区数量太增大并行度,分区数据量太少导致节点间通信时间在整个任务执行时长占比被放大,所以需要设置一个比较合理的分区;
有两种方法可以重设RDD分区:分别是coalesce()方法和repartition(),Repartition是coalesce函数中shuffle为true的特例;
当增加并行度的时候,额外的shuffle是有利的。例如,数据中有一些文件是不可分割的,那么该大文件对应的分区就会有大量的记录,而不是说将数据分散到尽可能多的分区内部来使用所有已经申请cpu。在这种情况下,使用Reparition重新产生更多的分区数,以满足后面转换算子所需的并行度,这会提升很大性能;

Coalesce
如果N>M
1)N和M相差不多,(假如N是1000,M是100)那么就可以将N个分区中的
若干个分区合并成一个新的分区,最终合并为M个分区,这时可以将shuffle设置为false;

2)如果M>N时,coalesce是无效的,不进行shuffle过程,父RDD和子RDD之间是窄依赖关系,无法使文件数(partiton)变多。 总之如果shuffle为false时,如果传入的参数大于现有的分区数目,RDD的分区数不变,也就是说不经过shuffle,是无法将RDD的分区数变多的;

3)如果N>M并且两者相差悬殊,这时要看executor数与要生成的partition关系,如果executor数 <= 要生成partition数,coalesce效率高,反之如果用coalesce会导致(executor数-要生成partiton数)个excutor空跑从而降低效率。
数据倾斜调优
详情请参考美团技术博客:
https://tech.meituan.com/2016/05/12/spark-tuning-pro.html

数据倾斜发生的原理:

数据倾斜的原理很简单,在进行shuffle的时候,必须将各个节点上相同的key拉取到某个节点上的一个task来进行处理,比如按照key进行聚合或join等操作,此时如果某个key对应的数据量特别大的话,就会发生数据倾斜。比如大部分key对应10条数据,但是个别key却对应了100万条数据,那么大部分task可能就只会分配到10条数据,然后1s就执行完成,但是个别task可能分配到100万数据,要运行一两个小时,因此,整个spark作业的运行进度是由运行时间最长的那个task决定的。
如何定位数据倾斜
数据倾斜只会发生在shuffle过程中,这里给大家罗列一些常用的并且可能会触发shuffle操作的算子:
distinct, groupByKey, reduceByKey, aggregateByKey, join, cogroup, repartition等。
如果使用的是spark sql,那么group by等语句也会触发shuffle

某个task莫名其妙内存溢出的情况
通过异常调用栈信息就可以定位到你的代码中哪一行发生了内存溢出,然后在哪行代码附近找找,一般也会有shuffle类算子,此时很可能就是这个算子导致了数据倾斜。
但是大家需要注意的是,不能单纯靠偶然的内存溢出就判定发生了数据倾斜,因为自己编写的代码bug,以及偶然出现的数据异常,也可能导致内存溢出。因此还需要通过Spark UI查看报错的那个stage的各个task的运行时间以及分配的数据量,才能确定是否是由于数据倾斜导致了这次内存溢出。

查看导致数据倾斜的key的数据分布情况:
知道了数据倾斜发生在哪里之后,通常需要分析一下那个执行了shuffle操作并且导致了数据倾斜的RDD/Hive表,查看一下其中key的分布情况。这主要是为之后选择哪一种技术方案提供依据。针对不同的key分布与不同的shuffle算子组合起来的各种情况,可能需要选择不同的技术方案来解决。
此时根据你执行操作的情况不同,可以有很多种查看key分布的方式:
如果是Spark SQL中的group by、join语句导致的数据倾斜,那么就查询一下SQL中使用的表的key分布情况。
如果是对Spark RDD执行shuffle算子导致的数据倾斜,那么可以在Spark作业中加入查看key分布的代码,比如RDD.countByKey()。然后对统计出来的各个key出现的次数,collect/take到客户端打印一下,就可以看到key的分布情况。
举例来说,对于上面所说的单词计数程序,如果确定了是stage1的reduceByKey算子导致了数据倾斜,那么就应该看看进行reduceByKey操作的RDD中的key分布情况,在这个例子中指的就是pairs RDD。如下示例,我们可以先对pairs采样10%的样本数据,然后使用countByKey算子统计出每个key出现的次数,最后在客户端遍历和打印样本数据中各个key的出现次数。
1 val sampledPairs = pairs.sample(false, 0.1)
2 val sampledWordCounts = sampledPairs.countByKey()
3 sampledWordCounts.foreach(println(_))
数据倾斜的解决方案
解决方案一:使用Hive ETL预处理数据
方案适用场景:导致数据倾斜的是Hive表。如果该Hive表中的数据本身很不均匀(比如某个key对应了100万数据,其他key才对应了10条数据),而且业务场景需要频繁使用Spark对Hive表执行某个分析操作,那么比较适合使用这种技术方案。
方案实现思路:此时可以评估一下,是否可以通过Hive来进行数据预处理(即通过Hive ETL预先对数据按照key进行聚合,或者是预先和其他表进行join),然后在Spark作业中针对的数据源就不是原来的Hive表了,而是预处理后的Hive表。此时由于数据已经预先进行过聚合或join操作了,那么在Spark作业中也就不需要使用原先的shuffle类算子执行这类操作了。
方案实现原理:这种方案从根源上解决了数据倾斜,因为彻底避免了在Spark中执行shuffle类算子,那么肯定就不会有数据倾斜的问题了。但是这里也要提醒一下大家,这种方式属于治标不治本。因为毕竟数据本身就存在分布不均匀的问题,所以Hive ETL中进行group by或者join等shuffle操作时,还是会出现数据倾斜,导致Hive ETL的速度很慢。我们只是把数据倾斜的发生提前到了Hive ETL中,避免Spark程序发生数据倾斜而已。
方案优点:实现起来简单便捷,效果还非常好,完全规避掉了数据倾斜,Spark作业的性能会大幅度提升。
方案缺点:治标不治本,Hive ETL中还是会发生数据倾斜。
方案实践经验:在一些Java系统与Spark结合使用的项目中,会出现Java代码频繁调用Spark作业的场景,而且对Spark作业的执行性能要求很高,就比较适合使用这种方案。将数据倾斜提前到上游的Hive ETL,每天仅执行一次,只有那一次是比较慢的,而之后每次Java调用Spark作业时,执行速度都会很快,能够提供更好的用户体验。
项目实践经验:在美团·点评的交互式用户行为分析系统中使用了这种方案,该系统主要是允许用户通过Java Web系统提交数据分析统计任务,后端通过Java提交Spark作业进行数据分析统计。要求Spark作业速度必须要快,尽量在10分钟以内,否则速度太慢,用户体验会很差。所以我们将有些Spark作业的shuffle操作提前到了Hive ETL中,从而让Spark直接使用预处理的Hive中间表,尽可能地减少Spark的shuffle操作,大幅度提升了性能,将部分作业的性能提升了6倍以上。

解决方案二:过滤少数导致倾斜的key
方案适用场景:如果发现导致倾斜的key就少数几个,而且对计算本身的影响并不大的话,那么很适合使用这种方案。比如99%的key就对应10条数据,但是只有一个key对应了100万数据,从而导致了数据倾斜。
方案实现思路:如果我们判断那少数几个数据量特别多的key,对作业的执行和计算结果不是特别重要的话,那么干脆就直接过滤掉那少数几个key。比如,在Spark SQL中可以使用where子句过滤掉这些key或者在Spark Core中对RDD执行filter算子过滤掉这些key。如果需要每次作业执行时,动态判定哪些key的数据量最多然后再进行过滤,那么可以使用sample算子对RDD进行采样,然后计算出每个key的数量,取数据量最多的key过滤掉即可。
方案实现原理:将导致数据倾斜的key给过滤掉之后,这些key就不会参与计算了,自然不可能产生数据倾斜。
方案优点:实现简单,而且效果也很好,可以完全规避掉数据倾斜。
方案缺点:适用场景不多,大多数情况下,导致倾斜的key还是很多的,并不是只有少数几个。
方案实践经验:在项目中我们也采用过这种方案解决数据倾斜。有一次发现某一天Spark作业在运行的时候突然OOM了,追查之后发现,是Hive表中的某一个key在那天数据异常,导致数据量暴增。因此就采取每次执行前先进行采样,计算出样本中数据量最大的几个key之后,直接在程序中将那些key给过滤掉。

解决方案三:提高shuffle操作的并行度
方案适用场景:如果我们必须要对数据倾斜迎难而上,那么建议优先使用这种方案,因为这是处理数据倾斜最简单的一种方案。
方案实现思路:在对RDD执行shuffle算子时,给shuffle算子传入一个参数,比如reduceByKey(1000),该参数就设置了这个shuffle算子执行时shuffle read task的数量。对于Spark SQL中的shuffle类语句,比如group by、join等,需要设置一个参数,即spark.sql.shuffle.partitions,该参数代表了shuffle read task的并行度,该值默认是200,对于很多场景来说都有点过小。
方案实现原理:增加shuffle read task的数量,可以让原本分配给一个task的多个key分配给多个task,从而让每个task处理比原来更少的数据。举例来说,如果原本有5个key,每个key对应10条数据,这5个key都是分配给一个task的,那么这个task就要处理50条数据。而增加了shuffle read task以后,每个task就分配到一个key,即每个task就处理10条数据,那么自然每个task的执行时间都会变短了。具体原理如下图所示。
方案优点:实现起来比较简单,可以有效缓解和减轻数据倾斜的影响。
方案缺点:只是缓解了数据倾斜而已,没有彻底根除问题,根据实践经验来看,其效果有限。
方案实践经验:该方案通常无法彻底解决数据倾斜,因为如果出现一些极端情况,比如某个key对应的数据量有100万,那么无论你的task数量增加到多少,这个对应着100万数据的key肯定还是会分配到一个task中去处理,因此注定还是会发生数据倾斜的。所以这种方案只能说是在发现数据倾斜时尝试使用的第一种手段,尝试去用嘴简单的方法缓解数据倾斜而已,或者是和其他方案结合起来使用。
在这里插入图片描述

解决方案四:两阶段聚合(局部聚合+全局聚合)
方案适用场景:对RDD执行reduceByKey等聚合类shuffle算子或者在Spark SQL中使用group by语句进行分组聚合时,比较适用这种方案。
方案实现思路:这个方案的核心实现思路就是进行两阶段聚合。第一次是局部聚合,先给每个key都打上一个随机数,比如10以内的随机数,此时原先一样的key就变成不一样的了,比如(hello, 1) (hello, 1) (hello, 1) (hello, 1),就会变成(1_hello, 1) (1_hello, 1) (2_hello, 1) (2_hello, 1)。接着对打上随机数后的数据,执行reduceByKey等聚合操作,进行局部聚合,那么局部聚合结果,就会变成了(1_hello, 2) (2_hello, 2)。然后将各个key的前缀给去掉,就会变成(hello,2)(hello,2),再次进行全局聚合操作,就可以得到最终结果了,比如(hello, 4)。
方案实现原理:将原本相同的key通过附加随机前缀的方式,变成多个不同的key,就可以让原本被一个task处理的数据分散到多个task上去做局部聚合,进而解决单个task处理数据量过多的问题。接着去除掉随机前缀,再次进行全局聚合,就可以得到最终的结果。具体原理见下图。
方案优点:对于聚合类的shuffle操作导致的数据倾斜,效果是非常不错的。通常都可以解决掉数据倾斜,或者至少是大幅度缓解数据倾斜,将Spark作业的性能提升数倍以上。
方案缺点:仅仅适用于聚合类的shuffle操作,适用范围相对较窄。如果是join类的shuffle操作,还得用其他的解决方案。

在这里插入图片描述

解决方案五:将reduce join转为map join
方案适用场景:在对RDD使用join类操作,或者是在Spark SQL中使用join语句时,而且join操作中的一个RDD或表的数据量比较小(比如几百M或者一两G),比较适用此方案。
方案实现思路:不使用join算子进行连接操作,而使用Broadcast变量与map类算子实现join操作,进而完全规避掉shuffle类的操作,彻底避免数据倾斜的发生和出现。将较小RDD中的数据直接通过collect算子拉取到Driver端的内存中来,然后对其创建一个Broadcast变量;接着对另外一个RDD执行map类算子,在算子函数内,从Broadcast变量中获取较小RDD的全量数据,与当前RDD的每一条数据按照连接key进行比对,如果连接key相同的话,那么就将两个RDD的数据用你需要的方式连接起来。
方案实现原理:普通的join是会走shuffle过程的,而一旦shuffle,就相当于会将相同key的数据拉取到一个shuffle read task中再进行join,此时就是reduce join。但是如果一个RDD是比较小的,则可以采用广播小RDD全量数据+map算子来实现与join同样的效果,也就是map join,此时就不会发生shuffle操作,也就不会发生数据倾斜。具体原理如下图所示。
方案优点:对join操作导致的数据倾斜,效果非常好,因为根本就不会发生shuffle,也就根本不会发生数据倾斜。
方案缺点:适用场景较少,因为这个方案只适用于一个大表和一个小表的情况。毕竟我们需要将小表进行广播,此时会比较消耗内存资源,driver和每个Executor内存中都会驻留一份小RDD的全量数据。如果我们广播出去的RDD数据比较大,比如10G以上,那么就可能发生内存溢出了。因此并不适合两个都是大表的情况。

在这里插入图片描述

解决方案六:采样倾斜key并分拆join操作
方案适用场景:两个RDD/Hive表进行join的时候,如果数据量都比较大,无法采用“解决方案五”,那么此时可以看一下两个RDD/Hive表中的key分布情况。如果出现数据倾斜,是因为其中某一个RDD/Hive表中的少数几个key的数据量过大,而另一个RDD/Hive表中的所有key都分布比较均匀,那么采用这个解决方案是比较合适的。
方案实现思路:
对包含少数几个数据量过大的key的那个RDD,通过sample算子采样出一份样本来,然后统计一下每个key的数量,计算出来数据量最大的是哪几个key。
然后将这几个key对应的数据从原来的RDD中拆分出来,形成一个单独的RDD,并给每个key都打上n以内的随机数作为前缀,而不会导致倾斜的大部分key形成另外一个RDD。
接着将需要join的另一个RDD,也过滤出来那几个倾斜key对应的数据并形成一个单独的RDD,将每条数据膨胀成n条数据,这n条数据都按顺序附加一个0~n的前缀,不会导致倾斜的大部分key也形成另外一个RDD。
再将附加了随机前缀的独立RDD与另一个膨胀n倍的独立RDD进行join,此时就可以将原先相同的key打散成n份,分散到多个task中去进行join了。
而另外两个普通的RDD就照常join即可。
最后将两次join的结果使用union算子合并起来即可,就是最终的join结果。
方案实现原理:对于join导致的数据倾斜,如果只是某几个key导致了倾斜,可以将少数几个key分拆成独立RDD,并附加随机前缀打散成n份去进行join,此时这几个key对应的数据就不会集中在少数几个task上,而是分散到多个task进行join了。具体原理见下图。
方案优点:对于join导致的数据倾斜,如果只是某几个key导致了倾斜,采用该方式可以用最有效的方式打散key进行join。而且只需要针对少数倾斜key对应的数据进行扩容n倍,不需要对全量数据进行扩容。避免了占用过多内存。
方案缺点:如果导致倾斜的key特别多的话,比如成千上万个key都导致数据倾斜,那么这种方式也不适合。
在这里插入图片描述

解决方案七:使用随机前缀和扩容RDD进行join
方案适用场景:如果在进行join操作时,RDD中有大量的key导致数据倾斜,那么进行分拆key也没什么意义,此时就只能使用最后一种方案来解决问题了。
方案实现思路:
该方案的实现思路基本和“解决方案六”类似,首先查看RDD/Hive表中的数据分布情况,找到那个造成数据倾斜的RDD/Hive表,比如有多个key都对应了超过1万条数据。
然后将该RDD的每条数据都打上一个n以内的随机前缀。
同时对另外一个正常的RDD进行扩容,将每条数据都扩容成n条数据,扩容出来的每条数据都依次打上一个0~n的前缀。
最后将两个处理后的RDD进行join即可。
方案实现原理:将原先一样的key通过附加随机前缀变成不一样的key,然后就可以将这些处理后的“不同key”分散到多个task中去处理,而不是让一个task处理大量的相同key。该方案与“解决方案六”的不同之处就在于,上一种方案是尽量只对少数倾斜key对应的数据进行特殊处理,由于处理过程需要扩容RDD,因此上一种方案扩容RDD后对内存的占用并不大;而这一种方案是针对有大量倾斜key的情况,没法将部分key拆分出来进行单独处理,因此只能对整个RDD进行数据扩容,对内存资源要求很高。
方案优点:对join类型的数据倾斜基本都可以处理,而且效果也相对比较显著,性能提升效果非常不错。
方案缺点:该方案更多的是缓解数据倾斜,而不是彻底避免数据倾斜。而且需要对整个RDD进行扩容,对内存资源要求很高。
方案实践经验:曾经开发一个数据需求的时候,发现一个join导致了数据倾斜。优化之前,作业的执行时间大约是60分钟左右;使用该方案优化之后,执行时间缩短到10分钟左右,性能提升了6倍。

解决方案八:多种方案组合使用
在实践中发现,很多情况下,如果只是处理较为简单的数据倾斜场景,那么使用上述方案中的某一种基本就可以解决。但是如果要处理一个较为复杂的数据倾斜场景,那么可能需要将多种方案组合起来使用。比如说,我们针对出现了多个数据倾斜环节的Spark作业,可以先运用解决方案一和二,预处理一部分数据,并过滤一部分数据来缓解;其次可以对某些shuffle操作提升并行度,优化其性能;最后还可以针对不同的聚合或join操作,选择一种方案来优化其性能。大家需要对这些方案的思路和原理都透彻理解之后,在实践中根据各种不同的情况,灵活运用多种方案,来解决自己的数据倾斜问题。

shuffle调优
调优概述
大多数Spark作业的性能主要就是消耗在了shuffle环节,因为该环节包含了大量的磁盘IO、序列化、网络数据传输等操作。因此,如果要让作业的性能更上一层楼,就有必要对shuffle过程进行调优。但是也必须提醒大家的是,影响一个Spark作业性能的因素,主要还是代码开发、资源参数以及数据倾斜,shuffle调优只能在整个Spark的性能调优中占到一小部分而已。因此大家务必把握住调优的基本原则,千万不要舍本逐末。下面我们就给大家详细讲解shuffle的原理,以及相关参数的说明,同时给出各个参数的调优建议。
ShuffleManager发展概述
在Spark的源码中,负责shuffle过程的执行、计算和处理的组件主要就是ShuffleManager,也即shuffle管理器。而随着Spark的版本的发展,ShuffleManager也在不断迭代,变得越来越先进。
在Spark 1.2以前,默认的shuffle计算引擎是HashShuffleManager。该ShuffleManager而HashShuffleManager有着一个非常严重的弊端,就是会产生大量的中间磁盘文件,进而由大量的磁盘IO操作影响了性能。
因此在Spark 1.2以后的版本中,默认的ShuffleManager改成了SortShuffleManager。SortShuffleManager相较于HashShuffleManager来说,有了一定的改进。主要就在于,每个Task在进行shuffle操作时,虽然也会产生较多的临时磁盘文件,但是最后会将所有的临时文件合并(merge)成一个磁盘文件,因此每个Task就只有一个磁盘文件。在下一个stage的shuffle read task拉取自己的数据时,只要根据索引读取每个磁盘文件中的部分数据即可。
下面我们详细分析一下HashShuffleManager和SortShuffleManager的原理。
HashShuffleManager运行原理
未经优化的HashShuffleManager
下图说明了未经优化的HashShuffleManager的原理。这里我们先明确一个假设前提:每个Executor只有1个CPU core,也就是说,无论这个Executor上分配多少个task线程,同一时间都只能执行一个task线程。
我们先从shuffle write开始说起。shuffle write阶段,主要就是在一个stage结束计算之后,为了下一个stage可以执行shuffle类的算子(比如reduceByKey),而将每个task处理的数据按key进行“分类”。所谓“分类”,就是对相同的key执行hash算法,从而将相同key都写入同一个磁盘文件中,而每一个磁盘文件都只属于下游stage的一个task。在将数据写入磁盘之前,会先将数据写入内存缓冲中,当内存缓冲填满之后,才会溢写到磁盘文件中去。
那么每个执行shuffle write的task,要为下一个stage创建多少个磁盘文件呢?很简单,下一个stage的task有多少个,当前stage的每个task就要创建多少份磁盘文件。比如下一个stage总共有100个task,那么当前stage的每个task都要创建100份磁盘文件。如果当前stage有50个task,总共有10个Executor,每个Executor执行5个Task,那么每个Executor上总共就要创建500个磁盘文件,所有Executor上会创建5000个磁盘文件。由此可见,未经优化的shuffle write操作所产生的磁盘文件的数量是极其惊人的。
接着我们来说说shuffle read。shuffle read,通常就是一个stage刚开始时要做的事情。此时该stage的每一个task就需要将上一个stage的计算结果中的所有相同key,从各个节点上通过网络都拉取到自己所在的节点上,然后进行key的聚合或连接等操作。由于shuffle write的过程中,task给下游stage的每个task都创建了一个磁盘文件,因此shuffle read的过程中,每个task只要从上游stage的所有task所在节点上,拉取属于自己的那一个磁盘文件即可。
shuffle read的拉取过程是一边拉取一边进行聚合的。每个shuffle read task都会有一个自己的buffer缓冲,每次都只能拉取与buffer缓冲相同大小的数据,然后通过内存中的一个Map进行聚合等操作。聚合完一批数据后,再拉取下一批数据,并放到buffer缓冲中进行聚合操作。以此类推,直到最后将所有数据到拉取完,并得到最终的结果。

HashShuffleManager运行原理
未经优化的HashShuffleManager
下图说明了未经优化的HashShuffleManager的原理。这里我们先明确一个假设前提:每个Executor只有1个CPU core,也就是说,无论这个Executor上分配多少个task线程,同一时间都只能执行一个task线程。
我们先从shuffle write开始说起。shuffle write阶段,主要就是在一个stage结束计算之后,为了下一个stage可以执行shuffle类的算子(比如reduceByKey),而将每个task处理的数据按key进行“分类”。所谓“分类”,就是对相同的key执行hash算法,从而将相同key都写入同一个磁盘文件中,而每一个磁盘文件都只属于下游stage的一个task。在将数据写入磁盘之前,会先将数据写入内存缓冲中,当内存缓冲填满之后,才会溢写到磁盘文件中去。
那么每个执行shuffle write的task,要为下一个stage创建多少个磁盘文件呢?很简单,下一个stage的task有多少个,当前stage的每个task就要创建多少份磁盘文件。比如下一个stage总共有100个task,那么当前stage的每个task都要创建100份磁盘文件。如果当前stage有50个task,总共有10个Executor,每个Executor执行5个Task,那么每个Executor上总共就要创建500个磁盘文件,所有Executor上会创建5000个磁盘文件。由此可见,未经优化的shuffle write操作所产生的磁盘文件的数量是极其惊人的。
接着我们来说说shuffle read。shuffle read,通常就是一个stage刚开始时要做的事情。此时该stage的每一个task就需要将上一个stage的计算结果中的所有相同key,从各个节点上通过网络都拉取到自己所在的节点上,然后进行key的聚合或连接等操作。由于shuffle write的过程中,task给下游stage的每个task都创建了一个磁盘文件,因此shuffle read的过程中,每个task只要从上游stage的所有task所在节点上,拉取属于自己的那一个磁盘文件即可。

shuffle read的拉取过程是一边拉取一边进行聚合的。每个shuffle read task都会有一个自己的buffer缓冲,每次都只能拉取与buffer缓冲相同大小的数据,然后通过内存中的一个Map进行聚合等操作。聚合完一批数据后,再拉取下一批数据,并放到buffer缓冲中进行聚合操作。以此类推,直到最后将所有数据到拉取完,并得到最终的结果。

在这里插入图片描述

SortShuffleManager运行原理
SortShuffleManager的运行机制主要分成两种,一种是普通运行机制,另一种是bypass运行机制。当shuffle read task的数量小于等于spark.shuffle.sort.bypassMergeThreshold参数的值时(默认为200),就会启用bypass机制。
普通运行机制
下图说明了普通的SortShuffleManager的原理。在该模式下,数据会先写入一个内存数据结构中,此时根据不同的shuffle算子,可能选用不同的数据结构。如果是reduceByKey这种聚合类的shuffle算子,那么会选用Map数据结构,一边通过Map进行聚合,一边写入内存;如果是join这种普通的shuffle算子,那么会选用Array数据结构,直接写入内存。接着,每写一条数据进入内存数据结构之后,就会判断一下,是否达到了某个临界阈值。如果达到临界阈值的话,那么就会尝试将内存数据结构中的数据溢写到磁盘,然后清空内存数据结构。
在溢写到磁盘文件之前,会先根据key对内存数据结构中已有的数据进行排序。排序过后,会分批将数据写入磁盘文件。默认的batch数量是10000条,也就是说,排序好的数据,会以每批1万条数据的形式分批写入磁盘文件。写入磁盘文件是通过Java的BufferedOutputStream实现的。BufferedOutputStream是Java的缓冲输出流,首先会将数据缓冲在内存中,当内存缓冲满溢之后再一次写入磁盘文件中,这样可以减少磁盘IO次数,提升性能。
一个task将所有数据写入内存数据结构的过程中,会发生多次磁盘溢写操作,也就会产生多个临时文件。最后会将之前所有的临时磁盘文件都进行合并,这就是merge过程,此时会将之前所有临时磁盘文件中的数据读取出来,然后依次写入最终的磁盘文件之中。此外,由于一个task就只对应一个磁盘文件,也就意味着该task为下游stage的task准备的数据都在这一个文件中,因此还会单独写一份索引文件,其中标识了下游各个task的数据在文件中的start offset与end offset。
SortShuffleManager由于有一个磁盘文件merge的过程,因此大大减少了文件数量。比如第一个stage有50个task,总共有10个Executor,每个Executor执行5个task,而第二个stage有100个task。由于每个task最终只有一个磁盘文件,因此此时每个Executor上只有5个磁盘文件,所有Executor只有50个磁盘文件。

在这里插入图片描述
bypass运行机制
下图说明了bypass SortShuffleManager的原理。bypass运行机制的触发条件如下:
shuffle map task数量小于spark.shuffle.sort.bypassMergeThreshold参数的值。
不是聚合类的shuffle算子(比如reduceByKey)。
此时task会为每个下游task都创建一个临时磁盘文件,并将数据按key进行hash然后根据key的hash值,将key写入对应的磁盘文件之中。当然,写入磁盘文件时也是先写入内存缓冲,缓冲写满之后再溢写到磁盘文件的。最后,同样会将所有临时磁盘文件都合并成一个磁盘文件,并创建一个单独的索引文件。
该过程的磁盘写机制其实跟未经优化的HashShuffleManager是一模一样的,因为都要创建数量惊人的磁盘文件,只是在最后会做一个磁盘文件的合并而已。因此少量的最终磁盘文件,也让该机制相对未经优化的HashShuffleManager来说,shuffle read的性能会更好。
而该机制与普通SortShuffleManager运行机制的不同在于:第一,磁盘写机制不同;第二,不会进行排序。也就是说,启用该机制的最大好处在于,shuffle write过程中,不需要进行数据的排序操作,也就节省掉了这部分的性能开销。

在这里插入图片描述

shuffle相关参数调优
以下是Shffule过程中的一些主要参数,这里详细讲解了各个参数的功能、默认值以及基于实践经验给出的调优建议。
spark.shuffle.file.buffer
默认值:32k
参数说明:该参数用于设置shuffle write task的BufferedOutputStream的buffer缓冲大小。将数据写到磁盘文件之前,会先写入buffer缓冲中,待缓冲写满之后,才会溢写到磁盘。
调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如64k),从而减少shuffle write过程中溢写磁盘文件的次数,也就可以减少磁盘IO次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。
spark.reducer.maxSizeInFlight
默认值:48m
参数说明:该参数用于设置shuffle read task的buffer缓冲大小,而这个buffer缓冲决定了每次能够拉取多少数据。
调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如96m),从而减少拉取数据的次数,也就可以减少网络传输的次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。
spark.shuffle.io.maxRetries
默认值:3
参数说明:shuffle read task从shuffle write task所在节点拉取属于自己的数据时,如果因为网络异常导致拉取失败,是会自动进行重试的。该参数就代表了可以重试的最大次数。如果在指定次数之内拉取还是没有成功,就可能会导致作业执行失败。
调优建议:对于那些包含了特别耗时的shuffle操作的作业,建议增加重试最大次数(比如60次),以避免由于JVM的full gc或者网络不稳定等因素导致的数据拉取失败。在实践中发现,对于针对超大数据量(数十亿~上百亿)的shuffle过程,调节该参数可以大幅度提升稳定性。
spark.shuffle.io.retryWait
默认值:5s
参数说明:具体解释同上,该参数代表了每次重试拉取数据的等待间隔,默认是5s。
调优建议:建议加大间隔时长(比如60s),以增加shuffle操作的稳定性。
spark.shuffle.memoryFraction
默认值:0.2
参数说明:该参数代表了Executor内存中,分配给shuffle read task进行聚合操作的内存比例,默认是20%。
调优建议:在资源参数调优中讲解过这个参数。如果内存充足,而且很少使用持久化操作,建议调高这个比例,给shuffle read的聚合操作更多内存,以避免由于内存不足导致聚合过程中频繁读写磁盘。在实践中发现,合理调节该参数可以将性能提升10%左右。
spark.shuffle.manager
默认值:sort
参数说明:该参数用于设置ShuffleManager的类型。Spark 1.5以后,有三个可选项:hash、sort和tungsten-sort。HashShuffleManager是Spark 1.2以前的默认选项,但是Spark 1.2以及之后的版本默认都是SortShuffleManager了。tungsten-sort与sort类似,但是使用了tungsten计划中的堆外内存管理机制,内存使用效率更高。
调优建议:由于SortShuffleManager默认会对数据进行排序,因此如果你的业务逻辑中需要该排序机制的话,则使用默认的SortShuffleManager就可以;而如果你的业务逻辑不需要对数据进行排序,那么建议参考后面的几个参数调优,通过bypass机制或优化的HashShuffleManager来避免排序操作,同时提供较好的磁盘读写性能。这里要注意的是,tungsten-sort要慎用,因为之前发现了一些相应的bug。
spark.shuffle.sort.bypassMergeThreshold
默认值:200
参数说明:当ShuffleManager为SortShuffleManager时,如果shuffle read task的数量小于这个阈值(默认是200),则shuffle write过程中不会进行排序操作,而是直接按照未经优化的HashShuffleManager的方式去写数据,但是最后会将每个task产生的所有临时磁盘文件都合并成一个文件,并会创建单独的索引文件。
调优建议:当你使用SortShuffleManager时,如果的确不需要排序操作,那么建议将这个参数调大一些,大于shuffle read task的数量。那么此时就会自动启用bypass机制,map-side就不会进行排序了,减少了排序的性能开销。但是这种方式下,依然会产生大量的磁盘文件,因此shuffle write性能有待提高。
spark.shuffle.consolidateFiles
默认值:false
参数说明:如果使用HashShuffleManager,该参数有效。如果设置为true,那么就会开启consolidate机制,会大幅度合并shuffle write的输出文件,对于shuffle read task数量特别多的情况下,这种方法可以极大地减少磁盘IO开销,提升性能。
调优建议:如果的确不需要SortShuffleManager的排序机制,那么除了使用bypass机制,还可以尝试将spark.shffle.manager参数手动指定为hash,使用HashShuffleManager,同时开启consolidate机制。在实践中尝试过,发现其性能比开启了bypass机制的SortShuffleManager要高出10%~30%。

附录

特定的JDK版本
原文名称:为Spark Application指定不同的JDK版本
原文链接:https://www.jianshu.com/p/d6377b00f678

某些特殊的场景下,我们对集群没有管理权限,只能通过YARN提交Application,并且集群里没有部署我们需要的JDK版本,这种情形就需要将JDK的安装包也一并提交了。
这里要求我们的JDK安装包必须为gz格式的,和你代码打包后的jar包放在同一目录下,假设我们下载的JDK的安装包为:jdk-8u141-linux-x64.tar.gz。

关键配置如下:
$SPARK_HOME/bin/spark-submit
–conf “spark.yarn.dist.archives=jdk-8u141-linux-x64.tar.gz”
–conf “spark.executorEnv.JAVA_HOME=./jdk-8u141-linux-x64.tar.gz/jdk1.8.0_141”
–conf “spark.yarn.appMasterEnv.JAVA_HOME=./jdk-8u141-linux-x64.tar.gz/jdk1.8.0_141”

我们可以通过指定spark.yarn.dist.archives配置,将JDK的安装包分发到所有Executor的工作目录下(包括Application Master的Executor),另外tar.gz的压缩包也会被自动解压,假设jdk-8u141-linux-x64.tar.gz解压后的目录为jdk1.8.0_141,那么我们特定的JDK的目录就是:./jdk-8u141-linux-x64.tar.gz/jdk1.8.0_141,不同的JDK版本以此类推即可。

Yarn资源调度策略
FIFO Scheduler
Campacity Scheduler
Fair Scheduler

Hadoop配置文件解析

Yarn-site.xml
yarn.resourcemanager.am.max-attempts

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值