大資料中臺之Kafka,到底好在哪裡?

奈學教育發表於2020-06-04

今天給大家分享一個大資料裡面很火的技術——Kafka,Kafka 是一個分散式的訊息系統,其高效能在圈內很出名。本人閱讀過多個大資料生態的開源技術的原始碼,個人感覺 Kafka 的原始碼質量是比較高的一個,如果有同學感興趣的話,可以拿來閱讀一下。網上也有不少的文章分析 Kafka 的效能為什麼那麼好,但是我感覺很多文章都沒說到點上,所以今天藉著這個機會跟大家交流一下 kafka 的效能為什麼那麼好?

優秀設計之基於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 快取區設計

這個快取區裡的程式碼技術含量很高,感興趣的同學,可以自己去閱讀以下原始碼。今天 Kafka 就先給大家分析到這兒了!

更多免費資料及影片


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69976011/viewspace-2696254/,如需轉載,請註明出處,否則將追究法律責任。

相關文章