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/