千億級、大規模:騰訊超大 Apache Pulsar 集羣性能調優實踐

語言: CN / TW / HK

關於作者

鮑明宇

騰訊高級軟件工程師,目前就職於騰訊 TEG 數據平台部,負責 Apache Pulsar、Apache Inlong、DB 數據採集等項目的開發工作。目前專注於大數據領域,消息中間件、大數據數據接入等方向,擁有 10 年 Java 相關開發經驗。

張大偉

騰訊高級軟件工程師,Apache Pulsar Committer,目前就職於騰訊 TEG 數據平台部,主要負責 Apache Pulsar 項目相關工作。目前專注於 MQ 和數據實時處理等領域,擁有 6 年大數據平台相關開發經驗。

關於 Apache Pulsar

雲原生時代消息隊列和流融合系統,提供統一的消費模型,支持消息隊列和流兩種場景,既能為隊列場景提供企業級讀寫服務質量和強一致性保障,又能為流場景提供高吞吐、低延遲;採用存儲計算分離架構,支持大集羣、多租户、百萬級 Topic、跨地域數據複製、持久化存儲、分層存儲、高可擴展性等企業級和金融級功能。

GitHub 地址:http://github.com/apache/pulsar/

導讀

近期,騰訊 TEG 數據平部 MQ 團隊開發部署了一套底層運維指標性能分析系統(本文簡稱 Data 項目) ,目前作為通用基礎設施服務整個騰訊集團。該系統旨在收集性能指標、上報數據以用於業務的運維監控,後續也將延用至前後端實時分析場景。

騰訊 Data 項目選用 Apache Pulsar 作為消息系統,其服務端採用 CVM 服務器(Cloud Virtual Machine,CVM)部署,並將生產者和消費者部署在 Kubernetes 上,該項目 Pulsar 集羣是騰訊數據平台部 MQ 團隊接入的消息量最大的 Pulsar 集羣。在整個項目中,我們在 Apache Pulsar 大規模集羣運維過程中遇到了一些問題和挑戰。本文將對這些問題展開描述分析,並分享對應處理方案,同時也會解析涉及到的相關 Apache Pulsar 設計原理。

希望本文能夠對面臨同類場景的用户與開發者提供參考。

業務消息量大,對生產與消費耗時指標敏感

Data 項目的業務場景,具有非常明顯的特點。首先,業務系統運行過程中,消息的生產、消費量都非常大,而且生產消息的 QPS(每秒查詢率)波動性不明顯,即業務會在近乎固定的 QPS 生產和消費數據。

其次,業務方對系統的可靠性、生產耗時、消費耗時這幾個指標項比較敏感,需要在比較低的延遲基礎上完成業務處理流程。像 Data 項目這樣的業務場景和需求,對集羣的部署、運營和系統穩定性都提出了非常高的要求。

Data 項目集羣最大的特點是消息量大、節點多,一個訂閲裏可高達數千消費者。雖然 Data 項目當前 Topic 總量並不多,但單個 Topic 對應的客户端比較多,每個分區要對應 100+ 個生產者和 10000+個消費者。在對消息系統選型時,團隊將消息系統的低延遲、高吞吐設為關鍵指標。經過綜合對比市面上常見的消息系統,Apache Pulsar 憑藉其功能和性能勝出。

Apache Pulsar 提供了諸多消費模型如獨佔、故障轉移、共享(Shared)和鍵共享(Key_Shared),其中在 Key_Shared 和 Shared 訂閲下可以支撐大量消費者節點。其他消息流系統如 Kafka,因為消費者節點受限於分區個數,導致其在多分區時性能相對較低。(編者注: Pulsar 和 Kafka 的最新性能測評,敬請期待 StreamNative 即將發佈的 2022 版報告)

超大 Pulsar 集羣:單分區最大消費者數量超8K

目前 Data 項目業務數據接入兩套 Pulsar 集羣,分為 T-1 和 T-2。其中,T-1 對接的業務的客户端 Pod(分為生產者和消費者,且不在同一個 Pod 上,部署在騰訊雲容器化平台 (STKE) ,與 Pulsar 集羣在相同機房;T-2 對接業務的客户端 Pod 與 Pulsar 集羣不在相同的機房(注:機房之間的數據時延相比同機房內部略高)。

服務器側相關參數

| 序號 | 參數 | 詳情 | 備註 | | -- | ----- | ---- | ---- | | 1 | 集羣 數| 2 | T-1/ T-2 | | 2 | 機器數量 | 100+台/集羣 | CVM IT5(非物理機/64 核/256G 內存/ 4 SSD) | | 3 | Broker 數量 | 100+ | 2.8.1.2(內部版本),部署三台 Discovery 服務 | | 4 | Bookie 數量 | 100+ | | | 5 | Discovery 服務 | 3/集羣 | 與 Bookie 混合部署在同一台機器上面 | | 6 | ZooKeeper 數量 | 5/每集羣 | 3.6.3 版本,單獨部署,使用 SA2.4XLARGE32 機型 | | 7 | 部署方式 | Broker+Bookie 混合部署在同一台機器上面 | | | 8 | Topic 數 | 3/集羣 | | | 9 | 分區數 | 100+/Topic | | | 10 | 消息副本數 | 2 | 副本寫入策略:E=5, W=2, A=2 | | 11 | 消息保存時長 | 1 天 | Retention\TTL 均配置為 1 天 | | 12 | namespace 數 | 3 | 每個 namespace下一個 Topic | | 13 | 當前消息量/天(按照每條消息大小 4k 均值評估) | 千億 /天/集羣 | | | 14 | 當前消息量/分鐘 | 千萬/分鐘 | |

業務側相關參數

Data 項目業務側使用 Go 語言開發,接入 Pulsar 集羣使用 Pulsar Go Client 社區 Master 分支的最新版本。使用雲梯 STKE 容器方式部署。

| 序號 | 參數 | 描述 | 備註 | | -- | ------ | ------ | ----------------------- | | 1 | 單分區最大生產者數量 | 150左右 | | | 2 | 單分區最大消費者數量 | 1w個左右 | 單分區這個量,服務器端需要維護大量的元數據信息 | | 3 | 客户端接入方式 | Go SDK | 目前使用 Master 分支最新代碼 | | 4 | 生產者 Pod 數量 | 150左右 | | | 5 | 消費者 Pod 數量 | 1w個左右 | | | 6 | 客户端部署平台 | STKE | 騰訊內部的騰訊雲容器服務平台 |

本文接下來將介紹 Pulsar 客户端在多種場景下的性能調優,分別針對項目在使用 Pulsar 的過程中遇到的客户端生產超時、客户端頻繁斷開等情況進行原因解析,並提供我們的解決方案,供大家參考。

客户端性能調優:問題與方案

調優一:客户端生產超時,服務器端排查

在大集羣下,導致客户端生產消息耗時較長或生產超時的原因有很多,我們先來看幾個服務器端的原因,包括:

  • 消息確認信息過大(確認空洞)
  • Pulsar-io 線程卡死
  • Ledger 切換耗時過長
  • BookKeeper-io 單線程耗時過長
  • DEBUG 級別日誌影響
  • Topic 分區數據分佈不均\

接下來,針對每個可能的服務器端原因,我們逐個進行解析。

解析 1:消費確認信息過大(確認空洞)

與 Kafka、RocketMQ、TubeMQ 等不同,Apache Pulsar 不僅僅會針對每個訂閲的消費進度保存一個最小的確認位置(即這個位置之前的消息都已經被確認已消費),也會針對這個位置之後且已經收到確認響應的消息,用 range 區間段的方式保存確認信息。

如下圖所示:

1.png

另外,由於 Pulsar 的每個分區都會對應一個訂閲組下的所有消費者。Broker 向客户端推送消息的時候,通過輪詢的方式(此處指 Shared 共享訂閲;Key_Shared 訂閲是通過 key 與一個消費者做關聯來進行推送)給每個消費者推送一部分消息。每個消費者分別確認一部分消息後,Broker 端可能會保存很多這種確認區段信息。

如下圖所示:

2.png

確認空洞是兩個連續區間之間的點,用於表示確認信息的區間段的個數。確認空洞受相同訂閲組下消費者個數的多少、消費者消費進度的快慢等因素的影響。空洞較多或特別多即表示消費確認的信息非常大。

Pulsar 會週期性地將每個消費組的確認信息組成一個 Entry,寫入到 Bookie 中進行存儲,寫入流程與普通消息寫入流程一樣。因此當消費組的消費確認空洞比較多、消費確認信息比較大、寫入比較頻繁的時候,會對系統的整體響應機制產生壓力,在客户端體現為生產耗時增長、生產超時增多、耗時毛刺明顯等現象。

在此情況下,可以通過減少消費者個數、提高消費者消費速率、調整保存確認信息的頻率和保存的 range 段的個數等方式處理確認空洞。

解析 2:Pulsar-io 線程卡死

Pulsar-io 線程池是 Pulsar Broker 端用於處理客户端請求的線程池。當這裏的線程處理慢或卡住的時候,會導致客户端生產超時、連接斷連等。Pulsar-io 線程池的問題,可以通過 jstack 信息進行分析,在 Broker 端體現為存在大量的 CLOSE_WAIT 狀態的連接, 如下圖所示:

3.png

Pulsar-io 線程池卡住的現象,一般為服務器端代碼 bug 導致,目前處理過的有: - 部分併發場景產生的死鎖; - 異步編程 Future 異常分支未處理結束等。

除了程序自身的 bug 外,配置也可能引起線程池卡住。如果 Pulsar-io 線程池的線程長時間處於運行狀態,在機器 CPU 資源足夠的情況下,可以通過變更 broker.conf 中的 numioThreads 參數來調整 Pulsar-io 線程池中的工作線程個數,來提高程序的並行處理性能。

注意:Pulsar-io 線程池繁忙,本身並不會導致問題。 但是,Broker 端有一個後台線程,會週期的判斷每一個 Channel(連接)有沒有在閾值時間內收到客户端的請求信息。如果沒有收到,Broker 會主動的關閉這個連接(相反,客户端 SDK 中也有類似的邏輯)。因此,當 Pulsar-io 線程池被卡住或者處理慢的時候,客户端會出現頻繁的斷連-重聯的現象。

解析 3:Ledger 切換耗時過長

Ledger 作為 Pulsar 單個分區消息的一個邏輯組織單位,每個 Ledger 下包含一定大小和數量的 Entry,而每個 Entry 下會保存至少一條消息( batch 參數開啟後,可能是多條)。每個 Ledger 在滿足一定的條件時,如包含的 Entry 數量、總的消息大小、存活的時間三個維度中的任何一個超過配置限制,都會觸發 Ledger 的切換。

Ledger 切換時間耗時比較長的現象如下:

4.png

當 Ledger 發生切換時,Broker 端新接收到或還未處理完的消息會放在 appendingQueue 隊列中,當新的 Ledger 創建完成後,會繼續處理這個隊列中的數據,保證消息不丟失。

因此,當 Ledger 切換過程比較慢時會導致消息生產的耗時比較長甚至超時。這個場景下,一般需要關注下 ZooKeeper 的性能,排查下 ZooKeeper 所在機器的性能和 ZooKeeper 進程的 GC 狀況。

解析 4:BookKeeper-io 單線程耗時過長

目前 Pulsar 集羣中,BookKeeper 的版本要相對比較穩定,一般通過調整相應的客户端線程個數、保存數據時的 E、QW、QA 等參數可以達到預期的性能。

如果通過 Broker 的 jstack 信息發現 BookKeeper 客户端的 Bookkeeper-io 線程池比較繁忙時或線程池中的單個線程比較繁忙時,首先要排查 ZooKeeper、Bookie 進程的 Full GC 情況。如果沒有問題,可以考慮調整 Bookkeeper-io 線程池的線程個數和 Topic 的分區數。

解析 5:Debug 級別日誌影響

在日誌級別下,影響生產耗時的場景一般出現在 Java 客户端。如客户端業務引入的是 Log4j,使用的是 Log4j 的日誌輸出方式,同時開啟了 Debug 級別的日誌則會對 Pulsar Client SDK 的性能有一定的影響。建議使用 Pulsar Java 程序引入 Log4j 或 Log4j + SLF4J 的方式輸出日誌。同時,針對 Pulsar 包調整日誌級別至少到 INFO 或 ERROR 級別。

在比較誇張的情況下,Debug 日誌影響生產耗時的線程能將生產耗時拉長到秒級別,調整後降低到正常水平(毫秒級)。具體現象如下:

5.png

在消息量特別大的場景下,服務器端的 Pulsar 集羣需要關閉 Broker、Bookie 端的 Debug 級別的日誌打印(建議線網環境),直接將日誌調整至 INFO 或 ERROR 級別。

解析 6:Topic 分區分佈不均

Pulsar 會在每個 Namespace 級別配置 bundles ,默認 4 個,如下圖所示。每個 bundle range 範圍會與一個 Broker 關聯,而每個 Topic 的每個分區會經過 hash 運算,落到對應的 Broker 進行生產和消費。當過多的 Topic 分區落入到相同的 Broker 上面,會導致這個 Broker 上面的負載過高,影響消息的生產和消費效率。

6.png

中度可信度描述已自動生成]()Data 項目在開始部署的時候,每個 Topic 的分區數和每個 Namespace 的 bundle 數都比較少。通過調整 Topic 的分區個數和 bundle 的分割個數,使得 Topic 的分區在 Broker 上面達到逐步均衡分佈的目的。

在 bundle 的動態分割和 Topic 的分佈調整上,Pulsar 還是有很大的提升空間,需要在 bundle 的分割算法(目前支持 range_equally_dividetopic_count_equally_divide,默認目前支持 range_equally_divide,建議使用 topic_count_equally_divide)、Topic 分區的分佈層面,在保證系統穩定、負載均衡的情況下做進一步的提升。

調優二:客户端頻繁斷開與重連

客户端斷連/重連的原因有很多,結合騰訊 Data 項目場景,我們總結出客户端 SDK 導致斷連的主要有如下幾個原因,主要包括: - 客户端超時斷連-重連機制 - Go SDK 的異常處理 - Go SDK 生產者 sequence id 處理 - 消費者大量、頻繁的創建和銷燬

下面依次為大家解析這些問題的原因與解決方案。

解析 1:客户端超時斷連-重連機制

Pulsar 客户端 SDK 中有與 Broker 端類似邏輯(可參考#解析2部分內容),週期判斷是否在閾值的時間內收到服務器端的數據,如果沒有收到則會斷開連接。

這種現象,排除服務器端問題的前提下,一般問題出現在客户端的機器資源比較少,且使用率比較高的情況,導致應用程序沒有足夠的 CPU 能力處理服務器端的數據。此種情況,可以調整客户端的業務邏輯或部署方式,進行規避處理。

解析 2:Go SDK 的異常處理

Pulsar 社區提供多語言的客户端的接入能力,如支持 Java、Go、C++、Python 等。但是除了 Java 和 Go 語言客户端外,其他的語言實現相對要弱一些。Go 語言的 SDK 相對於 Java 語言 SDK 還有很多地方需要完善和細化,比方説在細節處理上與 Java 語言 SDK 相比還不夠細膩。

如收到服務器端的異常時,Java SDK 能夠區分哪些異常需要銷燬連接重連、哪些異常不用銷燬連接(如 ServerError_TooManyRequests),但 Go 客户端會直接銷燬 Channel ,重新創建。

解析 3:Go SDK 生產者 Sequence id 處理

發送消息後,低版本的 Go SDK 生產者會收到 Broker 的響應。如果響應消息中的 sequenceID 與本端維護的隊列頭部的 sequenceID 不相等時會直接斷開連接——這在部分場景下,會導致誤斷,需要區分小於和大於等於兩種場景。

這裏描述的場景和解析 1-客户端超時中的部分異常場景,已經在高版本 Go SDK 中做了細化和處理,建議大家在選用 Go SDK 時儘量選用新的版本使用。目前,Pulsar Go SDK 也在快速的迭代中,歡迎感興趣的同學一起參與和貢獻。

解析 4:消費者大量且頻繁地創建和銷燬

集羣運維過程中在更新 Topic 的分區數後,消費者會大量且頻繁地創建和銷燬。針對這個場景,我們已排查到是 SDK  bug 導致,該問題會在 Java 2.6.2 版本出現。運維期間,消費者與 Broker 端已經建立了穩定的連接;運維過程中,因業務消息量的增長需求,需要調整 Topic 的分區數。客户端在不需要重啟的前提下,感知到了服務器端的調整,開始創建新增分區的消費者,這是因為處理邏輯的 bug,會導致客户端大量且頻繁地反覆創建消費者。如果你也遇到類似問題,建議升級 Java 客户端的版本。 除了上面的斷連-重連場景外,在我們騰訊 Data 項目的客户端還遇到過 Pod 頻繁重啟的問題。經過排查和分析,我們確定是客户端出現異常時,拋出 Panic 錯誤導致。建議在業務實現時,要考慮相關的容錯場景,在實現邏輯層面進行一定程度的規避。

調優三:升級 ZooKeeper

由於我們騰訊 Data 項目中使用的 Pulsar 集羣消息量比較大,機器的負載也相對較高。涉及到 ZooKeeper,我們開始使用的是 ZooKeeper 3.4.6 版本。在日常運維過程中,整個集羣多次觸發 ZooKeeper 的一個 bug,現象如下截圖所示:

7.png

當前,ZooKeeper 項目已經修復該 Bug,感興趣的小夥伴可以點擊該連接查看詳情: http://issues.apache.org/jira/browse/ZOOKEEPER-2044。因此,在 Pulsar 集羣部署的時候建議打上相應的補丁或升級 ZooKeeper 版本。在騰訊 Data 項目中,我們則是選擇將 ZooKeeper 版本升級到 3.6.3 進行了對應處理。

小結:Pulsar 集羣運維排查指南

不同的業務有不同的場景和要求,接入和運維 Apache Pulsar 集羣時遇到的問題,可能也不太一樣,但我們還是能從問題排查角度方面做個梳理,以便找到相應規律提升效率。

針對 Apache Pulsar 集羣運維過程中遇到的問題,如生產耗時長、生產超時(timeout)、消息推送慢、消費堆積等,如果日誌中沒有什麼明顯的或有價值的異常(Exception)、錯誤(Error) 之類的信息時,可以從如下幾個方面進行排查: - 集羣資源配置及使用現狀 - 客户端消費狀況 - 消息確認信息 - 線程狀態 - 日誌分析\ 下面針對每個方面做個簡要説明。

1. 集羣資源配置及使用現狀

首先,需要排查進程的資源配置是否能夠滿足當前系統負載狀況。可以通過 Pulsar 集羣監控儀表盤平台查看 Broker、Bookie、ZooKeeper 的 CPU、內存及磁盤 IO 的狀態。

其次,查看 Java 進程的 GC 情況(特別是 Full GC 情況),處理 Full GC 頻繁的進程。如果是資源配置方面的問題,需要集羣管理人員或者運維人員調整集羣的資源配置。

2. 客户端消費狀況

可以排查消費能力不足引起的反壓(背壓)。出現背壓的現象一般是存在消費者進程,但是收不到消息或緩慢收到消息。 可以通過 Pulsar 管理命令,查看受影響 Topic 的統計信息(stats),可重點關注未確認消息數量數量(unackedMessages)backlog 數量消費訂閲類型,以及處理未確認消息(unackmessage)比較大的訂閲/消費者。如果未確認消息(unackmessage)數量過多,會影響 Broker 向客户端的消息分發推送。這類問題一般是業務側的代碼處理有問題,需要業務側排查是否有異常分支,沒有進行消息的 ack 處理。

3. 消息確認信息

如果集羣生產耗時比較長或生產耗時毛刺比較多,除了系統資源配置方面排查外,還需要查看是否有過大的消息確認信息。

查看是否有過大的確認空洞信息,可以通過管理命令針對單個 Topic 使用 stats-internal 信息,查看訂閲組中的 individuallyDeletedMessages 字段保存的信息大小。

消息確認信息的保存過程與消息的保存過程是一樣的,過大的確認信息如果頻繁下發存儲,則會對集羣存儲造成壓力,進而影響消息的生產耗時。

4. 線程狀態

如果經過上面的步驟,問題還沒有分析清楚,則需要再排查下 Broker 端的線程狀態,主要關注 pulsar-iobookkeeper-iobookkeeper-ml-workers-OrderedExecutor 線程池的狀態,查看是否有線程池資源不夠或者線程池中某個線程被長期佔用。

可以使用 top -p PID(具體pid) H 命令,查看當前 CPU 佔用比較大的線程,結合 jstack 信息找到具體的線程。

5. 日誌分析

如果經過上面所述步驟仍然沒有確認問題來源,就需要進一步查看日誌,找到含有有價值的信息,並結合客户端、Broker 和 Bookie 的日誌及業務的使用特點、問題出現時的場景、最近的操作等進行綜合分析。

回顧與計劃

上面我們花了很大篇幅來介紹客户端性能調優的內容,給到客户端生產超時、頻繁斷開與重連、ZooKeeper 等相應的排查思路與解決方案,並彙總了常見 Pulsar 集羣問題排查指南 5 條建議,為在大消息量、多節點、一個訂閲裏高達數千消費者的 Pulsar 應用場景運維提供參考。

當然,我們對 Pulsar 集羣的調優不會停止,也會繼續深入並參與社區項目共建。

由於單個 Topic 對應的客户端比較多,每個客户端所在的 Pod、Client 內部會針對每個 Topic 創建大量的生產者和消費者。由此就對 Pulsar SDK 在斷連、重連、生產等細節的處理方面的要求比較高,對各種細節處理流程都會非常的敏感。SDK 內部處理比較粗糙的地方會導致大面積的重連,進而影響生產和消費。目前,Pulsar Go SDK 在很多細節方面處理不夠細膩,與 Pulsar Java SDK 的處理有很多不一樣的地方,需要持續優化和完善。騰訊 TEG 數據平台 MQ 團隊也在積極參與到社區,與社區共建逐步完善 Go SDK 版本。

另外,針對騰訊 Data 項目的特大規模和業務特點,Broker 端需要處理大量的元數據信息,後續 Broker 自身的配置仍需要做部分的配置和持續調整。同時,我們除了擴容機器資源外,還計劃在 Apache Pulsar 的讀/寫線程數、Entry 緩存大小、Bookie 讀/寫 Cache 配置、Bookie 讀寫線程數等配置方面做進一步的調優處理。