PinalyticsDB:基於HBase的時間序列數據庫

PinalyticsDB是Pinterest打造的一套專有時間序列數據庫。在Pinterest,我們將PinalyticsDB作爲後端以實現成千上萬份時間序列報表的存儲與可視化,例如下圖所示案例(按國家與地區劃分)。

我們在數年前以HBase爲基礎開發出PinalyticsDB,其利用實時map-reduce架構通過HBase協處理器實現聚合。但隨着Pinterest業務的持續增長以及報表數量的不斷提升,PinalyticsDB在處理龐大數據量及應用負載中開始遭遇一系列可擴展性挑戰。

過去幾個月以來,我們着手對PinalyticsDB進行重構,希望進一步提高其性能與可靠性水平。在本文中,我們將共同瞭解Pinterest面臨的性能與可擴展性挑戰,以及我們如何通過服務的重新設計構建出更爲強大的PinalyticsDB新形態。

Hbase區域服務器熱點

隨着Pinterest內平臺使用量的快速提升,熱點問題也開始成爲PinalyticsDB區域服務器面臨的一大挑戰。在以往的設計中,PinalyticsDB會爲每份報表創建新的HBase表。

原有架構設計

原本的行鍵設計方案爲:

原行鍵=前綴|日期|後綴

前綴=指標名稱(字符串)

日期=YYYY-MM-DD格式,同樣爲字符串形式

後綴=由段號組成

行鍵由ASCII字符表示,其中“|”充當分隔符。

這套方案存在以下幾個問題:

  • 需要爲每份報表創建一個新表,但由於某些報表的訪問頻率遠超其他報表,因此承載這些報表的區域服務器將面對更多往來流量。

  • 我們擁有成千上萬份報表,因此HBase管理員基本不可能以手動方式監控報表並根據觀察到的熱點進行表拆分。

  • 在單一報表之內,某些指標的使用頻率可能較高。由於指標名稱爲行鍵的第一部分,因此導致熱點數量進一步增加。

  • 最近的數據使用趨勢傾向於更頻繁的訪問操作,而日期則爲指標後行鍵內容的組成部分,同樣導致熱點數量進一步增加。

以上提到的熱點主要針對讀取而言,但類似的熱點在寫入層面也同樣存在,主要表現爲某些報表表具有相對更高的寫入次數,且指標數據始終會寫入最新日期——這就導致各項指標的最新日期部分成爲區域內的寫入熱點。

新的架構設計

我們通過改進行鍵架構與HBase表設計的方式解決了這個問題。我們利用以下行鍵設計創建出一份面向所有報表的表。

新的行鍵 = SALT | <報表-id> | <指標-id> | <日期-時間> | <段>

該行鍵以字節數組(byte[])形式表示,而不再使用字符串形式。

  • 鍵內的各個部分都有固定的長度,作爲定義行鍵的固定結構,同時也支持模糊行過濾器。

  • 由於採用固定結構,因此我們不再需要“|”分隔符——這又進一步節約了空間資源。

對讀取與寫入操作的影響

Pinalytics DB V2 讀取架構

如大家所見,由於採用salting邏輯,讀取在整個集羣當中得到良好分發。更重要的是,寫入操作在這套架構中的分發效果同樣令人滿意。

改進協處理器性能

我們還計劃通過修改請求結構與協處理器掃描行爲的方式,進一步優化PinalyticsDB協處理器的性能。我們的優化舉措使得區域服務器CPU利用率、RPC延遲以及JVM線程阻塞情況都得到顯著改善。

我們的原始設計會針對發送至PinalyticsDB內每一項指標的對應請求創建Hbase掃描。Pinalytics會持續收到大量同類請求,從而引發大量掃描操作。通過將與同一報表及指標相關的聚合請求合併起來,並對與所請求段相關的FuzzyRowFilter進行單一掃描,我們顯著減少了實際產生的Hbase掃描次數。

在使用Pinalytics時,用戶通過會發出大批量請求,這些請求當中包含的不同細份指標往往數量有限。下圖所示爲跨美國多個州段請求某一樣本指標。

這是一類非常常見的用例。相當一部分用戶使用的儀表板中都包含這類圖表。

在這個用例的啓發下,我們嘗試進行“多細分優化”,即利用協處理器對與同一指標相關聯的PinalyticsRequest中的所有細分執行一次掃描(每區域salt)。

PinalyticsDB V2韓國人協處理器設計

  • Pinalytics Thrift Server將來自Pinalytics的全部請求按指標進行分組。接下來,協處理器會針對各個指標接收一條請求,其中包含與該指標相關的所有段FuzzyRowFilters。

  • 對於該協處理器區域內的各salt,協處理器會創建一條MUST_PASS_ONE掃描,並在單一FilterList的聚合請求中包含所有FuzzyRowFilters。

  • 該協處理器隨後按照日期與FuzzyRowFilter對所有掃描結合進行聚合,並將響應結果發送回Thrift Server。
    這意味着無論存在多少指向特定指標的不同段組合請求,我們都只需要處理一條指向該指標的聚合請求。

PinalyticsDB V2協處理器設計:對於每種salt,都只需要對指向同一指標的所有段創建一次掃描。

這一全新協處理器設計顯著改善了區域服務器的CPU利用率、RPC延遲以及JVM線程阻塞情況。

注意:下圖所示爲部署多段優化數小時後得到的捕捉結果,因此無法準確反映系統的當前性能。但總體來看,這些設計舉措仍然有助於提高協處理器的性能表現。

在部署新的協處理器後,區域服務器CPU利用率得到改善。

部署新的協處理器之後,區域服務器JVM線程阻塞情況得到改善。

部署新的協處理器之後,區域服務器RPG延遲得到改善。

大型報表元數據與Thrift Server OOM

我們的Thrift Server還存在OOM頻繁崩潰的問題,如果這時用戶嘗試加載相關圖表,那麼就會發生Web應用程序超時的狀況。這是因爲Thrift Server的jvm沒有設置XX:+ExitOnOutOfMemoryError,因此Thrift Server無法退出OOM,而繼續調用只能產生超時。快速的解決辦法是添加XX:+ExitOnOutOfMemoryError標記,以便在OOM生產Thrift Server上自動進行重啓。

爲了調試這個問題,我們將jconsole指向其中一臺生產Thrift Server,使其能夠捕捉到其他Thrift Server的崩潰情況。以下圖表所示爲總heap、上一代與新一代架構的運行情況。

Thrift Server的總heap內存爲8G。
請注意,內存使用量從低於4G突然提升至8G以上,引發問題的根源正是OOM。

用於PinalyticsDB Thrift Server的上一代CMS。

同樣的,舊一代架構的內存使用量會突然猛增至4G以上,直接超出了系統極限。峯值出現得太快,CMS收集器根本無暇介入,甚至連full GC都來不及出現。

用於PinalyticsDB Thrift Server的Eden空間。

我們通過負載測試在開發環境當中重現這類問題,並最終確定問題根源與我們的報表元數據存儲與讀取機制有關。對於大部分報表而言,元數據一般僅爲數KB。但對於某些大型報表,元數據可能超過60 MB,甚至高達120 MB。這是因爲此類報表中可能包含大量指標。

報表元數據結構

以下爲單一報表中的元數據。報表元數據存儲在一個專門的二級索引表當中。

#!/usr/bin/python
# -*- coding: utf-8 -*-
ReportInfo(
   tableName=u’growth_ResurrectionSegmentedReport’,
   reportName=u’growth_ResurrectionSegmentedReport’,
   segInfo={
       u’segKey2': u’gender’,
       u’segKey1': u’country’,
       u’segKeyNum’: u’2',
       },
   metrics={u’resurrection’: MetricMetadata(name=u’resurrection’,
       valueNames=None),
       u’5min_resurrection’:MetricMetadata(name=u’5min_resurrection’
       , valueNames=None)},
   segKeys={
       1: {
         u’1': u’US’,
         u’2': u’UK’,
         u’3': u’CA’,
…
       }
   )

優化表元數據的存儲與檢索使用

報表元數據以序列化blob的形式存儲在HBase的二次索引表中。因此,可以判斷問題的根源在於報表元數據的體量太大,而我們每次都在加載完整元數據——而非僅加載我們需要使用的部分。在高負載情況下,jvm leap可能會很快被填滿,導致jvm無法處理洶涌而來的full GC。

爲了從根本上解決問題,我們決定將報表元數據內容分發至報表行鍵下的多個列族與限定符當中。

經過增強的PinalyticsDB V2報表行鍵結構,分佈在多個列族與限定符之間。

行鍵採用原子更新,因此所有列族都將在單一PUT操作時以原子化方式更新。

我們還創建了一種新的report-info獲取方法。

getReportInfo(final String siTableName, final String reportName, List<String> metrics)

此方法會返回所有seg-info與seg-keys數據,且僅返回“metrics”列族中的相關指標數據。由於報表的大部分內容基於指標數據,因此我們只需要返回幾kb數據,而非完整的100 mb以上數據。

Thrift Server輪循池

我們還對Thrift Server做出另一項變更,旨在實現可擴展性。每個Thrift Server都具有hbase org.apache.hadoop.hbase.client.Connection的單一實例。

hbase.client.ipc.pool.size=5
hbase.client.ipc.pool.type=RoundRobinPool

每個區域服務器默認只擁有1個連接。這樣的設置增加了併發連接數,且有助於我們進一步擴展每個服務器的請求數量。

缺點與侷限

通過以上設計,我們的業務體系運行得更爲順暢。但我們也意識到,其中仍存在一定侷限性。

雖然橫向擴展架構帶來了均勻的讀取寫入分佈,但卻會對可用性造成影響。例如:任何區域服務器或者Zookeeper問題,都會影響到全部讀取與寫入請求。我們正在設置一套具有雙向複製能力的備份集羣,從而在主集羣發生任何問題時實現讀寫機制的自動故障轉移。

由於各個段屬於行鍵的一部分,因此包含多個段的報表必然佔用更多磁盤存儲空間。在創建報表後,我們也無法對報表內的細分內容進行添加或刪除。此外,儘管能夠快速轉發FuzzyRowwFilter,但對於基數很高的報表及大量數據而言,整個過程仍然非常緩慢。相信通過在協處理器中添加併發機制,從而併發執行對每種salt的掃描(甚至按日期進行分區掃描),能夠有效解決這個問題。

這套架構利用協處理器執行讀取,但不支持利用協處理器複製讀取內容。接下來,我們可以通過將結果存儲在高可用性表中實現對緩存層的聚合,從而在一定程度上彌補這種協處理器複製能力缺失問題。另外,我們還會在無法從主區域處讀取數據時,執行副本讀取操作(利用常規Hbase掃描,不涉及協處理器)。

我們計劃在下一次迭代當中解決一部分侷限性問題。此外,我們還計劃添加對前N項、百分位、按…分組以及min/max等函數的支持。

鳴謝:

感謝Rob Claire、Chunyan Wang、Justin Mejorada-Pier以及Bryant Xiao爲PinalyticsDB支持工作做出的貢獻。同樣感謝分析平臺與存儲/緩存團隊爲Pinalytics Web應用以及HBase集羣提供的寶貴支持。

原文鏈接:

https://medium.com/pinterest-engineering/pinalyticsdb-a-time-series-database-on-top-of-hbase-946f236bb29a

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章