最全的Spark基础知识解答

一、选择Parquet + 优化Parquet

大数据领域中对数据的一次处理往往只针对一行数据中的若干列,因此列式存储是大数据领域中最常见的数据存储优化方式,而Parquet毫无疑问是其中最优秀的代表。

Parquet存储格式

Parquet以其独特的存储格式可以取得良好的数据压缩率和数据读取速度而广受青睐。Spark-2.0 特别针对Parquet做了优化,使其读取性能更上了一个台阶。

Parquet不同版本读写性能对比

这里将不具体展开介绍Parquet的存储格式,如想了解更多,请访问Parquet官网。下面将重点介绍Parquet使用中的3个优化点,从此之后Parquet的使用将不再仅仅是:

df.write.option("compression","none").mode("overwrite").parquet("hdfs://xxxxxx/data")

1)重排数据行

Parquet存储数据时是先按行将一个file切分成若干个Row group,再将Row group按照切分为若干个Column chunk,再针对Column chunk选择一系列的压缩编码存储数据。对数据先排序再存Parquet,存储后的数据可以获得更高的压缩比,数据读取时可以skip更多的Row group。

假设数据中有一列表示性别,其有两个值0和1分别表示男女。若未对数据进行排序,那么包含性别为男的数据行就会分散在所有的Rowgroup中,若要查询性别为男的数据,那么必须scan所有的Rowgroup读取性别列对应的Column chunk到内存查找对应数据。若对数据按照性别列进行排序,那么性别为男的数据只会出现在部分Row group中,要查询性别为男的数据只需要读取部分Row group即可。

实际生产中我们发现,对于拥有50+列的数据,对其中常用的若干列进行排序存储成Paruqet相比于未排序数据,数据压缩比可提高30%,针对排序列的计算数据读取速度可以提高2倍以上。

2)重整数据列

对于Parquet数据,同一个Rowgroup的Column chunk是连续存储的,Spark读取数据时是按Column chunk读取的。若大量数据处理任务中存在一些pattern,这些pattern由若干列组成,数据处理时经常需要将这些列同时读入内存进行处理,那么在存储数据时若将这些列依序存储,在数据读取时就可以一次将所需的多个Column chunk一次读入内存,读取数据的性能就可以有较大的提升。

我们团队开发和运营着一个针对海量数据OLAP查询的数据平台,该平台每天会有大量的用户查询,这些用户查询是针对广告数据的多维分析,每个查询会涉及到多个维度。平台现支持超过200个维度的查询,已积累了数十万的用户分析任务,我们基于所有查询任务构建了一个连通图来分析挖掘最优的pattern,并用挖掘出的pattern来优化Parquet数据的存储。该连通图的vertex是所有的维度,edge是一次查询共现的维度的连接,edge的weight由维度共现的频次以及任务本身的权重组成。

3)Filter Push Down

Filter Push Down是Spark针对Parquet数据提供的一个特性,spark.sql.parquet.filterPushdown 参数控制FilterPush Down的开启,Spark默认开启。FilterPush Down最常用在Spark SQL的查询中,其基本思想是基于Parquet预先存储的statistic数据(包括Row group的statistic数据和Column chunk的statistic数据)以及Spark SQL语句中带的where条件来skip掉无关的数据,只读取必须的部分数据。但如此重要的Spark SQL优化点官方并没有给出详细而具体的使用说明,在这里特别强调一下Filter Push Down真正生效的应用场景,基于此可在将数据存储为Parquet时选择合适的数据类型。

·   能够FilterPush Down的数据类型:int

·   能够FilterPush Down的运算符:<, <=, > ,>=

因此在将数据存储为Parquet格式时,从使用性能的角度考虑应尽可能使用int类型。

二、CPU 和内存资源的合理分配

1)单个executor的core和memory配置

提交spark任务时,有三个和资源相关的基本参数需要设置:executor个数(--num-executors)

、单个executor的core数量(--executor-cores)、单个executor的memory大小(--executormemory),这三个参数的设置很大程度上决定了任务运行的时长。

executor个数设置相对简单,若集群资源足够,那么executor个数越多任务的并发度就越高任务运行时间就越短。但executor的增长也应该在一个合理的范围内,大任务建议控制在1000以内,小任务建议在200以内。过多的executor会导致向集群申请资源的过程变得较长。

在一个Spark任务中,core的数量就是该任务能同时并发的task的个数,core越多就意味着task并发越高,任务的运行时间越短。对于需要读取几十T数据的大任务,core数量的大小很大程度上直接决定了任务的运行时长。那么单个executor的core数量是否越大越好呢?答案是否定的。Spark任务用到的资源除了core和memory外还包括网络带宽和磁盘I/O,对于普通的server,当单个executor的core个数大于5时,server的带宽和I/O就会被打满,此时增加core只会导致单个task的完成时间大幅增长,导致整个任务的耗时反而变长。因此一般建议单个executor的core数量控制在3个左右。

单个executor的最适合的memory大小是刚好满足task的内存需求,过多则浪费集群资源,过少则会导致GC较高影响任务进度,一般建议控制在15G以内。若在任务中需要broadcast一个较大的配置或者需要实现自定义的耗内存的mapPartition、foreachPartition算子,那么可以合理的增加内存大小。

2)dynamicAllocation并不一定高效

对于一个需要同时跑大量任务的Spark集群,开启spark.dynamicAllocation选项是一个很常见的选择。但是开启spark.dynamicAllocation选项是基于以下前提:一个Spark Application划分为若干个job,一个job又分为若干个stage,每个stage对于资源的需求是不一样的,因此若当前Application暂时用不到申请到的executor时(Spark默认一分钟未使用即视为闲置),集群会将移除其已申请但闲置的executor,等某个stage需要更多的executor时就会再次向集群申请。释放出的资源就可以有其他的Spark Application获得。这样可以一定程度上提高集群资源的利用率。

但是,上述前提在有些场景下并不适用。一个集群中需要跑大量的任务,每个任务正常运行完成的时间不一致,每个任务的重要性也不同,若将任务都开启spark.dynamicAllocation选项,那么我们发现就会出现这样的现象:集群任务的高峰时段,会有大量的任务同时在跑,但每个任务的完成时间都远超过其正常运行完成时间,不少任务甚至一直无法正常完成。这时候集群资源利用率的确是达到了100%,但集群任务的吞吐量却将为了0,适得其反了。

那么为何会出现这种现象呢?原因是这样的:假设有个application申请到了200个executor,当其任务DAG的某些stage需要的executor数较少,因此集群就会移除闲置的executor资源, 在任务高峰期,此时集群中新提交的任务就会立刻占用了这些资源,当之前释放资源的application需要更多的executor而再向集群申请executor时机会申请不到资源,导致任务只能以较少的executor,较慢的速度缓慢执行。

3)数据本地性在有些场景并不适用

Spark中任务的处理需要考虑所涉及的数据的本地性, 在理想的情况下,任务执行的最佳方式是从本地读取数据并在本地进行计算。但是每个任务的执行速度无法准确估计,每个executor所在的机器的负载也不一致,当集群中有较多任务,且部分任务执行时间较长,那么执行任务较长的节点就会一直占用所在机器的CPU资源,当另一个任务需要读取这些节点上的数据时,根据数据本地性原则这些任务就需要等待即使别的节点已有空闲资源。

Spark中spark.locality.wait.process、spark.locality.wait.node、spark.locality.wait.rack这三个参数用来调节任务执行时的本地策略,这几个参数决定了选择暂时不分配任务,而是等待获得满足进程内部/节点内部/机架内部这样的不同层次的本地性资源的最长等待时间。默认都是3000毫秒。从Spark的UI中可以看到每个task执行时的数据本地性策略。

数据本地性(Locality Level)

总结一下,如果你的任务数量较大和单个任务运行时间比较长的情况下,单个任务是否在数据本地运行,代价区别可能比较显著,如果数据本地性不理想,那么调大这些参数对于性能优化可能会有一定的好处。反之如果等待的代价超过带来的收益,那就不要考虑了,可以直接将上述三个参数设置为0s。在我们的集群中,任务高峰期将上述三个参数设置为0s可以取得更好的效果。

三、不可忽视的driver调优

在spark调优中,driver往往是最容易被忽视的,但其很多特性对于任务的稳定运行息息相关。这里将介绍几点我们在实践中总结的经验。

 1)spark.driver.memory配置

driver内存分配主要考虑两个因素,一个是broadcast变量的大小,另一个是整个Application中task的数量。对于broadcast,driver内存会将原始数据切分为一系列的小的block,在切分完成之前,driver需要两倍于broadcast变量大小的内存空间。driver同时还负责维护集群中每个task的状态,每个task的执行结果,task个数越多,消耗的内存也会越多。当spark.driver.memory配置的内存较小时,会导致OOM。因此需要根据任务的实际情况合理分配spark.driver.memory的大小。

 2)过多partition下的参数配置

上文提到driver维护着每个task的状态信息,当任务中存在较多的partition(几十万),一个partition对应一个task,那么driver和每个task的通信负担就会变得很重。此时需要设置spark.driver.cores来增加driver的线程个数,设置spark.rpc.io.serverThreads来增加处理和task之间的RPC请求的线程个数。

过多partition

3)spark.driver.maxResultSize配置

除了上述两个参数外还有一个参数至关重要,即spark.driver.maxResultSize,该参数用来控制每个task送回driver的数据总量大小,当数据总量大小超过该值时,任务就是失败了。以上图中的任务为例,任务执行过程中从driver的log中可以看到如下报错信息:

exe error:org.apache.spark.SparkException: Job aborted due to stage failure: Total sizeof serialized results of 285085 tasks (10.0 GB) is bigger thanspark.driver.maxResultSize (10.0 GB)

因此若某个任务需要读取的数据量非常大,partition个数达到几十万甚至上百万,那么久需要根据实际情况增大driver的内存。

END

碧茂课堂精彩课程推荐:

1.Cloudera数据分析课;

2.Spark和Hadoop开发员培训;

3.大数据机器学习之推荐系统;

4.Python数据分析与机器学习实战;

详情请关注我们公众号:碧茂大数据-课程产品-碧茂课堂

现在注册互动得海量学币,大量精品课程免费送!

猜你喜欢

转载自blog.csdn.net/ShuYunBIGDATA/article/details/89471630