轉自:阿里雲社區
Phoenix是什么
Phoenix查詢引擎支持使用SQL進行HBase數據的查詢,會將SQL查詢轉換為一個或多個HBase API,協同處理器與自定義過濾器的實現,並編排執行。使用Phoenix進行簡單查詢,其性能量級是毫秒。
更多的信息可以參考官網
Phoenix使用建議
1、 二級索引使用指南
二級索引是Phoenix中非常重要的功能,用戶在使用前需要深入了解,可以參考資料:二級索引社區文檔,二級索引中文文檔,全局索引設計實踐。
下面介紹一些在使用二級索引過程中的注意事項:
1)是否需要使用覆蓋索引?
覆蓋索引需要將查詢返回字段加入到索引表中,這樣在命中索引時,只需要查詢一次索引表即可,非覆蓋索引,要想拿到完整結果則需要回查主表。不難理解,覆蓋索引查詢性能更好,但是會浪費一定存儲空間,影響一定寫性能。非覆蓋索引使用時,有時執行計划並不能默認命中索引,此時,用戶需要加索引Hint。
2)應該使用local Index還是global Index?實現上,一個global index表對應着一個hbase 表,local index是在主表上新增一列存儲索引數據。
適用場景上,global index 適用於多讀的場景,但存在同步索引時帶來網絡開銷較大的問題。而local由於和原數據存儲在一張表中同步索引數據會相對快一點。雖然local index也有一定適用場景,但仍然推薦使用global index, 其原因有以下幾點:
- 當前版本的phoneix的local index的實現相對global index不太完善,問題較多,使用存在一定的風險。
- local index不太完善,大的改動后,可能會存在不兼容,升級流程比較復雜。
- 在大數據量下,原始數據和索引數據放在一起會加劇region分裂,且分裂后索引數據的本地性也會喪失。
因此,在阿里雲HBase SQL服務中LOCAL INDEX功能已經被禁止!
3)索引表最多可以創建多少個?
索引會保證實時同步,也會引來寫放大問題,一般建議不超過10個,如果超過建議使用HBase全文索引功能。
4) 構建索引需要注意哪些事項?
使用創建索引語句(CREATE INDEX)時,如果指定async參數,則為異步構建,語句完成時,會在SYSTEM.CATALOG表中簡歷索引表的元信息,並建立跟主表的關系,但是狀態是building,索引表中沒有數據,也不可查,需要后續用REBUILD語句來構建,或者借助MR工具來構建索引,並將狀態更新為active。如果不指定,則為同步構建,一般建議數據量小於1億行都使用同步構建即可,比較簡單。
2、 加鹽注意事項
注意:本特性有副作用,不建議使用。使用前請確保您已經充分了解其原理與適用場景。如果您無法確定您的場景是否適合,請釘釘與我們聯系,我們會幫您進行評估。
加鹽通常用來解決數據熱點和范圍查詢同時存在的場景。原理介紹可以參考:Phoenix社區文檔和中文文檔。
加鹽有較強的適用場景要求,場景不合適將適得其反:
- 寫熱點或寫不均衡:比如以時間作為第一列主鍵,永遠寫表頭或者表尾
- 需要范圍查詢:要按第一列主鍵進行范圍查詢,故不能使用hash打散
有熱點就要打散,但打散就難以做范圍查詢。因此,要同時滿足這對相互矛盾的需求,必須有一種折中的方案:既能在一定程度上打散數據,又能保證有序。這個解決方案就是加鹽,亦稱分桶(salt buckets)。數據在桶內保序,桶之間隨機。寫入時按桶個數取模,數據隨機落在某個桶里,保證寫請求在桶之間是均衡的。查詢時讀取所有的桶來保證結果集的有序和完備。
一般來說,嚴格滿足上述條件的業務場景並不常見。大多數場景都可以找到其他的業務字段來協助散列。考慮到其嚴重的副作用,我們不建議使用這個特性。
副作用:
- 寫瓶頸:一般全表只有buckets個region用於承擔寫。當業務體量不斷增長時,因為無法調整bucket數量,不能有更多的region幫助分擔寫,會導致寫入吞吐無法隨集群擴容而線性增加。導致寫瓶頸,從而限制業務發展。
- 讀擴散:select會按buckets數量進行拆分和並發,每個並發都會在執行時占用一個線程。select本身一旦並發過多會導致線程池迅速耗盡或導致QueryServer因過高的並發而FGC。同時,本應一個RPC完成的簡單查詢,現在也會拆分成多個,使得查詢RT大大增加。
這兩個副作用會制約業務的發展,尤其對於大體量的、發展快速的業務。因為桶個數不能修改,寫瓶頸會影響業務的擴張。讀擴散帶來的RT增加也大大降低了資源使用效率。
常見的使用誤區:
- 預分區:用分桶來實現建表的預分區最常見的誤用。這是因為Phoenix提供的split on預分區語法很難使用。目前可用hbase shell的建表,指定預分區,之后關聯為Phoenix表。在海量數據場景,合理的預分區是一個很有挑戰的事情,我們后續會對此進行專題討論,敬請期待。
- 偽熱點:寫入熱點或不均衡大多數情況都是假象,通常還有其他字段可用於打散數據。比如監控數據場景,以metric名字的hash值做首列主鍵可有效解決寫入均衡的問題。
一定不要為了預分區而使用加鹽特性,要結合業務的讀寫模式來進行表設計。如果您無法做出准確的判斷,可以釘釘聯系雲HBase值班咨詢。
Buckets個數跟機型配置和數據量有關系,可以參考下列方式計算,其中 N 為 Core/RS 節點數量:
- 單節點內存 8G: 2*N
- 單節點內存 16G: 3*N
- 單節點內存 32G: 4*N
- 單節點內存 64G: 5*N
- 單節點內存 128G: 6*N
注意:索引表默認會繼承主表的鹽值;bucket的數目不能超過256;一個空的Region在內存中的數據結構大概2MB,用戶可以評估下單個RegionServer承載的總Region數目,有用戶發生過在低配置節點上,建大量加鹽表直接把集群內存耗光的問題。
3、 慎用掃全表、OR、Join和子查詢
雖然Phoenix支持各種Join操作,但是Phoenix主要還是定位為在線數據庫,復雜Join,比如子查詢返回數據量特別大或者大表Join大表,在實際計算過程中十分消耗系統資源,會嚴重影響在線業務,甚至導致OutOfMemory異常。對在線穩定性和實時性要求高的用戶,建議只使用Phoenix的簡單查詢,且查詢都命中主表或者索引表的主鍵。另外,建議用戶在運行SQL前都執行下explain,確認是否命中索引,或者主鍵,參考explain社區文檔和explain中文文檔。
4、 Phoenix不支持復雜查詢
Phoenix的二級索引本質還是前綴匹配,用戶可以建多個二級索引來增加對數據的查詢模式,二級索引的一致性是通過協處理器實現的,索引數據可以實時可見,但也會影響寫性能,特別是建多個索引的情況下。對於復雜查詢,比如任意條件的and/or組合,模糊查找,分詞檢索等Phoenix不支持。
5、 Phoenix不支持復雜分析
Phoenix定位為操作型分析(operational analytics),對於復雜分析,比如前面提到的復雜join則不適合,這種建議用Spark這種專門的大數據計算引擎來實現,參考X-Pack Spark分析服務, HBase SQL(Phoenix)與Spark的選擇。
6、 Phoenix是否支持映射已經存在的HBase表?
支持,參考社區相關文檔。用戶可以通過Phoenix創建視圖或者表映射已經存在的HBase表,注意如果使用表的方式映射HBase表,在Phoenix中執行DROP TABLE語句同樣也會刪除HBase表。另外,由於column family和列名是大小寫敏感的,必須一一對應才能映射成功。另外,Phoenix的字段編碼方式大部分跟HBase的Bytes工具類不一樣,一般建議如果只有varchar類型,才進行映射,包含其他類型字段時不要使用映射。
import
java.io.IOException;
import
java.util.ArrayList;
import
java.util.List;
import
com.google.common.base.Strings;
import
org.apache.hadoop.conf.Configuration;
import
org.apache.hadoop.hbase.TableName;
import
org.apache.hadoop.hbase.client.Connection;
import
org.apache.hadoop.hbase.client.ConnectionFactory;
import
org.apache.hadoop.hbase.client.RegionLocator;
import
org.apache.hadoop.hbase.mapreduce.TableInputFormat;
import
org.apache.hadoop.hbase.mapreduce.TableSplit;
import
org.apache.hadoop.hbase.util.Bytes;
import
org.apache.hadoop.hbase.util.Pair;
import
org.apache.hadoop.mapreduce.InputSplit;
import
org.apache.hadoop.mapreduce.JobContext;
public
class
SaltRangeTableInputFormat
extends
TableInputFormat {
@Override
public
List<InputSplit> getSplits(JobContext context)
throws
IOException {
Configuration conf = context.getConfiguration();
String tableName = conf.get(TableInputFormat.INPUT_TABLE);
if
(Strings.isNullOrEmpty(tableName)) {
throw
new
IOException(
"tableName must be provided."
);
}
Connection connection = ConnectionFactory.createConnection(conf);
val table = TableName.valueOf(tableName)
RegionLocator regionLocator = connection.getRegionLocator(table);
String scanStart = conf.get(TableInputFormat.SCAN_ROW_START);
String scanStop = conf.get(TableInputFormat.SCAN_ROW_STOP);
Pair<
byte
[][],
byte
[][]> keys = regionLocator.getStartEndKeys();
if
(keys ==
null
|| keys.getFirst() ==
null
|| keys.getFirst().length ==
0
) {
throw
new
RuntimeException(
"At least one region is expected"
);
}
List<InputSplit> splits =
new
ArrayList<>(keys.getFirst().length);
for
(
int
i =
0
; i < keys.getFirst().length; i++) {
String regionLocation = getTableRegionLocation(regionLocator, keys.getFirst()[i]);
String regionSalt =
null
;
if
(keys.getFirst()[i].length >
0
) {
regionSalt = Bytes.toString(keys.getFirst()[i]).split(
"-"
)[
0
];
}
byte
[] startRowKey = Bytes.toBytes(regionSalt +
"-"
+ scanStart);
byte
[] endRowKey = Bytes.toBytes(regionSalt +
"-"
+ scanStop);
InputSplit split =
new
TableSplit(TableName.valueOf(tableName),
startRowKey, endRowKey, regionLocation);
splits.add(split);
}
return
splits;
}
private
String getTableRegionLocation(RegionLocator regionLocator,
byte
[] rowKey)
throws
IOException {
return
regionLocator.getRegionLocation(rowKey).getHostname();
}
}
