為獲得更好的閱讀體驗,建議您訪問原文地址:傳送門
前言:在之前的文章裡面已經瞭解到了「訊息佇列」是怎麼樣的一種存在(傳送門),Kafka 作為當下流行的一種中介軟體,我們現在開始學習它!
一、Kafka 簡介
Kafka 建立背景
Kafka 是一個訊息系統,原本開發自 LinkedIn,用作 LinkedIn 的活動流(Activity Stream)和運營資料處理管道(Pipeline)的基礎。現在它已被多家不同型別的公司 作為多種型別的資料管道和訊息系統使用。
活動流資料是幾乎所有站點在對其網站使用情況做報表時都要用到的資料中最常規的部分。活動資料包括頁面訪問量(Page View)、被檢視內容方面的資訊以及搜尋情況等內容。這種資料通常的處理方式是先把各種活動以日誌的形式寫入某種檔案,然後週期性地對這些檔案進行統計分析。運營資料指的是伺服器的效能資料(CPU、IO 使用率、請求時間、服務日誌等等資料)。運營資料的統計方法種類繁多。
近年來,活動和運營資料處理已經成為了網站軟體產品特性中一個至關重要的組成部分,這就需要一套稍微更加複雜的基礎設施對其提供支援。
Kafka 簡介
Kafka 是一種分散式的,基於釋出 / 訂閱的訊息系統。主要設計目標如下:
- 以時間複雜度為 O(1) 的方式提供訊息持久化能力,即使對 TB 級以上資料也能保證常數時間複雜度的訪問效能。
- 高吞吐率。即使在非常廉價的商用機器上也能做到單機支援每秒 100K 條以上訊息的傳輸。
- 支援 Kafka Server 間的訊息分割槽,及分散式消費,同時保證每個 Partition 內的訊息順序傳輸。
- 同時支援離線資料處理和實時資料處理。
- Scale out:支援線上水平擴充套件。
Kafka 基礎概念
概念一:生產者與消費者
對於 Kafka 來說客戶端有兩種基本型別:生產者(Producer)和消費者(Consumer)。除此之外,還有用來做資料整合的 Kafka Connect API 和流式處理的 Kafka Streams 等高階客戶端,但這些高階客戶端底層仍然是生產者和消費者API,它們只不過是在上層做了封裝。
這很容易理解,生產者(也稱為釋出者)建立訊息,而消費者(也稱為訂閱者)負責消費or讀取訊息。
概念二:主題(Topic)與分割槽(Partition)
在 Kafka 中,訊息以主題(Topic)來分類,每一個主題都對應一個「訊息佇列」,這有點兒類似於資料庫中的表。但是如果我們把所有同類的訊息都塞入到一個“中心”佇列中,勢必缺少可伸縮性,無論是生產者/消費者數目的增加,還是訊息數量的增加,都可能耗盡系統的效能或儲存。
我們使用一個生活中的例子來說明:現在 A 城市生產的某商品需要運輸到 B 城市,走的是公路,那麼單通道的高速公路不論是在「A 城市商品增多」還是「現在 C 城市也要往 B 城市運輸東西」這樣的情況下都會出現「吞吐量不足」的問題。所以我們現在引入分割槽(Partition)的概念,類似“允許多修幾條道”的方式對我們的主題完成了水平擴充套件。
概念三:Broker 和叢集(Cluster)
一個 Kafka 伺服器也稱為 Broker,它接受生產者傳送的訊息並存入磁碟;Broker 同時服務消費者拉取分割槽訊息的請求,返回目前已經提交的訊息。使用特定的機器硬體,一個 Broker 每秒可以處理成千上萬的分割槽和百萬量級的訊息。(現在動不動就百萬量級..我特地去查了一把,好像確實叢集的情況下吞吐量挺高的..摁..)
若干個 Broker 組成一個叢集(Cluster),其中叢集內某個 Broker 會成為叢集控制器(Cluster Controller),它負責管理叢集,包括分配分割槽到 Broker、監控 Broker 故障等。在叢集內,一個分割槽由一個 Broker 負責,這個 Broker 也稱為這個分割槽的 Leader;當然一個分割槽可以被複制到多個 Broker 上來實現冗餘,這樣當存在 Broker 故障時可以將其分割槽重新分配到其他 Broker 來負責。下圖是一個樣例:
Kafka 的一個關鍵性質是日誌保留(retention),我們可以配置主題的訊息保留策略,譬如只保留一段時間的日誌或者只保留特定大小的日誌。當超過這些限制時,老的訊息會被刪除。我們也可以針對某個主題單獨設定訊息過期策略,這樣對於不同應用可以實現個性化。
概念四:多叢集
隨著業務發展,我們往往需要多叢集,通常處於下面幾個原因:
- 基於資料的隔離;
- 基於安全的隔離;
- 多資料中心(容災)
當構建多個資料中心時,往往需要實現訊息互通。舉個例子,假如使用者修改了個人資料,那麼後續的請求無論被哪個資料中心處理,這個更新需要反映出來。又或者,多個資料中心的資料需要彙總到一個總控中心來做資料分析。
上面說的分割槽複製冗餘機制只適用於同一個 Kafka 叢集內部,對於多個 Kafka 叢集訊息同步可以使用 Kafka 提供的 MirrorMaker 工具。本質上來說,MirrorMaker 只是一個 Kafka 消費者和生產者,並使用一個佇列連線起來而已。它從一個叢集中消費訊息,然後往另一個叢集生產訊息。
二、Kafka 的設計與實現
上面我們知道了 Kafka 中的一些基本概念,但作為一個成熟的「訊息佇列」中介軟體,其中有許多有意思的設計值得我們思考,下面我們簡單列舉一些。
討論一:Kafka 儲存在檔案系統上
是的,您首先應該知道 Kafka 的訊息是存在於檔案系統之上的。Kafka 高度依賴檔案系統來儲存和快取訊息,一般的人認為 “磁碟是緩慢的”,所以對這樣的設計持有懷疑態度。實際上,磁碟比人們預想的快很多也慢很多,這取決於它們如何被使用;一個好的磁碟結構設計可以使之跟網路速度一樣快。
現代的作業系統針對磁碟的讀寫已經做了一些優化方案來加快磁碟的訪問速度。比如,預讀會提前將一個比較大的磁碟快讀入記憶體。後寫會將很多小的邏輯寫操作合併起來組合成一個大的物理寫操作。並且,作業系統還會將主記憶體剩餘的所有空閒記憶體空間都用作磁碟快取,所有的磁碟讀寫操作都會經過統一的磁碟快取(除了直接 I/O 會繞過磁碟快取)。綜合這幾點優化特點,如果是針對磁碟的順序訪問,某些情況下它可能比隨機的記憶體訪問都要快,甚至可以和網路的速度相差無幾。
上述的 Topic 其實是邏輯上的概念,面相消費者和生產者,物理上儲存的其實是 Partition,每一個 Partition 最終對應一個目錄,裡面儲存所有的訊息和索引檔案。預設情況下,每一個 Topic 在建立時如果不指定 Partition 數量時只會建立 1 個 Partition。比如,我建立了一個 Topic 名字為 test ,沒有指定 Partition 的數量,那麼會預設建立一個 test-0 的資料夾,這裡的命名規則是:<topic_name>-<partition_id>
。
任何釋出到 Partition 的訊息都會被追加到 Partition 資料檔案的尾部,這樣的順序寫磁碟操作讓 Kafka 的效率非常高(經驗證,順序寫磁碟效率比隨機寫記憶體還要高,這是 Kafka 高吞吐率的一個很重要的保證)。
每一條訊息被髮送到 Broker 中,會根據 Partition 規則選擇被儲存到哪一個 Partition。如果 Partition 規則設定的合理,所有訊息可以均勻分佈到不同的 Partition中。
討論二:Kafka 中的底層儲存設計
假設我們現在 Kafka 叢集只有一個 Broker,我們建立 2 個 Topic 名稱分別為:「topic1」和「topic2」,Partition 數量分別為 1、2,那麼我們的根目錄下就會建立如下三個資料夾:
| --topic1-0
| --topic2-0
| --topic2-1
在 Kafka 的檔案儲存中,同一個 Topic 下有多個不同的 Partition,每個 Partition 都為一個目錄,而每一個目錄又被平均分配成多個大小相等的 Segment File 中,Segment File 又由 index file 和 data file 組成,他們總是成對出現,字尾 ".index" 和 ".log" 分表表示 Segment 索引檔案和資料檔案。
現在假設我們設定每個 Segment 大小為 500 MB,並啟動生產者向 topic1 中寫入大量資料,topic1-0 資料夾中就會產生類似如下的一些檔案:
| --topic1-0
| --00000000000000000000.index
| --00000000000000000000.log
| --00000000000000368769.index
| --00000000000000368769.log
| --00000000000000737337.index
| --00000000000000737337.log
| --00000000000001105814.index
| --00000000000001105814.log
| --topic2-0
| --topic2-1
Segment 是 Kafka 檔案儲存的最小單位。Segment 檔案命名規則:Partition 全域性的第一個 Segment 從 0 開始,後續每個 Segment 檔名為上一個 Segment 檔案最後一條訊息的 offset 值。數值最大為 64 位 long 大小,19 位數字字元長度,沒有數字用0填充。如 00000000000000368769.index 和 00000000000000368769.log。
以上面的一對 Segment File 為例,說明一下索引檔案和資料檔案對應關係:
其中以索引檔案中後設資料 <3, 497>
為例,依次在資料檔案中表示第 3 個 message(在全域性 Partition 表示第 368769 + 3 = 368772 個 message)以及該訊息的物理偏移地址為 497。
注意該 index 檔案並不是從0開始,也不是每次遞增1的,這是因為 Kafka 採取稀疏索引儲存的方式,每隔一定位元組的資料建立一條索引,它減少了索引檔案大小,使得能夠把 index 對映到記憶體,降低了查詢時的磁碟 IO 開銷,同時也並沒有給查詢帶來太多的時間消耗。
因為其檔名為上一個 Segment 最後一條訊息的 offset ,所以當需要查詢一個指定 offset 的 message 時,通過在所有 segment 的檔名中進行二分查詢就能找到它歸屬的 segment ,再在其 index 檔案中找到其對應到檔案上的物理位置,就能拿出該 message 。
由於訊息在 Partition 的 Segment 資料檔案中是順序讀寫的,且訊息消費後不會刪除(刪除策略是針對過期的 Segment 檔案),這種順序磁碟 IO 儲存設計師 Kafka 高效能很重要的原因。
Kafka 是如何準確的知道 message 的偏移的呢?這是因為在 Kafka 定義了標準的資料儲存結構,在 Partition 中的每一條 message 都包含了以下三個屬性:
- offset:表示 message 在當前 Partition 中的偏移量,是一個邏輯上的值,唯一確定了 Partition 中的一條 message,可以簡單的認為是一個 id;
- MessageSize:表示 message 內容 data 的大小;
- data:message 的具體內容
討論三:生產者設計概要
當我們傳送訊息之前,先問幾個問題:每條訊息都是很關鍵且不能容忍丟失麼?偶爾重複訊息可以麼?我們關注的是訊息延遲還是寫入訊息的吞吐量?
舉個例子,有一個信用卡交易處理系統,當交易發生時會傳送一條訊息到 Kafka,另一個服務來讀取訊息並根據規則引擎來檢查交易是否通過,將結果通過 Kafka 返回。對於這樣的業務,訊息既不能丟失也不能重複,由於交易量大因此吞吐量需要儘可能大,延遲可以稍微高一點。
再舉個例子,假如我們需要收集使用者在網頁上的點選資料,對於這樣的場景,少量訊息丟失或者重複是可以容忍的,延遲多大都不重要只要不影響使用者體驗,吞吐則根據實時使用者數來決定。
不同的業務需要使用不同的寫入方式和配置。具體的方式我們在這裡不做討論,現在先看下生產者寫訊息的基本流程:
流程如下:
- 首先,我們需要建立一個ProducerRecord,這個物件需要包含訊息的主題(topic)和值(value),可以選擇性指定一個鍵值(key)或者分割槽(partition)。
- 傳送訊息時,生產者會對鍵值和值序列化成位元組陣列,然後傳送到分配器(partitioner)。
- 如果我們指定了分割槽,那麼分配器返回該分割槽即可;否則,分配器將會基於鍵值來選擇一個分割槽並返回。
- 選擇完分割槽後,生產者知道了訊息所屬的主題和分割槽,它將這條記錄新增到相同主題和分割槽的批量訊息中,另一個執行緒負責傳送這些批量訊息到對應的Kafka broker。
- 當broker接收到訊息後,如果成功寫入則返回一個包含訊息的主題、分割槽及位移的RecordMetadata物件,否則返回異常。
- 生產者接收到結果後,對於異常可能會進行重試。
討論四:消費者設計概要
消費者與消費組
假設這麼個場景:我們從Kafka中讀取訊息,並且進行檢查,最後產生結果資料。我們可以建立一個消費者例項去做這件事情,但如果生產者寫入訊息的速度比消費者讀取的速度快怎麼辦呢?這樣隨著時間增長,訊息堆積越來越嚴重。對於這種場景,我們需要增加多個消費者來進行水平擴充套件。
Kafka消費者是消費組的一部分,當多個消費者形成一個消費組來消費主題時,每個消費者會收到不同分割槽的訊息。假設有一個T1主題,該主題有4個分割槽;同時我們有一個消費組G1,這個消費組只有一個消費者C1。那麼消費者C1將會收到這4個分割槽的訊息,如下所示:
如果我們增加新的消費者C2到消費組G1,那麼每個消費者將會分別收到兩個分割槽的訊息,如下所示:
如果增加到4個消費者,那麼每個消費者將會分別收到一個分割槽的訊息,如下所示:
但如果我們繼續增加消費者到這個消費組,剩餘的消費者將會空閒,不會收到任何訊息:
總而言之,我們可以通過增加消費組的消費者來進行水平擴充套件提升消費能力。這也是為什麼建議建立主題時使用比較多的分割槽數,這樣可以在消費負載高的情況下增加消費者來提升效能。另外,消費者的數量不應該比分割槽數多,因為多出來的消費者是空閒的,沒有任何幫助。
Kafka一個很重要的特性就是,只需寫入一次訊息,可以支援任意多的應用讀取這個訊息。換句話說,每個應用都可以讀到全量的訊息。為了使得每個應用都能讀到全量訊息,應用需要有不同的消費組。對於上面的例子,假如我們新增了一個新的消費組G2,而這個消費組有兩個消費者,那麼會是這樣的:
在這個場景中,消費組G1和消費組G2都能收到T1主題的全量訊息,在邏輯意義上來說它們屬於不同的應用。
最後,總結起來就是:如果應用需要讀取全量訊息,那麼請為該應用設定一個消費組;如果該應用消費能力不足,那麼可以考慮在這個消費組裡增加消費者。
消費組與分割槽重平衡
可以看到,當新的消費者加入消費組,它會消費一個或多個分割槽,而這些分割槽之前是由其他消費者負責的;另外,當消費者離開消費組(比如重啟、當機等)時,它所消費的分割槽會分配給其他分割槽。這種現象稱為重平衡(rebalance)。重平衡是 Kafka 一個很重要的性質,這個性質保證了高可用和水平擴充套件。不過也需要注意到,在重平衡期間,所有消費者都不能消費訊息,因此會造成整個消費組短暫的不可用。而且,將分割槽進行重平衡也會導致原來的消費者狀態過期,從而導致消費者需要重新更新狀態,這段期間也會降低消費效能。後面我們會討論如何安全的進行重平衡以及如何儘可能避免。
消費者通過定期傳送心跳(hearbeat)到一個作為組協調者(group coordinator)的 broker 來保持在消費組記憶體活。這個 broker 不是固定的,每個消費組都可能不同。當消費者拉取訊息或者提交時,便會傳送心跳。
如果消費者超過一定時間沒有傳送心跳,那麼它的會話(session)就會過期,組協調者會認為該消費者已經當機,然後觸發重平衡。可以看到,從消費者當機到會話過期是有一定時間的,這段時間內該消費者的分割槽都不能進行訊息消費;通常情況下,我們可以進行優雅關閉,這樣消費者會傳送離開的訊息到組協調者,這樣組協調者可以立即進行重平衡而不需要等待會話過期。
在 0.10.1 版本,Kafka 對心跳機制進行了修改,將傳送心跳與拉取訊息進行分離,這樣使得傳送心跳的頻率不受拉取的頻率影響。另外更高版本的 Kafka 支援配置一個消費者多長時間不拉取訊息但仍然保持存活,這個配置可以避免活鎖(livelock)。活鎖,是指應用沒有故障但是由於某些原因不能進一步消費。
Partition 與消費模型
上面提到,Kafka 中一個 topic 中的訊息是被打散分配在多個 Partition(分割槽) 中儲存的, Consumer Group 在消費時需要從不同的 Partition 獲取訊息,那最終如何重建出 Topic 中訊息的順序呢?
答案是:沒有辦法。Kafka 只會保證在 Partition 內訊息是有序的,而不管全域性的情況。
下一個問題是:Partition 中的訊息可以被(不同的 Consumer Group)多次消費,那 Partition中被消費的訊息是何時刪除的? Partition 又是如何知道一個 Consumer Group 當前消費的位置呢?
無論訊息是否被消費,除非訊息到期 Partition 從不刪除訊息。例如設定保留時間為 2 天,則訊息釋出 2 天內任何 Group 都可以消費,2 天后,訊息自動被刪除。
Partition 會為每個 Consumer Group 儲存一個偏移量,記錄 Group 消費到的位置。 如下圖:
為什麼 Kafka 是 pull 模型
消費者應該向 Broker 要資料(pull)還是 Broker 向消費者推送資料(push)?作為一個訊息系統,Kafka 遵循了傳統的方式,選擇由 Producer 向 broker push 訊息並由 Consumer 從 broker pull 訊息。一些 logging-centric system,比如 Facebook 的Scribe和 Cloudera 的Flume,採用 push 模式。事實上,push 模式和 pull 模式各有優劣。
push 模式很難適應消費速率不同的消費者,因為訊息傳送速率是由 broker 決定的。push 模式的目標是儘可能以最快速度傳遞訊息,但是這樣很容易造成 Consumer 來不及處理訊息,典型的表現就是拒絕服務以及網路擁塞。而 pull 模式則可以根據 Consumer 的消費能力以適當的速率消費訊息。
對於 Kafka 而言,pull 模式更合適。pull 模式可簡化 broker 的設計,Consumer 可自主控制消費訊息的速率,同時 Consumer 可以自己控制消費方式——即可批量消費也可逐條消費,同時還能選擇不同的提交方式從而實現不同的傳輸語義。
討論五:Kafka 如何保證可靠性
當我們討論可靠性的時候,我們總會提到*保證**這個詞語。可靠性保證是基礎,我們基於這些基礎之上構建我們的應用。比如關係型資料庫的可靠性保證是ACID,也就是原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和永續性(Durability)。
Kafka 中的可靠性保證有如下四點:
- 對於一個分割槽來說,它的訊息是有序的。如果一個生產者向一個分割槽先寫入訊息A,然後寫入訊息B,那麼消費者會先讀取訊息A再讀取訊息B。
- 當訊息寫入所有in-sync狀態的副本後,訊息才會認為已提交(committed)。這裡的寫入有可能只是寫入到檔案系統的快取,不一定重新整理到磁碟。生產者可以等待不同時機的確認,比如等待分割槽主副本寫入即返回,後者等待所有in-sync狀態副本寫入才返回。
- 一旦訊息已提交,那麼只要有一個副本存活,資料不會丟失。
- 消費者只能讀取到已提交的訊息。
使用這些基礎保證,我們構建一個可靠的系統,這時候需要考慮一個問題:究竟我們的應用需要多大程度的可靠性?可靠性不是無償的,它與系統可用性、吞吐量、延遲和硬體價格息息相關,得此失彼。因此,我們往往需要做權衡,一味的追求可靠性並不實際。
三、動手搭一個 Kafka
通過上面的描述,我們已經大致瞭解到了「Kafka」是何方神聖了,現在我們開始嘗試自己動手本地搭一個來實際體驗一把。
第一步:下載 Kafka
這裡以 Mac OS 為例,在安裝了 Homebrew 的情況下執行下列程式碼:
brew install kafka
由於 Kafka 依賴了 Zookeeper,所以在下載的時候會自動下載。
第二步:啟動服務
我們在啟動之前首先需要修改 Kafka 的監聽地址和埠為 localhost:9092
:
vi /usr/local/etc/kafka/server.properties
然後修改成下圖的樣子:
依次啟動 Zookeeper 和 Kafka:
brew services start zookeeper
brew services start kafka
然後執行下列語句來建立一個名字為 "test" 的 Topic:
kafka-topics --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test
我們可以通過下列的命令檢視我們的 Topic 列表:
kafka-topics --list --zookeeper localhost:2181
第三步:傳送訊息
然後我們新建一個控制檯,執行下列命令建立一個消費者關注剛才建立的 Topic:
kafka-console-consumer --bootstrap-server localhost:9092 --topic test --from-beginning
用控制檯往剛才建立的 Topic 中新增訊息,並觀察剛才建立的消費者視窗:
kafka-console-producer --broker-list localhost:9092 --topic test
能通過消費者視窗觀察到正確的訊息:
參考資料
- https://www.infoq.cn/article/kafka-analysis-part-1 - Kafka 設計解析(一):Kafka 背景及架構介紹
- http://www.dengshenyu.com/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/11/06/kafka-Meet-Kafka.html - Kafka系列(一)初識Kafka
- https://lotabout.me/2018/kafka-introduction/ - Kafka 入門介紹
- https://www.zhihu.com/question/28925721 - Kafka 中的 Topic 為什麼要進行分割槽? - 知乎
- https://blog.joway.io/posts/kafka-design-practice/ - Kafka 的設計與實踐思考
- http://www.dengshenyu.com/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/11/21/kafka-data-delivery.html - Kafka系列(六)可靠的資料傳輸
按照慣例黏一個尾巴:
歡迎轉載,轉載請註明出處!
獨立域名部落格:wmyskxz.com
簡書ID:@我沒有三顆心臟
github:wmyskxz
歡迎關注公眾微訊號:wmyskxz
分享自己的學習 & 學習資料 & 生活
想要交流的朋友也可以加qq群:3382693