分享

hbase源码系列(八)从Snapshot恢复表

本帖最后由 坎蒂丝_Swan 于 2014-12-29 13:00 编辑

问题导读
1.恢复表之前先要对这个表做如何判断?
2.snapshot的region和现在的table的region逐个对比,有什么不同的情况?






在看这一章之前,建议大家先去看一下snapshot的使用。这一章是上一章snapshot的续集,上一章了讲了怎么做snapshot的原理,这一章就怎么从snapshot恢复表。

restoreSnapshot方法位于HMaster当中,这个方法没几行代码,调用了SnapshotManager的restoreSnapshot方法。

  1. // 检查meta表当中是否存在该表
  2.     if (MetaReader.tableExists(master.getCatalogTracker(), tableName)) {
  3.       //不能对在线的表进行恢复操作
  4.       if (master.getAssignmentManager().getZKTable().isEnabledTable(
  5.           TableName.valueOf(fsSnapshot.getTable()))) {
  6.         throw new UnsupportedOperationException("Table '" +
  7.             TableName.valueOf(fsSnapshot.getTable()) + "' must be disabled in order to " +
  8.             "perform a restore operation" +
  9.             ".");
  10.       }
  11.       //从snapshot恢复表,通过提交RestoreSnapshotHandler
  12.       restoreSnapshot(fsSnapshot, snapshotTableDesc);
  13.       
  14.     } else {
  15.       //如果meta表当中没有这个表(可能这个表被删除了,还是咋地),就克隆出来一张新表
  16.       HTableDescriptor htd = RestoreSnapshotHelper.cloneTableSchema(snapshotTableDesc, tableName);
  17.       //克隆snapshot到一个新的表,通过提交CloneSnapshotHandler
  18.       cloneSnapshot(fsSnapshot, htd);
  19.       
  20.     }
复制代码

恢复之前先判断这个表还在不在,有可能表都被删除掉了,分开两种情况处理,但是我们也可以看到它只是通过两个handler去处理了,走的是线程池提交handler。我们直接去RestoreSnapshotHandler和CloneSnapshotHandler的handleTableOperation方法。先说RestoreSnapshotHandler吧。
  1. protected void handleTableOperation(List<HRegionInfo> hris) throws IOException {
  2.     MasterFileSystem fileSystemManager = masterServices.getMasterFileSystem();
  3.     CatalogTracker catalogTracker = masterServices.getCatalogTracker();
  4.     FileSystem fs = fileSystemManager.getFileSystem();
  5.     Path rootDir = fileSystemManager.getRootDir();
  6.     TableName tableName = hTableDescriptor.getTableName();
  7.     try {
  8.       // 1. 用snapshot当中的表定义来覆盖现在的表定义
  9.       this.masterServices.getTableDescriptors().add(hTableDescriptor);
  10.       // 2. 找到snapshot的地址,使用restoreHelper开始恢复
  11.       Path snapshotDir = SnapshotDescriptionUtils.getCompletedSnapshotDir(snapshot, rootDir);
  12.       RestoreSnapshotHelper restoreHelper = new RestoreSnapshotHelper(
  13.           masterServices.getConfiguration(), fs,
  14.           snapshot, snapshotDir, hTableDescriptor, rootDir, monitor, status);
  15.       RestoreSnapshotHelper.RestoreMetaChanges metaChanges = restoreHelper.restoreHdfsRegions();
  16.       // 3. 更改的变化的region的RegionStates为offline状态
  17.       forceRegionsOffline(metaChanges);
  18.       // 4.1 把那些删除了的region在meta表里面也删除掉
  19.       List<HRegionInfo> hrisToRemove = new LinkedList<HRegionInfo>();
  20.       if (metaChanges.hasRegionsToRemove()) hrisToRemove.addAll(metaChanges.getRegionsToRemove());
  21.       if (metaChanges.hasRegionsToRestore()) hrisToRemove.addAll(metaChanges.getRegionsToRestore());
  22.       //删除meta表当中的region
  23.       MetaEditor.deleteRegions(catalogTracker, hrisToRemove);
  24.       // 4.2 添加新增的region到META表
  25.       hris.clear();
  26.       //再把新的加进去
  27.       if (metaChanges.hasRegionsToAdd()) hris.addAll(metaChanges.getRegionsToAdd());
  28.       //删掉旧的,再添加回来
  29.       if (metaChanges.hasRegionsToRestore()) hris.addAll(metaChanges.getRegionsToRestore());
  30.       MetaEditor.addRegionsToMeta(catalogTracker, hris);
  31.       metaChanges.updateMetaParentRegions(catalogTracker, hris);
  32.     } catch (IOException e) {
  33.       String msg = "restore snapshot=" + ClientSnapshotDescriptionUtils.toString(snapshot)
  34.           + " failed. Try re-running the restore command.";throw new RestoreSnapshotException(msg, e);
  35.     }
  36. }
复制代码


从代码上看上面主要包括4个步骤:
(1)更新表的定义
(2)恢复region
(3)把变化了的region在RS端的RegionStates里面强制下线,否则会出现region在恢复之前是split状态的再也无法被分配的情况
(4)修改meta表当中的region记录,根据新增和删除的两种情况来处理

恢复region的过程
我们接下来看RestoreSnapshotHelper的restoreHdfsRegions这个方法吧。

  1. public RestoreMetaChanges restoreHdfsRegions() throws IOException {
  2.     LOG.debug("starting restore");
  3.     //遍历一下Snapshot目录下的region,没有region就退出了
  4.     Set<String> snapshotRegionNames = SnapshotReferenceUtil.getSnapshotRegionNames(fs, snapshotDir);
  5.     RestoreMetaChanges metaChanges = new RestoreMetaChanges(parentsMap);
  6.     List<HRegionInfo> tableRegions = getTableRegions();
  7.     if (tableRegions != null) {
  8.       //for (HRegionInfo regionInfo: tableRegions) {
  9.         String regionName = regionInfo.getEncodedName();
  10.         //snapshot当中包含的region就要恢复,snapshot当中不包括的region就要删除
  11.         if (snapshotRegionNames.contains(regionName)) {
  12.           snapshotRegionNames.remove(regionName);
  13.           metaChanges.addRegionToRestore(regionInfo);
  14.         } else {
  15.           metaChanges.addRegionToRemove(regionInfo);
  16.         }
  17.       }
  18.       // 恢复需要的恢复region
  19.       restoreHdfsRegions(metaChanges.getRegionsToRestore());
  20.     // 删除掉不属于snapshot的region
  21.       removeHdfsRegions(metaChanges.getRegionsToRemove());
  22.     }
  23.     // 以前有,现在没有的region也要做恢复
  24.     if (snapshotRegionNames.size() > 0) {
  25.       List<HRegionInfo> regionsToAdd = new LinkedList<HRegionInfo>();
  26.       for (String regionName: snapshotRegionNames) {
  27.         Path regionDir = new Path(snapshotDir, regionName);
  28.         regionsToAdd.add(HRegionFileSystem.loadRegionInfoFileContent(fs, regionDir));
  29.       }
  30.       // 要新增的region
  31.       HRegionInfo[] clonedRegions = cloneHdfsRegions(regionsToAdd);
  32.       metaChanges.setNewRegions(clonedRegions);
  33.     }
  34.     // Restore WALs
  35.     restoreWALs();  
  36.    return metaChanges;
  37.   }
复制代码


首先要拿snapshot的region和现在的table的region逐个对比,分为三种情况:
(1)以前没有的region,现在有的region,这个region是要删掉的
(2)以前有,现在也有的region,这个region要被恢复
(3)以前有,现在没有了,这个region也要恢复,这个情况和前面的有点儿区别,要创建新的region目录和定义

接下来我们看restoreHdfsRegions这个方法吧,对region挨个恢复。

  1. private void restoreRegion(HRegionInfo regionInfo) throws IOException {
  2.     Path snapshotRegionDir = new Path(snapshotDir, regionInfo.getEncodedName());
  3.     //获得要恢复<family,storeFiles>列表
  4.     Map<String, List<String>> snapshotFiles =
  5.                 SnapshotReferenceUtil.getRegionHFileReferences(fs, snapshotRegionDir);
  6.     Path regionDir = new Path(tableDir, regionInfo.getEncodedName());
  7.     String tableName = tableDesc.getTableName().getNameAsString();
  8.     // 恢复当前在表里面的列族
  9.     for (Path familyDir: FSUtils.getFamilyDirs(fs, regionDir)) {
  10.       byte[] family = Bytes.toBytes(familyDir.getName());
  11.       Set<String> familyFiles = getTableRegionFamilyFiles(familyDir);
  12.       List<String> snapshotFamilyFiles = snapshotFiles.remove(familyDir.getName());
  13.       if (snapshotFamilyFiles != null) {
  14.         List<String> hfilesToAdd = new LinkedList<String>();
  15.         for (String hfileName: snapshotFamilyFiles) {
  16.           //snapshot中的文件,现有的文件当中已经有的就留着,多了的删除,缺少的就要添加
  17.           if (familyFiles.contains(hfileName)) {
  18.             // 已经存在的hfile,从这里删除之后,后面就不用处理了
  19.             familyFiles.remove(hfileName);
  20.           } else {
  21.             // 缺少的hfile
  22.             hfilesToAdd.add(hfileName);
  23.           }
  24.         }
  25.         // 归档那些不在snapshot当中的hfile
  26.         for (String hfileName: familyFiles) {
  27.           Path hfile = new Path(familyDir, hfileName);
  28.           HFileArchiver.archiveStoreFile(conf, fs, regionInfo, tableDir, family, hfile);
  29.         }
  30.         // 现在缺少的文件就添加
  31.         for (String hfileName: hfilesToAdd) {
  32.           restoreStoreFile(familyDir, regionInfo, hfileName);
  33.         }
  34.       } else {// 在snapshot当中不存在,直接把这个列族的文件归档并删掉
  35.         HFileArchiver.archiveFamily(fs, conf, regionInfo, tableDir, family);
  36.         fs.delete(familyDir, true);
  37.       }
  38.     }
  39.     // 添加不在当前表里的列族,然后恢复
  40.     for (Map.Entry<String, List<String>> familyEntry: snapshotFiles.entrySet()) {
  41.       Path familyDir = new Path(regionDir, familyEntry.getKey());
  42.       if (!fs.mkdirs(familyDir)) {
  43.         throw new IOException("Unable to create familyDir=" + familyDir);
  44.       }
  45.       for (String hfileName: familyEntry.getValue()) {
  46.         restoreStoreFile(familyDir, regionInfo, hfileName);
  47.       }
  48.     }
  49.   }
复制代码


恢复这块的逻辑也差不多,首先先把hfile和列族挂钩,弄成一个<family, List<hfiles>>的map,一个一个列族去恢复,列族这块也存在上面region的3种情况,这里就不说了。

下面有3点是我们要注意的:
(1)相信看了上一章的朋友都有印象,它给hfile创建引用的时候,并未实际保存文件,而是创建了一个同名的空文件。在上面的情况当中,已经存在的同名的hfile,就不需要管了,为什么不要管了?因为hfile一旦写入到文件,writer关闭之后就不会修改了,即使是做compaction的时候,是把多个hfile合成一个新的hfile,把旧的文件删除来一个新的文件。
(2)对于那些后来新增的,在snapshot当前没有的文件,它们不是被直接删除,而是被移到了另外一个地方,归档的位置是archive目录,归档的操作是用HFileArchiver类来归档。碰到极端的情况,该文件已经存在了,就在文件后面加上".当前时间戳"。
(3)对于缺少的文件走的restoreStoreFile方法,下面是它的代码。

  1. private void restoreStoreFile(final Path familyDir, final HRegionInfo regionInfo,
  2.       final String hfileName) throws IOException {
  3.     if (HFileLink.isHFileLink(hfileName)) {
  4.       //是HFileLink的情况
  5.       HFileLink.createFromHFileLink(conf, fs, familyDir, hfileName);
  6.     } else if (StoreFileInfo.isReference(hfileName)) {
  7.       //是Reference的情况
  8.       restoreReferenceFile(familyDir, regionInfo, hfileName);
  9.     } else {
  10.       //是hfile的情况
  11.       HFileLink.create(conf, fs, familyDir, regionInfo, hfileName);
  12.     }
  13. }
复制代码


在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方法看看,它是怎么恢复日志的。

  1. private void restoreWALs() throws IOException {
  2.     final SnapshotLogSplitter logSplitter = new SnapshotLogSplitter(conf, fs, tableDir,
  3.         snapshotTable, regionsMap);
  4.     try {
  5.       // Recover.Edits 遍历snapshot目录下的edits日志
  6.       SnapshotReferenceUtil.visitRecoveredEdits(fs, snapshotDir,
  7.           new FSVisitor.RecoveredEditsVisitor() {
  8.         public void recoveredEdits (final String region, final String logfile) throws IOException {
  9.           Path path = SnapshotReferenceUtil.getRecoveredEdits(snapshotDir, region, logfile);
  10.           logSplitter.splitRecoveredEdit(path);
  11.         }
  12.       });
  13.       // 前面那个是基于region的日志,这个是基于Region Server的日志WALs日志
  14.       SnapshotReferenceUtil.visitLogFiles(fs, snapshotDir, new FSVisitor.LogFileVisitor() {
  15.         public void logFile (final String server, final String logfile) throws IOException {
  16.           logSplitter.splitLog(server, logfile);
  17.         }
  18.       });
  19.     } finally {
  20.       logSplitter.close();
  21.     }
  22.   }
复制代码


logSplitter.splitRecoveredEdit和logSplitter.splitLog的最后都调用了一个叫做splitLog的方法(editPath)的方法,区别的地方在于splitLog传了一个HLogLink(HLog的快捷方式。。。)
下面看看splitLog这个方法吧

  1. public void splitLog(final Path logPath) throws IOException {
  2.     HLog.Reader log = HLogFactory.createReader(fs, logPath, conf);
  3.     try {
  4.       HLog.Entry entry;
  5.       LogWriter writer = null;
  6.       byte[] regionName = null;
  7.       byte[] newRegionName = null;
  8.       while ((entry = log.next()) != null) {
  9.         HLogKey key = entry.getKey();
  10.         // 只处理要snapshot的表的
  11.         if (!key.getTablename().equals(snapshotTableName)) continue;
  12.         // 为每一个新的region实例化一个Writer,但奇怪的是旧的没有close,就直接切换引用了
  13.         if (!Bytes.equals(regionName, key.getEncodedRegionName())) {
  14.           regionName = key.getEncodedRegionName().clone();
  15.           // Get the new region name in case of clone, or use the original one
  16.           newRegionName = regionsMap.get(regionName);
  17.           if (newRegionName == null) newRegionName = regionName;
  18.           writer = getOrCreateWriter(newRegionName, key.getLogSeqNum());
  19.         }
  20.         //一个一个追加,没啥好说的
  21.         key = new HLogKey(newRegionName, tableName,
  22.                           key.getLogSeqNum(), key.getWriteTime(), key.getClusterIds());
  23.         writer.append(new HLog.Entry(key, entry.getEdit()));
  24.       }
  25.     } catch (IOException e) {
  26.       LOG.warn("Something wrong during the log split", e);
  27.     } finally {
  28.       log.close();
  29.     }
  30.   }
复制代码


上面这段代码也没干啥,创建一个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。



hbase源码系列(二)HTable 探秘hbase源码系列(三)Client如何找到正确的Region Server
hbase源码系列(四)数据模型-表定义和列族定义的具体含义
hbase源码系列(五)Trie单词查找树
hbase源码系列(六)HMaster启动过程
hbase源码系列(七)Snapshot的过程
hbase源码系列(八)从Snapshot恢复表
hbase源码系列(九)StoreFile存储格式
hbase源码系列(十)HLog与日志恢复
hbase源码系列(十一)Put、Delete在服务端是如何处理?
hbase源码系列(十二)Get、Scan在服务端是如何处理?
hbase源码系列(十三)缓存机制MemStore与Block Cache
hbase源码之如何查询出来下一个KeyValue
hbase源码Compact和Split



欢迎加入about云群90371779322273151432264021 ,云计算爱好者群,亦可关注about云腾讯认证空间||关注本站微信

没找到任何评论,期待你打破沉寂

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

推荐上一条 /2 下一条