概述
筆者本人接觸研究HBase也有半年之久了,雖說不上深入和系統,但至少算是比較沉迷。作為部門里大數據技術的探路者,筆者還要承擔起技術傳播的職責,所以在摸索研究的過程中總是不斷地進行總結和測試,一路走來,慢慢地積累了一些東西,整理了一下,做成一個技術系列文檔,暫時就叫做“HBase應用開發回顧與總結”。雖然稱不上什么高深莫測的技術,但本着開源和分享的精神,筆者本人還是很樂意將它逐篇貼出來。另外,筆者認為《HBase權威指南》算是比較好的HBase方面的技術書籍了,推薦給大家閱讀。
下面是這個系列文檔的目錄介紹:
第一章 HBase設計規范
介紹了HBase應用開發時建議遵循的設計規范,主要是針對開發層面的。
第二章 RowKey行鍵設計規范
介紹了RowKey行鍵的部分特性和設計規范,當然,行鍵的具體設計,還是要遵循具體的業務,並借助豐富的設計經驗。
第三章 RowKey行鍵生成器
設計了一個RowKey行鍵生成器,可通過界面的方式手動制訂行鍵的生成策略,並可將此策略文件序列化成本地文件,也可將本地策略文件反序列化成策略對象,通過此策略對象可批量動態生成行鍵信息。
第四章 HBase配置管理類接口設計
設計HBase配置工具類,包括如何加載讀取hbase相關配置文件,以及生成Configuration對象等等。
第五章 HBase表信息管理接口設計
設計HBase表信息管理工具類,包括命名空間、表信息、列族等管理接口。
第六章HBase表寫數據接口設計
設計了幾個HBase寫數據模型類,通過這些模型類,方便在開發期組織數據並將其寫入HBase數據庫。
第七章 HBase表讀數據接口設計
設計了幾個HBase讀數據模型類,介紹了多種數據檢索方案,包括批量檢索、范圍檢索、版本檢索等等。
第八章 HBase過濾器應用設計
介紹了幾類常用的過濾器以及過濾器使用時應該注意的細節問題,包括分頁過濾器、前綴過濾器等。
第九章 HBase輕量級ORM設計
模仿Hibernate的對象映射,做了一個針對HBase的輕量級ORM設計方案,並沒有過多考慮實用性,只是一種原型測試。
第十章 HBase表數據瀏覽器
綜合應用以上幾個章節,設計了一個HBase表數據瀏覽器,具備表信息導航、按條件分頁查詢、多版本查詢等功能。效果圖如下所示:
1. HBase設計規范
在這里講什么設計規范實在是有些不知天高地厚,畢竟筆者本人也只是一個大數據技術的初學者,斷然不敢制訂什么設計規范的,所以請原諒我的狂妄,這個設計規范,只是本人對自己制訂的,與別人無關。
之前,HBase官方和大批的高人已經總結了一部分HBase設計方面的規范,筆者進行了搜集整理,再加上自己的理解和豐富,就整理出了一份自己感覺適合自己開發所應遵循的規范。
Hbase中與表結構相關的邏輯模型涉及到以下幾個詞匯:命名空間、表、列族、列、行鍵、版本等,這些是構建hbase表的所有元素。筆者就依據這幾個關鍵詞匯,陳述下相關的規范。
1.1. Namespace命名空間設計
通俗地講,命名空間可視為表組(與Oracle中的表空間類似),划分依據不固定,可依據業務類型划分,也可依據時間周期划分。譬如,針對電力氣象方面的數據表,可以創建一個電力氣象的命名空間,取名為DLQX,將電力氣象相關的表都組織在此命名空間下面。引進命名空間的好處就是方便對表進行組織管理。
HBase默認的命名空間是default,默認情況下,如果在創建表時沒有顯式地指定命名空間,那么表將創建在default命名空間下。如果表隸屬於某個非默認的命名空間,那么在引用表(譬如讀取表數據)時,就必須指定命名空間,否則將出現類似“無法定位到表”的錯誤,完整表名的格式為“命名空間名稱:表名稱”,譬如”DLQX:SYSTEM_USER”;如果是默認的命名空間,則完整表名也可以省略掉“default:”,直接拼寫表名SYSTEM_USER即可。
命名空間與表的關系,可以用下圖表示:
命名空間與表之間是一對多的關系,即一個命名空間下面可以包含多個hbase表,但一個hbase表只能屬於一個命名空間。在創建表時,如果沒有指定命名空間(或者命名空間為空),則系統會將此hbase表放置在默認命名空間(default)下。
另外,刪除命名空間之前,必須先刪除掉此命名空間下的所有hbase表,否則將無法刪除此命名空間。
1.2. Table表設計
HBase有幾個高級特性,在你設計表時可以使用。這些特性不一定聯系到模式或行鍵設計,但是它們定義了某些方面的表行為。
1.2.1 理想HBase表
Hbase作為列數據庫,根據官方的說法,在性能和效率上更擅長處理“高而瘦”的表,而非“矮而胖”的表。所謂“高而瘦”,是指表的列的數量較少,但是行的數量極大,從而使表展現出一種又高又瘦的形象。所謂“矮而胖”,是指表的列的數據居多,但是行的數量卻有限,給人一種又矮又胖的形象,雖然hbase表號稱可容納百萬列,但是那也僅僅限於理論上的極限,在實際應用中,請盡量構建“高而瘦”的表,同時需要對列的數量進行測試,以避免過度影響讀寫性能。
1.2.2 預創建分區
默認情況下,在創建HBase表的時候會自動創建一個region分區,當導入數據的時候,所有的HBase客戶端都向這一個region寫數據,直到這個region足夠大了才進行切分。一種可以加快批量寫入速度的方法是通過預先創建一些空的regions,這樣當數據寫入HBase時,會按照region分區情況,在集群內做數據的負載均衡。
1.2.3 列族數量
不要在一張表里定義太多的column family。目前Hbase並不能很好的處理超過2~3個column family的表。因為某個column family在flush的時候,它鄰近的column family也會因關聯效應被觸發flush,最終導致系統產生更多的I/O。所以,根據官方的建議,一個HBase表中創建一個列族即可。
1.2.4 可配置的數據塊大小
HFile數據塊大小可以在列族層次設置。這個數據塊不同於HDFS數據塊。其默認值是65,536字節,或64KB。數據塊索引存儲每個HFile數據塊的起始鍵。數據塊大小設置影響到數據塊索引的大小。數據塊越小,索引越大,從而占用更大內存空間。同時因為加載進內存的數據塊更小,隨機查找性能更好。但是如果你需要更好的序列掃描性能,那么一次能夠加載更多HFile數據進入內存則更為合理,這意味着數據塊應該設置為更大的值。相應地索引變小,你將在隨機讀性能上付出代價。
1.2.5 數據塊緩存
把數據放進讀緩存,但工作負載卻經常不能從中獲得性能提升。例如,如果一張表或表里的列族只被順序化掃描訪問或者很少被訪問,你不會介意Get或Scan花費時間是否有點兒長。在這種情況下,你可以選擇關閉那些列族的緩存。如果你只是執行很多順序化掃描,你會多次倒騰緩存,並且可能會濫用緩存把應該放進緩存獲得性能提升的數據給排擠出去。如果關閉緩存,你不僅可以避免上述情況發生,而且可以讓出更多緩存給其他表和同一表的其他列族使用。
1.2.6 激進緩存
你可以選擇一些列族,賦予它們在數據塊緩存里有更高的優先級(LRU緩存)。如果你預期一個列族比另一個列族隨機讀更多,這個特性遲早用得上。
IN_MEMORY參數的默認值是false。因為HBase除了在數據塊緩存里保存這個列族相比其他列族更激進之外並不提供額外的保證,該參數在實踐中設置為true不會變化太大。
創建表的時候,可以通過HColumnDescriptor.setInMemory(true)將表放到RegionServer的緩存中,保證在讀取的時候被cache命中。
1.2.7 布隆過濾器(Bloom filters)
數據塊索引提供了一個有效的方法,在訪問一個特定的行時用來查找應該讀取的HFile的數據塊。但是它的效用是有限的。HFile數據塊的默認大小是64KB,這個大小不能調整太多。
如果你要查找一個短行,只在整個數據塊的起始行鍵上建立索引無法給你細粒度的索引信息。例如,如果你的行占用100字節存儲空間,一個64KB的數據塊包含(64 * 1024)/100 = 655.53 = ~700行,而你只能把起始行放在索引位上。你要查找的行可能落在特定數據塊上的行區間里,但也不是肯定存放在那個數據塊上。這有多種情況的可能,或者該行在表里不存在,或者存放在另一個HFile里,甚至在MemStore里。這些情況下,從硬盤讀取數據塊會帶來IO開銷,也會濫用數據塊緩存。這會影響性能,尤其是當你面對一個巨大的數據集並且有很多並發讀用戶時。
布隆過濾器允許你對存儲在每個數據塊的數據做一個反向測試。當某行被請求時,先檢查布隆過濾器看看該行是否不在這個數據塊。布隆過濾器要么確定回答該行不在,要么回答它不知道。這就是為什么我們稱它是反向測試。布隆過濾器也可以應用到行里的單元上。當訪問某列標識符時先使用同樣的反向測試。
布隆過濾器也不是沒有代價。存儲這個額外的索引層次占用額外的空間。布隆過濾器隨着它們的索引對象數據增長而增長,所以行級布隆過濾器比列標識符級布隆過濾器占用空間要少。當空間不是問題時,它們可以幫助你榨干系統的性能潛力。
你可以在列族上打開布隆過濾器,如下所示:
hbase(main)> create 'mytable',{NAME=>'colfam1',BLOOMFILTER=>'ROWCOL'}
BLOOMFILTER參數的默認值是NONE。一個行級布隆過濾器用ROW打開,列標識符級布隆過濾器用ROWCOL打開。行級布隆過濾器在數據塊里檢查特定行鍵是否不存在,列標識符級布隆過濾器檢查行和列標識符聯合體是否不存在。ROWCOL布隆過濾器的開銷高於ROW布隆過濾器。
1.2.8 生存時間(TTL)
應用系統經常需要從數據庫里刪除老數據。由於數據庫很難超過某種規模,所以傳統上數據庫內建了許多靈活處理辦法。例如,在TwitBase里你不願意刪除用戶在使用應用系統期間生成的任何推帖。這些都是用戶生成數據,將來有一天當你執行一些高級分析時可能有用。但是並不需要保存所有推帖用於實時訪問。所以早於某個時間的推帖可以歸檔存放到平面文件里。
HBase可以讓你在數秒內在列族級別設置一個TTL。早於指定TTL值的數據在下一次大合並時會被刪除。如果你在同一單元上有多個時間版本,早於設定TTL的版本會被刪除。你可以關閉TTL或者通過設置其值為INT.MAX_VALUE (2147483647)來讓它永遠打開(這是默認值)。你可以在建表時設置TTL,如下所示:
hbase(main)> create 'mytable',{NAME=>'colfam1',TTL=>'18000'}
該命令在colfam1列族上設置TTL為18,000秒=5小時。colfam1里超過5小時的數據將會在下一次大合並時被刪除。
1.2.9 數據壓縮
HFile可以被壓縮並存放在HDFS上。這有助於節省硬盤IO,但是讀寫數據時壓縮和解壓縮會抬高CPU利用率。壓縮是表定義的一部分,可以在建表或模式改變時設定。除非你確定不會從壓縮中受益,我們推薦你打開表的壓縮。只有在數據不能被壓縮或者因為某種原因服務器的CPU利用率有限制要求的情況下,有可能會關閉壓縮特性。
HBase可以使用多種壓縮編碼,包括LZO、Snappy和GZIP。LZO[1]和Snappy[2]是其中最流行的兩種。Snappy由Google在2011年發布,發布不久Hadoop和HBase項目開始提供支持。在此之前,選擇的是LZO編碼。Hadoop使用的LZO原生庫受GPLv2版權控制,不能放在Hadoop和Hbase的任何發行版里;它們必須單獨安裝。另一方面,Snappy擁有BSD許可(BSD-licensed),所以它更容易和Hadoop和HBase發行版捆綁在一起。LZO和Snappy的壓縮比例和壓縮/解壓縮速度差不多。
當建表時你可以在列族上打開壓縮,如下所示:
hbase(main)> create 'mytable',{NAME=>'colfam1',COMPRESSION=>'SNAPPY'}
注意數據只在硬盤上是壓縮的。在內存里(MemStore或BlockCache)或網絡傳輸時是沒有壓縮的。
改變壓縮編碼的做法不應該經常發生,但是如果你的確需要改變某個列族的壓縮編碼,直接做就可以。你需要更改表定義,設定新壓縮編碼。此后合並時,生成的HFile全部會采用新編碼壓縮。這個過程不需要創建新表和復制數據。但你要確保直到改變編碼后所有老HFile被合並后才能從集群中刪除老編碼函數庫。
1.2.10 數據分割
在HBase中,數據在更新時首先寫入WAL 日志(HLog)和內存(MemStore)中,MemStore中的數據是排序的,當MemStore累計到一定閾值時,就會創建一個新的MemStore,並且將老的MemStore添加到flush隊列,由單獨的線程flush到磁盤上,成為一個StoreFile。於此同時, 系統會在zookeeper中記錄一個redo point,表示這個時刻之前的變更已經持久化了(minor compact)。
StoreFile是只讀的,一旦創建后就不可以再修改。因此Hbase的更新其實是不斷追加的操作。當一個Store中的StoreFile達到一定的閾值后,就會進行一次合並(major compact),將對同一個key的修改合並到一起,形成一個大的StoreFile,當StoreFile的大小達到一定閾值后,又會對 StoreFile進行分割(split),等分為兩個StoreFile。
由於對表的更新是不斷追加的,處理讀請求時,需要訪問Store中全部的StoreFile和MemStore,將它們按照row key進行合並,由於StoreFile和MemStore都是經過排序的,並且StoreFile帶有內存中索引,通常合並過程還是比較快的。
實際應用中,可以考慮必要時手動進行major compact,將同一個row key的修改進行合並形成一個大的StoreFile。同時,可以將StoreFile設置大些,減少split的發生。
1.2.11 單元時間版本
HBase在默認情況下每個單元維護三個時間版本。這個屬性是可以設置的。如果你只需要一個版本,推薦你在設置表時只維護一個版本。這樣系統就不會保留更新單元的多個時間版本。時間版本也是在列族級設置的,可以在表實例化時設定:
hbase(main)> create 'mytable',{NAME=>'colfam1', VERSIONS=>1}
你可以在同一個create語句里為列族指定多個屬性,如下所示:
hbase(main)> create 'mytable',{NAME=>'colfam1',VERSIONS=>1,TTL=>'18000'}
你也可以指定列族存儲的最少時間版本數,如下所示:
hbase(main)> create 'mytable',{NAME=>'colfam1',VERSIONS=>5,
MIN_VERSIONS=>'1'}
在列族上同時設定TTL也是遲早有用的。如果當前存儲的所有時間版本都早於TTL,至少MIN_VERSION個最新版本會保留下來。這樣確保在你的查詢以及數據早於TTL時有結果返回。
1.3. ColumnFamily列族設計
列族是針對多個列的分組,分組的依據是不固定的。雖然理論上HBase一個表可以創建多個列族,但是HBase官方建議一個表不要創建多於一個的列族。經過測試,單個列族的寫入和讀取效率要遠遠超過多個列族時的情況。在存儲時,一個列族會存儲成一個StoreFile,多個列族對應的多個文件在分裂時會對服務器造成更大的壓力。所以建議,一個表創建一個列族。
列族的名稱不宜過長,因為在存儲時每列都會拼上列族名稱,過長的列族將會浪費更多的存儲空間。
刪除列族時,將同時刪除列族下的列及列值數據。
創建表時,最少要創建一個列族。創建表后,可以添加多個列族。
Version版本是針對列族而言的,如果一個表有多個列族,可以為每個列族設置不同的版本數量。譬如,允許列族A最多有5個版本,列族B最多有3個版本。
1.4. Qualifier列設計
HBase與傳統的關系數據庫一個明顯的不同之處,就是創建表時不需要創建列,而是在寫入數據時動態地創建列。而且其中的空列並不真正占用存儲空間。
列內容被封裝成為KeyValue對象,從中可以獲取多個信息,如下所示:
//行鍵 String rowKey = Bytes.toString(kv.getRow()); //列族 String family = Bytes.toString(kv.getFamily()); //列名稱 String qualifier = Bytes.toString(kv.getQualifier()); //列值 String value = Bytes.toString(kv.getValue()); //版本號 long timestamp = kv.getTimestamp();
1.5. 版本設計
如果表的某個列族涉及到多版本的問題,則必須在創建列族時指定MaxVersions。雖然,HBase默認的版本數是3,但是如果在創建表時沒有明確指定,則仍然只能保存一個版本,因為HBase會認為你不想啟用列族的多版本機制。
可以在寫入數據時指定版本號,如果不指定版本號,則將采用默認的版本號,即時間戳。
讀取數據時,如果沒有指定版本號,將只讀取最新版本數據,而非最新版本號的數據。
1.6. HBase命名規范
項目 |
說明 |
示例 |
命名空間 |
|
|
表名稱 |
|
USER_INFO_MANAGE、 WEATHER_DATA、 T_ELECTRIC_GATHER等。 |
列族名稱 |
|
D1、D2、DATA等。
USER_INFO、D_1等。 |
列名稱 |
|
USER_ID、DATA_1、REMARK等。
UserID、1_DATA等。 |
作者:商兵兵
單位:河南省電力科學研究院智能電網所
QQ:52190634