spark streaming无缝切换job之实践
**方案主要内容:**通过并行运行两个job,同时保证数据不丢失和中间状态相同,并行运行自然无缝切换;最终保证的是结果计算的最终一致性。
1.该方案需要解决的问题:
- 1.保证kafka中数据不丢失(at last once);
- 2.对增量更新状态的保存(Redis已经做了)。
- 3.生产中首次消费积压的数据的时候,数据量过大导致OOM的问题;
由于数据接收速度和处理速度的不匹配导致kafka中的数据没来得及消费,消息cache在了spark的executor中,停止spark时候导致数据丢失;
图1 数据
注释:(1)spark streaming中来不及消费的数据在下次启动时候丢失,因为原始的offset已经提交,但是实际上并没有完全有效消费,下次启动job时候消费时候自动直接跳过这部分未消费消息。(2)spark接收端一次性接受大量上个任务未消费的数据,未消费数据积压,最终导致Executor OOM或任务奔溃。
2.各种问题的应对策略
2.1保证数据不丢失
2.1.1问题描述
上面的图中已经讲解。
2.1.2 解决方法
如何确保数据无丢失?方案一共三种:
采用Direct的方式来读取kafka(版本0.10.x以上)数据,可以指定offset,但是默认不会将当前已读取的位置存储到zookeeper。所以,如果不手动存储,那么,下次执行还是从默认的位置开始读取(或者指定的位置)
①offset存入zk
当然,也可以将读取到数据的offset,人工的存储在数据库或者其它组件中,每次执行前都读取数据库,设置offset。
还有一种方式,就是将当前已读取的位置更新到zookeeper中,这样,下次读取就会从新的位置开始。
利用direct方式能很轻松的满足每条消息,最多被处理一次、最少被处理一次、只会被处理一次,当然最后的“只会被处理一次”还是有点困难的。
kafka streaming数据无丢失读取,虽然不能保证数据只能被处理一次。但是我们可以从代码逻辑方面处理,只有在将数据处理完成后再向zk中提交kafka偏移信息。就算程序发生异常退出,我们在批次每运行时都清理上次没有完成的状态即可。
②offset存入checkpoint
为什么我这里没有讲到使用checkpoint,因为checkpoint也不能完全保证数据不丢失,反而会降低streming的吞吐量。这种方案也最简单,但是效率和灵活性很差,而且需要外部分布式存储(例如hdfs);
③offset存入默认的kafka
kafka消费者在会保存其消费的进度,也就是offset,存储的位置根据选用的kafka api不同而不同。
kafka0.10.0以上的版本中使用Direct方式topic在zookeeper中,但是offset由原来的存储在zk中,改为存储在kafka的consumer中,主要原因是zk作为分布式协调服务,对频繁的读写修改支持并不好。因此,我们可以手动提交offset到kafka中,来保证消息不丢失;
本次采用方案③的方式,因为这是一种折中的方案,更加轻量级,复杂度适中,易于基于现有的kafka监控整合在一起。
已经在本地测试通过:
spark-streaming消费kafka消息不丢失(at least once),完全消费成功才会将offset提交到kafka中;
本地实验已经可以使用spark将offset手动提交到kafka中保存,保证ML的消息消费不丢失;
主要代码逻辑如下:
stream.foreachRDD(new VoidFunction<JavaRDD<ConsumerRecord<String,String>>>() {
@Override
public void call(JavaRDD<ConsumerRecord<String, String>> rdd) {
//获取偏移
final OffsetRange[] offsetRanges = ((HasOffsetRanges) rdd.rdd()).offsetRanges();
System.out.println("********************---------------------");
// some time later, after outputs have completed
((CanCommitOffsets) stream.inputDStream()).commitAsync(offsetRanges);
}
});
注释:原始的是采用自动提交offset的方式(kafkaParams.put("enable.auto.commit", "true");
),导致数据丢失问题(数据积压的时候,spark receiver注释接受kafka批次的缓存生成kafkaRDD,此时很多批次没来得及处理,offset已经提交;如果此时重启job导致大量kafka中的重要更新数据没有消费),改为手动提交提交offset的方式;
仍然存在的问题:
只是本地测试通过,但是没有将现有的成果整合在up中,已经完成部分代码编写,但是需要黄爽(太忙没时间)协助整合部分;
(需要进一步完成…,这部分整合应该问题不大)
2.1.3 结果对比
结合前后对比,对Direct方式手动提交offset方式已经实现,保证未消费的消息的offset不会提交。
2.2 解决数据积压可能导致的OOM问题
2.2.1 现象描述:
使用kafka导致数据积压严重,再次重新启动job的时候,原始积压的数据会一次性的被spark streaming全部拉取消费,最终导致原始数据很长时间消费不完甚至OOM的问题;(所以要设置控制ML接受kafka数据的速率)
2.2.2解决方法:
//初始化sparkConf
SparkConf sparkConf = new SparkConf()
.setMaster("local[1]")
.setAppName("fct_testapp");
//设置是否启动反压机制,系统会根据数据处理情况,自定调节数据速率
sparkConf.set("spark.streaming.backpressure.enabled", "true")
//首次处理减压策略: 因为首次启动JOB的时候,由于冷启动会造成内存使用太大,为了防止这种情况出现,限制首次处理的数据量:但是只是对Receiver起作用;
//.set("spark.streaming.backpressure.initialRate", "1")
//设置该参数,会控制每个分区中每秒拉取消息体的条数
.set("spark.streaming.kafka.maxRatePerPartition", "1");
处理之前:
图2-1 重启之后一次性拉取过多,可能导致OOM问题
处理之后:
图2-2 限制spark接收端摄入数据速率
2.2.3结果对比:
- 同时开启
spark.streaming.backpressure.enabled == ture
,自动调节数据拉取速率,但是前提是要有相应的历史经验值(该经验值来自于spark算子多次计算之后,推断出的spark的摄取kafka数据点速率,但是对首次大数据量是没有经验值,因此没有作用) - 相比重新启动job时候,一次性拉取过多的积压数据,设置
spark.streaming.kafka.maxRatePerPartition==1
可以很好一次性的大数据量为多批次的小数据量;如图2-2 所示,削减了首次冷启动的时候,摄取kafka的最大拉取速率;
其中的关于spark.streaming.kafka.maxRatePerPartition
具体值的设置,
ka的最大拉取速率;
其中的关于spark.streaming.kafka.maxRatePerPartition
具体值的设置,