Spark Streaming + Kafka

Spark Streaming基於kafka低階api的Direct訪問方式(No Receivers)

我的原文地址https://hywelzhang.github.io/2017/04/01/Spark-Streaming-kafka.html

關於使用Direct Approach (No Receivers)方式來接收Kafka數據的好處我就不多講了。長話短說:
1. 防止數據丟失。基於Receiver的方式,會啓用一個接數線程將接收的數據暫時保存在excutor,另起線程處理數據。如果程序中途失敗,在excutor中未來得及處理的數據將會丟失。所以基於Receiver的方式需要啓用WAL機制來防止數據丟失。這樣就會造成數據一次寫兩份,效率不夠高效。
2. 與Receiver方式相比更加高效(原因如1中所講)
3. kafka分區與接收過來的RDD分區一一對應,更符合邏輯,在不用重新分區時,能夠提升效率。但是也有例外情況,當kafka分區比較少時,directDStream分區也相應比較少,這樣並行度不夠。repartition又會引發shuffle操作。所以需要自己權衡一下分區策略。

初步實現

先申明本篇Blog使用的版本,注意適用範圍(不說版本,上來就講的都是耍流氓 –Spark1.6.1 –kafka-0.8)
Direct方式,會將每批讀取數據的offset保存在流裏邊,所以如果不需要將offset寫會基於zookeeper的監控工具中,實現起來超級簡單
首先,導包是必不可少的,我默認大家使用的是maven構建的項目,在pom.xml中添加依賴

<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming-kafka_2.10</artifactId>
    <version>1.6.1</version>
</dependency>

示例:

//需要導入kafka包
import org.apache.spark.streaming.kafka_
...

object directTest{
  def main(args: Array[String]) {
    val conf = new SparkConf().setAppName("kafka direct test")
    val sc = new SparkContext(conf)
    val ssc = new StreamingContext(sc,Seconds(10))
    /**
    *設置kafka的參數
    *metadata.broker.list 設置你的kafka brokers列表,一般是"IP:端口,IP:端口..."
    *
    *auto.offset.reset 這裏沒設置,默認爲kafka讀數從最新數據開始。
    *還有一個可選設置值smallest,會從kafka上最小的offset值開始讀取
    */
    val kafkaParams = Map("metadata.broker.list" -> yourBrokers)
    val topic = "testTopic"
    directKafkaStream = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams, Set(topic))
    directKafkaStream.foreachRDD{
    }
  }
}

OK,到這裏,你就已經把數給取到了,接下來用directKafkaStream.foreachRDD進行操作了,是不是超級簡單?

但是問題來了,試想一下,生產環境上肯定都會有一個kafka監控工具,用direct的方式,你如果不把offset推回去,監控程序怎麼能知道你數據消費沒有?

進階實現

你拿到了directDstream,官方文檔只是簡單的介紹了一下你可以通過offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges去獲得該批數據的offset,但是沒講怎麼推回去。雖然對大神來講,so easy的問題。但是我這對zk(zookeeper簡寫,偷懶ing)編程又不熟的一小白開始搞的時候也是遇到很多問題。
下面我就用代碼+註釋的方式詳細講講我是怎麼實現的:
當然,需要和zk協作,必須先加上zk的依賴

 <dependency>
    <groupId>com.101tec</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.3</version>
</dependency>

代碼及講解:

object directTest{
  def main(args: Array[String]) {
    val conf = new SparkConf().setAppName("kafka direct test")
    val sc = new SparkContext(conf)
    val ssc = new StreamingContext(sc,Seconds(10))

    //kafka基本參數,yourBrokers你的brokers集羣
    val kafkaParams = Map("metadata.broker.list" -> yourBrokers)
    val topic = "testTopic"
    val customGroup = "testGroup"

    //新建一個zkClient,zk是你的zk集羣,和broker一樣,也是"IP:端口,IP端口..."
    /**
    *如果你使用val zkClient = new ZKClient(zk)新建zk客戶端,
    *在後邊讀取分區信息的文件數據時可能會出現錯誤
    *org.I0Itec.zkclient.exception.ZkMarshallingError: 
    *  java.io.StreamCorruptedException: invalid stream header: 7B226A6D at org.I0Itec.zkclient.serialize.SerializableSerializer.deserialize(SerializableSerializer.java:37) at org.I0Itec.zkclient.ZkClient.derializable(ZkClient.java:740) ..
    *那麼使用我的這個新建方法就可以了,指定讀取數據時的序列化方式
    **/
    val zkClient = new ZkClient(zk, Integer.MAX_VALUE, 10000,ZKStringSerializer)
    //獲取zk下該消費者的offset存儲路徑,一般該路徑是/consumers/test_spark_streaming_group/offsets/topic_name   
    val topicDirs = new ZKGroupTopicDirs(fvpGroup, fvpTopic)
    val children = zkClient.countChildren(s"${topicDirs.consumerOffsetDir}")

    //設置第一批數據讀取的起始位置
    var fromOffsets: Map[TopicAndPartition, Long] = Map()
    var directKafkaStream : InputDStream[(String,String)] = null

    //如果zk下有該消費者的offset信息,則從zk下保存的offset位置開始讀取,否則從最新的數據開始讀取(受auto.offset.reset設置影響,此處默認)
    if (children > 0) {
      //將zk下保存的該主題該消費者的每個分區的offset值添加到fromOffsets中
      for (i <- 0 until children) {
        val partitionOffset = zkClient.readData[String](s"${topicDirs.consumerOffsetDir}/$i")
        val tp = TopicAndPartition(fvpTopic, i)
        //將不同 partition 對應的 offset 增加到 fromOffsets 中
        fromOffsets += (tp -> partitionOffset.toLong)
        println("@@@@@@ topic[" + fvpTopic + "] partition[" + i + "] offset[" + partitionOffset + "] @@@@@@")
        val messageHandler = (mmd: MessageAndMetadata[String, String]) =>  (mmd.topic,mmd.message())
        directKafkaStream = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder, (String,String)](ssc, kafkaParams, fromOffsets, messageHandler)
      }
    }else{
      directKafkaStream = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams, Set(fvpTopic))
    }

    /**
    *上邊已經實現從zk上保存的值開始讀取數據
    *下邊就是數據處理後,再講offset值寫會到zk上
    */
    //用於保存當前offset範圍
    var offsetRanges = Array.empty[OffsetRange]
    val directKafkaStream1 = directKafkaStream.transform { rdd =>
      //取出該批數據的offset值
      offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
      rdd
    }.map(_._2).foreachRDD(rdd=>{
      //數據處理
      ...

      //數據處理完畢後,將offset值更新到zk集羣
      for (o <- offsetRanges) {
        val zkPath = s"${topicDirs.consumerOffsetDir}/${o.partition}"
        ZkUtils.updatePersistentPath(zkClient, zkPath, o.fromOffset.toString)
      } 
    })
  }
}

好的,基本操作已經完成,按照上邊操作,已經能夠實現direct方式讀取kafka,並實現zk來控制offset。
更多的細節優化,下次再更。。。
更多請關注我的博客:https://hywelzhang.github.io/

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