一、序列化在 Spark 中的核心作用
分布式计算中,Spark 需频繁在 Driver 与 Executor 间传输数据(如 Shuffle 过程),且需将数据序列化后存储在内存 / 磁盘中。序列化的效率直接决定了:
-
网络传输速度:序列化后的数据体积越小,网络 IO 开销越低
-
内存利用率:紧凑的序列化格式可减少内存占用,降低 GC 压力
-
任务执行效率:序列化 / 反序列化的速度直接影响 Task 启动和数据处理耗时
Spark2 提供两种核心序列化器:JavaSerializer(默认) 和 KryoSerializer,二者在灵活性、性能和易用性上各有侧重。
二、JavaSerializer 详解:兼容优先的默认选择
1. 原理与特性
JavaSerializer 基于 Java 原生的 ObjectOutputStream 实现,是 Spark2 的默认序列化器。其核心特点:
-
全类型支持 :只要类实现
java.io.Serializable接口(或Externalizable接口自定义序列化逻辑),即可自动序列化 -
零配置成本:无需手动注册类,开箱即用
-
兼容性强:支持所有 Java/Scala 数据类型,包括复杂嵌套对象
-
性能短板:序列化速度慢,生成的字节流体积大(比 Kryo 大 5-10 倍)
2. 适用场景
-
快速原型开发,无需复杂配置
-
少量数据传输的简单作业
-
使用了难以注册的复杂第三方类
-
对性能要求不高的场景
3. 常见问题与解决方案
问题 1:Task not serializable 异常
最典型的序列化错误,通常因闭包中引用了不可序列化的对象(如未实现 Serializable 的类实例)。
示例报错场景(UDF 中引用内部类):
java
class MySparkJob{
def entry(spark:SparkSession):Unit={
def getInnerRsrp(outer_rsrp: Double, wear_loss: Double): Double = {
outer_rsrp - wear_loss // 隐式引用外部类 MySparkJob 实例
}
spark.udf.register("getInnerRsrp", getInnerRsrp _) // 提交任务时抛出序列化异常
}
}
报错信息:
java
org.apache.spark.SparkException: Task not serializable
Caused by: java.io.NotSerializableException: com.dx.fpd_withscenetype.MySparkJob
解决方案:
- 让外部类实现
Serializable接口:
java
class MySparkJob extends Serializable { ... }
-
将不可序列化对象通过广播变量传递(适用于大对象)
-
避免在闭包中引用外部类实例,提取独立方法或对象
问题 2:序列化性能瓶颈
当作业存在大量 Shuffle 或数据传输时,JavaSerializer 的低效率会导致作业运行缓慢。
解决方案:切换到 KryoSerializer 并优化配置。
三、KryoSerializer 详解:性能优先的优化选择
1. 原理与特性
Kryo 是一款高性能的 Java 序列化库(Spark2 内置 Kryo 4 版本),核心优势:
-
速度快:序列化 / 反序列化速度比 JavaSerializer 快 5-10 倍
-
体积小:生成的字节流体积仅为 Java 序列化的 1/10 左右
-
内存效率高:紧凑的存储格式可显著降低内存占用
-
局限性:不支持所有类型,需手动注册自定义类(否则性能退化)
2. 启用与配置步骤
步骤 1:声明使用 KryoSerializer
两种配置方式,任选其一:
方式 1:代码中配置
scala
val conf = new SparkConf()
.setAppName("KryoDemo")
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer") // 核心配置
方式 2:spark-submit 命令行配置
bash
spark-submit \
--class com.example.KryoDemo \
--conf spark.serializer=org.apache.spark.serializer.KryoSerializer \
--conf spark.kryo.registrationRequired=true \ # 强制要求注册类
your-spark-app.jar
步骤 2:注册自定义类
未注册的类会通过反射序列化,性能大幅下降。推荐两种注册方式:
方式 1:直接注册类数组
scala
conf.registerKryoClasses(Array(
classOf[User], // 自定义样例类
classOf[JiebaSegmenter], // 第三方工具类
classOf[scala.collection.mutable.ArrayBuffer]
))
方式 2:自定义 KryoRegistrator 类
- 实现
KryoRegistrator接口:
scala
class MyKryoRegistrator extends KryoRegistrator {
override def registerClasses(kryo: Kryo): Unit = {
kryo.register(classOf[User])
kryo.register(classOf[JiebaSegmenter])
}
}
- 配置注册器:
scala
conf.set("spark.kryo.registrator", "com.example.MyKryoRegistrator")
步骤 3:关键优化配置
| 配置项 | 作用 | 推荐值 |
|---|---|---|
| spark.kryo.registrationRequired | 强制注册类(未注册则报错) | true(生产环境) |
| spark.kryoserializer.buffer.max | Kryo 缓冲区最大大小(防止缓冲区溢出) | 256m |
| spark.rpc.message.maxSize | RPC 消息最大大小(适配大对象传输) | 800m |
3. 实战案例:Kryo + 结巴分词 UDF 优化
场景:使用结巴分词 UDF 处理文本数据,需序列化 JiebaSegmenter 实例。
优化前问题:JiebaSegmenter 不可序列化,直接使用会抛出异常。
优化方案:Kryo 注册 + 广播变量。
完整代码:
scala
import com.huaban.analysis.jieba.{JiebaSegmenter, SegToken}
import org.apache.spark.SparkConf
import org.apache.spark.sql.{SparkSession, DataFrame}
import org.apache.spark.sql.functions._
object JiebaSegWithKryo {
def main(args: Array[String]): Unit = {
// 1. 配置 Kryo 序列化
val conf = new SparkConf()
.setAppName("JiebaSeg")
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.set("spark.kryo.registrationRequired", "true")
.set("spark.kryoserializer.buffer.max", "256m")
.registerKryoClasses(Array(classOf[JiebaSegmenter])) // 注册结巴分词类
// 2. 初始化 SparkSession
val spark = SparkSession.builder()
.config(conf)
.enableHiveSupport()
.getOrCreate()
// 3. 定义分词 UDF(使用广播变量传递序列化后的 JiebaSegmenter)
def jiebaSeg(df: DataFrame, colName: String): DataFrame = {
val segmenter = new JiebaSegmenter()
val segBroadCast = spark.sparkContext.broadcast(segmenter) // 广播序列化对象
val jiebaUdf = udf((sentence: String) => {
val seg = segBroadCast.value
seg.process(sentence, JiebaSegmenter.SegMode.INDEX)
.toArray()
.map(_.asInstanceOf[SegToken].word)
.filter(_.length > 1)
.mkString(" ")
})
df.withColumn("seg_result", jiebaUdf(col(colName)))
}
// 4. 执行分词任务
val df = spark.sql("select content from news_data limit 10000")
val result = jiebaSeg(df, "content")
result.write.mode("overwrite").saveAsTable("news_seg_result")
spark.stop()
}
}
4. 常见问题与排查
问题 1:Kryo 序列化异常(如 EOFException、NullPointerException)
可能原因:
-
存在冲突的 Kryo Jar 包(如同时存在 kryo-2.21.jar 和 kryo-shaded-3.0.3.jar)
-
未注册必要的类,导致反射序列化失败
排查步骤:
-
临时切换回 JavaSerializer,验证是否为 Kryo 配置问题
-
检查依赖包,删除冲突的 Kryo 版本
-
启用
spark.kryo.registrationRequired=true,确保所有类已注册
问题 2:大对象序列化溢出
解决方案:
-
增大
spark.kryoserializer.buffer.max(如设置为 256m 或 512m) -
拆分大对象,避免单次传输过大数据
四、两种序列化器核心对比与选型建议
| 特性 | JavaSerializer | KryoSerializer |
|---|---|---|
| 序列化速度 | 慢(1x) | 快(5-10x) |
| 序列化体积 | 大(10x) | 小(1x) |
| 类型支持 | 全支持 | 部分支持(需注册) |
| 配置成本 | 零配置 | 需注册类,配置参数 |
| 兼容性 | 强(跨版本) | 一般(需确保类结构兼容) |
| 内存效率 | 低 | 高 |
选型决策树
-
快速开发 / 原型验证 → 优先 JavaSerializer(默认配置)
-
生产环境 / 大数据量作业 → 优先 KryoSerializer(性能优化)
-
存在大量 Shuffle / 数据传输 → 强制使用 KryoSerializer
-
使用复杂第三方类且无法注册 → 选择 JavaSerializer
-
内存紧张 / GC 频繁 → 切换到 KryoSerializer 并优化配置
五、总结
-
生产环境优先启用 Kryo:即使是简单类型,Kryo 也能显著提升 Shuffle 性能(Spark2 已默认对简单类型 RDD Shuffle 使用 Kryo)
-
强制类注册 :启用
spark.kryo.registrationRequired=true,避免遗漏注册导致性能退化 -
合理配置缓冲区 :根据数据大小调整
spark.kryoserializer.buffer.max,避免溢出 -
广播大对象:对于不可序列化或体积大的对象,优先使用广播变量传递
-
避免序列化陷阱:
-
闭包中不引用不可序列化对象
-
自定义类尽量使用样例类(自动实现 Serializable)
-
第三方工具类需提前测试序列化兼容性
- 性能监控:通过 Spark UI 查看 Shuffle 数据量和 Task 执行时间,验证序列化优化效果