之前我的公眾號名字叫做:’‘Java不睡覺’’,原因就是當時看了一本書,名字是《HBase不睡覺書》,這本書正如其名字一樣,是一本讓人讀起來根本不會發困的書,very奈斯,本文就是整理了這本書上的知識點而形成的文章,準備分為上下兩篇文章系統梳理HBase核心知識點,如果你想了解HBase,那么這篇文章不會讓你失望的,同時推薦閱讀一下原書,讓我們開始吧,
文章目錄
- 前言
- 一、整體架構
- 1.1 Region
- 1.2 RegionServer
- 1.3 Master
- 1.4 Zookeeper
- 1.5 微觀架構
- 二、常用Shell命令與API
- 2.1 常用Shell命令
- 2.2 Java API
- 三、存盤機制
- 3.1 RegionServer結構
- 3.2 Region內部結構
- 3.3 WAL(預寫日志)
- 3.4 Store內部結構
- 3.5 MemStore
- 3.6 HFile(StoreFile)
- 3.7 Data資料塊
- 3.8 KeyValue類
- 3.9 資料增刪改查的真面目
- 3.10 Hbase資料結構總結
- 四、KeyValue的寫入和讀出
- 4.1 寫入
- 4.2 讀出
- 五、 Region的定位
前言
HBase 是一個開源的、面向列的非關系型分布式資料庫,目前是Hadoop體系中非常關鍵的一部分,
在最初,HBase是基于谷歌的 BigTable 原型實作的,許多技術來自于Fay Chang在2006年所撰寫的Google論文"BigTable",與 BigTable基于Google檔案系統(File System)一樣,HBase則是基于HDFS(Hadoop的分布式檔案系統)之上而開發的,
HBase 采用 Java 語言實作,在其內部實作了BigTable論文提到的一些壓縮演算法、記憶體操作和布隆過濾器等,這些能力使得HBase 在海量資料存盤、高性能讀寫場景中得到了大量應用,如 Facebook 在 2010年11 月開始便一直選用 HBase來作為訊息平臺的存盤層技術,
HBase 以 Apache License Version 2.0開源,這是一種對商業應用友好的協議,同時該專案當前也是Apache軟體基金會的頂級專案之一,
有什么特性?
- 基于列式存盤模型,對于資料實作了高度壓縮,節省存盤成本
- 采用 LSM 機制而不是B(+)樹,這使得HBase非常適合海量資料實時寫入的場景
- 高可靠,一個資料會包含多個副本(默認是3副本),這得益于HDFS的復制能力,由RegionServer提供自動故障轉移的功能
- 高擴展,支持分片擴展能力(基于Region),可實作自動、資料均衡
- 強一致性讀寫,資料的讀寫都針對主Region上進行,屬于CP型的系統
- 易操作,HBase提供了Java API、RestAPI/Thrift API等介面
- 查詢優化,采用Block Cache 和 布隆過濾器來支持海量資料的快速查找
一、整體架構
整個HBase 集群主要由 Zookeeper、HBase Master、HBase RegionServer和HDFS構成,HBase集群架構圖如下:

其中 Master 節點是允許存在多個的,當多個 Master 節點共存時,只有一個 Master 是提供服務的,這種主備角色的"仲裁"由 ZooKeeper 實作,
RegionServer是直接負責存盤資料的服務器,RegionServer保存的表資料直接存盤在Hadoop的HDFS上,RegionServer非常依賴ZooKeeper服務,ZooKeeper管理了HBase中所有的RegionServer的資訊,包括具體的資料段存放在哪個 RegionServer上, 客戶端每次與HBase連接,其實都是先與ZooKeeper通信,查詢出具體需要連接哪個RegionServer,然后再連接到RegionServer,
1.1 Region
Region就是一段資料的集合,HBase中的表一般擁有一個到多個Region,Region具有以下特性:
- Region不能跨服務器,一個RegionServer上有一個或者多個 Region,
- 資料量小的時候,一個Region足以存盤所有資料;但是,當資料 量大的時候,HBase會拆分Region,
- 當HBase在進行負載均衡的時候,也有可能會從一臺 RegionServer上把Region移動到另一臺RegionServer上,
- Region是基于HDFS的,它的所有資料存取操作都是呼叫了HDFS的 客戶端介面來實作的,
1.2 RegionServer
RegionServer就是存放Region的容器,直觀上說就是服務器上的一 個服務,當客戶端從ZooKeeper獲取RegionServer的地址后,它會直接從 RegionServer獲取資料,
1.3 Master
在1.2小節中提到過,客戶端從 ZooKeeper獲取了RegionServer的地址后,會直接從RegionServer獲取資料,其實不光是獲取資料,包括插入、洗掉等所有的資料操作都是直接操作RegionServer,而不需要經過Master,
不像Hadoop等其他分布式系統,在HBase中,Master更像是一個打雜的,Master只負責各種協調作業,比如建表、刪表、 移動Region、合并等操作,它們的共性就是需要跨RegionServer,這些 操作由哪個RegionServer來執行都不合適,所以HBase就將這些操作放 到了Master上了,
這種結構的好處是大大降低了集群對Master的依賴,Master節點一般只有一個到兩個,一旦宕機,如果集群對Master的依賴度很大,那么就會產生單點故障問題,在HBase中,即使Master宕機了,集群依然 可以正常地運行,依然可以存盤和洗掉資料,
1.4 Zookeeper
Zookeeper 對于 HBase的作用是至關重要的,
- Zookeeper 提供了 HBase Master 的高可用實作,并保證同一時刻有且僅有一個主 Master 可用,
- Zookeeper 保存了 Region 和 Region Server 的關聯資訊(提供尋址入口),并保存了集群的元資料(Schema/Table),
- Zookeeper 實時監控Region server的上線和下線資訊,并實時通知Master,
除了 HBase之外,有許多分布式大資料相關的開源框架,都依賴于 Zookeeper 實作 HA,
1.5 微觀架構
HBase是一個分布式列式資料庫,最基本的存盤單位是列(column),一個列或者多個列形成一行(row),在HBase中,這一行有三個列a、b、 c,下一個行也許是有4個列a、e、f、g,行跟行的列可以完全不一樣,這個行的資料跟另外一個行的資料也可以存盤在不同的機器上,甚至同一行內的列也可以存盤在完全不同的機器上!
每個行(row)都擁有唯一的行鍵(row key)來標定這個行的唯一 性,每個列都有多個版本,多個版本的值存盤在單元格(cell)中,
綜上,HBase的存盤結構可以表示成下圖所示的結構:

二、常用Shell命令與API
2.1 常用Shell命令
①進入hbase命令列:
$HBASE_HOME/bin/hbase shell

②create命令建表
在hbase shell下執行:
create 'zhb_test', 'cf'
創建一個名為’zhb_test’的表,并且帶有一個名為’cf’的列族,

補充一下列族的知識點:
前面提到過,HBase是一個分布式列式資料庫,
HBase的表都是由列族(Column Family)組成的;
沒有列族的表是沒有意義的;
列并不是依附于表上,而是依附于列族上;如下圖所示:

通過剛才的命令,我們現在建立的表有一個列族,叫cf,但是我們沒有指定這個列族里面有什么列,向表TableA中插入資料時,你只是向HBase中插入了一個單元格(Cell),而這個單元格是由表:列族:行:列來定位的,而別的行有沒有此列HBase并不知道,
HBase的所有資料屬性都是定義在列族上的,同一個表的不同列族可以定義完全不同的兩套屬性,所以從這個意義上來說,列族更像是傳統關系資料庫中的表,而表本身反倒變成只是存放列族的空殼了,
③ list命令看到整個庫中有哪些表

可以看到庫里共有15個表,包括我們剛才創建的’zhb_test’
④ describe命令來查看表屬性

輸入命令alter ‘zhb_test’, 'cf2’再添加一個列族,然后再describe看一下:

可以看到現在describe輸出的是兩個元素,分別對應cf和cf2兩 個列族,也印證了我們之前說的資料屬性是存在于列族上的,
補充:
在執行alter命令之前,最好先停用(disable)這個表 ,因為對列族的所有操作都會同步到所有擁有這個表的RegionServer上,你在執行命令的時候可以看到總共 有多少個RegionServer,當前執行了幾個RegionServer,當有很多客戶端都在連著的時候,直接新增一個列族對性能的影響較大,
⑤ put命令來插入資料
在HBase中,如果你的一行有10列,那存盤一行的資料得寫10行的 陳述句,這是因為HBase中行的每一個列都存盤在不同的位置,你必須指 定你要存盤在哪個單元格;而單元格需要根據表、行、列這幾個維度來 定位,
執行命令:
put ‘zhb_test’, ‘rowkey1’, ‘cf:name’, ‘zhb’

表示:
往’zhb_test’表插入一個單元格,這個單元格的rowkey為’rowkey1’,也就是說它是屬于’rowkey1’這個行中的 一個列,該單元格的列族為’cf’,該單元格的列名為’name’,資料值為’zhb’,
之后我們用scan命令掃描一下表,就可以看到我們剛才插入的資料了:
scan ‘zhb_test’

看這條記錄的時候,你會看到時間戳屬性,每一個單元格都可 以存盤多個版本(version)的值,HBase的單元格并沒有version這個 屬性,它用timestamp來存盤該條記錄的時間戳,這個時間戳就用來當 版本號使用,如果你在寫put陳述句的時候不指定時間戳,系統就會自動用當前時 間幫你指定它,有意思的是,這個timestamp雖然說是時間的標定,其 實你可以輸入任意的數字,比如1、2、3都可以存盤進去,當你用scan命令的時候HBase會顯示擁有最大(最新)的timestamp的資料版本,可以指定列族中保存的Cell版本數,
⑥ get命令獲取單元格資料
過get只能查詢一個單元格的記錄,在表的資料很大的時候,get查詢 的速度遠遠高于scan,
get ‘zhb_test’, ‘rowkey1’, ‘cf:name’

2.2 Java API
Hbase API 檔案:https://hbase.apache.org/apidocs/index.html
創建一個Maven專案,在pom.xml中添加如下依賴:
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>2.0.0</version>
</dependency>
HbaseClient.java
package javaa.sg.bigo;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class HbaseClient {
// config zookeeper
static private org.apache.hadoop.conf.Configuration configuration = null;
static private Connection connection = null;
private static Logger logger = LoggerFactory.getLogger(HbaseClient.class);
static private Lock lock = new ReentrantLock();
static Connection getConnectionInstance() {
if (null == connection) {
lock.lock();
try {
if (null == connection) {
configuration = HBaseConfiguration.create();
configuration.set("hbase.zookeeper.quorum", "zk1:2182,zk2:2182,zk3:2182");
configuration.set("hbase.client.keyvalue.maxsize", "100000000");
connection = ConnectionFactory.createConnection(configuration);
}
} catch (IOException e) {
logger.error("create hbase error ", e);
} finally {
lock.unlock();
}
}
return connection;
}
}
使用demo
// 懶加載單例模式
static private Connection connection = HbaseClient.getConnectionInstance();
/**
* 創建表
*
* @param tableName
*/
public static void createTable(String tableStr, String[] familyNames) {
System.out.println("start create table ......");
try {
Admin admin = connection.getAdmin();
TableName tableName = TableName.valueOf(tableStr);
if (admin.tableExists(tableName)) {// 如果存在要創建的表,那么先洗掉,再創建
admin.disableTable(tableName);
admin.deleteTable(tableName);
System.out.println(tableName + " is exist,detele....");
}
HTableDescriptor tableDescriptor = new HTableDescriptor(tableName);
// 添加表列資訊
if (familyNames != null && familyNames.length > 0) {
for (String familyName : familyNames) {
tableDescriptor.addFamily(new HColumnDescriptor(familyName));
}
}
admin.createTable(tableDescriptor);
} catch (MasterNotRunningException e) {
e.printStackTrace();
} catch (ZooKeeperConnectionException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("end create table ......");
}
/**
* 添加行列資料資料
*
* @param tableName
* @throws Exception
*/
public static void insertData(String tableName, String rowId, String familyName,String qualifier, String value) throws Exception {
System.out.println("start insert data ......");
Table table = connection.getTable(TableName.valueOf(tableName));
Put put = new Put(rowId.getBytes());// 一個PUT代表一行資料,再NEW一個PUT表示第二行資料,每行一個唯一的ROWKEY,此處rowkey為put構造方法中傳入的值
put.addColumn(familyName.getBytes(), qualifier.getBytes(), value.getBytes());// 本行資料的第一列
try {
table.put(put);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("end insert data ......");
}
/**
* 添加行列資料資料
*
* @param tableName
* @throws Exception
*/
public static void batchInsertData(String tableName, String rowId, List<String> familyNames,
String qualifier, List<String> values) throws Exception {
if (null == qualifier) qualifier = "tmp";
Table table = connection.getTable(TableName.valueOf(tableName));
Put put = new Put(rowId.getBytes());// 一個PUT代表一行資料,再NEW一個PUT表示第二行資料,每行一個唯一的ROWKEY,此處rowkey為put構造方法中傳入的值
for (int i = 0; i < familyNames.size(); ++i) {
put.addColumn(familyNames.get(i).getBytes(),
qualifier.getBytes(), values.get(i).getBytes());// 本行資料的第一列
}
try {
table.put(put);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 洗掉行
*
* @param tablename
* @param rowkey
*/
public static void deleteRow(String tablename, String rowkey) {
try {
Table table = connection.getTable(TableName.valueOf(tablename));
Delete d1 = new Delete(rowkey.getBytes());
table.delete(d1);//d1.addColumn(family, qualifier);d1.addFamily(family);
System.out.println("洗掉行成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 查詢所有資料
*
* @param tableName
* @throws Exception
*/
public static void queryAll(String tableName) throws Exception {
Table table = connection.getTable(TableName.valueOf(tableName));
try {
ResultScanner rs = table.getScanner(new Scan());
for (Result r : rs) {
System.out.println("獲得到rowkey:" + new String(r.getRow()));
for (Cell keyValue : r.rawCells()) {
System.out.println("列:" + new String(CellUtil.cloneFamily(keyValue))+":"+
new String(CellUtil.cloneQualifier(keyValue)) + "====值:" + new String(CellUtil.cloneValue(keyValue)));
}
}
rs.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 根據rowId查詢
*
* @param tableName
* @throws Exception
*/
public static void queryByRowId(String tableName, String rowId) throws Exception {
Table table = connection.getTable(TableName.valueOf(tableName));
try {
Get scan = new Get(rowId.getBytes());// 根據rowkey查詢
Result r = table.get(scan);
System.out.println("獲得到rowkey:" + new String(r.getRow()));
for (Cell keyValue : r.rawCells()) {
System.out.println("列:" + new String(CellUtil.cloneFamily(keyValue))+":"+
new String(CellUtil.cloneQualifier(keyValue)) + "====值:" + new String(CellUtil.cloneValue(keyValue)));
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 根據列條件查詢
*
* @param tableName
*/
public static void queryByCondition(String tableName, String familyName,String qualifier,String value) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Filter filter = new SingleColumnValueFilter(Bytes.toBytes(familyName),
Bytes.toBytes(qualifier), CompareOp.EQUAL, Bytes.toBytes(value)); // 當列familyName的值為value時進行查詢
Scan s = new Scan();
s.setFilter(filter);
ResultScanner rs = table.getScanner(s);
for (Result r : rs) {
System.out.println("獲得到rowkey:" + new String(r.getRow()));
for (Cell keyValue : r.rawCells()) {
System.out.println("列:" + new String(CellUtil.cloneFamily(keyValue))+":"+
new String(CellUtil.cloneQualifier(keyValue)) + "====值:" + new String(CellUtil.cloneValue(keyValue)));
}
}
rs.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 多條件查詢
*
* @param tableName
*/
public static void queryByConditions(String tableName, String[] familyNames, String[] qualifiers,String[] values) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
List<Filter> filters = new ArrayList<Filter>();
if (familyNames != null && familyNames.length > 0) {
int i = 0;
for (String familyName : familyNames) {
Filter filter = new SingleColumnValueFilter(Bytes.toBytes(familyName),
Bytes.toBytes(qualifiers[i]), CompareOp.EQUAL, Bytes.toBytes(values[i]));
filters.add(filter);
i++;
}
}
FilterList filterList = new FilterList(filters);
Scan scan = new Scan();
scan.setFilter(filterList);
ResultScanner rs = table.getScanner(scan);
for (Result r : rs) {
System.out.println("獲得到rowkey:" + new String(r.getRow()));
for (Cell keyValue : r.rawCells()) {
System.out.println("列:" + new String(CellUtil.cloneFamily(keyValue))+":" +
new String(CellUtil.cloneQualifier(keyValue)) +
"====值:" + new String(CellUtil.cloneValue(keyValue)));
}
}
rs.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 洗掉表
*
* @param tableName
*/
public static void dropTable(String tableStr) {
try {
Admin admin = connection.getAdmin();
TableName tableName = TableName.valueOf(tableStr);
admin.disableTable(tableName);
admin.deleteTable(tableName);
admin.close();
} catch (MasterNotRunningException e) {
e.printStackTrace();
} catch (ZooKeeperConnectionException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
三、存盤機制
這一節我們來看一下HBase這個資料庫是怎么存盤資料的,資料是怎么存盤到磁盤上的,我們會從最宏觀的Master和RegionServer結構一直 到最小的單元格(Cell),
再把宏觀架構圖拿出來復習一下:

從這張圖上可以看出一個HBase集群由一個Master(也可以把兩個 Master做成 HighAvailable)和多個RegionServer組成,右下角是其中 一個RegionServer的內部構造圖,我們先看完這幅圖的圖解后,再解剖 它,
RegionServer上有一個或者多個Region,我們讀 寫的資料就存盤在Region上,HBase是一個會自動分片的資料庫, 一個Region就相當于關系型資料庫中磁區表的一個磁區,
3.1 RegionServer結構
放大RegionServer,如下圖:

從這幅圖中我們可以看出一個RegionServer包含有:
-
一個WAL:
預寫日志,WAL是Write-Ahead Log的縮寫,從名字就可以看出它的用途,就是:預先寫入,當操作到達Region的時候,HBase先不管三七二十一把操作寫到WAL里面去,HBase會先把資料放到基于記憶體實作的Memstore里,等資料達到一定的數量 時才刷寫(flush)到最終存盤的HFile內,而如果在這個程序中 服務器宕機或者斷電了,那么資料就丟失了,WAL是一個保隙訓 制,資料在寫到Memstore之前,先被寫到WAL了,這樣當故障恢復的時候可以從WAL中恢復資料, -
多個Region:
Region相當于一個資料分片,每一個Region都有起始rowkey和結束rowkey,代表了它所存盤的row范圍,
3.2 Region內部結構
接下來我們來看單個Region內部的結構

一個Region包含有:
- 多個Store:每一個Region內都包含有多個Store實體,一個 Store對應一個列族的資料,舉個例子,如果一個表有兩個列族,那么在一 個Region里面就有兩個Store,在最右邊的單個Store的解剖圖上,我們可以看到Store內部有MemStore和HFile這兩個組成部分,
3.3 WAL(預寫日志)
預寫日志(Write-ahead log,WAL)就是設計來解決宕機之后的操作恢復問題的,資料到達 Region 的時候是先寫入 WAL,然后再被加載到 Memstore,就算 Region 的機器宕掉了,由于 WAL 的資料是存盤在 HDFS 上的,所以資料并不會丟失,
WAL 是默認開啟的,可以通過下面的代碼關閉 WAL,
Mutation.setDurability(Durability.SKIP_WAL);
Put、Append、Increment、Delete 都是 Mutation 的子類,所以他們都有 setDurability 方法,這樣可以讓該資料操作快一點,但是最好不要這樣做,因為當服務器宕機時,資料就會丟失,
如果你實在想不惜通過關閉 WAL 來提高性能,可以選擇異步寫入 WAL,
Mutation.setDurability(Durability.ASYNC WAL);
這樣設定后 Region 會等到條件滿足的時候才把操作寫入 WAL,這里提到的條件主要指的是時間間隔 hbase.regionserver.optionallogflushinterval,這個時間間隔的意思是 HBase 間隔多久會把操作從記憶體寫入 WAL,默認值是 1s,
3.4 Store內部結構
在 Store 中有兩個重要組成部分:
-
MemStore:每個 Store 中有一個 MemStore 實體,資料寫入 WAL 之后就會被放入 MemStore,MemStore 是記憶體的存盤物件,只有當 MemStore 滿了的時候才會將資料刷寫(flush)到 HFile 中;
-
HFile:在 Store 中有多個 HFile,當 MemStore 滿了之后 HBase 就會在 HDFS 上生成一個新的 HFile,然后把 MemStore 中的內容寫到這個 HFile 中,HFile 直接跟 HDFS 打交道,它是資料的存盤物體,

WAL 是存盤在 HDFS 上的,Memstore 是存盤在記憶體中的,HFile 又是存盤在 HDFS 上的;資料是先寫入 WAL,再被放入 Memstore,最后被持久化到 HFile 中,資料在進入 HFile 之前已經被存盤到 HDFS 一次了,為什么還需要被放入 Memstore?
這是因為 HDFS 上的檔案只能創建、追加、洗掉,但是不能修改,對于一個資料庫來說,按順序地存放資料是非常重要的,這是性能的保障,所以我們不能按照資料到來的順序來寫入硬碟,
可以使用記憶體先把資料整理成順序存放,然后再一起寫入硬碟,這就是 Memstore 存在的意義,雖然 Memstore 是存盤在記憶體中的,HFile 和 WAL 是存盤在 HDFS 上的,但由于資料在寫入 Memstore 之前,要先被寫入 WAL,所以增加 Memstore 的大小并不能加速寫入速度,Memstore 存在的意義是維持資料按照 rowkey 順序排列,而不是做一個快取,
3.5 MemStore
設計 MemStore 的原因有以下幾點:
-
由于 HDFS 上的檔案不可修改,為了讓資料順序存盤從而提高讀取效率,HBase 使用了 LSM 樹結構來存盤資料,資料會先在 Memstore 中整理成 LSM 樹,最后再刷寫到 HFile 上,
-
優化資料的存盤,比如一個資料添加后就馬上洗掉了,這樣在刷寫的時候就可以直接不把這個資料寫到 HDFS 上,
不過不要想當然地認為讀取也是先讀取 Memstore 再讀取磁盤喲!讀取的時候是有專門的快取叫 BlockCache,這個 BlockCache 如果開啟了,就是先讀 BlockCache,讀不到才是讀 HFile+Memstore,
3.6 HFile(StoreFile)
HFile是資料存盤的實際載體,我們創建的所有表、列等資料都存盤在HFile里面,HFile類似 Hadoop的TFile類,它模仿了BigTable的SSTable格式,HFile中組成部分如下圖所示:

我們可以看到HFile是由一個一個的塊組成的,在HBase中一個塊的 大小默認為64KB,由列族上的BLOCKSIZE屬性定義,這些塊區分了不同 的角色:
-
Data:資料塊,每個 HFile 有多個 Data 塊,我們存盤在 HBase 表中的資料就在這里,Data 塊其實是可選的,但是幾乎很難看到不包含 Data 塊的 HFile,
-
Meta:元資料塊,Meta 塊是可選的,Meta 塊只有在檔案關閉的時候才會寫入,Meta 塊存盤了該 HFile 檔案的元資料資訊,在 v2 之前布隆過濾器(Bloom Filter)的資訊直接放在 Meta 里面存盤,v2 之后分離出來單獨存盤,
-
FileInfo:檔案資訊,其實也是一種資料存盤塊,FileInfo 是 HFile 的必要組成部分,是必選的,它只有在檔案關閉的時候寫入,存盤的是這個檔案的資訊,比如最后一個 Key(LastKey),平均的 Key 長度(AvgKeyLen)等;
-
DataIndex:存盤 Data 塊索引資訊的塊檔案,索引的資訊其實也就是 Data 塊的偏移值(offset),DataIndex 也是可選的,有 Data 塊才有 DataIndex;
-
MetaIndex:存盤 Meta 塊索引資訊的塊檔案,MetaIndex 塊也是可選的,有 Meta 塊才有 MetaIndex;
-
Trailer:必選的,它存盤了 FileInfo、DataIndex、MetaIndex 塊的偏移值,
其實叫 HFile 或者 StoreFile 都沒錯,在物理存盤上我們管 MemStore 刷寫而成的檔案叫 HFile,StoreFile 就是 HFile 的抽象類而已,
3.7 Data資料塊
剛剛我們講完了HBase的底層存盤HFile,不過我們現在還沒有達到 HBase架構的最微觀結構,接下來,我們繼續解剖Data資料塊,

Data資料塊的第一位存盤的是塊的型別,后面存盤的是多個 KeyValue鍵值對,也就是單元格(Cell)的實作類,Cell是一個介面, KeyValue是它的實作類,
3.8 KeyValue類
讓我們來看看單元格最重要的實作類KeyValue類的結構,如下圖所示:

一個 KeyValue 類里面最后一個部分是存盤資料的 Value,而前面的部分都是存盤跟該單元格相關的元資料資訊,如果你存盤的 value 很小,那么這個單元格的絕大部分空間就都是 rowkey、column family、column 等的元資料,所以大家的列族和列的名字如果很長,大部分的空間就都被拿來存盤這些資料了,
不過如果采用適當的壓縮演算法就可以極大地節省存盤列族、列等資訊的空間了,所以在實際的使用中,可以通過指定壓縮演算法來壓縮這些元資料,不過壓縮和解壓必然帶來性能損耗,所以使用壓縮也需要根據實際情況來取舍,如果你的資料主要是歸檔資料,不太要求讀寫性能,那么壓縮演算法就比較適合你,
至此,我們終于將HBase剖析到了最小的不可分割的資料結構 KeyValue,
3.9 資料增刪改查的真面目
HBase 是一個可以隨機讀寫的資料庫,而它所基于的持久化層 HDFS 卻是要么新增,要么整個洗掉,不能修改的系統,那 HBase 怎么實作我們的增刪查改的?真實的情況是這樣的:HBase 幾乎總是在做新增操作,
-
當你新增一個單元格的時候,HBase 在 HDFS 上新增一條資料;
-
當你修改一個單元格的時候,HBase 在 HDFS 又新增一條資料,只是版本號比之前那個大(或者你自己定義);
-
當你洗掉一個單元格的時候,HBase 還是新增一條資料!只是這條資料沒有 value,型別為 DELETE,這條資料叫墓碑標記(Tombstone),
由于資料庫在使用程序中積累了很多增刪查改操作,資料的連續性和順序性必然會被破壞,為了提升性能,HBase 每間隔一段時間都會進行一次合并(Compaction),合并的物件為 HFile 檔案,
合并分為 minor compaction 和 major compaction,在 HBase 進行 major compaction 的時候,它會把多個 HFile 合并成 1 個 HFile,在這個程序中,一旦檢測到有被打上墓碑標記的記錄,在合并的程序中就忽略這條記錄,這樣在新產生的 HFile 中,就沒有這條記錄了,自然也就相當于被真正地洗掉了,
3.10 Hbase資料結構總結
HBase 資料的內部結構大體如下:
-
一個 RegionServer 包含多個 Region,劃分規則是:一個表的一段鍵值在一個 RegionServer 上會產生一個 Region,不過當某一行的資料量太大了(要非常大),HBase 也會把這個 Region 根據列族切分到不同的機器上去;
-
一個 Region 包含多個 Store,劃分規則是:一個列族分為一個 Store,如果一個表只有一個列族,那么這個表在這個機器上的每一個 Region 里面都只有一個 Store;
-
一個 Store 里面只有一個 Memstore;
-
一個 Store 里面有多個 HFile,每次 Memstore 的刷寫(flush)就產生一個新的 HFile 出來,
對應關系如下圖:

四、KeyValue的寫入和讀出
4.1 寫入
一個KeyValue被持久化到HDFS的程序的總結見下圖:

- WAL:資料被發出之后第一時間被寫入WAL,由于WAL是基于HDFS 來實作的,所以也可以說現在單元格就已經被持久化了,但是 WAL只是一個暫存的日志,它是不區分Store的,這些資料是不能
被直接讀取和使用, - Memstore:資料隨后會立即被放入Memstore中進行整理, Memstore會負責按照LSM樹的結構來存放資料,這個程序就像我 們在打牌的時候,抓牌之后在手上對牌進行整理的程序,
- HFile:最后,當Memstore太大了達到尺寸上的閥值,或者達到了刷寫時間間隔閥值的時候,HBaes會被這個Memstore的內容刷 寫到HDFS系統上,稱為一個存盤在硬碟上的HFile檔案,至此, 我們可以稱為資料真正地被持久化到硬碟上,就算宕機,斷電, 資料也不會丟失了,
4.2 讀出
由于有 MemStore(基于記憶體)和 HFile(基于HDFS)這兩個機制,你一定會立馬想到先讀取 MemStore,如果找不到,再去 HFile 中查詢,這是顯而易見的機制,可惜 HBase 在處理讀取的時候并不是這樣的,實際的讀取順序是先從 BlockCache 中找資料,找不到了再去 Memstore 和 HFile 中查詢資料,
墓碑標記和資料不在一個地方,讀取資料的時候怎么知道這個資料要洗掉呢?如果這個資料比它的墓碑標記更早被讀到,那在這個時間點真是不知道這個資料會被刪 除,只有當掃描器接著往下讀,讀到墓碑標記的時候才知道這個資料是被標記為洗掉的,不需要回傳給用戶,
所以 HBase 的 Scan 操作在取到所需要的所有行鍵對應的資訊之后還會繼續掃描下去,直到被掃描的資料大于給出的限定條件為止,這樣它才能知道哪些資料應該被回傳給用戶,而哪些應該被舍棄,所以你增加過濾條件也無法減少 Scan 遍歷的行數,只有縮小 STARTROW 和 ENDROW 之間的行鍵范圍才可以明顯地加快掃描的速度,
在 Scan 掃描的時候 store 會創建 StoreScanner 實體,StoreScanner 會把 MemStore 和 HFile 結合起來掃描,所以具體從 MemStore 還是 HFile 中讀取資料,外部的呼叫者都不需要知道具體的細節,當 StoreScanner 打開的時候,會先定位到起始行鍵(STARTROW)上,然后開始往下掃描,

其中紅色塊部分都是屬于指定row的資料,Scan要把所有符合條件 的StoreScanner都掃描過一遍之后才會回傳資料給用戶,
五、 Region的定位
Region 的查找,早期的設計(0.96.0)之前是被稱為三層查詢架構:

-
Region:查找的資料所在的 Region;
-
.META.:是一張元資料表,它存盤了所有 Region 的簡要資訊,.META. 表中的一行記錄就是一個 Region,該行記錄了該 Region 的起始行、結束行和該 Region 的連接資訊,這樣客戶端就可以通過這個來判斷需要的資料在哪個 Region 上;
-
ROOT-:是一張存盤 .META. 表的表,.META. 可以有很多張,而 -ROOT- 就是存盤了 .META. 表在什么 Region 上的資訊(.META. 表也是一張普通的表,也在 Region 上),通過兩層的擴展最多可以支持約 171 億個 Region,
-ROOT- 表記錄在 ZooKeeper 上,路徑為:/hbase/root-region-server;Client 查找資料的流程從宏觀角度來看是這樣的:
-
用戶通過查找 zk(ZooKeeper)的 /hbase/root-regionserver 節點來知道 -ROOT- 表在什么 RegionServer 上;
-
訪問 -ROOT- 表,看需要的資料在哪個 .META. 表上,這個 .META. 表在什么 RegionServer 上;
-
訪問 .META. 表來看要查詢的行鍵在什么 Region 范圍里面;
-
連接具體的資料所在的 RegionServer,這回就真的開始用 Scan 來遍歷 row 了,

從 0.96 版本之后這個三層查詢架構被改成了二層查詢架構,-ROOT- 表被去掉了,同時 zk 中的 /hbase/root-region-server 也被去掉了,直接把 .META. 表所在的 RegionServer 資訊存盤到了 zk 中的 /hbase/meta-region-server,再后來引入了 namespace,.META. 表被修改成了 hbase:meta,
新版 Region 查找流程:
-
客戶端先通過 ZooKeeper 的 /hbase/meta-region-server 節點查詢到哪臺 RegionServer 上有 hbase:meta 表,
-
客戶端連接含有 hbase:meta 表的 RegionServer,hbase:meta 表存盤了所有 Region 的行鍵范圍資訊,通過這個表就可以查詢出要存取的 rowkey 屬于哪個 Region 的范圍里面,以及這個 Region 又是屬于哪個 RegionServer;
-
獲取這些資訊后,客戶端就可以直連其中一臺擁有要存取的 rowkey 的 RegionServer,并直接對其操作;
-
客戶端會把 meta 資訊快取起來,下次操作就不需要進行以上加載 hbase:meta 的步驟了,
二層查詢架構如下圖所示:

下半篇主要介紹:
① Region的拆分和合并,及相關經驗總結,
② HFile的合并(compaction)
③ Region自動均衡
④ BlockCache 和 BloomFilter
等,
參考資料:
《HBase不睡覺書》
https://www.cnblogs.com/littleatp/p/11946199.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/302191.html
標籤:其他
上一篇:深入理解HDFS
