Druid在愛奇藝的實踐和技術演進

運營增長 發佈 2020-06-15T16:52:21+00:00

RAP 實時分析平台目前已經在愛奇藝會員、推薦、BI 等多個業務落地,配置了上千張報表,幫助業務在實時監控報警、實時運營分析、實時 AB 測試對比等場景提升排障響應速度、運營決策效率。

最近幾年大數據技術在各行各業得到廣泛應用,為企業的運營決策和各種業務提供支持。隨著數據的增長,業務對數據時效性的要求,給企業的大數據分析帶來了巨大挑戰。針對海量數據的實時分析需求,近年來市場上湧現出眾多 OLAP 分析引擎。這些 OLAP 引擎有各自的適用場景和優缺點,如何選擇一款合適的引擎來更快地分析數據、更高效地挖掘數據的潛在價值?

愛奇藝大數據服務團隊評估了市面上主流的 OLAP 引擎,最終選擇 Apache Druid 時序資料庫來滿足業務的實時分析需求。本文將介紹 Druid 在愛奇藝的實踐情況、優化經驗以及平台化建設的一些思考。

01

愛奇藝大數據 OLAP 服務


愛奇藝大數據 OLAP 服務在2015年前主要以離線分析為主,主要基於 Hive+MySQL、HBase 等。2016年起引入 Kylin 和 Impala 分別支持固定報表和 Ad-hoc 查詢。2018年以來引入 Kudu 和 Druid 支持實時分析需求。

在引入 Druid 之前,業務的一些場景無法通過離線分析滿足,如廣告主想要實時基於投放效果調整投放策略、算法工程師調整模型推到線上 A/B 要隔天離線報表才能看到效果。這些場景都可以歸納為對海量事件流進行實時分析,經典的解決方案有如下幾種:

1. 離線分析:

使用 Hive、Impala 或者 Kylin,它們一個共同的缺點是時效性差,即只能分析一天或者一小時前的數據,Kylin 還面臨維度爆炸的問題

2. 實時分析:

  • 用 ElasticSearch 或 OpenTSDB,由於數據結構本質是行存儲,聚合分析速度都比較慢;可以通過查詢緩存、OpenTSDB 預計算進行優化,但不根本解決問題;
  • 用流任務 ( Spark/Flink ) 實時地計算最終結果,存儲在 MySQL 提供進一步服務;問題是每當需求調整,如維度變更時,則需要寫新的流任務代碼;
  • 使用 Kudu 和 Impala 結合能夠做到實時分析。在實踐過程中發現,Kudu 受限於內存和單機分區數,支撐海量數據成本很大。

3. Lambda 架構:

無論選用哪種實時或離線方案的組合,都會採用 Lambda 架構,用離線數據校準實時數據。這意味著從攝入、處理、查詢都需要維護兩套架構,新增一個維度,離線和實時均需對應修改,維護困難。

以上種種方案的不足,促使我們尋找新的解決方案,最終決定採用 Druid。

02

Apache Druid

Apache Druid 是針對海量事件流進行存儲和實時多維分析的開源系統。它具有如下特性:

  • 實時可見:消息攝入後分鐘級查詢可見
  • 交互查詢:查詢延時在秒級,核心思想為內存計算和並行計算
  • 維度靈活:支持幾十個維度任意組合,僅在索引時指定的維度查詢可見
  • 易於變更:需求變更後調整索引配置立馬生效
  • 流批一體:新版本 KIS 模式可實現 Exactly Once 語義

上圖為 Druid 架構圖,大體分為幾個模塊:

  • MiddleManager:索引節點,負責實時處理消息,將其轉成列式存儲,並通過 Rollup 精簡數據量;索引節點定期將內存中數據持久化為不可修改的文件 ( Segment ),保存至 HDFS 保證數據不會丟失;
  • Historical:歷史節點,將 Segment 加載到本地,負責大部分查詢的計算;
  • Broker:查詢節點,將查詢分解為實時和離線部分,轉發給索引節點和歷史節點,並匯總最終的查詢結果;
  • Overlord:負責索引任務管理;
  • Coordinator:負責負載均衡,確保 Segment 在歷史節點之間儘量均衡。

03

Druid 在愛奇藝的實踐


Druid 很好地填補了愛奇藝在實時 OLAP 分析領域的空白,隨著業務實時分析需求的增加,Druid 集群和業務規模也在穩步增長。目前集群規模在數百個結點,每天處理數千億條消息,Rollup 效果在10倍以上。平均每分鐘6千條查詢,P99 延時一秒內,P90 延時在200毫秒內。在建設 Druid 服務過程中,我們也不斷遇到規模增長帶來的性能瓶頸和穩定性問題。

1. Coordinator 瓶頸

當時的挑戰是實時索引任務經常被阻塞。Druid 的 Handoff 總結如下,索引節點將 Segment 持久化到 HDFS,然後 Coordinator 制定調度策略,將計劃發布到 ZooKeeper。歷史節點從 ZooKeeper 獲取計劃後異步地加載 Segment。當歷史節點加載完 Segment 索引節點的 Handoff 過程才結束。這個過程中,由於 Coordinator 制定計劃是單線程串行的,如果一次觸發了大量 Segment 加載,執行計劃制定就會很慢,從而會阻塞 Handoff 過程,進而索引節點所有的 Slot 均會被用滿。

而以下過程均會觸發大量 Segment 加載,在解決 Coordinator 調度性能瓶頸前, 很容易引發故障:

  • 歷史節點因硬體故障、GC、主動運維退出
  • 調整 Segment 副本數、保留規則

通過火焰圖對 Coordinator 進行 Profiling 最終定位了問題,如下圖所示,將最耗時部分放大出來,是負載均衡策略對每個 Segment 要選擇一個最佳的伺服器。閱讀源碼可知其過程為,加載 Segment X,需要計算它和伺服器的每個 Segment Y 的代價 Cost(X,Y),其和為伺服器和 Segment X 的代價。假設集群有 N 個 Segment,M 個 Historical 節點,則一個節點宕機,有 N/M 個 Segment 需要加載,每個 Segment 都和剩餘的 N 個節點計算一次代價,調度耗時和 N 成平方關係。

一個節點宕機調度耗時 = (N/M) 個 Segment * 每個 Segment 調度耗時 = (N/M) * N = O(N^2)

分析清楚原因後,很容易了解到 Druid 新很容易了解到 Druid 新版本提供了新的負載均衡策略:

druid.coordinator.balancer.strategy = CachingCostBalancerStrategy

應用後調度性能提升了10000倍,原先一個歷史節點宕機會阻塞 Coordinator 1小時到2小時,現在30秒內即可完成。

2. Overlord 瓶頸

Overlord 性能慢,我們發現升級到0.14後 Overlord API 性能較差,導致的後果是索引任務機率性因調用 API 超時而失敗。通過 Jstack 分析,看到大部分的 HTTP 線程均為阻塞態,結合代碼分析,定位到 API 慢的原因,如左圖所示,Tranquility 會定期調用 Overlord API,獲取所有 RunningTasks,Overlord 內部維護了和 MySQL 的連接池,該連接池默認值為8,該默認值值過小,阻塞了 API 處理。解決方法是增大 dbcp 連接池大小。

druid.metadata.storage.connector.dbcp.maxTotal = 64

調整後,Overlord 性能得到了大幅提升,Overlord 頁面打開從幾十秒降低到了幾秒。但意料之外的事情發生了,API 處理能力增加帶來了 CPU 的飆升,如右圖所示,並且隨著 Tranquility 任務增加 CPU 逐漸打滿,Overlord 頁面性能又逐步降低。通過火焰圖 Profile 可知,CPU 主要花費在 getRunningTasks 的處理過程,進一步分析 Tranquility 源碼後得知,Tranquility 有一個配置項:

druidBeam.overlordPollPeriod

可以控制 Tranquility 輪詢該 API 的間隔,增大該間隔後問題得到了暫時緩解,但根本的解決方案還是將任務切換為 KIS 模式。

3. 索引成本

Druid 索引成本過高。基於 Druid 官方文檔,一個 Druid 索引任務需要3個核,一個核用於索引消息,一個核用於處理查詢,一個核用於 Handoff 過程。我們採用該建議配置索引任務,壓測結果是3核配置下能夠支撐百萬/分鐘的攝入。

在最初,集群所有的索引任務都是統一配置,但實際使用過程中,大部分的索引任務根本達不到百萬/分鐘的消息量,造成了資源大量浪費。如下圖所示,我們按照索引任務的內存使用量從高到低排序,9 GB 為默認配置,80%的任務利用率低於1/3,即 3 GB。我們以 3 GB 繪製一條橫線,以內存使用最接近的任務繪製一條豎線,定義 A 為實際使用的內存,B 為第二象限空白部分,C 為第四象限空白部分,D 為第一象限空白部分,則浪費的資源 = ( B+C+D ) 的面積。

我們思考能否採取索引任務分級的策略,定義一種新的類型索引節點 – Tiny 節點。Tiny 節點配置改為 1 core\3GB,能夠滿足80%小任務的資源需求,而 default 節點繼續使用 3 core9 GB 的配置,滿足20%大任務的需求,在這種新的配置下,浪費的資源 = ( B + C ) 的面積,D 這一大塊被省下來。簡單地計算可知,在不增加機器的情況下,總 Slots 能夠增加1倍。

默認 slot 資源需求為1,Tiny 為1/3,調整後單位任務需要的資源 = 0.2 * 1 + 0.8 * 1/3 = 0.5

在實際操作層面,還需解決一個問題,即如何把 Datasource 指定給合適的 Worker 節點。在 Druid 低版本中,需要通過配置文件將每一個 Datasource 和 Worker 節點進行關聯,假設有 N 個 Datasource,M 個 Worker 節點,這種配置的複雜度為 N * M,且無法較好地處理 Worker 節點負載均衡,Worker 宕機等場景。在 Druid 0.17中,引入了節點 Category 概念,只需將 Datasource 關聯特定的 Category,再將 Category 和 Worker 綁定,新的配置方法有2個 Category,複雜度 = 2 * N + 2 * M。

4. Tranquility vs KIS

剛使用 Druid 時,當時主力模式是 Tranquility。Tranquility 本質上仍然是經典的 Lambda 架構,實時數據通過 Tranquility 攝入,離線數據通過 HDFS 索引覆蓋。通過離線覆蓋的方式解決消息延遲的問題,缺點是維護兩套框架。對於節點失敗的問題,Tranquility 的解決方案是鏈路冗餘,即同時在兩個索引節點各起一份索引任務,任一節點失敗仍有一份能夠成功,缺點是浪費了一倍的索引資源。自0.14版本起,Druid 官方建議使用KIS模式索引數據,它提供了 Exactly Once 語義,能夠很好地實現流批一體。

和 Tranquility 的 Push 模式不同,KIS 採取 Pull 模式,索引任務從 Kafka 拉取消息,構建 Segment。關鍵點在於最後持久化 Segment 的時候,KIS 任務有一個數據結構記錄了上一次持久化的 Offset 位置,如圖例左下角所示,記錄了每個 Kafka Partition 消費的 Offset。在持久化時會先檢查 Segment 的開始 Offset 和元信息是否一致。如果不一致,則會放棄本次持久化,如果一致,則觸發提交邏輯。提交中,會同時記錄 Segment 元信息和 Kafka Offset,該提交過程為原子化操作,要麼都成功,要麼都失敗。

KIS 如何處理各個節點失敗的情況呢?假設 Kafka 集群失敗,由於是 Pull 模式,Druid 在 Kafka 恢復後繼續從上一個 Offset 開始消費;假設 Druid 索引節點失敗,Overlord 後台的 Supervisor 會監控到相應任務狀態,在新的索引節點啟動 KIS 任務,由於內存中的狀態丟失,新的 KIS 任務會讀取元信息,從上一次的 Offset 開始消費。假設是 MySQL 或者更新元數據過程失敗,則取決於提交的原子操作是否成功,若成功則 KIS 從新的 Offset 開始消費,失敗則從上一次 Offset 開始消費。

進一步看一下 KIS 是如何保證 Exactly Once 語義。其核心是保證 Kafka 消費的 Offset 連續,且每個消息都有唯一 ID。Exactly Once 可以分為兩個部分,一是 At Least Once,由 KIS 檢查 Offset 的機制保證,一旦發現缺失了部分 Offset,KIS 會重新消費歷史數據,該過程相當於傳統的離線補數據,只是現在由 Druid 自動完成了。另一個是 At Most Once,只要保證 Offset 沒有重疊部分,則每條消息只被處理了一次。

以下是 KIS 在愛奇藝的一個實例,左下圖為業務消息量和昨天的對比圖,其中一個小時任務持久化到 HDFS 失敗了,看到監控曲線有一個缺口。之後 Druid 後台啟動了一個新的 KIS 任務,一段時間後,隨著 KIS 補錄數據完成,曲線圖恢復到右下圖所示。那麼,如果業務不是一直盯著曲線看,而是定期查看的話,完全感受不到當中發生了異常。

04

基於 Druid 的實時分析平台建設

Druid 性能很好,但在初期推廣中卻遇到很大的阻力,主要原因是 Druid 的易用性差,體現在如下幾個方面:

  • 數據攝入需要撰寫一個索引配置,除了對數據自身的描述 ( 時間戳、維度和度量 ),還需要配置 Kafka 信息、Druid 集群信息、任務優化信息等
  • 查詢的時候需要撰寫一個 JSON 格式的查詢,語法為 Druid 自定義,學習成本高
  • 返回結果為一個 JSON 格式的數據,用戶需自行將其處理成最終圖表、告警
  • 報錯信息不友好,上述所有配置均通過 JSON 撰寫,一個簡單的逗號、格式錯誤都會引起報錯,需花費大量時間排查

為解決 Druid 易用性差的問題,愛奇藝自研了實時分析平台 RAP ( Realtime Analysis Platform ),屏蔽了 Kafka、Druid、查詢的細節,業務只需描述數據格式即可攝入數據,只需描述報表樣式、告警規則,即可配置實時報表和實時告警。

RAP 實時分析平台,主要有六大特性:

  • 全嚮導配置:業務無需手寫 ETL 任務
  • 計算存儲透明:業務無需關心底層 OLAP 選型
  • 豐富報表類型:支持常見的線圖、柱狀圖、餅圖等
  • 數據延時低:從 APP 數據採集到生成可視化報表的端到端延時在5分鐘內,支持數據分析師、運營等業務實時統計分析 UV、VV、在線用戶數等
  • 秒級查詢:大部分查詢都是秒以內
  • 靈活變更:更改維度後重新上線即可生效

RAP 實時分析平台目前已經在愛奇藝會員、推薦、BI 等多個業務落地,配置了上千張報表,幫助業務在實時監控報警、實時運營分析、實時 AB 測試對比等場景提升排障響應速度、運營決策效率。

關於 RAP 的更多技術細節和業務應用場景,可以閱讀之前分享的技術文章:愛奇藝大數據實時分析平台的建設與實踐

05

未來展望

進一步疊代完善 Druid 及 RAP,提升穩定性、服務能力,簡化業務接入成本:

  • 接入愛奇藝自研的 Pilot 智能 SQL 引擎,支持異常查詢攔截、限流等功能
  • 運維平台:包括元信息管理、任務管理、服務健康監測等,提升運維效率
  • 離線索引:支持直接索引 Parquet 文件,通過 Rollup 進一步提升查詢效率
  • 支持 JOIN:支持更豐富的語義

作者:愛奇藝大數據服務團隊

關鍵字: