大數據技術棧速覽之:Kafka

https://blog.csdn.net/lingbo229/article/details/80761778

Kafka的特性:

- 高吞吐量、低延遲:kafka每秒可以處理幾十萬條消息,它的延遲最低只有幾毫秒,每個topic可以分多個partition, consumer group 對partition進行consume操作。

- 可擴展性:kafka集羣支持熱擴展

- 持久性、可靠性:消息被持久化到本地磁盤,並且支持數據備份防止數據丟失

- 容錯性:允許集羣中節點失敗(若副本數量爲n,則允許n-1個節點失敗)

- 高併發:支持數千個客戶端同時讀寫

 

Kafka的使用場景:

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

- 消息系統:解耦和生產者和消費者、緩存消息等。

- 用戶活動跟蹤:Kafka經常被用來記錄web用戶或者app用戶的各種活動,如瀏覽網頁、搜索、點擊等活動,這些活動信息被各個服務器發佈到kafka的topic中,然後訂閱者通過訂閱這些topic來做實時的監控分析,或者裝載到hadoop、數據倉庫中做離線分析和挖掘。

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

- 流式處理:比如spark streaming和storm

- 事件源

 

1) Producer端使用zookeeper用來"發現"broker列表,以及和Topic下每個partition leader建立socket連接併發送消息.

2) Broker端使用zookeeper用來註冊broker信息,已經監測partition leader存活性.

3) Consumer端使用zookeeper用來註冊consumer信息,其中包括consumer消費的partition列表等,同時也用來發現broker列表,並和partition leader建立socket連接,並獲取消息。

Leader的選擇

Kafka的核心是日誌文件,日誌文件在集羣中的同步是分佈式數據系統最基礎的要素。

如果leaders永遠不會down的話我們就不需要followers了!一旦leader down掉了,需要在followers中選擇一個新的leader.但是followers本身有可能延時太久或者crash,所以必須選擇高質量的follower作爲leader.必須保證,一旦一個消息被提交了,但是leader down掉了,新選出的leader必須可以提供這條消息。大部分的分佈式系統採用了多數投票法則選擇新的leader,對於多數投票法則,就是根據所有副本節點的狀況動態的選擇最適合的作爲leader.Kafka並不是使用這種方法

Kafka動態維護了一個同步狀態的副本的集合(a set of in-sync replicas),簡稱ISR,在這個集合中的節點都是和leader保持高度一致的,任何一條消息必須被這個集合中的每個節點讀取並追加到日誌中了,纔會通知外部這個消息已經被提交了。因此這個集合中的任何一個節點隨時都可以被選爲leader.ISR在ZooKeeper中維護。ISR中有f+1個節點,就可以允許在f個節點down掉的情況下不會丟失消息並正常提供服。ISR的成員是動態的,如果一個節點被淘汰了,當它重新達到“同步中”的狀態時,他可以重新加入ISR.這種leader的選擇方式是非常快速的,適合kafka的應用場景。

一個邪惡的想法:如果所有節點都down掉了怎麼辦?Kafka對於數據不會丟失的保證,是基於至少一個節點是存活的,一旦所有節點都down了,這個就不能保證了。

實際應用中,當所有的副本都down掉時,必須及時作出反應。可以有以下兩種選擇:

1. 等待ISR中的任何一個節點恢復並擔任leader。

2. 選擇所有節點中(不只是ISR)第一個恢復的節點作爲leader.

這是一個在可用性和連續性之間的權衡。如果等待ISR中的節點恢復,一旦ISR中的節點起不起來或者數據都是了,那集羣就永遠恢復不了了。如果等待ISR意外的節點恢復,這個節點的數據就會被作爲線上數據,有可能和真實的數據有所出入,因爲有些數據它可能還沒同步到。Kafka目前選擇了第二種策略,在未來的版本中將使這個策略的選擇可配置,可以根據場景靈活的選擇。

這種窘境不只Kafka會遇到,幾乎所有的分佈式數據系統都會遇到。

一個partition只能被一個消費者消費(一個消費者可以同時消費多個partition)

Zookeeper 協調控制

1. 管理broker與consumer的動態加入與離開。(Producer不需要管理,隨便一臺計算機都可以作爲Producer向Kakfa Broker發消息)

2. 觸發負載均衡,當broker或consumer加入或離開時會觸發負載均衡算法,使得一個consumer group內的多個consumer的消費負載平衡。(因爲一個comsumer消費一個或多個partition,一個partition只能被一個consumer消費)

3.  維護消費關係及每個partition的消費信息。

Zookeeper上的細節:

1. 每個broker啓動後會在zookeeper上註冊一個臨時的broker registry,包含broker的ip地址和端口號,所存儲的topics和partitions信息。

2. 每個consumer啓動後會在zookeeper上註冊一個臨時的consumer registry:包含consumer所屬的consumer group以及訂閱的topics。

3. 每個consumer group關聯一個臨時的owner registry和一個持久的offset registry。對於被訂閱的每個partition包含一個owner registry,內容爲訂閱這個partition的consumer id;同時包含一個offset registry,內容爲上一次訂閱的offset。

 

ack: 0-不用通知; 1-leader寫成功,ack通知成功; -1 - 所有副本寫成功,才ack通知成功。

 

生產實踐:

1)選舉出了一個新的kafka controller,但是原來的controller在shut down的時候總是不成功,這個時候producer進來的message由於Kafka集羣中存在兩個kafka controller而無法落地。導致數據淤積。

 2) ack=1的時候,一旦有個broker宕機導致partition的follower和leader切換,會導致丟數據。 

 

Kafka中的優秀設計

https://mp.weixin.qq.com/s?__biz=MzIzODIzNzE0NQ==&mid=2654418186&idx=1&sn=c18ae8ad03d0907d2f71f91fd6fb00ca&chksm=f2fff1bcc58878aab50300aa88a4d2e62acd62569e056ab3c92dfa84a9a275b473abe3c46502&mpshare=1&scene=2&srcid=&sharer_sharetime=1577675009037&sharer_shareid=1cfbb4ec295246e3649598007e341b6f&key=507d39fb4abb5ee532a1815c45e582d247ea75676634d979254980c9fd586c0d42d206c810113aab7dde341f058ffa91c893427dce20111d22d18a11045f08874cacc32d3db233ae0c4b22cb1e20ee6e&ascene=14&uin=MjUwOTQyMjQyMw%3D%3D&devicetype=Windows+10&version=62070158&lang=zh_CN&exportkey=AWh6IKI1CwKaf4RapvyE6G4%3D&pass_ticket=S16nnpvopBoyVP9yirPmcmmNT4Jt47BP%2F5FAx8xhQKd1Bcw8Hd%2BnbidoS9axPYmV

優秀設計之基於NIO編程 

Kafka 底層的 IO 用的是 NIO,這個事雖然簡單,但是也需要提一提。我們開發一個分佈式文件系統的時候避免不了需要思考需要什麼樣的 IO?BIO 性能較差,NIO 性能要比 BIO 要好很多,而且編程難度也不算大,當然性能最好的那就是 AIO 了,但是 AIO 編程難度較大,代碼設計起來較爲複雜,所以 Kafka 選擇的是 NIO,也是因爲這些原因,目前我們看到很多開源的技術也都是用的 NIO。

優秀設計之高性能網絡設計 

個人認爲 Kafka 的網絡部分的代碼設計是整個 Kafka 比較精華的部分。我們接下來一步一步分析一下 Kafka Server 端爲了支持超高併發是如何設計其網絡架構的?

我們先不看 kafka 本身的網絡架構,我們先簡單瞭解一下 Reactor 模式:

   圖1  Reactor模型

(1) 首先服務端創建了 ServerSocketChannel 對象並在 Selector 上註冊了 OP_ACCEPT 事件,ServerSocketChannel 負責監聽指定端口上的連接。
(2)當客戶端發起到服務端的網絡連接請求時,服務端的 Selector 監聽到 OP_ACCEPT 事件,會觸發 Acceptor 來處理 OP_ACCEPT 事件.
(3)當 Acceptor 接收到來自客戶端的 socket 請求時會爲這個連接創建對應的 SocketChannel,將這個 SocketChannel 設置爲非阻塞模式,並在 Selector 上註冊它關注的 I/O 事件。如:OP_WRITER,OP_READ 事件。此時客戶端與服務端的 socket 連接正式建立完成。
(4)當客戶端通過上面建立好的 socket 連接向服務端發送請求時,服務端的 Selector 會監聽到 OP_READ 事件,並觸發對應的處理邏輯(read handler)。服務端像客戶端發送響應時,服務端的 Selector 可以監聽到 OP_WRITER 事件,並觸發對應的處理邏輯(writer handler)。

我們看到這種設計就是將所有的事件處理都在同一個線程中完成。這樣的設計適合用在客戶端這種併發比較小的場景。如果併發量比較大,或者有個請求處理邏輯要較爲複雜,耗時較長,那麼就會影響到後續所有的請求,接着就會導致大量的任務超時。要解決這個問題,我們對上述的架構稍作調整,如下圖所示:

圖2 Reactor 改進模型

Accept 單獨運行在一個線程中,這個線程使用 ExecutorService 實現,因爲這樣的話,當 Accept 線程異常退出的時候,ExecutorService 也會創建新的線程進行補償。Read handler 裏面也是一個線程池,這個裏面所有的線程都註冊了 OP_READ 事件,負責接收客戶端傳過來的請求,當然也是一個線程對應了多個 socket 連接。Read handler 裏的線程接收到請求以後把請求都存入到 MessageQueue 裏面。Handler Poll 線程池中的線程就會從 MessageQueue 隊列裏面獲取請求,然後對請求進行處理。這樣設計的話,即使某個請求需要大量耗時,Handler Poll 線程池裏的其它線程也會去處理後面的請求,避免了整個服務端的阻塞。當請求處理完了以後 handler Pool 中的線程註冊 OP_WRITER 事件,實現往客戶端發送響應的功能。

通過這種設計就解決了性能瓶頸的問題,但是如果突然發生了大量的網絡 I/O。單個 Selector 可能會在分發事件的時候成爲性能瓶頸。所以我們很容易想的到應該將上面的單獨的 Selector 擴展爲多個,所以架構圖就變成了如下的這幅圖:

圖3 Reactor 改進模型

如果我們理解了上面的設計以後,再去理解 Kafka 的網絡架構就簡單多了,如下圖所示:

圖4 Kafka 網絡模型

這個就是 Kafka 的 Server 端的網絡架構設計,就是按照前面的網路架構演化出來的。Accepetor 啓動了以後接收連接請求,接收到了請求以後把請求發送給一個線程池(Processor)線程池裏的每個線程獲取到請求以後,把請求封裝爲一個個 SocketChannel 緩存在自己的隊列裏面。接下來給這些 SocketChannel 註冊上 OP_READ 事件,這樣就可以接收客戶端發送過來的請求了,Processor 線程就把接收到的請求封裝成 Request 對象存入到 RequestChannel 的 RequestQueue 隊列。接下來啓動了一個線程池,默認是 8 個線程來對隊列裏面的請求進行處理。處理完了以後把對應的響應放入到對應 ReponseQueue 裏面。每個 Processor 線程從其對應的 ReponseQueue 裏面獲取響應,註冊 OP_WRITER 事件,最終把響應發送給客戶端。

個人覺得 Kafka 的網絡設計部分代碼設計得很漂亮,就是因爲這個網絡架構,保證了 kafka 的高性能。

  優秀設計之順序寫 

一開始很多人質疑 kafka,大家認爲一個架構在磁盤之上的系統,性能是如何保證的。這點需要跟大家解釋一下,客戶端寫入到 Kafka 的數據首先是寫入到操作系統緩存的(所以很快),然後緩存裏的數據根據一定的策略再寫入到磁盤,並且寫入到磁盤的時候是順序寫,順序寫如果磁盤的個數和轉數跟得上的話,都快趕上寫內存的速度了!

 優秀設計之跳錶、稀鬆索引、零拷貝

上面我們看到 kafka 通過順序寫的設計保證了高效的寫性能,那讀數據的高性能又是如何設計的呢?kafka 是一個消息系統,裏面的每個消息都會有 offset,如果消費者消費某個 offset 的消息的時候是如何快速定位呢?

 

01 /  跳 表

如下截圖是我們線上的 kafka 的存儲文件,裏面有兩個重要的文件,一個是 index 文件,一個是 log 文件。

 

圖5 Kafka 存儲文件

 

log 文件裏面存儲的是消息,index 存儲的是索引信息,這兩個文件的文件名都是一樣的,成對出現的,這個文件名是以 log 文件裏的第一條消息的 offset 命名的,如下第一個文件的文件名叫 00000000000012768089,代表着這個文件裏的第一個消息的 offset 是 12768089,也就是說第二條消息就是 12768090 了。

 

在 kafka 的代碼裏,我們一個的 log 文件是存儲是 ConcurrentSkipListMap 裏的,是一個 map 結構,key 用的是文件名(也就是 offset),value 就是 log 文件內容。而 ConcurrentSkipListMap 是基於跳錶的數據結構設計的。

 

圖6 concurrentSkipListMap設計

 

這樣子,我們想要消費某個大小的 offset,可以根據跳錶快速的定位到這個 log 文件了。

02 /  稀鬆索引

經過上面的步驟,我們僅僅也就是定位了 log 文件而已,但是要消費的數據具體的物理位置在哪兒?,我們就得靠 kafka 的稀鬆索引了。假設剛剛我們定位要消費的偏移量是在 00000000000000368769.log 文件裏,如果說要整個文件遍歷,然後一個 offset 一個 offset 比對,性能肯定很差。這個時候就需要藉助剛剛我們看到的 index 文件了,這個文件裏面存的就是消息的 offset 和其對應的物理位置,但 index 不是爲每條消息都存一條索引信息,而是每隔幾條數據才存一條 index 信息,這樣 index 文件其實很小,也就是這個原因我們就管這種方式叫稀鬆索引。

 

圖7 稀鬆索引

 

比如現在我們要消費 offset 等於 368776 的消息,如何根據 index 文件定位呢?(1)首先在 index 文件裏找,index 文件存儲的數據都是成對出現的,比如我們到的 1,0 代表的意思是,offset=368769+1=368770 這條信息存儲的物理位置是 0 這個位置。那現在我們現在想要定位的消息是 368776 這條消息,368776 減去 368769 等於 7,我們就在 index 文件裏找 offset 等於 7 對應的物理位置,但是因爲是稀鬆索引,我們沒找到,不過我們找到了 offset 等於 6 的物理值 1407。

(2)接下來就到 log 文件裏讀取文件的 1407 的位置,然後遍歷後面的 offset,很快就可以遍歷到 offset 等於 7(368776)的數據了,然後從這兒開始消費即可。

03 /  零拷貝

接下來消費者讀取數據的流程用的是零拷貝技術,我們先看一下如下是非零拷貝的流程:

(1)操作系統將數據從磁盤文件中讀取到內核空間的頁面緩存;
(2)應用程序將數據從內核空間讀入用戶空間緩衝區;
(3)應用程序將讀到數據寫回內核空間並放入 socket 緩衝區;
(4)操作系統將數據從 socket 緩衝區複製到網卡接口,此時數據才能通過網絡發送。

 

圖8 非零拷貝流程

 

上圖我們發現裏面會涉及到兩次數據拷貝,Kafka 這兒爲了提升性能,所以就採用了零拷貝,零拷貝”只用將磁盤文件的數據複製到頁面緩存中一次,然後將數據從頁面緩存直接發送到網絡中(發送給不同的訂閱者時,都可以使用同一個頁面緩存),避免了重複複製操作,提升了整個讀數據的性能。

 

圖9 零拷貝流程

 優秀設計之批處理 

在 kafka-0.8 版本的設計中,生產者往服務端發送數據,是一條發送一次,這樣吞吐量低,後來的版本里面加了緩衝區和批量提交的概念,一下子吞吐量提高了很多。下圖就是修改過後的生產者發送消息的原理圖:(1) 消費先被封裝成爲 ProducerRecord 對象.
(2)對消息進行序列化(因爲涉及到網絡傳輸).
(3)使用分區器進行分區(到這兒就決定了這個消息要被髮送到哪兒了).
(4)接着下來這條消息不着急被髮送出去,而是被存到緩衝區裏.
(5)會有一個 sender 線程,從緩衝區裏取數據,把多條數據封裝成一個批次,再一把發送出去,因爲有了這個批量發送的設計,吞吐量成倍的提升了。

圖10 緩存區設計

這個緩存區裏的代碼技術含量很高,感興趣的同學,可以自己去閱讀以下源碼。

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