Phoenix系列|構建Phoenix二級索引

目錄導讀


  • 目錄導讀

  • 1. 前言

  • 2. 二級索引的分類

  • 3. 配置 HBase 支持 Phoenix 二級索引

  • 4. 實戰

    • 4.1 全局索引測試

    • 4.2 本地索引測試

    • 4.3 異步構建索引

    • 4.4 繼續本地索引的測試

  • 5. 主表數據的不同更新方式對索引表數據的影響

  • 6. 索引性能調優

  • 7. 最後

  • 8. 參考鏈接

  • 9. 模擬測試數據的腳本


1. 前言

本文是 Phoenix 系列的第二篇文章,在此我將着重介紹使用 Phoenix 來構建二級索引。文中涉及到的理論介紹以及案例分享,大部分都直接選自知乎大佬棟公子的文章,phoenix 構建二級索引,但在此基礎上我又做了大量額外的補充和說明,儘可能做到全面而詳實地記錄 Phoenix 二級索引的特性和使用細節。

對於 HBase 而言,如果想精確定位到某行記錄,唯一的辦法是通過 rowkey 來查詢,如果不通過 rowkey 來查找數據,就必須逐行地比較每一列的值,即全表掃瞄。

對於數據量較大的表,全表掃描的代價是不可接受的。但是,在很多情況下,我們又不得不需要從多個維度來查詢數據。例如,在定位某個人的時候,可以通過姓名、身份證號、學籍號等不同的維度來查詢,可要想把這麼多維度的數據都放到 rowkey 中幾乎不可能(業務的靈活性不允許,對 rowkey 長度的要求也不允許)。所以需要 secondary index(二級索引)來完成這件事。secondary index 的原理很簡單,但是如果自己來維護二級索引的話則會麻煩一些。現在,Phoenix 已經提供了對 HBase secondary index 的支持。

有關我測試所用的 Phoenix 的環境,請參見我 Phoenix 系列一的第一篇文章。

2. 二級索引的分類

二級索引分爲全局索引和本地索引。

「Global Indexing」Global Indexing,即全局索引,適用於讀多寫少的業務場景。

使用 Global Indexing 在寫數據的時候開銷很大,因爲所有對數據表的更新操作(DELETE, UPSERT VALUES and UPSERT SELECT),都會引起索引表的更新,而索引表是分佈在不同的數據節點上的,跨節點的數據傳輸帶來了較大的性能消耗。

在讀數據的時候 Phoenix 會選擇優先查詢索引表來降低查詢消耗的時間。在默認情況下如果待查詢的字段不是索引字段的話,索引表是不會被使用的,也就是說不會帶來查詢速度上的提升。

「Local Indexing」Local Indexing,即本地索引,適用於寫操作頻繁以及空間受限制的場景。

與 Global Indexing 一樣,Phoenix 會自動判定在進行查詢的時候是否使用索引。使用 Local indexing 時,索引數據和數據表的數據存放在相同的服務器中,這樣避免了在寫操作的時候往不同服務器的索引表中寫索引帶來的額外開銷。使用 Local indexing 的時候即使查詢的字段不是索引字段索引表也會被使用,這會帶來查詢速度的提升,這點跟 Global indexing 不同。對於 Local Indexing,一個數據表的所有索引數據都存儲在一個單一的獨立的可共享的表中。

「Immutable Index」Immutable Index,不可變索引,適用於數據只增加不更新並且按照時間先後順序存儲(time-series data)的場景,如保存日誌數據或者事件數據等。

不可變索引的存儲方式是 write one,append only。當在 Phoenix 使用 create table 語句時指定 IMMUTABLE_ROWS = true 表示該表上創建的索引將被設置爲不可變索引。Phoenix 默認情況下如果在 create table 時不指定 IMMUTABLE_ROWS = true 時,表示該表爲 mutable。不可變索引分爲 Global immutable index 和 Local immutable index 兩種。

CREATE TABLE my_table (k VARCHAR PRIMARY KEY, v VARCHAR) IMMUTABLE_ROWS=true;

「mutable index」mutable index,可變索引,適用於數據有增刪改的場景。

Phoenix 默認情況創建的索引都是可變索引,除非在 create table 的時候顯式地指定 IMMUTABLE_ROWS = true。可變索引同樣分爲 Global mutable index 和 Local mutable index 兩種。(這裏原文其實有筆誤)

3. 配置 HBase 支持 Phoenix 二級索引

「修改配置文件」如果要啓用 Phoenix 的二級索引功能,需要修改 HBase 的配置文件 hbase-site.xml,在 hbase 集羣的 conf/hbase-site.xml 文件中添加以下內容。

不同的版本對 Phoenix 的配置要求還不太一樣,大家可以按需配置,更具體的細節可以參考其官網文檔。http://phoenix.apache.org/secondary_indexing.html

<!-- For Phoenix 4.12 and later -->
<property>
          <name>hbase.regionserver.wal.codec</name>
          <value>org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec</value>
</property>

<!-- For Phoenix 4.8 - 4.11
The following configuration changes are also required to the server-side hbase-site.xml on the master and regions server nodes: -->


<property>
           <name>hbase.region.server.rpc.scheduler.factory.class</name>
           <value>org.apache.hadoop.hbase.ipc.PhoenixRpcSchedulerFactory</value>
</property>
 <property>
         <name>hbase.rpc.controllerfactory.class</name>
         <value>org.apache.hadoop.hbase.ipc.controller.ServerRpcControllerFactory</value>
 </property>
<!-- For Phoenix versions 4.7 and below -->
<property>
          <name>hbase.master.loadbalancer.class</name>
          <value>org.apache.phoenix.hbase.index.balancer.IndexLoadBalancer</value>
</property>
<property>
          <name>hbase.coprocessor.master.classes</name>
          <value>org.apache.phoenix.hbase.index.master.IndexMasterObserver</value>
</property>
<property>
          <name>hbase.coprocessor.regionserver.classes</name>
          <value>org.apache.hadoop.hbase.regionserver.LocalIndexMerger</value>
</property>

在 CDH 中做如下配置修改:然後加入如下配置:配置修改完成之後需要重啓集羣。

4. 實戰

實戰中引用的小例子也來自大佬的博客,只是大佬博客裏測試數據的網盤路徑已經失效了,然後我只能按照原文中的數據格式,自己 mock 了 500w 條數據。模擬數據的 python 腳本,貼在文末,就不佔用此處的空間。測試數據準備好之後,就可以進行接下來的實戰了。

首先,在 Phoenix 中創建一個 user 表。

create table user (
"session_id" varchar(100not null primary key,
"f"."cookie_id" varchar(100),
"f"."visit_time" varchar(100),
"f"."user_id" varchar(100),
"f"."age" Integer,
"f"."sex" varchar(100),
"f"."visit_url" varchar(100),
"f"."visit_os" varchar(100),
"f"."browser_name" varchar(100),
"f"."visit_ip" varchar(100),
"f"."province" varchar(100),
"f"."city" varchar(100),
"f"."page_id" varchar(100),
"f"."goods_id" varchar(100),
"f"."shop_id" varchar(100)) column_encoded_bytes=0;

然後導入數據,該 CSV 文件中有 500 萬條記錄,存儲的路徑/data/leo_jie/user.csv,導入的命令爲:

phoenix-psql -t USER localhost:2181 /data/leo_jie/user.csv

備註:因爲我安裝的是 CDH 版的 Phoenix,所以命令會和原文略有不同。這種方式導入數據的效率挺慢的,數據量大的話還是推薦使用之前提到的 bulkload CSV 文件的方式。500 萬的測試數據大概三個 G,導入耗時 15 分鐘左右。

4.1 全局索引測試

在爲 USER 表創建 secondary index 之前,先來看看查詢一條數據所需的時間:

select * from user where "cookie_id" = 'b80da72c-c6e4-4c79-9fc5-08e7253f6596';

查詢耗時:可見,對名爲 cookie_id 的列進行按值查詢需要 31 秒左右。

然後執行下邏輯計劃:

explain select * from user where "cookie_id" = 'b80da72c-c6e4-4c79-9fc5-08e7253f6596';
explainz

由圖可知,上述查詢會進行全表掃描,然後再通過過濾器來篩選出目標數據,顯然這種查詢方式的效率是很低的。

此時我們來創建 Global Indexing 的二級索引,在 cookie_id 列上面創建二級索引。其實這裏隱含了一個坑,如果你的 Phoenix 表數據量過大,而你直接就以下面的這種方式去創建索引,十有八九會遇到超時異常發生,甚至創建的索引表可能也不是完整的。要想避免這個問題,就需要用到異步索引創建了,這個在下文中會有詳細說明。

create index USER_COOKIE_ID_INDEX on USER ("f"."cookie_id");

等索引創建的命令運行結束,再來查看當前所有的表,你會發現多了一張名叫 USER_COOKIE_ID_INDEX 的索引表,查詢下該索引表的數據。

USER_COOKIE_ID_INDEX 這個索引表,其實就是 cookie_id 和 session_id 的對應表,這個 session_id 就是 hbase 中的 rowkey.這個時候查詢會根據 cookie_id 先找到 session_id,然後 HBase 會利用布隆過濾器來查詢。

在來運行下上述的查詢命令:

你會發現查詢用時跟之前一樣,依舊是幾十秒的時間,難道是索引表未生效?莫急,再來運行如下語句。

select "cookie_id" from user where "cookie_id" = 'b80da72c-c6e4-4c79-9fc5-08e7253f6596';
fast

0.063 秒,是不是快了。看到這裏大家也許會在心裏暗罵,有毛病啊,我以這個條件去查這個條件?原來,在 Phoenix 中,我們建立的是 cookie_id 列的二級索引,這裏只能查詢 cookie_id 列的時候索引機制纔會起作用,再來看一下該 SQL 的執行計劃。

explain select "cookie_id" from user where "cookie_id" = 'b80da72c-c6e4-4c79-9fc5-08e7253f6596';
explain

可以從描述中看到,上述的查詢掃描的是 USER_COOKIE_ID_INDEX 索引表來進行查詢的。單獨查詢這一列是沒有問題,可如果只能查詢這一列,顯然太不合理了。我想再增加一列,比如如下的 SQL 語句。

select "cookie_id","age" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';
explain select "cookie_id","age" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';

查詢計劃和執行計劃如下圖:耗時四十多秒,依舊是全掃描,可見,只要是涉及到非索引列的查詢,就會發生全表掃描。到這裏先不繼續往下延伸,我們先不管全局索引,我們來測試下本地索引。

4.2 本地索引測試

給 user_id 列上面創建二級索引,本地索引比全局索引在創建語法上多了一個 local。

create local index USER_USER_ID_INDEX on USER ("f"."user_id");

在等待本地索引命令創建完畢的過程中,果不其然,在客戶端遇到了超時異常,異常信息大致如下所示:

Caused by: org.apache.hadoop.hbase.ipc.CallTimeoutException: Call to rs-server1:16020 failed on local exception: org.apache.hadoop.hbase.ipc.CallTimeoutException: Call id=150214, waitTime=60016, rpcTimeout=60000

看一下待建索引的狀態:BUILDING 意味着索引還在構建中,如果此時你查詢下 USER_USER_ID_INDEX 索引表的數據,裏面雖然可以查得到數據,但我想,該索引表的數據十有八九是不完整的。

不信的話,運行一下如下 SQL 語句。

select * from user where "user_id"='590335014';
explain select * from user where "user_id"='590335014';

上圖中所看到的結果,並不像棟公子文章裏描述的那樣,查詢語句在毫秒級返回結果。這可能跟我們的索引表構建失敗有關,所以這裏有必要先提前說明下異步索引的創建,然後再回過頭來繼續我們的索引表測試。

4.3 異步構建索引

上述創建索引的語句,默認以同步的方式運行,如果表的數據量過大,服務端構建索引的過程耗時過長,極大可能會造成客戶端操作超時,使待建的索引表陷入 BUILDING 的狀態。不太確定這種狀態過一段時間會不會自己消失,在這裏我的做法是,刪除索引表,異步創建索引,然後用 IndexTool 的 MR 任務來構建索引數據。

「刪除索引表」索引表的刪除不能像 drop view 那樣直接刪除,而是需要用如下的刪除語句。

drop index USER_USER_ID_INDEX on "USER";

刪除時雖然也遇到了客戶端超時,但查看所有表時,索引表已經消失了。

「異步創建索引」直接上語句,跟之前相比只是多了一個關鍵字 async 而已。

create local index USER_USER_ID_INDEX on USER ("f"."user_id") async;
create index

對於全局異步索引表,亦是相同的創建方式。上述語句瞬間返回結果,只是當前索引表的狀態還是 BUILDING,並且索引表裏是沒有數據的。

「IndexTool 構建索引表數據」

hbase org.apache.phoenix.mapreduce.index.IndexTool --data-table USER --index-table USER_USER_ID_INDEX --output-path USER_USER_ID_INDEX

如果你的索引表有 schema,請參考如下創建語句。

${HBASE_HOME}/bin/hbase org.apache.phoenix.mapreduce.index.IndexTool
  --schema MY_SCHEMA --data-table MY_TABLE --index-table ASYNC_IDX
  --output-path ASYNC_IDX_HFILES

上述命令運行成功後,IndexTool 的 MR 任務會幫助我們構建好索引數據,同時會把索引表的當前狀態置爲 ACTIVE,然後就可以使用這個索引表進行數據查詢了。

index build

查看所有表

4.4 繼續本地索引的測試

再一次執行如下 SQL:

select * from user where "user_id"='590335014';
explain select * from user where "user_id"='590335014';

看結果:

同樣的查詢語句,前後用時的差距十分明顯。

接下來我們就討論一下怎麼建立索引表一定就可以使用索引。

第一種方式是使用 local indexing 這種本地索引來建立二級索引。

第二種方式是創建 converted index。如果在某次查詢中,查詢項或者查詢條件中包含有被索引列之外的列(主鍵除外)。默認情況下,該查詢就會觸發 full table scan(全表掃描),但是使用 covered index 則可以避免全表掃描。 創建包含某個字段的覆蓋索引,創建方式如下:

 create index USER_COOKIE_ID_AGE_INDEX on USER ("f"."cookie_id"include("f"."age");

查看當前所有表,會發現多出來一張名爲 USER_COOKIE_ID_AGE_INDEX 的索引表,再以如下 SQL 查詢下 USER 表的數據。

select "cookie_id","age" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';
explain select "cookie_id","age" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';

毫秒級響應查詢。

第三種方式是在查詢的時候提示其使用索引。 在 select 和 column_name 之間加上/+ Index(<表名> <index 名>)/,通過這種方式強制使用索引。 例如:

select /*+ index(user,USER_COOKIE_ID_AGE_INDEX) */ "age" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';

如果 sex 是索引字段,那麼就會直接從索引表中查詢,如果 sex 不是索引字段,那麼將會進行全表掃描,所以當用戶明確知道表中數據較少且符合檢索條件時才適用,此時的性能纔是最佳的。

比如如下 SQL,走的還是全表掃描,因爲性別列並不包含在索引表中。

select /*+ index(user, USER_COOKIE_ID_AGE_INDEX) */"age","sex" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';

到這裏難道就真的可以收尾了嗎?棟公子文章中的演示也是到此爲止了,那麼請各位看官繼續看下面的 SQL。

select /*+ index(user USER_COOKIE_ID_AGE_INDEX) */"age""sex" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';
explain select /*+ index(user USER_COOKIE_ID_AGE_INDEX) */"age""sex" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';

執行結果貼圖:同樣的 SQL,細微的差別,最終的效果卻天壤之別,怎麼來解釋這種情況呢?(有可能是棟公子文章中漏寫了一個逗號???) 其實,這種寫法纔是在進行查詢的時候通過 sql 語句來強制使用索引。

比較官方的解釋是,這樣的查詢語句會導致二次檢索數據表,第一次檢索是去索引表中查找符合 cookie_id 爲 b80da72c-c6e4-4c79-9fc5-08e7253f6596 的數據,這時候發現 sex 字段並不在索引字段中,會去 user 表中進行第二次掃描,因此只有當用戶明確知道符合檢索條件的數據較少的時候才適合使用,否則就又會造成全表掃描,對性能影響較大。也就是說,如果第二次檢索的數據量過大時,依舊等同於發生了全表掃描,同樣會影響查詢效率。

5. 主表數據的不同更新方式對索引表數據的影響

按照索引表的工作原理,即使沒有 Phoenix 的存在,我們也可以實現自己在 HBase 中維護數據表的二級索引表,只不過,在這種情況下,就需要依靠我們自己來維護索引表的數據更新了。這樣的話,我們的維護工作就會成倍增加。而 Phoenix 二級索引表的便利之處也正在此時凸顯,隨着數據表的數據更新,它在內部自動幫我們維護與之綁定的索引表的數據更新。

那麼,不管是怎麼樣的數據寫入場景,數據表的更新都會引起索引表的自動更新嗎?開始實驗求證之前,我們先來思考這幾個問題,數據經 HBase client API 直接 put 寫入數據表,會引起索引表的數據更新嗎?數據經過 HBase 的 bulkload 方式寫入,會引起索引表的數據更新嗎?數據經 Phoenix 的 upsert 方式寫入,會引起索引表的數據更新嗎?

下面開始我們的實驗,爲了更好的觀察測試的結果,我們另行創建一個具有簡單表結構和少量數據的 PERSON.USER 表。

create table person.user(
id VARCHAR PRIMARY KEY,
info.name VARCHAR,
info.age VARCHAR,
info.phone_id VARCHAR,
info.create_time VARCHAR
)column_encoded_bytes=0;

爲 phone_id 列創建索引(全局索引):

 create index user_phone_index on person.user (info.phone_id) ;

此時,數據表和索引表都沒有數據:

「put 寫入數據」在 HBase 表中 put 幾條數據,然後觀察索引表裏的數據變化。

hbase(main):004:0> put 'PERSON:USER','1','INFO:NAME','leo'
Took 0.1438 seconds
hbase(main):005:0> put 'PERSON:USER','1','INFO:AGE','18'
Took 0.0189 seconds
hbase(main):006:0> put 'PERSON:USER','1','INFO:PHONE_ID','18739577988'
Took 0.0118 seconds
hbase(main):007:0> put 'PERSON:USER','1','INFO:CREATE_TIME','2019-01-01 10:23:12

Phoenix 數據表和索引表中數據變化:

數據表中有數據,索引表中數據爲空。

「upsert into 插入數據」

upsert into person.user(ID,NAME,AGE,PHONE_ID,CREATE_TIME) values ('2','leo2','17','18739577932','2019-01-01 10:21:12');

Phoenix 數據表和索引表中數據變化:數據表中有數據,索引表中也存在數據。

然後分別查看如下兩條 SQL 的執行計劃:

select * from person.user;
select PHONE_ID from person.user where PHONE_ID = '18739577988';
select PHONE_ID from person.user where PHONE_ID = '18739577932';
select * from person.user where PHONE_ID = '18739577988';

且看查詢結果:

以索引列 PHONE_ID 作爲篩選條件時,查詢語句 1 和 2 都會先走索引表,然後再從數據表中拿數據。如果索引表不存在該對應查詢條件的索引數據,即使數據表中存在該數據,查詢結果也依然是空。語句 3 因爲走了全表掃描,所以會查詢出結果。

Phoenix 可以幫助我們自動維護索引表數據的更新,但前提是,數據的更新採取的是 Phoenix API 的寫入方式,至於以 HBase bulkload 的方式導入的數據,更不會引起索引表數據的更新了,各位看官可以自行嘗試。那麼,有什麼辦法來解決這種問題呢?

第一種方式就是重建索引,把索引表清空後重新裝配數據,這就意味着我們需要手動來觸發索引表數據的更新機制。而重建索引表的方式一般使用如下命令。

alter index USER_PHONE_INDEX  on person.USER rebuild;
alter index USER_PHONE_INDEX  on person.USER rebuild async;

如果你的 Phoenix 表數據量很大,還是老老實實使用語句 2 來進行異步索引重建吧。異步索引重建之後,索引的狀態被置爲了 BUILDING,此時,還需要運行 IndexTool 命令來加載索引數據。

hbase org.apache.phoenix.mapreduce.index.IndexTool \
--schema PERSON --data-table USER --index-table USER_PHONE_INDEX \
--output-path USER_PHONE_INDEX

如果沒有異步重建索引的那一步,不管你怎麼運行這個 MR 任務,貌似都沒有任何效果,索引表依舊是空的。

這裏說起索引表的狀態,不得不提一下索引表的生命週期。我們以異步的方式初次創建或者二次創建的索引表的狀態是 BUILDING,即構建狀態,此時索引表還不能正常使用,當我們運行完 IndexTool 這個索引表構建工具之後,索引表的狀態被修改爲 ACTIVE,意味激活狀態。此時索引表就可以正常使用了,這裏就涉及到了索引表的生命週期。有關生命週期更詳細的介紹,還請移步至 Phoenix 的官方文檔,或者參考此篇博客https://www.cnblogs.com/hbase-community/p/8879848.html

異步重建完索引表,其狀態被改變,IndexTool 的命令也執行成功,此時再來查看下我們的索引表數據,一切都恢復了正常。對於大批量數據沒有被加載進索引表的,這種方式是可行的。但是對於非 Phoenix 客戶端一次次的數據寫入,都得以這樣的方式進行索引重建,顯然是不合理的,所以就有了下面的這種方式。

第二種: 非 Phoenix 客戶端寫入數據之後,手動更新索引表數據,也即寫入兩份數據,先來看一下 HBase 中索引表數據的構造。

Phoenix 的索引表會把所有索引字段+ID 拼接起來寫進 HBase,做爲 RowKey。那我們也仿照這樣的格式,插入主表數據的同時,把相應索引字段拼接起來插進索引表。格式爲:索引列\x00ID

先在 HBase 數據表中 put 一條數據。

put 'PERSON:USER','3','INFO:NAME','leo3'
put 'PERSON:USER','3','INFO:AGE','16'
put 'PERSON:USER','3','INFO:PHONE_ID','18739577966'
put 'PERSON:USER','3','INFO:CREATE_TIME','2019-11-01 10:23:12
no data

往 HBase 對應的索引表裏插入一條我們自己構造的索引數據。

 put 'PERSON:USER_PHONE_INDEX',"18739577966\x003",'0:_0','x'

(注意,這裏的 rowkey 是雙引號,如果是單引號會把‘\’轉譯掉,插入的數據就不對了)

最終的效果:

當我們以非 Phoenix API 的方式寫入數據的時候,在數據表中更新數據之後,還得需要在與之對應的索引表中插入拼裝後的索引數據。保證索引表數據的完整性很重要,否則,即使不出現查詢超時,也可能存在明明表裏有數據,可就是查不到數據的尷尬場景。

6. 索引性能調優

一般來說,索引已經很快了,不需要特別的優化。但這裏也提供了一些方法,讓你在面對特定的環境和負載的時候可以進行一些調優。下面的這些配置需要在 hbase-site.xml 文件中設置,針對所有的 RegionServer 節點,以下配置也是來自棟公子的原文,同樣的官網文檔裏也有很詳細的說明。其具體的作用我並沒有在生產環境中測試過,寫下來只是爲了做一個參考。

1. index.builder.threads.max
創建索引時,使用的最大線程數。
默認值: 10。

2. index.builder.threads.keepalivetime
創建索引的創建線程池中線程的存活時間,單位:秒。
默認值: 60

3. index.writer.threads.max
寫索引表數據的寫線程池的最大線程數。
更新索引表可以用的最大線程數,也就是同時可以更新多少張索引表,數量最好和索引表的數量一致。
默認值: 10

4. index.writer.threads.keepalivetime
索引寫線程池中,線程的存活時間,單位:秒。
默認值:60

5. hbase.htable.threads.max
每一張索引表可用於寫的線程數。
默認值: 2,147,483,647

6. hbase.htable.threads.keepalivetime
索引表線程池中線程的存活時間,單位:秒。
默認值: 60

7. index.tablefactory.cache.size
允許緩存的索引表的數量。
增加此值,可以在寫索引表時不用每次都去重複的創建htable,這個值越大,內存消耗越多。
默認值: 10

8. org.apache.phoenix.regionserver.index.handler.count
處理全局索引寫請求時,可以使用的線程數。
默認值: 30

7. 最後

Phoenix 的二級索引在某些場景中還是非常有用的,可一旦使用姿勢不當,極大可能會造成全表掃描,嚴重時線上其他的查詢服務也會深受其害。其次牢記,不走 Phoenix 的 API 而更新數據的方式,索引表可能不會隨之更新,必要時需要手動維護索引表數據,索引表的數據要嚴格與主表的數據保持一致,否則,會出現遺漏數據的情況。

Phoenix 系列到這裏就先告一段落,後續使用過程中如果遇見比較有意思的事,會繼續給大家分享。

8. 參考鏈接

  • https://zhuanlan.zhihu.com/p/107748046
  • https://www.jianshu.com/p/5d855f8f1e1d
  • http://phoenix.apache.org/secondary_indexing.html

9. 模擬測試數據的腳本

#!/usr/bin/env python
# -*- coding:utf-8 -*-

"""
:Description: 模擬用戶數據
:Owner: leo_jie
:Create time: 2020/7/14 3:24 下午
"""


import uuid
import time
import random
import struct
import socket


def generate_random_time():
    a1 = (201811000000)  # 設置開始日期時間元組(1976-01-01 00:00:00)
    a2 = (20191231235959000)  # 設置結束日期時間元組(1990-12-31 23:59:59)
    start = time.mktime(a1)  # 生成開始時間戳
    end = time.mktime(a2)  # 生成結束時間戳
    # 隨機生成10個日期字符串
    t = random.randint(start, end)  # 在開始和結束時間戳中隨機取出一個
    date_tuple = time.localtime(t)  # 將時間戳生成時間元組
    random_date = time.strftime("%Y-%m-%d %H:%M:%S", date_tuple)  # 將時間元組轉成格式化字符串
    return random_date


def generate_random_user_id(length=9):
    user_id = str(random.randint(1999999999))
    if len(user_id) < length:
        append_length = length - len(user_id)
        user_id = '0' * append_length + user_id
    return user_id


def random_str():
    data = "1234567890zxcvbnmlkjhgfdsaqwertyuiop"
    # 用時間來做隨機播種
    random.seed(time.time())
    # 隨機選取數據
    sa = []
    for i in range(20):
        sa.append(random.choice(data))
    salt = "gp_" + ''.join(sa)
    return salt


def generate_random_url():
    channels = [
        "BTV2""BTV3""BTV4""BTV5""BTV6""BTV7""BTV8""BTV9",
        "BTVWorld""CCTV12""CCTV2""CCTV3""CCTV5""CCTV6""CCTV6World""CCTV7",
        "CCTV8""CCTV9World""CCTVA""CCTVChild""CCTVE""CCTVEN""CCTVF""CCTVFYYY",
        "CCTVHJJC""CCTVMusic""CCTVNEWS""CCTVR""CETV1""CETV3""ChongQingTV""ChongQingWorld",
        "GZChild""GZEnglish""GZFinance""GZJingSai""GZMovie""GanSuTV""GuangXiTV""GuiZhouTV",
    ]

    host = '10.160.1.21'
    date = '2014/10/31'
    bitrates = ['800000']
    channel = random.choice(channels)
    bitrate = random.choice(bitrates)
    ts_url = ''.join(
        ['http://', host, '/''online01/stream/', bitrate, '/', channel, '/', date, '/', random_str()])
    return ts_url

def generate_random_ip():
    RANDOM_IP_POOL = ['192.168.10.222/0']
    str_ip = RANDOM_IP_POOL[random.randint(0, len(RANDOM_IP_POOL) - 1)]
    str_ip_addr = str_ip.split('/')[0]
    str_ip_mask = str_ip.split('/')[1]
    ip_addr = struct.unpack('>I', socket.inet_aton(str_ip_addr))[0]
    mask = 0x0
    for i in range(3131 - int(str_ip_mask), -1):
        mask = mask | (1 << i)
    ip_addr_min = ip_addr & (mask & 0xffffffff)
    ip_addr_max = ip_addr | (~mask & 0xffffffff)
    return socket.inet_ntoa(struct.pack('>I', random.randint(ip_addr_min, ip_addr_max)))

def generate_random_province():
    provinces = ['河北省''山西省''遼寧省''吉林省''黑龍江省''江蘇省''浙江省''安徽省''福建省',
                 '江西省''山東省''河南省''湖北省''湖南省''廣東省''海南省''四川省''貴州省',
                 '雲南省''陝西省''甘肅省''青海省''臺灣省''上海市''北京市''天津市''重慶市',
                 '香港''澳門']
    return provinces[random.randint(0, len(provinces) - 1)]


def generate_shop_id():
    shop_no = ['A''B''C''D''F''F''G']
    return shop_no[random.randint(0, len(shop_no) - 1)] + str(random.randint(19999))


def mock_user_data():
    session_id = uuid.uuid4()
    cookie_id = uuid.uuid4()
    visit_time = generate_random_time()
    user_id = generate_random_user_id()
    age = str(random.randint(1080))
    sex = 'F' if random.randint(01) == 1 else 'M'
    visit_url = generate_random_url()
    visit_os = 'Windows OS' if random.randint(01) == 1 else 'Mac OS'
    browser_name = 'Google' if random.randint(01) == 1 else 'IE'
    visit_ip = generate_random_ip()
    province = generate_random_province()
    city = province
    page_id = str(random.randint(199999))
    goods_id = str(random.randint(1999999999))
    shop_id = generate_shop_id()
    return "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s" \
           % (session_id, cookie_id, visit_time, user_id, age, sex, visit_url,
              visit_os, browser_name, visit_ip, province, city,
              page_id, goods_id, shop_id)


def create_data(rows=5000000):
    for i in range(rows):
        with open('user.csv''a')as f:
            f.write(mock_user_data() + "\n")


create_data()

據說,關注我的人都找到了對象👇

本文分享自微信公衆號 - HBase工作筆記(HBase-Notes)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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