大數據中臺之Kafka,到底好在哪裏?

作者 | 孫玄

責編 | 阿禿

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

優秀設計之基於NIO編程

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

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

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

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

大數據中臺之Kafka,到底好在哪裏?

圖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)。

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

大數據中臺之Kafka,到底好在哪裏?

圖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 擴展爲多個,所以架構圖就變成了如下的這幅圖:

大數據中臺之Kafka,到底好在哪裏?

圖3 Reactor 改進模型

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

大數據中臺之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 文件。

大數據中臺之Kafka,到底好在哪裏?

圖5 Kafka 存儲文件

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

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

大數據中臺之Kafka,到底好在哪裏?

圖6 concurrentSkipListMap設計

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

02 / 稀鬆索引

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

大數據中臺之Kafka,到底好在哪裏?

圖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 緩衝區複製到網卡接口,此時數據才能通過網絡發送。

大數據中臺之Kafka,到底好在哪裏?

圖8 非零拷貝流程

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

大數據中臺之Kafka,到底好在哪裏?

圖9 零拷貝流程

優秀設計之批處理

在 kafka-0.8 版本的設計中,生產者往服務端發送數據,是一條發送一次,這樣吞吐量低,後來的版本里面加了緩衝區和批量提交的概念,一下子吞吐量提高了很多。下圖就是修改過後的生產者發送消息的原理圖:(1) 消費先被封裝成爲 ProducerRecord 對象.

(2)對消息進行序列化(因爲涉及到網絡傳輸).

(3)使用分區器進行分區(到這兒就決定了這個消息要被髮送到哪兒了).

(4)接着下來這條消息不着急被髮送出去,而是被存到緩衝區裏.

(5)會有一個 sender 線程,從緩衝區裏取數據,把多條數據封裝成一個批次,再一把發送出去,因爲有了這個批量發送的設計,吞吐量成倍的提升了。

大數據中臺之Kafka,到底好在哪裏?

圖10 緩存區設計

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

今天 Kafka 就先給大家分析到這兒了!

大家加油! !!

相關文章