環境信息:
HBase 1.2.2,Hadoop 2.7.2
使用需求:
爲什麼需要合併Region呢?這個需要從Region的Split來說。當一個Region被不斷的寫數據,達到Region的Split的閥值時(由屬性hbase.hregion.max.filesize來決定,默認是10GB),該Region就會被Split成2個新的Region。隨着業務數據量的不斷增加,Region不斷的執行Split,那麼Region的個數也會越來越多。
一個業務表的Region越多,在進行讀寫操作時,或是對該表執行Compaction操作時,此時集羣的壓力是很大的。這裏筆者做過一個線上統計,在一個業務表的Region個數達到9000+時,每次對該表進行Compaction操作時,集羣的負載便會加重。而間接的也會影響應用程序的讀寫,一個表的Region過大,勢必整個集羣的Region個數也會增加,負載均衡後,每個RegionServer承擔的Region個數也會增加。
另外的場景,當你建表的時候進行了預分區,由於各種原因,預分區不是很合理,導致各個region的數據不均衡
最後,如果你的hbase集羣每個regionserver上的region個數超過1000個的時候,該服務器上的region將不會進行split,進而導致一系列的問題
因此,上面這些情況是很有必要的進行Region合併的。對特定條件下的Region進行一次合併,減少每個業務表的Region,從而降低整個集羣的Region,減緩每個RegionServer上的Region壓力。
如何合併:
源生API合併:
# 合併相鄰的兩個Region
hbase> merge_region 'ENCODED_REGIONNAME', 'ENCODED_REGIONNAME'
# 強制合併兩個Region
hbase> merge_region 'ENCODED_REGIONNAME', 'ENCODED_REGIONNAME', true
這是HBase有提供的合併Region的命令,但是這種方式會有一個問題,就是隻能一次合併2個Region,如果這裏有幾千個Region需要合併,這種方式是不可取的。
批量合併:
通過編寫代碼,提供一種按條件批量merge的方法,規則如下:
使用hbase java api對hbase的特定表按條件進行合併,由於未找到合適的api,所以hbase的region大小從hdfs上獲取,如果有java api能實現該功能希望大家告知
閥值:
- lower_size:小region,直接合並,默認100M
- upper_size:大region,按條件合併,默認5G
- region_max_size:最大region大小,建議和region split的閥值大小相同,默認10G
- least_region_count:最少region個數,默認10個
算法:
首先從hdfs的hbase存儲路徑獲得table的region名稱和size對應關係的map,然後通過api獲取排序好的region實例List,然後遍歷該List進行兩兩合併
規則:(待合併的region定義爲A,B兩個region)
當前region大小小於upper_size:
- 如果A region爲空,則將當前region賦值給A
- 如果A region不爲空,則將當前region賦值給B,則直接調用api對A,B進行合併,成功之後清空A,B繼續遍歷
當前region大小大於upper_size:
- 如果A region爲空,放棄處理該region,直接繼續遍歷
- 如果A region不爲空,並且A的大小小於lower_size或者A+當前的大小小於region_max_size,則將當前region賦值給B,則直接調用api對A,B進行合併,成功之後清空A,B繼續遍歷
上代碼:
package com.taochy.util;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.HRegionInfo;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.util.Bytes;
/**
* 使用hbase java api對hbase的特定表按條件進行合併,由於未找到合適的api,所以hbase的region大小從hdfs上獲取,如果有java api能實現該功能希望大家告知
* 閥值:lower_size:小region,直接合並,默認100M
* upper_size:大region,按條件合併,默認5G
* region_max_size:最大region大小,建議和region split的閥值大小相同,默認10G
* least_region_count:最少region個數,默認10個
* 算法:首先從hdfs的hbase存儲路徑獲得table的region名稱和size對應關係的map,然後通過api獲取排序好的region實例List,然後遍歷該List進行兩兩合併
* 規則:(待合併的region定義爲A,B兩個region)
* 當前region大小小於upper_size:
* 如果A region爲空,則將當前region賦值給A
* 如果A region不爲空,則將當前region賦值給B,則直接調用api對A,B進行合併,成功之後清空A,B繼續遍歷
* 當前region大小大於upper_size:
* 如果A region爲空,放棄處理該region,直接繼續遍歷
* 如果A region不爲空,並且A的大小小於lower_size或者A+當前的大小小於region_max_size,則將當前region賦值給B,則直接調用api對A,B進行合併,成功之後清空A,B繼續遍歷
* @author taochy
*
*
*/
public class HBaseRegionMergeUtil {
//小region,直接合並,默認100M
private static long lower_size = 100 * 1024 * 1024L;
//大region,按條件合併,默認5G
private static long upper_size = 5 * 1024 * 1024 * 1024L;
//最大region大小,建議和region split的閥值大小相同,默認10G
private static long region_max_size = 10 * 1024 * 1024 * 1024L;
//最少region個數,默認10個
private static int least_region_count = 10;
public static void main(String[] args) {
Configuration conf = HBaseConfiguration.create();
conf.set("hbase.rootdir", "hdfs://hb1:9000/hbase");
conf.set("hbase.master", "hb1:60000");
conf.set("hbase.zookeeper.property.clientPort", "2181");
conf.set("hbase.zookeeper.quorum",
"hb1:2181,hb2:2181,hb3:2181,hd4:2181,hd5:2181,hd6:2181,hd7:2181,hd8:2181,hd9:2181,hd10:2181,hd11:2181");
mergeRegion4Table(conf, "staticlog_2018");
}
/**
* 按表進行region的merge
* @param conf
* @param tableName
*/
public static void mergeRegion4Table(Configuration conf, String tableName) {
FileSystem fs = null;
conf.set("fs.defaultFS", "hdfs://hb1:9000");
conf.set("fs.hdfs.impl", "org.apache.hadoop.hdfs.DistributedFileSystem");
Map<String, Long> tableRegionSizeMap = null;
long currentTimeMillis = 0L;
long currentTimeMillis2 = 0L;
int merge_count = 0;
try {
fs = FileSystem.get(URI.create("hdfs://hb1:9000"), conf);
tableRegionSizeMap = getTableRegionSizeMap(fs, tableName);
} catch (IOException e) {
e.printStackTrace();
}
try {
Connection connection = ConnectionFactory.createConnection(conf);
Admin admin = connection.getAdmin();
List<HRegionInfo> tableRegions = admin.getTableRegions(TableName.valueOf(tableName));
System.out.println("table " + tableName + " has " + tableRegions.size() + " regions!");
// 合併的執行條件,大於最小的region個數
if (tableRegions.size() > least_region_count) {
byte[] regionA = null;
long sizeA = 0L;
byte[] regionB = null;
for (HRegionInfo info : tableRegions) {
String regionName = info.getRegionNameAsString();
regionName = regionName.substring(regionName.lastIndexOf(".") - 32, regionName.lastIndexOf("."));
Long size = tableRegionSizeMap.get(regionName);
if (size != 0L) {
// region大小小於upper_size
if (size < upper_size) {
if (regionA == null) {
//如果A region爲空,則將當前region賦值給A
regionA = info.getRegionName();
sizeA = size;
} else {
//如果A region不爲空,則將當前region賦值給B,則直接調用api對A,B進行合併,成功之後清空A,B繼續遍歷
regionB = info.getRegionName();
currentTimeMillis = System.currentTimeMillis();
admin.mergeRegions(regionA, regionB, true);
currentTimeMillis2 = System.currentTimeMillis();
System.out.println(Bytes.toString(regionA) + " & " + Bytes.toString(regionB)
+ "merge cost " + (currentTimeMillis2 - currentTimeMillis) + " milliseconds!");
regionA = null;
regionB = null;
sizeA = 0L;
merge_count++;
}
} else {
// region大小大於upper_size
if (regionA == null) {
//如果A region爲空,放棄處理該region,直接繼續遍歷
continue;
} else {
//如果A region不爲空,並且A的大小小於lower_size或者A+當前的大小小於region_max_size,則將當前region賦值給B,則直接調用api對A,B進行合併,成功之後清空A,B繼續遍歷
if (sizeA < lower_size || (sizeA + size) < region_max_size) {
regionB = info.getRegionName();
currentTimeMillis = System.currentTimeMillis();
admin.mergeRegions(regionA, regionB, true);
currentTimeMillis2 = System.currentTimeMillis();
System.out.println(
Bytes.toString(regionA) + " & " + Bytes.toString(regionB) + "merge cost "
+ (currentTimeMillis2 - currentTimeMillis) + " milliseconds!");
regionA = null;
regionB = null;
sizeA = 0L;
merge_count++;
} else {
regionA = null;
sizeA = 0L;
}
}
}
} else {
System.out.println(regionName + "is empty,ignore!");
}
}
admin.close();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("merge run " + merge_count + " times,continue!");
// if(merge_count != 0){
// System.out.println("merge run "+ merge_count +" times,continue!");
// //遞歸操作,直到不需要繼續合併
// mergeRegion4Table(conf,tableName);
// }else {
// System.out.println("no need to merge any more,finish!");
// }
}
/**
* 獲得table的region名稱和size對應關係的map
* @param fs
* @param tableName
* @return
*/
public static Map<String, Long> getTableRegionSizeMap(FileSystem fs, String tableName) {
Map<String, Long> tableRegionSizeMap = new HashMap<>();
String hbase_path = "/hbase/data/";
if (tableName.contains(":")) {
String[] split = tableName.split(":");
hbase_path = hbase_path + split[0] + "/" + split[1];
} else {
hbase_path = hbase_path + "default/" + tableName;
}
Path table_path = new Path(hbase_path);
try {
FileStatus[] files = fs.listStatus(table_path);
System.out.println(files.length);
for (FileStatus file : files) {
String name = file.getPath().getName();
long length = fs.getContentSummary(file.getPath()).getLength();
tableRegionSizeMap.put(name, length);
}
} catch (IOException e) {
e.printStackTrace();
}
return tableRegionSizeMap;
}
}
最後:本想通過遞歸操作完成所有的合併過程,但是考慮到RIT以及HBase負載等問題,所以改成手動執行,當一次執行完畢後,無RIT的region後再根據情況考慮是否進行下一輪的merge