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。

时间: 2024-10-23 08:55:36

hbase源码系列(八)从Snapshot恢复表的相关文章

hbase源码系列(七)Snapshot的过程

在看这一章之前,建议大家先去看一下snapshot的使用.可能有人会有疑问为什么要做Snapshot,hdfs不是自带了3个备份吗,这是个很大的误区,要知道hdfs的3个备份是用于防止网络传输中的失败或者别的异常情况导致数据块丢失或者不正确,它不能避免人为的删除数据导致的后果.它就想是给数据库做备份,尤其是做删除动作之前,不管是hbase还是hdfs,请经常做Snapshot,否则哪天手贱了... 直接进入主题吧,上代码. public void takeSnapshot(SnapshotDes

hbase源码系列(十)HLog与日志恢复

HLog概述 hbase在写入数据之前会先写入MemStore,成功了再写入HLog,当MemStore的数据丢失的时候,还可以用HLog的数据来进行恢复,下面先看看HLog的图. 旧版的HLog是实际上是一个SequceneFile,0.96的已经使用Protobuf来进行序列化了.从Writer和Reader上来看HLog的都是Entry的,换句话说就是,它的每一条记录就是一个Entry. class Entry implements Writable { private WALEdit e

hbase源码系列(十一)Put、Delete在服务端是如何处理?

在讲完之后HFile和HLog之后,今天我想分享是Put在Region Server经历些了什么?相信前面看了<HTable探秘>的朋友都会有印象,没看过的建议回去先看看,Put是通过MultiServerCallable来提交的多个Put,好,我们就先去这个类吧,在call方法里面,我们找到了这句. responseProto = getStub().multi(controller, requestProto); 它调用了Region Server的multi方法.好,我们立即杀到HReg

hbase源码系列(十三)缓存机制MemStore与Block Cache

这一章讲hbase的缓存机制,这里面涉及的内容也是比较多,呵呵,我理解中的缓存是保存在内存中的特定的便于检索的数据结构就是缓存. 之前在讲put的时候,put是被添加到Store里面,这个Store是个接口,实现是在HStore里面,MemStore其实是它底下的小子. 那它和Region Server.Region是什么关系? Region Server下面有若干个Region,每个Region下面有若干的列族,每个列族对应着一个HStore. HStore里面有三个很重要的类,在这章的内容都

hbase源码系列(十四)Compact和Split

先上一张图讲一下Compaction和Split的关系,这样会比较直观一些. Compaction把多个MemStore flush出来的StoreFile合并成一个文件,而Split则是把过大的文件Split成两个. 之前在Delete的时候,我们知道它其实并没有真正删除数据的,那总不能一直不删吧,下面我们就介绍一下它删除数据的过程,它就是Compaction. 在讲源码之前,先说一下它的分类和作用. Compaction主要起到如下几个作用: 1)合并文件 2)清除删除.过期.多余版本的数据

hbase源码系列(六)HMaster启动过程

这一章是server端开始的第一章,有兴趣的朋友先去看一下hbase的架构图,我专门从网上弄下来的. 按照HMaster的run方法的注释,我们可以了解到它的启动过程会去做以下的动作. * <li>阻塞直到变成ActiveMaster * <li>结束初始化操作 * <li>循环 * <li>停止服务并执行清理操作* </ol> HMaster是没有单点问题是,因为它可以同时启动多个HMaster,然后通过zk的选举算法选出一个HMaster来.

hbase源码系列(二)HTable 如何访问客户端

hbase的源码终于搞一个段落了,在接下来的一个月,着重于把看过的源码提炼一下,对一些有意思的主题进行分享一下.继上一篇讲了负载均衡之后,这一篇我们从client开始讲吧,从client到master再到region server,按照这个顺序来开展,网友也可以对自己感兴趣的部分给我留言或者直接联系我的QQ. 现在我们讲一下HTable吧,为什么讲HTable,因为这是我们最常见的一个类,这是我们对hbase中数据的操作的入口. 1.Put操作 下面是一个很简单往hbase插入一条记录的例子.

hbase源码系列(三)Client如何找到正确的Region Server

客户端在进行put.delete.get等操作的时候,它都需要数据到底存在哪个Region Server上面,这个定位的操作是通过HConnection.locateRegion方法来完成的. loc = hConnection.locateRegion(this.tableName, row.getRow()); 这里我们首先要讲hbase的两张元数据表-ROOT-和.META.表,它们一个保存着region的分部信息,一个保存着region的详细信息.在<hbase实战>这本书里面详细写了

hbase源码系列(五)单词查找树

在上一章中提到了编码压缩,讲了一个简单的DataBlockEncoding.PREFIX算法,它用的是前序编码压缩的算法,它搜索到时候,是全扫描的方式搜索的,如此一来,搜索效率实在是不敢恭维,所以在hbase当中单独拿了一个工程出来实现了Trie的数据结果,既达到了压缩编码的效果,亦达到了方便查询的效果,一举两得,设置的方法是在上一章的末尾提了. 下面讲一下这个Trie树的原理吧. hbase源码系列(五)单词查找树,布布扣,bubuko.com

hbase源码系列(十二)Get、Scan在服务端是如何处理?

继上一篇讲了Put和Delete之后,这一篇我们讲Get和Scan, 因为我发现这两个操作几乎是一样的过程,就像之前的Put和Delete一样,上一篇我本来只打算写Put的,结果发现Delete也可以走这个过程,所以就一起写了. Get 我们打开HRegionServer找到get方法.Get的方法处理分两种,设置了ClosestRowBefore和没有设置的,一般来讲,我们都是知道了明确的rowkey,不太会设置这个参数,它默认是false的. if (get.hasClosestRowBef