zh:
Spark 是一个用于处理大数据的框架。在第一部分中,我们重点介绍了 Spark 的基础知识以及 为什么它这么快。
- 在这篇博客中,我们将重点讨论一些常见的错误及其修复和优化方法,这些方法将用于提升我们Spark应用程序的性能和内存利用率。
- 这些内容包括集群的优化、调整配置值、代码层面的优化等。
Spark 并不是像传统脚本那样一行行地执行代码。
data = spark.read.csv("large_file.csv")
data.filter(data["age"] > 30)
print('年龄筛选完成。')
这里会执行打印语句,但 Spark 还未执行过滤操作。
Spark 使用惰性计算,也就是说它只会在触发动作(如 .collect()
或 .saveAsTextFile()
)时执行转换操作。如果你期望立即看到结果,这种情况下可能会导致困惑。
- 弄清楚转换(延迟操作)和行动(触发)之间的区别。
data = spark.read.csv("large_file.csv")
filtered_data = data.filter(data["age"] > 30)
filtered_data.show() # 这会触发执行
错误 2: 没有考虑到数据分布,使用默认的分区方式。
数据分区不当可能导致工作分配不均,从而导致某些任务会变慢甚至系统崩溃。
- 忽略分区操作
数据分区对于性能至关重要,尤其是在连接数据集时更是如此。如果不进行适当的分区,Spark 可能会反复进行数据重排,从而导致不必要的性能损失。
并行处理在优化Spark作业时扮演着非常重要的角色。每个分区或任务都需要一个核心来处理。
- 使用过多或过少的分区
过多的分区会增加不必要的开销,而过少的分区会导致集群资源利用率不高。这需要根据数据量和集群资源来调整。通常建议分区数量是核心数的2到3倍。
不调整大型数据集的分区可能导致处理不均衡。
large_data = spark.read.csv("large_file.csv")
print(large_data.rdd.getNumPartitions()) # 这可能会显示默认的分区数量,这个数字可能较高或较低
2.2 使用 .repartition()
或 .coalesce()
来调整分区数量
partitioned_data = large_data.repartition(10)
print(partitioned_data.rdd.getNumPartitions()) # 当前设置为10个分区
**.重新分区(numPartitions)**
- 用于增加或减少分区的数量。它会在集群中的节点之间重新分配数据,因此这是一项昂贵的操作。
- 当你需要更多分区或在完成连接操作后,用于均匀分配数据。
- 使用
.repartition()
函数,当你需要增加分区或需要完全重新洗牌时。
**.coalesce(numPartitions)**
:将分区数量减少到指定数目。
- 主要用于减少分区数量。它通过在同一个节点上合并分区来避免数据重新分配,因此它比
**.repartition()**
更经济。 - 在处理结束时用于合并分区,特别适合准备输出时。示例:
data.coalesce(5)
- 当你想要减少分区而不进行数据重新分配以提高效率时,使用
.coalesce()
。
未对多次重复使用的数据进行缓存。
Spark 每次都会重新计算这些转换,如果这些计算过程比较复杂,这可能会消耗很多资源。
## 没有使用缓存多次使用数据:
多次使用数据而没有使用缓存:
## 过滤年龄大于30的数据
filtered_data = data.filter(data["age"] > 30)
## 计算过滤后的数据条数
filtered_data.count()
## 显示过滤后的数据
filtered_data.show()
每次动作都会再次执行过滤步骤。
对于频繁用于多个步骤中的数据,请使用 .cache()
或 .persist()
,等等。
# 如果你需要重复使用这些数据,请先将其缓存(即保存数据以提高后续访问速度)
filtered_data = data.filter(data["age"] > 30).cache()
filtered_data.count() # 计算过滤后的数据条数
filtered_data.show() # 显示过滤后的数据
# 再次显示时会更快
3.1 缓存和持久化使用不当
很多人没注意到缓存(例如 cache()
或 persist()
),这可以避免在迭代过程中或重复访问时重新计算。
然而,缓存所有内容可能会引发内存问题。仅在确实需要时才缓存,并确保在不再需要数据时执行 unpersist
。
3.2 过长的转换链
没有中间操作或缓存的很长的转换链会使流程变复杂,增加执行时长。有时最好把复杂的链拆分一下,加入一些操作或把数据缓存起来。
不了解 shuffle 是如何工作的会导致变慢或崩溃。默认的 shuffle 在大型连接时可能会引起延迟或错误。
- shuffle 操作(如
join
和groupBy
)对网络和内存消耗较大。如果配置不当,可能可能导致这些操作失败。
我们把large_data1和large_data2通过'id'这个字段连接在一起,然后展示出来的。
joined_data = large_data1.join(large_data2, "id")
joined_data.show()
- 根据数据大小调整 shuffle 相关的配置(例如
spark.sql.shuffle.partitions
),并尽可能减少 shuffle。
spark.conf.set("spark.sql.shuffle.partitions", "100") # 根据数据量调整分区数量
合并数据 = large_data1.join(large_data2, "id")
合并数据.show()
为驱动而收集大量数据集
请注意:在处理大型数据集时不要使用 .collect()
或 .take()
。
all_data = data.collect() # 将整个数据集收集到驱动器中:
这可能会引发内存错误,当data
太大时,
- 使用
collect
操作时:collect
会将整个数据集带到驱动程序中,可能导致驱动程序崩溃。通常最好使用如take
或takeSample
这样的操作来处理少量数据,并且除非数据集足够小,否则避免使用collect
。 - 尽量减少收集的数据量,或者直接将数据写入存储中,而不是将其带到驱动程序中。
sample_data = data.limit(1000).collect() # 只收集1000条数据
# 或者直接将数据写入存储路径 "output_path"
误区6:忽略优化技术
不使用诸如广播连接或Catalyst优化器之类的优化技术。
Spark 有一些优化技术来提升性能的表现,但需要理解何时使用它们。
large_data.join(small_data, "id").show() # 根据"id"字段在大型数据表和小型数据表之间连接会相当慢
使用广播连接来处理小数据量的表,并使用DataFrame接口,这些接口经过Spark Catalyst优化器的优化。
# 我们对小数据集使用广播连接来优化连接操作
from pyspark.sql.functions import broadcast
large_data.join(broadcast(small_data), "id").show() # 广播连接可以优化这个连接操作
spark/apache-spark-优化技巧 (https://www.toptal.com/spark/apache-spark-optimization-techniques)
错误 7. 不太给力的聚合- 错误:在不了解其对性能影响的情况下跑聚合。
像 .groupBy()
或 .reduceByKey()
这样的操作会涉及大量的数据洗牌(shuffle),在处理大规模数据集时可能会变得很慢。尽可能使用 预聚合 的数据或在洗牌前进行局部聚合。这样在进行数据洗牌之前先执行局部聚合会更有效。
例子:
# 不要直接对大量数据进行分组操作
data.groupBy("category").sum("amount").show()
# 尽可能在 map 端进行聚合
data = data.map(lambda x: (x['category'], x['amount'])) \
.reduceByKey(lambda a, b: a + b)
- 使用
groupByKey
而不是reduceByKey
groupByKey
将所有具有相同键的值发送到单个执行器,这可能导致内存不足的问题。而reduceByKey
会在将数据发送到其他节点之前,在每个分区上先进行局部缩减,从而通常更高效,而且更节省内存。
其他步骤……
优化Spark配置: 这包括更改Spark属性。例如:调整Spark执行器,调整Spark内存……
优化存储: 使用正确的文件格式,例如ORC和Parquet,这些格式内存效率高,可以加速查询。
记录转换步骤
在调试或优化过程中,记录哪些转换步骤被应用是非常重要的。这样当某个转换或操作失败时,就能更容易识别问题和瓶颈。
共同学习,写下你的评论
评论加载中...
作者其他优质文章