spark-streaming-kafka之createDirectStream模式 轉

最近一直在用directstream方式消費kafka中的數據,特此總結,整個代碼工程分爲三個部分
一. 完整工程代碼如下(某些地方特意做了說明, 這個代碼的部分函數直接用的是spark-streaming-kafka-0.8_2.11)

package directStream

import kafka.message.MessageAndMetadata;
import kafka.serializer.StringDecoder
import kafka.common.TopicAndPartition

import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.kafka.common.TopicPartition
//import java.util._
import org.apache.spark.{SparkContext,SparkConf,TaskContext, SparkException}
import org.apache.spark.SparkContext._
import org.apache.spark.streaming.StreamingContext
import org.apache.spark.streaming.Seconds
import org.apache.spark.streaming.dstream._
import org.apache.spark.streaming.kafka.{KafkaUtils,HasOffsetRanges, OffsetRange,KafkaCluster}


import com.typesafe.config.ConfigFactory
import scalikejdbc._
import scala.collection.JavaConverters._

object SetupJdbc {
  def apply(driver: String, host: String, user: String, password: String): Unit = {
    Class.forName(driver)
    ConnectionPool.singleton(host, user, password)
  }
}
object SimpleApp{
  def main(args: Array[String]): Unit = {
  
    val conf = ConfigFactory.load // 加載工程resources目錄下application.conf文件,該文件中配置了databases信息,以及topic及group消息
    val kafkaParams = Map[String, String](
      "metadata.broker.list" -> conf.getString("kafka.brokers"),
      "group.id" -> conf.getString("kafka.group"),
      "auto.offset.reset" -> "smallest"
    )    
    val jdbcDriver = conf.getString("jdbc.driver")
    val jdbcUrl = conf.getString("jdbc.url")
    val jdbcUser = conf.getString("jdbc.user")
    val jdbcPassword = conf.getString("jdbc.password")

    val topic = conf.getString("kafka.topics")
    val group = conf.getString("kafka.group")

    val ssc = setupSsc(kafkaParams, jdbcDriver, jdbcUrl, jdbcUser, jdbcPassword,topic, group)()
    ssc.start()
    ssc.awaitTermination()
  }

  def createStream(taskOffsetInfo: Map[TopicAndPartition, Long], kafkaParams: Map[String, String], conf:SparkConf, ssc: StreamingContext, topics:String):InputDStream[_] = {
    // 若taskOffsetInfo 不爲空, 說明這不是第一次啓動該任務, database已經保存了該topic下該group的已消費的offset, 則對比kafka中該topic有效的offset的最小值和數據庫保存的offset,去比較大作爲新的offset.  
    if(taskOffsetInfo.size != 0){
        val kc = new KafkaCluster(kafkaParams)
        val earliestLeaderOffsets = kc.getEarliestLeaderOffsets(taskOffsetInfo.keySet) 
        if(earliestLeaderOffsets.isLeft)
          throw new SparkException("get kafka partition failed:") 
        val earliestOffSets = earliestLeaderOffsets.right.get

        val offsets = earliestOffSets.map(r => 
          new TopicAndPartition(r._1.topic, r._1.partition) -> r._2.offset.toLong)

        val newOffsets = taskOffsetInfo.map(r => {
            val t = offsets(r._1) 
            if (t > r._2) { 
              r._1 -> t
            } else {
              r._1 -> r._2
            }
          }
        ) 
        val messageHandler = (mmd: MessageAndMetadata[String, String]) => 1L 
        KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder, Long](ssc, kafkaParams, newOffsets, messageHandler)    //val stream = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](
     } else {
        val topicSet = topics.split(",").toSet 
        KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams,topicSet)
   }
  }
    
  def setupSsc(
    kafkaParams: Map[String, String],
    jdbcDriver: String,
    jdbcUrl: String,
    jdbcUser: String,
    jdbcPassword: String,
    topics:String, 
    group:String
  )(): StreamingContext = {

    val conf = new SparkConf()
      .setMaster("mesos://10.142.113.239:5050")
      .setAppName("offset")
      .set("spark.worker.timeout", "500")
      .set("spark.cores.max", "10")
      .set("spark.streaming.kafka.maxRatePerPartition", "500")
      .set("spark.rpc.askTimeout", "600s")
      .set("spark.network.timeout", "600s")
      .set("spark.streaming.backpressure.enabled", "true")
      .set("spark.task.maxFailures", "1")
      .set("spark.speculationfalse", "false")



    val ssc = new StreamingContext(conf, Seconds(5))
    SetupJdbc(jdbcDriver, jdbcUrl, jdbcUser, jdbcPassword)  // connect to mysql 

    // begin from the the offsets committed to the database
    val fromOffsets = DB.readOnly { implicit session =>
      sql"select topic, part, offset from streaming_task where group_id=$group".
      map { resultSet =>
        new TopicAndPartition(resultSet.string(1), resultSet.int(2)) -> resultSet.long(3)
      }.list.apply().toMap
    }

    val stream = createStream(fromOffsets, kafkaParams, conf, ssc, topics)
    
    stream.foreachRDD { rdd =>
       if(rdd.count != 0){          
          // you task 
          val t = rdd.map(record => (record, 1))
          val results = t.reduceByKey {_+_}.collect
          

          // persist the offset into the database  
          val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
          DB.localTx { implicit session =>
               offsetRanges.foreach { osr => 
                 sql"""replace into streaming_task values(${osr.topic}, ${group}, ${osr.partition}, ${osr.untilOffset})""".update.apply()
                 if(osr.partition == 0){
                   println(osr.partition, osr.untilOffset)
                 }
               }
          }
      }
    }
    ssc
  }
}

二. 工程的resources文件下的有個application.conf配置文件,其配置如下

jdbc {
 driver = "com.mysql.jdbc.Driver"
 url = "jdbc:mysql://xxx.xxx.xxx.xxx:xxxx/xxxx"
 user = "xxxx"
 password = "xxxx"
}
kafka {
 topics = "xxxx"
 brokers = "xxxx.xxx.xxx.:xxx,xxx.xxx.xxx.xxx:9092,xxx.xxxx.xxx:xxxx"
 group = "xxxxxx"
}
jheckpointDir = "hdfs://xxx.xxx.xxx.xxx:9000/shouzhucheckpoint"
batchDurationMs = xxxx

三. 配置文件中可以看到, 我把offset 保存在 mysql裏,這裏我定義了一個table 名稱爲streaming_task, 表的結構信息如下:

+----------+--------------+------+-----+---------+-------+
| Field    | Type         | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+-------+
| topic    | varchar(100) | NO   | PRI | NULL    |       |
| group_id | varchar(50)  | NO   | PRI |         |       |
| part     | int(4)       | NO   | PRI | 0       |       |
| offset   | mediumtext   | YES  |     | NULL    |       |
+----------+--------------+------+-----+---------+-------+

部分解釋如下:

一. 選用direct 的原因
官方爲spark提供了兩種方式來消費kafka中的數據, 高階api由kafka自己來來維護offset, 有篇blog總結的比較好

第一種是利用 Kafka 消費者高級 API 在 Spark 的工作節點上創建消費者線程,訂閱 Kafka 中的消息,數據會傳輸到 Spark 工作節點的執行器中,但是默認配置下這種方法在 Spark Job 出錯時會導致數據丟失,如果要保證數據可靠性,需要在 Spark Streaming 中開啓Write Ahead Logs(WAL),也就是上文提到的 Kafka 用來保證數據可靠性和一致性的數據保存方式。可以選擇讓 Spark 程序把 WAL 保存在分佈式文件系統(比如 HDFS)中,

第二種方式不需要建立消費者線程,使用 createDirectStream 接口直接去讀取 Kafka 的 WAL,將 Kafka 分區與 RDD 分區做一對一映射,相較於第一種方法,不需再維護一份 WAL 數據,提高了性能。讀取數據的偏移量由 Spark Streaming 程序通過檢查點機制自身處理,避免在程序出錯的情況下重現第一種方法重複讀取數據的情況,消除了 Spark Streaming 與 ZooKeeper/Kafka 數據不一致的風險。保證每條消息只會被 Spark Streaming 處理一次。以下代碼片通過第二種方式讀取 Kafka 中的數據:

在我在使用第一種方式的時候,如果數據量太大, 往往會出現報錯,瞭解這這兩種方式的不同後, 果斷選用了第二種,

二. 引入KafkaCluster類的原因

引入KafkaCluster是爲了在整個任務啓動之前, 首先獲取topic的有效的最舊offset. 這跟kafka的在實際的使用場景,大公司都是按時間刪除kafka上數據有關,如果任務掛的時間太久,在還未能啓動任務之前,database中保存的offset已經在kafak中失效,這時候爲了最大程度的減少損失,只能從該topic的最舊數據開始消費..

三. 存入database的原因

看上面的代碼,估計好多人也扒過KafkaCluster的源碼, 這個類裏面其實有一個setConsumerOffsets這樣的方法�, 其實在處理過一個batch的數據後, 更新一下該topic下group的offset即可,但是還是在開始啓動這個 job 的時候還得驗證該offset否有效. 貌似這樣還不用外部數據庫,豈不方便? 其實這樣做確實挺方便,
有些場景下這樣做無可厚非, 但我覺得: 如果處理完數據,要寫到外部數據庫, 此時,如果能把寫數據和寫offset放在一個事務中(前提是這個數據庫是支持事務), 那麼就可以即可保證嚴格消費一次

四. conf 中兩個特殊設置設置

爲了確保task不會重複執行請設置下面兩個參數:

  • spark.task.maxFailures=1, Task重試次數爲1,即不重試
  • spark.speculation=false 關閉推測執行, 重點說下這個參數spark.speculation這個參數表示空閒的資源節點會不會嘗試執行還在運行,並且運行時間過長的Task,避免單個節點運行速度過慢導致整個任務卡在一個節點上。這個參數最好設置爲true。與之相配合可以一起設置的參數有spark.speculation.×開頭的參數(設置spark.speculation=true將執行事件過長的節點去掉並重新分配任務而spark.speculation.interval用來設置執行間隔)


 

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