本文介紹Phoenix在2345公司的實踐,主要是實時查詢平台的背景、難點、Phoenix解決的問題、Phoenix-Sql的優化以及Phoenix與實時數倉的融合思路。具體内容如下:
實時資料查詢時客服系統中一個很重要的子產品,提供全公司所有主要産品的資料的查詢功能,由于各産品的資料庫、資料表錯綜複雜、形式多樣,在平台建設的初期走了很多彎路。本文後續會詳細介紹實時資料查詢疊代更新的過程、期間遇到的問題以及對應的解決方案。
目前公司的資料庫類型主要有MySQL和MongoDB。它們本身是異構的,二者都會涉及分庫、分表,還有冷表、熱表。分庫分表的字段不同、個數不同;冷熱表的實作方式也不同,有些産品是冷熱雙寫,有些則是熱表過期插入到冷表;還有周表、月表、自定義分表邏輯等。
在實體位置上,這些資料庫執行個體會分布在不同的節點,如果用JDBC查詢需要配置不同的連接配接。再加上根據産品性質,一般會查詢從庫,而由于技術原因,從庫的實體位置會變化,配置起來也會比較麻煩。
系統的疊代程序大概分為四個階段。
初期,産品少、資料庫形式簡單、庫和表數量少,各産品單獨提供查詢頁面。
後面各查詢頁面整合,提供統一的界面,但随着資料庫、表結構形式複雜,開發任務逐漸變得繁重,難以為繼。特别是查詢條件複雜,需要上遊業務表建立對應的索引,對業務具有一定的侵入性。
第三階段,元件單獨團隊開發,将MongoDB、MySQL實時同步到Kafka,屏蔽分庫、分表、冷熱表等結構化差異,基于實時流的訂閱機制,對資料進行聚合插入ElasticSearch和HBase。但會面臨實時流Join,經常丢資料,造成資料不準;性能也跟不上,特别是新增查詢表,需要全量鋪底的時候。後期增加了T+1補數機制,雖然彌補了資料缺失的問題,但容易造成實時邏輯與離線邏輯不一緻的問題。
其實這個階段最大的問題就是實時流的Join,比如下圖中有一個寬表需要四個表進行關聯,每個表之間的關聯字段不同,如果有一個表的資料沒有到,就必須等待。剛開始是SparkStreaming進行微批處理,但每個庫資料的到達順序、時延不同,很容易出現關聯不上的現象,導緻資料丢失。後面有用java代碼重構了SparkStreaming的邏輯,将關聯不上的資料進行緩存,進行重試,但考慮到網絡抖動的問題,不能無限次重試。仍然不能解決資料丢失的問題,由于重試的存在,性能也會受到很大的影響。
最後選擇Phoenix作為資料存儲和查詢引擎,基本解決了所有的難題。資料經過Kafka寫入Phoenix表,不再進行實時ETL也就是沒有實時join 。該方案屏蔽了資料庫異構,形式不同的問題;查詢頁面通過Phoenix-Sql,也解決了實時流Join的問題。雖然沒有解決資料時延,但資料到齊後即可查詢到,不存在重試和丢資料。
下面介紹引入思路。其實就是參考Schema on Read & Schema on Write的概念,把Schema on Write 改成Schema on Read ,其實就是計算邏輯後置到查詢的時候。雖然簡單,但有時候思路決定出路。
Schema on Write 定義好目标表結構,根據定義寫入資料,需要提前進行關聯清洗;Schema on Read 直接源表資料進行查詢,不再提前進行清洗,優先寫入資料。知道了二者的差别,改造起來也變得簡單了。
思路有了,就要選擇技術。其實根據上面的思路,選擇用MySQL做資料存儲和查詢引擎也是可以的。但考慮到上遊業務庫的數量和資料量,同步到一個MySQL壓力還是很大的。如果并發寫入,MySQL可能扛不住,串行則寫入性能低下。綜合Phoenix的優點,最終選擇了它。
Phoenix可以幫助我們屏蔽資料源的異構、資料庫SCHEMA和表的異構,統一了資料視圖。
有了思路和技術方案,改造起來就順利多了。簡單來說就是把上遊業務庫的資料簡單的同步到Phoenix中,結構相同的資料寫入同一張Phoenix表,比如分庫分表、冷熱表;對Phoenix表按照查詢條件建立二級索引;查詢頁面調用背景的SQL語句即可。但注意Phoenix表的主鍵是源表分庫分表的序号和源表主鍵組合而成。
改造後的實時查詢平台,變得簡單、高效、穩定、靈活,使用者體驗非常好。
高性能的SQL是系統更新的關鍵。如果查詢性能和寫入性能跟不上,會直接宣告更新失敗。
寫入性能跟HBase的節點數、預分區個數有關,這裡不再詳細介紹。主要講解如何進行查詢優化,簡單來說就是建立合理的索引。
Phoenix的索引類型有以下四種。
全局索引适用于多讀少寫的場景。寫入時可能跨region。
本地索引适用于寫多讀少,空間有限的場景,和全局索引一樣,Phoneix在查詢時會自動選擇是否使用本地索引,為避免進行寫操作所帶來的網絡開銷,索引資料和表資料都存放在相同的伺服器中,當查詢的字段不完全是索引字段時本地索引也會被使用,與全局索引不同的是,所有的本地索引都單獨存儲在同一張共享表中,由于無法預先确定region的位置,是以在讀取資料時會檢查每個region上的資料因而帶來一定性能開銷。
覆寫索引隻需要通過索引就能傳回所要查詢的資料,是以索引的列必須包含所需查詢的列(SELECT的列和WHRER的列)。
函數索引從Phoeinx4.3開始支援函數索引,其索引不局限于列,可以合适任意的表達式來建立索引,當在查詢時用到了這些表達式時就直接傳回表達式結果。
此次改造我們隻使用了全局索引,因為如果使用得當,完全沒有使用其他三種類型索引的必要。
全局索引加HINT是一種常見的優化方案,因為如果不顯式的加HINT,是不會走索引的。但這種方式的問題就是索引的名稱無法修改。
覆寫索引會無形增加存儲的壓力,個人覺得很不劃算,而且如果臨時需要增加一個查詢字段,是需要修改索引的。如果源表資料量很大,重建索引的代價是非常大的。
禁止索引也是一種正常優化。如果不是字首掃描,查詢索引的性能損耗是非常大的,得不償失,此時禁止索引是最好的選擇。
進階優化有兩種方式,這需要對Phoenix的索引原理有深刻的了解,才能知道這兩種優化的由來及其原理。Inner-join其實深刻了解了走Phoenix索引的機制:當查詢字段全都在索引表中時才會走索引。Phoenix索引包含索引字段和主鍵,通過inner-join,其中一個表隻查詢索引字段和主鍵,則一定會走索引,另一個表通過主鍵查詢其他字段是很快的。是以,這個inner-join先通過查詢字段找到對應資料的主鍵,再通過主鍵反查資料表得到資料的其他字段,而索引查詢和主鍵查詢都是非常快的。
直接查詢索引表,需要了解Phoenix索引其實就是一個普通的Phoenix表。可以直接關聯索引表進行查詢,這種方式跟inner-join差不多,隻不過更直接,且不會受索引狀态的影響。如果索引失效,inner-join也會很慢,但這種方式就不會。
索引的建立也是我們此次優化的重點。
建立同步索引适用于小表;設定預分區和禁止WAL一般适用大表;對于特大表,一般使用官方提供的IndexScrutinyTool工具建立異步索引。但該工具的原理是建立MR作業,并行掃描源表然後建立索引,其實速度還是有一定限制的。如果表資料量過大,該工具的性能也不太好,而且也容易失敗。
經過研究以及對Phoenix索引原理的深刻了解,我們對異步索引的建立方式進行了改進。資料在導入Phoenix之前,HIVE中會有一份全量資料,一般進行離線分析。而我們可以通過HQL将該資料按照索引的格式,直接導出為HFile檔案,再通過HBase提供的bulkLoad将索引的HFile直接導入,最後将其狀态改成ACTIVE即可完成索引的建立。此種方式的性能最高,但需要額外的全量資料和HIVE技術棧。
Phoenix在完成對資料查詢平台的改造和更新之後,還可以做更多的事。Phoenix是對HBase的擴充,資料存入Phoenix也就是存入HBase,所有其他基于HBase的應用都可以建立,再加上hive映射HBase進行離線分析,資料查詢平台就變成了資料中台。
下面是我們基于Phoenix資料做的應用擴充,可以發現該架構下,既可以做實時查詢,又可以做T+1離線分析,還可以做準實時(每小時或每30分鐘甚至更短)報表。
可以說上面的架構圖極大的擴充了我們資料服務的邊界。
最後說一下我們遇到的難題,以及對應的解決方案。
首先就是資料鋪底的問題。Phoenix表是先導入資料,再建立索引,還是先建立索引再導入資料呢?我們選擇了前者,因為資料和索引我們都是通過bulkLoad的方式導入的,速度非常快。
補數是必不可少的。無論是CDC故障還是網絡抖動,資料總是有可能丢失的。映射Phoenix底層的HBase表後,我們會使用HQL對資料進行校驗,計算出丢失的資料,再通過HIVE映射Phoenix表将資料補進去。
資料覆寫的問題也是很頭疼的。其實就是同一個ID的兩條更新資料沒有按照順序寫入Phoenix時,如何確定最新的資料被寫入。補數和實時寫入就引起有順序錯亂的問題,比如補數腳本運作時候,發現資料缺失,在補數之前,實時又寫入了最新資料,補數就會覆寫這個最新資料,造成資料的不一緻。
通過修改Phoenix的源碼,我們完美解決了這個問題。
簡單來說就是增加了一個ROW_TS資料類型,該資料類型的字段的值會寫入底層HBase的rowtimestamp,也就是資料的版本号。這樣無論資料的寫入順序是怎樣的,隻要資料的更新時間映射到HBase的rowtimestamp就不會有問題,因為查詢時候隻能查詢到版本最大也就是資料更新時間最大的那條記錄。
時效性其實比較麻煩。目前CDC是接入到業務庫的從庫,如果從庫資料有延時,Phoenix的資料就一定有延時,且不可控。另外如果叢集寫入壓力過大,寫入也會有延時。
離線分析性能也需要優化。Hive映射HBase進行分析時,也會遇到性能問題。比如分析師隻需要查詢最近幾天的資料,在Hive中可以按照時間建立分區表,但Phoenix并沒有分區的概念。後期我們将考慮修改Phoenix源碼,增加分區的概念,将Phoenix表映射為底層不同的HBase表,也就是每個分區對應一個HBase表。同時具有統一的查詢視圖,也就是屏蔽分區表的底層邏輯。這樣Hive就可以映射對應的分區HBase表,減少資料讀取的數量。