HBase是什么
最近學習了HBase,正常來說寫這篇文章,應該從DB有什么缺點,HBase如何彌補DB的缺點開始講會更有體感,但是本文這些暫時不講,只講HBase,把HBase相關原理和使用講清楚,后面有一篇文章會專門講DB與NoSql各自的優缺點以及使用場景。
HBase是谷歌Bigtable的開源版本,2006年谷歌發布《Bigtable:A Distributed Storage System For Structured Data》論文之后,Powerset公司就宣布HBase在Hadoop項目中成立,作為子項目存在。后來,在2010年左右逐漸成為Apache旗下的一個頂級項目,因此HBase名稱的由來就是由於其作為Hadoop Database存在的,用於存儲非結構化、半結構化的數據。
下圖展示了HBase在Hadoop生態中的位置:
可以看到HBase建立在HDFS上,HBase內部管理的文件全部都是存儲在HDFS中,同時MapReduce這個計算框架在HBase之上又提供了高性能的計算能力來處理海量數據。
HBase的特點與不足
HBase的基本特點概括大致如下:
- 海量數據存儲(PB)級別,在PB級別數據以及采用廉價PC存儲的情況下,數據能在幾十到百毫秒內返回數據
- 高可用,WAL + Replication機制保證集群異常不會導致寫入數據丟失與數據損壞,且HBase底層使用HDFS,HDFS本身也有備份
- 數據寫入性能強勁
- 列式存儲,和傳統數據庫行式存儲有本質的區別,這個在之后HBase存儲原理的時候詳細解讀
- 半結構化或非結構化數據存儲
- 存儲稀松靈活,列數據為空的情況下不占據存儲空間
- 同一份數據,可存儲多版本號數據,方便歷史數據回溯
- 行級別事務,可以保證行級別數據的ACID特性
- 擴容方便,無需數據遷移,及擴即用
當然事事不是完美的,HBase也存在着以下兩個最大的不足:
- 無法做到條件查詢,這是最大的問題,假如你的代碼中存在多個查詢條件,且每次使用哪個/哪組查詢條件不確定,那么使用HBase是不合適的,除非數據冗余,設計多份RowKey
- 做不了分頁,數據總記錄數幾乎無法統計,因為HBase本身提供的表行數統計功能是一個MapReduce任務,極為耗時,既然拿不到總記錄數,分頁總署也沒法確定,自然分頁也無法做了
總的來說,對於HBase需要了解以上的一些個性應該大致上就可以了,根據HBase的特點與不足,在合適的場景下選擇使用HBase,接下來針對HBase的一些知識點逐一解讀。
HBase的基本架構
下圖是HBase的基本架構:
從圖上可以看到,HBase中包含的一些組件如下:
- Client----包含訪問HBase的接口
- Zookeeper----通過選舉保證任何時候集群中只有一個HMaster、HMaster與Region Server啟動時向注冊、存儲所有Region的尋址入口、實時監控Region Server的上下線信息並實時通知給HMaster、存儲HBase的Schema與Table原數據
- HMaster----為Region Server分配Region、負責Region Server的負載均衡、發現失效的Region Server並重新分配其上的Region、管理用戶對Table的增刪改查
- Region Server----維護Region並處理對Region的IO請求、切分在運行過程中變得過大的Region
其中,Region是分布式存儲和負載均衡中的最小單元,不過並不是存儲的最小單元。Region由一個或者多個Store組成,每個Store保存一個列簇;每個Store又由一個memStore和0~N個StoreFile組成,StoreFile包含HFile,StoreFile只是對HFile做了輕量級封裝,底層就是HFile。
介於上圖元素有點多,我這邊畫了一張圖,把HBase架構中涉及的元素的關系理了一下:
HBase的基本概念
接着看一下HBase的一些基本概念,HBase是以Table(表)組織數據的,一個Table中有着以下的一些元素:
- RowKey(行鍵)----即關系型數據庫中的主鍵,它是唯一的,在HBase中這個主鍵可以是任意的字符串,最大長度為64K,在內部存儲中會被存儲為字節數組,HBase表中的數據是按照RowKey的字典序排列的。例如1、2、3、4、5、10,按照自然數的順序是這樣的,但是在HBase中1后面跟的是10而不是2,因此在設計RowKey的時候一定要充分利用字典序這個特性,將一下經常讀取的行存儲到一起或者靠近,減少Scan耗時,提高讀取的效率
- Column Family(列族)----表Schema的一部分,HBase表中的每個列都歸屬於某個列族,即列族是由一系列的列組成的,必須在創建表的時候就指定好。列明都以列族作為前綴,例如courses:history、courses:math都屬於courses這個列族。列族不是越多越好,過多的列族會導致io增多及分裂時數據不均勻,官方推薦列族數量為1~3個。列族不僅能幫助開發者構建數據的語義邊界,還能有助於開發者設置某些特性,例如可以指定某個列族內的數據壓縮形式。訪問控制、磁盤和內存怒的使用統計都是在列族層面進行的
- Column(列)----一般從屬於某個列族,列的數量一般沒有強限制,一個列族中可以有數百萬列且這些列都可以動態添加
- Version Number(版本號)----HBase中每一列的值或者說每個單元格的值都是具有版本號的,默認使用系統當前時間,精確到毫秒,也可以用戶顯式地設置。每個單元格中,不同版本的數據按照時間倒序排序,即最新的數據排在最前面。另外,為了避免數據存在過多版本造成的管理(存儲 + 索引)負擔,HBase提供了兩種數據版本回收的方式,一是保存數據的最后n個版本,二是保存最近一段時間內的版本,用戶可以針對每個列族進行設置
- Cell(單元格)----一個單元格就是由RowKey、Column Family:Column、Version Number唯一確定的,Cell中的數據是沒有類型的,全部都是字節碼
另外一個概念就是,訪問HBase Table中的行,只有三種方式:
- 通過單個Row Key訪問
- 通過Row Key的range
- 全表掃描
這部分介紹的Table、RowKey、Column Family、Column等都屬於邏輯概念,而上部分中的Region Server、Region、Store等都屬於物理概念,下圖展示了邏輯概念與物理概念之間的關系:
即:table和region是一對多的關系,因為table的數據可能被打在多個region中;region和columnFamily是一對多的關系,一個store對應一個columnFamily,一個region可能對應多個store。
HBase的邏輯表視圖與物理表視圖
接着看一下HBase中的表邏輯視圖與物理視圖。首先是邏輯表視圖:
看到這里定義了2個列族,一個Personal Info、一個Family Info,對應到數據庫中,相當於把兩張表合並到一個一起。
從邏輯視圖看,上圖由ZhangSan、LiSi兩行組成,但是在實際物理存儲上卻不是按照這種方式進行的存儲:
看到主要是有兩點差別:
- 一行被拆開了,按照列族進行存儲
- 空列不會被存儲,例如LiSi在Peronal Info中沒有Provice與Phone,在Family Info中沒有Brother
HBase的增刪改查
光說不練假把式,不能光講理論,代碼也是要有的,為了方便起見,我用的是阿里雲HBase,和HBase一樣,只是省去了運維成本。當然雖然本人是內部員工,但是工作之外的學習是不會占用公司資源的^_^悄悄告訴大家,阿里雲HBase有個福利,第一個月免費試用,想同樣玩一下HBase的可以去阿里雲搞一個。
首先添加一下pom依賴,用阿里雲指定的HBase,使用上和原生的HBase API一模一樣:
<dependency> <groupId>com.aliyun.hbase</groupId> <artifactId>alihbase-client</artifactId> <version>2.0.3</version> </dependency> <dependency> <groupId>jdk.tools</groupId> <artifactId>jdk.tools</artifactId> <version>1.8</version> <scope>system</scope> <systemPath>${JAVA_HOME}/lib/tools.jar</systemPath> </dependency>
注意一下第二個dependency,jdk.tools不添加pom文件可能會報錯"Missing artifact jdk.tools:jdk.tools:jar:1.8",錯誤原因是tools.jar包是JDK自帶的,pom.xml中的包隱式依賴tools.jar包,而tools.jar並未在庫中,因此需要將tools.jar包添加到jdk庫中。
首先寫個HBaseUtil,用單例模式來寫,好久沒寫了,順便練習一下:
1 /** 2 * 五月的倉頡https://www.cnblogs.com/xrq730/p/11134806.html 3 */ 4 public class HBaseUtil { 5 6 private static HBaseUtil hBaseUtil; 7 8 private Configuration config = null; 9 10 private Connection connection = null; 11 12 private Map<String, Table> tableMap = new HashMap<String, Table>(); 13 14 private HBaseUtil() { 15 16 } 17 18 public static HBaseUtil getInstance() { 19 if (hBaseUtil == null) { 20 synchronized (HBaseUtil.class) { 21 if (hBaseUtil == null) { 22 hBaseUtil = new HBaseUtil(); 23 } 24 } 25 } 26 27 return hBaseUtil; 28 } 29 30 /** 31 * 初始化Configuration與Connection 32 */ 33 public void init(String zkAddress) { 34 config = HBaseConfiguration.create(); 35 config.set(HConstants.ZOOKEEPER_QUORUM, zkAddress); 36 37 try { 38 connection = ConnectionFactory.createConnection(config); 39 } catch (IOException e) { 40 e.printStackTrace(); 41 System.exit(0); 42 } 43 } 44 45 /** 46 * 創建table 47 */ 48 public void createTable(String tableName, byte[]... columnFamilies) { 49 // HBase創建表的時候必須創建指定列族 50 if (columnFamilies == null || columnFamilies.length == 0) { 51 return ; 52 } 53 54 TableDescriptorBuilder tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(TableName.valueOf(tableName)); 55 for (byte[] columnFamily : columnFamilies) { 56 tableDescriptorBuilder.setColumnFamily(ColumnFamilyDescriptorBuilder.newBuilder(columnFamily).build()); 57 } 58 59 try { 60 Admin admin = connection.getAdmin(); 61 admin.createTable(tableDescriptorBuilder.build()); 62 // 這個Table連接存入內存中 63 tableMap.put(tableName, connection.getTable(TableName.valueOf(tableName))); 64 } catch (Exception e) { 65 e.printStackTrace(); 66 System.exit(0); 67 } 68 69 } 70 71 public Table getTable(String tableName) { 72 Table table = tableMap.get(tableName); 73 if (table != null) { 74 return table; 75 } 76 77 try { 78 table = connection.getTable(TableName.valueOf(tableName)); 79 if (table != null) { 80 // table對象存入內存 81 tableMap.put(tableName, table); 82 } 83 84 return table; 85 } catch (IOException e) { 86 e.printStackTrace(); 87 return null; 88 } 89 } 90 91 }
注意,HBase中的數據一切皆二進制,因此從上面代碼到后面代碼,字符串全部都轉換成了二進制。
接着定義一個BaseHBaseUtilTest類,把一些基本的定義放在里面,保持主測試類清晰:
1 /** 2 * 五月的倉頡https://www.cnblogs.com/xrq730/p/11134806.html 3 */ 4 public class BaseHBaseUtilTest { 5 6 protected static final String TABLE_NAME = "student"; 7 8 protected static final byte[] COLUMN_FAMILY_PERSONAL_INFO = "personalInfo".getBytes(); 9 10 protected static final byte[] COLUMN_FAMILY_FAMILY_INFO = "familyInfo".getBytes(); 11 12 protected static final byte[] COLUMN_NAME = "name".getBytes(); 13 14 protected static final byte[] COLUMN_AGE = "age".getBytes(); 15 16 protected static final byte[] COLUMN_PHONE = "phone".getBytes(); 17 18 protected static final byte[] COLUMN_FATHER = "father".getBytes(); 19 20 protected static final byte[] COLUMN_MOTHER = "mother".getBytes(); 21 22 protected HBaseUtil hBaseUtil; 23 24 }
第一件事情,創建Table,注意前面說的,HBase必須Table和列族一起創建:
1 /** 2 * 五月的倉頡https://www.cnblogs.com/xrq730/p/11134806.html 3 */ 4 public class HBaseUtilTest extends BaseHBaseUtilTest { 5 6 @Before 7 public void init() { 8 hBaseUtil = HBaseUtil.getInstance(); 9 hBaseUtil.init("xxx"); 10 } 11 12 /** 13 * 創建表 14 */ 15 @Test 16 public void testCreateTable() { 17 hBaseUtil.createTable(TABLE_NAME, COLUMN_FAMILY_PERSONAL_INFO, COLUMN_FAMILY_FAMILY_INFO); 18 } 19 20 }
我自己申請的HBase,zk地址就不給大家看啦,如果同樣申請了的,替換一下就好了。testCreateTable方法運行一下,就創建好了student表。接着利用put創建四條數據,多創建幾條,等下scan可以測試:
1 /** 2 * 添加數據 3 */ 4 @Test 5 public void testPut() throws Exception { 6 Table table = hBaseUtil.getTable(TABLE_NAME); 7 // 用戶1,用戶id:12345 8 Put put1 = new Put("12345".getBytes()); 9 put1.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_NAME, "Lucy".getBytes()); 10 put1.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_AGE, "18".getBytes()); 11 put1.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_PHONE, "13511112222".getBytes()); 12 put1.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_FATHER, "LucyFather".getBytes()); 13 put1.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_MOTHER, "LucyMother".getBytes()); 14 // 用戶2,用戶id:12346 15 Put put2 = new Put("12346".getBytes()); 16 put2.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_NAME, "Lily".getBytes()); 17 put2.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_AGE, "19".getBytes()); 18 put2.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_PHONE, "13522223333".getBytes()); 19 put2.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_FATHER, "LilyFather".getBytes()); 20 put2.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_MOTHER, "LilyMother".getBytes()); 21 // 用戶3,用戶id:12347 22 Put put3 = new Put("12347".getBytes()); 23 put3.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_NAME, "James".getBytes()); 24 put3.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_AGE, "22".getBytes()); 25 put3.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_FATHER, "JamesFather".getBytes()); 26 put3.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_MOTHER, "JamesMother".getBytes()); 27 // 用戶4,用戶id:12447 28 Put put4 = new Put("12447".getBytes()); 29 put4.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_NAME, "Micheal".getBytes()); 30 put4.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_AGE, "22".getBytes()); 31 put2.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_PHONE, "13533334444".getBytes()); 32 put4.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_MOTHER, "MichealMother".getBytes()); 33 34 table.put(Lists.newArrayList(put1, put2, put3, put4)); 35 }
同樣的,運行一下testPut方法,四條數據就創建完畢了。注意為了提升處理效率,HBase的get、put這些API都提供的批量處理方式,這樣一次提交可以提交多條數據,發起一次請求即可,不用發起請求。
接着看一下利用Get API查詢數據:
1 /** 2 * 獲取數據 3 */ 4 @Test 5 public void testGet() throws Exception { 6 Table table = hBaseUtil.getTable(TABLE_NAME); 7 // get1,拿到全部數據 8 Get get1 = new Get("12345".getBytes()); 9 // get2,只拿personalInfo數據 10 Get get2 = new Get("12346".getBytes()); 11 get2.addFamily(COLUMN_FAMILY_PERSONAL_INFO); 12 13 Result[] results = table.get(Lists.newArrayList(get1, get2)); 14 if (results == null || results.length == 0) { 15 return ; 16 } 17 18 for (Result result : results) { 19 printResult(result); 20 } 21 } 22 23 private void printResult(Result result) { 24 System.out.println("====================分隔符===================="); 25 printBytes(result.getValue(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_NAME)); 26 printBytes(result.getValue(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_AGE)); 27 printBytes(result.getValue(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_PHONE)); 28 printBytes(result.getValue(COLUMN_FAMILY_FAMILY_INFO, COLUMN_FATHER)); 29 printBytes(result.getValue(COLUMN_FAMILY_FAMILY_INFO, COLUMN_MOTHER)); 30 } 31 32 private void printBytes(byte[] bytes) { 33 if (bytes != null && bytes.length != 0) { 34 System.out.println(new String(bytes)); 35 } 36 }
HBase查詢數據比較靈活的是,可以查詢RowKey下對應的所有數據、可以按照RowKey-Column Family的維度查詢數據、可以按照RowKey-Column Family-Column的維度查詢數據,也可以按照RowKey-Column Family-Column-Timestamp的維度查詢數據,可以查詢Timestamp區間內的數據,也可以查詢RowKey-Column Family-Column下所有Timestamp數據。上面的代碼執行結果為:
====================分隔符==================== Lucy 18 13511112222 LucyFather LucyMother ====================分隔符==================== Lily 19 13533334444
和我們的預期相符,即"12345"這個RowKey查詢出了所有數據,"12346"這個RowKey只查了personalInfo這個列族的數據。
最后這一部分我們看一下更新,更新的API和新增的API都是一樣的,都是Put:
@Test public void testUpdate() throws Exception { Table table = hBaseUtil.getTable(TABLE_NAME); // 用戶1,用戶id:12345 Put put = new Put("12346".getBytes()); put.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_AGE, 1, "22".getBytes()); table.put(put); }
Get看一下執行12346這條數據的值:
Lily 19 13533334444
看到12346對應的數據,原本Age是19,更新到22,依然是19,這就是一個值得注意的點了。HBase的更新其實是往Table里面新增一條記錄,按照Timestamp進行排序,最新的數據在前面,每次Get的時候將第一條數據取出來。在這里我們指定的Timestamp=1,這個值落后於先前插入的Timestamp,自然就排在后面,因此讀取出來的Age依然是原值19,這個細節特別注意一下。
HBase的Scan
感覺前面篇幅有點大,所以這里專門抽一個篇幅出來寫一下Scan,Scan是HBase掃描數據的方式。
首先可以看一下最基本的Scan:
1 /** 2 * 掃描 3 */ 4 @Test 5 public void testScan() throws Exception { 6 Table table = hBaseUtil.getTable(TABLE_NAME); 7 Scan scan = new Scan().withStartRow("12345".getBytes(), true).withStopRow("12347".getBytes(), true); 8 9 ResultScanner rs = table.getScanner(scan); 10 if (rs != null) { 11 for (Result result : rs) { 12 printResult(result); 13 } 14 } 15 }
執行結果為:
====================分隔符==================== Lucy 19 13511112222 LucyFather LucyMother ====================分隔符==================== Lily 19 13533334444 LilyFather LilyMother ====================分隔符==================== James 22 JamesFather JamesMother
表示查詢12345~12347這個范圍內的所有RowKey,withStartRow的第二個參數true表示包含,如果為false那么12345這個RowKey就查不出來了。
進階的,HBase為我們提供了帶過濾器的Scan,一共有十來種,我這邊只演示兩種以及組合的情況,其他的查詢一下HBase API文檔即可,2.1版本的API文檔地址為http://hbase.apache.org/2.1/apidocs/index.html。演示代碼如下:
1 @Test 2 public void testScanFilter() throws Exception { 3 Table table = hBaseUtil.getTable(TABLE_NAME); 4 5 System.out.println("********************RowFilter測試********************"); 6 Scan scan0 = new Scan().withStartRow("12345".getBytes(), true); 7 scan0.setFilter(new RowFilter(CompareOperator.EQUAL, new BinaryComparator("12346".getBytes()))); 8 ResultScanner rs0 = table.getScanner(scan0); 9 printResultScanner(rs0); 10 11 System.out.println("********************PrefixFilter測試********************"); 12 Scan scan1 = new Scan().withStartRow("12345".getBytes(), true); 13 scan1.setFilter(new PrefixFilter("124".getBytes())); 14 ResultScanner rs1 = table.getScanner(scan1); 15 printResultScanner(rs1); 16 17 System.out.println("********************兩種Filter同時滿足測試********************"); 18 Scan scan2 = new Scan().withStartRow("12345".getBytes(), true); 19 Filter filter0 = new RowFilter(CompareOperator.EQUAL, new BinaryComparator("12447".getBytes())); 20 Filter filter1 = new PrefixFilter("124".getBytes()); 21 FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL, filter0, filter1); 22 scan2.setFilter(filterList); 23 ResultScanner rs2 = table.getScanner(scan2); 24 printResultScanner(rs2); 25 }
執行結果為:
********************RowFilter測試******************** ====================分隔符==================== Lily 19 13533334444 LilyFather LilyMother ********************PrefixFilter測試******************** ====================分隔符==================== Micheal 22 MichealMother ********************兩種Filter同時滿足測試******************** ====================分隔符==================== Micheal 22 MichealMother
總的來說,HBase本質上是KV型NoSql,根據Key查詢Value是最高效的,Scan這個API還是慎用,范圍里面的數據量小倒無所謂,一旦RowKey設計不合理,StartRow和EndRow沒有指定好,可能會造成大范圍的掃描,降低HBase整體能力。
HBase和KV型緩存的區別
看了上面的代碼演示,不知道大家有沒有和我一開始有一樣的疑問:HBase看上去也是K-V形式的,那么它和支持KV型數據的緩存(例如Redis、MemCache、Tair)有什么區別?
我用一張表格總結一下二者的區別:
總的來說,同樣作為數據庫的NoSql替代方案,HBase更加適合用於海量數據的持久化場景,KV型緩存更加適合用於對數據的高性能讀寫上。
HBase的Region分裂及會導致的熱點問題
經典問題,首先看一下什么是Region分裂,只把Region分裂講清楚,不講具體Region分裂的實現方式,理由也很簡單,Region分裂細節學得再清楚,對工作中的幫助也不大,沒必要太過於追根究底。
Region分裂是HBase能夠擁有良好擴張性的最重要因素之一,也必然是所有分布式系統追求無限擴展性的一副良葯。通過前面的部分我們知道HBase的數據以行為單位存儲在HBase表中,HBase表按照多行被分割為多個Region,這個Region分布在HBase集群中,並且由Region Server進程負責講這些Region提供給Client訪問。一個Region中,RowKey是一個連續的范圍,也就是說表中的記錄在Region中是按照startKey到endKey的范圍為RowKey進行排序存儲的。通常一個表由多個Region構成,這些Region分布在多個Region Server上,也就是說,Region是在Region Server中插入和查詢數據時負載均衡的物理機制。一張HBase表在剛剛創建的時候默認只有一個Region,所以關於這張表的請求都被路由到同一個Region Server,無論集群中有多少Region Server,而一旦某個Region的大小達到一定值,就會自動分裂為兩個Region,這也就是為什么HBase表在剛剛創建的階段不能充分利用整個集群吞吐量的原因。
在HBase管理界面可以查看每個Region,startKey與endKey的范圍,例如(圖片來自網絡):
這里特別注意一個點,RowKey是按照Key的字符自然順序進行排序的,因此RowKey=9的Key,會落在最后一個Region Server中而不是第一個Region Server中。
那么什么是熱點問題應該也很好理解了:
雖然HBase的單機讀寫性能強勁,但是當集群中成千上萬的請求RowKey都落在aaaaa-ddddd之間,那么這成千上萬請求最終落到Region Server1這台服務器上,一旦超出服務器自身承受能力,那么必然導致服務器不可用甚至宕機。因此我們說設計RowKey的時候千萬把時間戳或者id自增的方式作為RowKey方案就是這個道理,時間戳或者id自增的方式,雖然最終可以讓RowKey落到不同的Region中,但是在當下或者當下往后的一段時間內,RowKey一定是會落到同一個Region中的,數據熱點問題將嚴重影響HBase集群能力。
解決熱點問題通常有兩個方案,最初級的方案是設置預分區,即在Table創建的時候就先設置幾個Region,為每個Region划分不同的startKey與endKey,但這么做有以下兩個缺點:
- 高度依賴RowKey,必須事先知道插入數據的RowKey的分布
- 即使事先知道插入數據的RowKey分布,但是如果數據分布不均勻或者存在熱點行,依然無法均勻分攤負載
但是無論如何,設置預分區依然是一種解決熱點問題的方案。
第二個解決方案是一勞永逸的解決方案也是使用HBase最核心的一個點:合理設計RowKey。即讓RowKey均勻分布在Region中,大致有以下幾個方案可供參考:
- 倒序。例如手機號碼135ABCD、135EFGH、135IJKL這種,前綴沒有區分度,非常容易落到相同的Region中,此時做倒序即DCBA531、HGFE531、LKJI531,將有區分度的部分放在前面,就非常容易將數據散落在不同的Region中
- 原數據加密。例如做MD5,因為MD5的隨機性是非常強的,因此做了MD5后,數據將會非常分散
- 加隨機前綴。例如ASCII碼中隨機選5位作為數據前綴,同樣可以達到分散RowKey的效果,但是缺點是必須記住每個原數據對應的前綴
無論如何,還是那句話,合理設計RowKey是HBase使用的核心。
WAL機制
最后講一下前面提到的WAL機制,WAL的全稱為Write Ahead Log,它是HBase的RegionServer在處理數據插入和刪除的過程中用來記錄操作內容的一種日志,是用來做災難恢復的。
其實WAL並不是什么新鮮思想,在數據庫領域很常見:
- mysql有binlog,記錄每一次數據變更
- redis有aof,在開啟aof的情況下,每隔短暫時間,將這段時間產生的操作記錄文件
其核心都是,變更數據前先寫磁盤日志文件,在系統發生異常的時候,重放日志文件對數據進行恢復,HBase的WAL機制也是一樣的思想,數據變更步驟為:
- 首先從之前的圖上可以看到有HLog,HLog是實現WAL的類,一個RegionServer對應一個HLog,多個Region共享一個HLog,不過從HBase1.0版本開始可以定義多個HLog以提高吞吐量
- 客戶端的一次數據提交先寫HLog,這個是告知客戶端數據提交成功的前提
- HLog寫入成功后寫入MemStore
- 當MemStore的值達到一定程度后,flush到hdfs,形成一個一個的StoreFile(HFile)
- flush過后,HLog中對應的數據就沒用了,刪除
因為有了HLog,即使在MemStore中的數據還沒有flush到hdfs的時候系統發生了宕機或者重啟,數據都不會出現丟失。