本文基于hbase-1.3.0源碼
1. 前言
本文主要介紹在cluster模式下(并且使用zookeeper協(xié)調(diào))region發(fā)生split的整個(gè)過程。這其中會(huì)涉及到HMaster和HRegionSever。
2. split發(fā)生的時(shí)機(jī)
2.1 不能split的場(chǎng)景
- 當(dāng)前region包含reference file(也就是當(dāng)前region從另外一個(gè)region split出來之后,依然通過reference file持有父region hfile的一個(gè)區(qū)間(上或下半部分)) ,compact操作會(huì)導(dǎo)致reference file被刪除,因此若干次minor compact或則一次major compact之后reference file就會(huì)被刪除。
- 當(dāng)前region正處在recovering狀態(tài)。
- meta table以及namespace table的region不可以split。
- 未滿足split policy(見2.2)
- 此外由于split之前需要獲取table的read lock,所以此時(shí)如果在修改table的meta信息的話(持有write lock)也會(huì)導(dǎo)致split阻塞。
- 當(dāng)前regionserver的region數(shù)量已經(jīng)達(dá)到了上限。由
hbase.regionserver.regionSplitLimit
配置決定
1,2,3,4都由HRegion#checkSplit檢查,split需要一個(gè)midKey作為切分點(diǎn),checkSplit返回值就是midKey。5下文會(huì)提到。
2.2 split policy
split policy決定了region 是否達(dá)到split條件,以及splitKey。表元數(shù)據(jù)'SPLIT_POLICY'配置policy策略。未配置的情況下使用hbase.regionserver.region.split.policy
配置的值,默認(rèn)使用IncreasingToUpperBoundRegionSplitPolicy策略。
下面介紹的所有policy類都繼承自RegionSplitPolicy
, 它有兩個(gè)重要的方法:1, shouldSplit返回true|false決定是否可以split; 2, getSplitPoint返回進(jìn)行split的點(diǎn)(一個(gè)row值,將所有row劃成兩段)
-
ConstantSizeRegionSplitPolicy
這種策略下,只要HRegion的任何一個(gè)HStore的size達(dá)到一個(gè)固定大小之后,就會(huì)發(fā)生split。關(guān)于這個(gè)大小的設(shè)置遵循一下規(guī)則(大小為desiredMaxFileSize):
- 將desiredMaxFileSize設(shè)置為table的元數(shù)據(jù)MAX_FILESIZE的大小,沒有該元數(shù)據(jù)就從配置文件中讀取
hbase.hregion.max.filesize
的值(當(dāng)前版本默認(rèn)為10G) - 1中的值不是一個(gè)嚴(yán)格的值,還有一個(gè)抖動(dòng)
hbase.hregion.max.filesize.jitter
- 將desiredMaxFileSize設(shè)置為table的元數(shù)據(jù)MAX_FILESIZE的大小,沒有該元數(shù)據(jù)就從配置文件中讀取
-
IncreasingToUpperBoundRegionSplitPolicy
這種策略下,和1不同的是size的選取。這種策略size和當(dāng)前RegionServer上屬于表t1的region個(gè)數(shù)有關(guān)(假設(shè)個(gè)數(shù)Rn)。
- Rn =0 || Rn > 100,size的選取和1一樣。
- 不符合1的情況下,此時(shí)有一個(gè)‘initialSize’. initialSize 的大小由
hbase.increasing.policy.initial.size
,如果該配置未設(shè)置或者設(shè)置成負(fù)數(shù),則initialSize被設(shè)置為2 * MEMSTORE_FLUSHSIZE(這是table的元數(shù)據(jù)),如果table沒有該元數(shù)據(jù)或則initialSize依然小于0, initialSize被設(shè)置為2 *"hbase.hregion.memstore.flush.size
(該配置默認(rèn)值為128M) - 有了initialSize值后,size被設(shè)置為min(ConstantSizeRegionSplitPolicy規(guī)則下的size, Rn * Rn * Rn * intialSize)
-
KeyPrefixRegionSplitPolicy
這種策略直接繼承至IncreasingToUpperBoundRegionSplitPolicy,所以判斷是否應(yīng)該split的條件和它一樣,但是splitKey的選取不一樣。
這種策略中有一個(gè)‘prefixLength’, 有table元數(shù)據(jù)
KeyPrefixRegionSplitPolicy.prefix_length
決定。它在拿到splitKey之后,會(huì)截取splitKey的前prefixLength位作為新的prefixKey。例如原splitKey為"1234567890",截取5位變成"1234500000", 這樣保證了比"1234500000"小的在一個(gè)region里,比他大的在另一個(gè)region里,也就是說擁有公共前綴的key是不會(huì)被split到不同的region中。 -
DelimitedKeyPrefixRegionSplitPolicy
這種策略繼承IncreasingToUpperBoundRegionSplitPolicy,所以判斷是否應(yīng)該split的條件和它一樣,但是splitKey的選取不一樣。
這種策略下有一個(gè)分割符‘delimiter’, 從table元數(shù)據(jù)
DelimitedKeyPrefixRegionSplitPolicy.delimiter
獲取,它在拿到原始的splitKey之后,找到delimiter最早的出現(xiàn)位置,然后截取該位置之前的前綴作為新的splitKey。?
2.3 region split時(shí)機(jī):
- 內(nèi)存中的數(shù)據(jù)(HMemStore)flush到磁盤時(shí),判斷是否需要split。
- 用戶通過api或者shell觸發(fā)split(RsRpcServices#splitRegion)。
- compaction導(dǎo)致split。
1,2,3都需要滿足split policy.
2.4 與split相關(guān)的配置參數(shù)
下文都假設(shè)配置zookeeper.znode.parent
采用了默認(rèn)值‘hbase’,zookeeper上所有的node都會(huì)以此配置值作為根路徑。
zookeeper.znode.unassigned
默認(rèn)值為“region-in-transition”。
HMaster啟動(dòng)時(shí)會(huì)根據(jù)該配置的值在zookeeper上創(chuàng)建路徑'/hbase/region-in-transition', 發(fā)生split的的HRegion會(huì)在此路徑下創(chuàng)建一個(gè)以region name為名的新節(jié)點(diǎn),HMaster 監(jiān)聽該節(jié)點(diǎn),因此知道正在split的region。-
hbase.regionserver.regionSplitLimit
,默認(rèn)值1000它規(guī)定了一個(gè)region server上的最多容納的region的個(gè)數(shù),超過這個(gè)值就不會(huì)再split。
hbase.regionserver.thread.split
用來split的線程池個(gè)數(shù)。默認(rèn)為1.
3. split過程
3.1 基本概念
-
HRegionInfo
記錄的region信息有:
1. tableName, 即region所屬table 2. startKey, region 其實(shí)row key, 第一個(gè)region為空 3. regionId, region 創(chuàng)建時(shí)的timestamp 4. replicaId, region備份的情況下,主region 為0,其他依次遞增 5. encodedName, 前4項(xiàng)的32位md5值 以上5項(xiàng)構(gòu)成了region name,下面是'.meta.'這張表中ROW 值(meta表row值即region name): t1,,1500458612081.a33bb6a3c49157ab43876540d083e5f1 t1表名,startKey是空的,1500458612081是regionId,沒有replicaId,即沒有備份,后面是md5值。 除上述以外,HRegionInfo還包括: endKey, 開區(qū)間,為空表示region是最后一個(gè)region split,是否region正在split offline,region是否下線,split之后的region應(yīng)該下線
-
region split過程中的狀態(tài)
public enum SplitTransactionPhase { /** * Started */ STARTED, /** * Prepared */ PREPARED, /** * Before preSplit coprocessor hook */ BEFORE_PRE_SPLIT_HOOK, /** * After preSplit coprocessor hook */ AFTER_PRE_SPLIT_HOOK, /** * Set region as in transition, set it into SPLITTING state. */ SET_SPLITTING, /** * We created the temporary split data directory. */ CREATE_SPLIT_DIR, /** * Closed the parent region. */ CLOSED_PARENT_REGION, /** * The parent has been taken out of the server's online regions list. */ OFFLINED_PARENT, /** * Started in on creation of the first daughter region. */ STARTED_REGION_A_CREATION, /** * Started in on the creation of the second daughter region. */ STARTED_REGION_B_CREATION, /** * Opened the first daughter region */ OPENED_REGION_A, /** * Opened the second daughter region */ OPENED_REGION_B, /** * Point of no return. * If we got here, then transaction is not recoverable other than by * crashing out the regionserver. */ PONR, /** * Before postSplit coprocessor hook */ BEFORE_POST_SPLIT_HOOK, /** * After postSplit coprocessor hook */ AFTER_POST_SPLIT_HOOK, /** * Completed */ COMPLETED }
region split在hbase里作為一個(gè)事務(wù)處理,整個(gè)split過程由SplitTransactionImpl(實(shí)現(xiàn)了Runnable接口)包裝,上面枚舉了這一事務(wù)執(zhí)行過程中會(huì)經(jīng)歷的狀態(tài)變化。在PONR狀態(tài)之前發(fā)生fail,都會(huì)回滾。
3.2 HRegionServer端
在hbase學(xué)習(xí) - HRegionServer啟動(dòng)一文2.2節(jié)中提到hregion server會(huì)啟動(dòng)CompactionSplitThread專門負(fù)責(zé)split,compact,merge。CompactionSplitThread有三個(gè)重載的requestSplit方法如下:
1. public synchronized boolean requestSplit(final Region r)
2. public synchronized void requestSplit(final Region r, byte[] midKey)
3. public synchronized void requestSplit(final Region r, byte[] midKey, User user) {
if (midKey == null) {
LOG.debug("Region " + r.getRegionInfo().getRegionNameAsString() +
" not splittable because midkey=null");
if (((HRegion)r).shouldForceSplit()) {
((HRegion)r).clearSplit();
}
return;
}
try {
//splits線程池執(zhí)行SplitRequest
this.splits.execute(new SplitRequest(r, midKey, this.server, user));
if (LOG.isDebugEnabled()) {
LOG.debug("Split requested for " + r + ". " + this);
}
} catch (RejectedExecutionException ree) {
LOG.info("Could not execute split for " + r, ree);
}
}
}
這三個(gè)方法
1調(diào)用2, 2調(diào)用3。 其中方法1在MemStoreFlusher和CompactSplitThread#compaction調(diào)用,他會(huì)使用split policy的checkSplit(返回null或則midKey)檢查是否可以split。 方法3由RsRpcServices(rpc服務(wù))調(diào)用,盡管3里面沒有調(diào)用split policy的checkSplit,但是3的調(diào)用澤RsRpcService#splitRegion在傳midKey參數(shù)時(shí)使用了checkSplit,因此不滿足的policy時(shí)midKey為null,依然不能split。
1. SplitRequest
SplitRequest實(shí)現(xiàn)了Runnable接口,主要邏輯在其方法doSplitting中:
private void doSplitting(User user) {
boolean success = false;
server.metricsRegionServer.incrSplitRequest();
long startTime = EnvironmentEdgeManager.currentTime();
// 真正的split的主要過程都封裝在SplitTransactionImpl中完成,包含了split過程中的狀態(tài)轉(zhuǎn)移.
// 參考3.2 -1 split中會(huì)出現(xiàn)的狀態(tài)。
SplitTransactionImpl st = new SplitTransactionImpl(parent, midKey);
try {
// 獲取table lock,獲取的是read lock,因此不會(huì)影響table的其他region的split,compact等操作
// 但是由于任何試圖修改table schema的操作需要獲取write lock,因此會(huì)被阻塞。
tableLock =
server.getTableLockManager().readLock(parent.getTableDesc().getTableName()
, "SPLIT_REGION:" + parent.getRegionInfo().getRegionNameAsString());
try {
tableLock.acquire();
} catch (IOException ex) {
tableLock = null;
throw ex;
}
if (!st.prepare()) return;
try {
st.execute(this.server, this.server, user);
success = true;
} catch (Exception e) {
if (this.server.isStopping() || this.server.isStopped()) {
LOG.info(
"Skip rollback/cleanup of failed split of "
+ parent.getRegionInfo().getRegionNameAsString() + " because server is"
+ (this.server.isStopping() ? " stopping" : " stopped"), e);
return;
}
if (e instanceof DroppedSnapshotException) {
server.abort("Replay of WAL required. Forcing server shutdown", e);
return;
}
try {
LOG.info("Running rollback/cleanup of failed split of " +
parent.getRegionInfo().getRegionNameAsString() + "; " + e.getMessage(), e);
// split過程中出現(xiàn)錯(cuò)誤,都會(huì)嘗試rollback,具體細(xì)節(jié)將會(huì)在split過程講完后講解。
if (st.rollback(this.server, this.server)) {
LOG.info("Successful rollback of failed split of " +
parent.getRegionInfo().getRegionNameAsString());
} else {
this.server.abort("Abort; we got an error after point-of-no-return");
}
} catch (RuntimeException ee) {
String msg = "Failed rollback of failed split of " +
parent.getRegionInfo().getRegionNameAsString() + " -- aborting server";
// If failed rollback, kill this server to avoid having a hole in table.
LOG.info(msg, ee);
this.server.abort(msg + " -- Cause: " + ee.getMessage());
}
return;
}
} catch (IOException ex) {
LOG.error("Split failed " + this, RemoteExceptionHandler.checkIOException(ex));
server.checkFileSystem();
} finally {
if (this.parent.getCoprocessorHost() != null) {
try {
this.parent.getCoprocessorHost().postCompleteSplit();
} catch (IOException io) {
LOG.error("Split failed " + this,
RemoteExceptionHandler.checkIOException(io));
}
}
// rpc發(fā)起的split會(huì)設(shè)置forceSplit=true,完成split之后需要?dú)w位為false。
if (parent.shouldForceSplit()) {
parent.clearSplit();
}
//釋放 read lock
releaseTableLock();
...
}
核心邏輯在SplitTransactionImpl中完成,調(diào)用器prepare和execute完成。doSplitting中完成table的readlock獲取和釋放,以及出現(xiàn)異常后rollback。
2. SplitTransactionImpl
在SplitTransacntionImple中完成split全部過程,他有一些重要成員:
- parent, 等待split的HRegion
- hri_a, hri_b, parent分裂成這兩個(gè)
- currentPhase, 當(dāng)前split所處狀態(tài),參考3.1 - 2,初始狀態(tài)為STARTED。
- private final List<JournalEntry> journal, 當(dāng)前所有已經(jīng)完成的狀態(tài)列表
- splitRow,以這個(gè)值降parent分裂成兩個(gè)。
上面1中代碼,先是調(diào)用了SplitTransactionImpl#prepare主要完成一項(xiàng)工作:
- 拿到splitKey,parent的startKey和endKey,創(chuàng)建出兩個(gè)HRegionInfo實(shí)例hri_a, hri_b,它們分別擁有的rowKey區(qū)間[startKey, splitRow), [splitRow, endKey)
- 將當(dāng)前狀態(tài)currentPhase又STARTED轉(zhuǎn)換成PREPARED。
接下來時(shí)SplitTransactionImpl#execute方法的調(diào)用,剩下split的所有過程以及涉及到的狀態(tài)變化都是在這里完成,這里面調(diào)用鏈比較復(fù)雜,代碼較多,下面是調(diào)用鏈的核心點(diǎn)以及狀態(tài)變化(大寫的表示狀態(tài)):
execute() --> SplitTransactionImpl# createDaughters
|
v
(PREPARED >> BEFORE_PRE_SPLIT_HOOK)
|
v
RegionCoprocessorHost # preSplit()
|
v
(BEFORE_PRE_SPLIT_HOOK >> AFTER_PRE_SPLIT_HOOK)
|
|-----------> SplitTransactionImpl#stepsBeforePONR()
|
v
ZkSplitTransactionCordination # startSplitTransaction
[注:startSplitTransaction在zk的/hbase/region-in-transition
節(jié)點(diǎn)下創(chuàng)建一個(gè)parent的encodedName的臨時(shí)節(jié)點(diǎn), 節(jié)點(diǎn)的
data包含hri_a, hri_b;
以及狀態(tài)'RS_ZK_REQUEST_REGION_SPLIT'枚舉值(10). ]
|
v
(AFTER_PRE_SPLIT_HOOK >> SET_SPLITTING)
|
v
ZkSplitTransactionCordination # waitForSplitTransaction
[注: 上一步創(chuàng)建的節(jié)點(diǎn)master被master監(jiān)聽到后將data里的狀態(tài)修改為'RS_ZK_REGION_SPLITTING'枚舉值(5),
此方法等待master修改完成后返回]
|
v
this.parent.getRegionFileSystem().createSplitsDir();
[注:在hdfs上table下面 parent region目錄下面創(chuàng)建
一個(gè)'.splits'的目錄,例如這是筆者defualt.t1表一個(gè)region
encodedName為a33...的路徑的目錄:
/hbase/data/default/t1/a33bb6a3c49157ab43876540d083e5f1]
|
v
(SET_SPLITTING >> CREATE_SPLIT_DIR)
|
-> parent.close(false)
|
coprocessorHost#preClose
|
[注:closing設(shè)為true
region不再提供服務(wù),
flush memstore,close all HStore]
|
coprocessorHost#postClose
|<--|
|
(CREATE_SPLIT_DIR >> CLOSED_PARENT_REGION)
|
services.removeFromOnlineRegions(this.parent, null);
[注:service即RegionServerService
此時(shí)parent已經(jīng)是closed狀態(tài),不再提供服務(wù),
移除parent。]
|
( CLOSED_PARENT_REGION >> OFFLINED_PARENT )
|
|--> SplitTransactionImpl # splitStoreFiles
|
[注:Region對(duì)應(yīng)table所有了column family,每一個(gè)colume family下有HFile,每個(gè)HFile都需要split。
下文假設(shè)分裂的HFile屬于列族cf-n,分裂的HFile文件名了file-n, 被分裂的parent region的encoded name為r-n,b_name, a_name表示分裂后上下兩個(gè)region的encoded name。
需要split的HFile包裝成StoreFileSplitter(實(shí)現(xiàn)Callable)提交線程池執(zhí)行。
對(duì)于需要split的HFile,分裂成上下兩部分,執(zhí)行如下:
1. 創(chuàng)建下半部分引用文件(Reference),在'.splits'下創(chuàng)建'a_name/cf-n'路徑 ,在cf-n路徑下創(chuàng)建名為file-n.r-n的文件。
并在文件中寫入bottom, splitKey信息(二進(jìn)制格式),表示它引用split前hfile的下半部分.
2. 創(chuàng)建上半部引用,在'.splits'下創(chuàng)建'b_name/cf-n'路徑 ,在cf-n路徑下創(chuàng)建名為file-n.r-n的文件。
并在文件中寫入top, splitKey信息(二進(jìn)制格式),表示它引用split前hfile的上半部分。
|<--------|
|
v
((OFFLINE_PARENT >> STARTED_REGION_A_CREATION))
[注: 標(biāo)記成開始創(chuàng)建hri_a這個(gè)region,上一步雖然已完成parent的所有的hfile的split,
但是,分裂后的region:a,b各自的hfile都在parent_region_encode_name/.splits/a_encoded_name和b_encoded_name下.
需要移動(dòng)到正確的目錄下,也就是table_name/encoded_region_name/ 這個(gè)目錄。]
|
HRegion# parent.createDaughterRegionFromSplits(this.hri_a)
[注:首先移動(dòng)region a, region a持有parent region的下部分,步驟如下(假設(shè)parent region所屬表的目錄是/hbase/data/default/table-n):
1. 創(chuàng)建/hbase/data/default/table-n/a_encoded_name路徑
2. 在上面路徑下創(chuàng)建.regioninfo文件,寫入a的RegionInfo二進(jìn)制信息.
3. 通過hdfs的rename調(diào)用將.splits/a_encoded_name所有文件移動(dòng)到1中的路徑下。至此,region_a創(chuàng)建完,此時(shí)a不可用,因?yàn)閙aster還沒有把新的region調(diào)度到某個(gè)region server上,metatable也還沒有region a的信息。]
|
((STARTED_REGION_A_CREATION >> STARTED_REGION_B_CREATION))
[注: 省略region b的過程,和a是一樣的]
|------------------------ |
|
v
RegionCoprocessorHost # preSplitBeforePONR
|
v
((STARTED_REGION_B_CREATION >> PONR))
[到這一步,region a,b已經(jīng)的hfile已經(jīng)寫到hdfs中,但是meta table還沒更新,依然是parent的,a,b也還沒有被指派到RegionServer。
PONR之后發(fā)生任何失敗異常,都不會(huì)做rollback,而是直接關(guān)閉當(dāng)前region server。
HMaster檢查到region server的失敗,會(huì)做失敗處理,split會(huì)繼續(xù),meta table會(huì)更新,region a,b會(huì)被正確管理。]
|
v
MetaTableAccessor.splitRegion()
[注:下線parent,為parent構(gòu)建一個(gè)meta table的Put操作如下:
- 將parent的region Info標(biāo)記為offline=true && split=true(meta中rowkey是regionname,參考3.1 - 1),修改的列為info:regioninfo
- 增加兩列info:splitA和info:splitB,value是a,b的regioninfo
在meta table中加入region a, b的信息,為a,b構(gòu)建Put操作,每一個(gè)在meta table中的region至少包含這些信息(列):
1. info:server,region 所在server。
2. info:startcode, region所在region server的startcode,是region server啟動(dòng)的時(shí)間戳。
3. info:seqnumDuringOpen, 新region為1 。
metatable更新成功后,region a,b還沒有在region server上打開,所以還不能被訪問。]
|-------------------|
|
v
parent.getCoprocessorHost().preSplitAfterPONR();
[調(diào)用注冊(cè)在parent region上的coprocessor]
|
|--------------->stepsAfterPONR()
[注:并行執(zhí)行兩個(gè)線程打開a, b兩個(gè)region(調(diào)用HRegion#openRegion),此時(shí)region a,b就可以被訪問了。
通知master完成region的split,即將zk上/hbase/region-in-transition下encoded_parent_name的值改成'RS_ZK_REGION_SPLIT' 枚舉值6.]
|
((PONR >> BEFORE_POST_SPLIT_HOOK))
|
parent.getCoprocessorHost().postSplit
[調(diào)用parent region注冊(cè)的coprocessor]
|
((BEFORE_POST_SPLIT_HOOK >> AFTER_POST_SPLIT_HOOK))
|-----------------------|
|
v
((AFTER_POST_SPLIT_HOOK >> COMPLETED))
[至此,region server端完成]
3.3 HMaster端
這里開始是split過程中HMaster端做的一些事情。回顧3.2 -2 中split過程中哪些需要和HMaster交互:
調(diào)用ZkSplitTransactionCordination # waitForSplitTransaction
等待HMaster將zk的/hbase/region-in-transition/{encoded-regionname}的data中狀態(tài)設(shè)置由‘ RS_ZK_REQUEST_REGION_SPLIT’改變成‘ RS_ZK_REGION_SPLITTING’。調(diào)用stepsAfterPONR()過程中,HRegionServer端將/hbase/region-in-transition/{encoded-regionname}的data中狀態(tài)設(shè)置由'RS_ZK_REGION_SPLITTING'變?yōu)椤甊S_ZK_REGION_SPLIT’,由于HMaster會(huì)監(jiān)控這幾zk節(jié)點(diǎn),所以HMaster也會(huì)做出處理。
先說說1, HMaster通過AssignmentManager監(jiān)控/hbase/region-in-transition節(jié)點(diǎn)的改變(可以參考文章HMaster啟動(dòng)), AssignmentManager通過如下的調(diào)用:
nodeCreated -> handleAssignmentEvent -> handleRegion -> handleRegionSplitting
進(jìn)入到方法‘handleRegionSplitting’處理RS_ZK_REQUEST_REGION_SPLIT
狀態(tài),它所在的處理就是將RS_ZK_REQUEST_REGION_SPLIT
狀態(tài)改變成RS_ZK_REGION_SPLITTING
使得ZkSplitTransactionCordination # waitForSplitTransaction
能夠返回繼續(xù)處理split。
再是2,同樣是在AssignmentManager #handleRegionSplitting中處理,到狀態(tài)‘ RS_ZK_REGION_SPLIT’說明split已經(jīng)完成了,這時(shí)會(huì)處理的是parent以及a,b的replicas。它需要unassign parent的所有replicas,然后建立a,b的replicas,完成replicas region的assign。在方法的最后刪除zk /hbase/region-in-transition/{encoded-parent-region-name}這個(gè)節(jié)點(diǎn).