| name | spark-optimization |
| description | Apache Spark 作业优化:分区策略、缓存、shuffle 优化和内存调优。用于提升 Spark 性能、调试慢作业或扩展数据处理管道。触发词:Spark、性能优化、分区、缓存、shuffle、数据倾斜。 |
Apache Spark Optimization
生产级 Apache Spark 作业优化模式,包括分区策略、内存管理、shuffle 优化和性能调优。
何时使用此 Skill
- 优化慢的 Spark 作业
- 调优内存和执行器配置
- 实现高效分区策略
- 调试 Spark 性能问题
- 为大数据集扩展 Spark 管道
- 减少 shuffle 和数据倾斜
核心概念
1. Spark 执行模型
Driver Program
↓
Job(由 action 触发)
↓
Stages(由 shuffle 分隔)
↓
Tasks(每个分区一个)
2. 关键性能因素
| 因素 | 影响 | 解决方案 |
|---|
| Shuffle | 网络 I/O、磁盘 I/O | 最小化宽转换 |
| 数据倾斜 | 任务时长不均 | 加盐、广播连接 |
| 序列化 | CPU 开销 | 使用 Kryo、列式格式 |
| 内存 | GC 压力、数据溢出 | 调优执行器内存 |
| 分区 | 并行度 | 合理设置分区大小 |
快速开始
from pyspark.sql import SparkSession
from pyspark.sql import functions as F
spark = (SparkSession.builder
.appName("OptimizedJob")
.config("spark.sql.adaptive.enabled", "true")
.config("spark.sql.adaptive.coalescePartitions.enabled", "true")
.config("spark.sql.adaptive.skewJoin.enabled", "true")
.config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.config("spark.sql.shuffle.partitions", "200")
.getOrCreate())
df = (spark.read
.format("parquet")
.option("mergeSchema", "false")
.load("s3://bucket/data/"))
result = (df
.filter(F.col("date") >= "2024-01-01")
.select("id", "amount", "category")
.groupBy("category")
.agg(F.sum("amount").alias("total")))
result.write.mode("overwrite").parquet("s3://bucket/output/")
模式
模式 1: 最优分区
def calculate_partitions(data_size_gb: float, partition_size_mb: int = 128) -> int:
"""最优分区大小:128MB - 256MB"""
return max(int(data_size_gb * 1024 / partition_size_mb), 1)
df_repartitioned = df.repartition(200, "partition_key")
df_coalesced = df.coalesce(100)
模式 2: 连接优化
small_df = spark.read.parquet("s3://bucket/small_table/")
large_df = spark.read.parquet("s3://bucket/large_table/")
result = large_df.join(
F.broadcast(small_df),
on="key",
how="left"
)
result = large_df1.join(large_df2, on="key", how="inner")
(df.write
.bucketBy(200, "customer_id")
.sortBy("customer_id")
.mode("overwrite")
.saveAsTable("bucketed_orders"))
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true")
spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionFactor", "5")
spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes", "256MB")
模式 3: 缓存和持久化
from pyspark import StorageLevel
df = spark.read.parquet("s3://bucket/data/")
df_filtered = df.filter(F.col("status") == "active")
df_filtered.cache()
df_filtered.count()
模式 4: 内存调优
spark = (SparkSession.builder
.config("spark.executor.memory", "8g")
.config("spark.executor.memoryOverhead", "2g")
.config("spark.memory.fraction", "0.6")
.config("spark.memory.storageFraction", "0.5")
.getOrCreate())
模式 5: Shuffle 优化
spark.conf.set("spark.sql.shuffle.partitions", "auto")
spark.conf.set("spark.shuffle.compress", "true")
df_optimized = (df
.groupBy("key", "partition_col")
.agg(F.sum("value").alias("partial_sum"))
.groupBy("key")
.agg(F.sum("partial_sum").alias("total")))
模式 6: 数据格式优化
(df.write
.option("compression", "snappy")
.option("parquet.block.size", 128 * 1024 * 1024)
.parquet("s3://bucket/output/"))
(df.write
.format("delta")
.option("optimizeWrite", "true")
.option("autoCompact", "true")
.mode("overwrite")
.save("s3://bucket/delta_table/"))
模式 7: 监控和调试
spark.conf.set("spark.sql.codegen.wholeStage", "true")
df.explain(mode="extended")
def check_partition_skew(df):
partition_counts = (df
.withColumn("partition_id", F.spark_partition_id())
.groupBy("partition_id")
.count()
.orderBy(F.desc("count")))
skew_ratio = stats["max"] / stats["avg"]
print(f"Skew ratio: {skew_ratio:.2f}x (>2x indicates skew)")
配置速查表
spark_configs = {
"spark.sql.adaptive.enabled": "true",
"spark.sql.adaptive.coalescePartitions.enabled": "true",
"spark.sql.adaptive.skewJoin.enabled": "true",
"spark.executor.memory": "8g",
"spark.memory.fraction": "0.6",
"spark.memory.storageFraction": "0.5",
"spark.sql.shuffle.partitions": "200",
"spark.serializer": "org.apache.spark.serializer.KryoSerializer",
"spark.sql.autoBroadcastJoinThreshold": "50MB",
}
最佳实践
应该做
- 启用 AQE - 自适应查询执行处理许多问题
- 使用 Parquet/Delta - 带压缩的列式格式
- 广播小表 - 避免小连接的 shuffle
- 监控 Spark UI - 检查倾斜、溢出、GC
- 正确设置分区 - 每个分区 128MB - 256MB
不应该做
- 不收集大数据 - 保持数据分布式
- 不必要地使用 UDF - 使用内置函数
- 不过度缓存 - 内存有限
- 不忽略数据倾斜 - 它主导作业时间
- 不用
.count() 判断存在 - 使用 .take(1) 或 .isEmpty()