Kafka 是一個(gè)基于發(fā)布-訂閱模式的消息系統(tǒng),它可以在多個(gè)生產(chǎn)者和消費(fèi)者之間傳遞大量的數(shù)據(jù)。Kafka 的一個(gè)顯著特點(diǎn)是它的高吞吐率,即每秒可以處理百萬級(jí)別的消息。那么 Kafka 是如何實(shí)現(xiàn)這樣高得性能呢?本文將從七個(gè)方面來分析 Kafka 的速度優(yōu)勢(shì)。
-
零拷貝技術(shù) -
僅可追加日志結(jié)構(gòu) -
消息批處理 -
消息批量壓縮 -
消費(fèi)者優(yōu)化 -
未刷新的緩沖寫入 -
GC 優(yōu)化
以下是對(duì)本文中使用得一些英文單詞解釋:
Broker:Kafka 集群中的一臺(tái)或多臺(tái)服務(wù)器統(tǒng)稱 broker
Producer:消息生產(chǎn)者
Consumer:消息消費(fèi)者
zero copy:零拷貝
1. 零拷貝技術(shù)
零拷貝技術(shù)是指在讀寫數(shù)據(jù)時(shí),避免將數(shù)據(jù)在內(nèi)核空間和用戶空間之間進(jìn)行拷貝,而是直接在內(nèi)核空間進(jìn)行數(shù)據(jù)傳輸。對(duì)于 Kafka 來說,它使用了零拷貝技術(shù)來加速磁盤文件的網(wǎng)絡(luò)傳輸,以提高讀取速度和降低 CPU 消耗。下圖說明了數(shù)據(jù)如何在生產(chǎn)者和消費(fèi)者之間傳輸,以及零拷貝原理。
步驟 1.1~1.3:生產(chǎn)者將數(shù)據(jù)寫入磁盤
步驟 2:消費(fèi)者不使用零拷貝方式讀取數(shù)據(jù)
2.1:數(shù)據(jù)從磁盤加載到 OS 緩存
2.2:將數(shù)據(jù)從 OS 緩存復(fù)制到 Kafka 應(yīng)用程序
2.3:Kafka 應(yīng)用程序?qū)?shù)據(jù)復(fù)制到 socket 緩沖區(qū)
2.4:將數(shù)據(jù)從 socket 緩沖區(qū)復(fù)制到網(wǎng)卡
2.5:網(wǎng)卡將數(shù)據(jù)發(fā)送給消費(fèi)者
步驟 3:消費(fèi)者以零拷貝方式讀取數(shù)據(jù)
3.1:數(shù)據(jù)從磁盤加載到 OS 緩存
3.2:OS 緩存通過 sendfile() 命令直接將數(shù)據(jù)復(fù)制到網(wǎng)卡
3.3:網(wǎng)卡將數(shù)據(jù)發(fā)送到消費(fèi)者
可以看到,零拷貝技術(shù)避免了多余得兩步操作,數(shù)據(jù)直接從OS 緩存復(fù)制到網(wǎng)卡再到消費(fèi)者。這樣做的好處是極大地提高了I/O效率,降低了CPU和內(nèi)存的消耗。
2. 僅可追加日志結(jié)構(gòu)
Kafka 中存在大量的網(wǎng)絡(luò)數(shù)據(jù)持久化到磁盤(生產(chǎn)者到代理)和磁盤文件通過網(wǎng)絡(luò)發(fā)送(代理到消費(fèi)者)的過程。這一過程的性能會(huì)直接影響 Kafka 的整體吞吐量。為了優(yōu)化 Kafka 的數(shù)據(jù)存儲(chǔ)和傳輸,Kafka 采用了一種僅可追加日志結(jié)構(gòu)方式來持久化數(shù)據(jù)。僅可追加日志結(jié)構(gòu)是指將數(shù)據(jù)以順序追加(Append-only)的方式寫入到文件中,而不是進(jìn)行隨機(jī)寫入或更新。這樣做的好處是可以減少磁盤 I/O 的開銷,提高寫入速度。
人們普遍認(rèn)為磁盤的讀寫速度很慢,但實(shí)際上存儲(chǔ)介質(zhì)(尤其是旋轉(zhuǎn)介質(zhì))的性能很大程度上取決于訪問模式。常見的 7,200 RPM SATA 磁盤上的隨機(jī) I / O 的性能要比順序 I / O 慢 3 ~ 4 個(gè)數(shù)量級(jí)。此外,現(xiàn)代操作系統(tǒng)提供了預(yù)讀和延遲寫入技術(shù),可以預(yù)先取出大塊的數(shù)據(jù),并將較小的邏輯寫入組合成較大的物理寫入。因此,即使在閃存和其他形式的固態(tài)非易失性介質(zhì)中,隨機(jī) I/O 和順序 I/O 的差異仍然很明顯,盡管與旋轉(zhuǎn)介質(zhì)相比,這種差異性已經(jīng)很小了。
3. 消息批處理
Kafka 的高吞吐率設(shè)計(jì)的核心要點(diǎn)之一是批處理,即 Kafka 在消息發(fā)送端和接收端都引入了一個(gè)緩沖區(qū),將多條消息打包成一個(gè)批次(Batch),然后一次性發(fā)送或接收。這樣做的好處是可以減少網(wǎng)絡(luò)請(qǐng)求的次數(shù),減少了網(wǎng)絡(luò)壓力,提高了傳輸效率。
Kafka 的消息批處理優(yōu)化主要涉及以下幾個(gè)方面:
發(fā)送端(Producer)
Kafka 的 Producer 只提供了單條發(fā)送的 send()方法,并沒有提供任何批量發(fā)送的接口。當(dāng)調(diào)用 send()方法發(fā)送一條消息之后,無論是同步還是異步發(fā)送,這條消息不會(huì)立即發(fā)送出去,而是先放入到一個(gè)雙端隊(duì)列中,然后 Kafka 使用一個(gè)異步線程從隊(duì)列中成批發(fā)送消息。
Kafka 提供了以下幾個(gè)參數(shù)來控制發(fā)送端的批處理策略:
-
batch.size:指定每個(gè)批次可以收集的消息數(shù)量的最大值。默認(rèn)是 16KB。 -
buffer.memory:指定每個(gè) Producer 可以使用的緩沖區(qū)內(nèi)存的總量。默認(rèn)是 32MB。 -
linger.ms:指定每個(gè)批次可以等待的時(shí)間的最大值。默認(rèn)是 0ms。 -
compression.type:指定是否對(duì)每個(gè)批次進(jìn)行壓縮,以及使用哪種壓縮算法。默認(rèn)是 none。
接收端(Broker)
Kafka 的 Broker 在接收到 Producer 發(fā)送過來的批次后,不會(huì)把批次再還原成多條消息,而是直接將整個(gè)批次寫入到磁盤中。這樣做的好處是可以減少磁盤 I/O 的開銷,提高寫入速度。
Kafka 利用了操作系統(tǒng)提供的內(nèi)存映射文件(memory mapped file)功能,將文件映射到內(nèi)存中,使得對(duì)文件的讀寫操作就相當(dāng)于對(duì)內(nèi)存的讀寫操作。這樣就避免了用戶空間和內(nèi)核空間之間的數(shù)據(jù)拷貝,也避免了系統(tǒng)調(diào)用的開銷。
消費(fèi)端(Consumer)
Kafka 的 Consumer 在從 Broker 拉取數(shù)據(jù)時(shí),也是以批次為單位進(jìn)行傳遞的。Consumer 從 Broker 拉到一批消息后,客戶端把批次解開,再一條一條交給用戶代碼處理。
Kafka 提供了以下幾個(gè)參數(shù)來控制消費(fèi)端的批處理策略:
-
fetch.min.bytes:指定每次拉取請(qǐng)求至少要獲取多少字節(jié)的數(shù)據(jù)。默認(rèn)是 1B。 -
fetch.max.bytes:指定每次拉取請(qǐng)求最多能獲取多少字節(jié)的數(shù)據(jù)。默認(rèn)是 50MB。 -
fetch.max.wAIt.ms:指定每次拉取請(qǐng)求最多能等待多長時(shí)間。默認(rèn)是 500ms。 -
max.partition.fetch.bytes:指定每個(gè)分區(qū)每次拉取請(qǐng)求最多能獲取多少字節(jié)的數(shù)據(jù)。默認(rèn)是 1MB。
4. 消息批量壓縮
消息批量壓縮通常與消息批處理一起使用。Kafka 會(huì)將多個(gè)消息打包成一個(gè)批次(Batch),并對(duì)批次進(jìn)行壓縮(例如使用 gzip 或 snappy 算法),然后再發(fā)送給消費(fèi)者。這樣做的好處是可以節(jié)省網(wǎng)絡(luò)帶寬,提高傳輸效率。
當(dāng)然,壓縮也有一定的代價(jià),即需要消耗 CPU 資源來進(jìn)行壓縮和解壓縮。但是對(duì)于 Kafka 這樣的高吞吐量的系統(tǒng)來說,網(wǎng)絡(luò)帶寬往往是更大的瓶頸,所以壓縮是值得的。
Kafka 還提供了一種靈活的壓縮策略,即可以讓生產(chǎn)者、代理和消費(fèi)者之間協(xié)商壓縮格式和級(jí)別。生產(chǎn)者可以選擇是否對(duì)消息進(jìn)行壓縮,以及使用哪種壓縮算法;代理可以選擇是否保留生產(chǎn)者壓縮的消息,或者對(duì)其進(jìn)行重新壓縮;消費(fèi)者可以選擇是否對(duì)收到的消息進(jìn)行解壓縮。這樣可以根據(jù)不同的場(chǎng)景和需求來平衡性能和資源的消耗。
5. 消費(fèi)者優(yōu)化
Kafka 的消費(fèi)者是基于拉模式(pull)的,即消費(fèi)者主動(dòng)向服務(wù)器請(qǐng)求數(shù)據(jù),而不是服務(wù)器主動(dòng)推送數(shù)據(jù)給消費(fèi)者。這樣做的好處是可以讓消費(fèi)者自己控制消費(fèi)的速度和時(shí)機(jī),也可以減輕服務(wù)器的負(fù)擔(dān),提高整體的吞吐量。
Kafka 的消費(fèi)者所實(shí)現(xiàn)的功能是比較簡(jiǎn)潔的,即它們不需要維護(hù)太多的狀態(tài)和資源,也不需要和服務(wù)器進(jìn)行復(fù)雜的交互。Kafka 的消費(fèi)者只需要做以下幾件事:
-
訂閱一個(gè)或多個(gè)主題(topic),并加入一個(gè)消費(fèi)者組(consumer group)。向群組協(xié)調(diào)器(group coordinator)發(fā)送心跳,表明自己還活著,并參與分區(qū)再均衡(partition rebalance)。 -
向分區(qū)所在的代理(broker)發(fā)送拉取請(qǐng)求(fetch request),獲取消息數(shù)據(jù)。 -
提交自己消費(fèi)到的偏移量(offset),以便在出現(xiàn)故障時(shí)恢復(fù)消費(fèi)位置。
可以看到,Kafka 的消費(fèi)者并不需要保存消息數(shù)據(jù),也不需要對(duì)消息進(jìn)行確認(rèn)或回復(fù),也不需要處理重試或重復(fù)的問題。這些都由服務(wù)器端來負(fù)責(zé)。Kafka 的消費(fèi)者只需要關(guān)注如何從服務(wù)器獲取數(shù)據(jù),并進(jìn)行業(yè)務(wù)處理即可。
6. 未刷新的緩沖寫入
Kafka 在寫入數(shù)據(jù)時(shí),使用了一種未刷新(flush)的緩沖寫入技術(shù),即它不會(huì)立即將數(shù)據(jù)寫入硬盤,而是先寫入內(nèi)存緩存中,然后由操作系統(tǒng)在適當(dāng)?shù)臅r(shí)候刷新到硬盤上。這樣做的好處是可以提高寫入速度,減少磁盤 I/O 的開銷。
Kafka 利用了操作系統(tǒng)提供的內(nèi)存映射文件(memory mapped file)功能,將文件映射到內(nèi)存中,使得對(duì)文件的讀寫操作就相當(dāng)于對(duì)內(nèi)存的讀寫操作。這樣就避免了用戶空間和內(nèi)核空間之間的數(shù)據(jù)拷貝,也避免了系統(tǒng)調(diào)用的開銷。
當(dāng)生產(chǎn)者向 Kafka 發(fā)送消息時(shí),Kafka 會(huì)將消息追加到內(nèi)存映射文件中,并返回一個(gè)確認(rèn)給生產(chǎn)者。此時(shí)消息并沒有真正寫入硬盤,而是由操作系統(tǒng)負(fù)責(zé)將內(nèi)存中的數(shù)據(jù)刷新到硬盤上。操作系統(tǒng)會(huì)根據(jù)一些策略來決定何時(shí)刷新數(shù)據(jù),例如定期刷新、緩存滿了刷新、系統(tǒng)空閑時(shí)刷新等。
當(dāng)然,這種技術(shù)也有一定的風(fēng)險(xiǎn),即如果操作系統(tǒng)在刷新數(shù)據(jù)之前發(fā)生崩潰或斷電,那么內(nèi)存中未刷新的數(shù)據(jù)就會(huì)丟失。為了解決這個(gè)問題,Kafka 提供了一些參數(shù)來控制刷新策略,例如:
-
log.flush.interval.messages:指定多少條消息后強(qiáng)制刷新數(shù)據(jù)。 -
log.flush.interval.ms:指定多少毫秒后強(qiáng)制刷新數(shù)據(jù)。 -
producer.type:指定生產(chǎn)者是同步還是異步模式。同步模式下,生產(chǎn)者會(huì)等待服務(wù)器刷新數(shù)據(jù)后再返回確認(rèn);異步模式下,生產(chǎn)者不會(huì)等待服務(wù)器刷新數(shù)據(jù),而是立即返回確認(rèn)。
7. GC 優(yōu)化
Kafka 作為一個(gè) JAVA 編寫得高性能的分布式消息系統(tǒng),它需要處理大量的數(shù)據(jù)讀寫和網(wǎng)絡(luò)傳輸。這些操作都會(huì)涉及到 Java 虛擬機(jī)(JVM)的內(nèi)存管理和垃圾回收(GC)機(jī)制。如果 GC 不合理或不及時(shí),就會(huì)導(dǎo)致 Kafka 的性能下降,甚至出現(xiàn)內(nèi)存溢出或頻繁的停頓。為了幫助使用者優(yōu)化 GC,Kakfa 有如下建議。
堆內(nèi)存大小
堆內(nèi)存是 JVM 用來存儲(chǔ)對(duì)象實(shí)例的內(nèi)存區(qū)域,它會(huì)受到 GC 的管理和回收。堆內(nèi)存的大小會(huì)影響 Kafka 的性能和穩(wěn)定性,如果堆內(nèi)存太小,就會(huì)導(dǎo)致頻繁的 GC,影響吞吐量和延遲;如果堆內(nèi)存太大,就會(huì)導(dǎo)致 GC 時(shí)間過長,影響響應(yīng)速度和可用性。
通常來說,Kafka 并不需要設(shè)置太大的堆內(nèi)存,因?yàn)樗饕蕾囉诓僮飨到y(tǒng)的文件緩存(page cache)來緩存和讀寫數(shù)據(jù),而不是將數(shù)據(jù)保存在堆內(nèi)存中。因此 Kafka 建議將堆內(nèi)存大小設(shè)置為 4GB 到 6GB 之間。
堆外內(nèi)存大小
堆外內(nèi)存是 JVM 用來存儲(chǔ)非對(duì)象實(shí)例的內(nèi)存區(qū)域,它不會(huì)受到 GC 的管理和回收。堆外內(nèi)存主要用于網(wǎng)絡(luò) I/O 緩沖區(qū)、直接內(nèi)存映射文件、壓縮庫等。
Kafka 在進(jìn)行網(wǎng)絡(luò) I/O 時(shí),會(huì)使用堆外內(nèi)存作為緩沖區(qū),以減少數(shù)據(jù)在用戶空間和內(nèi)核空間之間的拷貝。同時(shí),Kafka 在進(jìn)行數(shù)據(jù)壓縮時(shí),也會(huì)使用堆外內(nèi)存作為臨時(shí)空間,以減少 CPU 資源的消耗。
因此,堆外內(nèi)存對(duì)于 Kafka 的性能也很重要,如果堆外內(nèi)存不足,就會(huì)導(dǎo)致緩沖區(qū)分配失敗或壓縮失敗,影響吞吐量和延遲。通常來說,Kafka 建議將堆外內(nèi)存大小設(shè)置為 8GB 左右。
GC 算法和參數(shù)
GC 算法是 JVM 用來回收無用對(duì)象占用的堆內(nèi)存空間的方法,它會(huì)影響 Kafka 的停頓時(shí)間和吞吐量。GC 算法有多種選擇,例如串行 GC、并行 GC、CMS GC、G1 GC 等。
不同的 GC 算法有不同的優(yōu)缺點(diǎn)和適用場(chǎng)景,例如串行 GC 適合小型應(yīng)用和低延遲場(chǎng)景;并行 GC 適合大型應(yīng)用和高吞吐量場(chǎng)景;CMS GC 適合大型應(yīng)用和低停頓時(shí)間場(chǎng)景;G1 GC 適合大型應(yīng)用和平衡停頓時(shí)間和吞吐量場(chǎng)景等。
通常來說,Kafka 建議使用 G1 GC 作為默認(rèn)的 GC 算法,因?yàn)樗梢栽诒WC較高吞吐量的同時(shí),控制停頓時(shí)間在 200ms 以內(nèi)。同時(shí),Kafka 還建議根據(jù)具體情況調(diào)整一些 GC 參數(shù),例如:
-
-XX:MaxGCPauseMillis:指定最大停頓時(shí)間目標(biāo),默認(rèn)是 200ms。 -
-XX:InitiatingHeapOccupancyPercent:指定觸發(fā)并發(fā)標(biāo)記周期的堆占用百分比,默認(rèn)是 45%。 -
-XX:G1ReservePercent:指定為拷貝存活對(duì)象預(yù)留的空間百分比,默認(rèn)是 10%。 -
-XX:G1HeapRegionSize:指定每個(gè)堆區(qū)域的大小,默認(rèn)是 2MB。
本文參考
-
https://medium.com/swlh/why-kafka-is-so-fast-bde0d987cd03 -
https://blog.bytebytego.com/p/why-is-kafka-fast -
https://blog.csdn.NET/csdnnews/article/details/104471147
總結(jié)
最后感謝大家閱讀,希望本文能對(duì)你有所幫助。