大數(shù)據(jù)中臺之Kafka,到底好在哪里?

架構(gòu)之美
架構(gòu)之美
在 kafka-0.8 版本的設計中,生產(chǎn)者往服務端發(fā)送數(shù)據(jù),是一條發(fā)送一次,這樣吞吐量低,后來的版本里面加了緩沖區(qū)和批量提交的概念,一下子吞吐量提高了很多。

優(yōu)秀設計之基于NIO編程

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

優(yōu)秀設計之高性能網(wǎng)絡設計

個人認為 Kafka 的網(wǎng)絡部分的代碼設計是整個 Kafka 比較精華的部分。我們接下來一步一步分析一下 Kafka Server 端為了支持超高并發(fā)是如何設計其網(wǎng)絡架構(gòu)的?

我們先不看 kafka 本身的網(wǎng)絡架構(gòu),我們先簡單了解一下 Reactor 模式:

圖1 Reactor模型

(1) 首先服務端創(chuàng)建了 ServerSocketChannel 對象并在 Selector 上注冊了 OP_ACCEPT 事件,ServerSocketChannel 負責監(jiān)聽指定端口上的連接。

(2)當客戶端發(fā)起到服務端的網(wǎng)絡連接請求時,服務端的 Selector 監(jiān)聽到 OP_ACCEPT 事件,會觸發(fā) Acceptor 來處理 OP_ACCEPT 事件.

(3)當 Acceptor 接收到來自客戶端的 socket 請求時會為這個連接創(chuàng)建對應的 SocketChannel,將這個 SocketChannel 設置為非阻塞模式,并在 Selector 上注冊它關(guān)注的 I/O 事件。如:OP_WRITER,OP_READ 事件。此時客戶端與服務端的 socket 連接正式建立完成。

(4)當客戶端通過上面建立好的 socket 連接向服務端發(fā)送請求時,服務端的 Selector 會監(jiān)聽到 OP_READ 事件,并觸發(fā)對應的處理邏輯(read handler)。服務端像客戶端發(fā)送響應時,服務端的 Selector 可以監(jiān)聽到 OP_WRITER 事件,并觸發(fā)對應的處理邏輯(writer handler)。

我們看到這種設計就是將所有的事件處理都在同一個線程中完成。這樣的設計適合用在客戶端這種并發(fā)比較小的場景。如果并發(fā)量比較大,或者有個請求處理邏輯要較為復雜,耗時較長,那么就會影響到后續(xù)所有的請求,接著就會導致大量的任務超時。要解決這個問題,我們對上述的架構(gòu)稍作調(diào)整,如下圖所示:

圖2 Reactor 改進模型

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

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

圖3 Reactor 改進模型

如果我們理解了上面的設計以后,再去理解 Kafka 的網(wǎng)絡架構(gòu)就簡單多了,如下圖所示:

圖4 Kafka 網(wǎng)絡模型

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

個人覺得 Kafka 的網(wǎng)絡設計部分代碼設計得很漂亮,就是因為這個網(wǎng)絡架構(gòu),保證了 kafka 的高性能。

優(yōu)秀設計之順序?qū)?/strong>

一開始很多人質(zhì)疑 kafka,大家認為一個架構(gòu)在磁盤之上的系統(tǒng),性能是如何保證的。這點需要跟大家解釋一下,客戶端寫入到 Kafka 的數(shù)據(jù)首先是寫入到操作系統(tǒng)緩存的(所以很快),然后緩存里的數(shù)據(jù)根據(jù)一定的策略再寫入到磁盤,并且寫入到磁盤的時候是順序?qū)?,順序?qū)懭绻疟P的個數(shù)和轉(zhuǎn)數(shù)跟得上的話,都快趕上寫內(nèi)存的速度了!

優(yōu)秀設計之跳表、稀松索引、零拷貝

上面我們看到 kafka 通過順序?qū)懙脑O計保證了高效的寫性能,那讀數(shù)據(jù)的高性能又是如何設計的呢?kafka 是一個消息系統(tǒng),里面的每個消息都會有 offset,如果消費者消費某個 offset 的消息的時候是如何快速定位呢?

01 / 跳 表

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

圖5 Kafka 存儲文件

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

在 kafka 的代碼里,我們一個的 log 文件是存儲是 ConcurrentSkipListMap 里的,是一個 map 結(jié)構(gòu),key 用的是文件名(也就是 offset),value 就是 log 文件內(nèi)容。而 ConcurrentSkipListMap 是基于跳表的數(shù)據(jù)結(jié)構(gòu)設計的。

圖6 concurrentSkipListMap設計

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

02 / 稀松索引

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

圖7 稀松索引

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

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

03 / 零拷貝

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

(1)操作系統(tǒng)將數(shù)據(jù)從磁盤文件中讀取到內(nèi)核空間的頁面緩存;

(2)應用程序?qū)?shù)據(jù)從內(nèi)核空間讀入用戶空間緩沖區(qū);

(3)應用程序?qū)⒆x到數(shù)據(jù)寫回內(nèi)核空間并放入 socket 緩沖區(qū);

(4)操作系統(tǒng)將數(shù)據(jù)從 socket 緩沖區(qū)復制到網(wǎng)卡接口,此時數(shù)據(jù)才能通過網(wǎng)絡發(fā)送。

圖8 非零拷貝流程

上圖我們發(fā)現(xiàn)里面會涉及到兩次數(shù)據(jù)拷貝,Kafka 這兒為了提升性能,所以就采用了零拷貝,零拷貝”只用將磁盤文件的數(shù)據(jù)復制到頁面緩存中一次,然后將數(shù)據(jù)從頁面緩存直接發(fā)送到網(wǎng)絡中(發(fā)送給不同的訂閱者時,都可以使用同一個頁面緩存),避免了重復復制操作,提升了整個讀數(shù)據(jù)的性能。

圖9 零拷貝流程

優(yōu)秀設計之批處理

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

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

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

(4)接著下來這條消息不著急被發(fā)送出去,而是被存到緩沖區(qū)里.

(5)會有一個 sender 線程,從緩沖區(qū)里取數(shù)據(jù),把多條數(shù)據(jù)封裝成一個批次,再一把發(fā)送出去,因為有了這個批量發(fā)送的設計,吞吐量成倍的提升了。

圖10 緩存區(qū)設計

THEEND

最新評論(評論僅代表用戶觀點)

更多
暫無評論