/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.hadoop.hbase.backup.impl; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.hbase.ServerName; import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.backup.BackupInfo; import org.apache.hadoop.hbase.backup.BackupType; import org.apache.hadoop.hbase.backup.util.BackupUtils; import org.apache.hadoop.hbase.classification.InterfaceAudience; import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil; import org.apache.hadoop.hbase.shaded.protobuf.generated.BackupProtos; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos; /** * Backup manifest contains all the meta data of a backup image. The manifest info will be bundled * as manifest file together with data. So that each backup image will contain all the info needed * for restore. BackupManifest is a storage container for BackupImage. * It is responsible for storing/reading backup image data and has some additional utility methods. * */ @InterfaceAudience.Private public class BackupManifest { private static final Log LOG = LogFactory.getLog(BackupManifest.class); // manifest file name public static final String MANIFEST_FILE_NAME = ".backup.manifest"; /** * Backup image, the dependency graph is made up by series of backup images * BackupImage contains all the relevant information to restore the backup and * is used during restore operation */ public static class BackupImage implements Comparable<BackupImage> { static class Builder { BackupImage image; Builder() { image = new BackupImage(); } Builder withBackupId(String backupId) { image.setBackupId(backupId); return this; } Builder withType(BackupType type) { image.setType(type); return this; } Builder withRootDir(String rootDir) { image.setRootDir(rootDir); return this; } Builder withTableList(List<TableName> tableList) { image.setTableList(tableList); return this; } Builder withStartTime(long startTime) { image.setStartTs(startTime); return this; } Builder withCompleteTime(long completeTime) { image.setCompleteTs(completeTime); return this; } BackupImage build() { return image; } } private String backupId; private BackupType type; private String rootDir; private List<TableName> tableList; private long startTs; private long completeTs; private ArrayList<BackupImage> ancestors; private HashMap<TableName, HashMap<String, Long>> incrTimeRanges; static Builder newBuilder() { return new Builder(); } public BackupImage() { super(); } private BackupImage(String backupId, BackupType type, String rootDir, List<TableName> tableList, long startTs, long completeTs) { this.backupId = backupId; this.type = type; this.rootDir = rootDir; this.tableList = tableList; this.startTs = startTs; this.completeTs = completeTs; } static BackupImage fromProto(BackupProtos.BackupImage im) { String backupId = im.getBackupId(); String rootDir = im.getBackupRootDir(); long startTs = im.getStartTs(); long completeTs = im.getCompleteTs(); List<HBaseProtos.TableName> tableListList = im.getTableListList(); List<TableName> tableList = new ArrayList<TableName>(); for (HBaseProtos.TableName tn : tableListList) { tableList.add(ProtobufUtil.toTableName(tn)); } List<BackupProtos.BackupImage> ancestorList = im.getAncestorsList(); BackupType type = im.getBackupType() == BackupProtos.BackupType.FULL ? BackupType.FULL : BackupType.INCREMENTAL; BackupImage image = new BackupImage(backupId, type, rootDir, tableList, startTs, completeTs); for (BackupProtos.BackupImage img : ancestorList) { image.addAncestor(fromProto(img)); } image.setIncrTimeRanges(loadIncrementalTimestampMap(im)); return image; } BackupProtos.BackupImage toProto() { BackupProtos.BackupImage.Builder builder = BackupProtos.BackupImage.newBuilder(); builder.setBackupId(backupId); builder.setCompleteTs(completeTs); builder.setStartTs(startTs); builder.setBackupRootDir(rootDir); if (type == BackupType.FULL) { builder.setBackupType(BackupProtos.BackupType.FULL); } else { builder.setBackupType(BackupProtos.BackupType.INCREMENTAL); } for (TableName name : tableList) { builder.addTableList(ProtobufUtil.toProtoTableName(name)); } if (ancestors != null) { for (BackupImage im : ancestors) { builder.addAncestors(im.toProto()); } } setIncrementalTimestampMap(builder); return builder.build(); } private static HashMap<TableName, HashMap<String, Long>> loadIncrementalTimestampMap( BackupProtos.BackupImage proto) { List<BackupProtos.TableServerTimestamp> list = proto.getTstMapList(); HashMap<TableName, HashMap<String, Long>> incrTimeRanges = new HashMap<TableName, HashMap<String, Long>>(); if (list == null || list.size() == 0) return incrTimeRanges; for (BackupProtos.TableServerTimestamp tst : list) { TableName tn = ProtobufUtil.toTableName(tst.getTableName()); HashMap<String, Long> map = incrTimeRanges.get(tn); if (map == null) { map = new HashMap<String, Long>(); incrTimeRanges.put(tn, map); } List<BackupProtos.ServerTimestamp> listSt = tst.getServerTimestampList(); for (BackupProtos.ServerTimestamp stm : listSt) { ServerName sn = ProtobufUtil.toServerName(stm.getServerName()); map.put(sn.getHostname() + ":" + sn.getPort(), stm.getTimestamp()); } } return incrTimeRanges; } private void setIncrementalTimestampMap(BackupProtos.BackupImage.Builder builder) { if (this.incrTimeRanges == null) { return; } for (Entry<TableName, HashMap<String, Long>> entry : this.incrTimeRanges.entrySet()) { TableName key = entry.getKey(); HashMap<String, Long> value = entry.getValue(); BackupProtos.TableServerTimestamp.Builder tstBuilder = BackupProtos.TableServerTimestamp.newBuilder(); tstBuilder.setTableName(ProtobufUtil.toProtoTableName(key)); for (Map.Entry<String, Long> entry2 : value.entrySet()) { String s = entry2.getKey(); BackupProtos.ServerTimestamp.Builder stBuilder = BackupProtos.ServerTimestamp.newBuilder(); HBaseProtos.ServerName.Builder snBuilder = HBaseProtos.ServerName.newBuilder(); ServerName sn = ServerName.parseServerName(s); snBuilder.setHostName(sn.getHostname()); snBuilder.setPort(sn.getPort()); stBuilder.setServerName(snBuilder.build()); stBuilder.setTimestamp(entry2.getValue()); tstBuilder.addServerTimestamp(stBuilder.build()); } builder.addTstMap(tstBuilder.build()); } } public String getBackupId() { return backupId; } private void setBackupId(String backupId) { this.backupId = backupId; } public BackupType getType() { return type; } private void setType(BackupType type) { this.type = type; } public String getRootDir() { return rootDir; } private void setRootDir(String rootDir) { this.rootDir = rootDir; } public List<TableName> getTableNames() { return tableList; } private void setTableList(List<TableName> tableList) { this.tableList = tableList; } public long getStartTs() { return startTs; } private void setStartTs(long startTs) { this.startTs = startTs; } public long getCompleteTs() { return completeTs; } private void setCompleteTs(long completeTs) { this.completeTs = completeTs; } public ArrayList<BackupImage> getAncestors() { if (this.ancestors == null) { this.ancestors = new ArrayList<BackupImage>(); } return this.ancestors; } private void addAncestor(BackupImage backupImage) { this.getAncestors().add(backupImage); } public boolean hasAncestor(String token) { for (BackupImage image : this.getAncestors()) { if (image.getBackupId().equals(token)) { return true; } } return false; } public boolean hasTable(TableName table) { return tableList.contains(table); } @Override public int compareTo(BackupImage other) { String thisBackupId = this.getBackupId(); String otherBackupId = other.getBackupId(); int index1 = thisBackupId.lastIndexOf("_"); int index2 = otherBackupId.lastIndexOf("_"); String name1 = thisBackupId.substring(0, index1); String name2 = otherBackupId.substring(0, index2); if (name1.equals(name2)) { Long thisTS = Long.valueOf(thisBackupId.substring(index1 + 1)); Long otherTS = Long.valueOf(otherBackupId.substring(index2 + 1)); return thisTS.compareTo(otherTS); } else { return name1.compareTo(name2); } } @Override public boolean equals(Object obj) { if (obj instanceof BackupImage) { return this.compareTo((BackupImage) obj) == 0; } return false; } @Override public int hashCode() { int hash = 33 * this.getBackupId().hashCode() + type.hashCode(); hash = 33 * hash + rootDir.hashCode(); hash = 33 * hash + Long.valueOf(startTs).hashCode(); hash = 33 * hash + Long.valueOf(completeTs).hashCode(); for (TableName table : tableList) { hash = 33 * hash + table.hashCode(); } return hash; } public HashMap<TableName, HashMap<String, Long>> getIncrTimeRanges() { return incrTimeRanges; } private void setIncrTimeRanges(HashMap<TableName, HashMap<String, Long>> incrTimeRanges) { this.incrTimeRanges = incrTimeRanges; } } // backup image directory private String tableBackupDir = null; private BackupImage backupImage; /** * Construct manifest for a ongoing backup. * @param backup The ongoing backup info */ public BackupManifest(BackupInfo backup) { BackupImage.Builder builder = BackupImage.newBuilder(); this.backupImage = builder.withBackupId(backup.getBackupId()).withType(backup.getType()) .withRootDir(backup.getBackupRootDir()).withTableList(backup.getTableNames()) .withStartTime(backup.getStartTs()).withCompleteTime(backup.getCompleteTs()).build(); } /** * Construct a table level manifest for a backup of the named table. * @param backup The ongoing backup session info */ public BackupManifest(BackupInfo backup, TableName table) { this.tableBackupDir = backup.getTableBackupDir(table); List<TableName> tables = new ArrayList<TableName>(); tables.add(table); BackupImage.Builder builder = BackupImage.newBuilder(); this.backupImage = builder.withBackupId(backup.getBackupId()).withType(backup.getType()) .withRootDir(backup.getBackupRootDir()).withTableList(tables) .withStartTime(backup.getStartTs()).withCompleteTime(backup.getCompleteTs()).build(); } /** * Construct manifest from a backup directory. * @param conf configuration * @param backupPath backup path * @throws IOException */ public BackupManifest(Configuration conf, Path backupPath) throws IOException { this(backupPath.getFileSystem(conf), backupPath); } /** * Construct manifest from a backup directory. * @param fs the FileSystem * @param backupPath backup path * @throws BackupException exception */ public BackupManifest(FileSystem fs, Path backupPath) throws BackupException { if (LOG.isDebugEnabled()) { LOG.debug("Loading manifest from: " + backupPath.toString()); } // The input backupDir may not exactly be the backup table dir. // It could be the backup log dir where there is also a manifest file stored. // This variable's purpose is to keep the correct and original location so // that we can store/persist it. try { FileStatus[] subFiles = BackupUtils.listStatus(fs, backupPath, null); if (subFiles == null) { String errorMsg = backupPath.toString() + " does not exist"; LOG.error(errorMsg); throw new IOException(errorMsg); } for (FileStatus subFile : subFiles) { if (subFile.getPath().getName().equals(MANIFEST_FILE_NAME)) { // load and set manifest field from file content FSDataInputStream in = fs.open(subFile.getPath()); long len = subFile.getLen(); byte[] pbBytes = new byte[(int) len]; in.readFully(pbBytes); BackupProtos.BackupImage proto = null; try { proto = BackupProtos.BackupImage.parseFrom(pbBytes); } catch (Exception e) { throw new BackupException(e); } this.backupImage = BackupImage.fromProto(proto); LOG.debug("Loaded manifest instance from manifest file: " + BackupUtils.getPath(subFile.getPath())); return; } } String errorMsg = "No manifest file found in: " + backupPath.toString(); throw new IOException(errorMsg); } catch (IOException e) { throw new BackupException(e.getMessage()); } } public BackupType getType() { return backupImage.getType(); } /** * Get the table set of this image. * @return The table set list */ public List<TableName> getTableList() { return backupImage.getTableNames(); } /** * Persist the manifest file. * @throws IOException IOException when storing the manifest file. */ public void store(Configuration conf) throws BackupException { byte[] data = backupImage.toProto().toByteArray(); // write the file, overwrite if already exist String logBackupDir = BackupUtils.getLogBackupDir(backupImage.getRootDir(), backupImage.getBackupId()); Path manifestFilePath = new Path(new Path((tableBackupDir != null ? tableBackupDir : logBackupDir)), MANIFEST_FILE_NAME); try (FSDataOutputStream out = manifestFilePath.getFileSystem(conf).create(manifestFilePath, true);) { out.write(data); } catch (IOException e) { throw new BackupException(e.getMessage()); } LOG.info("Manifest file stored to " + manifestFilePath); } /** * Get this backup image. * @return the backup image. */ public BackupImage getBackupImage() { return backupImage; } /** * Add dependent backup image for this backup. * @param image The direct dependent backup image */ public void addDependentImage(BackupImage image) { this.backupImage.addAncestor(image); } /** * Set the incremental timestamp map directly. * @param incrTimestampMap timestamp map */ public void setIncrTimestampMap(HashMap<TableName, HashMap<String, Long>> incrTimestampMap) { this.backupImage.setIncrTimeRanges(incrTimestampMap); } public Map<TableName, HashMap<String, Long>> getIncrTimestampMap() { return backupImage.getIncrTimeRanges(); } /** * Get the image list of this backup for restore in time order. * @param reverse If true, then output in reverse order, otherwise in time order from old to new * @return the backup image list for restore in time order */ public ArrayList<BackupImage> getRestoreDependentList(boolean reverse) { TreeMap<Long, BackupImage> restoreImages = new TreeMap<Long, BackupImage>(); restoreImages.put(backupImage.startTs, backupImage); for (BackupImage image : backupImage.getAncestors()) { restoreImages.put(Long.valueOf(image.startTs), image); } return new ArrayList<BackupImage>(reverse ? (restoreImages.descendingMap().values()) : (restoreImages.values())); } /** * Get the dependent image list for a specific table of this backup in time order from old to new * if want to restore to this backup image level. * @param table table * @return the backup image list for a table in time order */ public ArrayList<BackupImage> getDependentListByTable(TableName table) { ArrayList<BackupImage> tableImageList = new ArrayList<BackupImage>(); ArrayList<BackupImage> imageList = getRestoreDependentList(true); for (BackupImage image : imageList) { if (image.hasTable(table)) { tableImageList.add(image); if (image.getType() == BackupType.FULL) { break; } } } Collections.reverse(tableImageList); return tableImageList; } /** * Get the full dependent image list in the whole dependency scope for a specific table of this * backup in time order from old to new. * @param table table * @return the full backup image list for a table in time order in the whole scope of the * dependency of this image */ public ArrayList<BackupImage> getAllDependentListByTable(TableName table) { ArrayList<BackupImage> tableImageList = new ArrayList<BackupImage>(); ArrayList<BackupImage> imageList = getRestoreDependentList(false); for (BackupImage image : imageList) { if (image.hasTable(table)) { tableImageList.add(image); } } return tableImageList; } /** * Check whether backup image1 could cover backup image2 or not. * @param image1 backup image 1 * @param image2 backup image 2 * @return true if image1 can cover image2, otherwise false */ public static boolean canCoverImage(BackupImage image1, BackupImage image2) { // image1 can cover image2 only when the following conditions are satisfied: // - image1 must not be an incremental image; // - image1 must be taken after image2 has been taken; // - table set of image1 must cover the table set of image2. if (image1.getType() == BackupType.INCREMENTAL) { return false; } if (image1.getStartTs() < image2.getStartTs()) { return false; } List<TableName> image1TableList = image1.getTableNames(); List<TableName> image2TableList = image2.getTableNames(); boolean found = false; for (int i = 0; i < image2TableList.size(); i++) { found = false; for (int j = 0; j < image1TableList.size(); j++) { if (image2TableList.get(i).equals(image1TableList.get(j))) { found = true; break; } } if (!found) { return false; } } LOG.debug("Backup image " + image1.getBackupId() + " can cover " + image2.getBackupId()); return true; } /** * Check whether backup image set could cover a backup image or not. * @param fullImages The backup image set * @param image The target backup image * @return true if fullImages can cover image, otherwise false */ public static boolean canCoverImage(ArrayList<BackupImage> fullImages, BackupImage image) { // fullImages can cover image only when the following conditions are satisfied: // - each image of fullImages must not be an incremental image; // - each image of fullImages must be taken after image has been taken; // - sum table set of fullImages must cover the table set of image. for (BackupImage image1 : fullImages) { if (image1.getType() == BackupType.INCREMENTAL) { return false; } if (image1.getStartTs() < image.getStartTs()) { return false; } } ArrayList<String> image1TableList = new ArrayList<String>(); for (BackupImage image1 : fullImages) { List<TableName> tableList = image1.getTableNames(); for (TableName table : tableList) { image1TableList.add(table.getNameAsString()); } } ArrayList<String> image2TableList = new ArrayList<String>(); List<TableName> tableList = image.getTableNames(); for (TableName table : tableList) { image2TableList.add(table.getNameAsString()); } for (int i = 0; i < image2TableList.size(); i++) { if (image1TableList.contains(image2TableList.get(i)) == false) { return false; } } LOG.debug("Full image set can cover image " + image.getBackupId()); return true; } public BackupInfo toBackupInfo() { BackupInfo info = new BackupInfo(); info.setType(backupImage.getType()); List<TableName> list = backupImage.getTableNames(); TableName[] tables = new TableName[list.size()]; info.addTables(list.toArray(tables)); info.setBackupId(backupImage.getBackupId()); info.setStartTs(backupImage.getStartTs()); info.setBackupRootDir(backupImage.getRootDir()); if (backupImage.getType() == BackupType.INCREMENTAL) { info.setHLogTargetDir(BackupUtils.getLogBackupDir(backupImage.getRootDir(), backupImage.getBackupId())); } return info; } }