Kafka 線上效能調優

小得盈满發表於2024-04-18

Kafka 線上效能調優是一項綜合工程,不僅僅是 Kafka 本身,還應該從硬體(儲存、網路、CPU)以及作業系統方面來整體考量,首先我們要有一套生產部署方案,基於這套方案再進行調優,這樣就有了可靠的底層保證,才能保證 Kafka 叢集整體的穩定性。

1. 線上部署方案

1.1 作業系統

我們知道 Kafka 是用 Scala 和 Java 這兩種語言編寫的,而這兩種語言都依賴 JVM 執行,理論上和作業系統關係應該不大。但是不同作業系統底層的 API 是不同的,綜合來說採用 Linux 系列的發行版比 Windows 系統來說效能優勢更佳。

從 I/O 模型上來看,通常有阻塞 I/O、非阻塞 I/O、I/O 多路複用、事件驅動 I/O 以及 非同步 I/O 這 5 種,每種 I/O 模型都有適合的應用場景,比如 Java 中的 Socket 就有阻塞和非阻塞兩種使用模式,而 Linux 中的 select 就屬於 I/O 多路複用,epoll 其實像介於多路複用和事件驅動模型之間的一種模型。

Kafka 在客戶端底層使用了 Java 的 selector,而 selector 在不同平臺上的實現也不完全一樣,比如在 Linux 上的實現機制是 epoll,而在 Windows 上的實現機制是 select,由於 epoll 相比 select 的優勢很大,所以將 Kafka 部署在 Linux 上的 I/O 效能會更好。

然後就是網路傳輸效能上的差距,在 Linux 上 Kafka 會直接利用零複製的特性來提升傳輸的效能,但是在 Windows 上直到 JDK 8 Update 60 版本才能支援零複製的特性,所以 Linux 上的支援天然的就比較好。

最後是社群的支援,社群上的大部分 bug 修復都是建立在 Linux 環境之上的,因為絕大部分人的執行環境都是 Linux,Windows 上面的 bug 基本上不會修復或者速度奇慢,所以社群對 Linux 更友好。

綜合來看,生產環境一定要選擇 Linux 發行版來部署,Windows 只用於個人測試即可。

1.2 磁碟

Kafka 訊息儲存會佔用大量的磁碟空間,磁碟也是 Kafka 執行最重要的底層保證,Kafka 之所以效能這麼高,除了上面說的零複製技術之外,還有非常重要的一點是所有的訊息都是順序儲存的,即使是機械盤也能有出色的表現,因為對於機械硬碟來說順序讀取相比隨機讀取的優勢是碾壓性的,SSD 在順序讀取這塊雖然比機械硬碟更高,但是仍然算是一個數量級的,所以 SSD 更適合用在隨機讀寫頻繁的場景,例如資料庫搜尋方面。

當然由於 SSD 價格不斷降低,很多公司 Kafka 也都是跑在了 SSD 上,所以 SSD 對於 Kafka 來說不是必須,有條件可以用,否則用機械硬碟也是沒問題的,但是無論如何千萬不要使用 NFS 等網路儲存,否則丟資料的風險會比較大,穩定性也會大打折扣。

另外就是是否使用 RAID 的問題,RAID 的作用主要就是提供本地資料冗餘以及磁碟的負載均衡,但是 Kafka 本身已經提供了副本機制,所以對 RAID 的需求也不是太強,但是由於 RAID 可以在底層資料層面提供保障,所以也確實可以為 Kafka 帶來可靠性的提升,但是 Kafka 的副本仍然不可或缺。

所以綜上我們可以得出結論,生產環境直接使用普通機械硬碟並且建立 2 個或以上副本即可達到比較高的可靠性要求,如果有條件想進一步提升效能或可靠性可以選擇 SSD 或者 RAID。

1.3 儲存空間評估

Kafka 的儲存空間評估其實並不複雜,在叢集上會存在多個 Topic,每個 Topic 所佔用的容量評估的計算方法是完全一樣的,假如我們有一個 Topic,每天會接收 1 億條左右的訊息,每條訊息的大小大約 1KB,那麼我們來看一下怎麼評估所佔用的空間。

首先是每天 1 億條 1KB 的訊息佔用的空間大小為:100GB,那麼還要考慮到 Topic 下面的分割槽會存在副本以及訊息所保留的天數,假如這個 Topic 有兩個副本,並且儲存 7 天,那麼磁碟最大的佔用是:2 × 7 × 100GB = 1.4TB,另外 Kafka 還存在著索引資料和臨時檔案等,我們通常留出 10% 的冗餘,那麼總佔用為:1.54 TB。

另外就要考慮壓縮的因素了,如果我們的生產者開啟了資料壓縮,我們需要根據實際訊息測試一下壓縮比,假如我們的壓縮比測試結果是 1.2,那麼實際的空間佔用就是:1.28 TB。

單個 Topic 的容量就估計出來了,然後對每個 Topic 都按照同樣的方法估計出容量之後並求和得到 Kafka 叢集總的儲存大小,通常資料是均勻分佈的,我們用總的儲存佔用除以 Broker 節點數量,就得到了每個節點所使用的容量,最後我們就可以根據這個評估結果來合理分配硬體。

所以綜合來說,評估 Kafka 的儲存空間要考慮下面的幾個主要因素:

  1. Topic 數量
  2. 平均訊息數量
  3. 平均訊息大小
  4. 訊息儲存時間
  5. 副本數量
  6. 是否壓縮

充分考慮這些因素就可以合理評估 Kafka 的儲存空間了。

1.4 頻寬評估

Kafka 在實際使用中會大量使用網路進行訊息傳輸以及副本的同步,所以頻寬很容易出現瓶頸,出問題的機率往往也是比較高的,如果還涉及到跨網傳輸資料,那麼會更容易出現問題。

我們通常使用的網路環境是 1Gbps 千兆網路或者 10Gbps 萬兆網路兩種,而且千兆網路應該是最低配置,假如對於千兆網路要實現每小時 1TB 的業務處理,需要多少臺呢,我們下面來分析一下。

對於千兆網路來說,為了防止丟包並且保證系統其他的關鍵程序的正常使用,我們的可用頻寬按照 70% 來計算,也就是說 Kafka 最多可用 700Mbps 的資源。但是這個值是 Kafka 的最大值,我們不可能讓 Kafka 日常都跑在這個頻寬上,還要考慮到業務高峰期的佔用等,我們必須留出一部分冗餘,保守估計是 1/2 左右,因此 Kafka 的常規執行頻寬為:700/2 = 350Mbps。

那麼每小時傳輸 1TB 每秒的頻寬大約為:2330Mbps,那麼 Kafka 節點個數為:2330/350 = 6.66,節點個數必須向上舍入,因此需要 7 個節點。

如果我們此時需要建立 3 個副本,那麼共需要:3*7 = 21 個節點才能完成要處理資料的目標,看起來這個值比較大,可能存在 CPU 過剩的問題,而事實上 Kafka 確實是 I/O 密集的,CPU 通常也是夠用的,所以我們可以透過增加頻寬來適當地減少節點個數,當然我們推薦在生產環境中使用萬兆網路頻寬。

有了上面的規劃和準備,在這個基礎之上,我們就可以來著手進行 Kafka 叢集的整體最佳化了。

2. 作業系統引數最佳化

作業系統是保證 Kafka 叢集正常執行的關鍵因素,不過 Kafka 對作業系統本身的最佳化並沒有很強的依賴性,也就是說預設情況下的引數也是夠用的,我們這裡只需要調整幾個特別重要的引數即可。

2.1 最大檔案數

這個引數是資料系統中最重要的引數之一,因為作業系統預設只給允許程序同時開啟 1024 個檔案,這個數量顯然有些小了,而且網路連線也佔用檔案控制代碼,如果程序開啟的總檔案控制代碼數超出了限制那麼就會報錯:Too many open files.,因此我麼有必要將這個引數調大,檢視當前的設定值:

ulimit -n

預設應該是 1024,如果臨時調大可以使用命令:

ulimit -n 65535

不過這個調整是臨時的,只有在當前會話中啟動的程序才是有效的。

那麼我們怎麼確定一個正在執行的程序所能用的最大檔案數量是多少呢?可以先拿到程序的 PID,然後檢視其對應的狀態資料:

cat /proc/$PID/limits

這樣就可以看到當前程序所能開啟的最大檔案數量限制了。

如果我們想實現在使用者會話建立後使得最大檔案數配置自動生效,可以有下面兩種方法。

第一個方法是藉助登入時的 PAM 設定來實現,在使用 SSH 連線時如果開啟了 PAM 策略,就可以實現一系列的限制,比如 fail2ban、會話保持時長等,預設情況下是自動為 SSH 開啟 PAM 的,可以透過配置檔案 /etc/ssh/sshd_config 來確認。

UsePAM yes

然後可以檢視 PAM 關於登入的配置檔案 /etc/pam.d/login 其中會直接配置或者透過 include 的相關檔案來間接配置了 PAM limits:

session required pam_limits.so

預設情況下這些都是會配置上的,只有在出現問題時我們才考慮排查這些設定。

關於 PAM limits 的配置檔案需要編輯 /etc/security/limits.conf 然後增加配置如下:

# * 表示設定所有的使用者的檔案數 (注意: root使用者除外)
* soft nofile 1000000
* hard nofile 1000000
# 設定root使用者的檔案數
root soft nofile 1000000
root hard nofile 1000000

這裡我們設定最大檔案數的軟限制和硬限制都是 100 萬,設定好之後儲存這個配置。

需要特別注意的是這個值一定不能超過核心引數 fs.nr_open 的設定,這個值在 Linux 核心程式碼中被定義為 1048576 差不多就是 100 萬多一點,設定前可以執行下面的命令確認:

sysctl fs.nr_open

超出這個值會導致後面無法登入系統,只能透過救援模式修改回來,所以務必謹慎。

關於 fs.nr_open 核心引數在 Linux 核心原始碼中的定義部分如下:

unsigned int sysctl_nr_open __read_mostly = 1024*1024;
unsigned int sysctl_nr_open_min = BITS_PER_LONG;
/* our min() is unusable in constant expressions ;-/ */
#define __const_min(x, y) ((x) < (y) ? (x) : (y))
unsigned int sysctl_nr_open_max =
	__const_min(INT_MAX, ~(size_t)0/sizeof(void *)) & -BITS_PER_LONG;

這個值預設是 1024*1024 也就是 1048576,並且最大值受限於 sysctl_nr_open_max 的值,這個值結果是 2147483584,當設定超過這個值的時候也會報錯。在較新的 Linux 發行版中 fs.nr_open 的值會被修改為 1073741816,總之 fs.nr_open 核心引數不需要修改,只需要在設定之前確認不要超過即可。

最後儲存完上面的配置後,重新建立 SSH 連線登入會話即可生效,然後就可以啟動 Kafka 服務了。

第二種方法比較簡單,我們透過登入時設定當前使用者的環境變數即可,可以修改 ~/.bashrc 配置檔案並新增內容:

ulimit -HSn 1000000

其中 -H-S 分別指定硬限制和軟限制,儲存後重新開啟會話也可以生效。

另外要注意的是如果程序採用 Systemd 來管理,那麼不受上面會話引數配置的影響,由具體的 Systemd 服務檔案來配置,上面這些引數只能用於在當前會話中手動啟動的程序,比如 Kafka 通常採用 kafka-server-start.sh 指令碼來啟動就是可以的。

2.2 交換分割槽

交換分割槽是磁碟上的一塊空間,可以在實體記憶體的空間不足時,將一些不太常用的頁面換出到交換分割槽,從而支援更多的程序執行,但是由於磁碟和記憶體的效能差距不在一個數量級,交換分割槽一旦使用頻繁系統執行將被嚴重拖慢。

一方面當我們檢視核心程序 kswapd0 的佔用比較高,說明記憶體已經開始換入換出了,需要排查是哪個程序佔用了大量的記憶體,是記憶體確實不夠用還是應用程式設計缺陷導致。

另外我們也可以檢視程序的狀態檔案來確認是否佔用了交換分割槽:

grep '^Swap:' /proc/$PID/smaps

如果發現存在不少的分割槽佔用,可以彙總看下一共有多少佔用:

grep '^Swap:' /proc/$PID/smaps | awk '{sum+=$2}END{print sum}'

如果確實存在不少的佔用,可以進一步分析下程序的換頁率:

# -B 檢視記憶體頁面統計 1 秒顯示一次,共顯示 10 次
sar -B 1 10

如果發現 pgpgin/s 或者 pgpgout/s 數值比較高,那麼就要小心了,這會導致程序的執行被拖慢。

現在伺服器的記憶體通常都比較大,對於 Kafka 其實大量的記憶體都是 page cache 的佔用,雖然記憶體是夠用的,但是作業系統仍然會有使用交換分割槽的情況,所以為了保證執行的效能,建議將交換分割槽關閉掉或者降低交換分割槽的使用傾向。

如果是降低交換分割槽使用的權重可以透過調整核心引數 vm.swappiness 實現:

sysctl -w vm.swappiness=0 >> /etc/sysctl.conf
# 檢視修改的檔案內容
sysctl -p

預設情況下 vm.swappiness 的值是 60,調整為 0 之後就可以很大程度降低交換分割槽的使用,當實體記憶體確實不夠用的時候,作業系統仍然會使用交換分割槽,徹底關閉交換分割槽可以先執行命令:

swapoff -a

執行這個命令後作業系統會將交換分割槽的資料都換到記憶體中,所以如果交換分割槽有佔用那麼命令執行會比較緩慢,執行完成後我們編輯 /etc/fstab 從中刪除掉交換分割槽自動掛載的條目,那麼下次機器再重啟時就不會掛載交換分割槽了。

2.3 檔案系統

檔案系統方面建議選擇比較主流成熟的檔案系統,目前推薦 ext4 或者 XFS,建議使用 XFS,因為 XFS 具備高效能和高伸縮性等特點,更加適合於生產環境中。

不過根據比較新的研究結果,使用 ZFS 作為 Kafka 的底層儲存可以取得更好的效果,比如 ZFS 的多級快取機制可以幫助 Kafka 改善 I/O 效能,不過這只是處於實驗室的研究,我們瞭解即可,目前生產環境還沒辦法很好地使用。

另外還可以在掛載裝置時關閉 atime 記錄,atime 其實就是 access time,預設掛載情況下每次訪問檔案後檔案系統都會進行記錄從而更新 atime,比如使用 cat 訪問檔案就會更新 atime,記錄 atime 需要額外訪問 inode 資訊,如果禁用掉 atime 就可以避免每次訪問檔案後寫入時間的操作,從而在一定程度上提高檔案系統的效能:

mount -o noatime /dev/sdX /data

2.4 程序 VMA(記憶體區域)數量

程序的VMA(虛擬記憶體區域),其實就是程序透過 mmap 等系統呼叫建立的虛擬記憶體空間,當然也包括檔案對映I/O,但是作業系統預設對程序所能使用的虛擬記憶體數量是有限制的,預設值是 65530,可以透過命令檢視當前的值:

sysctl vm.max_map_count

如果在 Kafka 叢集中存在非常多的 Topic,如果 VMA 的數量超過了限制可能會報錯 OutOfMemory Error: Map failed ,這其實是記憶體溢位的另外一種情況。

因此我們在生產環境建議調大此核心引數的配置:

sysctl -w vm.max_map_count=2048000 >> /etc/sysctl.conf
# 檢視當前的設定
sysctl -p

3. 最重要的叢集引數配置

3.1 JVM 引數

首先 Kafka 是執行在 JVM 上的,所以 JVM 的合理設定對於 Kafka 的正常執行至關重要,首先是 JDK 版本,建議最低使用 JDK 1.8 版本,因為從 Kafka 2.0 開始已經摒棄了對 JDK 1.7 的支援,所以在生產環境中至少使用 JDK 1.8 版本,建議使用 JDK 11,這樣可以帶來更多的最佳化。

然後就是最重要的 JVM 堆大小引數,現代伺服器的記憶體都比較大,64GB、128GB 都非常常見,但是 JVM 堆記憶體不建議設定太大,因為 Kafka 本身並不會佔用非常大的記憶體,相反 Kafka 會充分利用作業系統的檔案系統快取來加速檔案的讀寫,因此大部分的記憶體應該留給檔案系統快取。不過預設的情況下 Kafka 的堆記憶體只有 1GB,這確實也有點小了,所以我們直接建議將堆記憶體設定為 6GB,這個值是業界公認的一個合理值,Kafka Broker 在和客戶端通訊時會在堆上建立很多 ByteBuffer 的例項,所以在大吞吐的前提下還是需要不少 JVM 空間的,修改方法是編輯啟動指令碼 kafka-server-start.sh 修改其中的 KAFKA_HEAP_OPTS 變數即可:

if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
    export KAFKA_HEAP_OPTS="-Xmx6G -Xms6G"
fi

然後啟動 Kafka 服務就可以了。

除了堆記憶體大小,還有比較重要的一塊就是垃圾收集器部分,當然在 Kafka 2.5 中會自動設定 JVM 引數開啟 G1GC,如果是 JDK 11 則預設情況下的垃圾回收器就是 G1GC,我們無需做任何配置,我們可以檢視程序的啟動引數或者檢視程序當前的 JVM 引數:

jinfo -flags $PID

如果我們看到 JVM 引數中沒有開啟 G1GC,我們可以修改 Kafka 的指令碼 kafka-run-class.sh 在其中新增下面的變數即可:

export KAFKA_JVM_PERFORMANCE_OPTS="-server -XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35 -XX:+ExplicitGCInvokesConcurrent -Djava.awt.headless=true"

這樣我們就可以使用 G1GC 了。

總的來說 Kafka 正常並不需要對 JVM 做過多的最佳化,主要注意下面幾點即可:

  1. 最低要使用 JDK 1.8 版本,推薦用 JDK 11
  2. 設定堆記憶體大小為 6GB
  3. 使用 G1GC 垃圾回收器

3.2 Broker 端引數

Kafka Broker 端有眾多的引數需要配置,但是大部分只需保持預設即可,有一些非常重要的引數需要我們來修改,我們來重點看下這些引數,下面的配置都是基於 Kafka 2.5 版本進行調優,其餘版本需要具體參考相關的文件。

首先是埠和通訊的監聽配置,這主要涉及下面兩個引數:

  1. listeners 這個配置 Broker 要監聽的列表,預設情況下的值為 PLAINTEXT://:9092 如果主機名留空則會繫結到預設的網路介面,其實也就是全部的網路介面,然後後面跟要監聽的埠號,預設是 9092。
  2. advertised.listeners 這個配置釋出到 ZooKeeper 供客戶端使用的監聽器,當這個配置和 listeners 不同時會用到,預設如果沒有設定會和 listeners 的值保持一致。

通常我們在生產環境中 listeners 保持預設即可,建議客戶端都使用和叢集一致的主機名進行訪問,如果客戶端使用 IP 地址訪問,而客戶端本地又沒有配置 hosts 時就會出現剛接觸 Kafka 常見的主機無法解析的錯誤,這是因為在沒有配置 advertised.listeners 的情況下其配置和 listeners 一致,這樣當客戶端發起連線時 ZooKeeper 會返回類似於 PLAINTEXT://<hostname>:9092 這樣的地址,但是本地又找不到這個地址,所以就會出現報錯,這種情況下我們可以在客戶端也放一份和 Kafka Broker 上一樣的 hosts 檔案,或者在 Kafka 中配置:

advertised.listeners=PLAINTEXT://10.1.0.2:9092

這樣相當於告訴 ZooKeeper 返回的是 Kafka Broker 的 IP 地址,這樣客戶端不需要任何修改就可以正常生產消費資料了。

另外還有一種情況是 Kafka Broker 和客戶端不在一個網段,是透過在路由上配置了 NAT 的方式進行訪問,這種方式也會使得客戶端訪問的網路地址和 Kafka Broker 區域網的 IP 地址不相同,也需要透過合理設定 advertised.listeners 實現正常訪問。

另外還有一些引數,比如像 advertised.host.nameadvertised.porthost.name 還有 port 引數都已經廢棄了,直接忽略存在即可,只配置上面兩個引數就可以了。

然後我們再來看 Kafka 訊息儲存的配置,這個同樣也是有兩個可配置的引數:

  1. log.dirs 這個是非常重要的引數,這個指定了 Kafka Broker 儲存資料的目錄列表,注意這個是支援多個目錄配置的。
  2. log.dir 這個同樣是配置 Kafka Broker 儲存資料的目錄,不過這個只能配置單個路徑

第一個引數 log.dirs 是沒有預設值的,如果不配置的話會使用 log.dir 的配置,也就是 /tmp/kafka-logs 。不過在 Kafka 的配置檔案中預設已經給了 log.dirs 配置值 /tmp/kafka-logs ,所以我們在配置過程中只需要關心 log.dirs 而忽略 log.dir 就可以了。

通常我們會準備單獨的資料儲存硬碟給 Kafka 使用,如果只配置一個目錄的話 Kafka 和早期版本是一樣的,就是將資料分割槽分段直接寫入磁碟。

如果配置多個目錄的話,比如:

log.dirs=/data1/kafka-logs,/data2/kafka-logs,/data3/kafka-logs

這樣 Kafka 會將分割槽均勻分散到多個目錄中,相當於 RAID 0 寫入,這樣可以進一步提高資料寫入和讀取的 I/O。

從 Kafka 1.1 開始還支援了強大的故障轉移功能,假如我們配置的多個儲存盤有其中 1 個盤壞掉了,那麼 Kafka 會自動從其他可用的分割槽拉取這部分壞掉的副本,並寫入其他可用的儲存目錄,使得整體可用的副本數量和 Topic 建立時指定的保持一致。有了這種能力就相當於在之前副本的基礎上進一步增加了容錯性,這樣的話即使底層不使用 RAID 也可以滿足基本的儲存可靠性。

配置完儲存後我們再來看下 ZooKeeper 的配置,Kafka 依賴 ZooKeeper 管理後設資料並實現 Leader 的選舉等功能,是 Kafka 比較重要的底層支撐,不過配置確實比較簡單,通常只需要配置 zookeeper.connect 即可,例如:

zookeeper.connect=zk1:2181,zk2:2181,zk3:2181/kafka

首先是 ZooKeeper 的節點儘量要寫全,另外就是儘量配置 znode,比如 /kafka ,如果有多套 Kafka 叢集為了後設資料互不影響可以配置多個 znode 來隔離,例如:/kafka1/kafka2 等。

然後我們再來看一下 Topic 相關的配置引數,這裡主要有下面的 4 個引數:

  1. auto.create.topics.enable 是否允許 Kafka Broker 自動建立 Topic。
  2. unclean.leader.election.enable 是否允許 Unclean Leader 選舉。
  3. auto.leader.rebalance.enable 是否啟用自動 Leader 平衡。
  4. offsets.topic.replication.factor 用於管理偏移的 Topic 副本數量。

我們依次來看,首先是 auto.create.topics.enable 這個引數,這個引數預設設定是 true ,表示 Kafka Broker 允許自動建立不存在的 Topic,我們有時候會發現線上的 Kafka 環境中經常會有一大堆 Topic,都不確定哪些有用哪些沒用。有時候一不小心 Topic 名稱拼寫錯了,只要啟動生產者後就會自動建立,這樣會導致叢集上出現很多稀奇古怪的 Topic。

另外,如果我們沒有配置預設的分割槽和副本數量,那麼建立出來的 Topic 可能只有 1 個分割槽並且不存在副本,線上都執行很長時間了等到效能出現瓶頸後經過排查發現只有 1 個分割槽,這種情況往往是還沒有建立 Topic 就開始跑生產者導致 Kafka Broker 自動建立了 Topic,而這個 Topic 的引數大機率不是最優的。

綜合以上問題來看,真正用於生產環境的 Kafka 是不允許隨意建立 Topic 的情況出現的,這會帶來很多的不好的影響,往往是之後導致問題的根源,所以這個引數我們建議設定為 false ,一定要明確先建立 Topic 然後才能使用。

然後看第二個引數 unclean.leader.election.enable 這個引數的含義之前關於 Kafka 分片和副本的文件已經詳細解釋過了,如果開啟雖然會提升可用性,但是也可能造成更多的資料丟失,幸好在 Kafka 中這個引數預設值就是 false ,我們一般不用關心,但是這個引數的含義我們應該充分理解,生產環境為了避免出現問題我們甚至可以顯式設定為 false

然後來看 auto.leader.rebalance.enable 這個引數我們並不常見,但是對生產環境其實是有一定的影響的,預設情況下這個引數是 true ,表示 Kafka 會定期對分配不均的 Leader 進行重新選舉。具體實現是由後臺執行緒定期檢查所有分割槽中 Leader 的分佈,檢查的週期由引數 leader.imbalance.check.interval.seconds 配置,預設是 300s,如果節點間分配不平衡的程度超過引數 leader.imbalance.per.broker.percentage 的設定,這個引數預設是 10,表示 10%,則會重新執行選舉。

但是事實上,線上的 Kafka 如果節點出現故障,Leader 會重新選舉到其他的節點,如果故障節點恢復後會自動成為 Follower,所以隨著叢集的執行,Leader 的分佈會不斷偏離初始情況下的均勻狀態,這個引數就是透過選舉來恢復初始的平衡狀態,從而減小 Leader 比較多的節點的壓力。但是仍然會存在很多節點 Leader 執行的狀態比較好,但是仍然被強行換成了其他節點,而更換 Leader 的過程中所有的生產者和消費者都會阻塞完成這個變更,所以更換 Leader 的代價還是非常高的,最主要的是會造成業務的暫時中斷,所以對於生產環境建議將該引數設定為 false 關閉 Leader 平衡操作,或者將檢查週期調長以及將不平衡的比例調大,儘量降低 Leader 重平衡帶來的影響。

最後一個引數是 offsets.topic.replication.factor 這個參數列示偏移管理 Topic 的副本數量,預設情況下使用 Kafka 消費者 API 時,如果選擇讓 Kafka 來儲存偏移,那麼 Kafka 會建立一個 Topic 叫做 __consumer_offsets 並使用這個 Topic 來管理不同消費者組的偏移量,這個 Topic 採用 compact 策略儲存,也就是每個 Topic 的消費者組只會儲存一份最新的偏移,所以這個 Topic 副本數量建議設定大一些,從而保證穩定性。預設這個引數的值是 3,但是在 Kafka 配置檔案中顯式設定成了 1,這可能是考慮到如果 Kafka 是單節點的,那麼只能是設定 1 個,如果設定多了是無法消費的,只能是滿足了節點的個數才可以。

我們有時候在消費 Kafka 的時候其中一個節點出現了故障,雖然我們消費的 Topic 存在副本,但是有可能我們始終消費不到某個分割槽的資料,最終導致消費出現不均衡,另外我們重啟消費者程序之後有可能會出現直接阻塞無法消費到資料的情況。對於這個情況來說,有可能就是偏移管理 Topic 的副本設定為 1 導致的,而我們要消費的 Topic 對應消費組的偏移恰好落在故障的節點上,但是由於這些節點故障而之前儲存的偏移又沒有副本,只要故障的節點沒有恢復那麼這個偏移就永遠獲取不了,消費者也就無法消費資料了,我們檢視 __consumer_offsets 這個 Topic 的詳情其實就可以發現問題。

所以對於這個引數的值我們推薦如下:

  1. 如果 Kafka 是單節點的,那麼 offsets.topic.replication.factor 值只能設定為 1
  2. 如果 Kafka 有兩個節點,那麼 offsets.topic.replication.factor 的值要設定為 2
  3. 如果 Kafka 有 3 個或 3 個以上節點,那麼 offsets.topic.replication.factor 的值至少設定為 3

對於節點比較多的 Kafka 叢集來說,偏移管理 Topic 佔的空間相比訊息來說非常小,所以副本設定大一些是沒有什麼問題的。

最後我們再來看一下訊息儲存和傳輸方面的幾個引數:

  1. log.retention.{hours|minutes|ms} 這其實是 3 個引數,都是配置預設情況下 Topic 中訊息儲存的時間,從優先順序來看是 ms > minutes > hours ,預設情況下 msminutes 都是 null ,只有 hours 預設是 168,也就是 7 天的時間。
  2. log.retention.bytes 這個引數限制磁碟能儲存訊息的總大小,預設是 -1,表示不限制大小。
  3. message.max.bytes 這個引數限制 Broker 能接受的最大訊息大小,預設是 1048588,大約是 1MB
  4. num.replica.fetchers 這個表示 Follower Replica 從 Leader Replica 複製訊息的執行緒數,預設是 1。

其中第一個引數我們通常設定 log.retention.hours 就足夠了,一般不需要太精細的控制,這個值預設是 168h,也就是儲存 7 天,這個要根據我們實際的需要來配置儲存時長。

第二個引數 log.retention.bytes 預設是 -1,說明 Kafka 並不主動限制訊息的總大小,但是假如我們磁碟空間比較緊張,在指定的儲存時間內或者高峰期資料增長容易導致磁碟佔滿,這種情況下最好做一下限制,防止磁碟寫滿後出現故障。

第三個引數是 message.max.bytes 這個值預設是 1048588,那麼為什麼不是 1048576 呢?我們可以算一下這之間差了 12 個位元組,這就是訊息頭部所佔用的空間大小,包括:8 位元組的 offset + 4 位元組的訊息長度,正好佔用 12 位元組,其餘的才是訊息本身的內容,所以訊息本身最大限制是 1MB。對於很多業務場景 1MB 的訊息限制可能是不夠的,所以這個要根據我們實際的業務需要進行調整。

最後的引數是 num.replica.fetchers 這個值預設是 1,如果是訊息 TPS 非常高的情況下,一個執行緒複製可能會跟不上,時間長了有可能會被移出 ISR,所以如果我們機器的 CPU 不是太繁忙,建議適當增大該值,從而提高複製資料的吞吐,使得副本同步更加實時,但是這個值不要超過 CPU 的核數,對於 16 核及以上的機器,大部分情況設定為 8 就足夠了。

以上就是比較重要的 Broker 端引數,合理設定這些引數在大多數情況下都可以保證叢集必要的可用性和穩定性。

3.3 Topic 引數

除了 Broker 引數之外,Kafka 還支援為不同的 Topic 設定不同的引數值,Topic 引數比 Broker 引數少很多,大約也就 26 個左右,但是 Topic 引數的優先順序比 Broker 引數的更高,這樣就可以對不同的 Topic 實現不同的引數設定,適應更加靈活的場景,這就是 Topic 引數主要的意義。

那麼下面我們來看一下比較重要且常用的幾個的 Topic 引數:

  1. retention.ms 設定該 Topic 的訊息儲存時長,預設和全域性一致,當設定這個值之後將會覆蓋全域性的值。
  2. retention.bytes 設定該 Topic 所能使用的最大磁碟空間,預設和全域性一致,當設定之後會單獨為這個 Topic 設定最大可用空間。
  3. max.message.bytes 設定該 Topic 所能接收的訊息最大長度,這個設定同樣會覆蓋全域性的設定。如果 Kafka 叢集承載的業務不同,那麼在全域性上可能給不出一個比較合適的限制,這時候就可以根據每個 Topic 的業務特性來獨立限制訊息的大小。

關於 Topic 最常用的引數就是上面幾個,更多的引數可以參考文件瞭解,比如 Kafka 2.5 版本的引數可以參考:https://kafka.apache.org/25/documentation.html#topicconfigs

那麼下面我們來看下怎麼去設定這些引數,從命令列工具來說有兩種方式可以設定:

  1. 在建立 Topic 時設定
  2. 先建立 Topic,然後再修改設定

如果是建立 Topic 時設定如下:

bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic testMessage --partitions 3 --replication-factor 2 --config retention.ms=15552000000 --config max.message.bytes=5242880

這樣透過新增 --config 引數就可以完成設定,建立後檢視 Topic 詳情就可以看到我們的引數了。

有很大一部分情況是已經存在了 Topic,我們這時候要動態調整 Topic 的設定,那麼這時候可以使用 kafka-configs.sh 工具調整 Topic 的設定:

bin/kafka-configs.sh --zookeeper localhost:2181/kafka --entity-type topics --entity-name testMessage --alter --add-config max.message.bytes=10485760

這樣也可以完成 Topic 配置的調整,不過要注意的是這裡需要指定 ZooKeeper 的地址。

4. Kafka 效能調優進階

在有了上面硬體、作業系統、JVM 以及叢集引數配置的基礎之上,我們的 Kafka 叢集通常可以獲得比較好的效能,但是仍然有一些可以持續最佳化的方向或者一些需要注意的細節,這些屬於比較進階的調優,我們一起來看一下。

4.1 零複製

關於零複製技術以及 Kafka 是如何利用零複製技術的,在 Kafka 分片和副本機制中已經做了敘述,這裡不再詳細贅述,我們只需要注意一點:保持客戶端版本和 Broker 版本一致,這樣就可以獲得足夠的效能收益。

image

比如其中的綠色表示快速通道,也就是會利用 Zero Copy 的特性實現高吞吐,但是如果使用舊版本的消費者客戶端,那麼訊息需要進行轉換,因此就必須複製到 JVM 的堆記憶體中進行中轉,所以就走了其中紅色的慢速通道。

所以不需要太多的最佳化,我們只需要嚴格讓客戶端版本和 Broker 版本保持一致,就可以充分發揮 Kafka 的效能優勢了。

4.2 應用層調優思路

應用層調優其實沒有固定的方法,大多數情況都和具體的業務邏輯相關,但是仍然是有一些公共的方法是可以遵守的:

  1. 不要頻繁地建立 Producer 或 Consumer 物件例項,這樣會帶來額外的開銷,我們應該儘量複用他們。
  2. 用完後要記得關閉,無論是 Producer 還是 Consumer 例項,底層都會包括 Socket、ByteBuffer 等資源,如果程式長期執行而又沒有及時關閉的話,可能會造成資源洩漏。
  3. 合理利用多執行緒來提升效能,比如在大部分程式語言中,Producer 例項是執行緒安全的,我們可以放心地在多個執行緒中共享這個例項。但是 Consumer 不是執行緒安全的,但是可以開啟多個執行緒,每個執行緒中建立獨立的例項,這樣就可以實現並行的處理,但是同一個消費者組下執行緒數量不能超過 Topic 的分割槽數量。

4.3 吞吐量調優

我們大部分人其實對吞吐量和延時的概念存在一定的誤解,例如我們的 Kafka 傳送一條訊息需要 2ms,那麼延時也就是 2ms,那麼吞吐量就是 500 條/s,所以簡單地將吞吐量 = 1000/Latency(ms),事實上我們假如一條一條的傳送資料這種計算方式是沒問題的,或者這種計算方式更常用在 Web API 呼叫的時候計算使用,但是對於資料系統來說不是這麼簡單的計算關係。

假如我們的 Producer 每次不是傳送 1 條訊息,而是快取一批訊息再傳送,比如 10ms 快取了 1000 條訊息,傳送這 1000 條用了 10ms,那麼此時的時延大約是 20ms,而吞吐量則變成了:1000/20 * 1000 = 50000 條/s,延時增加 10 倍,但是吞吐確增加了 100 倍,這其實就是利用計算機系統中經典的攤銷思想或者批次化(batch)的思想,在每次網路的必要開銷下,傳送更多的訊息以實現更大的吞吐。實際上在作業系統的程序排程中,每次上下文切換是存在開銷的,所以作業系統分配恰當的時間片長度,使得上下文切換的開銷在 CPU 總時間中佔用比較小,這樣就像額外的開銷分攤到非常多的始終週期中,從而讓程式有更多的時間利用 CPU 資源。

所以 Kafka 的 Producer 就是這麼設計的,是透過犧牲很小一部分延遲,來換取 TPS 也就是吞吐的顯著提升。我們呼叫一次生產訊息的操作其實是先寫到本地的緩衝區,這個速度是非常快的,然後 Producer 客戶端定期透過非同步的方式將緩衝區的資料寫入網路中,這樣就可以獲得較好的吞吐效能。

另外如果生產者設定了引數 acks=all ,那麼需要等待所有副本都同步成功才認為訊息生產成功,那麼這個時候副本同步效能就和生產者的效能相關聯了,這種情況下,可以按照前面說的增大副本拉取的並行執行緒數量 num.replica.fetchers 從而獲得更好的生產效能。所以在 Producer 端如果允許我們儘量不要設定成 acks=all ,而是設定 acks=1 或者 acks=0 可以獲得更好的效能。

另外在 Producer 端還有兩個引數需要注意:

  1. batch.size 這個是傳送到同一個分割槽訊息的批次大小限制,預設為 16KB。
  2. linger.ms 當批次大小沒有達到 batch.size 的限制時,最大允許多長時間的延遲傳送資料。

其中 batch.size 預設是 16KB,這個比較小,很容易就滿了,所以實際生產環境中我們可以調大該引數的值,例如改為 512KB。linger.ms 預設值是 0,也就是表示無延遲,這顯然太小了,所以我們可以調大,比如設定為 20ms,這表示訊息的最大的傳送延遲,這樣可以減少請求的傳送次數,從而提升吞吐量。

如果 Producer 例項在多執行緒環境中共享,並且訊息寫入比較頻繁,那麼我們需要調大引數 buffer.memory 這個值預設是 32MB,當寫入的訊息速度大於傳送的速度時,如果緩衝區寫滿,那麼生產者會阻塞 max.block.ms 時間,然後將丟擲異常,預設是 60s,異常內容大約是 TimeoutException:Failed to allocate memory within the configured max blocking time ,這個時候我們可以考慮增加 buffer.memory 的值,前提是網路傳送最終是可以跟得上到來速度的,這樣就可以提供給到來的訊息充足的空間來使用。

最後在生產者端還可以開啟壓縮來降低對網路的壓力,推薦使用 LZ4 和 zstd 壓縮演算法,這也是 Kafka 適配比較好的兩款演算法。

最後來看一下 Consumer 端,Consumer 端最簡單的方式是使用多執行緒來提升吞吐,同時我們可以透過併發佇列連線比較複雜的業務處理過程,讓消費者執行緒本身只接收資料,保證消費的效能,後續處理可以採用不對等的自定義執行緒數量來實現其他的業務邏輯,可以調的引數其實並不多,主要注意引數 fetch.min.bytes 的配置,這個引數預設是 1B,也就是說只要 Broker 端有資料,就可以立即返回給消費者端,這個確實是比較小,所以我們可以調大該值,讓 Broker 端多返回一些資料給我們,但是這會導致 Broker 不斷等待新資料的到來,直到攢到配置的大小才可以返回,所以同樣是在犧牲一部分延遲的情況下提升吞吐。

最後我們總結一下吞吐量的調優思路:

  1. 如果 Producer 端設定了 acks=all ,那麼要提高 Broker 端 num.replica.fetchers 引數的值;否則儘量設定 acks=1 或者 acks=0
  2. 調大 Producer 端 batch.size 的值實現更大的批次。
  3. 調大 Producer 端的 linger.ms 提高等待時間,降低網路開銷。
  4. 多執行緒共享 Producer 的場景下調大 buffer.memory 的值,避免緩衝區佔滿。
  5. Producer 端開啟壓縮引數 compression.type=lz4 或者 compression.type=zstd
  6. Consumer 端調大 fetch.min.bytes 的值,提升消費的吞吐量。

如果反過來我們只對延時敏感,而不是太在意吞吐的話,根據上面的思路也很容易進行最佳化了:

  1. Broker 端增大 num.replica.fetchers 的值
  2. Producer 端設定 acks=1
  3. Producer 端設定 linger.ms=0
  4. 關閉壓縮,即設定 compression.type=none
  5. 在 Consumer 端設定 fetch.min.bytes=1

首先在 Broker 端我們加快 Follower Replica 同步的實時性,在 Producer 端我們希望訊息儘可能快的傳送出去,因此不壓縮、不等待,只需要 Leader 確認即可。在 Consumer 端我們希望訊息儘可能快的到達,所以只要 Broker 端有資料就可以立即獲取到,降低消費的延遲。

透過上面的調優,我們就可以將訊息的延遲最佳化到極致。不過生產環境中對吞吐量的調優更多一些,只關注延遲的情況比較少,畢竟 Kafka 作為一個訊息系統在應對海量資料的場景下我們更願意透過少量的延遲增加來提升整體系統的吞吐能力。

相關文章