简述
Spark Streaming的应用一般是从上游接到数据,进行处理,然后将结果输出。这是一个流式的过程,持续不断的接收、持续不断的输出,流式过程中的每一个环节(输入、处理、输出)出错,都可能会造成数据的丢失,这在生产中是不能容忍的。
这相当于是一个可靠性的问题,当出错时可以通过上游或者spark streaming自身来保证。
上游一般是kafka、rocketmq等消息队列,队列都有偏移量、消费位点类似的概念,我们可以将其重置来保证数据的可靠性。
spark streaming自身提供了checkpoint、wal机制来保证数据的可靠。
checkpoint
checkpoint通过设置检查点,将数据保存到文件系统,在出现出故障的时候进行数据恢复。
spark streaming中有两种检查点:
-
元数据检查点,主要是Driver的恢复
保存应用程序的配置、Dstream操作、没有完成的批处理等。这里需要注意的是没有完成的批处理,代表的是生成批处理的元数据(Driver端),并没有具体的存储RDD内容(Executor端),RDD内容需要WAL来持久化。
-
数据检查点
主要是将已生成的RDD保存到可靠的存储系统中,在带state操作中是必须的。
checkpoint需要指定一个文件目录,来保存元数据和数据信息;每生成一个batch RDD,就会触发checkpoint操作,进行写。
假如Driver出故障挂掉了,我们再次重启的时候,就会去checkpoint设置的目录去读取一些配置,定位到挂之前的状态,继续进行工作。
WAL
write ahead log预写日志,在数据库中经常被用到,用来在宕机之后根据WAL恢复数据
实现是Executor的receiver在接收到数据时,如果设置了WAL,会将数据写入WAL
checkpoint + WAL
checkpoint 保存着执行进度(比如已生成但未完成的 jobs)
WAL 中保存着 blocks 及 blocks 元数据(比如保存着未完成的 jobs 对应的 blocks 信息及 block 文件)
两者结合就可以保证数据的可靠性。
code
def main(args: Array[String]): Unit = {
// checkpoint目录
val checkpointDir = s"hdfs://*****/user/test"
val sparkConf = new SparkConf()
// 创建SparkSession
val sparkSession = SparkSession
.builder()
.appName("checkpoint-wal")
.config(sparkConf)
// 设置开启wal
.config("spark.streaming.receiver.writeAheadLog.enable", "true")
.getOrCreate()
val sc = sparkSession.sparkContext
// StreamingContext.getOrCreate(checkpointDir, creatingFunc)
// 这个方法表示如果checkpoint目录存在,则从目录读取ssc对象;不存在才调用creatingFunc创建ssc对象
val ssc = StreamingContext.getOrCreate(
checkpointDir,
() => createStreamingContext(sc, checkpointDir)
)
ssc.start()
ssc.awaitTermination()
}
// 创建ssc的func
def createStreamingContext(sc: SparkContext,
checkpointDir: String): StreamingContext = {
val ssc = new StreamingContext(sc, Seconds(10))
// 指定checkpoint目录
ssc.checkpoint(checkpointDir)
// 做我们自己的逻辑处理
val stream = ...
// do something ...
// 返回ssc对象
ssc
}
以上代码,就是使用checkpoint+wal的方式
存在的坑
-
自己的业务逻辑,应该在creatingFunc中,否则首次启动后,将其杀掉,再次启动会报如下异常:
org.apache.spark.streaming.dstream.MappedDStream@5a69b104 has not been initialized
-
streaming程序打包扔到集群正常运行以后,如果我们对代码进行修改,再重新打包;将旧的任务杀掉,用新的代码去跑;可能出现更改不生效,或者报ClassNotFoundException的错。问题就出在checkpoint上,因为checkpoint的元数据会记录jar的序列化的二进制文件,因为你改动过代码,然后重新编译,新的序列化jar文件,在checkpoint的记录中并不存在,所以就导致了上述错误。
解决方案:删除checkpoint目录下checkpoint开头的的文件即可,再次重启就可以正常工作(这个方法我没有试过)。 我的做法是把目录下所有的文件删掉,然后重启,通过上游来保证可靠。
总结
-
使用checkpoint+wal,主要用来解决【数据已接收但未处理】的时候,出现宕机的情况。
-
如果上游可以保证可靠,且不要求至少一次,完全不需要这么做。
-
开启WAL,对性能的影响较大,生产中使用还需要权衡。
-
上游是kafka的话,可以自己管理偏移量,而不用采用这种方法
参考
https://cloud.tencent.com/developer/article/1122403
https://zhuanlan.zhihu.com/p/26297594
https://www.waitingforcode.com/apache-spark-streaming/sparkexception-org.apache.spark.streaming.dstream.MappedDStream-has-not-been-initialized/read
https://www.jianshu.com/p/5e096df2618d
个人理解,有偏差的地方欢迎指出。