这篇文章介绍sparkstreaming对接kafka时遇到的两个offset的问题,首选我们介绍下offset的存储。
sparkstreaming offset存储
sparkstreaming采用kafkaUtils的createDirectStream()处理kafka数据的方式,会直接从kafka的broker的分区中读取数据,跳过zookeeper,并且没有receiver,是spark的task直接对接kafka topic partition。
由于这种方式没有经过ZK,topic的offset没有保存,当job重启后只能从最新的offset开始消费数据,造成重启过程中的消息丢失。
如果spark自动提交,会在sparkstreaming刚运行时就立马提交offset,如果这个时候Spark streaming消费信息失败了,那么offset也就错误提交了。
所以要在sparkstreaming中实现exactly-once恰好一次,必须
1.手动提交偏移量
2.处理完业务数据后再提交offset
手动维护偏移量 需设置kafka参数enable.auto.commit改为false
手动维护提交offset有两种选择:
1.处理完业务数据后手动提交到Kafka
2.处理完业务数据后手动提交到本地库 如MySql、HBase
也可以将offset提交到zookeeper,但是经过我们测试,发现zookeeper不适合存储大量数据,在大数据量的情况下很容易崩溃。
我们来看下如何将offset存储到mysql中:
// 处理完 业务逻辑后,手动提交offset偏移量到本地Mysql中
stream.foreachRDD(rdd => {
val sqlProxy = new SqlProxy()
val client = DataSourceUtil.getConnection
try {
val offsetRanges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
for (or <- offsetRanges) {
sqlProxy.executeUpdate(client, "replace into `offset_manager` (groupid,topic,`partition`,untilOffset) values(?,?,?,?)",
Array(groupid, or.topic, or.partition.toString, or.untilOffset))
}
} catch {
case e: Exception =< e.printStackTrace()
} finally {
sqlProxy.shutdown(client)
}
})
HBase中也是类似的
inputDStream.foreachRDD((rdd, batchTime) => {
val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
offsetRanges.foreach(offset => println(offset.topic, offset.partition, offset.fromOffset, offset.untilOffset))
val newRDD = rdd.map(message => processMessage(message))
newRDD.count()
//save the offsets to HBase 批量处理把数据存储到Hbase当中
saveOffsets(topic, consumerGroupID, offsetRanges, hbaseTableName, batchTime)
})
ssc
}
/**
* 对数据进行处理
* @param message
* @return
*/
def processMessage(message: ConsumerRecord[String, String]): ConsumerRecord[String, String] = {
message
}
/*
Save Offsets into HBase
*/
def saveOffs