Kafka 設計詳解之網絡通信
前言
Kafka 是 LinkedIn 開發的一個分布式的消息中間件。由于其高吞吐量、可水平擴展等特性,目前被廣泛使用,已經是目前大數據生態系統中不可或缺的一環,有關其詳細介紹可以查看官方的文檔。Kafka 的流行源于他優秀的設計,如依靠磁盤(以及操作系統的 Page Cache)而不是內存來存儲隊列數據、充分使用零拷貝(zero-copy)以減少數據在不同內存空間間的拷貝、數據盡可能的使用順序讀寫等。今天準備深度解析 kafka 的網絡通信模塊,來學習下實現一個高吞吐量的系統要設計一個怎么樣的網絡通信機制。
網絡通訊協議
作為一個消息隊列,涉及的網絡通信主要有兩塊:
- 消息生產者與消息隊列服務器之間(Kafka 中是生產者向隊列「推」消息)
- 消息消費者與消息隊列服務器之間(Kafka 中是消費者向隊列「拉」消息)
要實現上述的網絡通信,我們可以使用 HTTP 協議,比如服務端內嵌一個 jetty 容器,通過 servlet 來實現客戶端與服務端之間的交互,但是其性能存在問題,無法滿足高吞吐量這個需求。要實現高性能的網絡通信,我們可以使用更底層的 TCP 或者 UDP 來實現自己的私有協議,而 UDP 協議是不可靠的傳輸協議,畢竟我們不希望一條消息在投遞或者消費途中丟失了,所以 Kafka 選擇 TCP 作為服務間通訊的協議。
網絡 IO 模型
談到網絡通信,繞不過 IO 模型,IO 模型主要是同步與異步,阻塞與非阻塞之間進行選擇。
Kafka 的生產者同時實現了同步和異步兩種類型的客戶端(即:向服務端發完請求后可以一直等待響應也可以繼續干后面的事),其異步客戶端實現方式是通過線程池加回調函數。
Kafka 的服務端使用了 NIO 的 IO 多路復用技術,是非阻塞的 IO, kafka 的早期版本中,服務端是通過同步的方式處理客戶端請求,最新版本是通過異步的方式進行的。
Kafka 自帶的消費者是通過同步阻塞的方式進行數據拉取的,當然如果需要異步處理,可以自己另外寫一個異步消費者。
Reactor 線程模型
Kafka 采用的是 Reactor 多線程模型,即通過一個 Acceptor 線程處理所有的新連接,通過多個 Processor 線程對請求進行處理(解析協議、封裝請求并轉發)。在早期版本中,對請求的處理在 Processor 線程中同步進行,也就是說,有多少個 Processor 線程就有多少個處理請求的線程。在新版本中,kafka 新增了一個 Handler 模塊,通過指定的線程數對請求進行專門處理,Handler 與 Processor 之間通過一個 block queue 進行連接。線程模型如圖:
kafka 線程模型
網絡通信流程剖析
Kafka 的整個網絡通信框架并非一成不變,從早期版本到現在經歷了一些變化,下面我們通過分析早期的版本與最新版本的網絡通信流程,了解其演變過程,以供自己在設計系統的網絡通信時的一些參考。
早期版本(0.7)
Kafka 以 NIO 作為網絡通信的基礎,其通過將許多 socket 連接注冊到一個 Selector 監聽,可以只用一個線程就能管理很多的連接,減少了大量線程的系統開銷。
早期版本的 kafka 的網絡通信實現是一個簡單的 Reactor 多線程模型,如圖:
kafka 早期版本網絡通信流程(白色虛線框內是一個 Processor 線程內部做的工作)
-
客戶端向服務端發起請求時,Accept 負責接受這個 TCP 連接,連接成功后傳遞給其中一個 Processor 線程(先添加到 Processor 線程中的內部新連接隊列)。
-
Processor 線程收到該新連接后(從新連接隊列中 poll),將其注冊到自身的 Selector 中,監聽其 READ 事件。
-
每當 Client 在這個連接上寫入數據,就會觸發 Processor 線程中 Selector 監聽的 READ 事件,這時該線程會讀出連接中的元數據,根據協議(Handler Mapping)調用相應的 Handler 進行處理
-
Handler 處理完成后,可能會有返回值需要返回給客戶端(如 Fetch 請求就需要返回具體內容給客戶端),這時將 Handler 返回的 Response 綁定到連接上(SelectionKey.attach 方法),同時將這個連接的監聽事件從 READ 轉為 WRITE。
-
Selector 監聽到剛才注冊的 WRITE 事件,將連接中綁定的 Response 發送。
個人理解 4、5 兩步可以合并,即如果 Handler 有返回值,就直接返回,個人猜測 kafka 這樣設計可能是出于整個架構上更加清晰優美的目的。
新版本
新版 Kafka 也是以 NIO 作為網絡通信的基礎,也是用 Reactor 多線程模型,所不同的是新版把具體業務處理模塊(Handler 模塊)獨立出去,用單獨的線程池進行控制。具體如下圖:
kafka 新版本網絡通信流程
新版本分離出 Handler 模塊,我理解的好處有以下幾個:
-
可以單獨指定 Handler 的線程數量,便于調優和管理
-
可以避免一個超大請求堵住整一個 Processor 線程的情況
-
因為 Request 與 Handler、Handler 與 Response 之間都是通過隊列進行連接,所以彼此是解耦的,可以讓請求變為異步,對系統的性能會有提升
總結
本文通過分析 kafka 的網絡通信設計對網絡編程進行了一次學習,筆者之后又對 netty 的網絡通信進行了了解,發現大部分也類似,可見目前的高性能的網絡通信可能存在「最佳實踐」,不過真正在設計一個系統的網絡通信時,還有很多工程上的問題需要解決,有許多的「坑」,很容易為系統埋下定時炸彈,因此,我看很多大牛都建議不要自己去實現網絡通信模塊,因為 netty 已經足夠優秀了。
來自:http://www.jianshu.com/p/eab8f15880b5