hbase原始碼系列(八)從Snapshot恢復表
在看這一章之前,建議大家先去看一下snapshot的使用。這一章是上一章snapshot的續集,上一章了講了怎麼做snapshot的原理,這一章就怎麼從snapshot恢復表。
restoreSnapshot方法位於HMaster當中,這個方法沒幾行程式碼,呼叫了SnapshotManager的restoreSnapshot方法。
// 檢查meta表當中是否存在該表 if (MetaReader.tableExists(master.getCatalogTracker(), tableName)) { //不能對線上的表進行恢復操作 if (master.getAssignmentManager().getZKTable().isEnabledTable( TableName.valueOf(fsSnapshot.getTable()))) { throw new UnsupportedOperationException("Table '" + TableName.valueOf(fsSnapshot.getTable()) + "' must be disabled in order to " + "perform a restore operation" + "."); } //從snapshot恢復表,通過提交RestoreSnapshotHandler restoreSnapshot(fsSnapshot, snapshotTableDesc); } else { //如果meta表當中沒有這個表(可能這個表被刪除了,還是咋地),就克隆出來一張新表 HTableDescriptor htd = RestoreSnapshotHelper.cloneTableSchema(snapshotTableDesc, tableName); //克隆snapshot到一個新的表,通過提交CloneSnapshotHandler cloneSnapshot(fsSnapshot, htd); }
恢復之前先判斷這個表還在不在,有可能表都被刪除掉了,分開兩種情況處理,但是我們也可以看到它只是通過兩個handler去處理了,走的是執行緒池提交handler。我們直接去RestoreSnapshotHandler和CloneSnapshotHandler的handleTableOperation方法。先說RestoreSnapshotHandler吧。
protected void handleTableOperation(List<HRegionInfo> hris) throws IOException { MasterFileSystem fileSystemManager = masterServices.getMasterFileSystem(); CatalogTracker catalogTracker = masterServices.getCatalogTracker(); FileSystem fs = fileSystemManager.getFileSystem(); Path rootDir = fileSystemManager.getRootDir(); TableName tableName = hTableDescriptor.getTableName(); try { // 1. 用snapshot當中的表定義來覆蓋現在的表定義 this.masterServices.getTableDescriptors().add(hTableDescriptor); // 2. 找到snapshot的地址,使用restoreHelper開始恢復 Path snapshotDir = SnapshotDescriptionUtils.getCompletedSnapshotDir(snapshot, rootDir); RestoreSnapshotHelper restoreHelper = new RestoreSnapshotHelper( masterServices.getConfiguration(), fs, snapshot, snapshotDir, hTableDescriptor, rootDir, monitor, status); RestoreSnapshotHelper.RestoreMetaChanges metaChanges = restoreHelper.restoreHdfsRegions(); // 3. 更改的變化的region的RegionStates為offline狀態 forceRegionsOffline(metaChanges); // 4.1 把那些刪除了的region在meta表裡面也刪除掉 List<HRegionInfo> hrisToRemove = new LinkedList<HRegionInfo>(); if (metaChanges.hasRegionsToRemove()) hrisToRemove.addAll(metaChanges.getRegionsToRemove()); if (metaChanges.hasRegionsToRestore()) hrisToRemove.addAll(metaChanges.getRegionsToRestore()); //刪除meta表當中的region MetaEditor.deleteRegions(catalogTracker, hrisToRemove); // 4.2 新增新增的region到META表 hris.clear(); //再把新的加進去 if (metaChanges.hasRegionsToAdd()) hris.addAll(metaChanges.getRegionsToAdd()); //刪掉舊的,再添加回來 if (metaChanges.hasRegionsToRestore()) hris.addAll(metaChanges.getRegionsToRestore()); MetaEditor.addRegionsToMeta(catalogTracker, hris); metaChanges.updateMetaParentRegions(catalogTracker, hris); } catch (IOException e) { String msg = "restore snapshot=" + ClientSnapshotDescriptionUtils.toString(snapshot) + " failed. Try re-running the restore command.";throw new RestoreSnapshotException(msg, e); } }
從程式碼上看上面主要包括4個步驟:
(1)更新表的定義
(2)恢復region
(3)把變化了的region在RS端的RegionStates裡面強制下線,否則會出現region在恢復之前是split狀態的再也無法被分配的情況
(4)修改meta表當中的region記錄,根據新增和刪除的兩種情況來處理
恢復region的過程
我們接下來看RestoreSnapshotHelper的restoreHdfsRegions這個方法吧。
public RestoreMetaChanges restoreHdfsRegions() throws IOException { LOG.debug("starting restore"); //遍歷一下Snapshot目錄下的region,沒有region就退出了 Set<String> snapshotRegionNames = SnapshotReferenceUtil.getSnapshotRegionNames(fs, snapshotDir); RestoreMetaChanges metaChanges = new RestoreMetaChanges(parentsMap); List<HRegionInfo> tableRegions = getTableRegions(); if (tableRegions != null) { //for (HRegionInfo regionInfo: tableRegions) { String regionName = regionInfo.getEncodedName(); //snapshot當中包含的region就要恢復,snapshot當中不包括的region就要刪除 if (snapshotRegionNames.contains(regionName)) { snapshotRegionNames.remove(regionName); metaChanges.addRegionToRestore(regionInfo); } else { metaChanges.addRegionToRemove(regionInfo); } } // 恢復需要的恢復region restoreHdfsRegions(metaChanges.getRegionsToRestore()); // 刪除掉不屬於snapshot的region removeHdfsRegions(metaChanges.getRegionsToRemove()); } // 以前有,現在沒有的region也要做恢復 if (snapshotRegionNames.size() > 0) { List<HRegionInfo> regionsToAdd = new LinkedList<HRegionInfo>(); for (String regionName: snapshotRegionNames) { Path regionDir = new Path(snapshotDir, regionName); regionsToAdd.add(HRegionFileSystem.loadRegionInfoFileContent(fs, regionDir)); } // 要新增的region HRegionInfo[] clonedRegions = cloneHdfsRegions(regionsToAdd); metaChanges.setNewRegions(clonedRegions); } // Restore WALs restoreWALs(); return metaChanges; }
首先要拿snapshot的region和現在的table的region逐個對比,分為三種情況:
(1)以前沒有的region,現在有的region,這個region是要刪掉的
(2)以前有,現在也有的region,這個region要被恢復
(3)以前有,現在沒有了,這個region也要恢復,這個情況和前面的有點兒區別,要建立新的region目錄和定義
接下來我們看restoreHdfsRegions這個方法吧,對region挨個恢復。
private void restoreRegion(HRegionInfo regionInfo) throws IOException {
Path snapshotRegionDir = new Path(snapshotDir, regionInfo.getEncodedName());
//獲得要恢復<family,storeFiles>列表
Map<String, List<String>> snapshotFiles =
SnapshotReferenceUtil.getRegionHFileReferences(fs, snapshotRegionDir);
Path regionDir = new Path(tableDir, regionInfo.getEncodedName());
String tableName = tableDesc.getTableName().getNameAsString();
// 恢復當前在表裡面的列族
for (Path familyDir: FSUtils.getFamilyDirs(fs, regionDir)) {
byte[] family = Bytes.toBytes(familyDir.getName());
Set<String> familyFiles = getTableRegionFamilyFiles(familyDir);
List<String> snapshotFamilyFiles = snapshotFiles.remove(familyDir.getName());
if (snapshotFamilyFiles != null) {
List<String> hfilesToAdd = new LinkedList<String>();
for (String hfileName: snapshotFamilyFiles) {
//snapshot中的檔案,現有的檔案當中已經有的就留著,多了的刪除,缺少的就要新增
if (familyFiles.contains(hfileName)) {
// 已經存在的hfile,從這裡刪除之後,後面就不用處理了
familyFiles.remove(hfileName);
} else {
// 缺少的hfile
hfilesToAdd.add(hfileName);
}
}
// 歸檔那些不在snapshot當中的hfile
for (String hfileName: familyFiles) {
Path hfile = new Path(familyDir, hfileName);
HFileArchiver.archiveStoreFile(conf, fs, regionInfo, tableDir, family, hfile);
}
// 現在缺少的檔案就新增
for (String hfileName: hfilesToAdd) {
restoreStoreFile(familyDir, regionInfo, hfileName);
}
} else {// 在snapshot當中不存在,直接把這個列族的檔案歸檔並刪掉
HFileArchiver.archiveFamily(fs, conf, regionInfo, tableDir, family);
fs.delete(familyDir, true);
}
}
// 新增不在當前表裡的列族,然後恢復
for (Map.Entry<String, List<String>> familyEntry: snapshotFiles.entrySet()) {
Path familyDir = new Path(regionDir, familyEntry.getKey());
if (!fs.mkdirs(familyDir)) {
throw new IOException("Unable to create familyDir=" + familyDir);
}
for (String hfileName: familyEntry.getValue()) {
restoreStoreFile(familyDir, regionInfo, hfileName);
}
}
}
恢復這塊的邏輯也差不多,首先先把hfile和列族掛鉤,弄成一個<family, List<hfiles>>的map,一個一個列族去恢復,列族這塊也存在上面region的3種情況,這裡就不說了。
下面有3點是我們要注意的:
(1)相信看了上一章的朋友都有印象,它給hfile建立引用的時候,並未實際儲存檔案,而是建立了一個同名的空檔案。在上面的情況當中,已經存在的同名的hfile,就不需要管了,為什麼不要管了?因為hfile一旦寫入到檔案,writer關閉之後就不會修改了,即使是做compaction的時候,是把多個hfile合成一個新的hfile,把舊的檔案刪除來一個新的檔案。
(2)對於那些後來新增的,在snapshot當前沒有的檔案,它們不是被直接刪除,而是被移到了另外一個地方,歸檔的位置是archive目錄,歸檔的操作是用HFileArchiver類來歸檔。碰到極端的情況,該檔案已經存在了,就在檔案後面加上".當前時間戳"。
(3)對於缺少的檔案走的restoreStoreFile方法,下面是它的程式碼。
private void restoreStoreFile(final Path familyDir, final HRegionInfo regionInfo,
final String hfileName) throws IOException {
if (HFileLink.isHFileLink(hfileName)) {
//是HFileLink的情況
HFileLink.createFromHFileLink(conf, fs, familyDir, hfileName);
} else if (StoreFileInfo.isReference(hfileName)) {
//是Reference的情況
restoreReferenceFile(familyDir, regionInfo, hfileName);
} else {
//是hfile的情況
HFileLink.create(conf, fs, familyDir, regionInfo, hfileName);
}
}
在hbase裡面檔案分3種,HFileLink、ReferenceFile、Hfile3種,所以恢復的時候需要按照這種方式。
HFileLink是一個連結檔案,名字形式是table=region-hfile,在讀取hfile的時候,如果是HFileLink它會做自動處理,去讀取真正的hfile。
ReferenceFile不同於上一章的引用檔案,那個檔案只是只是用來記錄名字的,它是split產生的檔案,分Top和Bottom兩種,也是一種連結檔案,讀取的時候會建立一個以分割點為中點的Reader,只讀一半的檔案。
這個怎麼讀取連結,之後再介紹,到時候在放連結過來,下面我們回到restoreStoreFile方法上來。
比如一個叫abc的hfile檔案,根據這三種情況來恢復,"->"左邊是原來的檔名,右邊是新的檔名。
(a)Hfile3: abc -> table=region-abc
(b)ReferenceFile: abc.1234 -> table=region-abc.1234
(c)HFileLink: table=region-abc -> table=region-abc
可以看得出來,它並沒有把一個真正的hfile檔案恢復回去,都是在建立類似桌面快捷方式,這樣可以節省空間。
恢復hfile這塊就結束了,然後到restoreWALs方法看看,它是怎麼恢復日誌的。
private void restoreWALs() throws IOException {
final SnapshotLogSplitter logSplitter = new SnapshotLogSplitter(conf, fs, tableDir,
snapshotTable, regionsMap);
try {
// Recover.Edits 遍歷snapshot目錄下的edits日誌
SnapshotReferenceUtil.visitRecoveredEdits(fs, snapshotDir,
new FSVisitor.RecoveredEditsVisitor() {
public void recoveredEdits (final String region, final String logfile) throws IOException {
Path path = SnapshotReferenceUtil.getRecoveredEdits(snapshotDir, region, logfile);
logSplitter.splitRecoveredEdit(path);
}
});
// 前面那個是基於region的日誌,這個是基於Region Server的日誌WALs日誌
SnapshotReferenceUtil.visitLogFiles(fs, snapshotDir, new FSVisitor.LogFileVisitor() {
public void logFile (final String server, final String logfile) throws IOException {
logSplitter.splitLog(server, logfile);
}
});
} finally {
logSplitter.close();
}
}
logSplitter.splitRecoveredEdit和logSplitter.splitLog的最後都呼叫了一個叫做splitLog的方法(editPath)的方法,區別的地方在於splitLog傳了一個HLogLink(HLog的快捷方式。。。)
下面看看splitLog這個方法吧
public void splitLog(final Path logPath) throws IOException {
HLog.Reader log = HLogFactory.createReader(fs, logPath, conf);
try {
HLog.Entry entry;
LogWriter writer = null;
byte[] regionName = null;
byte[] newRegionName = null;
while ((entry = log.next()) != null) {
HLogKey key = entry.getKey();
// 只處理要snapshot的表的
if (!key.getTablename().equals(snapshotTableName)) continue;
// 為每一個新的region例項化一個Writer,但奇怪的是舊的沒有close,就直接切換引用了
if (!Bytes.equals(regionName, key.getEncodedRegionName())) {
regionName = key.getEncodedRegionName().clone();
// Get the new region name in case of clone, or use the original one
newRegionName = regionsMap.get(regionName);
if (newRegionName == null) newRegionName = regionName;
writer = getOrCreateWriter(newRegionName, key.getLogSeqNum());
}
//一個一個追加,沒啥好說的
key = new HLogKey(newRegionName, tableName,
key.getLogSeqNum(), key.getWriteTime(), key.getClusterIds());
writer.append(new HLog.Entry(key, entry.getEdit()));
}
} catch (IOException e) {
LOG.warn("Something wrong during the log split", e);
} finally {
log.close();
}
}
上面這段程式碼也沒幹啥,建立一個HLog.Reader讀取日誌檔案,然後迭代一下,把屬於我們要做snapshot的表的日誌讀取出來,它為每一個region的例項化一個Writer,呼叫的Writer的Append方法追加HLog。
Writer寫入的目錄在recovered.edits,還是這個目錄,之前是hmaster啟動的時候,對那些掛了的region,也是把日誌split到這個目錄,可能在Region Server恢復的時候直接去找這個目錄吧,後面講到Region Server的時候關注一下。額,到這裡為止,恢復的過程就到此結束了。
後面還有兩步,強制更新變化的region的Region States為offline和修改meta表中的region都比較簡單,這裡就不講了。
對於被刪除了的表,處理起來就簡單一些了,直接從走了restoreHdfsRegions的方法,這裡的可能有點兒疑惑,為啥沒建表,原來在它繼承的CreateTableHandler的prepare方法裡面把這活給幹了。
總結一下:從上面的過程我們可以看出來,snapshot還是很輕量級的,除了歸檔刪除的情況外,它的備份和恢復大多數都是建立的連結檔案,而不是直接大規模複製、替換HFile的方式,可能也就是因為這樣才叫snapshot。