Spark 增加幾行源代碼,解決棘手Kafka消息堆積問題
導致Kafka數據積壓的幾種情況
-
第一種情況,SparkStreaming 通過receivers(或者Direct方式)以生產者生產數據的速率接收數據。當
Batch procecing time > batch interval
的時候,也就是每個批次數據處理的時間要比SparkStreaming批處理間隔時間長;越來越多的數據被接收,但是數據的處理速度沒有跟上,導致系統開會出現數據堆積,可能進一步導致Excutor端OOM問題而出現失敗的情況。 -
第二種情況,就是你的流計算程序沒有自動拉起腳本,或者是自動拉起腳本設計不合理,導致服務較長時間處於停止狀態。
-
第三種情況,是由於kafka內數據自定義Key導致Kafka分區內數據分佈不均勻。
Spark Streaming消費Kafka的方式
- 基於Receiver的方式
這種方式使用Receiver來獲取數據。Receiver是使用Kafka的高級Consumer API來實現的。receiver從Kafka中獲取的數據都是存儲在Spark Executor的內存中的(如果突然數據暴增,大量batch堆積,很容易出現內存溢出的問題),然後Spark Streaming啓動的job會去處理那些數據。
然而,在默認的配置下,這種方式可能會因爲底層的失敗而丟失數據。如果要啓用高可靠機制,讓數據零丟失,就必須啓用Spark Streaming的預寫日誌機制(Write Ahead Log,WAL)。該機制會同步地將接收到的Kafka數據寫入分佈式文件系統(比如HDFS)上的預寫日誌中。所以,即使底層節點出現了失敗,也可以使用預寫日誌中的數據進行恢復。
2. 基於Direct的方式
這種新的直接方式,是在Spark 1.3中引入的,從而能夠確保更加健壯的機制。替代掉使用Receiver來接收數據後,這種方式會週期性地查詢Kafka,來獲得每個
topic+partition的最新的offset
,從而定義每個batch的offset的範圍。當處理數據的job啓動時,就會使用Kafka的簡單Consumer api來獲取Kafka指定offset範圍的數據。
常規解決上述三種場景的方式
針對第一種由於數據量較大分區較小的情況產生數據積壓
- 最常見的就是開啓Spark Streaming的反壓制機制。
- 再有就是通過shuffle,在Spark消費階段進行repartition
- 最根本的方法就是增加Kafka分區數
針對第二種程序宕機導致的消費滯後
- 最常見的方式任務啓動從最新的消費,歷史數據採用離線修補。
- 還可以再次啓動時加大資源,提升消費速度
針對第三種Kafka數據分佈不均勻的情況
- 最常見的就是給Key增加隨機後綴,儘可能讓數據散列均勻避免數據傾斜
通過修改源碼來解決數據積壓
如果按常規的模式來解決這些問題,就太平凡了,根本不夠騷。
Spark如何確定分區數
Spark Streaming生產KafkaRDD-Rdd的分區數,完全可以是
大於kakfa分區數
的!
其實,經常閱讀源碼應該瞭解,RDD的分區數,是由RDD的getPartitions函數決定。比如KafkaRDD的getPartitions方法實現如下:
val offsetRanges: Array[OffsetRange]
override def getPartitions: Array[Partition] = {
offsetRanges.zipWithIndex.map { case (o, i) =>
new KafkaRDDPartition(i, o.topic, o.partition, o.fromOffset, o.untilOffset)
}.toArray
}
具體位置:
KafkaRDD --> getPartitions
OffsetRange存儲一個kafka分區元數據及其offset範圍,然後進行map操作,轉化爲KafkaRDDPartition。實際上,我們可以在這裏下手,將map改爲flatmap,然後對offsetrange的範圍進行拆分,但是這個會引發一個問題
修改分區規則
其實,我們可以在offsetRange生成的時候做下轉換。位置是DirectKafkaInputDstream的compute方法。具體實現:
// 是否開啓自動重分區分區
sparkConf.set("enable.auto.repartition","true")
// 避免不必要的重分區操作,增加個閾值,只有該批次要消費的kafka的分區內數據大於該閾值才進行拆分
sparkConf.set("per.partition.offsetrange.threshold","300")
// 拆分後,每個kafkardd 的分區數據量。
sparkConf.set("per.partition.after.partition.size","100")
然後,在DirectKafkaInputDstream裏獲取着三個配置
val repartitionStep = _ssc.conf.getInt("per.partition.offsetrange.size",1000)
val repartitionThreshold = _ssc.conf.getLong("per.partition.offsetrange.threshold",1000)
val enableRepartition = _ssc.conf.getBoolean("enable.auto.repartition",false)
對offsetRanges生成的過程進行改造,只需要增加幾行源碼即可
val offsetRanges = untilOffsets.flatMap{ case (tp, uo) =>
val fo = currentOffsets(tp)
val delta = uo -fo
if(enableRepartition&&(repartitionThreshold < delta)){
val offsets = fo to uo by repartitionStep
offsets.map(each =>{
val tmpOffset = each + repartitionStep
OffsetRange(tp.topic, tp.partition, each, Math.min(tmpOffset,uo))
}).toList
}else{
Array(OffsetRange(tp.topic, tp.partition, fo, uo))
}
}
修改後: