前言
工作原因,时常接到一些用户关于Spark SQL运行缓慢的问题,总体是上游数据存储不规范,SQL优化不合理,Spark框架优化参数不到位。用户时常问出令我们一时语塞的问题,为什么就几百万条数据,用了(1C4G) * 30的资源量处理了两个小时呢?为什么会内存溢出呢?显然需要具体问题具体分析,但是没给出原因之前,用户只会将责任抛给我们。
大部分问题是用户自身的问题,奈何用户不断向上投诉,不得不解。这也就是总结本文的主要动机。很遗憾,我原只想总结出一些优化建议让用户做些什么,不建议做什么,但最终发现出现性能问题的往往是复杂SQL,而复杂SQL优化是一个系统性的工程,不了解Spark以及Spark SQL原理是无法作出合适的优化决策的,但如果事事俱细,文章又会大且杂,因此本文先抛出结论,如果对原理感兴趣,可以点击各个结论右侧的链接。有些链接是我阅读过认为很棒的文章,以及Spark官网介绍,有些是发现网上文章存在一些模糊的表述,自己阅读源码的总结。
本文基于3.1.2版本的Spark源码。 数仓用户更倾向使用SQL,因此文章更偏向从SQL角度优化(有些可以通过代码优化的方式没有体现),本文针对的是Hive数据源。一些观点是个人总结,比如优化思路,未必能覆盖所有场景,考虑不周,还请指出。
优化方向
Spark SQL性能调优总体区分为三个方向(为什么是这三个优化方向?):
- 数据存储结构优化;
- Spark框架优化;
- SQL优化。
数据存储结构优化
数据存储结构影响了读写的效率,并很大程度上影响整体运行效率。比如Hive不合理的分区,导致读取时数据倾斜,让少部分Spark task承担了大部分数据;小文件数量过多,甚至会出现数十万task的场景(为什么小文件多,task也多?)。在不合理的存储结构定下来后,Spark框架和SQL的调优就变得复杂,且提升空间较少,甚至有可能出现为了调优,使得集群带宽成为瓶颈,因此优先需要考虑数据存储结构。
数据存储结构设计的最终目的是减少大量的目录,以及尽可能地避免产生大量小文件,数据应当尽可能均匀分布,在允许条件下选择合适压缩算法,尽可能使得文件可分割。
分区设计
数据存储结构中,分区设计是相当重要的环节。它应当根据业务查询和处理来设计分区键,理想的分区设计在数据持续增长的情况下,不应该产生太多分区和目录,并且每个目录下的文件应当足够大,一般是文件系统中块大小的数倍,每个分区的数据量均匀分布。
常见解决方案是按天分区、二级分区。常见的二级分区是第一级按天,第二级使用不同的维度,取决于预期的频繁查询SQL。
分桶设计
当分区设计始终无法接近于理想分区时,应考虑分桶存储。考虑这种场景:一级分区按天,二级分区按userId,在用户较多的情况下,会出现很多二级分区。因此可以考虑将userId分桶,同个userId会存到同个桶内,若干个userId会在同个桶内,比如:
# 引用《Hive编程指南》9.6章节
CREATE TABLE demo (user_id STRING, source_ip STRING)
PARTITIONED BY (dt STRING)
CLUSTERED BY (user_id) INTO 96 BUCKETS;
分桶键的值需要足够多且均匀,避免出现hive分桶时哈希取模数据倾斜到若干个桶里。
当我们使用Spark以分桶键进行聚合和连接时,经常出现Shuffle,只要Shuffle不发生明显数据倾斜,运行效率还是会比不分桶造成大量小文件的情况快(一般)。当然,当业务频繁地需要以分桶键进行聚合和连接,并且发生了Shuffle,且处理的数据量较大,就意味着可能会有大量数据在网络上传输,也会导致运行缓慢,当集群带宽已经成为瓶颈,那就应当小心。
数据压缩
压缩和解压自然需要时间,会影响计算效率,但在大数据场景下,也需要重视集群带宽和存储能力,因此需要选择合适的压缩算法。
选择压缩格式需要综合考虑:
- 支持并行计算,如GZIP和Snappy不支持Split,不利于发挥出Spark的并行计算能力;
- 压缩/解压效率,写文件时,期望压缩越快越好;读文件时,期望解压越快越好。各种格式的压缩/解压效率参考这篇文章;
- 压缩比,一般在兼顾上面两点的情况下,选择压缩比高的,一般压缩比为:Snappy<LZO<GZIP<BZIP2
主要根据数据本身的场景选择,比如大文件归档场景,极少用来计算,那么可以考虑采用GZIP和BZIP2;热点数据场景,可以选择不压缩,但当文件比较大的时候,建议压缩,考虑使用LZO。
文章主要面向ETL场景,因此建议使用LZO,但非绝对,要根据实际场景选择压缩算法。
存储格式
Spark针对TextFile没有特别的优化,当小文件过多时,相应地,Task也会多,当无法避免大量小文件时,后续又需要Spark进行处理,那么TextFile不是一个良好的选择(源码分析TextFile格式小文件数量与Task的关系)。
Spark针对ORC和Parquet文件有一定优化。需要根据业务场景选择存储格式。实测读取ORC文件时Task的数量,源码分析Spark读取Parquet格式文件生成的Task数量。
这里做一个小结:
数据生产者应注意的事项
我们处理数据需要与上下游业务制定数据存储规范:
- 不要使用过多的分区:如果指定表分区增长较快,一天增长数百个分区,那么一天就可能增长数万个小文件,影响Hadoop的性能和扩展性(为什么?),Spark不适合处理小文件;
- 数据分区应均匀:数据倾斜影响下游处理效率,小部分Spark task处理大部分的数据;
- 应避免大量小文件:尤其是上游是流计算相关业务时,又或者不合理的join(Spark SQL),可能会输出大量小文件,小文件多会直接导致Spark task数量多,时间耗费在创建和销毁task上;
- 合理的压缩算法,尽可能选择可分割、压缩/解压效率高的,归档大文件场景则使用压缩比高的;
- 合适的存储格式,Spark针对ORC/Parquet格式有优化,能一定程度上避免小文件过多带来的调度开销。
当然,数仓用户了解的优化措施更多,此处只是给出会影响到Spark处理的优化思路。
如果小文件多是既定事实,建议优先通过Hive原生支持合并小文件,再使用Spark处理,或者确认是否能够将数据存储格式转换为ORC/Parquet,Spark针对这两种格式有RDD分区合并上的优化;如果源数据倾斜是既定事实,那么只好从Spark调优参数和SQL去优化。
Spark框架调优和SQL往往是无法分割的,因此这两个优化方向应按整体来看,通过问题场景来分析:
优化场景
优化前的信息收集是关键手段,因此建议不要关闭Spark UI(spark-submit … --conf spark.ui.enabled=true,默认开启),以及开启history server(start-history-server.sh)。
Spark提供了一些调优参数,建议在使用参数时优先查看官网参数说明,目前最新版本是3.1.2,可以看到历史版本的参数。官方文档提供了语法示例,必要时参考。
个别Task运行缓慢
现象很明显,大部分task早已结束,少数task仍在运行。如果是纯SQL的情况,一般是数据倾斜导致的,需要检查源端数据分布情况,以及处理过程中是否造成了数据倾斜;如果是结合业务处理编写代码的情况,那首先要排除掉数据倾斜,再分析业务逻辑中可能出现阻塞/运行缓慢的点,排查这种场景就是具体问题具体分析了。
源端数据倾斜
可以检查读取数据的Stage(怎么确认当前Stage是否在读取数据?)中的Task数据分布情况(怎么查看?),是否出现了明显的倾斜,少部分Task处理了大部分数据,或存在部分Task中处理的数据量过少。
如果源端供数尚能调整,建议调整,调整时应当注意选择的压缩格式应是可分割的,这样在读取大文件场景下,也可以更加合理的分区;如果无法调整,那么就需要采取REPARTITION/COALESCE Hint、DISRTIBUTE BY/CLUSTER BY的方式重分布数据,让数据均匀分布到各个RDD分区上,但要注意不是每个转换都能确保均匀分布,要注意各个转换的区别,合理选择。
如果不采用SQL方式,可以主动对RDD/DataFrame调用repartition/coalesce。
处理过程中的数据倾斜
主要表现在Shuffle时分布不均,根据Stage中的Summer Metrics以及各个Task中的Shuffle Read字段分析(怎么查看?)。
不合理的哈系分布
当数据倾斜到一个分区时,有可能是选择的分布键不合理,尤其是使用HashPartition的转换(有GROUP BY, DISTRIBUTE BY, CLUSTER BY等),Spark通过对分布键哈系取模的方式将数据分发到不同RDD分区。那么此时的优化手段有四种:
- 增加Reduc
最低0.47元/天 解锁文章
2237

被折叠的 条评论
为什么被折叠?



