在移動互聯網時代,處處都存在着實時處理或者流處理,目前比較常用的框架包括spark-streaming + kafka 等;由於spark-streaming讀取kafka維護元數據的方式有
1、通過checkpoint保存
2、Direct DStream API 可以通過設置commit.offset.auto=true 設置自動提交
3、自己手動維護,自己實現方法將消費到的DStream中的偏移量信息同步到zookeeper,Redis,mysql等等
其實方法有很多,但是方法有很多的弊端:
1、checkpoint: 程序升級可序列化問題,不能保證不重複消費
2、Direct DStream方式 當driver端失敗的時候,會存在數據丟失的情況
3、自己手動維護,隨便比較好,但是不能解決絕對的數據不丟失,舉個例子:
假如:
kafkaStream.foreachRDD { rdd =>
val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
// 對外輸出
rdd.map(_.value()).output...
// 提交消費的offset到zookeeper
commitOffset(offsetRanges)
}
假如rdd輸出到外部系統如DB了,但是此時在提交offset的時候出現了問題,此時會導致未提交的offset對應的數據在程序後續的處理中會出現重複消費的問題。
那麼該怎麼解決這個重複消費的問題呢?
方法一、對數據設置唯一Id,每次寫入外部DB的時候,採用upsert的方式處理,存在則更新不存在則插入,此時只需設置direct DStream消費的offset方式 commit.offset.auto=true
kafkaStream.foreachRDD { rdd =>
val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
// 對外輸出
rdd.foreachPartition(partition =>
partition.foreach(document => db.coll.update(document.id, upsert=true))
)
}
這樣子的方式的確不會存在重複消費的情況,但是此方法可以應用的場景比較少;
假如我的數據存在複雜的聚合或者window函數會導致該生成唯一主鍵Id成爲瓶頸。
方法二:
在將數據輸出落地到DB的時候,在每條數據中添加分區的偏移量,即在每條數據中都添加
該topic下的分區的最新偏移量,當程序再次啓動的時候,只需要從該DB中讀取最新的partition的偏移量信息即可。
kafkaStream.foreachRDD { rdd =>
// 該batch對應topic中分區的偏移量
val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
// 獲取每個分區的最大的偏移量信息
val perPartitionMaxOffset = offsetRanges.map(tpo => (tpo.partition, tpo.untilOffset)).
groupBy(_._1).mapValues(_.max)
val transform = rdd.map(line => (line.value(), 1)).reduceByKey(_ + _)...
transform.map(line => combine(line, perPartitionMaxOffset)).output
}
該方法在對外輸出的時候保證了數據和offset操作的原子性,要麼寫入成功要麼寫入失敗下次重新消費。
如果有更加好的方法,歡迎在評論區留言,交流。