Spark 的优化思路主要从代码、数据结构、资源配置和执行计划等多个方面入手,以提升计算性能和资源利用率。以下是一些关键的优化方法和思路:
1. 数据读取优化
• 数据格式:优先选择列式存储格式(如 Parquet、ORC),能减少 I/O 和提升数据压缩率,特别适合分析场景。
• 数据分区:对于大数据集,合理的数据分区可以减少任务调度和数据传输成本。在加载数据时可以使用分区字段,通过条件过滤减少数据读取量。
• 过滤条件下推(Predicate Pushdown) :确保过滤条件下推到数据源端,比如对于 Parquet 文件,Spark 会将过滤条件传递到读取层,减少不必要的数据读取。
• 文件合并:避免过多的小文件导致的文件系统开销,Spark 支持小文件合并,可以通过调整 spark.sql.files.maxPartitionBytes 及 spark.sql.files.openCostInBytes 来控制读取行为。
2. Spark SQL 优化
• 广播小表:在 Join 操作中,针对小表可以使用 broadcast() 方法将其广播到每个节点,减少数据传输成本。自动广播阈值可以通过 spark.sql.autoBroadcastJoinThreshold 设置。
• 分区剪裁:确保查询涉及到分区键,Spark SQL 会自动识别和跳过无关的分区。
• 选择合适的 Join 策略:合理使用 Broadcast Hash Join、Sort Merge Join 等策略。通常,Hash Join 适合小数据集,Sort Merge Join 适合大数据集。
• 缓存中间数据:对于多次使用的中间结果,可以使用 cache() 或 persist() 方法将数据缓存,以避免重复计算。
3. 持久化和缓存
• 选择持久化级别:持久化时,可以选择不同的存储级别(如 MEMORY_ONLY、MEMORY_AND_DISK)。尽量使用内存存储数据,但如果内存不足,可以选择 MEMORY_AND_DISK,将数据溢出到磁盘。
• 及时清理缓存:用完后及时释放缓存,避免内存泄漏,特别是在长时间运行的任务中,缓存无用数据会导致内存不足。
4. 任务并行度(Parallelism)调整
• 分区数调整:默认分区数可以通过 spark.sql.shuffle.partitions 或 spark.default.parallelism 设置,避免分区过多或过少导致资源利用率低。通常建议每个核心处理 2-4 个任务。
• 合理配置并行度:针对 Shuffle 操作和 Join 操作,调整并行度以提升计算性能,同时防止任务过度分区带来的调度开销。
5. 减少 Shuffle 操作
• 使用 map-side combine:可以在数据倾斜较为严重的情况下先在 map 端进行局部聚合,减少数据传输量。
• 避免重复 Shuffle:对于会导致 Shuffle 的算子(如 groupByKey、join 等),可以尽量合并多次 Shuffle 操作,以减少网络开销。
• 数据倾斜处理:数据倾斜是 Shuffle 操作中的常见问题。可以使用随机前缀、分区键重新分布或广播 Join 来均衡数据负载。
6. 优化代码逻辑
• 使用 DataFrame API:相比 RDD,DataFrame 和 Dataset 提供了更高级的优化能力(Catalyst 优化器),并且可以通过 Tungsten 优化引擎提升物理执行效率。
• 避免过多的窄依赖转换:尽量减少链式的窄依赖转换(如 map、filter 等),减少任务生成的 DAG 层次,避免过度复杂的逻辑导致性能下降。
• 将复用的逻辑函数提取:避免重复代码引起的数据加载和数据转换,提升代码清晰度和执行效率。
7. 合理的内存和资源配置
• Executor 内存分配:可以通过 spark.executor.memory 和 spark.executor.memoryOverhead 调整 Executor 内存大小,以避免内存不足或溢出。
• 动态资源分配:启用 spark.dynamicAllocation.enabled,根据负载自动分配 Executor 数量,以节省资源。
• GC 优化:对于长时间运行的 Spark 应用,调整 GC 参数以减少垃圾回收的开销。可以通过增大 spark.executor.memoryOverhead 来增加用于非 Java 堆内存的空间。
8. 使用合理的压缩和序列化
• 压缩:对于 Shuffle 数据,可以通过设置 spark.shuffle.compress 和 spark.shuffle.spill.compress 开启压缩。数据缓存也可以使用 spark.rdd.compress 开启压缩。
• 序列化方式:选择高效的序列化方式,比如 Kryo 序列化(配置 spark.serializer=org.apache.spark.serializer.KryoSerializer),它比默认的 Java 序列化更加高效。
9. 监控和调试
• 使用 Spark UI 监控:通过 Spark UI 查看任务的执行 DAG、执行时间和资源使用情况,识别瓶颈和资源浪费点。
• 日志分析:通过 Executor 和 Driver 的日志可以定位任务执行的具体信息,帮助排查问题。
• 分布式追踪:通过分布式追踪工具(如 Apache Zipkin 或 Spark 内部监控)分析任务之间的数据流和依赖,找到慢任务和性能瓶颈。
示例总结
假设一个数据处理任务中,需要从大量 CSV 文件中读取数据、对不同列进行过滤和聚合并最终输出到 Parquet 文件。在实际优化过程中,可以:
-
使用 Parquet 作为中间结果的存储格式,减少后续读取开销。
-
使用 filter 早期筛选数据、减少后续数据量。
-
根据数据量和集群节点数,调整 Shuffle 分区数。
-
使用广播 Join 对较小表的关联操作。
-
开启序列化压缩、优化资源配置,确保资源的高效利用。