一文詳解消息隊列——Kafka如何實現高性能IO?

本文已收錄GitHub,更有互聯網大廠面試真題,面試攻略,高效學習資料等

Kafka 是一個高性能的消息隊列,在衆多消息隊列產品中,Kafka 的性能絕對是處於第一梯隊的。我曾經在一臺配置比較好的服務器上,對 Kafka 做過極限的性能壓測,Kafka 單個節點的極限處理能力接近每秒鐘 2000 萬條消息,吞吐量達到每秒鐘 600MB。

你可能會問,Kafka 是如何做到這麼高的性能的?

之前就曾探討過:怎麼開發一個高性能的網絡應用程序。其中提到了像全異步化的線程模型、高性能的異步網絡傳輸、自定義的私有傳輸協議和序列化、反序列化等等,這些方法和優化技巧,你都可以在 Kafka 的源代碼中找到對應的實現。

在性能優化方面,除了這些通用的性能優化手段之外,Kafka 還有哪些“獨門絕技”呢?

使用批量消息提升服務端處理能力

我們知道,批量處理是一種非常有效的提升系統吞吐量的方法。在 Kafka 內部,消息都是以“批”爲單位處理的。一批消息從發送端到接收端,是如何在 Kafka 中流轉的呢?

我們先來看發送端,也就是 Producer 這一端。

在 Kafka 的客戶端 SDK(軟件開發工具包)中,Kafka 的 Producer 只提供了單條發送的send() 方法,並沒有提供任何批量發送的接口。原因是,Kafka 根本就沒有提供單條發送的功能,是的,你沒有看錯,雖然它提供的 API 每次只能發送一條消息,但實際上,Kafka的客戶端 SDK 在實現消息發送邏輯的時候,採用了異步批量發送的機制。

當你調用 send() 方法發送一條消息之後,無論你是同步發送還是異步發送,Kafka 都不會立即就把這條消息發送出去。它會先把這條消息,存放在內存中緩存起來,然後選擇合適的時機把緩存中的所有消息組成一批,一次性發給 Broker。簡單地說,就是攢一波一起發。在 Kafka 的服務端,也就是 Broker 這一端,又是如何處理這一批一批的消息呢?

在服務端,Kafka 不會把一批消息再還原成多條消息,再一條一條地處理,這樣太慢了。

Kafka 這塊兒處理的非常聰明,每批消息都會被當做一個“批消息”來處理。也就是說,在Broker 整個處理流程中,無論是寫入磁盤、從磁盤讀出來、還是複製到其他副本這些流程中,批消息都不會被解開,一直是作爲一條“批消息”來進行處理的

在消費時,消息同樣是以批爲單位進行傳遞的,Consumer 從 Broker 拉到一批消息後,在客戶端把批消息解開,再一條一條交給用戶代碼處理。

比如說,你在客戶端發送 30 條消息,在業務程序看來,是發送了 30 條消息,而對於Kafka 的 Broker 來說,它其實就是處理了 1 條包含 30 條消息的“批消息”而已。顯然處理 1 次請求要比處理 30 次請求要快得多。

構建批消息和解開批消息分別在發送端和消費端的客戶端完成,不僅減輕了 Broker 的壓力,最重要的是減少了 Broker 處理請求的次數,提升了總體的處理能力。

這就是 Kafka 用批量消息提升性能的方法。

我們知道,相比於網絡傳輸和內存,磁盤 IO 的速度是比較慢的。對於消息隊列的服務端來說,性能的瓶頸主要在磁盤 IO 這一塊。接下來我們看一下,Kafka 在磁盤 IO 這塊兒做了哪些優化。

使用順序讀寫提升磁盤IO性能

對於磁盤來說,它有一個特性,就是順序讀寫的性能要遠遠好於隨機讀寫。在 SSD(固態硬盤)上,順序讀寫的性能要比隨機讀寫快幾倍,如果是機械硬盤,這個差距會達到幾十倍。爲什麼呢?

操作系統每次從磁盤讀寫數據的時候,需要先尋址,也就是先要找到數據在磁盤上的物理位置,然後再進行數據讀寫。如果是機械硬盤,這個尋址需要比較長的時間,因爲它要移動磁頭,這是個機械運動,機械硬盤工作的時候會發出咔咔的聲音,就是移動磁頭髮出的聲音。

順序讀寫相比隨機讀寫省去了大部分的尋址時間,它只要尋址一次,就可以連續地讀寫下去,所以說,性能要比隨機讀寫要好很多。

Kafka 就是充分利用了磁盤的這個特性。它的存儲設計非常簡單,對於每個分區,它把從Producer 收到的消息,順序地寫入對應的 log 文件中,一個文件寫滿了,就開啓一個新的文件這樣順序寫下去。消費的時候,也是從某個全局的位置開始,也就是某一個 log 文件中的某個位置開始,順序地把消息讀出來。

這樣一個簡單的設計,充分利用了順序讀寫這個特性,極大提升了 Kafka 在使用磁盤時的IO 性能。

接下來我們說一下 Kafka 是如何實現緩存的。

利用PageCache加速消息讀寫

在 Kafka 中,它會利用 PageCache 加速消息讀寫。PageCache 是現代操作系統都具有的一項基本特性。通俗地說,PageCache 就是操作系統在內存中給磁盤上的文件建立的緩存。無論我們使用什麼語言編寫的程序,在調用系統的 API 讀寫文件的時候,並不會直接去讀寫磁盤上的文件,應用程序實際操作的都是 PageCache,也就是文件在內存中緩存的副本。

應用程序在寫入文件的時候,操作系統會先把數據寫入到內存中的 PageCache,然後再一批一批地寫到磁盤上。讀取文件的時候,也是從 PageCache 中來讀取數據,這時候會出現兩種可能情況。

一種是 PageCache 中有數據,那就直接讀取,這樣就節省了從磁盤上讀取數據的時間;另一種情況是,PageCache 中沒有數據,這時候操作系統會引發一個缺頁中斷,應用程序的讀取線程會被阻塞,操作系統把數據從文件中複製到 PageCache 中,然後應用程序再從PageCache 中繼續把數據讀出來,這時會真正讀一次磁盤上的文件,這個讀的過程就會比較慢。

用戶的應用程序在使用完某塊 PageCache 後,操作系統並不會立刻就清除這個PageCache,而是儘可能地利用空閒的物理內存保存這些 PageCache,除非系統內存不夠用,操作系統纔會清理掉一部分 PageCache。清理的策略一般是 LRU 或它的變種算法,這個算法我們不展開講,它保留 PageCache 的邏輯是:優先保留最近一段時間最常使用的那些 PageCache。

Kafka 在讀寫消息文件的時候,充分利用了 PageCache 的特性。一般來說,消息剛剛寫入到服務端就會被消費,按照 LRU 的“優先清除最近最少使用的頁”這種策略,讀取的時候,對於這種剛剛寫入的 PageCache,命中的機率會非常高。

也就是說,大部分情況下,消費讀消息都會命中 PageCache,帶來的好處有兩個:一個是讀取的速度會非常快,另外一個是,給寫入消息讓出磁盤的 IO 資源,間接也提升了寫入的性能。

ZeroCopy:零拷貝技術

Kafka 的服務端在消費過程中,還使用了一種“零拷貝”的操作系統特性來進一步提升消費
的性能。

我們知道,在服務端,處理消費的大致邏輯是這樣的:

  • 首先,從文件中找到消息數據,讀到內存中;
  • 然後,把消息通過網絡發給客戶端。

這個過程中,數據實際上做了 2 次或者 3 次複製:

  1. 從文件複製數據到 PageCache 中,如果命中 PageCache,這一步可以省掉;
  2. 從 PageCache 複製到應用程序的內存空間中,也就是我們可以操作的對象所在的內存;
  3. 從應用程序的內存空間複製到 Socket 的緩衝區,這個過程就是我們調用網絡應用框架的 API 發送數據的過程。

Kafka 使用零拷貝技術可以把這個複製次數減少一次,上面的 2、3 步驟兩次複製合併成一次複製。直接從 PageCache 中把數據複製到 Socket 緩衝區中,這樣不僅減少一次數據複製,更重要的是,由於不用把數據複製到用戶內存空間,DMA 控制器可以直接完成數據複製,不需要 CPU 參與,速度更快。

下面是這個零拷貝對應的系統調用:

#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

它的前兩個參數分別是目的端和源端的文件描述符,後面兩個參數是源端的偏移量和複製數據的長度,返回值是實際複製數據的長度。

如果你遇到這種從文件讀出數據後再通過網絡發送出去的場景,並且這個過程中你不需要對這些數據進行處理,那一定要使用這個零拷貝的方法,可以有效地提升性能。

總結

我們總結了 Kafka 的高性能設計中的幾個關鍵的技術點:

  • 使用批量處理的方式來提升系統吞吐能力。
  • 基於磁盤文件高性能順序讀寫的特性來設計的存儲結構。
  • 利用操作系統的 PageCache 來緩存數據,減少 IO 並提升讀性能。
  • 使用零拷貝技術加速消費流程。

以上這些,就是 Kafka 之所以能做到如此高性能的關鍵技術點。你可以看到,要真正實現一個高性能的消息隊列,是非常不容易的,你需要熟練掌握非常多的編程語言和操作系統的底層技術。
這些優化的方法和技術,同樣可以用在其他適合的場景和應用程序中。我希望你能充分理解這幾項優化技術的原理,知道它們在什麼情況下適用,什麼情況下不適用。這樣,當你遇到
合適場景的時候,再深入去學習它的細節用法,最終就能把它真正地用到你開發的程序中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章