【華爲雲技術分享】HBase與AI/用戶畫像/推薦系統的結合:CloudTable標籤索引特性介紹

標籤數據已經成爲越來越普遍的一類數據,以用戶畫像場景最爲典型,如在電商場景中,這類數據可被應用於精準營銷推薦。常見的用戶畫像標籤數據舉例如下:

  • 基礎信息:如性別,職業,收入,房產,車輛等。

  • 購買能力:消費水平、敗家指數等。

  • 行爲特徵:活躍程度,購物類型,起居時間等。

  • 興趣偏好:品牌偏好,顏色偏好,價格偏好等。

在AI、圖計算、時序、時空數據領域,標籤數據也是關鍵的構成部分:

  • AI領域:人工標註通常爲標籤,而預測結果中也可能包含標籤信息。

  • 圖計算領域:無論是實體還是關係數據,都包含各類標籤數據。

  • 時序數據領域:可使用標籤來描述不同的Timelines。

  • 時空數據領域:基於時空條件查詢時,也通常帶有標籤特徵來進行結果過濾:如在地圖上劃定一個區域,查詢在某一個時間區間內在該範圍內出現過的具有某些特徵的嫌疑人。

標籤通常與某類實體對象(Entity)有關,每一個Entity所擁有的標籤數量不等,這屬於典型的半結構數據場景,HBase的"稀疏矩陣"模型天然適合存儲這類數據,但HBase受限的查詢能力卻極大的限制了基於靈活標籤組合條件的數據探索能力。CloudTable提出了一種新的HBase索引思路,能夠基於預定義的規則自動提取標籤並且構建標籤索引,在TB/PB級別標籤數據規模下,達到ms級別的組合標籤ad-hoc查詢能力。這套方案目前已經在推薦系統、用戶畫像標籤平臺以及某客戶的時空搜索平臺等多個項目中得到了應用。在2017年首屆HBaseCon Asia大會上,我們曾經做過簡單的披露,本文將呈現更豐富的內容。

本文內容共分爲4個部分:

  1. HBase爲何適用於標籤數據存儲?

  2. 現有的標籤數據存儲方案與技術挑戰。

  3. 基於倒排/位圖索引的標籤索引技術。

  4. 方案設計思路簡介。

HBase天然適合標籤數據存儲

HBase的數據表,是一種"稀疏矩陣"的結構,沒有嚴格的列結構定義,或者說,每一行都可以擁有自己的列定義。

舉例說明如下:

Row1: {[Age:10_20], [City:Shenzhen]}

Row2: {[Age:20_30], [Occupation:Teacher]}

Row3: {[Age:30_40], [City:Guangzhou], [Occupation:Accountant]}

注: 假設Row1, Row2, Row3爲RowKey,而"{""}"中的部分爲這一行數據中所包含的所有列,每一個"[""]"中的內容一個列。可以看出來:不同的行所包含的列的集合可能不同。

如果將擁有標籤數據的對象稱之爲一個實體,實體可理解成人、車輛、手機號碼、圖片等等,每一個實體所擁有的標籤數量是不固定的,這天然與HBase數據表的"稀疏矩陣"特點吻合,再結合HBase自身的高性能隨機讀寫能力、強數據一致性、優秀的線性擴展能力等特點,促成了很多標籤應用選型HBase作爲數據存儲系統。

現有技術難以滿足標籤數據需求

關於標籤數據,支持靈活的標籤組合條件查詢(ad-hoc查詢)是一個普遍的需求,但基於原生HBase接口能力卻難以達成此需求,最關鍵的原因在於HBase僅僅支持基於RowKey的快速查詢能力,這意味着原生HBase所能提供的查詢場景是有限的、確定的。如果要支持標籤組合條件的ad-hoc查詢,需要對HBase進行擴展,或者依賴於第三方技術來實現。常見的實現思路,有如下幾種:

自建索引表

方案思路

方案共涉及到兩個表:

  • Entity表:數據表,用來存儲每一個Entity所關聯的索引,通常以Entity ID信息作爲RowKey。可以通過Entity ID快速獲取一個Entity所擁有的標籤集合。

  • Tag表:索引表,以Tag爲RowKey,這樣,通過一個Tag可以快速獲取擁有這個Tag的RowKey列表。多個Tag組合計算時,分別提取出每一個Tag所關聯的RowKey列表後再進行組合計算。

方案缺點

索引表部分涉及到大量的二次開發量,如如何高效存儲RowKey列表,如何進行Tag組合計算等等。

傳統二級索引思路

方案思路

以Apache Phoenix中提供的全局二級索引方案爲例,每一個索引都是一個獨立的索引表,每一條數據表中的數據,都生成對應的一條索引記錄:

  • 數據表:按數據RowKey順序組織。

  • 索引表:按索引RowKey順序組織。索引RowKey中包含索引列與數據RowKey。

舉一個例子來簡單理解這種設計思路:

假設一個數據表中有三個列{UserID, Name, Phone}。

  • 數據表RowKey爲:UserID

  • 索引表RowKey爲:Name + UserID

這樣,索引表的數據是按照Name排序組織的。按Name查詢時,自然可以快速定位到滿足條件的UserID列表。

當然,Phoenix的實際實現中,RowKey還包含更多的信息,如Tenant ID,Salt Byte等等,這裏不詳述。

事實上,這種二級索引方案,可以理解爲在現有的LSM-Tree架構基礎上,做的簡單擴展,索引數據表本質上還是一個HBase表,只是與數據表擁有不同的RowKey結構以及列信息而已。

基於這種思路來構建標籤索引,需要爲每一個Tag創建一個索引,而Tag數量通常在數千級別以上,顯然此方案無法滿足。

方案缺點

每一個Tag所關聯的Entities,被存成了多行記錄,這意味着查詢響應時延不確定。

而每創建一個索引,都意味着數據總記錄數增大一倍,可見,這種方案不適合創建過多的索引。

因此,基於這種方案來構建標籤索引,場景非常受限,或者說基本不可行。

HBase + Solr/Elasticsearch

方案思路

使用HBase來存儲標籤數據,而標籤索引則基於Solr/Elasticsearch這樣的"外掛"索引方案來實現,這是一種比較常見的組合應用方案,或者,直接將所有數據都存儲在Solr/Elasticsearch中。

方案缺點

先不論數據與索引分離帶來的架構複雜度,我們先來看一下使用Solr/Elasticsearch來存儲標籤數據所存在的幾個典型問題:

1. 無法支持高效的數據更新

以用戶畫像場景爲例,一個用戶所關聯的標籤會被時常更新。

以Elasticsearch爲例,Document被設計成Immutable的,因此,應用是無法直接更新一個Document的,除非進行完整的Document替換,而替換一個Document的步驟如下:

  1. 先獲取舊的Document。

  2. 更新Document的值。

  3. 標記刪除舊的Document。

  4. 寫入新的Document。

可見,這種方案難以應對高頻的數據更新場景。

2. 數據一致性弱,寫入的數據無法立馬可見

3. 單Index/Collection支持數據量有限

在實際應用中以按天/周建索引的方式最爲常見,跨多天的查詢很慢或無法計算出結果。

4. 讀寫併發場景下,高價值數據無法有效緩存

Segment需要時常合併,合併後原來的Cache失效,需要重新加載。

5. 難以支持高效的分頁查詢能力

Solr/Elasticsearch的分頁查詢也存在很大的性能問題,頁數越大查詢越慢。

Elasticsearch/Solr底層都基於Lucene,而這些問題大多是因Lucene自身的架構受限所致。

現有方案對比總結匯總

基於倒排/位圖索引的標籤索引技術

我們基於分佈式倒排索引/位圖索引技術,來構建標籤索引,這個項目的名稱爲Lemon Bitmap,Lemon項目聚焦於爲HBase生態構建統一的索引與輕量級SQL能力。這套方案有如下幾個技術特點:

  1. 支持用戶自定義標籤提取規則,標籤提取與建索引過程對應用不可見。

  2. 支持靈活的組合標籤查詢條件。

接下來,我們一個標籤數據的例子,從使用角度着手介紹一下整個方案的思路。

樣例數據

假設數據表只有一個列族I,而且I中存放了所用客戶的標籤信息,Qualifier名稱爲標籤分類,而Value中存放了標籤值,示例數據如下表所示:

建表

建表時,需要定義標籤索引,該定義中主要包含兩部分信息:

  1. 爲哪些列構建標籤索引?

  2. 如何從列中提取標籤?

建表代碼示例如下:

HTableDescriptor desc = new HTableDescriptor(this.table);
HColumnDescriptor cd = new HColumnDescriptor(Constants.FAMILY);
cd.setCompressionType(Compression.Algorithm.SNAPPY);
desc.addFamily(cd);

// Add bitmap index definitions.
List<BitmapIndexDescriptor> bitmaps = new ArrayList<>();
bitmaps.add(BitmapIndexDescriptor.builder()
            .setColumnName(FamilyOnlyName.valueOf(Constants.FAMILY))
            .setTermExtractor(QualifierValueExtractor.class)
            .build());
// Enable bitmap index for this new table.
IndexHelper.enableAutoIndex(desc, 2, bitmaps);

BitmapIndexDescriptor用來描述要構建的位圖索引信息:

  • FamilyOnlyName定義了要爲某一個ColumnFamily中所有的列構建位圖索引。

  • TermExtractor指定爲QualifierValueExtrator,即需要從Qualifier與Value信息中提取Tag信息。

寫數據

寫數據接口與HBase Table接口保持一致,標籤索引的構建工作在服務端基於Coprocessor完成。

查詢

查詢接口在原生Table接口上做了簡單擴展,原因在於原來的Get/Scan接口無法描述靈活的組合標籤查詢,另外,我們還可以支持分頁查詢與抽樣查詢能力:

普通查詢

查詢滿足組合標籤條件"City:Shenzhen AND Age:20_30"的記錄,首次請求取回10條記錄:

LemonTable lemonTable = new LemonTable(table);
LemonQuery query = LemonQuery.builder()
    .setQuery("City:Shenzhen AND Age:20_30")
    .setCaching(10)
    .build();
ResultSet resultSet = lemonTable.query(query);

通過如下方式可以取到緩存到Client側的數據記錄:

List<EntityEntry> entries = resultSet.listRows();

通過如下方式可以進行分頁查詢:

// 從index位置爲100開始獲取20行記錄
resultSet.listRows(100, 20);

統計型查詢

查詢滿足條件"City:Shenzhen AND (Age:10_20 OR Age:20_30) AND Occupation:Engineer"的記錄總數:

LemonTable lemonTable = new LemonTable(table);
LemonQuery query = LemonQuery.builder()
    .setQuery("City:Shenzhen AND (Age:10_20 OR Age:20_30) AND Occupation:Engineer")
    // 設置查詢只返回滿足條件的統計結果條數.
    .setCountOnly()
    .addFamily(TableTmpl.FAM_M)
    .build();
ResultSet resultSet = lemonTable.query(query);
// Read count.
int count = resultSet.getCount();

抽樣查詢

隨機查詢一個數據分片的結果(普通查詢將請求發送到所有的數據分片):

LemonQuery query = LemonQuery.builder()
    .setQuery("City:Shenzhen AND (Age:10_20 OR Age:20_30) AND Occupation:Engineer")
    // 設置爲抽樣查詢
    .setSampling()
    .addFamily(TableTmpl.FAM_M)
    .setCaching(CACHING)
    .build();
ResultSet resultSet = lt.query(query);
// List all the caching rows.
List<EntityEntry> entries = resultSet.listRows();

標籤加權排序

爲查詢標籤設置權重,依據標籤權重信息爲Entity進行評分,得分越高的(整體權重值越高)的Entity排序越排在前面:

LemonQuery query = LemonQuery.builder()
    // 設置不同標籤的權重
    .setQuery("City:Shenzhen^0.5 AGE:20_30^0.3 Occupation:Engineer^0.9")
    // 啓用評分排序
    .setScoring()
    .addFamily(TableTmpl.FAM_M)
    .setCaching(CACHING)
    .build();
ResultSet resultSet = lt.query(query);
// List all the caching rows.
List<EntityEntry> entries = resultSet.listRows();

方案概述

數據模型

Entity與Term

Lemon Bitmap中,Entity爲對象實體,Term是用來描述Entity的特徵/標籤/關鍵詞信息。

Term也是查詢的基本單元。

Entity Table與Index Table

Entity Table中以Entity Key爲主鍵(使用者自定義),存放了每一個Entity所擁有的Terms信息。Entity Table由使用者自定義,Lemon Bitmap方案並不限制該表的設計。

Index Table中存放了Term到Entities的倒排索引信息。

數據分片

無論是Entity Table還是Index Table,都基於Entity Key進行數據分片。Index Table中的每一個數據分片稱之爲一個Shard,關聯了一個HBase Region,但這個Region做了大量的定製,借鑑Solr/Elasticsearch的定義,將其稱之爲Shard。查詢時,需要將請求發到每一個Shard中。每一個Shard都是一個獨立計算的單元:

 

Q & A

Q: 該方案有哪些架構優勢?

A: 可簡單概括爲如下幾點:

  1. 強數據一致性,寫入即對查詢可見。

  2. 對數據頻繁更新場景友好。

  3. 高價值標籤的Bitmap可常駐內存,並支持緩存數據實時更新。

  4. 分頁查詢性能可預期。

另外,我們還在Bitmap計算與FPGA硬件結合方面做了技術探索,較之CPU場景,計算性能提升五倍以上。

Q: 單表可支持多大的數據量級?查詢時延如何?

A: 每一個Shard建議關聯的Entity數量在千萬級,可支持的數據量級與Shard數量呈線性比例關係。在我們的實際客戶場景中,單表數據量達到了數百TB級別。數十個至數百個組合標籤條件,結果集在數十萬級別時,查詢時延可在毫秒級響應,當組合查詢條件達到數萬級別,查詢也可以在數秒級別內響應。

Q: Lemon Bitmap與Apache Druid中提供的Bitmap有何異同?

A: Apache Druid基於列存+Bitmap的設計,更適用於OLAP場景,但其Immutable Segments的設計並不適用於數據頻繁更新場景。Lemon Bitmap定位於爲HBase生態提供實時多維度數據快速過濾能力,更偏OLTP場景,與HBase的深度結合,使得Lemon Bitmap天然適用於數據頻繁更新、Flexible Schema場景,另外,強數據一致性也是方案的一大優勢。

Q: 除了標籤數據,Lemon Bitmap是否有更廣泛的應用場景?

A: 此文內容聚焦於標籤數據場景,但從設計上來看,Lemon Bitmap並未對數據表提出嚴格的設計要求,因此,Lemon Bitmap可以構建在任何現有的數據表中,只要現有的數據表結構存在一種公共的範式。Lemon Bitmap設計之初就定位於加速整個HBase生態的複雜OLTP查詢能力,而且可以與排序二級索引、全文索引、SQL進行深度結合,未來將應用於時序數據、時空數據、圖數據等場景。

總結

本文基於標籤數據場景,介紹了一種新的HBase索引思路,可以實現數百TB數據場景下的毫秒級多維數據過濾能力,本文先介紹了現有技術存在的問題,接下來從如何使用角度着手介紹了方案思路與設計思想。Lemon Bitmap特性已在華爲雲表格存儲服務CloudTable中上線,歡迎感興趣的同學討論/諮詢。

作者-bijieshan

點擊這裏,瞭解更多精彩內容

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