1. 程式人生 > >淺談Phoenix在HBase中的應用

淺談Phoenix在HBase中的應用

put core get 版本控制 not 反饋 sage one info

一、前言

業務使用HBase已經有一段時間了,期間也反饋了很多問題,其中反饋最多的是HBase是否支持SQL查詢和二級索引,由於HBase在這兩塊上目前暫不支持,導致業務在使用時無法更好的利用現有的經驗來查詢HBase。雖然HBase本身不支持SQL,但業界還是有現成的方案來支持,如Hive、Impala、Phoenix等。眾多方案各有各的優勢,本文主要對Phoenix作一個大概的介紹。

Phoenix中文翻譯為鳳凰, 其最早是Salesforce的一個開源項目,Salesforce背景是一個搞ERP的,ERP軟件一個很大的特點就是數據庫操作,所以能搞出一個數據庫中間件也是很正常的。而後,Phoenix成為Apache基金的頂級項目。

Phoenix具體是什麽呢,其本質是用Java寫的基於JDBC API操作HBase的開源SQL引擎。它有如下幾個功能特性:

技術分享圖片

圖1.phoenix功能特性

我覺得值得關註的幾個特性主要有以下幾塊:

  • 通過JDBC API實現了大部分的java.sql接口,包括元數據API
  • DDL支持:通過CREATE TABLE、DROP TABLE及ALTER TABLE來添加/刪除
  • DML支持:用於逐行插入的UPSERT VALUES,用於相同或不同表之間大量數據傳輸的UPSERT SELECT,用於刪除行的DELETE
  • 事務支持:通過客戶端的批處理實現的有限的事務支持(beta測試中)
  • 二級索引支持:
  • 遵循ANSI SQL標準

當前使用Phoenix的公司有很多,如下圖所示:

![2]
圖2.phoenix使用公司

對於我們公司來說,雖然HBase用得多,但用Phoenix的比較少。從自己測試來看,Phoenix確實還存在各種不穩定,如下面描述的幾點問題:

  • 最新版本對HBase、Hadoop等有嚴格版本控制,對於已經用上HBase的業務來說要升級HBase版本適配Phoenix代價太大
  • 與HBase強相關,作為HBase中的一個組件啟動,HBase元數據容易遭到破壞
  • 官方提供的創建索引方法,容易導致插入失敗,查詢失敗,程序崩潰等問題

我覺得Phoenix總體思路還是很不錯的,但本身太冒進,急於集成新功能,但現有的功能所存在的問題卻並未有很好的解決方案,導致版本很多,但沒有一個版本能放心在生產環境使用。下面關註一下Phoenix的整體設計思路。

二、Phoenix架構

上面說到,Phoenix是以JDBC驅動方式嵌入到HBase中的,在部署時只有一個包,直接放HBase的lib目錄,邏輯構架如下:

![3]
圖3.phoenix_structure

從圖中可看出,每個RS結點上,都會有一個Phoenix協處理器來處理每個表、每個region的數據,應用端通過Phoneix客戶端與HBase客戶端打交道,從而實現Sql化訪問HBase數據。下面先來說下Coprocessor。

2.1 Coprocessor

HBase的協處理器主要受Google BigTable的影響,具體可參考Dean-Keynote-Ladis2009-page 66-67。 對於HBase來說,引入Coprocessor也是為了提供更好的並行計算能力,而無需依賴於Hadoop的MapReduce。同時,基於Coprocessor,可以更好的實現二級索引、復雜過濾規則、權限訪問控制等更接地氣的特性。Coprocessor有兩種類型,ObserverEndPoint

前者Observer,類似於RDBMS的觸發器,主要作用於RegionServer服務端,通過重載Coprocessor框架的Upcall函數插入用戶自己的邏輯,這些邏輯只有在固定的事件發生時才會被觸發調用執行,主要有三類hook接口:RegionObserverWALObserverMasterObserver。RegionObserver提供了一些數據層操作事件的hook,如Put、Get、Delete和Scan等,在每個操作發生或結束時,會觸發調用一些前置的Hook(pre+操作,如preGet)或後置的Hook(post+操作,如postGet);WALObserver提供了WAL相關的Hook;MasterObserver提供了HMaster相關的Hook。

後者EndPoint類似於RDBMS的存儲過程,主要作用於客戶端,客戶端可以調用這些EndPoint執行一段Server端代碼,並將Server端代碼結果返回給客戶端進一步處理,如常見聚合操作,找一張大表某個字段的最大值,如果不用Coprocesser則只能全表掃描,在客戶端遍歷所有結果找出最大值,且只能利用有限的客戶端資源進行叠代計算,無法利用上HBase的並發計算能力;如果用了Coprocessor,則client端可在RegionServer端執行統計每個Region最大值的邏輯,並將Server端結果返回客戶端,再找出所有Server端所返回的最大值中的最大值得到最終結果,很明顯,這種方式盡量將統計執行下放到Server端,Client端只執行一些最後的聚合,大幅提高了統計效率;還有一個很常見的需求可能就是統計表的行數,其邏輯和上面一樣,具體可參考Coprocessor Introduction,在這裏就不展開了,後面有機會針對Coprocessor單獨展開介紹。

2.2 Phoenix 實現原理

Phoenix的SQL實現原理主要也是基於一系列的Scan操作來完成,Scan是HBase的批量掃描過程。這一系列的Scan操作也是分散到各臺RegionServer上通過Coprocessor來完成。主要用到的是RegionObserver,通過RegionObserver在postScannerOpen Hook中將RegionScanner替換成支持聚合操作的定制化Scanner,在真正執行聚合時,會通過自定的Scan屬性傳遞給RegionScanner,在這個Scan中也可加入一些過濾規則,盡量減少返回Client的結果。

2.3 Phoenix 數據模型

Phoenix在數據模型上是將HBase非關系型形式轉換成關系型數據模型 ,如下圖所示

技術分享圖片

圖4.Phoenix Data Model

對於Phoenix來說,HBase的rowkey會被轉換成primary key,column family如果不指定則為0否則字段名會帶上,qualifier轉換成表的字段名,如下是創建一個Phoenix表的例子,以創建表test為例,主鍵為id即為HBase的rowkey, column family為i, qualifier為name和age。

create table "test" ("id" varchar(20) primary key,"i"."name" varchar(20) ,"i"."age" varchar(20));

Phoenix還支持組合primary key,即由多個字段聯合組成主鍵,對於組合主鍵來說,在HBase底層會把主鍵的多個字段組合成rowkey顯示,其它字段為HBase的qualifier顯示。如上面test表,假設id和name為主鍵,創建表語句又變成:

create table "test" ("id" varchar(20), "name" varchar(20) ,"i"."age" varchar(20),constraint pk PRIMARY KEY("id","name"));

這樣,假設插入一條數據:如下所示

upsert into "test" values ('1','a','23');

在HBase中,rowkey即為"1a", i:age 為 23。這裏,可能大家對雙引號有點疑問,對於Phoenix來說,加了引號的話,不管是表還是字段名,會變成大小寫敏感,不加的話,會統一轉換成大寫字母。

2.4 Phoenix所支持的語法

目前Phoenix已經支持關系型數據庫的大部分語法,如下圖所示:

技術分享圖片

圖4.Phoenix 語法

具體語法用法可參考Phoenix官網,寫得比較詳細。

三、 Phoenix二級索引

我相信,二級索引這個特性應該是大部分用戶引入Phoenix主要考慮的因素之一。HBase因其歷史原因只支持rowkey索引,當使用rowkey來查詢數據時可以很快定位到數據位置。現實中,業務查詢需求條件往往比較復雜,帶有多個查詢字段組合,如果用HBase查的話,只能全表掃描進行過濾,效率很低。而Phoenix支持除rowkey外的其它字段的索引創建,即二級索引,查詢效率可大幅提升。

3.1 索引類別

3.1.1 Covered Indexes

從字面上可理解為覆蓋索引,什麽意思呢,即索引表中就包含你想要的全部字段數據,這樣就只需要通過訪問索引表而無需訪問主表就能得到數據。創建方式如下:

create index my_index on test (v1) include(v2);

當執行select v2 from test where v1=‘...‘時,就只會查找索引表數據,不會去主表掃描。

3.1.2 Global Indexes

全局索引適用於讀多寫少的場景。全局索引在寫數據時會消耗大量資源,所有對數據的增刪改操作都會更新索引表,而索引表是分布在各個結點上的,性能會受到影響。好處就是,在讀多的場景下如果查詢的字段用到索引,效率會很快,因為可以很快定位到數據所在具體結點region上,對於寫性能就很慢了,因為每寫一次,需要更新所有結點上的索引表數據。創建方式如下:

create index my_index on test (v1);

如果執行`select v2 from test where v1=‘...‘, 實際是用不上索引的,因為v2不在索引字段中,對於全局索引來說,如果查詢的字段不包含在索引表中,則還是會去全表掃描主表。

3.1.3 Local Indexes

局部索引適用於寫多讀少場景,和全局索引類似,Phoenix會在查詢時自動選擇是否使用索引。如果定義為局部索引,索引表數據和主表數據會放在同一regionserver上,避免寫操作時跨節點寫索引表帶來的額外開銷(如Global Indexes)。當使用局部索引查詢時,即使查詢字段不是索引字段,索引表也會正常使用,這和Global Indexes是有區別的。在4.8版本之前,所有局部索引數據存放在一個單獨的共享表中,4.8之後是存儲在主表的一個獨立的列族中。因為是局部索引,所以在client端查詢使用索引時,需要掃描每個結點上的索引表以得到數據所在具體region位置,當region多時,查詢時耗會很高,所以查詢性能比較低,適合讀少寫多場景。創建局部索引方式:

create local index my_index on test (v1);

3.2 Mutable Indexing 和Immutable Indexing

3.2.1 IMMutable Indexing

不可變索引主要創建在不可變表上,適用於數據只寫一次不會有Update等操作,在什麽場景下會用到不可變索引呢,很經典的時序數據:write once read many times。在這種場景下,所有索引數據(primary和index)要麽全部寫成功,要麽一個失敗全都失敗返回錯誤給客戶端。不可變索引用到場景比較少,下面是創建不可變索引的方式:

create table test (pk VARCHAR primary key,v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true;

即在創建表時指定IMMUTABLE_ROWS參數為true,默認這個參數為false。如果想把不可變索引改為可變索引,可用alter修改:

alter table test set IMMUTABLE_ROWS=false;

3.2.2 Mutable Indexing

可變索引意思是在修改數據如Insert、Update或Delete數據時會同時更新索引。這裏的索引更新涉及WAL,即主表數據更新時,會把索引數據也同步更新到WAL,只有當WAL同步到磁盤時才會去更新實際的primary/index數據,以保證當中間任何一個環節異常時可通過WAL來恢復主表和索引表數據。

四、性能

在官網,有作一個性能測試,主要是將Phoenix和Hive、Impala作一個對比。
先來看下和Hive的性能對比,測試基準如下:

 select count(1) from table over 10M and 100M rows. Data is 5 narrow columns. Number of Region Servers: 4 (HBase heap: 10GB, Processor: 6 cores @ 3.3GHz Xeon)

測試結果:

技術分享圖片

圖6.Phoenix性能對比

從圖中可看出,帶有Key過濾的Phoenix耗時最少,不帶Key過濾的Phoenix和基於HDFS的Hive性能差不多,直接基於HBase的Hive性能最差。

再來看下和Impala的對比,測試基準如下:

select count(1) from table over 1M and 5M rows. Data is 3 narrow columns. Number of Region Server: 1 (Virtual Machine, HBase heap: 2GB, Processor: 2 cores @ 3.3GHz Xeon)

測試結果:

技術分享圖片

圖7.Phoenix性能對比Impala

從圖中可看出,Impala執行時間比Phoenix長很多,原因大概有幾點:Impala基於內存進行並行計算,容易內存吃緊,對HBase和HDFS的支持也還遠遠不夠,性能比較差。

我在自己的HBase測試集群也作了下測試,主要測試數據插入和一些SQL操作的查詢時耗。測試集群如下:

![11]
圖8.測試集群

先來測試下插入100萬記錄的測試基準,如下所示:

  • 1.創建基本表,表主鍵由4個字段組成,HOST字段稱為First PK,DOMAIN為Second PK, 依此類推,SPLIT ON指定8個分區。
CREATE TABLE IF NOT EXISTS %s (HOST CHAR(2) NOT NULL,
DOMAIN VARCHAR NOT NULL, 
FEATURE VARCHAR NOT NULL,
DATE DATE NOT NULL,
USAGE.CORE BIGINT,
USAGE.DB BIGINT,
STATS.ACTIVE_VISITOR INTEGER
CONSTRAINT PK PRIMARY KEY (HOST, DOMAIN, FEATURE, DATE))  
SPLIT ON   ('CSGoogle','CSSalesforce','EUApple','EUGoogle','EUSalesforce','NAApple','NAGoogle','NASalesforce')
  • 2.插入100萬行記錄
  • 3.執行如下查詢條件測試
Query # 1 - Count - SELECT COUNT(1) FROM PERFORMANCE_1000000;
Query # 2 - Group By First PK - SELECT HOST FROM PERFORMANCE_1000000 GROUP BY HOST;
Query # 3 - Group By Second PK - SELECT DOMAIN FROM PERFORMANCE_1000000 GROUP BY DOMAIN;
Query # 4 - Truncate + Group By - SELECT TRUNC(DATE,'DAY') DAY FROM PERFORMANCE_1000000 GROUP BY TRUNC(DATE,'DAY');
Query # 5 - Filter + Count - SELECT COUNT(1) FROM PERFORMANCE_1000000 WHERE CORE<10;

測試結果如下:

    1. 插入100萬條記錄耗時70s
    1. Query #1 耗時1.032s
    1. Query #2 耗時0.025s
    1. Query #3 耗時0.615s
    1. Query #4 耗時0.608s
    1. Query #5 耗時1.026s

具體結果如下:

csv columns from database.
CSV Upsert complete. 1000000 rows upserted
Time: 69.672 sec(s)

                                COUNT(1) 
---------------------------------------- 
                                 1000000 
Time: 1.032 sec(s)

HO 
-- 
CS 
EU 
NA 
Time: 0.025 sec(s)

DOMAIN                                   
---------------------------------------- 
Apple.com                                
Google.com                               
Salesforce.com                           
Time: 0.615 sec(s)

DAY                     
----------------------- 
2018-01-28 00:00:00.000 
2018-01-29 00:00:00.000 
2018-01-30 00:00:00.000 
2018-01-31 00:00:00.000 
2018-02-01 00:00:00.000 
2018-02-02 00:00:00.000 
2018-02-03 00:00:00.000 
2018-02-04 00:00:00.000 
2018-02-05 00:00:00.000 
2018-02-06 00:00:00.000 
2018-02-07 00:00:00.000 
2018-02-08 00:00:00.000 
2018-02-09 00:00:00.000 
Time: 0.608 sec(s)

                                COUNT(1) 
---------------------------------------- 
                                   20209 
Time: 1.026 sec(s)

還作了下三種不同數量級下的性能對比,作了5種SQL查詢操作對比,如上測試基準第3條所描述的查詢條件,結果如下:

技術分享圖片

圖9.Phoenix不同數據量級測試對比

從結果看,隨著數量級的增加,查詢時耗也隨之增加,有一個例外,就是當用First PK索引字段作聚合查詢時,用時相差不大。總的來說,Phoenix在用到索引時查詢性能會比較好。那對於Count來說,如果不用Phoenix,用HBase自帶的Count耗時是怎樣的呢,測了一下,HBase Count 100萬需要33s, 500萬需要139s,1000萬需要284s,性能還是很差的。對於大表來說基本不能用Count來統計行數,還得依賴於基於Coprocessor機制來統計。

從上面測試來看下,Phoenix的性能不能說最好,也存在各種問題,就如開篇說的,版本不穩定,BUG過多,容易影響集群穩定性。

五、總結

總的來說,目前並沒有一種很完美的方案來解決SQL查詢、二級索引問題,都或多或少存在各種問題。不過HBase的Coprocessor是個好東西,很多功能可以基於此特性進行二次開發,後續可以深入研究一下。

六、參考

[1] https://community.hortonworks.com/articles/61705/art-of-phoenix-secondary-indexes.html

[2] https://github.com/forcedotcom/phoenix/wiki/Secondary-Indexing

[3] http://phoenix.apache.org/secondary_indexing.html

淺談Phoenix在HBase中的應用