spark 騷操作實現高效處理kafka數據積壓

一、  開篇

spark streaming消費kafka,大家都知道有兩種方式,也是面試考基本功常問的:

1.基於receiver的機制。

這個是spark streaming最基本的方式,spark streaming的receiver會定時生成block,默認是200ms,然後每個批次生成blockrdd,分區數就是block數。架構如下:

 

2.direct API。

這種api就是spark streaming會每個批次生成一個kafkardd,然後kafkardd的分區數,由spark streaming消費的kafkatopic分區數決定。過程如下:

kafkardd與消費的kafka分區數的關係如下:

 

二.常見積壓問題

kafka的producer生產數據到kafka,正常情況下,企業中應該是輪詢或者隨機,以保證kafka分區之間數據是均衡的。

在這個前提之下,一般情況下,假如針對你的數據量,kafka分區數設計合理。實時任務,如spark streaming或者flink,有沒有長時間的停掉,那麼一般不會有有積壓。

消息積壓的場景:

a.任務掛掉。比如,週五任務掛了,有沒有寫自動拉起腳本,週一早上才處理。那麼spark streaming消費的數據相當於滯後兩天。這個確實新手會遇到。

b.kafka分區數設少了。其實,kafka單分區生產消息的速度qps還是很高的,但是消費者由於業務邏輯複雜度的不同,會有不同的時間消耗,就會出現消費滯後的情況。

c.kafka消息的key不均勻,導致分區間數據不均衡。kafka生產消息支持指定key,用key攜帶寫信息,但是key要均勻,否則會出現kafka的分區間數據不均衡。

上面三種積壓情況,企業中很常見,那麼如何處理數據積壓呢?

 

一般解決辦法,針對性的有以下幾種:

a.任務掛掉導致的消費滯後。

任務啓動從最新的消費,歷史數據採用離線修補。

最重要的是故障拉起腳本要有,還要就是實時框架異常處理能力要強,避免數據不規範導致的不能拉起。

 

b.任務掛掉導致的消費滯後。

任務啓動從上次提交處消費處理,但是要增加任務的處理能力,比如增加資源,讓任務能儘可能的趕上消費最新數據。

 

c.kafka分區少了。

假設數據量大,直接增加kafka分區是根本,但是也可以對kafkardd進行repartition,增加一次shuffle。

 

d.個別分區不均衡。

可以生產者處可以給key加隨機後綴,使其均衡。也可以對kafkardd進行repartition。

 

三、騷操作

其實,以上都不是大家想要的,因爲spark streaming生產的kafkardd的分區數,完全可以是大於kakfa分區數的。

其實,經常閱讀源碼的朋友應該瞭解,rdd的分區數,是由rdd的getPartitions函數決定。比如kafkardd的getPartitions方法實現如下:

  override def getPartitions: Array[Partition] = {    offsetRanges.zipWithIndex.map { case (o, i) =>        new KafkaRDDPartition(i, o.topic, o.partition, o.fromOffset, o.untilOffset)    }.toArray  }

offsetRanges其實就是一個數組:

    val offsetRanges: Array[OffsetRange],

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生成的過程進行改造,只需要增加7行源碼即可。


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))
  }
}

 

測試的主函數如下:


import bigdata.spark.config.Config
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.{SparkConf, TaskContext}
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, HasOffsetRanges, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}

/*
1. 直接消費新數據,數據離線修補。
2. repartition(10---->100),給足夠多的資源,以便任務逐漸消除滯後的數據。
3. directDstream api 生成的是kafkardd,該rdd與kafka分區一一對應。
 */
object kafka010Repartition {

   def main(args: Array[String]) {
      //    創建一個批處理時間是2s的context 要增加環境變量
      val sparkConf = new SparkConf().setAppName(this.getClass.getName).setMaster("local[*]")
     sparkConf.set("enable.auto.repartition","true")
     sparkConf.set("per.partition.offsetrange.threshold","300")
     sparkConf.set("per.partition.offsetrange.step","100")

     val ssc = new StreamingContext(sparkConf, Seconds(5))
      //    使用broker和topic創建DirectStream
      val topicsSet = "test1".split(",").toSet
      val kafkaParams = Map[String, Object]("bootstrap.servers" -> Config.kafkaHost,
        "key.deserializer"->classOf[StringDeserializer],
        "value.deserializer"-> classOf[StringDeserializer],
        "group.id"->"test1",
        "auto.offset.reset" -> "earliest",
        "enable.auto.commit"->(false: java.lang.Boolean))

      val messages = KafkaUtils.createDirectStream[String, String](
        ssc,
        LocationStrategies.PreferConsistent,
        ConsumerStrategies.Subscribe[String, String](topicsSet, kafkaParams))

      messages.transform(rdd=>{
        println("partition.size : "+rdd.getNumPartitions)
        rdd
      }).foreachRDD(rdd=>{
//        rdd.foreachPartition(each=>println(111))
        val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
        offsetRanges.foreach(o=>{
          println(s"${o.topic} ${o.partition} ${o.fromOffset} ${o.untilOffset}")
        })
      })

     ssc.start()
     ssc.awaitTermination()
    }

}

結果如下:

partition.size : 67
test1 0 447 547
test1 0 547 647
test1 0 647 747
test1 0 747 847
test1 0 847 947
test1 0 947 1047
test1 0 1047 1147
test1 0 1147 1247
test1 0 1247 1347
test1 0 1347 1447
test1 0 1447 1547
test1 0 1547 1647
test1 0 1647 1747
test1 0 1747 1847
test1 0 1847 1947
test1 0 1947 2047
test1 0 2047 2147
test1 0 2147 2247
test1 0 2247 2347
test1 0 2347 2447
test1 0 2447 2547
test1 0 2547 2647
test1 0 2647 2747
test1 0 2747 2847
test1 0 2847 2947
test1 0 2947 3047
test1 0 3047 3147
test1 0 3147 3247
test1 0 3247 3347

 

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