整合Kafka到Spark Streaming——代碼示例和挑戰

作者Michael G. Noll是瑞士的一位工程師和研究員,效力於Verisign,是Verisign實驗室的大規模數據分析基礎設施(基礎Hadoop)的技術主管。本文,Michael詳細的演示瞭如何將Kafka整合到Spark Streaming中。 期間, Michael還提到了將Kafka整合到 Spark Streaming中的一些現狀,非常值得閱讀,雖然有一些信息在Spark 1.2版本中已發生了一些變化,比如HA策略: 通過Spark Contributor、Spark佈道者陳超我們瞭解到 ,在Spark 1.2版本中,Spark Streaming開始支持fully HA模式(選擇使用),通過添加一層WAL(Write Ahead Log),每次收到數據後都會存在HDFS上,從而避免了以前版本中的數據丟失情況,但是不可避免的造成了一定的開銷,需要開發者自行衡量。

以下爲譯文

作爲一個實時大數據處理工具, Spark Sreaming 近日一直被廣泛關注,與 Apache Storm 的對比也經常出現。但是依我說,缺少與Kafka整合,任何實時大數據處理工具都是不完整的,因此我將一個示例Spark Streaming應用程序添加到 kafka-storm-starter ,並且示範如何從Kafka讀取,以及如何寫入到Kafka。在這個過程中,我還使用Avro作爲數據格式,以及Twitter Bijection進行數據序列化。

在本篇文章,我將詳細地講解這個Spark Streaming示例;同時,我還會穿插當下Spark Streaming與Kafka整合的一些焦點話題。免責聲明:這是我首次試驗Spark Streaming,僅作爲參考。

當下,這個Spark Streaming示例被上傳到GitHub,下載訪問: kafka-storm-starter。項目的名稱或許會讓你產生某些誤解,不過,不要在意這些細節:)

什麼是Spark Streaming

Spark Streaming 是Apache Spark的一個子項目。Spark是個類似於Apache Hadoop的開源批處理平臺,而Spark Streaming則是個實時處理工具,運行在Spark引擎之上。

Spark Streaming vs. Apache Storm

Spark Streaming與Apache Storm有一些相似之處,後者是當下最流行的大數據處理平臺。前不久,雅虎的Bobby Evans 和Tom Graves曾發表過一個“ Spark and Storm at Yahoo! ”的演講,在這個演講中,他們對比了兩個大平臺,並提供了一些選擇參考。類似的,Hortonworks的P. Taylor Goetz也分享過名爲 Apache Storm and Spark Streaming Compared 的講義。

這裏,我也提供了一個非常簡短的對比:對比Spark Streaming,Storm的產業採用更高,生產環境應用也更穩定。但是從另一方面來說,對比Storm,Spark擁有更清晰、等級更高的API,因此Spark使用起來也更加愉快,最起碼是在使用Scala編寫Spark應用程序的情況(毫無疑問,我更喜歡Spark中的API)。但是,請別這麼直接的相信我的話,多看看上面的演講和講義。

不管是Spark還是Storm,它們都是Apache的頂級項目,當下許多大數據平臺提供商也已經開始整合這兩個框架(或者其中一個)到其商業產品中,比如Hortonworks就同時整合了Spark和Storm,而Cloudera也整合了Spark。

附錄:Spark中的Machines、cores、executors、tasks和receivers 

本文的後續部分將講述許多Spark和Kafka中的parallelism問題,因此,你需要掌握一些Spark中的術語以弄懂這些環節。

  • 一個Spark集羣必然包含了1個以上的工者作節點,又稱爲從主機(爲了簡化架構,這裏我們先拋棄開集羣管理者不談)。
  • 一個工作者節點可以運行一個以上的executor
  • Executor是一個用於應用程序或者工作者節點的進程,它們負責處理tasks,並將數據保存到內存或者磁盤中。每個應用程序都有屬於自己的executors,一個executor則包含了一定數量的cores(也被稱爲slots)來運行分配給它的任務。
  • Task是一個工作單元,它將被傳送給executor。也就是說,task將是你應用程序的計算內容(或者是一部分)。SparkContext將把這些tasks發送到executors進行執行。每個task都會佔用父executor中的一個core(slot)。
  • Receiver( API  文檔 )將作爲一個長期運行的task跑在一個executor上。每個receiver都會負責一個所謂的input DStream(比如從Kafka中讀取的一個輸入流),同時每個receiver( input DStream)佔用一個core/slot。
  • input DStream:input DStream是DStream的一個類型,它負責將Spark Streaming連接到外部的數據源,用於讀取數據。對於每個外部數據源(比如Kafka)你都需要配置一個input DStream。一個Spark Streaming會通過一個input DStream與一個外部數據源進行連接,任何後續的DStream都會建立標準的DStreams。

在Spark的執行模型,每個應用程序都會獲得自己的executors,它們會支撐應用程序的整個流程,並以多線程的方式運行1個以上的tasks,這種隔離途徑非常類似Storm的執行模型。一旦引入類似YARN或者Mesos這樣的集羣管理器,整個架構將會變得異常複雜,因此這裏將不會引入。你可以通過Spark文檔中的 Cluster Overview 瞭解更多細節。

整合Kafka到Spark Streaming

概述

簡而言之,Spark是支持Kafka的,但是這裏存在許多不完善的地方。

Spark代碼庫中的 KafkaWordCount 對於我們來說是個非常好的起點,但是這裏仍然存在一些開放式問題。

特別是我想了解如何去做:

  • 從kafaka中並行讀入。在Kafka,一個話題(topic)可以有N個分區。理想的情況下,我們希望在多個分區上並行讀取。這也是 Kafka spout in Storm 的工作。
  • 從一個Spark Streaming應用程序向Kafka寫入,同樣,我們需要並行執行。

在完成這些操作時,我同樣碰到了Spark Streaming和/或Kafka中一些已知的問題,這些問題大部分都已經在Spark mailing list中列出。在下面,我將詳細總結Kafka集成到Spark的現狀以及一些常見問題。

Kafka中的話題、分區(partitions)和parallelism

詳情可以查看我之前的博文: Apache Kafka 0.8 Training Deck and Tutorial Running a Multi-Broker Apache Kafka 0.8 Cluster on a Single Node 

Kafka將數據存儲在話題中,每個話題都包含了一些可配置數量的分區。話題的分區數量對於性能來說非常重要,而這個值一般是消費者parallelism的最大數量:如果一個話題擁有N個分區,那麼你的應用程序最大程度上只能進行N個線程的並行,最起碼在使用Kafka內置Scala/Java消費者API時是這樣的。

與其說應用程序,不如說Kafka術語中的消費者羣(consumer group)。消費者羣,通過你選擇的字符串識別,它是邏輯消費者應用程序集羣範圍的識別符。同一個消費者羣中的所有消費者將分擔從一個指定Kafka話題中的讀取任務,同時,同一個消費組中所有消費者從話題中讀取的線程數最大值即是N(等同於分區的數量),多餘的線程將會閒置。

多個不同的Kafka消費者羣可以並行的運行:毫無疑問,對同一個Kafka話題,你可以運行多個獨立的邏輯消費者應用程序。這裏,每個邏輯應用程序都會運行自己的消費者線程,使用一個唯一的消費者羣id。而每個應用程序通常可以使用不同的read parallelisms(見下文)。當在下文我描述不同的方式配置read parallelisms時,我指的是如何完成這些邏輯消費者應用程序中的一個設置。

這裏有一些簡單的例子

  • 你的應用程序使用“terran”消費者羣id對一個名爲“zerg.hydra”的kafka話題進行讀取,這個話題擁有10個分區。如果你的消費者應用程序只配置一個線程對這個話題進行讀取,那麼這個線程將從10個分區中進行讀取。
  • 同上,但是這次你會配置5個線程,那麼每個線程都會從2個分區中進行讀取。
  • 同上,這次你會配置10個線程,那麼每個線程都會負責1個分區的讀取。
  • 同上,但是這次你會配置多達14個線程。那麼這14個線程中的10個將平分10個分區的讀取工作,剩下的4個將會被閒置。

這裏我們不妨看一下現實應用中的複雜性——Kafka中的再平衡事件。在Kafka中,再平衡是個生命週期事件(lifecycle event),在消費者加入或者離開消費者羣時都會觸發再平衡事件。這裏我們不會進行詳述,更多再平衡詳情可參見我的 Kafka training deck 一文。

你的應用程序使用消費者羣id“terran”,並且從1個線程開始,這個線程將從10個分區中進行讀取。在運行時,你逐漸將線程從1個提升到14個。也就是說,在同一個消費者羣中,parallelism突然發生了變化。毫無疑問,這將造成Kafka中的再平衡。一旦在平衡結束,你的14個線程中將有10個線程平分10個分區的讀取工作,剩餘的4個將會被閒置。因此如你想象的一樣,初始線程以後只會讀取一個分區中的內容,將不會再讀取其他分區中的數據。

現在,我們終於對話題、分區有了一定的理解,而分區的數量將作爲從Kafka讀取時parallelism的上限。但是對於一個應用程序來說,這種機制會產生一個什麼樣的影響,比如一個Spark Streaming job或者 Storm topology從Kafka中讀取數據作爲輸入。

1. Read parallelism: 通常情況下,你期望使用N個線程並行讀取Kafka話題中的N個分區。同時,鑑於數據的體積,你期望這些線程跨不同的NIC,也就是跨不同的主機。在Storm中,這可以通過TopologyBuilder#setSpout()設置Kafka spout的parallelism爲N來實現。在Spark中,你則需要做更多的事情,在下文我將詳述如何實現這一點。

2. Downstream processing parallelism: 一旦使用Kafka,你希望對數據進行並行處理。鑑於你的用例,這種等級的parallelism必然與read parallelism有所區別。如果你的用例是計算密集型的,舉個例子,對比讀取線程,你期望擁有更多的處理線程;這可以通過從多個讀取線程shuffling或者網路“fanning out”數據到處理線程實現。因此,你通過增長網絡通信、序列化開銷等將訪問交付給更多的cores。在Storm中,你通過shuffle grouping 將Kafka spout shuffling到下游的bolt中。在Spark中,你需要通過DStreams上的 repartition 轉換來實現。

通常情況下,大家都渴望去耦從Kafka的parallelisms讀取,並立即處理讀取來的數據。在下一節,我將詳述使用 Spark Streaming從Kafka中的讀取和寫入。

從Kafka中讀取

Spark Streaming中的Read parallelism

類似Kafka,Read parallelism中也有分區的概念。瞭解Kafka的per-topic話題與RDDs in Spark 中的分區沒有關聯非常重要。

Spark Streaming中的 KafkaInputDStream (又稱爲Kafka連接器)使用了Kafka的高等級消費者API ,這意味着在Spark中爲Kafka設置 read parallelism將擁有兩個控制按鈕。

1. Input DStreams的數量。 因爲Spark在每個Input DStreams都會運行一個receiver(=task),這就意味着使用多個input DStreams將跨多個節點並行進行讀取操作,因此,這裏寄希望於多主機和NICs。

2. Input DStreams上的消費者線程數量。 這裏,相同的receiver(=task)將運行多個讀取線程。這也就是說,讀取操作在每個core/machine/NIC上將並行的進行。

在實際情況中,第一個選擇顯然更是大家期望的。

爲什麼會這樣?首先以及最重要的,從Kafka中讀取通常情況下會受到網絡/NIC限制,也就是說,在同一個主機上你運行多個線程不會增加讀的吞吐量。另一方面來講,雖然不經常,但是有時候從Kafka中讀取也會遭遇CPU瓶頸。其次,如果你選擇第二個選項,多個讀取線程在將數據推送到blocks時會出現鎖競爭(在block生產者實例上,BlockGenerator的“+=”方法真正使用的是“synchronized”方式)。

input DStreams建立的RDDs分區數量:KafkaInputDStream將儲存從Kafka中讀取的每個信息到Blocks。從我的理解上,一個新的Block由 spark.streaming.blockInterval在毫秒級別建立,而每個block都會轉換成RDD的一個分區,最終由DStream建立。如果我的這種假設成立,那麼由KafkaInputDStream建立的RDDs分區數量由batchInterval / spark.streaming.blockInterval決定,而batchInterval則是數據流拆分成batches的時間間隔,它可以通過StreamingContext的一個構造函數參數設置。舉個例子,如果你的批時間價格是2秒(默認情況下),而block的時間間隔是200毫秒(默認情況),那麼你的RDD將包含10個分區。如果有錯誤的話,可以提醒我。

選項1:控制input DStreams的數量

下面這個例子可以從 Spark Streaming Programming Guide 中獲得:

val ssc:StreamingContext=???// ignore for now
val kafkaParams:Map[String,String]=Map("group.id"->"terran",/* ignore rest */)

val numInputDStreams =5
val kafkaDStreams =(1 to numInputDStreams).map {_=>KafkaUtils.createStream(...)}

在這個例子中,我們建立了5個input DStreams,因此從Kafka中讀取的工作將分擔到5個核心上,寄希望於5個主機/NICs(之所以說是寄希望於,因爲我也不確定Spark Streaming task佈局策略是否會將receivers投放到多個主機上)。所有Input Streams都是“terran”消費者羣的一部分,而Kafka將保證topic的所有數據可以同時對這5個input DSreams可用。換句話說,這種“collaborating”input DStreams設置可以工作是基於消費者羣的行爲是由Kafka API提供,通過KafkaInputDStream完成。

在這個例子中,我沒有提到每個input DSream會建立多少個線程。在這裏,線程的數量可以通過KafkaUtils.createStream方法的參數設置(同時,input topic的數量也可以通過這個方法的參數指定)。在下一節中,我們將通過實際操作展示。

但是在開始之前,在這個步驟我先解釋幾個Spark Streaming中常見的幾個問題,其中有些因爲當下Spark中存在的一些限制引起,另一方面則是由於當下Kafka input DSreams的一些設置造成:

當你使用我上文介紹的多輸入流途徑,而這些消費者都是屬於同一個消費者羣,它們會給消費者指定負責的分區。這樣一來則可能導致syncpartitionrebalance的失敗,系統中真正工作的消費者可能只會有幾個。爲了解決這個問題,你可以把再均衡嘗試設置的非常高,從而獲得它的幫助。然後,你將會碰到另一個坑——如果你的receiver宕機(OOM,亦或是硬件故障),你將停止從Kafka接收消息。

Spark用戶討論 markmail.org/message/…

這裏,我們需要對“停止從Kafka中接收”問題 做一些解釋 。當下,當你通過ssc.start()開啓你的streams應用程序後,處理會開始並一直進行,即使是輸入數據源(比如Kafka)變得不可用。也就是說,流不能檢測出是否與上游數據源失去鏈接,因此也不會對丟失做出任何反應,舉個例子來說也就是重連或者結束執行。類似的,如果你丟失這個數據源的一個receiver,那麼 你的流應用程序可能就會生成一些空的RDDs 

這是一個非常糟糕的情況。最簡單也是最粗糙的方法就是,在與上游數據源斷開連接或者一個receiver失敗時,重啓你的流應用程序。但是,這種解決方案可能並不會產生實際效果,即使你的應用程序需要將Kafka配置選項auto.offset.reset設置到最小——因爲Spark Streaming中一些已知的bug,可能導致你的流應用程序發生一些你意想不到的問題,在下文Spark Streaming中常見問題一節我們將詳細的進行介紹。

選擇2:控制每個input DStream上小發着線程的數量

在這個例子中,我們將建立一個單一的input DStream,它將運行3個消費者線程——在同一個receiver/task,因此是在同一個core/machine/NIC上對Kafka topic “zerg.hydra”進行讀取。

val ssc:StreamingContext=???// ignore for now
val kafkaParams:Map[String,String]=Map("group.id"->"terran",...)

val consumerThreadsPerInputDstream =3
val topics =Map("zerg.hydra"-> consumerThreadsPerInputDstream)
val stream =KafkaUtils.createStream(ssc, kafkaParams, topics,...)

KafkaUtils.createStream方法被重載,因此這裏有一些不同方法的特徵。在這裏,我們會選擇Scala派生以獲得最佳的控制。

結合選項1和選項2

下面是一個更完整的示例,結合了上述兩種技術:

val ssc:StreamingContext=???
val kafkaParams:Map[String,String]=Map("group.id"->"terran",...)

val numDStreams =5
val topics =Map("zerg.hydra"->1)
val kafkaDStreams =(1 to numDStreams).map{_ =>KafkaUtils.createStream(ssc, kafkaParams, topics,...)}

我們建立了5個input DStreams,它們每個都會運行一個消費者線程。如果“zerg.hydra”topic擁有5個分區(或者更少),那麼這將是進行並行讀取的最佳途徑,如果你在意系統最大吞吐量的話。

Spark Streaming中的並行Downstream處理

在之前的章節中,我們覆蓋了從Kafka的並行化讀取,那麼我們就可以在Spark中進行並行化處理。那麼這裏,你必須弄清楚Spark本身是如何進行並行化處理的。類似Kafka,Spark將parallelism設置的與(RDD)分區數量有關, 通過在每個RDD分區上運行task進行 。在有些文檔中,分區仍然被稱爲“slices”。

在任何Spark應用程序中,一旦某個Spark Streaming應用程序接收到輸入數據,其他處理都與非streaming應用程序相同。也就是說,與普通的Spark數據流應用程序一樣,在Spark Streaming應用程序中,你將使用相同的工具和模式。更多詳情可見Level of Parallelism in Data Processing 文檔。

因此,我們同樣將獲得兩個控制手段:

1. input DStreams的數量 ,也就是說,我們在之前章節中read parallelism的數量作爲結果。這是我們的立足點,這樣一來,我們在下一個步驟中既可以保持原樣,也可以進行修改。

2. DStream轉化的重分配 。這裏將獲得一個全新的DStream,其parallelism等級可能增加、減少,或者保持原樣。在DStream中每個返回的RDD都有指定的N個分區。DStream由一系列的RDD組成,DStream.repartition則是通過RDD.repartition實現。接下來將對RDD中的所有數據做隨機的reshuffles,然後建立或多或少的分區,並進行平衡。同時,數據會在所有網絡中進行shuffles。換句話說,DStream.repartition非常類似Storm中的shuffle grouping。

因此,repartition是從processing parallelism解耦read parallelism的主要途徑。在這裏,我們可以設置processing tasks的數量,也就是說設置處理過程中所有core的數量。間接上,我們同樣設置了投入machines/NICs的數量。

一個DStream轉換相關是 union 。這個方法同樣在StreamingContext中,它將從多個DStream中返回一個統一的DStream,它將擁有相同的類型和滑動時間。通常情況下,你更願意用StreamingContext的派生。一個union將返回一個由Union RDD支撐的UnionDStream。Union RDD由RDDs統一後的所有分區組成,也就是說,如果10個分區都聯合了3個RDDs,那麼你的聯合RDD實例將包含30個分區。換句話說,union會將多個 DStreams壓縮到一個 DStreams或者RDD中,但是需要注意的是,這裏的parallelism並不會發生改變。你是否使用union依賴於你的用例是否需要從所有Kafka分區進行“in one place”信息獲取決定,因此這裏大部分都是基於語義需求決定。舉個例子,當你需要執行一個不用元素上的(全局)計數。

注意: RDDs是無序的。因此,當你union RDDs時,那麼結果RDD同樣不會擁有一個很好的序列。如果你需要在RDD中進行sort。

你的用例將決定需要使用的方法,以及你需要使用哪個。如果你的用例是CPU密集型的,你希望對zerg.hydra topic進行5 read parallelism讀取。也就是說,每個消費者進程使用5個receiver,但是卻可以將processing parallelism提升到20。

val ssc:StreamingContext=???
val kafkaParams:Map[String,String]=Map("group.id"->"terran",...)
val readParallelism =5
val topics =Map("zerg.hydra"->1)
val kafkaDStreams =(1 to readParallelism).map{ _ =>KafkaUtils.createStream(ssc, kafkaParams, topics,...)}//> collection of five *input* DStreams = handled by five receivers/tasks
val unionDStream = ssc.union(kafkaDStreams)// often unnecessary, just showcasing how to do it//> single DStream
val processingParallelism =20
val processingDStream = unionDStream(processingParallelism)//> single DStream but now with 20 partitions

在下一節中,我將把所有部分結合到一起,並且聯合實際數據處理進行講解。

寫入到Kafka

寫入到Kafka需要從foreachRDD輸出操作進行:

通用的輸出操作者都包含了一個功能(函數),讓每個RDD都由Stream生成。這個函數需要將每個RDD中的數據推送到一個外部系統,比如將RDD保存到文件,或者通過網絡將它寫入到一個數據庫。需要注意的是,這裏的功能函數將在驅動中執行,同時其中通常會伴隨RDD行爲,它將會促使流RDDs的計算。

注意: 重提“功能函數是在驅動中執行”,也就是Kafka生產者將從驅動中進行,也就是說“功能函數是在驅動中進行評估”。當你使用foreachRDD從驅動中讀取Design Patterns時,實際過程將變得更加清晰。

在這裏,建議大家去閱讀Spark文檔中的 Design Patterns for using foreachRDD一節,它將詳細講解使用foreachRDD讀外部系統中的一些常用推薦模式,以及經常出現的一些陷阱。

在我們這個例子裏,我們將按照推薦來重用Kafka生產者實例,通過生產者池跨多個RDDs/batches。 我通過 Apache Commons Pool 實現了這樣一個工具,已經上傳到GitHub 。這個生產者池本身通過 broadcast variable 提供給tasks。

最終結果看起來如下:

val producerPool ={// See the full code on GitHub for details on how the pool is created
  val pool = createKafkaProducerPool(kafkaZkCluster.kafka.brokerList, outputTopic.name)
  ssc.sparkContext.broadcast(pool)}

stream.map {...}.foreachRDD(rdd =>{
  rdd.foreachPartition(partitionOfRecords =>{// Get a producer from the shared pool
    val p = producerPool.value.borrowObject()
    partitionOfRecords.foreach{case tweet:Tweet=>// Convert pojo back into Avro binary format
      val bytes = converter.value.apply(tweet)// Send the bytes to Kafka
      p.send(bytes)}// Returning the producer to the pool also shuts it down
    producerPool.value.returnObject(p)})})

需要注意的是, Spark Streaming每分鐘都會建立多個RDDs,每個都會包含多個分區,因此你無需爲Kafka生產者實例建立新的Kafka生產者,更不用說每個Kafka消息。上面的步驟將最小化Kafka生產者實例的建立數量,同時也會最小化TCP連接的數量(通常由Kafka集羣確定)。你可以使用這個池設置來精確地控制對流應用程序可用的Kafka生產者實例數量。如果存在疑惑,儘量用更少的。

完整示例

下面的代碼是示例Spark Streaming應用程序的要旨(所有代碼參見 這裏 )。這裏,我做一些解釋:

  • 並行地從Kafka topic中讀取Avro-encoded數據。我們使用了一個最佳的read parallelism,每個Kafka分區都配置了一個單線程 input DStream。
  • 並行化Avro-encoded數據到pojos中,然後將他們並行寫到binary,序列化可以通過Twitter Bijection 執行。
  • 通過Kafka生產者池將結果寫回一個不同的Kafka topic。
// Set up the input DStream to read from Kafka (in parallel)
val kafkaStream ={
  val sparkStreamingConsumerGroup ="spark-streaming-consumer-group"
  val kafkaParams =Map("zookeeper.connect"->"zookeeper1:2181","group.id"->"spark-streaming-test","zookeeper.connection.timeout.ms"->"1000")
  val inputTopic ="input-topic"
  val numPartitionsOfInputTopic =5
  val streams =(1 to numPartitionsOfInputTopic) map { _ =>KafkaUtils.createStream(ssc, kafkaParams,Map(inputTopic ->1),StorageLevel.MEMORY_ONLY_SER).map(_._2)}
  val unifiedStream = ssc.union(streams)
  val sparkProcessingParallelism =1// You'd probably pick a higher value than 1 in production.
  unifiedStream.repartition(sparkProcessingParallelism)}// We use accumulators to track global "counters" across the tasks of our streaming app
val numInputMessages = ssc.sparkContext.accumulator(0L,"Kafka messages consumed")
val numOutputMessages = ssc.sparkContext.accumulator(0L,"Kafka messages produced")// We use a broadcast variable to share a pool of Kafka producers, which we use to write data from Spark to Kafka.
val producerPool ={
  val pool = createKafkaProducerPool(kafkaZkCluster.kafka.brokerList, outputTopic.name)
  ssc.sparkContext.broadcast(pool)}// We also use a broadcast variable for our Avro Injection (Twitter Bijection)
val converter = ssc.sparkContext.broadcast(SpecificAvroCodecs.toBinary[Tweet])// Define the actual data flow of the streaming job
kafkaStream.map {case bytes =>
  numInputMessages +=1// Convert Avro binary data to pojo
  converter.value.invert(bytes) match {caseSuccess(tweet)=> tweet
    caseFailure(e)=>// ignore if the conversion failed}}.foreachRDD(rdd =>{
  rdd.foreachPartition(partitionOfRecords =>{
    val p = producerPool.value.borrowObject()
    partitionOfRecords.foreach{case tweet:Tweet=>// Convert pojo back into Avro binary format
      val bytes = converter.value.apply(tweet)// Send the bytes to Kafka
      p.send(bytes)
      numOutputMessages +=1}
    producerPool.value.returnObject(p)})})// Run the streaming job
ssc.start()
ssc.awaitTermination()

更多的細節和解釋可以在這裏看所有源代碼。

就我自己而言,我非常喜歡 Spark Streaming代碼的簡潔和表述。在Bobby Evans和 Tom Graves講話中沒有提到的是,Storm中這個功能的等價代碼是非常繁瑣和低等級的: kafka-storm-starter 中的 KafkaStormSpec 會運行一個Stormtopology來執行相同的計算。同時,規範文件本身只有非常少的代碼,當然是除下說明語言,它們能更好的幫助理解;同時,需要注意的是,在Storm的Java API中,你不能使用上文Spark Streaming 示例中所使用的匿名函數,比如map和foreach步驟。取而代之的是,你必須編寫完整的類來獲得相同的功能,你可以查看 AvroDecoderBolt 。這感覺是將Spark的API轉換到Java,在這裏使用匿名函數是非常痛苦的。

最後,我同樣也非常喜歡 Spark的說明文檔 ,它非常適合初學者查看,甚至還包含了一些 進階使用 。關於Kafka整合到Spark,上文已經基本介紹完成,但是我們仍然需要瀏覽mailing list和深挖源代碼。這裏,我不得不說,維護幫助文檔的同學做的實在是太棒了。

知曉Spark Streaming中的一些已知問題

你可能已經發現在Spark中仍然有一些尚未解決的問題,下面我描述一些我的發現:

一方面,在對Kafka進行讀寫上仍然存在一些含糊不清的問題,你可以在類似Multiple Kafka Receivers and Union  How to scale more consumer to Kafka stream mailing list的討論中發現。

另一方面,Spark Streaming中一些問題是因爲Spark本身的固有問題導致,特別是故障發生時的數據丟失問題。換句話說,這些問題讓你不想在生產環境中使用Spark。

  • 在Spark 1.1版本的驅動中,Spark並不會考慮那些已經接收卻因爲種種原因沒有進行處理的元數據( 點擊這裏查看更多細節 )。因此,在某些情況下,你的Spark可能會丟失數據。Tathagata Das指出驅動恢復問題會在Spark的1.2版本中解決,當下已經釋放。
  • 1.1版本中的Kafka連接器是基於Kafka的高等級消費者API。這樣就會造成一個問題,Spark Streaming不可以依賴其自身的KafkaInputDStream將數據從Kafka中重新發送,從而無法解決下游數據丟失問題(比如Spark服務器發生故障)。
  • 有些人甚至認爲這個版本中的Kafka連接器不應該投入生產環境使用,因爲它是基於Kafka的高等級消費者API。取而代之,Spark應該使用簡單的消費者API(就像Storm中的Kafka spout),它將允許你控制便宜和分區分配確定性。
  • 但是當下Spark社區已經在致力這些方面的改善,比如Dibyendu Bhattacharya的Kafka連接器。後者是Apache Storm Kafka spout的一個端口,它基於Kafka所謂的簡單消費者API,它包含了故障發生情景下一個更好的重放機制。
  • 即使擁有如此多志願者的努力,Spark團隊更願意非特殊情況下的Kafka故障恢復策略,他們的目標是“在所有轉換中提供強保證,通用的策略”,這一點非常難以理解。從另一個角度來說,這是浪費Kafka本身的故障恢復策略。這裏確實難以抉擇。
  • 這種情況同樣也出現在寫入情況中,很可能會造成數據丟失。
  • Spark的Kafka消費者參數auto.offset.reset的使用同樣與Kafka的策略不同。在Kafka中,將auto.offset.reset設置爲最小是消費者將自動的將offset設置爲最小offset,這通常會發生在兩個情況:第一,在ZooKeeper中不存在已有offsets;第二,已存在offset,但是不在範圍內。而在Spark中,它會始終刪除所有的offsets,並從頭開始。這樣就代表着,當你使用auto.offset.reset = “smallest”重啓你的應用程序時,你的應用程序將完全重新處理你的Kafka應用程序。更多詳情可以在下面的兩個討論中發現: 1  2 
  • Spark-1341:用於控制Spark Streaming中的數據傳輸速度。這個能力可以用於很多情況,當你已經受Kafka引起問題所煩惱時(比如auto.offset.reset所造成的),然後可能讓你的應用程序重新處理一些舊數據。但是鑑於這裏並沒有內置的傳輸速率控制,這個功能可能會導致你的應用程序過載或者內存不足。

在這些故障處理策略和Kafka聚焦的問題之外之外,擴展性和穩定性上的關注同樣不可忽視。再一次,仔細觀看 Bobby和Tom的視頻 以獲得更多細節。在Spark使用經驗上,他們都永遠比我更豐富。

當然,我也有我的 評論 ,在 G1 garbage(在Java 1.7.0u4+中) 上可能也會存在問題。但是,我從來都沒碰到這個問題。

Spark使用技巧和敲門

在我實現這個示例的代碼時,我做了一些重要的筆記。雖然這不是一個全面的指南,但是在你開始Kafka整合時可能發揮一定的作用。它包含了 Spark Streaming programming guide 中的一些信息,也有一些是來自Spark用戶的mailing list。

通用

  • 當你建立你的Spark環境時,對Spark使用的cores數量配置需要特別投入精力。你必須爲Spark配置receiver足夠使用的cores(見下文),當然實際數據處理所需要的cores的數量也要進行配置。在Spark中,每個receiver都負責一個input DStream。同時,每個receiver(以及每個input DStream) occies一個core,這樣做是服務於每個文件流中的讀取(詳見文檔)。舉個例子,你的作業需要從兩個input streams中讀取數據,但是隻訪問兩個cores,這樣一來,所有數據都只會被讀取而不會被處理。
  • 注意,在一個流應用程序中,你可以建立多個input DStreams來並行接收多個數據流。在上文從Kafka並行讀取一節中,我曾演示過這個示例作業。
  • 你可以使用 broadcast variables在不同主機上共享標準、只讀參數,相關細節見下文的優化指導。在示例作業中,我使用了broadcast variables共享了兩個參數:第一,Kafka生產者池(作業通過它將輸出寫入Kafka);第二,encoding/decoding Avro數據的注入(從Twitter Bijection中)。 Passing functions to Spark 
  • 你可以使用累加器參數來跟蹤流作業上的所有全局“計數器”,這裏可以對照Hadoop作業計數器。在示例作業中,我使用累加器分別計數所有消費的Kafka消息,以及所有對Kafka的寫入。如果你對累加器進行命名,它們同樣可以在Spark UI上展示。
  • 不要忘記import Spark和Spark Streaming環境:
// Required to gain access to RDD transformations via implicits.import org.apache.spark.SparkContext._

// Required when working on `PairDStreams` to gain access to e.g. `DStream.reduceByKey`// (versus `DStream.transform(rddBatch => rddBatch.reduceByKey()`) via implicits.//// See also http://spark.apache.org/docs/1.1.0/programming-guide.html#working-with-key-value-pairsimport org.apache.spark.streaming.StreamingContext.toPairDStreamFunctions

如果你是 Twitter Algebird的愛好者,你將會喜歡使用Count-Min Sketch和Spark中的一些特性,代表性的,你會使用reduce或者reduceByWindow這樣的操作(比如,DStreams上的轉換 )。Spark項目包含了 Count-Min Sketch  HyperLogLog 的示例介紹。

如果你需要確定Algebird數據結構的內存介紹,比如Count-Min Sketch、HyperLogLog或者Bloom Filters,你可以使用SparkContext日誌進行查看,更多細節參見 Determining Memory Consumption 

Kafka整合

我前文所述的一些增補:

  • 你可能需要修改Spark Streaming中的一些Kafka消費者配置。舉個例子,如果你需要從Kafka中讀取大型消息,你必須添加fetch.message.max.bytes消費設置。你可以使用KafkaUtils.createStream(…)將這樣定製的Kafka參數給Spark Streaming傳送。

測試

  • 首先,確定 已經 在一個finally bloc或者測試框架的teardown method中使用stop()關閉了StreamingContext 和/或 SparkContext,因爲在同一個程序(或者JVM?)中Spark不支持並行運行兩種環境。
  • 根據我的經驗,在使用sbt時,你希望在測試中將你的建立配置到分支JVM中。最起碼在kafka-storm-starter中,測試必須並行運行多個線程,比如ZooKeeper、Kafka和Spark的內存實例。開始時,你可以參考 build.sbt 
  • 同樣,如果你使用的是Mac OS X,你可能期望關閉JVM上的IPv6用以阻止DNS相關超時。這個問題與Spark無關,你可以查看 .sbtopts 來獲得關閉IPv6的方法。

性能調優

  • 確定你理解作業中的運行時影響,如果你需要與外部系統通信,比如Kafka。在使用foreachRDD時,你應該閱讀中 Spark Streaming programming guide 中的Design Patterns一節。舉個例子,我的用例中使用Kafka產生者池來優化 Spark Streaming到Kafka的寫入。在這裏,優化意味着在多個task中共享同一個生產者,這個操作可以顯著地減少由Kafka集羣建立的新TCP連接數。
  • 使用Kryo做序列化,取代默認的Java serialization,詳情可以訪問 Tuning Spark 。我的例子就使用了Kryo和註冊器,舉個例子,使用Kryo生成的Avro-generated Java類(見 KafkaSparkStreamingRegistrator )。除此之外,在Storm中類似的問題也可以使用Kryo來解決。
  • 通過將spark.streaming.unpersist設置爲true將Spark Streaming 作業設置到明確持續的RDDs。這可以顯著地減少Spark在RDD上的內存使用,同時也可以改善GC行爲。(點擊訪問 來源 
  • 通過MEMORY_ONLY_SER開始你的儲存級別P&S測試(在這裏,RDD被存儲到序列化對象,每個分區一個字節)。取代反序列化,這樣做更有空間效率,特別是使用Kryo這樣的高速序列化工具時,但是會增加讀取上的CPU密集操作。這個優化對 Spark Streaming作業也非常有效。對於本地測試來說,你可能並不想使用*_2派生(2=複製因子)。

總結

完整的Spark Streaming示例代碼可以在 kafka-storm-starter 查看。這個應用包含了Kafka、Zookeeper、Spark,以及上文我講述的示例。

總體來說,我對我的初次Spark Streaming體驗非常滿意。當然,在Spark/Spark Streaming也存在一些需要特別指明的問題,但是我肯定Spark社區終將解決這些問題。在這個過程中,得到了Spark社區積極和熱情的幫助,同時我也非常期待Spark 1.2版本的新特性。

在大型生產環境中,基於Spark還需要一些TLC才能達到Storm能力,這種情況我可能將它投入生產環境中麼?大部分情況下應該不會,更準確的說應該是現在不會。那麼在當下,我又會使用Spark Streaming做什麼樣的處理?這裏有兩個想法,我認爲肯定存在更多:

  • 它可以非常快的原型數據流。如果你因爲數據流太大而遭遇擴展性問題,你可以運行 Spark Streaming,在一些樣本數據或者一部分數據中。
  • 搭配使用Storm和Spark Streaming。舉個例子,你可以使用Storm將原始、大規模輸入數據處理到易管理等級,然後使用Spark Streaming來做下一步的分析,因爲後者可以開箱即用大量有趣的算法、計算指令和用例。

感謝Spark社區對大數據領域所作出的貢獻!

 

翻譯/童陽

文章出處:推酷-CSDN

發佈了60 篇原創文章 · 獲贊 0 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章