最近學習了HBase

HBase是什麼html

最近學習了HBase,正常來講寫這篇文章,應該從DB有什麼缺點,HBase如何彌補DB的缺點開始講會更有體感,可是本文這些暫時不講,只講HBase,把HBase相關原理和使用講清楚,後面有一篇文章會專門講DB與NoSql各自的優缺點以及使用場景。mysql

HBase是谷歌Bigtable的開源版本,2006年穀歌發佈《Bigtable:A Distributed Storage System For Structured Data》論文以後,Powerset公司就宣佈HBase在Hadoop項目中成立,做爲子項目存在。後來,在2010年左右逐漸成爲Apache旗下的一個頂級項目,所以HBase名稱的由來就是因爲其做爲Hadoop Database存在的,用於存儲非結構化、半結構化的數據。redis

下圖展現了HBase在Hadoop生態中的位置:sql

能夠看到HBase創建在HDFS上,HBase內部管理的文件所有都是存儲在HDFS中,同時MapReduce這個計算框架在HBase之上又提供了高性能的計算能力來處理海量數據。數據庫

 

HBase的特色與不足apache

HBase的基本特色歸納大體以下:api

  • 海量數據存儲(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的字典序排列的。例如一、二、三、四、五、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中,此時作倒序即DCBA53一、HGFE53一、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的時候系統發生了宕機或者重啓,數據都不會出現丟失。

相關文章
相關標籤/搜索