在眾多的消息中間件中,Kafka 的性能和吞吐量絕對是頂尖級別的,那么問題來了, Kafka 是如何做到高吞吐的。在性能優(yōu)化方面,它使用了哪些技巧呢?下面我們就來分析一下。
以'批'為單位
批量處理是一種非常有效的提升系統(tǒng)吞吐量的方法,操作系統(tǒng)提供的緩沖區(qū)也是如此。在 Kafka 內部,消息處理是以"批"為單位的,生產者、Broker、消費者,都是如此。
在 Kafka 的客戶端 SDK 中,生產者只提供了單條發(fā)送的 send() 方法,并沒有提供任何批量發(fā)送的接口。原因是 Kafka 根本就沒有提供單條發(fā)送的功能,是的你沒有看錯,雖然它提供的 API 每次只能發(fā)送一條消息,但實際上 Kafka 的客戶端 SDK 在實現消息發(fā)送邏輯的時候,采用了異步批量發(fā)送的機制。
當你調用 send() 方法發(fā)送一條消息之后,無論你是同步發(fā)送還是異步發(fā)送,Kafka 都不會立即就把這條消息發(fā)送出去。它會先把這條消息,存放在內存中緩存起來,然后選擇合適的時機把緩存中的所有消息組成一批,一次性發(fā)給 Broker。簡單地說,就是攢一波一起發(fā)。
而 Kafka Broker 在收到這一批消息后,也不會將其還原成多條消息、再一條一條地處理,這樣太慢了。Kafka 會直接將"批消息"作為一個整體,也就是說,在 Broker 整個處理流程中,無論是寫入磁盤、從磁盤讀出來、還是復制到其他副本,在這些流程中,批消息都不會被解開,而是一直作為一條"批消息"來進行處理的。
在消費時,消息同樣是以批為單位進行傳遞的,消費者會從 Broker 拉到一批消息。然后將批消息解開,再一條一條交給用戶代碼處理。
比如生產者發(fā)送 30 條消息,在業(yè)務程序看來雖然是發(fā)送了 30 條消息,但對于 Kafka 的 Broker 來說,它其實就是處理了 1 條包含 30 條消息的"批消息"而已。顯然處理 1 次請求要比處理 30 次請求快得多,因為構建批消息和解開批消息分別在生產者和消費者所在的客戶端完成,不僅減輕了 Broker 的壓力,最重要的是減少了 Broker 處理請求的次數,提升了總體的處理能力。
批處理只能算是一種常規(guī)的優(yōu)化手段,它是通過減少網絡 IO 從而實現優(yōu)化。而 Kafka 每天要處理海量日志,那么磁盤 IO 也是它的瓶頸。并且對于處在同一個內網的數據中心來說,相比讀寫磁盤,網絡傳輸是非??斓摹?br>
接下來我們看一下,Kafka 在磁盤 IO 這塊兒做了哪些優(yōu)化。
磁盤順序讀寫
我們知道 kafka 是將消息存儲在文件系統(tǒng)之上的,高度依賴文件系統(tǒng)來存儲和緩存消息,因此可能有人覺得這樣做效率是不是很低呢?因為要和磁盤打交道,而且使用的還是機械硬盤。
首先機械硬盤不適合隨機讀寫,但如果是順序讀寫,那么吞吐量實際上是不差的。在 SSD(固態(tài)硬盤)上,順序讀寫的性能要比隨機讀寫快幾倍,如果是機械硬盤,這個差距會達到幾十倍。因為操作系統(tǒng)每次從磁盤讀寫數據的時候,需要先尋址,也就是先要找到數據在磁盤上的物理位置,然后再進行數據讀寫。如果是機械硬盤,這個尋址需要比較長的時間,因為它要移動磁頭,這是個機械運動,機械硬盤工作的時候會發(fā)出咔咔的聲音,就是移動磁頭發(fā)出的聲音。
順序讀寫相比隨機讀寫省去了大部分的尋址時間,因為它只要尋址一次,就可以連續(xù)地讀寫下去,所以說性能要比隨機讀寫好很多。
而 kafka 正是利用了這個特性,任何發(fā)布到分區(qū)的消息都會被追加到 "分區(qū)數據文件" 的尾部,如果一個文件寫滿了,就創(chuàng)建一個新的文件繼續(xù)寫。消費的時候,也是從某個全局的位置開始,也就是某一個 log 文件中的某個位置開始,順序地把消息讀出來。這樣的順序寫操作讓 kafka 的效率非常高。
使用 PageCache
任何系統(tǒng),不管大小,如果想提升性能,使用緩存永遠是一個不錯的選擇,而 PageCache 就是操作系統(tǒng)在內存中給磁盤上的文件建立的緩存,它是由內核托管的。無論我們使用什么語言,編寫的程序在調用系統(tǒng)的 API 讀寫文件的時候,并不會直接去讀寫磁盤上的文件,應用程序實際操作的都是 PageCache,也就是文件在內存中緩存的副本。
應用程序在寫入文件的時候,操作系統(tǒng)會先把數據寫入到內存中的 PageCache,然后再一批一批地寫到磁盤上。讀取文件的時候,也是從 PageCache 中來讀取數據,這時候會出現兩種可能情況。
一種是 PageCache 中有數據,那就直接讀取,這樣就節(jié)省了從磁盤上讀取的時間;另一種情況是,PageCache 中沒有數據,這時候操作系統(tǒng)會引發(fā)一個缺頁中斷,應用程序的讀取線程會被阻塞,操作系統(tǒng)把數據從文件復制到 PageCache 中,然后應用程序再從 PageCache 繼續(xù)把數據讀出來,這時會真正讀一次磁盤上的文件,這個讀的過程就會比較慢。
用戶的應用程序在使用完某塊 PageCache 后,操作系統(tǒng)并不會立刻就清除這個 PageCache,而是盡可能地利用空閑的物理內存保存這些 PageCache,除非系統(tǒng)內存不夠用,操作系統(tǒng)才會清理掉一部分 PageCache。清理的策略一般是 LRU 或它的變種算法,核心邏輯就是:優(yōu)先保留最近一段時間最常使用的那些 PageCache。
另外 PageCache 還有預讀功能,假設我們讀取了 1M 的內容,但 Linux 實際讀取的卻并不止 1M,因為這樣你后續(xù)再讀取的時候就不需要從磁盤上加載了。因為從磁盤到內存的數據傳輸速度是很慢的,如果物理內存有空余,那么就可以多緩存一些內容。
而 Kafka 在讀寫消息文件的時候,充分利用了 PageCache 的特性。一般來說,消息剛剛寫入到服務端就會被消費,讀取的時候,對于這種剛剛寫入的 PageCache,命中的幾率會非常高。也就是說,大部分情況下,消費讀消息都會命中 PageCache,帶來的好處有兩個:一個是讀取的速度會非??欤硗庖粋€是,給寫入消息讓出磁盤的 IO 資源,間接也提升了寫入的性能。
ZeroCopy(零拷貝)
Kafka 還使用了零拷貝技術,首先 Broker 將消息發(fā)送給消費者的過程如下:
將指定的消息日志從文件讀到內存中;
將消息通過網絡發(fā)送給消費者客戶端;
這個過程會經歷幾次復制,以及用戶空間和內核空間的切換,示意圖如下。
整個過程大概是以上 6 個步驟,我們分別解釋一下。
1)應用程序要讀取磁盤文件,但只有內核才能操作硬件設備,所以此時會從用戶空間切換到內核空間。
2)通過 DMA 將文件讀到 PageCache 中,此時的數據拷貝是由 DMA 來做的,不耗費 CPU。關于 DMA,它是一種允許硬件系統(tǒng)訪問計算機內存的技術,說白了就是給 CPU 打工的,幫 CPU 干一些搬運數據的簡單工作。
CPU 告訴 DMA 自己需要哪些數據,然后 DMA 負責搬運到 PageCache,等搬運完成后,DMA 控制器再通過中斷通知 CPU,這樣就極大地節(jié)省了 CPU 的資源。
但如果要讀取的內容已經命中 PageCache,那么這一步可以省略。
3)將文件內容從 PageCache 拷貝到用戶空間中,因為應用程序在用戶空間,磁盤數據必須從內核空間搬運到用戶空間,應用程序才能操作它。注意:這一步的數據搬運不再由 DMA 負責,而是由 CPU 負責。
因為 DMA 主要用于硬件設備與內存之間的數據傳輸,例如從磁盤到 RAM,從 RAM 到網卡。雖然 DMA 可以減少 CPU 的負擔,但通常不用于內核空間和用戶空間之間的數據搬運,至于原因也很簡單:
操作系統(tǒng)需要保護內核空間,防止用戶程序直接訪問,以維護系統(tǒng)的安全和穩(wěn)定。通過 CPU 進行數據拷貝,操作系統(tǒng)可以控制哪些數據和資源可以被用戶程序訪問。
CPU 可以處理復雜的邏輯和任務調度,更適合執(zhí)行這種涉及系統(tǒng)安全和資源管理的任務。
在數據從內核空間傳輸到用戶空間的過程中,可能需要進行一些額外的處理,例如格式轉換、權限檢查等,這些都是 CPU 更擅長的。
另外用戶空間和內核空間的切換,本質上就是 CPU 的執(zhí)行上下文和權限級別發(fā)生了改變。
因此這一步會涉及用戶態(tài)和內核態(tài)之間的切換,和一個數據的拷貝。
4) 文件內容讀取之后,要通過網絡發(fā)送給消費者客戶端。而內核提供了一個 Socket 緩沖區(qū),位于用戶空間的應用程序在發(fā)送數據時,會先通過 CPU 將數據拷貝到內核空間的 Socket 緩沖區(qū)中,再由內核通過網卡發(fā)送給消費者。
同樣的,當數據從網絡到達時,也會先被放在 Socket 緩沖區(qū)中。應用程序從該緩沖區(qū)讀取數據,數據被拷貝到用戶空間。
所以應用程序在通過網絡收發(fā)數據時,其實都是在和 Socket 緩沖區(qū)打交道,具體的發(fā)送和接收任務都是由內核來做的,因為只有內核才能操作硬件設備。用戶空間的代碼要想與硬件設備交互,必須通過系統(tǒng)調用或操作系統(tǒng)提供的其它接口,然后由內核代為執(zhí)行。
所以通過網絡發(fā)送數據,會涉及一次數據的拷貝,以及用戶空間和內核空間的切換。因為 CPU 要將數據從用戶空間搬運到內核空間的 Socket 緩沖區(qū)中。
5) 內核要將 Socket 緩沖區(qū)里的數據通過網卡發(fā)送出去,于是再將數據從 Socket 緩沖區(qū)搬到網卡的緩沖區(qū)里面,而這一步搬運是由 DMA 來做的。只要不涉及用戶空間,大部分的數據搬運都可以由 DMA 來做,而一旦涉及到用戶空間,數據搬運就必須由 CPU 來做。
6) 發(fā)送完畢之后,再從內核空間切換到用戶空間,應用程序繼續(xù)干其它事情。
如果想要提升性能,那么關鍵就在于減少上下文切換的次數和數據拷貝的次數,因為用戶空間和內核空間的切換是需要成本的,至于數據拷貝就更不用說了。
而整個過程涉及了 4 次的上下文切換,因為用戶空間沒有權限操作磁盤或網卡,這些操作都需要交由操作系統(tǒng)內核來完成。而通過內核去完成某些任務的時候,需要使用操作系統(tǒng)提供的系統(tǒng)調用函數。而一次系統(tǒng)調用必然會發(fā)生兩次上下文切換:首先從用戶態(tài)切換到內核態(tài),當內核執(zhí)行完任務后,再切換回用戶態(tài)交由應用程序執(zhí)行其它代碼。
然后是數據拷貝,這個數據也被拷貝了 4 次,其中兩次拷貝由 DMA 負責,另外兩次由 CPU 負責。但很明顯,CPU 的兩次拷貝沒有太大必要,先將數據從 PageCache 拷貝到用戶空間,然后再從用戶空間拷貝到 Socket 緩沖區(qū)。既然這樣的話,那直接從 PageCache 拷貝到 Socket 緩沖區(qū)不行嗎。
如果文件在讀取之后不對它進行操作,或者說不對文件數據進行加工,只是單純地通過網卡發(fā)送出去,那么就沒必要到用戶空間這里繞一圈。
此時的 4 次上下文切換就變成了 2 次,因為系統(tǒng)調用只有 1 次。數據搬運也由 4 次變成了 3 次,所以總共減少了兩次上下文切換和一次數據拷貝。
而這種減少數據拷貝(特別是在用戶和內核之間的數據拷貝)的技術,便稱之為零拷貝。
Linux 內核提供了一個系統(tǒng)調用函數 sendfile(),便可以實現上面這個過程。
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd,
off_t *offset, size_t count);
out_fd 和 in_fd 均為文件描述符,分別代表要寫入的文件和要讀取的文件,offset 表示從文件的哪個位置開始讀,count 表示寫入多少個字節(jié)。返回值是實際寫入的長度。
當然像 Python、Java 都對 sendfile 進行了封裝,我們在使用 Python 進行 Socket 編程時,便可以使用該方法。
當然該方法會調用 os.sendfile(),它和 C 的 sendfile() 是一致的,如果是 Linux 系統(tǒng),那么不存在問題。如果是 Windows 系統(tǒng),os.sendfile() 則不可用,此時 Socket 的 sendfile 會退化為 send 方法。
然而目前來說,雖然實現了零拷貝,但還不是零拷貝的終極形態(tài)。我們看到 CPU 還是進行了一次拷貝,并且此時雖然不涉及用戶空間,但數據搬運依舊是 CPU 來做的。因為 DMA 主要負責硬件(例如磁盤或網卡)和內存的數據傳輸,但不適用于內存到內存的數據拷貝。
那么問題來了,數據文件從磁盤讀到 PageCache 之后,可不可以直接搬到網卡緩沖區(qū)里面呢?如果你的網卡支持 SG-DMA 技術,那么通過 CPU 將數據從 PageCache 拷貝到 socket 緩沖區(qū)這一步也可以省略。
你可以通過以下命令,查看網卡是否支持 SG(scatter-gather)特性:
[root@satori ~]# ethtool -k eth0 | grep scatter-gather
scatter-gather: on
tx-scatter-gather: on
tx-scatter-gather-fraglist: off [fixed]
Linux 內核從 2.4 版本開始起,對于那些支持 SG-DMA 技術的網卡,會進一步優(yōu)化 sendfile() 系統(tǒng)調用的過程,優(yōu)化后的過程如下:
DMA 將數據從磁盤拷貝到 PageCache;
將描述符和數據長度發(fā)送到 Socket 緩沖區(qū),網卡的 SG-DMA 控制器基于該信息直接將 PageCache 的數據拷貝到網卡緩沖區(qū)中;
整個過程如下:
此時便是零拷貝(Zero-copy)技術的終極形態(tài),因為我們沒有在內存層面去拷貝數據,也就是說全程沒有通過 CPU 來搬運數據,所有的數據都是通過 DMA 來進行傳輸的。
使用零拷貝技術只需要兩次上下文切換和數據拷貝,就可以完成文件的傳輸,因為它通過一次系統(tǒng)調用(sendfile 方法)將磁盤讀取與網絡發(fā)送兩個操作給合并了,從而降低了上下文切換次數。而且兩次的數據拷貝過程也不需要通過 CPU,都是由 DMA 來搬運。所以總體來看,零拷貝技術可以把文件傳輸的性能提高至少一倍以上。
但需要注意的是,零拷貝技術不允許進程對文件內容作進一步加工,比如壓縮數據再發(fā)送。如果希望對讀取的文件內容做額外的操作,那么就只能拷貝到用戶空間了。
另外當傳輸大文件時,不建議使用零拷貝,因為 PageCache 可能被大文件占據,而導致「熱點」小文件無法利用到 PageCache,并且大文件的緩存命中率也不高,因此這種情況建議繞過 PageCache。
使用 PageCache 的 IO 叫做緩存 IO,不使用 PageCache 的 IO 叫做直接 IO。
小結
以上我們就探討了 Kafka 為什么會有如此高的吞吐量,在處理海量數據時為什么這么快。核心就在于以下幾點:
1)消息是以 "批" 為單位的。
2)利用磁盤的順序讀寫遠遠快于隨機讀寫。
3)使用 PageCache。
4)使用零拷貝技術。
本文參考自:
極客時間《消息隊列高手課》
公眾號《小林 coding》
來自 ChatGPT 的回復
聯系客服