從 Kafka 到 Pulsar,BIGO 打造實時訊息系統之路

ApachePulsar發表於2021-11-17

關於 Apache Pulsar

Apache Pulsar 是 Apache 軟體基金會頂級專案,是下一代雲原生分散式訊息流平臺,集訊息、儲存、輕量化函式式計算為一體,採用計算與儲存分離架構設計,支援多租戶、持久化儲存、多機房跨區域資料複製,具有強一致性、高吞吐、低延時及高可擴充套件性等流資料儲存特性。
GitHub 地址:http://github.com/apache/pulsar/

BIGO 於 2014 年成立,是一家高速發展的科技公司。基於強大的音視訊處理技術、全球音視訊實時傳輸技術、人工智慧技術、CDN 技術,BIGO 推出了一系列音視訊類社交及內容產品,包括 Bigo Live(直播)和 Likee(短視訊)等,在全球已擁有近 1 億使用者,產品及服務已覆蓋超過 150 個國家和地區。

挑戰

最初,BIGO 的訊息流平臺主要採用開源 Kafka 作為資料支撐。隨著資料規模日益增長,產品不斷迭代,BIGO 訊息流平臺承載的資料規模出現了成倍增長,下游的線上模型訓練、線上推薦、實時資料分析、實時數倉等業務對訊息流平臺的實時性和穩定性提出了更高的要求。開源的 Kafka 叢集難以支撐海量資料處理場景,我們需要投入更多的人力去維護多個 Kafka 叢集,這樣成本會越來越高,主要體現在以下幾個方面:

  • 資料儲存和訊息佇列服務繫結,叢集擴縮容/分割槽均衡需要大量拷貝資料,造成叢集效能下降。
  • 當分割槽副本不處於 ISR(同步)狀態時,一旦有 broker 發生故障,可能會造成資料丟失或該分割槽無法提供讀寫服務。
  • 當 Kafka broker 磁碟故障/空間佔用率過高時,需要進行人工干預。
  • 叢集跨區域同步使用 KMM(Kafka Mirror Maker),效能和穩定性難以達到預期。
  • 在 catch-up 讀場景下,容易出現 PageCache 汙染,造成讀寫效能下降。
  • Kafka broker 上儲存的 topic 分割槽數量有限,分割槽數越多,磁碟讀寫順序性越差,讀寫效能越低。
  • Kafka 叢集規模增長導致運維成本急劇增長,需要投入大量的人力進行日常運維;在 BIGO,擴容一臺機器到 Kafka 叢集並進行分割槽均衡,需要 0.5 人/天;縮容一臺機器需要 1 人/天。

如果繼續使用 Kafka,成本會不斷上升:擴縮容機器、增加運維人力。同時,隨著業務規模增長,我們對訊息系統有了更高的要求:系統要更穩定可靠、便於水平擴充套件、延遲低。為了提高訊息佇列的實時性、穩定性和可靠性,降低運維成本,我們開始考慮是否要基於開源 Kafka 做本地化二次開發,或者看看社群中有沒有更好的解決方案,來解決我們在維護 Kafka 叢集時遇到的問題。

為什麼選擇 Pulsar

2019 年 11 月,我們開始調研訊息佇列,對比當前主流訊息流平臺的優缺點,並跟我們的需求對接。在調研過程中,我們發現 Apache Pulsar 是下一代雲原生分散式訊息流平臺,集訊息、儲存、輕量化函式式計算為一體。Pulsar 能夠無縫擴容、延遲低、吞吐高,支援多租戶和跨地域複製。最重要的是,Pulsar 儲存、計算分離的架構能夠完美解決 Kafka 擴縮容的問題。Pulsar producer 把訊息傳送給 broker,broker 通過 bookie client 寫到第二層的儲存 BookKeeper 上。

Pulsar 採用儲存、計算分離的分層架構設計,支援多租戶、持久化儲存、多機房跨區域資料複製,具有強一致性、高吞吐以及低延時的高可擴充套件流資料儲存特性。

  • 水平擴容:能夠無縫擴容到成百上千個節點。
  • 高吞吐:已經在 Yahoo! 的生產環境中經受了考驗,支援每秒數百萬條訊息的釋出-訂閱(Pub-Sub)。
  • 低延遲:在大規模的訊息量下依然能夠保持低延遲(小於 5 ms)。
  • 持久化機制:Pulsar 的持久化機制構建在 Apache BookKeeper 上,實現了讀寫分離。
  • 讀寫分離:BookKeeper 的讀寫分離 IO 模型極大發揮了磁碟順序寫效能,對機械硬碟相對比較友好,單臺 bookie 節點支撐的 topic 數不受限制。

為了進一步加深對 Apache Pulsar 的理解,衡量 Pulsar 能否真正滿足我們生產環境大規模訊息 Pub-Sub 的需求,我們從 2019 年 12 月開始進行了一系列壓測工作。由於我們使用的是機械硬碟,沒有 SSD,在壓測過程中遇到了一些效能問題,在 StreamNative 的協助下,我們分別對 Broker和 BookKeeper 進行了一系列的效能調優,Pulsar 的吞吐和穩定性均有所提高。

經過 3~4 個月的壓測和調優,我們認為 Pulsar 完全能夠解決我們使用 Kafka 時遇到的各種問題,並於 2020 年 4 月在測試環境上線 Pulsar。

Apache Pulsar at BIGO:Pub-Sub 消費模式

2020 年 5 月,我們正式在生產環境中使用 Pulsar 叢集。Pulsar 在 BIGO 的場景主要是 Pub-Sub 的經典生產消費模式,前端有 Baina 服務(用 C++ 實現的資料接收服務),Kafka 的 Mirror Maker 和 Flink,以及其他語言如 Java、Python、C++ 等客戶端的 producer 向 topic 寫入資料。後端由 Flink 和 Flink SQL,以及其他語言的客戶端的 consumer 消費資料。

在下游,我們對接的業務場景有實時數倉、實時 ETL(Extract-Transform-Load,將資料從來源端經過抽取(extract)、轉換(transform)、載入(load)至目的端的過程)、實時資料分析和實時推薦。大部分業務場景使用 Flink 消費 Pulsar topic 中的資料,並進行業務邏輯處理;其他業務場景消費使用的客戶端語言主要分佈在 C++、Go、Python 等。資料經過各自業務邏輯處理後,最終會寫入 Hive、Pulsar topic 以及 ClickHouse、HDFS、Redis 等第三方儲存服務。

Pulsar + Flink 實時流平臺

在 BIGO,我們藉助 Flink 和 Pulsar 打造了實時流平臺。在介紹這個平臺之前,我們先了解下 Pulsar Flink Connector 的內部執行機理。在 Pulsar Flink Source/Sink API 中,上游有一個 Pulsar topic,中間是 Flink job,下游有一個 Pulsar topic。我們怎麼消費這個 topic,又怎樣處理資料並寫入 Pulsar topic 呢?

按照上圖左側程式碼示例,初始化一個 StreamExecutionEnvironment,進行相關配置,比如修改 property、topic 值。然後建立一個 FlinkPulsarSource 物件,這個 Source 裡面填上 serviceUrl(brokerlist)、adminUrl(admin 地址)以及 topic 資料的序列化方式,最終會把 property 傳進去,這樣就能夠讀取 Pulsar topic 中的資料。Sink 的使用方法非常簡單,首先建立一個 FlinkPulsarSink,Sink 裡面指定 target topic,再指定 TopicKeyExtractor 作為 key,並呼叫 addsink,把資料寫入 Sink。這個生產消費模型很簡單,和 Kafka 很像。

Pulsar topic 和 Flink 的消費如何聯動呢?如下圖所示,新建 FlinkPulsarSource 時,會為 topic 的每一個分割槽新建立一個 reader 物件。要注意的是 Pulsar Flink Connector 底層使用 reader API 消費,會先建立一個 reader,這個 reader 使用 Pulsar Non-Durable Cursor。Reader 消費的特點是讀取一條資料後馬上提交(commit),所以在監控上可能會看到 reader 對應的 subscription 沒有 backlog 資訊。

在 Pulsar 2.4.2 版本中,由 Non-Durable Cursor 訂閱的 topic,在接收到 producer 寫入的資料時,不會將資料儲存在 broker 的 cache 中,導致大量資料讀取請求落到 BookKeeper 中,降低資料讀取效率。BIGO 在 Pulsar 2.5.1 版本中修正了這個問題。

Reader 訂閱 Pulsar topic 後,消費 Pulsar topic 中的資料,Flink 如何保證 exactly-once 呢?Pulsar Flink Connector 使用另外一個獨立的 subscription,這個 subscription 使用的是 Durable Cursor。當 Flink 觸發 checkpoint,Pulsar Flink Connector 會把 reader 的狀態(包括每個 Pulsar Topic Partition 的消費位置) checkpoint 到檔案、記憶體或 RocksDB 中,當 checkpoint 完成後,會發布一次 Notify Checkpoint Complete 通知。Pulsar Flink Connector 收到 checkpoint 完成通知後,把當前所有 reader 的消費 Offset,即 message id 以獨立的 SubscriptionName 提交給 Pulsar broker,此時才會把消費 Offset 資訊真正記錄下來。

Offset Commit 完成後,Pulsar broker 會將 Offset 資訊(在 Pulsar 中以 Cursor 表示)儲存到底層的分散式儲存系統 BookKeeper 中,這樣做的好處是當 Flink 任務重啟後,會有兩層恢復保障。第一種情況是從 checkpoint 恢復:可以直接從 checkpoint 裡獲得上一次消費的 message id,通過這個 message id 獲取資料,這個資料流就能繼續消費。如果沒有從 checkpoint 恢復,Flink 任務重啟後,會根據 SubscriptionName 從 Pulsar 中獲取上一次 Commit 對應的 Offset 位置開始消費。這樣就能有效防止 checkpoint 損壞導致整個 Flink 任務無法成功啟動的問題。

Checkpoint 流程如下圖所示。

先做 checkpoint N,完成後釋出一次 notify Checkpoint Complete,等待一定時間間隔後,接下來做 checkpoint N+1,完成後也會進行一次 notify Checkpoint Complete 操作,此時把 Durable Cursor 進行一次 Commit,最終 Commit 到 Pulsar topic 的服務端上,這樣能確保 checkpoint 的 exactly-once,也能根據自己設定的 subscription 保證 message “keep alive”。

Topic/Partition Discovery 要解決什麼問題呢?當 Flink 任務消費 topic 時,如果 Topic 增加分割槽,Flink 任務需要能夠自動發現分割槽。Pulsar Flink Connector 如何實現這一點呢?訂閱 topic 分割槽的 reader 之間相互獨立,每個 task manager 包含多個 reader thread,根據雜湊函式把單個 task manager 中包含的 topic 分割槽對映過來,topic 中新增分割槽時,新加入的分割槽會對映到某個 task manager 上,task manager 發現新增分割槽後,會建立一個 reader,消費掉新資料。使用者可以通過設定 partition.discovery.interval-millis 引數,調配檢測頻率。

為了降低 Flink 消費 Pulsar topic 的門檻,讓 Pulsar Flink Connector 支援更加豐富的 Flink 新特性,BIGO 訊息佇列團隊為 Pulsar Flink Connector 增加了 Pulsar Flink SQL DDL(Data Definition Language,資料定義語言) 和 Flink 1.11 支援。此前官方提供的 Pulsar Flink SQL 只支援 Catalog,要想通過 DDL 形式消費、處理 Pulsar topic 中的資料不太方便。在 BIGO 場景中,大部分 topic 資料都以 JSON 格式儲存,而 JSON 的 schema 沒有提前註冊,所以只能在 Flink SQL 中指定 topic 的 DDL 後才可以消費。針對這種場景,BIGO 基於 Pulsar Flink Connector 做了二次開發,提供了通過 Pulsar Flink SQL DDL 形式消費、解析、處理 Pulsar topic 資料的程式碼框架(如下圖所示)。

左邊的程式碼中,第一步是配置 Pulsar topic 的消費,首先指定 topic 的 DDL 形式,比如 rip、rtime、uid 等,下面是消費 Pulsar topic 的基礎配置,比如 topic 名稱、service-url、admin-url 等。底層 reader 讀到訊息後,會根據 DDL 解出訊息,將資料儲存在 test_flink_sql 表中。第二步是常規邏輯處理(如對錶進行欄位抽取、做 join 等),得出相關統計資訊或其他相關結果後,返回這些結果,寫到 HDFS 或其他系統上等。第三步,提取相應欄位,將其插入一張 hive 表。由於 Flink 1.11 對 hive 的寫入支援比 1.9.1 更加優秀,所以 BIGO 又做了一次 API 相容和版本升級,使 Pulsar Flink Connector 支援 Flink 1.11。BIGO 基於 Pulsar 和 Flink 構建的實時流平臺主要用於實時 ETL 處理場景和 AB-test 場景。

實時 ETL 處理場景

實時 ETL 處理場景主要運用Pulsar Flink Source 及 Pulsar Flink Sink。這個場景中,Pulsar topic 實現幾百甚至上千個 topic,每個 topic 都有獨立的 schema。我們需要對成百上千個 topic 進行常規處理,如欄位轉換、容錯處理、寫入 HDFS 等。每個 topic 都對應 HDFS 上的一張表,成百上千個 topic 會在 HDFS 上對映成百上千張表,每張表的欄位都不一樣,這就是我們遇到的實時 ETL 場景。

這種場景的難點在於 topic 數量多。如果每個 topic 維護一個 Flink 任務,維護成本太高。之前我們想通過 HDFS Sink Connector 把 Pulsar topic 中的資料直接 sink 到 HDFS 上,但處理裡面的邏輯卻很麻煩。最終我們決定使用一個或多個 Flink 任務去消費成百上千個 topic,每個 topic 配自己的 schema,直接用 reader 來訂閱所有 topic,進行 schema 解析後處理,將處理後的資料寫到 HDFS 上。

隨著程式執行,我們發現這種方案也存在問題:運算元之間壓力不均衡。因為有些 topic 流量大,有些流量小,如果完全通過隨機雜湊的方式對映到對應的 task manager 上去,有些 task manager 處理的流量會很高,而有些 task manager 處理的流量很低,導致有些 task 機器上積塞非常嚴重,拖慢 Flink 流的處理。所以我們引入了 slot group 概念,根據每個 topic 的流量情況進行分組,流量會對映到 topic 的分割槽數,在建立 topic 分割槽時也以流量為依據,如果流量很高,就多為 topic 建立分割槽,反之少一些。分組時,把流量小的 topic 分到一個 group 中,把流量大的 topic 單獨放在一個 group 中,很好地隔離了資源,保證 task manager 總體上流量均衡。

AB-test 場景

實時數倉需要提供小時表或天表為資料分析師及推薦演算法工程師提供資料查詢服務,簡單來講就是 app 應用中會有很多打點,各種型別的打點會上報到服務端。如果直接暴露原始打點給業務方,不同的業務使用方就需要訪問各種不同的原始表從不同維度進行資料抽取,並在表之間進行關聯計算。頻繁對底層基礎表進行資料抽取和關聯操作會嚴重浪費計算資源,所以我們提前從基礎表中抽取使用者關心的維度,將多個打點合併在一起,構成一張或多張寬表,覆蓋上面推薦相關的或資料分析相關的 80% ~ 90% 場景任務。

在實時數倉場景下還需實時中間表,我們的解決方案是,針對 topic A 到 topic K ,我們使用 Pulsar Flink SQL 將消費到的資料解析成相應的表。通常情況下,將多張表聚合成一張表的常用做法是使用 join,如把表 A 到 K 按照 uid 進行 join 操作,形成非常寬的寬表;但在 Flink SQL 中 join 多張寬表效率較低。所以 BIGO 使用 union 來替代 join,做成很寬的檢視,以小時為單位返回檢視,寫入 ClickHouse,提供給下游的業務方實時查詢。使用 union 來替代 join 加速表的聚合,能夠把小時級別的中間表產出控制在分鐘級別。

輸出天表可能還需要 join 存放在 hive 上的表或其他儲存介質上的離線表,即流表和離線表之間 join 的問題。如果直接 join,checkpoint 中需要儲存的中間狀態會比較大,所以我們在另外一個維度上做了優化。

左側部分類似於小時表,每個 topic 使用 Pulsar Flink SQL 消費並轉換成對應的表,表之間進行 union 操作,將 union 得到的表以天為單位輸入到 HBase(此處引入 HBase 是為了做替代它的 join)。

右側需要 join 離線資料,使用 Spark 聚合離線的 Hive 表(如表 a1、a2、a3),聚合後的資料會通過精心設計的 row-key 寫入 HBase 中。資料聚合後狀態如下:假設左邊資料的 key 填了寬表的前 80 列,後面 Spark 任務算出的資料對應同樣一個 key,填上寬表的後 20 列,在 HBase 中組成一張很大的寬表,把最終資料再次從 HBase 抽出,寫入 ClickHouse,供上層使用者查詢,這就是 AB-test 的主體架構。

業務收益

從 2020 年 5 月上線至今,Pulsar 執行穩定,日均處理訊息數百億,位元組入流量為 2~3 GB/s。Apache Pulsar 提供的高吞吐、低延遲、高可靠性等特性極大提高了 BIGO 訊息處理能力,降低了訊息佇列運維成本,節約了近 50% 的硬體成本。目前,我們在幾十臺物理主機上部署了上百個 Pulsar broker 和 bookie 程式,採用 bookie 和 broker 在同一個節點的混部模式,已經把 ETL 從 Kafka 遷移到 Pulsar,並逐步將生產環境中消費 Kafka 叢集的業務(比如 Flink、Flink SQL、ClickHouse 等)遷移到 Pulsar 上。隨著更多業務的遷移,Pulsar 上的流量會持續上漲。

我們的 ETL 任務有一萬多個 topic,每個 topic 平均有 3 個分割槽,使用 3 副本的儲存策略。之前使用 Kafka,隨著分割槽數增加,磁碟由順序讀寫逐漸退化為隨機讀寫,讀寫效能退化嚴重。Apache Pulsar 的儲存分層設計能夠輕鬆支援百萬 topic,為我們的 ETL 場景提供了優雅支援。

未來展望

BIGO 在 Pulsar broker 負載均衡、broker cache 命中率優化、broker 相關監控、BookKeeper 讀寫效能優、BookKeeper 磁碟 IO 效能優化、Pulsar 與 Flink、Pulsar 與 Flink SQL 結合等方面做了大量工作,提升了 Pulsar 的穩定性和吞吐,也降低了 Flink 與 Pulsar 結合的門檻,為 Pulsar 的推廣打下了堅實基礎。

未來,我們會增加 Pulsar 在 BIGO 的場景應用,幫助社群進一步優化、完善 Pulsar 功能,具體如下:

  1. 為 Apache Pulsar 研發新特性,比如支援 topic policy 相關特性。
  2. 遷移更多工到 Pulsar。這項工作涉及兩方面,一是遷移之前使用 Kafka 的任務到 Pulsar。二是新業務直接接入 Pulsar。
  3. BIGO 準備使用 KoP 來保證資料遷移平滑過渡。因為 BIGO 有大量消費 Kafka 叢集的 Flink 任務,我們希望能夠直接在 Pulsar 中做一層 KoP,簡化遷移流程。
  4. 對 Pulsar 及 BookKeeper 持續進行效能優化。由於生產環境中流量較高,BIGO 對系統的可靠性和穩定性要求較高。
  5. 持續優化 BookKeeper 的 IO 協議棧。Pulsar 的底層儲存本身是 IO 密集型系統,保證底層 IO 高吞吐,才能夠提升上層吞吐,保證效能穩定。

作者簡介

陳航,Apache Pulsar Committer,BIGO 大資料訊息平臺團隊負責人,負責建立與開發承載大規模服務與應用的集中釋出-訂閱訊息平臺。他將 Apache Pulsar 引入到 BIGO 訊息平臺,並打通上下游系統,如 Flink、ClickHouse 和其他實時推薦與分析系統。他目前主要負責 Pulsar 效能調優、新功能開發及 Pulsar 生態整合。

相關閱讀

點選 連結 ,獲取 Apache Pulsar 硬核乾貨資料!

相關文章