Spark 组件在 Java 大数据开发中的常见报错及解决方案
以下是几个 及解决方案,结合具体场景说明:
案例1:Guava 依赖冲突导致 IllegalAccessError
- 报错信息:
java.lang.IllegalAccessError: tried to access method com.google.common.base.Stopwatch.<init>()V from class org.apache.hadoop.hbase.zookeeper.MetaTableLocator
- 原因:
Spark 或 Hadoop 依赖的 Guava 版本与 HBase 不兼容(如 HBase 需要较低版本的 Guava)。 - 解决方案:
- 检查并统一依赖版本(如通过 Maven 的
<exclusion>
移除冲突的 Guava 包)。 - 在 Spark 提交命令中强制指定 Guava 版本:
spark-submit --conf "spark.driver.extraClassPath=/path/to/guava-xx.x.jar" ...
- 检查并统一依赖版本(如通过 Maven 的
案例2:Spark 任务序列化失败(NotSerializableException
)
- 报错信息:
org.apache.spark.SparkException: Task not serializable
Caused by: java.io.NotSerializableException: com.example.NonSerializableClass
- 原因:
在 Spark 算子(如map
、filter
)中引用了未实现Serializable
接口的类。 - 解决方案:
- 确保所有在算子中使用的自定义类实现
Serializable
接口。 - 使用
transient
关键字标记不需要序列化的字段。 - 将外部变量转换为局部变量传递到算子内部。
- 确保所有在算子中使用的自定义类实现
案例3:数据转换时数组越界(IndexOutOfBoundsException
)
- 报错信息:
java.lang.IndexOutOfBoundsException
- 原因:
在split
操作后未处理空字段(如文本行分隔符后的空列导致数组越界)。 - 解决方案:
- 指定
split
方法的limit
参数为负数以保留空字段:String[] parts = line.split("\\|", -1); // 保留空字段
- 指定
案例4:内存溢出(OutOfMemoryError
)
- 报错信息:
java.lang.OutOfMemoryError: Java heap space
或Connection reset by peer
(因 Executor 内存不足被 YARN 终止)。 - 原因:
- Executor 内存分配不足(默认配置过低)。
- 数据倾斜导致单个 Task 处理数据量过大。
- 解决方案:
- 调整内存参数:
spark-submit --executor-memory 8g --driver-memory 4g ...
- 优化数据分区,避免倾斜(如使用
repartition
或自定义分区器)。
(1)、增大分区数:通过扩大分区数量分散数据,但需避免过度分区导致调度开销。
(2)、针对倾斜Key预分区:对已知倾斜Key单独处理,其他Key保持默认分区。
val df = spark.read.parquet("path")
val repartitioned = df.repartition(200) // 根据集群资源调整分区数(建议CPU核数2~3倍)
import org.apache.spark.sql.functions._
(3)、自定义分区器
val skewedKeys = Seq("hot_user_1", "hot_user_2")
val dfSkewed = df.filter(col("user_id").isin(skewedKeys: _*)).repartition(100, col("user_id"))
val dfNormal = df.filter(!col("user_id").isin(skewedKeys: _*)).repartition(20)
val finalDF = dfSkewed.union(dfNormal)
1). 实现自定义分区器
继承Partitioner类:覆盖numPartitions和getPartition方法。
class SkewAwarePartitioner(override val numPartitions: Int, skewedKeys: Set[String]) extends Partitioner {
private val skewFactor = 10 // 倾斜Key分配10倍分区override def getPartition(key: Any): Int = {
val keyStr = key.toString
if (skewedKeys.contains(keyStr)) {
// 倾斜Key哈希到额外分区范围
(keyStr.hashCode % (numPartitions / skewFactor)).abs + (numPartitions - numPartitions / skewFactor)
} else {
// 普通Key哈希到主分区范围
(keyStr.hashCode % (numPartitions - numPartitions / skewFactor)).abs
}
}
}
2). 应用自定义分区器
RDD操作:在Shuffle操作前显式指定分区器。
val rdd = df.rdd.map(row => (row.getString(0), row))
val partitionedRDD = rdd.partitionBy(new SkewAwarePartitioner(200, Set("hot_user_1")))
3). Spark SQL集成
注册为UDF:通过spark.sql.shuffle.partitions控制全局分区数,或对特定Stage重分区。
spark.conf.set("spark.sql.shuffle.partitions", 200)
val resultDF = df.groupBy("user_id").agg(sum("value")).sort("user_id")
- 调整内存参数:
案例5:任务因 Shuffle 阶段失败(SparkException: Job aborted
)
- 报错信息:
org.apache.spark.SparkException: Job aborted due to stage failure: ShuffleMapStage X has failed the max allowed failures
- 原因:
- Shuffle 数据量过大,网络传输超时。
- Executor 内存不足导致 Shuffle 中间文件写入失败。
- 解决方案:
- 增加 Shuffle 操作的超时时间:
spark.conf.set("spark.network.timeout", "600s");
- 提升 Executor 内存或调整 Shuffle 缓冲区大小。
- 增加 Shuffle 操作的超时时间:
案例6:Kafka Offset 提交失败(CommitFailedException
)
- 报错信息:
CommitFailedException: Commit cannot be completed since the group has already rebalanced
- 原因:
- 消息处理耗时超过
max.poll.interval.ms
,导致消费者被踢出组。 - Kafka 消费者组配置不合理(如
session.timeout.ms
过短)。
- 消息处理耗时超过
- 解决方案:
- 优化消息处理逻辑,减少单次拉取数据量:
props.put("max.poll.records", "100"); // 减少每次 poll 的消息数
- 使用异步处理 + 手动提交 Offset,确保处理完成后再提交。
- 优化消息处理逻辑,减少单次拉取数据量:
总结
Spark 开发中的典型报错多与 依赖冲突、序列化问题、内存管理、Shuffle 过程 及 外部系统交互 相关。建议优先通过日志定位具体阶段(如 Driver/Executor 日志),结合参数调优和代码逻辑优化解决。
本文来自博客园,作者:业余砖家,转载请注明原文链接:https://www.cnblogs.com/yeyuzhuanjia/p/18789556