大資料基礎學習-6.Kafka2.11

閒人勿-發表於2018-04-26

一、kafka簡介

Apache Kafka是分散式釋出-訂閱訊息系統。它最初由LinkedIn公司於2010年12月份開源,之後成為Apache專案的一部分。Kafka是一種快速、可擴充套件的、設計內在就是分散式的,分割槽的和可複製的提交日誌服務。

參考:https://blog.csdn.net/suifeng3051/article/details/48053965

1.應用場景

日誌收集:一個公司可以用Kafka可以收集各種服務的log,通過kafka以統一介面服務的方式開放給各種consumer,例如hadoop、Hbase、Solr等。

使用者活動跟蹤:Kafka經常被用來記錄web使用者或者app使用者的各種活動,如瀏覽網頁、搜尋、點選等活動,這些活動資訊被各個伺服器釋出到kafka的topic中,然後訂閱者通過訂閱這些topic來做實時的監控分析,或者裝載到hadoop、資料倉儲中做離線分析和挖掘。

運營指標:Kafka也經常用來記錄運營監控資料。包括收集各種分散式應用的資料,生產各種操作的集中反饋,比如報警和報告。

流式處理:比如接入到spark streaming和storm中。

2.特點

– 訊息持久化:通過O(1)的磁碟資料結構提供資料的持久化,並且支援資料備份防止資料丟失。其快速讀寫的效能得益於預讀和後寫兩個特性。【 預讀:提前把下條記錄讀到記憶體中。後寫:一次寫入多個操作,要求必須是順序讀寫,這種作業系統級別的,對磁碟的一個順序訪問要比對記憶體隨機訪問還要快

– 高吞吐量:每秒百萬級的訊息讀寫,每秒可以處理上百兆的資料

– 分散式:擴充套件能力強

– 多客戶端支援:java、 php、 python、 c++ ……

– 實時性:生產者生產的message立即被消費者可見(message是kafka的基本單位)

– 高併發:支援數千個客戶端同時讀寫

二、kafka架構元件

Kafka中釋出訂閱的物件是topic。我們可以為每類資料建立一個topic,把向topic釋出訊息的客戶端稱作producer,從topic訂閱訊息的客戶端稱作consumer。Producers和consumers可以同時從多個topic讀寫資料。一個kafka叢集由一個或多個broker伺服器組成,它負責持久化和備份具體的kafka訊息。


• Broker:每一臺機器叫一個Broker,broker數量越多,吞吐越強。

• Producer:訊息生產者,用來寫資料

• Consumer:訊息消費者,用來讀資料

• Topic:不同消費者去指定的Topic中讀,不同的生產者往不同的Topic中寫,topic是個邏輯概念,partition是topic的物理實現,一個topic由一個或者多個partition組成,可以提高並行能力。

• Partition:partition物理上由多個segment組成,每個partition內部message有順序,但是全域性沒法保證順序。每個Partition代表一個並行單元,也就是說partition的存在是為了負載均衡,提高並行度。

下面具體講解這些概念。

1.架構特點

Kafka同時為釋出和訂閱提供高吞吐量,每秒可以生產約25萬訊息(約50 MB),每秒處理55萬訊息(約110 MB),生產和消費同時進行。可進行資料持久化操作,將訊息持久化到磁碟,可用於批量消費,例如ETL,以及實時應用程式。通過將資料持久化到硬碟以及replication防止資料丟失。

Kafka可構建分散式系統,易擴充套件。所有的producer、broker和consumer都會有多個,均為分散式。無需停機即可擴充套件叢集規模。訊息被處理的狀態是在consumer端維護,而不是由server端維護。Kafka需要zookeeper支撐,在叢集中Producer不依賴zookeeper,consumer依賴zookeeper。

2.Topic和Partition

• 訊息傳送時都被髮送到一個topic,其本質就是一個目錄。topic是個虛擬的概念,topic實際由一些Partition Logs(分割槽日誌)組成,其組織結構如下圖所示:

我們可以看到,每個Partition中的訊息都是有序的,生產的訊息被不斷追加到Partition log上,其中的每一個訊息都被賦予了一個唯一的offset值。 
Kafka叢集會儲存所有的訊息,不管訊息有沒有被消費;我們可以設定訊息的過期時間(預設七天),只有過期的資料才會被自動清除以釋放磁碟空間。

Kafka需要維持的後設資料只有一個–消費訊息在Partition中的offset值,通過offset值能夠知道資訊具體在哪個位置。

Consumer每消費一個訊息,就會自動移動到下一個資訊上,也就是自己記錄的offset值在不斷增大。其實訊息的消費完全是由Consumer控制的,Consumer可以跟蹤和重設這個offset值,這樣的話Consumer就可以讀取任意位置的訊息。這個偏移量kafka不會維護,如果讓kafka在去記錄每一個consumer的offset,因為每一個Consumer讀的offset是不一樣,再讓kafka去儲存這個資訊,勢必使系統更加複雜,也就是說kafka把更多的控制權給了consumer。 


把訊息日誌以Partition的形式存放有多重考慮,第一,方便在叢集中擴充套件,每個Partition可以通過調整以適應它所在的機器,而一個topic又可以有多個Partition組成,因此整個叢集就可以適應任意大小的資料了;第二就是可以提高併發,因為可以以Partition為單位讀寫了。

Kafka中的topic是以partition的形式存放的,每一個topic都可以設定它的partition數量,Partition的數量決定了組成topic的log的數量。Producer在生產資料時,會按照一定規則(這個規則是可以自定義的)把訊息釋出到topic的各個partition中。topic可以有多個副本,副本都是以partition為單位的,不過只有一個partition的副本會被選舉成leader作為讀寫用。 

關於如何設定partition值需要考慮的因素。一個partition只能被一個消費者消費(一個消費者可以同時消費多個partition),因此,如果設定的partition的數量小於consumer的數量,就會有消費者消費不到資料。所以,推薦partition的數量一定要大於同時執行的consumer的數量。另外一方面,建議partition的數量大於叢集broker的數量,這樣leader partition就可以均勻的分佈在各個broker中,最終使得叢集負載均衡。在Cloudera,每個topic都有上百個partition。需要注意的是,kafka需要為每個partition分配一些記憶體來快取訊息資料,如果partition數量越大,就要為kafka分配更大的heap space。

3.message

message是kafka的基本單位,Kafka 訊息由一個定長的header和變長的位元組陣列組成。因為kafka訊息支援位元組陣列,也就使得kafka可以支援任何使用者自定義的序列號格式或者其它已有的格式如Apache Avro、protobuf等。Kafka沒有限定單個訊息的大小,但我們推薦訊息大小不要超過1MB,通常一般訊息大小都在1~10kB之間。

• message format:轉化成二進位制

    – message length : 4 bytes(value: 1+4+n)

    – "magic" value: 1 byte

    – crc : 4 bytes檢驗碼

    – payload : n bytes

4.Producer

Producers直接傳送訊息到broker上的leader partition,不需要經過任何中介一系列的路由轉發。為了實現這個特性,kafka叢集中的每個broker都可以響應producer的請求,並返回topic的一些元資訊,這些元資訊包括哪些機器是存活的,topic的leader partition都在哪,現階段哪些leader partition是可以直接被訪問的。 

Producer客戶端自己控制著訊息被推送到哪些partition。實現的方式可以是隨機分配、實現一類隨機負載均衡演算法,或者指定一些分割槽演算法。Kafka提供了介面供使用者實現自定義的分割槽,使用者可以為每個訊息指定一個partitionKey,通過這個key來實現一些hash分割槽演算法。比如,把userid作為partition key的話,相同userid的訊息將會被推送到同一個分割槽。 

以Batch的方式推送資料可以極大的提高處理效率,kafka Producer 可以將訊息在記憶體中累計到一定數量後作為一個batch傳送請求。Batch的數量大小可以通過Producer的引數控制,引數值可以設定為累計的訊息的數量(如500條)、累計的時間間隔(如100ms)或者累計的資料大小(64KB)。通過增加batch的大小,可以減少網路請求和磁碟IO的次數,當然具體引數設定需要在效率和時效性方面做一個權衡。 

Producers可以非同步的並行的向kafka傳送訊息,但是通常producer在傳送完訊息之後會得到一個future響應,返回的是offset值或者傳送過程中遇到的錯誤。這其中有個非常重要的引數“acks”,這個引數決定了producer要求leader partition 收到確認的副本個數,如果acks設定數量為0,表示producer不會等待broker的響應,所以,producer無法知道訊息是否傳送成功,這樣有可能會導致資料丟失,但同時,acks值為0會得到最大的系統吞吐量。若acks設定為1,表示producer會在leader partition收到訊息時得到broker的一個確認,這樣會有更好的可靠性,因為客戶端會等待直到broker確認收到訊息。若設定為-1,producer會在所有備份的partition收到訊息時得到broker的確認,這個設定可以得到最高的可靠性保證。 

5.Consumer

consumer是消費資料的客戶端,對於同一個topic,可以有多個消費者,比如spark,storm等等。Kafka提供了兩套consumer api,分為high-level api和sample-api。Sample-api是一個底層的API,它維持了一個和單一broker的連線,並且這個API是完全無狀態的,每次請求都需要指定offset值,因此,這套API也是最靈活的。 

在kafka中,當前讀到訊息的offset值是由consumer來維護的,因此,consumer可以自己決定如何讀取kafka中的資料。比如,consumer可以通過重設offset值來重新消費已消費過的資料。不管有沒有被消費,kafka會儲存資料一段時間,這個時間週期是可配置的,只有到了過期時間,kafka才會刪除這些資料。 【offset之前存在zookeeper上,現在存在kafka的topic上】

High-level API封裝了對叢集中一系列broker的訪問,可以透明的消費一個topic。它自己維持了已消費訊息的狀態,即每次消費的都是下一個訊息。High-level API還支援以組的形式(group)消費topic,如果consumers有同一個組名,那麼kafka就相當於一個佇列訊息服務,而各個consumer均衡地消費相應partition中的資料,整個group消費的partition加起來就是完整的topic資訊。若consumers有不同的組名,那麼此時kafka就相當與一個廣播服務,會把topic中的所有訊息廣播到每個consumer。

每一個partition只能同一時間服務於一個Consumer。但是consumer可以同時消費多個partition。

6.Replication

kafka中的資料是持久化的並且能夠容錯的。Kafka允許使用者為每個topic設定副本數量,副本數量決定了有幾個broker來存放寫入的資料。如果你的副本數量設定為3,那麼一份資料就會被存放在3臺不同的機器上,那麼就允許有2個機器失敗。一般推薦副本數量至少為2,這樣就可以保證增減、重啟機器時不會影響到資料消費。如果對資料持久化有更高的要求,可以把副本數量設定為3或者更多。

三、Kafka核心特性

1.壓縮

Kafka支援以集合(batch)為單位傳送訊息,在此基礎上,Kafka還支援對訊息集合進行壓縮,Producer端可以通過GZIP或Snappy格式對訊息集合進行壓縮。Producer端進行壓縮之後,在Consumer端需進行解壓。壓縮的好處就是減少傳輸的資料量,減輕對網路傳輸的壓力,在對大資料處理上,瓶頸往往體現在網路上而不是CPU(壓縮和解壓會耗掉部分CPU資源)。 

那麼如何區分訊息是壓縮的還是未壓縮的呢,Kafka在訊息頭部新增了一個描述壓縮屬性位元組,這個位元組的後兩位表示訊息的壓縮採用的編碼,如果後兩位為0,則表示訊息未被壓縮。

2.資訊可靠性

在訊息系統中,保證訊息在生產和消費過程中的可靠性是十分重要的,在實際訊息傳遞過程中,可能會出現如下三中情況:
• 一個訊息傳送失敗
• 一個訊息被髮送多次

• 最理想的情況:exactly-once ,一個訊息傳送成功且僅傳送了一次

有許多系統聲稱它們實現了exactly-once,但是它們其實忽略了生產者或消費者在生產和消費過程中有可能失敗的情況。比如雖然一個Producer成功傳送一個訊息,但是訊息在傳送途中丟失,或者成功傳送到broker,也被consumer成功取走,但是這個consumer在處理取過來的訊息時失敗了。 

從Producer端看:Kafka是這麼處理的,當一個訊息被髮送後,Producer會等待broker成功接收到訊息的反饋(可通過引數控制等待時間),如果訊息在途中丟失或是其中一個broker掛掉,Producer會重新傳送(我們知道Kafka有備份機制,可以通過引數控制是否等待所有備份節點都收到訊息)。 

從Consumer端看:前面講到過partition,broker端記錄了partition中的一個offset值,這個值指向Consumer下一個即將消費message。當Consumer收到了訊息,但卻在處理過程中掛掉,此時Consumer可以通過這個offset值重新找到上一個訊息再進行處理。Consumer還有許可權控制這個offset值,對持久化到broker端的訊息做任意處理。

3.備份機制

備份機制是Kafka0.8版本的新特性,備份機制的出現大大提高了Kafka叢集的可靠性、穩定性。有了備份機制後,Kafka允許叢集中的節點掛掉後而不影響整個叢集工作。一個備份數量為n的叢集允許n-1個節點失敗。在所有備份節點中,有一個節點作為lead節點,這個節點儲存了其它備份節點列表,並維持各個備份間的狀體同步。下面這幅圖解釋了Kafka的備份機制:


• kafka將日誌(資料)複製到指定多個伺服器上。

• 複本的單元是partition。在正常情況下,每個partition有一個leader和0到多個follower。

• leader處理對應partition上所有的讀寫請求。partition可以多於broker數,leader也是分散式的。

• follower的日誌和leader的日誌是相同的, follower被動的複製leader。如果leader掛了,其中一個follower會自動變成新的leader。【ISR:是個集合,只有在ISR集合裡的follower才有機會被選為leader】

• kafka需要兩個條件保證是“活著”

– 節點在zookeeper註冊的session還在且可維護(基於zookeeper心跳機制)

– 如果是slave則能夠緊隨leader的更新不至於落得太遠。

• kafka採用in sync來實現“活著”機制

– 如果follower掛掉或卡住或落得很遠,則leader會移除同步列表中的in sync。至於落了多遠才叫遠,由replica.lag.max.messages配置,而表示複本“卡住”由replica.lag.time.max.ms配置。

• 所謂一條訊息是“提交”的,意味著所有in sync的複本也持久化到了他們的log中。這意味著消費者無需擔心leader掛掉導致資料丟失。另一方面,生產者可以選擇是否等待訊息“提交”。

• kafka動態的維護了一組in-sync(ISR)的複本,表示已追上了leader,只有處於該狀態的成員組才是能被選擇為leader。這些ISR組會在發生變化時被持久化到zookeeper中。通過ISR模型和n複本,可以讓kafka的topic支援最多n-1個節點掛掉而不會導致提交的資料丟失。

4.kafka高效性相關設計

1)訊息持久化

Kafka高度依賴檔案系統來儲存和快取訊息,一般的人認為磁碟是緩慢的,這導致人們對持久化結構具有競爭性持懷疑態度。其實,磁碟遠比你想象的要快或者慢,這決定於我們如何使用磁碟。 

一個和磁碟效能有關的關鍵事實是:磁碟驅動器的吞吐量跟尋到延遲是相背離的,也就是所,線性寫的速度遠遠大於隨機寫。比如:在一個67200rpm SATA RAID-5 的磁碟陣列上線性寫的速度大概是600M/秒,但是隨機寫的速度只有100K/秒,兩者相差將近6000倍。線性讀寫在大多數應用場景下是可以預測的,因此,作業系統利用read-ahead和write-behind技術來從大的資料塊中預取資料,或者將多個邏輯上的寫操作組合成一個大的物理寫操作中。對磁碟的線性讀在有些情況下可以比記憶體的隨機訪問要快一些。 為了補償這個效能上的分歧,現代作業系統都會把空閒的記憶體用作磁碟快取,儘管在記憶體回收的時候會有一點效能上的代價。所有的磁碟讀寫操作會在這個統一的快取上進行。 

此外,如果我們是在JVM的基礎上構建的,熟悉java記憶體應用管理的人應該清楚以下兩件事情:
• 一個物件的記憶體消耗是非常高的,經常是所存資料的兩倍或者更多。

• 隨著堆內資料的增多,Java的垃圾回收會變得非常昂貴。

基於這些事實,利用檔案系統並且依靠頁快取比維護一個記憶體快取或者其他結構要好——我們至少要使得可用的快取加倍,通過自動訪問可用記憶體,並且通過儲存更緊湊的位元組結構而不是一個物件,這將有可能再次加倍。這麼做的結果就是在一臺32GB的機器上,如果不考慮GC懲罰,將最多有28-30GB的快取。此外,這些快取將會一直存在即使服務重啟,然而程式內快取需要在記憶體中重構(10GB快取需要花費10分鐘)或者它需要一個完全冷快取啟動(非常差的初始化效能)。它同時也簡化了程式碼,因為現在所有的維護快取和檔案系統之間內聚的邏輯都在作業系統內部了,這使得這樣做比one-off in-process attempts更加高效與準確。如果你的磁碟應用更加傾向於順序讀取,那麼read-ahead在每次磁碟讀取中實際上獲取到這人快取中的有用資料。 

以上這些建議了一個簡單的設計:不同於維護儘可能多的記憶體快取並且在需要的時候重新整理到檔案系統中,我們換一種思路。所有的資料不需要呼叫重新整理程式,而是立刻將它寫到一個持久化的日誌中。事實上,這僅僅意味著,資料將被傳輸到核心頁快取中並稍後被重新整理。我們可以增加一個配置項以讓系統的使用者來控制資料在什麼時候被重新整理到物理硬碟上。

2)常數時間效能保證

訊息系統中持久化資料結構的設計通常是維護者一個和消費佇列有關的B樹或者其它能夠隨機存取結構的後設資料資訊。

B樹是一個很好的結構,可以用在事務型與非事務型的語義中。但是它需要一個很高的花費,儘管B樹的操作需要O(logN)。通常情況下,這被認為與常數時間等價,但這對磁碟操作來說是不對的。磁碟尋道一次需要10ms,並且一次只能尋一個,因此並行化是受限的。 

直覺上來講,一個持久化的佇列可以構建在對一個檔案的讀和追加上,就像一般情況下的日誌解決方案。儘管和B樹相比,這種結構不能支援豐富的語義,但是它有一個優點,所有的操作都是常數時間,並且讀寫之間不會相互阻塞。這種設計具有極大的效能優勢:最終系統效能和資料大小完全無關,伺服器可以充分利用廉價的硬碟來提供高效的訊息服務。 

事實上還有一點,磁碟空間的無限增大而不影響效能這點,意味著我們可以提供一般訊息系統無法提供的特性。比如說,訊息被消費後不是立馬被刪除,我們可以將這些訊息保留一段相對比較長的時間(比如一個星期)。

3)進一步提高效率

我們已經為效率做了非常多的努力。但是有一種非常主要的應用場景是:處理Web活動資料,它的特點是資料量非常大,每一次的網頁瀏覽都會產生大量的寫操作。更進一步,我們假設每一個被髮布的訊息都會被至少一個consumer消費,因此我們更要讓消費變得更廉價。 

通過上面的介紹,我們已經解決了磁碟方面的效率問題,除此之外,在此類系統中還有兩類比較低效的場景:

• 太多小的I/O操作

• 過多的位元組拷貝

為了減少大量小I/O操作的問題,kafka的協議是圍繞訊息集合構建的。Producer一次網路請求可以傳送一個訊息集合,而不是每一次只發一條訊息。在server端是以訊息塊的形式追加訊息到log中的,consumer在查詢的時候也是一次查詢大量的線性資料塊。訊息集合即MessageSet,實現本身是一個非常簡單的API,它將一個位元組陣列或者檔案進行打包。所以對訊息的處理,這裡沒有分開的序列化和反序列化的上步驟,訊息的欄位可以按需反序列化(如果沒有需要,可以不用反序列化)。 

另一個影響效率的問題就是位元組拷貝。為了解決位元組拷貝的問題,kafka設計了一種“標準位元組訊息”,Producer、Broker、Consumer共享這一種訊息格式。Kakfa的message log在broker端就是一些目錄檔案,這些日誌檔案都是MessageSet按照這種“標準位元組訊息”格式寫入到磁碟的。 

維持這種通用的格式對這些操作的優化尤為重要:持久化log 塊的網路傳輸。流行的unix作業系統提供了一種非常高效的途徑來實現頁面快取和socket之間的資料傳遞。在Linux作業系統中,這種方式被稱作:sendfile system call(Java提供了訪問這個系統呼叫的方法:FileChannel.transferTo api)。為了理解sendfile的影響,需要理解一般的將資料從檔案傳到socket的路徑:


資料先拷貝到系統級別的一個Readbuffer裡面去,再從核心(kemelcontext),就是系統快取裡面,讀到應用層(Application context)的快取,資料再進行輸出到外面的磁碟上或者是通過往IO的形式進行向外傳輸,也得通過一個應用層(Application buffer)把這個資料寫回到系統層(Socket buffer),然後系統層再對接到外部的裝置上(NIC buffer),很耗時間和效能。

這種操作方式明顯是非常低效的,這裡有四次拷貝,兩次系統呼叫。如果使用sendfile,就可以避免兩次拷貝:作業系統將資料直接從頁快取傳送到網路上。所以在這個優化的路徑中,只有最後一步將資料拷貝到網路卡快取中是需要的。 

• Kafka層採用無快取設計,而是依賴於底層的檔案系統頁快取。這有助於避免雙重快取,即訊息只快取了一份在頁快取中。同時這在kafka重啟後保持快取warm也有額外的優勢。因kafka根本不快取訊息在程式中,故gc開銷也就很小。

• zero-copy:kafka為了減少位元組拷貝,採用了大多數系統都會提供的sendfile系統呼叫。【效能提升60%】


在系統級別層進行直接讀入和讀出,kafka有預讀和後寫功能,預讀會把沒有讀到的資料提前放到快取裡,那這個快取其實就是Read buffer。

利用上述的zero-copy,資料只被拷貝到頁快取一次,然後就可以在每次消費時被重得利用,而不需要將資料存在記憶體中,然後在每次讀的時候拷貝到核心空間中。這使得訊息消費速度可以達到網路連線的速度。這樣以來,通過頁面快取和sendfile的結合使用,整個kafka叢集幾乎都已以快取的方式提供服務,而且即使下游的consumer很多,也不會對整個叢集服務造成壓力。

5.segment

• Kafka儲存佈局簡單:Topic的每個Partition對應一個邏輯日誌(一個日誌為相同大小的一組分段檔案),由實際的segment files組成。由這些segment files檔案指向實際的partition。

• 每次生產者釋出訊息到一個分割槽,代理就將訊息追加到最後一個段檔案中。當釋出的訊息數量達到設定值或者經過一定的時間後,段檔案真正flush磁碟中。寫入完成後,訊息公開給消費者。

• 與傳統的訊息系統不同,Kafka系統中儲存的訊息沒有明確的訊息Id。

• 訊息通過日誌中的邏輯偏移量offset來公開。

在設計中,一個partition對應多個segment檔案,當要去segment中查詢資料時,由於大segment拆分為多個小的segment,通過二分法(查messageID號)定位到小的segment,再通過offset從前往後查詢資料。

6.無狀態的Broker

• Kafka代理是無狀態的:意味著消費者必須維護已消費的狀態資訊。這些資訊由消費者自己維護,代理完全不管。這種設計非常微妙,它本身包含了創新。

    – 從代理刪除訊息變得很棘手,因為代理並不知道消費者是否已經使用了該訊息。 Kafka創新性地解決了這個問題,它將一個簡單的基於時間的SLA應用於保留策略。當訊息在代理中超過一定時間後,將會被自動刪除。

    – 這種創新設計有很大的好處,消費者可以故意倒回到老的偏移量再次消費資料。這違反了佇列的常見約定,但被證明是許多消費者的基本特徵。

7.交付保證

• Kafka預設採用at least once的訊息投遞策略。即在消費者端的處理順序是獲得訊息->處理訊息->儲存位置。這可能導致一旦客戶端掛掉,新的客戶端接管時處理前面客戶端已處理過的訊息。

• 三種保證策略:

– At most once 訊息可能會丟,但絕不會重複傳輸

– At least one 訊息絕不會丟,但可能會重複傳輸

– Exactly once 每條訊息肯定會被傳輸一次且僅傳輸一次

8.分散式協調

• 由於kafka中一個topic中的不同分割槽只能被消費組中的一個消費者消費,就避免了多個消費者消費相同的分割槽時會導致額外的開銷(如:要協調哪個消費者消費哪個訊息,還有鎖及狀態的開銷)。 kafka中消費程式只需要在代理和同組消費者有變化時時進行一次協調(這種協調不是經常性的,故可以忽略開銷)。

四、Zookeeper的kafka資訊

參考:https://cwiki.apache.org/confluence/display/KAFKA/Kafka+data+structures+in+Zookeeper

kafka使用zookeeper做以下事情:

– 探測broker和consumer的新增或移除

– 維護消費關係和追蹤消費者在分割槽消費的訊息的offset。

1.broker

• Broker NodeRegistry

• /brokers/ids/[0...N] --> host:port (ephemeral node)

– broker啟動時在/brokers/ids下建立一個znode,把broker id寫進去。

– 因為broker把自己註冊到zookeeper中實用的是瞬時節點,所以這個註冊是動態的,如果broker當機或者沒有響應該節點就會被刪除。

• BrokerTopic Registry

• /brokers/topics/[topic]/[0...N] --> nPartions (ephemeral node)

– 每個broker把自己儲存和維護的partion資訊註冊到該路徑下

2.consumer

• Consumersand Consumer Groups

– consumers也把它們自己註冊到zookeeper上,用以保持消費負載平衡和offset記錄。

– group id相同的多個consumer構成一個消費租,共同消費一個topic,同一個組的consumer會盡量均勻的消費,其中的一個consumer只會消費一個partion的資料。

• Consumer IdRegistry

• /consumers/[group_id]/ids/[consumer_id] --> {"topic1":#streams, ...,

"topicN": #streams} (ephemeralnode)

– 每個consumer在/consumers/[group_id]/ids下建立一個瞬時的唯一的consumer_id,用來描述當前該group下有哪些consumer是alive的,如果消費程式掛掉對應的consumer_id就會從該節點刪除。

• ConsumerOffset Tracking

• /consumers/[group_id]/offsets/[topic]/[partition_id] -->offset_counter_value((persistent node)

– consumer把每個partition的消費offset記錄儲存在該節點下。

3.partition

• PartitionOwner registry

•/consumers/[group_id]/owners/[topic]/[broker_id-partition_id]-->consumer_node_id (ephemeral node)

– 該節點維護著partion與consumer之間的對應關係。

基本看到這就暈的差不多了,現在進入實踐部分消化kafka的理論知識。

五、搭建基於Kafka訊息佇列系統

1.安裝和配置

下載安裝kafka,將kafka命令寫入系統環境變數。

export KAFKA_HOME=/usr/local/src/kafka_2.11-0.10.2.1
export PATH=$KAFKA_HOME/bin:$PATH

1)server.properties

broker.id=0#不同機子配置不同的id
delete.topic.enable=true
listeners=PLAINTEXT://master:9092
log.dirs=/usr/local/src/kafka_2.11-0.10.2.1/kafka-logs
zookeeper.connect=masteractive:2181,slave1:2181,slave2:2181

注意:如果要想讓其他機子遠端訪問你的kafka,加上一條配置:

advertised.host.name=192.168.101.101 #提供訪問的地址

2)zookeeper.properties

由於使用自己安裝的zookeeper,所以這裡不再配置。如果要用kafka自帶的zookeeper,可以在配置後,使用:

./bin/zookeeper-server-start.sh config/zookeeper.properties &

2.單機模式

• 啟動Zookeeper:
– ]# ./bin/zookeeper-server-start.sh

• 啟動server:
– ]#./bin/kafka-server-start.sh config/server.properties

• 建立topic:
– ]# bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test

• 檢視主題:
– ]# bin/kafka-topics.sh --list --zookeeper localhost:2181

• 檢視主題詳情:
– ]# bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic test

• 刪除主題:
– ]# bin/kafka-topics.sh --zookeeper localhost:2181 --delete --topic test
– 注意:如果kafaka啟動時載入的配置檔案中server.properties沒有配置delete.topic.enable=true,那麼此時的刪除並不是真正的刪除,而是把topic標記為:marked for deletion
–此時你若想真正刪除它,可以登入zookeeper客戶端,進入終端後,刪除相應節點

• 模擬Producer發訊息:
– ]# ./bin/kafka-console-producer.sh --broker-list master:9092--topic test

• 模擬Consumer讀訊息:
– ]# ./kafka-console-consumer.sh --zookeeper master:2181 --topic test --from beginning

• segment存在/kafka-logs的topic中。
00000000000000000000.index:msgid->offsetid,查到了msgid就可以根據offsetid定位到message。

3.叢集模式

1)單機模擬多節點

# cp server.properties server-1.properties
# cp server.properties server-2.properties
# cp server.properties server-3.properties

再分別修改,以server-3.properties為例,下面id引數要不一樣:

broker.id=3
listeners=PLAINTEXT://:9095#埠要不一樣
log.dirs=/usr/local/src/tmp/kafka-logs-3

在節點上分別啟動:

kafka-server-start.sh -daemon config/server-1.properties &
kafka-server-start.sh -daemon config/server-2.properties &
kafka-server-start.sh -daemon config/server-3.properties &

驗證:

# jps -m
25009 QuorumPeerMain/usr/local/src/zookeeper-3.4.5/bin/../conf/zoo.cfg
117321 Jps -m
117096 Kafka config/server-1.properties
117192 Kafka config/server-2.properties
117246 Kafka config/server-3.properties

# kafka-topics.sh --describe --zookeeper localhost:2181 test1 #假設已經建立了test1
# kafka-console-producer.sh --broker-list master:9093,master:9094,master:9095, --topic test1 #寫到master的不同埠上
# kafka-console-consumer.sh --zookeeper master:2181--topic test1 --from beginning

2)多機

剛剛使用的是在1臺機子模擬叢集,在真正的分散式系統中,配置的思路是相同的,只是在不同機器上面啟動而已,這裡要注意的是,zookeeper的myid一定要先設定好,不然多臺機器沒法在zookeeper上建立節點,那就別談能夠成叢集工作了。

4.consumer.py

可以用Python編寫自己的consumer程式碼,首先安裝kafka模組。

pip install kafka
from kafka import KafkaConsumer

def main():
        consumer = KafkaConsumer(b"test", bootstrap_servers=["master:9092"],
                auto_offset_reset='earliest')
        for message in consumer:
                print(message)

if __name__=="__main__":
        main()

啟動kafka,再啟動producer,最後執行consumer.py,可以進行訊息的消費。

[root@master kafka_2.11-0.9.0.0]# python consumer.py 
ConsumerRecord(topic=u'test', partition=0, offset=1106, timestamp=None, timestamp_type=None, key=None, value='dd', checksum=795512426, serialized_key_size=-1, serialized_value_size=2)
ConsumerRecord(topic=u'test', partition=0, offset=1107, timestamp=None, timestamp_type=None, key=None, value='jfdsajsfda;', checksum=-32431402, serialized_key_size=-1, serialized_value_size=11)
ConsumerRecord(topic=u'test', partition=0, offset=1108, timestamp=None, timestamp_type=None, key=None, value='dsa', checksum=-121585800, serialized_key_size=-1, serialized_value_size=3)

5.producer.py

可以編寫producer,模擬傳送資料。

# -*- coding: UTF-8 -*-
import time

from kafka import SimpleProducer, KafkaClient
from kafka.common import LeaderNotAvailableError

def print_response(response=None):
        if response:
                print('error:{0}'.format(response[0].error))
                print('offset:{0}'.format(response[0].error))
def main():
        kafka = KafkaClient("master:9092")
        producer = SimpleProducer(kafka)

        topic = b'test'
        msg = b'dragon is me'

        try:
                print_response(producer.send_messages(topic,msg))
        except LeaderNotAvailableError:
                time.sleep(1)
                print_response(producer.send_messages(topic,msg))
        kafka.close()

if __name__=="__main__":
        main()

6.groupid.py

這裡模擬建立1個group。

from kafka import KafkaConsumer

def main():
        consumer = KafkaConsumer(b"test", group_id =b"dragonid",bootstrap_servers=["master:9092",slave1:9092],auto_offset_reset='earliest')
        for message in consumer:
                print(message)

if __name__=="__main__":
        main()

master和slave1上的consumer將會一起消費topic為test的訊息,producer傳送多條資料,master和slave1將會一起消費,二者消費的總和將會等於producer總共發出的資料。

7.指定offset消費

當給topic設定了多個partition後,consumer可以指定具體消費哪個partition,並且從哪個offset開始。

from kafka import KafkaConsumer
from kafka.structs import TopicPartition

consumer = KafkaConsumer("test", bootstrap_servers=["master:9092"],
                auto_offset_reset='earliest')
consumer.seek(TopicPartition(topic=u'test',partition=2),21) #消費第二個partition,且偏移量是21

print consumer.partitions_for_topic("test")
print consumer.topics()
print consumer.assignment()
print consumer.begining_offsets(consumer.assignment())
for message in consumer:
        print("%s:%d:%d: key=%s value=%s" %(message.topic,message.partition,message.offset,message.key,message.value))

六、Java程式設計(IDEA)

1.producer

package com.imooc.spark.kafka;

import kafka.Kafka;
import kafka.javaapi.producer.Producer;
import kafka.producer.KeyedMessage;
import kafka.producer.ProducerConfig;

import java.util.Properties;

public class kafkaproducer extends Thread{
    private Stringtopic;
    private Producer<Integer,String>producer;
    public kafkaproducer(String topic){
        this.topic=topic;
        Properties properties = new Properties();
        properties.put("metadata.broker.list",kafkaproperties.BROKER_LIST);
        properties.put("serializer.class","kafka.serializer.StringEncoder");
        properties.put("request.required.acks","1");

        producer = new Producer<Integer,String>(new ProducerConfig(properties));
    }
    @Override
    public void run() {
        int messageNo= 1;
        while(true){
            String message = "message" +messageNo;
            producer.send(new KeyedMessage<Integer, String>(topic,message));
            //System.out.println("send:"+message);
            messageNo++;
            try {
                Thread.sleep(2000);
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}

2.consumer

package com.imooc.spark.kafka;

import kafka.consumer.Consumer;
import kafka.consumer.ConsumerConfig;
import kafka.consumer.ConsumerIterator;
import kafka.consumer.KafkaStream;
import kafka.javaapi.consumer.ConsumerConnector;


import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

public class KafkaConsumer extends Thread{
    private String topic;
    public KafkaConsumer(String topic){
        this.topic=topic;

    }
    private ConsumerConnector createConnector(){
        Properties properties = new Properties();
        properties.put("zookeeper.connect",kafkaproperties.ZK);
        properties.put("group.id",kafkaproperties.GROUP_ID);
        properties.put("zookeeper.session.timeout.ms", "15000");
        return Consumer.createJavaConsumerConnector(new ConsumerConfig(properties));
    }

    @Override
    public void run() {
        ConsumerConnector consumer = createConnector();
        Map<String,Integer> topicCountMap = new HashMap<String, Integer>();
        topicCountMap.put(topic,1);

        Map<String, List<KafkaStream<byte[], byte[]>>> messagestream = consumer.createMessageStreams(topicCountMap);
        KafkaStream<byte[], byte[]> stream = messagestream.get(topic).get(0);
        ConsumerIterator<byte[], byte[]> iterator = stream.iterator();

        while (iterator.hasNext()){
            String message = new String(iterator.next().message());
            System.out.println("rec:"+message);
        }
    }
}

3.properties

package com.imooc.spark.kafka;

public class kafkaproperties {

    public static final String ZK= "192.168.101.10:2181";
    public static final String TOPIC = "test1";
    public static final String BROKER_LIST = "192.168.101.10:9092";
    public static final String GROUP_ID = "test1";
}

4.test

package com.imooc.spark.kafka;

public class KafkaTestApp {

    public static void main(String[] args) {
        new kafkaproducer(kafkaproperties.TOPIC).start();
        new KafkaConsumer(kafkaproperties.TOPIC).start();
    }
}

5.具體步驟

1.建立maven工程,使用intellijidea

2.建立需要的依賴包,在pom.xml新增:

  <properties>
    <scala.version>2.11.8</scala.version>
    <spark.version>2.2.0</spark.version>
    <hadoop.version>2.6.0-cdh5.7.0</hadoop.version>
    <hbase.version>1.2.0-cdh5.7.0</hbase.version>
    <kafka.version>0.9.0.0</kafka.version>
  </properties>


  <repositories>
    <repository>
      <id>cloudera</id>
      <url>https://repository.cloudera.com/artifactory/cloudera-repos/</url>
    </repository>
  </repositories>


  <dependencies>
    <dependency>
      <groupId>org.scala-lang</groupId>
      <artifactId>scala-library</artifactId>
      <version>${scala.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.hadoop</groupId>
      <artifactId>hadoop-client</artifactId>
      <version>${hadoop.version}</version>
    </dependency>

    <dependency>
      <groupId>org.apache.hbase</groupId>
      <artifactId>hbase-client</artifactId>
      <version>${hbase.version}</version>
    </dependency>

    <dependency>
      <groupId>org.apache.hbase</groupId>
      <artifactId>hbase-server</artifactId>
      <version>${hbase.version}</version>
    </dependency>

    <dependency>
      <groupId>org.apache.spark</groupId>
      <artifactId>spark-streaming_2.11</artifactId>
      <version>${spark.version}</version>
    </dependency>

    <dependency>
      <groupId>org.apache.kafka</groupId>
      <artifactId>kafka_2.11</artifactId>
      <version>${kafka.version}</version>
    </dependency>

  </dependencies>

3.開啟Linux上的kafka,確保正常執行kafka。執行程式碼,可以自己產生資料,並用自己編寫的consumer來處理具體的業務。


相關文章