Spark修改幾行源碼,解決Kafka數據積壓

導致Kafka數據積壓的幾種情況

  • 第一種情況,SparkStreaming 通過receivers(或者Direct方式)以生產者生產數據的速率接收數據。當Batch procecing time > batch interval 的時候,也就是每個批次數據處理的時間要比SparkStreaming批處理間隔時間長;越來越多的數據被接收,但是數據的處理速度沒有跟上,導致系統開會出現數據堆積,可能進一步導致Excutor端OOM問題而出現失敗的情況。

  • 第二種情況,就是你的流計算程序沒有自動拉起腳本,或者是自動拉起腳本設計不合理,導致服務較長時間處於停止狀態。

  • 第三種情況,是由於kafka內數據自定義Key導致Kafka分區內數據分佈不均勻。

Spark Streaming消費Kafka的方式

  1. 基於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範圍的數據。

在這裏插入圖片描述

常規解決上述三種場景的方式

針對第一種由於數據量較大分區較小的情況產生數據積壓

  1. 最常見的就是開啓Spark Streaming的反壓制機制。
  2. 再有就是通過shuffle,在Spark消費階段進行repartition
  3. 最根本的方法就是增加Kafka分區數

針對第二種程序宕機導致的消費滯後

  1. 最常見的方式任務啓動從最新的消費,歷史數據採用離線修補。
  2. 還可以再次啓動時加大資源,提升消費速度

針對第三種Kafka數據分佈不均勻的情況

  1. 最常見的就是給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))
  }
}

修改後:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章