大數據中台之Kafka,到底好在哪裡?

科技百分百 發佈 2020-01-02T21:32:09+00:00

作者 | 孫玄責編 | 阿禿Hello,大家好,今天給大家分享一個大數據裡面很火的技術——Kafka,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 模式:

圖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 就先給大家分析到這兒了!

大家加油!!!

關鍵字: