1. 程式人生 > 其它 >hbase原始碼系列(八)從Snapshot恢復表

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。