spark的kafka的低階API createDirectStream

大家都知道在spark1.3版本後,kafkautil裏面提供了兩個創建dstream的方法,一個是老版本中有的createStream方法,還有一個是後面新加的createDirectStream方法。關於這兩個方法的優缺點,官方已經說的很詳細(http://spark.apache.org/docs/latest/streaming-kafka-integration.html),總之就是createDirectStream性能會更好一點,通過新方法創建出來的dstream的rdd partition和kafka的topic的partition是一一對應的,通過低階API直接從kafka的topic消費消息,但是它不再往zookeeper中更新consumer offsets,使得基於zk的consumer offsets的監控工具都會失效。

官方只是蜻蜓點水般的說了一下可以在foreachRDD中更新zookeeper上的offsets:

[plain] view plain copy
  1. directKafkaStream.foreachRDD { rdd =>   
  2.      val offsetRanges = rdd.asInstanceOf[HasOffsetRanges]  
  3.      // offsetRanges.length = # of Kafka partitions being consumed  
  4.      ...  
  5.  }  

對應Exactly-once semantics要自己去實現了,大致的實現思路就是在driver啓動的時候先從zk上獲得consumer offsets信息,createDirectStream有兩個重載方法,其中一個可以設置從任意offsets位置開始消費,部分代碼如下:

[plain] view plain copy
  1. def createDirectStream(implicit streamingConfig: StreamingConfig, kc: KafkaCluster) = {  
  2.   
  3.       val extractors = streamingConfig.getExtractors()  
  4.       //從zookeeper上讀取offset開始消費message  
  5.       val messages = {  
  6.         val kafkaPartitionsE = kc.getPartitions(streamingConfig.topicSet)  
  7.         if (kafkaPartitionsE.isLeft) throw new SparkException("get kafka partition failed:")  
  8.         val kafkaPartitions = kafkaPartitionsE.right.get  
  9.         val consumerOffsetsE = kc.getConsumerOffsets(streamingConfig.group, kafkaPartitions)  
  10.         if (consumerOffsetsE.isLeft) throw new SparkException("get kafka consumer offsets failed:")  
  11.         val consumerOffsets = consumerOffsetsE.right.get  
  12.         consumerOffsets.foreach {  
  13.           case (tp, n) => println("===================================" + tp.topic + "," + tp.partition + "," + n)  
  14.         }  
  15.         KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder, (String, String)](  
  16.           ssc, kafkaParams, consumerOffsets, (mmd: MessageAndMetadata[String, String]) => (mmd.key, mmd.message))  
  17.       }  
  18.       messages  
  19.     }  

這裏會有幾個問題,就是在一個group是新的consumer group時,即首次消費,zk上海沒有相應的group offsets目錄,這時要先初始化一下zk上的offsets目錄,或者是zk上記錄的offsets已經過時,由於kafka有定時清理策略,直接從zk上的offsets開始消費會報ArrayOutofRange異常,即找不到offsets所屬的index文件了,針對這兩種情況,做了以下處理:

[plain] view plain copy
  1. def setOrUpdateOffsets(implicit streamingConfig: StreamingConfig, kc: KafkaCluster): Unit = {  
  2.     streamingConfig.topicSet.foreach(topic => {  
  3.       println("current topic:" + topic)  
  4.       var hasConsumed = true  
  5.       val kafkaPartitionsE = kc.getPartitions(Set(topic))  
  6.       if (kafkaPartitionsE.isLeft) throw new SparkException("get kafka partition failed:")  
  7.       val kafkaPartitions = kafkaPartitionsE.right.get  
  8.       val consumerOffsetsE = kc.getConsumerOffsets(streamingConfig.group, kafkaPartitions)  
  9.       if (consumerOffsetsE.isLeft) hasConsumed = false  
  10.       if (hasConsumed) {  
  11.         //如果有消費過,有兩種可能,如果streaming程序執行的時候出現kafka.common.OffsetOutOfRangeException,說明zk上保存的offsets已經過時了,即kafka的定時清理策略已經將包含該offsets的文件刪除。  
  12.         //針對這種情況,只要判斷一下zk上的consumerOffsets和leaderEarliestOffsets的大小,如果consumerOffsets比leaderEarliestOffsets還小的話,說明是過時的offsets,這時把leaderEarliestOffsets更新爲consumerOffsets  
  13.         val leaderEarliestOffsets = kc.getEarliestLeaderOffsets(kafkaPartitions).right.get  
  14.         println(leaderEarliestOffsets)  
  15.         val consumerOffsets = consumerOffsetsE.right.get  
  16.         val flag = consumerOffsets.forall {  
  17.           case (tp, n) => n < leaderEarliestOffsets(tp).offset  
  18.         }  
  19.         if (flag) {  
  20.           println("consumer group:" + streamingConfig.group + " offsets已經過時,更新爲leaderEarliestOffsets")  
  21.           val offsets = leaderEarliestOffsets.map {  
  22.             case (tp, offset) => (tp, offset.offset)  
  23.           }  
  24.           kc.setConsumerOffsets(streamingConfig.group, offsets)  
  25.         }  
  26.         else {  
  27.           println("consumer group:" + streamingConfig.group + " offsets正常,無需更新")  
  28.         }  
  29.       }  
  30.       else {  
  31.         //如果沒有被消費過,則從最新的offset開始消費。  
  32.         val leaderLatestOffsets = kc.getLatestLeaderOffsets(kafkaPartitions).right.get  
  33.         println(leaderLatestOffsets)  
  34.         println("consumer group:" + streamingConfig.group + " 還未消費過,更新爲leaderLatestOffsets")  
  35.         val offsets = leaderLatestOffsets.map {  
  36.           case (tp, offset) => (tp, offset.offset)  
  37.         }  
  38.         kc.setConsumerOffsets(streamingConfig.group, offsets)  
  39.       }  
  40.     })  
  41.   }  
這裏又碰到了一個問題,從consumer offsets到leader latest offsets中間延遲了很多消息,在下一次啓動的時候,首個batch要處理大量的消息,會導致spark-submit設置的資源無法滿足大量消息的處理而導致崩潰。因此在spark-submit啓動的時候多加了一個配置:--conf spark.streaming.kafka.maxRatePerPartition=10000。限制每秒鐘從topic的每個partition最多消費的消息條數,這樣就把首個batch的大量的消息拆分到多個batch中去了,爲了更快的消化掉delay的消息,可以調大計算資源和把這個參數調大。

OK,driver啓動的問題解決了,那麼接下來處理處理完消息後更新zk offsets的工作,這裏要注意是在處理完之後再更新,想想如果你消費了消息先更新zk offset在去處理消息將處理好的消息保存到其他地方去,如果後一步由於處理消息的代碼有BUG失敗了,前一步已經更新了zk了,會導致這部分消息雖然被消費了但是沒被處理,等你把處理消息的BUG修復再重新提交後,這部分消息在下次啓動的時候不會再被消費了,因爲你已經更新了ZK OFFSETS,針對這些因素考慮,部分代碼實現如下:

[plain] view plain copy
  1. def updateZKOffsets(rdd: RDD[(String, String)])(implicit streamingConfig: StreamingConfig, kc: KafkaCluster): Unit = {  
  2.     println("rdd not empty,update zk offset")  
  3.     val offsetsList = rdd.asInstanceOf[HasOffsetRanges].offsetRanges  
  4.   
  5.   
  6.     for (offsets <- offsetsList) {  
  7.       val topicAndPartition = TopicAndPartition(offsets.topic, offsets.partition)  
  8.       val o = kc.setConsumerOffsets(streamingConfig.group, Map((topicAndPartition, offsets.untilOffset)))  
  9.       if (o.isLeft) {  
  10.         println(s"Error updating the offset to Kafka cluster: ${o.left.get}")  
  11.       }  
  12.     }  
  13.   }  
  14.   
  15.   def processData(messages: InputDStream[(String, String)])(implicit streamingConfig: StreamingConfig, kc: KafkaCluster): Unit = {  
  16.     messages.foreachRDD(rdd => {  
  17.       if (!rdd.isEmpty()) {  
  18.   
  19.         val datamodelRDD = streamingConfig.relation match {  
  20.           case "1" =>  
  21.             val (topic, _) = streamingConfig.topic_table_mapping  
  22.             val extractor = streamingConfig.getExtractor(topic)  
  23.             // Create direct kafka stream with brokers and topics  
  24.             val topicsSet = Set(topic)  
  25.             val datamodel = rdd.filter(msg => {  
  26.               extractor.filter(msg)  
  27.             }).map(msg => extractor.msgToRow(msg))  
  28.             datamodel  
  29.           case "2" =>  
  30.             val (topics, _) = streamingConfig.topic_table_mapping  
  31.             val extractors = streamingConfig.getExtractors(topics)  
  32.             val topicsSet = topics.split(",").toSet  
  33.   
  34.             //kafka msg爲key-value形式,key用來對msg進行分區用的,爲了散列存儲消息,採集器那邊key採用的是:topic|加一個隨機數的形式,例如:rd_e_pal|20,split by |取0可以拿到對應的topic名字,這樣union在一起的消息可以區分出來自哪一個topic  
  35.             val datamodel = rdd.filter(msg => {  
  36.               //kafka msg爲key-value形式,key用來對msg進行分區用的,爲了散列存儲消息,採集器那邊key採用的是:topic|加一個隨機數的形式,例如:rd_e_pal|20,split by |取0可以拿到對應的topic名字,這樣union在一起的消息可以區分出來自哪一個topic  
  37.               val keyValid = msg != null && msg._1 != null && msg._1.split("\\|").length == 2  
  38.               if (keyValid) {  
  39.                 val topic = msg._1.split("\\|")(0)  
  40.                 val (_, extractor) = extractors.find(p => {  
  41.                   p._1.equalsIgnoreCase(topic)  
  42.                 }).getOrElse(throw new RuntimeException("配置文件中沒有找到topic:" + topic + " 對應的extractor"))  
  43.                 //trim去掉末尾的換行符,否則取最後一個字段時會有一個\n  
  44.                 extractor.filter(msg._2.trim)  
  45.               }  
  46.               else {  
  47.                 false  
  48.               }  
  49.   
  50.             }).map {  
  51.               case (key, msgContent) =>  
  52.                 val topic = key.split("\\|")(0)  
  53.                 val (_, extractor) = extractors.find(p => {  
  54.                   p._1.equalsIgnoreCase(topic)  
  55.                 }).getOrElse(throw new RuntimeException("配置文件中沒有找到topic:" + topic + " 對應的extractor"))  
  56.                 extractor.msgToRow((key, msgContent))  
  57.             }  
  58.             datamodel  
  59.         }  
  60.         //先處理消息  
  61.         processRDD(datamodelRDD)  
  62.         //再更新offsets  
  63.         updateZKOffsets(rdd)  
  64.       }  
  65.     })  
  66.   }  
  67.   
  68.   def processRDD(rdd: RDD[Row])(implicit streamingConfig: StreamingConfig) = {  
  69.     if (streamingConfig.targetType == "mongo") {  
  70.       val target = streamingConfig.getTarget().asInstanceOf[MongoTarget]  
  71.       if (!MongoDBClient.db.collectionExists(target.collection)) {  
  72.         println("create collection:" + target.collection)  
  73.         MongoDBClient.db.createCollection(target.collection, MongoDBObject("storageEngine" -> MongoDBObject("wiredTiger" -> MongoDBObject())))  
  74.         val coll = MongoDBClient.db(target.collection)  
  75.         //創建ttl index  
  76.         if (target.ttlIndex) {  
  77.           val indexs = coll.getIndexInfo  
  78.           if (indexs.find(p => p.get("name") == "ttlIndex") == None) {  
  79.             coll.createIndex(MongoDBObject(target.ttlColumn -> 1), MongoDBObject("expireAfterSeconds" -> target.ttlExpire, "name" -> "ttlIndex"))  
  80.           }  
  81.         }  
  82.       }  
  83.   
  84.     }  
  85.   
  86.     val (_, table) = streamingConfig.topic_table_mapping  
  87.     val schema = streamingConfig.getTableSchema(table)  
  88.   
  89.     // Get the singleton instance of SQLContext  
  90.     val sqlContext = HIVEContextSingleton.getInstance(rdd.sparkContext)  
  91.   
  92.     // Convert RDD[String] to RDD[case class] to DataFrame  
  93.     val dataFrame = sqlContext.createDataFrame(rdd, schema)  
  94.   
  95.     // Register as table  
  96.     dataFrame.registerTempTable(table)  
  97.   
  98.     // Do word count on table using SQL and print it  
  99.     val results = sqlContext.sql(streamingConfig.sql)  
  100.     //select dt,hh(vtm) as hr,app_key, collect_set(device_id) as deviceids  from rd_e_app_header where dt=20150401 and hh(vtm)='01' group by dt,hh(vtm),app_key limit 100 ;  
  101.     //          results.show()  
  102.     streamingConfig.targetType match {  
  103.       case "mongo" => saveToMongo(results)  
  104.       case "show" => results.show()  
  105.     }  
  106.   
  107.   }  
  108.   
  109.   
  110.   def saveToMongo(df: DataFrame)(implicit streamingConfig: StreamingConfig) = {  
  111.     val target = streamingConfig.getTarget().asInstanceOf[MongoTarget]  
  112.     val coll = MongoDBClient.db(target.collection)  
  113.     val result = df.collect()  
  114.     if (result.size > 0) {  
  115.       val bulkWrite = coll.initializeUnorderedBulkOperation  
  116.       result.foreach(row => {  
  117.         val id = row(target.pkIndex)  
  118.         val setFields = target.columns.filter(p => p.op == "set").map(f => (f.name, row(f.index))).toArray  
  119.         val incFields = target.columns.filter(p => p.op == "inc").map(f => {  
  120.           (f.name, row(f.index).asInstanceOf[Long])  
  121.         }).toArray  
  122.         //        obj=obj.++($addToSet(MongoDBObject("test"->MongoDBObject("$each"->Array(3,4)),"test1"->MongoDBObject("$each"->Array(1,2)))))  
  123.         var obj = MongoDBObject()  
  124.         var addToSetObj = MongoDBObject()  
  125.         target.columns.filter(p => p.op == "addToSet").foreach(col => {  
  126.           col.mType match {  
  127.             case "Int" =>  
  128.               addToSetObj = addToSetObj.++(col.name -> MongoDBObject("$each" -> row(col.index).asInstanceOf[ArrayBuffer[Int]]))  
  129.             case "Long" =>  
  130.               addToSetObj = addToSetObj.++(col.name -> MongoDBObject("$each" -> row(col.index).asInstanceOf[ArrayBuffer[Long]]))  
  131.             case "String" =>  
  132.               addToSetObj = addToSetObj.++(col.name -> MongoDBObject("$each" -> row(col.index).asInstanceOf[ArrayBuffer[String]]))  
  133.           }  
  134.   
  135.         })  
  136.         if (addToSetObj.size > 0) obj = obj.++($addToSet(addToSetObj))  
  137.         if (incFields.size > 0) obj = obj.++($inc(incFields: _*))  
  138.         if (setFields.size > 0) obj = obj.++($set(setFields: _*))  
  139.         bulkWrite.find(MongoDBObject("_id" -> id)).upsert().updateOne(obj)  
  140.       })  
  141.       bulkWrite.execute()  
  142.     }  
  143.   }  

仔細想一想,還是沒有實現精確一次的語義,寫入mongo和更新ZK由於不是一個事務的,如果更新mongo成功,然後更新ZK失敗,則下次啓動的時候這個批次的數據就被重複計算,對於UV由於是addToSet去重操作,沒什麼影響,但是PV是inc操作就會多算這一個批次的的數據,其實如果batch time比較短的話,其實都還是可以接受的。

原文地址:http://blog.csdn.net/xiao_jun_0820/article/details/46911775

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