Kafka 精妙的高性能設計(上篇)

大家好,我是武哥。

Kafka 的高性能設計可以說是全方位的,從 Prodcuer 、到 Broker、再到 Consumer,Kafka 在掏空心思地優化每一個細節,最終才做到了這樣的極致性能。

這篇文章我想先帶大家建立一個高性能設計的思維模式,然後再一探究竟 Kafka 的高性能設計方案,最終讓大家更體系地掌握所有知識點,並理解它的設計哲學。

 1. 如何理解高性能設計?  

我們暫且把 Kafka 拋在一邊,先嚐試理解下高性能設計的本質。

有過高併發開發經驗的同學,對於線程池、多級緩存、IO 多路複用、零拷貝等技術概念早就瞭然於胸,但是返璞歸真,這些技術手段的本質到底是什麼?

這其實是一個系統性的問題,至少需要深入到操作系統層面,從 CPU 和存儲入手,去了解底層的實現機制,然後再自底往上,一層一層去解密和貫穿起來。

但是站在更高的視角來看,我認爲:高性能設計其實萬變不離其宗,一定是從**「計算和 IO」**這兩個維度出發,去考慮可能的優化點。

那「計算」維度的性能優化手段有哪些呢?無外乎這兩種方式:

1、讓更多的核來參與計算:比如用多線程代替單線程、用集羣代替單機等。

2、減少計算量:比如用索引來取代全局掃描、用同步代替異步、通過限流來減少請求處理量、採用更高效的數據結構和算法等。

再看下「IO」維度的性能優化手段又有哪些? 可以通過 Linux 系統的 IO 棧圖來輔助思考。

圖 1:Linux 系統的 IO 棧圖

可以看到,整個 IO 體系結構是分層的,我們能夠從應用程序、操作系統、磁盤等各個層次來考慮性能優化,而所有這些手段又幾乎圍繞以下兩個方面展開:

1、加快 IO 速度:比如用磁盤順序寫代替隨機寫、用 NIO 代替 BIO、用性能更好的 SSD 代替機械硬盤等。

2、減少 IO 次數或者 IO 數據量:比如藉助系統緩存或者外部緩存、通過零拷貝技術減少 IO 複製次數、批量讀寫、數據壓縮等。

上面這些內容可以理解成高性能設計的**「道」**,當然絕不是幾百字就可以說清楚的,我更多的是拋磚引玉,用另外一個視角來看高併發,給大家一個方向上的指引。

當大家抓住了**「計算和 IO」**這兩個最本質的東西,然後以這兩點作爲根,再去探究這兩個維度分別有哪些性能優化手段?它們的原理又是什麼樣的?便能一層一層剝開高性能設計的神祕面紗,形成可靠的知識體系。

這種分析方法可用來研究 Kafka,同樣可以用來研究我們熟知的 Redis、ES 以及其他高性能的應用系統。

 2. Kafka 高性能設計的全景圖   

有了高性能設計的思維模式後,我們再回到 Kafka 本身進行分析。

前文提到過 Kafka 的性能優化手段非常豐富,至少有 10 條以上的精妙設計,雖然我們可以從計算和 IO 兩個維度去聯想這些手段,但是要完整地記住它們,似乎也不是件容易的事。

這樣就引出了另外一個話題:我們應該選用一條什麼樣的脈絡,去串聯這些優化手段呢?

之前的文章做過分析:不管 Kafka 、RocketMQ 還是其他消息隊列,其本質都是「一發一存一消費」。

我們完全可以順着這條主線去做結構化梳理。基於這個思路,便形成了下面這張 Kafka 高性能設計的全景圖,我按照生產消息、存儲消息、消費消息 3 個模塊,將 Kafka 最具代表性的 12 條性能優化手段做了歸類。

圖 2:Kafka 高性能設計的全景圖

有了這張全景圖,下面我再挨個分析下每個手段背後的大致原理,並嘗試解讀下 Kafka 的設計哲學。

 3. 生產消息的性能優化手段  

我們先從生產消息開始看,下面是 Producer 端所採用的 4 條優化手段。

1、批量發送消息

Kafka 作爲一個消息隊列,很顯然是一個 IO 密集型應用,它所面臨的挑戰除了磁盤 IO(Broker 端需要對消息持久化),還有網絡 IO(Producer 到 Broker,Broker 到 Consumer,都需要通過網絡進行消息傳輸)。

在上一篇文章已經指出過:磁盤順序 IO 的速度其實非常快,不亞於內存隨機讀寫。這樣網絡 IO 便成爲了 Kafka 的性能瓶頸所在。

基於這個背景, Kafka 採用了批量發送消息的方式,通過將多條消息按照分區進行分組,然後每次發送一個消息集合,從而大大減少了網絡傳輸的 overhead。

看似很平常的一個手段,其實它大大提升了 Kafka 的吞吐量,而且它的精妙之處遠非如此,下面幾條優化手段都和它息息相關。

2、消息壓縮

消息壓縮的目的是爲了進一步減少網絡傳輸帶寬。而對於壓縮算法來說,通常是:數據量越大,壓縮效果纔會越好。

因爲有了批量發送這個前期,從而使得 Kafka 的消息壓縮機制能真正發揮出它的威力(壓縮的本質取決於多消息的重複性)。對比壓縮單條消息,同時對多條消息進行壓縮,能大幅減少數據量,從而更大程度提高網絡傳輸率。

有文章對 Kafka 支持的三種壓縮算法:gzip、snappy、lz4 進行了性能對比,測試 2 萬條消息,效果如下:

圖 3:壓縮效果對比,來源:https://www.jianshu.com/p/d69e27749b00

整體來看,gzip 壓縮效果最好,但是生成耗時更長,綜合對比 lz4 性能最佳。

其實壓縮消息不僅僅減少了網絡 IO,它還大大降低了磁盤 IO。因爲批量消息在持久化到 Broker 中的磁盤時,仍然保持的是壓縮狀態,最終是在 Consumer 端做了解壓縮操作。

這種端到端的壓縮設計,其實非常巧妙,它又大大提高了寫磁盤的效率。

3、高效序列化

Kafka 消息中的 Key 和 Value,都支持自定義類型,只需要提供相應的序列化和反序列化器即可。因此,用戶可以根據實際情況選用快速且緊湊的序列化方式(比如 ProtoBuf、Avro)來減少實際的網絡傳輸量以及磁盤存儲量,進一步提高吞吐量。

4、內存池複用

前面說過 Producer 發送消息是批量的,因此消息都會先寫入 Producer 的內存中進行緩衝,直到多條消息組成了一個 Batch,纔會通過網絡把 Batch 發給 Broker。

當這個 Batch 發送完畢後,顯然這部分數據還會在 Producer 端的 JVM 內存中,由於不存在引用了,它是可以被 JVM 回收掉的。

但是大家都知道,JVM GC 時一定會存在 Stop The World 的過程,即使採用最先進的垃圾回收器,也勢必會導致工作線程的短暫停頓,這對於 Kafka 這種高併發場景肯定會帶來性能上的影響。

有了這個背景,便引出了 Kafka 非常優秀的內存池機制,它和連接池、線程池的本質一樣,都是爲了提高複用,減少頻繁的創建和釋放。

具體是如何實現的呢?其實很簡單:Producer 一上來就會佔用一個固定大小的內存塊,比如 64MB,然後將 64 MB 劃分成 M 個小內存塊(比如一個小內存塊大小是 16KB)。

當需要創建一個新的 Batch 時,直接從內存池中取出一個 16 KB 的內存塊即可,然後往裏面不斷寫入消息,但最大寫入量就是 16 KB,接着將 Batch 發送給 Broker ,此時該內存塊就可以還回到緩衝池中繼續複用了,根本不涉及垃圾回收。最終整個流程如下圖所示:

圖 4:Kafka 發送端的流程

瞭解了 Producer 端上面 4 條高性能設計後,大家一定會有一個疑問:傳統的數據庫或者消息中間件都是想辦法讓 Client 端更輕量,將 Server 設計成重量級,僅讓 Client 充當應用程序和 Server 之間的接口。

但是 Kafka 卻反其道而行之,採取了獨具一格的設計思路,在將消息發送給 Broker 之前,需要先在 Client 端完成大量的工作,例如:消息的分區路由、校驗和的計算、壓縮消息等。這樣便很好地分攤 Broker 的計算壓力。

可見,沒有最好的設計,只有最合適的設計,這就是架構的本源。

 4. 寫在最後  

Kafka 在創造一個以性能爲核心導向的解決方案上做得極其出色,它有非常多的設計理念值得深入研究和學習。

考慮篇幅問題,我將 Kafka 的高性能設計分成了上下兩篇,下一篇將繼續展開闡述剩餘 8 條高性能設計手段以及背後的設計思想。

看到這裏,我更希望大家能建立起高性能設計的思維模式以及學習方法,這些技巧同樣可以幫助你喫透其他高性能的中間件。

大家好,我是武哥,前亞馬遜工程師,現大廠技術管理者,持續分享個人的成長收穫,關注我一定能提升你的視野,讓我們一起進階吧!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/bEG7Xg3PwirQXZjMiX8glw