001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.hadoop.hbase.snapshot;
020
021import java.io.BufferedInputStream;
022import java.io.DataInput;
023import java.io.DataOutput;
024import java.io.FileNotFoundException;
025import java.io.IOException;
026import java.io.InputStream;
027import java.util.ArrayList;
028import java.util.Collections;
029import java.util.Comparator;
030import java.util.LinkedList;
031import java.util.List;
032
033import org.apache.hadoop.conf.Configuration;
034import org.apache.hadoop.fs.FSDataInputStream;
035import org.apache.hadoop.fs.FSDataOutputStream;
036import org.apache.hadoop.fs.FileChecksum;
037import org.apache.hadoop.fs.FileStatus;
038import org.apache.hadoop.fs.FileSystem;
039import org.apache.hadoop.fs.FileUtil;
040import org.apache.hadoop.fs.Path;
041import org.apache.hadoop.fs.permission.FsPermission;
042import org.apache.hadoop.hbase.HBaseConfiguration;
043import org.apache.hadoop.hbase.HConstants;
044import org.apache.hadoop.hbase.TableName;
045import org.apache.hadoop.hbase.client.RegionInfo;
046import org.apache.hadoop.hbase.io.FileLink;
047import org.apache.hadoop.hbase.io.HFileLink;
048import org.apache.hadoop.hbase.io.WALLink;
049import org.apache.hadoop.hbase.io.hadoopbackport.ThrottledInputStream;
050import org.apache.hadoop.hbase.mapreduce.TableMapReduceUtil;
051import org.apache.hadoop.hbase.mob.MobUtils;
052import org.apache.hadoop.hbase.util.AbstractHBaseTool;
053import org.apache.hadoop.hbase.util.FSUtils;
054import org.apache.hadoop.hbase.util.HFileArchiveUtil;
055import org.apache.hadoop.hbase.util.Pair;
056import org.apache.hadoop.io.BytesWritable;
057import org.apache.hadoop.io.IOUtils;
058import org.apache.hadoop.io.NullWritable;
059import org.apache.hadoop.io.Writable;
060import org.apache.hadoop.mapreduce.InputFormat;
061import org.apache.hadoop.mapreduce.InputSplit;
062import org.apache.hadoop.mapreduce.Job;
063import org.apache.hadoop.mapreduce.JobContext;
064import org.apache.hadoop.mapreduce.Mapper;
065import org.apache.hadoop.mapreduce.RecordReader;
066import org.apache.hadoop.mapreduce.TaskAttemptContext;
067import org.apache.hadoop.mapreduce.lib.output.NullOutputFormat;
068import org.apache.hadoop.mapreduce.security.TokenCache;
069import org.apache.hadoop.util.StringUtils;
070import org.apache.hadoop.util.Tool;
071import org.apache.yetus.audience.InterfaceAudience;
072import org.slf4j.Logger;
073import org.slf4j.LoggerFactory;
074import org.apache.hbase.thirdparty.org.apache.commons.cli.CommandLine;
075import org.apache.hbase.thirdparty.org.apache.commons.cli.Option;
076import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotDescription;
077import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotFileInfo;
078import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotRegionManifest;
079
080/**
081 * Export the specified snapshot to a given FileSystem.
082 *
083 * The .snapshot/name folder is copied to the destination cluster
084 * and then all the hfiles/wals are copied using a Map-Reduce Job in the .archive/ location.
085 * When everything is done, the second cluster can restore the snapshot.
086 */
087@InterfaceAudience.Public
088public class ExportSnapshot extends AbstractHBaseTool implements Tool {
089  public static final String NAME = "exportsnapshot";
090  /** Configuration prefix for overrides for the source filesystem */
091  public static final String CONF_SOURCE_PREFIX = NAME + ".from.";
092  /** Configuration prefix for overrides for the destination filesystem */
093  public static final String CONF_DEST_PREFIX = NAME + ".to.";
094
095  private static final Logger LOG = LoggerFactory.getLogger(ExportSnapshot.class);
096
097  private static final String MR_NUM_MAPS = "mapreduce.job.maps";
098  private static final String CONF_NUM_SPLITS = "snapshot.export.format.splits";
099  private static final String CONF_SNAPSHOT_NAME = "snapshot.export.format.snapshot.name";
100  private static final String CONF_SNAPSHOT_DIR = "snapshot.export.format.snapshot.dir";
101  private static final String CONF_FILES_USER = "snapshot.export.files.attributes.user";
102  private static final String CONF_FILES_GROUP = "snapshot.export.files.attributes.group";
103  private static final String CONF_FILES_MODE = "snapshot.export.files.attributes.mode";
104  private static final String CONF_CHECKSUM_VERIFY = "snapshot.export.checksum.verify";
105  private static final String CONF_OUTPUT_ROOT = "snapshot.export.output.root";
106  private static final String CONF_INPUT_ROOT = "snapshot.export.input.root";
107  private static final String CONF_BUFFER_SIZE = "snapshot.export.buffer.size";
108  private static final String CONF_MAP_GROUP = "snapshot.export.default.map.group";
109  private static final String CONF_BANDWIDTH_MB = "snapshot.export.map.bandwidth.mb";
110  private static final String CONF_MR_JOB_NAME = "mapreduce.job.name";
111  protected static final String CONF_SKIP_TMP = "snapshot.export.skip.tmp";
112
113  static class Testing {
114    static final String CONF_TEST_FAILURE = "test.snapshot.export.failure";
115    static final String CONF_TEST_FAILURE_COUNT = "test.snapshot.export.failure.count";
116    int failuresCountToInject = 0;
117    int injectedFailureCount = 0;
118  }
119
120  // Command line options and defaults.
121  static final class Options {
122    static final Option SNAPSHOT = new Option(null, "snapshot", true, "Snapshot to restore.");
123    static final Option TARGET_NAME = new Option(null, "target", true,
124        "Target name for the snapshot.");
125    static final Option COPY_TO = new Option(null, "copy-to", true, "Remote "
126        + "destination hdfs://");
127    static final Option COPY_FROM = new Option(null, "copy-from", true,
128        "Input folder hdfs:// (default hbase.rootdir)");
129    static final Option NO_CHECKSUM_VERIFY = new Option(null, "no-checksum-verify", false,
130        "Do not verify checksum, use name+length only.");
131    static final Option NO_TARGET_VERIFY = new Option(null, "no-target-verify", false,
132        "Do not verify the integrity of the exported snapshot.");
133    static final Option OVERWRITE = new Option(null, "overwrite", false,
134        "Rewrite the snapshot manifest if already exists.");
135    static final Option CHUSER = new Option(null, "chuser", true,
136        "Change the owner of the files to the specified one.");
137    static final Option CHGROUP = new Option(null, "chgroup", true,
138        "Change the group of the files to the specified one.");
139    static final Option CHMOD = new Option(null, "chmod", true,
140        "Change the permission of the files to the specified one.");
141    static final Option MAPPERS = new Option(null, "mappers", true,
142        "Number of mappers to use during the copy (mapreduce.job.maps).");
143    static final Option BANDWIDTH = new Option(null, "bandwidth", true,
144        "Limit bandwidth to this value in MB/second.");
145  }
146
147  // Export Map-Reduce Counters, to keep track of the progress
148  public enum Counter {
149    MISSING_FILES, FILES_COPIED, FILES_SKIPPED, COPY_FAILED,
150    BYTES_EXPECTED, BYTES_SKIPPED, BYTES_COPIED
151  }
152
153  private static class ExportMapper extends Mapper<BytesWritable, NullWritable,
154                                                   NullWritable, NullWritable> {
155    private static final Logger LOG = LoggerFactory.getLogger(ExportMapper.class);
156    final static int REPORT_SIZE = 1 * 1024 * 1024;
157    final static int BUFFER_SIZE = 64 * 1024;
158
159    private boolean verifyChecksum;
160    private String filesGroup;
161    private String filesUser;
162    private short filesMode;
163    private int bufferSize;
164
165    private FileSystem outputFs;
166    private Path outputArchive;
167    private Path outputRoot;
168
169    private FileSystem inputFs;
170    private Path inputArchive;
171    private Path inputRoot;
172
173    private static Testing testing = new Testing();
174
175    @Override
176    public void setup(Context context) throws IOException {
177      Configuration conf = context.getConfiguration();
178
179      Configuration srcConf = HBaseConfiguration.createClusterConf(conf, null, CONF_SOURCE_PREFIX);
180      Configuration destConf = HBaseConfiguration.createClusterConf(conf, null, CONF_DEST_PREFIX);
181
182      verifyChecksum = conf.getBoolean(CONF_CHECKSUM_VERIFY, true);
183
184      filesGroup = conf.get(CONF_FILES_GROUP);
185      filesUser = conf.get(CONF_FILES_USER);
186      filesMode = (short)conf.getInt(CONF_FILES_MODE, 0);
187      outputRoot = new Path(conf.get(CONF_OUTPUT_ROOT));
188      inputRoot = new Path(conf.get(CONF_INPUT_ROOT));
189
190      inputArchive = new Path(inputRoot, HConstants.HFILE_ARCHIVE_DIRECTORY);
191      outputArchive = new Path(outputRoot, HConstants.HFILE_ARCHIVE_DIRECTORY);
192
193      try {
194        srcConf.setBoolean("fs." + inputRoot.toUri().getScheme() + ".impl.disable.cache", true);
195        inputFs = FileSystem.get(inputRoot.toUri(), srcConf);
196      } catch (IOException e) {
197        throw new IOException("Could not get the input FileSystem with root=" + inputRoot, e);
198      }
199
200      try {
201        destConf.setBoolean("fs." + outputRoot.toUri().getScheme() + ".impl.disable.cache", true);
202        outputFs = FileSystem.get(outputRoot.toUri(), destConf);
203      } catch (IOException e) {
204        throw new IOException("Could not get the output FileSystem with root="+ outputRoot, e);
205      }
206
207      // Use the default block size of the outputFs if bigger
208      int defaultBlockSize = Math.max((int) outputFs.getDefaultBlockSize(outputRoot), BUFFER_SIZE);
209      bufferSize = conf.getInt(CONF_BUFFER_SIZE, defaultBlockSize);
210      LOG.info("Using bufferSize=" + StringUtils.humanReadableInt(bufferSize));
211
212      for (Counter c : Counter.values()) {
213        context.getCounter(c).increment(0);
214      }
215      if (context.getConfiguration().getBoolean(Testing.CONF_TEST_FAILURE, false)) {
216        testing.failuresCountToInject = conf.getInt(Testing.CONF_TEST_FAILURE_COUNT, 0);
217        // Get number of times we have already injected failure based on attempt number of this
218        // task.
219        testing.injectedFailureCount = context.getTaskAttemptID().getId();
220      }
221    }
222
223    @Override
224    protected void cleanup(Context context) {
225      IOUtils.closeStream(inputFs);
226      IOUtils.closeStream(outputFs);
227    }
228
229    @Override
230    public void map(BytesWritable key, NullWritable value, Context context)
231        throws InterruptedException, IOException {
232      SnapshotFileInfo inputInfo = SnapshotFileInfo.parseFrom(key.copyBytes());
233      Path outputPath = getOutputPath(inputInfo);
234
235      copyFile(context, inputInfo, outputPath);
236    }
237
238    /**
239     * Returns the location where the inputPath will be copied.
240     */
241    private Path getOutputPath(final SnapshotFileInfo inputInfo) throws IOException {
242      Path path = null;
243      switch (inputInfo.getType()) {
244        case HFILE:
245          Path inputPath = new Path(inputInfo.getHfile());
246          String family = inputPath.getParent().getName();
247          TableName table =HFileLink.getReferencedTableName(inputPath.getName());
248          String region = HFileLink.getReferencedRegionName(inputPath.getName());
249          String hfile = HFileLink.getReferencedHFileName(inputPath.getName());
250          path = new Path(FSUtils.getTableDir(new Path("./"), table),
251              new Path(region, new Path(family, hfile)));
252          break;
253        case WAL:
254          LOG.warn("snapshot does not keeps WALs: " + inputInfo);
255          break;
256        default:
257          throw new IOException("Invalid File Type: " + inputInfo.getType().toString());
258      }
259      return new Path(outputArchive, path);
260    }
261
262    /**
263     * Used by TestExportSnapshot to test for retries when failures happen.
264     * Failure is injected in {@link #copyFile(Context, SnapshotFileInfo, Path)}.
265     */
266    private void injectTestFailure(final Context context, final SnapshotFileInfo inputInfo)
267        throws IOException {
268      if (!context.getConfiguration().getBoolean(Testing.CONF_TEST_FAILURE, false)) return;
269      if (testing.injectedFailureCount >= testing.failuresCountToInject) return;
270      testing.injectedFailureCount++;
271      context.getCounter(Counter.COPY_FAILED).increment(1);
272      LOG.debug("Injecting failure. Count: " + testing.injectedFailureCount);
273      throw new IOException(String.format("TEST FAILURE (%d of max %d): Unable to copy input=%s",
274          testing.injectedFailureCount, testing.failuresCountToInject, inputInfo));
275    }
276
277    private void copyFile(final Context context, final SnapshotFileInfo inputInfo,
278        final Path outputPath) throws IOException {
279      // Get the file information
280      FileStatus inputStat = getSourceFileStatus(context, inputInfo);
281
282      // Verify if the output file exists and is the same that we want to copy
283      if (outputFs.exists(outputPath)) {
284        FileStatus outputStat = outputFs.getFileStatus(outputPath);
285        if (outputStat != null && sameFile(inputStat, outputStat)) {
286          LOG.info("Skip copy " + inputStat.getPath() + " to " + outputPath + ", same file.");
287          context.getCounter(Counter.FILES_SKIPPED).increment(1);
288          context.getCounter(Counter.BYTES_SKIPPED).increment(inputStat.getLen());
289          return;
290        }
291      }
292
293      InputStream in = openSourceFile(context, inputInfo);
294      int bandwidthMB = context.getConfiguration().getInt(CONF_BANDWIDTH_MB, 100);
295      if (Integer.MAX_VALUE != bandwidthMB) {
296        in = new ThrottledInputStream(new BufferedInputStream(in), bandwidthMB * 1024 * 1024L);
297      }
298
299      try {
300        context.getCounter(Counter.BYTES_EXPECTED).increment(inputStat.getLen());
301
302        // Ensure that the output folder is there and copy the file
303        createOutputPath(outputPath.getParent());
304        FSDataOutputStream out = outputFs.create(outputPath, true);
305        try {
306          copyData(context, inputStat.getPath(), in, outputPath, out, inputStat.getLen());
307        } finally {
308          out.close();
309        }
310
311        // Try to Preserve attributes
312        if (!preserveAttributes(outputPath, inputStat)) {
313          LOG.warn("You may have to run manually chown on: " + outputPath);
314        }
315      } finally {
316        in.close();
317        injectTestFailure(context, inputInfo);
318      }
319    }
320
321    /**
322     * Create the output folder and optionally set ownership.
323     */
324    private void createOutputPath(final Path path) throws IOException {
325      if (filesUser == null && filesGroup == null) {
326        outputFs.mkdirs(path);
327      } else {
328        Path parent = path.getParent();
329        if (!outputFs.exists(parent) && !parent.isRoot()) {
330          createOutputPath(parent);
331        }
332        outputFs.mkdirs(path);
333        if (filesUser != null || filesGroup != null) {
334          // override the owner when non-null user/group is specified
335          outputFs.setOwner(path, filesUser, filesGroup);
336        }
337        if (filesMode > 0) {
338          outputFs.setPermission(path, new FsPermission(filesMode));
339        }
340      }
341    }
342
343    /**
344     * Try to Preserve the files attribute selected by the user copying them from the source file
345     * This is only required when you are exporting as a different user than "hbase" or on a system
346     * that doesn't have the "hbase" user.
347     *
348     * This is not considered a blocking failure since the user can force a chmod with the user
349     * that knows is available on the system.
350     */
351    private boolean preserveAttributes(final Path path, final FileStatus refStat) {
352      FileStatus stat;
353      try {
354        stat = outputFs.getFileStatus(path);
355      } catch (IOException e) {
356        LOG.warn("Unable to get the status for file=" + path);
357        return false;
358      }
359
360      try {
361        if (filesMode > 0 && stat.getPermission().toShort() != filesMode) {
362          outputFs.setPermission(path, new FsPermission(filesMode));
363        } else if (refStat != null && !stat.getPermission().equals(refStat.getPermission())) {
364          outputFs.setPermission(path, refStat.getPermission());
365        }
366      } catch (IOException e) {
367        LOG.warn("Unable to set the permission for file="+ stat.getPath() +": "+ e.getMessage());
368        return false;
369      }
370
371      boolean hasRefStat = (refStat != null);
372      String user = stringIsNotEmpty(filesUser) || !hasRefStat ? filesUser : refStat.getOwner();
373      String group = stringIsNotEmpty(filesGroup) || !hasRefStat ? filesGroup : refStat.getGroup();
374      if (stringIsNotEmpty(user) || stringIsNotEmpty(group)) {
375        try {
376          if (!(user.equals(stat.getOwner()) && group.equals(stat.getGroup()))) {
377            outputFs.setOwner(path, user, group);
378          }
379        } catch (IOException e) {
380          LOG.warn("Unable to set the owner/group for file="+ stat.getPath() +": "+ e.getMessage());
381          LOG.warn("The user/group may not exist on the destination cluster: user=" +
382                   user + " group=" + group);
383          return false;
384        }
385      }
386
387      return true;
388    }
389
390    private boolean stringIsNotEmpty(final String str) {
391      return str != null && str.length() > 0;
392    }
393
394    private void copyData(final Context context,
395        final Path inputPath, final InputStream in,
396        final Path outputPath, final FSDataOutputStream out,
397        final long inputFileSize)
398        throws IOException {
399      final String statusMessage = "copied %s/" + StringUtils.humanReadableInt(inputFileSize) +
400                                   " (%.1f%%)";
401
402      try {
403        byte[] buffer = new byte[bufferSize];
404        long totalBytesWritten = 0;
405        int reportBytes = 0;
406        int bytesRead;
407
408        long stime = System.currentTimeMillis();
409        while ((bytesRead = in.read(buffer)) > 0) {
410          out.write(buffer, 0, bytesRead);
411          totalBytesWritten += bytesRead;
412          reportBytes += bytesRead;
413
414          if (reportBytes >= REPORT_SIZE) {
415            context.getCounter(Counter.BYTES_COPIED).increment(reportBytes);
416            context.setStatus(String.format(statusMessage,
417                              StringUtils.humanReadableInt(totalBytesWritten),
418                              (totalBytesWritten/(float)inputFileSize) * 100.0f) +
419                              " from " + inputPath + " to " + outputPath);
420            reportBytes = 0;
421          }
422        }
423        long etime = System.currentTimeMillis();
424
425        context.getCounter(Counter.BYTES_COPIED).increment(reportBytes);
426        context.setStatus(String.format(statusMessage,
427                          StringUtils.humanReadableInt(totalBytesWritten),
428                          (totalBytesWritten/(float)inputFileSize) * 100.0f) +
429                          " from " + inputPath + " to " + outputPath);
430
431        // Verify that the written size match
432        if (totalBytesWritten != inputFileSize) {
433          String msg = "number of bytes copied not matching copied=" + totalBytesWritten +
434                       " expected=" + inputFileSize + " for file=" + inputPath;
435          throw new IOException(msg);
436        }
437
438        LOG.info("copy completed for input=" + inputPath + " output=" + outputPath);
439        LOG.info("size=" + totalBytesWritten +
440            " (" + StringUtils.humanReadableInt(totalBytesWritten) + ")" +
441            " time=" + StringUtils.formatTimeDiff(etime, stime) +
442            String.format(" %.3fM/sec", (totalBytesWritten / ((etime - stime)/1000.0))/1048576.0));
443        context.getCounter(Counter.FILES_COPIED).increment(1);
444      } catch (IOException e) {
445        LOG.error("Error copying " + inputPath + " to " + outputPath, e);
446        context.getCounter(Counter.COPY_FAILED).increment(1);
447        throw e;
448      }
449    }
450
451    /**
452     * Try to open the "source" file.
453     * Throws an IOException if the communication with the inputFs fail or
454     * if the file is not found.
455     */
456    private FSDataInputStream openSourceFile(Context context, final SnapshotFileInfo fileInfo)
457            throws IOException {
458      try {
459        Configuration conf = context.getConfiguration();
460        FileLink link = null;
461        switch (fileInfo.getType()) {
462          case HFILE:
463            Path inputPath = new Path(fileInfo.getHfile());
464            link = getFileLink(inputPath, conf);
465            break;
466          case WAL:
467            String serverName = fileInfo.getWalServer();
468            String logName = fileInfo.getWalName();
469            link = new WALLink(inputRoot, serverName, logName);
470            break;
471          default:
472            throw new IOException("Invalid File Type: " + fileInfo.getType().toString());
473        }
474        return link.open(inputFs);
475      } catch (IOException e) {
476        context.getCounter(Counter.MISSING_FILES).increment(1);
477        LOG.error("Unable to open source file=" + fileInfo.toString(), e);
478        throw e;
479      }
480    }
481
482    private FileStatus getSourceFileStatus(Context context, final SnapshotFileInfo fileInfo)
483        throws IOException {
484      try {
485        Configuration conf = context.getConfiguration();
486        FileLink link = null;
487        switch (fileInfo.getType()) {
488          case HFILE:
489            Path inputPath = new Path(fileInfo.getHfile());
490            link = getFileLink(inputPath, conf);
491            break;
492          case WAL:
493            link = new WALLink(inputRoot, fileInfo.getWalServer(), fileInfo.getWalName());
494            break;
495          default:
496            throw new IOException("Invalid File Type: " + fileInfo.getType().toString());
497        }
498        return link.getFileStatus(inputFs);
499      } catch (FileNotFoundException e) {
500        context.getCounter(Counter.MISSING_FILES).increment(1);
501        LOG.error("Unable to get the status for source file=" + fileInfo.toString(), e);
502        throw e;
503      } catch (IOException e) {
504        LOG.error("Unable to get the status for source file=" + fileInfo.toString(), e);
505        throw e;
506      }
507    }
508
509    private FileLink getFileLink(Path path, Configuration conf) throws IOException{
510      String regionName = HFileLink.getReferencedRegionName(path.getName());
511      TableName tableName = HFileLink.getReferencedTableName(path.getName());
512      if(MobUtils.getMobRegionInfo(tableName).getEncodedName().equals(regionName)) {
513        return HFileLink.buildFromHFileLinkPattern(MobUtils.getQualifiedMobRootDir(conf),
514                HFileArchiveUtil.getArchivePath(conf), path);
515      }
516      return HFileLink.buildFromHFileLinkPattern(inputRoot, inputArchive, path);
517    }
518
519    private FileChecksum getFileChecksum(final FileSystem fs, final Path path) {
520      try {
521        return fs.getFileChecksum(path);
522      } catch (IOException e) {
523        LOG.warn("Unable to get checksum for file=" + path, e);
524        return null;
525      }
526    }
527
528    /**
529     * Check if the two files are equal by looking at the file length,
530     * and at the checksum (if user has specified the verifyChecksum flag).
531     */
532    private boolean sameFile(final FileStatus inputStat, final FileStatus outputStat) {
533      // Not matching length
534      if (inputStat.getLen() != outputStat.getLen()) return false;
535
536      // Mark files as equals, since user asked for no checksum verification
537      if (!verifyChecksum) return true;
538
539      // If checksums are not available, files are not the same.
540      FileChecksum inChecksum = getFileChecksum(inputFs, inputStat.getPath());
541      if (inChecksum == null) return false;
542
543      FileChecksum outChecksum = getFileChecksum(outputFs, outputStat.getPath());
544      if (outChecksum == null) return false;
545
546      return inChecksum.equals(outChecksum);
547    }
548  }
549
550  // ==========================================================================
551  //  Input Format
552  // ==========================================================================
553
554  /**
555   * Extract the list of files (HFiles/WALs) to copy using Map-Reduce.
556   * @return list of files referenced by the snapshot (pair of path and size)
557   */
558  private static List<Pair<SnapshotFileInfo, Long>> getSnapshotFiles(final Configuration conf,
559      final FileSystem fs, final Path snapshotDir) throws IOException {
560    SnapshotDescription snapshotDesc = SnapshotDescriptionUtils.readSnapshotInfo(fs, snapshotDir);
561
562    final List<Pair<SnapshotFileInfo, Long>> files = new ArrayList<>();
563    final TableName table = TableName.valueOf(snapshotDesc.getTable());
564
565    // Get snapshot files
566    LOG.info("Loading Snapshot '" + snapshotDesc.getName() + "' hfile list");
567    SnapshotReferenceUtil.visitReferencedFiles(conf, fs, snapshotDir, snapshotDesc,
568      new SnapshotReferenceUtil.SnapshotVisitor() {
569        @Override
570        public void storeFile(final RegionInfo regionInfo, final String family,
571            final SnapshotRegionManifest.StoreFile storeFile) throws IOException {
572          // for storeFile.hasReference() case, copied as part of the manifest
573          if (!storeFile.hasReference()) {
574            String region = regionInfo.getEncodedName();
575            String hfile = storeFile.getName();
576            Path path = HFileLink.createPath(table, region, family, hfile);
577
578            SnapshotFileInfo fileInfo = SnapshotFileInfo.newBuilder()
579              .setType(SnapshotFileInfo.Type.HFILE)
580              .setHfile(path.toString())
581              .build();
582
583            long size;
584            if (storeFile.hasFileSize()) {
585              size = storeFile.getFileSize();
586            } else {
587              size = HFileLink.buildFromHFileLinkPattern(conf, path).getFileStatus(fs).getLen();
588            }
589            files.add(new Pair<>(fileInfo, size));
590          }
591        }
592    });
593
594    return files;
595  }
596
597  /**
598   * Given a list of file paths and sizes, create around ngroups in as balanced a way as possible.
599   * The groups created will have similar amounts of bytes.
600   * <p>
601   * The algorithm used is pretty straightforward; the file list is sorted by size,
602   * and then each group fetch the bigger file available, iterating through groups
603   * alternating the direction.
604   */
605  static List<List<Pair<SnapshotFileInfo, Long>>> getBalancedSplits(
606      final List<Pair<SnapshotFileInfo, Long>> files, final int ngroups) {
607    // Sort files by size, from small to big
608    Collections.sort(files, new Comparator<Pair<SnapshotFileInfo, Long>>() {
609      public int compare(Pair<SnapshotFileInfo, Long> a, Pair<SnapshotFileInfo, Long> b) {
610        long r = a.getSecond() - b.getSecond();
611        return (r < 0) ? -1 : ((r > 0) ? 1 : 0);
612      }
613    });
614
615    // create balanced groups
616    List<List<Pair<SnapshotFileInfo, Long>>> fileGroups = new LinkedList<>();
617    long[] sizeGroups = new long[ngroups];
618    int hi = files.size() - 1;
619    int lo = 0;
620
621    List<Pair<SnapshotFileInfo, Long>> group;
622    int dir = 1;
623    int g = 0;
624
625    while (hi >= lo) {
626      if (g == fileGroups.size()) {
627        group = new LinkedList<>();
628        fileGroups.add(group);
629      } else {
630        group = fileGroups.get(g);
631      }
632
633      Pair<SnapshotFileInfo, Long> fileInfo = files.get(hi--);
634
635      // add the hi one
636      sizeGroups[g] += fileInfo.getSecond();
637      group.add(fileInfo);
638
639      // change direction when at the end or the beginning
640      g += dir;
641      if (g == ngroups) {
642        dir = -1;
643        g = ngroups - 1;
644      } else if (g < 0) {
645        dir = 1;
646        g = 0;
647      }
648    }
649
650    if (LOG.isDebugEnabled()) {
651      for (int i = 0; i < sizeGroups.length; ++i) {
652        LOG.debug("export split=" + i + " size=" + StringUtils.humanReadableInt(sizeGroups[i]));
653      }
654    }
655
656    return fileGroups;
657  }
658
659  private static class ExportSnapshotInputFormat extends InputFormat<BytesWritable, NullWritable> {
660    @Override
661    public RecordReader<BytesWritable, NullWritable> createRecordReader(InputSplit split,
662        TaskAttemptContext tac) throws IOException, InterruptedException {
663      return new ExportSnapshotRecordReader(((ExportSnapshotInputSplit)split).getSplitKeys());
664    }
665
666    @Override
667    public List<InputSplit> getSplits(JobContext context) throws IOException, InterruptedException {
668      Configuration conf = context.getConfiguration();
669      Path snapshotDir = new Path(conf.get(CONF_SNAPSHOT_DIR));
670      FileSystem fs = FileSystem.get(snapshotDir.toUri(), conf);
671
672      List<Pair<SnapshotFileInfo, Long>> snapshotFiles = getSnapshotFiles(conf, fs, snapshotDir);
673      int mappers = conf.getInt(CONF_NUM_SPLITS, 0);
674      if (mappers == 0 && snapshotFiles.size() > 0) {
675        mappers = 1 + (snapshotFiles.size() / conf.getInt(CONF_MAP_GROUP, 10));
676        mappers = Math.min(mappers, snapshotFiles.size());
677        conf.setInt(CONF_NUM_SPLITS, mappers);
678        conf.setInt(MR_NUM_MAPS, mappers);
679      }
680
681      List<List<Pair<SnapshotFileInfo, Long>>> groups = getBalancedSplits(snapshotFiles, mappers);
682      List<InputSplit> splits = new ArrayList(groups.size());
683      for (List<Pair<SnapshotFileInfo, Long>> files: groups) {
684        splits.add(new ExportSnapshotInputSplit(files));
685      }
686      return splits;
687    }
688
689    private static class ExportSnapshotInputSplit extends InputSplit implements Writable {
690      private List<Pair<BytesWritable, Long>> files;
691      private long length;
692
693      public ExportSnapshotInputSplit() {
694        this.files = null;
695      }
696
697      public ExportSnapshotInputSplit(final List<Pair<SnapshotFileInfo, Long>> snapshotFiles) {
698        this.files = new ArrayList(snapshotFiles.size());
699        for (Pair<SnapshotFileInfo, Long> fileInfo: snapshotFiles) {
700          this.files.add(new Pair<>(
701            new BytesWritable(fileInfo.getFirst().toByteArray()), fileInfo.getSecond()));
702          this.length += fileInfo.getSecond();
703        }
704      }
705
706      private List<Pair<BytesWritable, Long>> getSplitKeys() {
707        return files;
708      }
709
710      @Override
711      public long getLength() throws IOException, InterruptedException {
712        return length;
713      }
714
715      @Override
716      public String[] getLocations() throws IOException, InterruptedException {
717        return new String[] {};
718      }
719
720      @Override
721      public void readFields(DataInput in) throws IOException {
722        int count = in.readInt();
723        files = new ArrayList<>(count);
724        length = 0;
725        for (int i = 0; i < count; ++i) {
726          BytesWritable fileInfo = new BytesWritable();
727          fileInfo.readFields(in);
728          long size = in.readLong();
729          files.add(new Pair<>(fileInfo, size));
730          length += size;
731        }
732      }
733
734      @Override
735      public void write(DataOutput out) throws IOException {
736        out.writeInt(files.size());
737        for (final Pair<BytesWritable, Long> fileInfo: files) {
738          fileInfo.getFirst().write(out);
739          out.writeLong(fileInfo.getSecond());
740        }
741      }
742    }
743
744    private static class ExportSnapshotRecordReader
745        extends RecordReader<BytesWritable, NullWritable> {
746      private final List<Pair<BytesWritable, Long>> files;
747      private long totalSize = 0;
748      private long procSize = 0;
749      private int index = -1;
750
751      ExportSnapshotRecordReader(final List<Pair<BytesWritable, Long>> files) {
752        this.files = files;
753        for (Pair<BytesWritable, Long> fileInfo: files) {
754          totalSize += fileInfo.getSecond();
755        }
756      }
757
758      @Override
759      public void close() { }
760
761      @Override
762      public BytesWritable getCurrentKey() { return files.get(index).getFirst(); }
763
764      @Override
765      public NullWritable getCurrentValue() { return NullWritable.get(); }
766
767      @Override
768      public float getProgress() { return (float)procSize / totalSize; }
769
770      @Override
771      public void initialize(InputSplit split, TaskAttemptContext tac) { }
772
773      @Override
774      public boolean nextKeyValue() {
775        if (index >= 0) {
776          procSize += files.get(index).getSecond();
777        }
778        return(++index < files.size());
779      }
780    }
781  }
782
783  // ==========================================================================
784  //  Tool
785  // ==========================================================================
786
787  /**
788   * Run Map-Reduce Job to perform the files copy.
789   */
790  private void runCopyJob(final Path inputRoot, final Path outputRoot,
791      final String snapshotName, final Path snapshotDir, final boolean verifyChecksum,
792      final String filesUser, final String filesGroup, final int filesMode,
793      final int mappers, final int bandwidthMB)
794          throws IOException, InterruptedException, ClassNotFoundException {
795    Configuration conf = getConf();
796    if (filesGroup != null) conf.set(CONF_FILES_GROUP, filesGroup);
797    if (filesUser != null) conf.set(CONF_FILES_USER, filesUser);
798    if (mappers > 0) {
799      conf.setInt(CONF_NUM_SPLITS, mappers);
800      conf.setInt(MR_NUM_MAPS, mappers);
801    }
802    conf.setInt(CONF_FILES_MODE, filesMode);
803    conf.setBoolean(CONF_CHECKSUM_VERIFY, verifyChecksum);
804    conf.set(CONF_OUTPUT_ROOT, outputRoot.toString());
805    conf.set(CONF_INPUT_ROOT, inputRoot.toString());
806    conf.setInt(CONF_BANDWIDTH_MB, bandwidthMB);
807    conf.set(CONF_SNAPSHOT_NAME, snapshotName);
808    conf.set(CONF_SNAPSHOT_DIR, snapshotDir.toString());
809
810    String jobname = conf.get(CONF_MR_JOB_NAME, "ExportSnapshot-" + snapshotName);
811    Job job = new Job(conf);
812    job.setJobName(jobname);
813    job.setJarByClass(ExportSnapshot.class);
814    TableMapReduceUtil.addDependencyJars(job);
815    job.setMapperClass(ExportMapper.class);
816    job.setInputFormatClass(ExportSnapshotInputFormat.class);
817    job.setOutputFormatClass(NullOutputFormat.class);
818    job.setMapSpeculativeExecution(false);
819    job.setNumReduceTasks(0);
820
821    // Acquire the delegation Tokens
822    Configuration srcConf = HBaseConfiguration.createClusterConf(conf, null, CONF_SOURCE_PREFIX);
823    TokenCache.obtainTokensForNamenodes(job.getCredentials(),
824      new Path[] { inputRoot }, srcConf);
825    Configuration destConf = HBaseConfiguration.createClusterConf(conf, null, CONF_DEST_PREFIX);
826    TokenCache.obtainTokensForNamenodes(job.getCredentials(),
827        new Path[] { outputRoot }, destConf);
828
829    // Run the MR Job
830    if (!job.waitForCompletion(true)) {
831      throw new ExportSnapshotException(job.getStatus().getFailureInfo());
832    }
833  }
834
835  private void verifySnapshot(final Configuration baseConf,
836      final FileSystem fs, final Path rootDir, final Path snapshotDir) throws IOException {
837    // Update the conf with the current root dir, since may be a different cluster
838    Configuration conf = new Configuration(baseConf);
839    FSUtils.setRootDir(conf, rootDir);
840    FSUtils.setFsDefault(conf, FSUtils.getRootDir(conf));
841    SnapshotDescription snapshotDesc = SnapshotDescriptionUtils.readSnapshotInfo(fs, snapshotDir);
842    SnapshotReferenceUtil.verifySnapshot(conf, fs, snapshotDir, snapshotDesc);
843  }
844
845  /**
846   * Set path ownership.
847   */
848  private void setOwner(final FileSystem fs, final Path path, final String user,
849      final String group, final boolean recursive) throws IOException {
850    if (user != null || group != null) {
851      if (recursive && fs.isDirectory(path)) {
852        for (FileStatus child : fs.listStatus(path)) {
853          setOwner(fs, child.getPath(), user, group, recursive);
854        }
855      }
856      fs.setOwner(path, user, group);
857    }
858  }
859
860  /**
861   * Set path permission.
862   */
863  private void setPermission(final FileSystem fs, final Path path, final short filesMode,
864      final boolean recursive) throws IOException {
865    if (filesMode > 0) {
866      FsPermission perm = new FsPermission(filesMode);
867      if (recursive && fs.isDirectory(path)) {
868        for (FileStatus child : fs.listStatus(path)) {
869          setPermission(fs, child.getPath(), filesMode, recursive);
870        }
871      }
872      fs.setPermission(path, perm);
873    }
874  }
875
876  private boolean verifyTarget = true;
877  private boolean verifyChecksum = true;
878  private String snapshotName = null;
879  private String targetName = null;
880  private boolean overwrite = false;
881  private String filesGroup = null;
882  private String filesUser = null;
883  private Path outputRoot = null;
884  private Path inputRoot = null;
885  private int bandwidthMB = Integer.MAX_VALUE;
886  private int filesMode = 0;
887  private int mappers = 0;
888
889  @Override
890  protected void processOptions(CommandLine cmd) {
891    snapshotName = cmd.getOptionValue(Options.SNAPSHOT.getLongOpt(), snapshotName);
892    targetName = cmd.getOptionValue(Options.TARGET_NAME.getLongOpt(), targetName);
893    if (cmd.hasOption(Options.COPY_TO.getLongOpt())) {
894      outputRoot = new Path(cmd.getOptionValue(Options.COPY_TO.getLongOpt()));
895    }
896    if (cmd.hasOption(Options.COPY_FROM.getLongOpt())) {
897      inputRoot = new Path(cmd.getOptionValue(Options.COPY_FROM.getLongOpt()));
898    }
899    mappers = getOptionAsInt(cmd, Options.MAPPERS.getLongOpt(), mappers);
900    filesUser = cmd.getOptionValue(Options.CHUSER.getLongOpt(), filesUser);
901    filesGroup = cmd.getOptionValue(Options.CHGROUP.getLongOpt(), filesGroup);
902    filesMode = getOptionAsInt(cmd, Options.CHMOD.getLongOpt(), filesMode);
903    bandwidthMB = getOptionAsInt(cmd, Options.BANDWIDTH.getLongOpt(), bandwidthMB);
904    overwrite = cmd.hasOption(Options.OVERWRITE.getLongOpt());
905    // And verifyChecksum and verifyTarget with values read from old args in processOldArgs(...).
906    verifyChecksum = !cmd.hasOption(Options.NO_CHECKSUM_VERIFY.getLongOpt());
907    verifyTarget = !cmd.hasOption(Options.NO_TARGET_VERIFY.getLongOpt());
908  }
909
910  /**
911   * Execute the export snapshot by copying the snapshot metadata, hfiles and wals.
912   * @return 0 on success, and != 0 upon failure.
913   */
914  @Override
915  public int doWork() throws IOException {
916    Configuration conf = getConf();
917
918    // Check user options
919    if (snapshotName == null) {
920      System.err.println("Snapshot name not provided.");
921      LOG.error("Use -h or --help for usage instructions.");
922      return 0;
923    }
924
925    if (outputRoot == null) {
926      System.err.println("Destination file-system (--" + Options.COPY_TO.getLongOpt()
927              + ") not provided.");
928      LOG.error("Use -h or --help for usage instructions.");
929      return 0;
930    }
931
932    if (targetName == null) {
933      targetName = snapshotName;
934    }
935    if (inputRoot == null) {
936      inputRoot = FSUtils.getRootDir(conf);
937    } else {
938      FSUtils.setRootDir(conf, inputRoot);
939    }
940
941    Configuration srcConf = HBaseConfiguration.createClusterConf(conf, null, CONF_SOURCE_PREFIX);
942    srcConf.setBoolean("fs." + inputRoot.toUri().getScheme() + ".impl.disable.cache", true);
943    FileSystem inputFs = FileSystem.get(inputRoot.toUri(), srcConf);
944    LOG.debug("inputFs=" + inputFs.getUri().toString() + " inputRoot=" + inputRoot);
945    Configuration destConf = HBaseConfiguration.createClusterConf(conf, null, CONF_DEST_PREFIX);
946    destConf.setBoolean("fs." + outputRoot.toUri().getScheme() + ".impl.disable.cache", true);
947    FileSystem outputFs = FileSystem.get(outputRoot.toUri(), destConf);
948    LOG.debug("outputFs=" + outputFs.getUri().toString() + " outputRoot=" + outputRoot.toString());
949
950    boolean skipTmp = conf.getBoolean(CONF_SKIP_TMP, false);
951
952    Path snapshotDir = SnapshotDescriptionUtils.getCompletedSnapshotDir(snapshotName, inputRoot);
953    Path snapshotTmpDir = SnapshotDescriptionUtils.getWorkingSnapshotDir(targetName, outputRoot);
954    Path outputSnapshotDir = SnapshotDescriptionUtils.getCompletedSnapshotDir(targetName, outputRoot);
955    Path initialOutputSnapshotDir = skipTmp ? outputSnapshotDir : snapshotTmpDir;
956
957    // Find the necessary directory which need to change owner and group
958    Path needSetOwnerDir = SnapshotDescriptionUtils.getSnapshotRootDir(outputRoot);
959    if (outputFs.exists(needSetOwnerDir)) {
960      if (skipTmp) {
961        needSetOwnerDir = outputSnapshotDir;
962      } else {
963        needSetOwnerDir = SnapshotDescriptionUtils.getWorkingSnapshotDir(outputRoot);
964        if (outputFs.exists(needSetOwnerDir)) {
965          needSetOwnerDir = snapshotTmpDir;
966        }
967      }
968    }
969
970    // Check if the snapshot already exists
971    if (outputFs.exists(outputSnapshotDir)) {
972      if (overwrite) {
973        if (!outputFs.delete(outputSnapshotDir, true)) {
974          System.err.println("Unable to remove existing snapshot directory: " + outputSnapshotDir);
975          return 1;
976        }
977      } else {
978        System.err.println("The snapshot '" + targetName +
979          "' already exists in the destination: " + outputSnapshotDir);
980        return 1;
981      }
982    }
983
984    if (!skipTmp) {
985      // Check if the snapshot already in-progress
986      if (outputFs.exists(snapshotTmpDir)) {
987        if (overwrite) {
988          if (!outputFs.delete(snapshotTmpDir, true)) {
989            System.err.println("Unable to remove existing snapshot tmp directory: "+snapshotTmpDir);
990            return 1;
991          }
992        } else {
993          System.err.println("A snapshot with the same name '"+ targetName +"' may be in-progress");
994          System.err.println("Please check "+snapshotTmpDir+". If the snapshot has completed, ");
995          System.err.println("consider removing "+snapshotTmpDir+" by using the -overwrite option");
996          return 1;
997        }
998      }
999    }
1000
1001    // Step 1 - Copy fs1:/.snapshot/<snapshot> to  fs2:/.snapshot/.tmp/<snapshot>
1002    // The snapshot references must be copied before the hfiles otherwise the cleaner
1003    // will remove them because they are unreferenced.
1004    try {
1005      LOG.info("Copy Snapshot Manifest");
1006      FileUtil.copy(inputFs, snapshotDir, outputFs, initialOutputSnapshotDir, false, false, conf);
1007    } catch (IOException e) {
1008      throw new ExportSnapshotException("Failed to copy the snapshot directory: from=" +
1009        snapshotDir + " to=" + initialOutputSnapshotDir, e);
1010    } finally {
1011      if (filesUser != null || filesGroup != null) {
1012        LOG.warn((filesUser == null ? "" : "Change the owner of " + needSetOwnerDir + " to "
1013            + filesUser)
1014            + (filesGroup == null ? "" : ", Change the group of " + needSetOwnerDir + " to "
1015            + filesGroup));
1016        setOwner(outputFs, needSetOwnerDir, filesUser, filesGroup, true);
1017      }
1018      if (filesMode > 0) {
1019        LOG.warn("Change the permission of " + needSetOwnerDir + " to " + filesMode);
1020        setPermission(outputFs, needSetOwnerDir, (short)filesMode, true);
1021      }
1022    }
1023
1024    // Write a new .snapshotinfo if the target name is different from the source name
1025    if (!targetName.equals(snapshotName)) {
1026      SnapshotDescription snapshotDesc =
1027        SnapshotDescriptionUtils.readSnapshotInfo(inputFs, snapshotDir)
1028          .toBuilder()
1029          .setName(targetName)
1030          .build();
1031      SnapshotDescriptionUtils.writeSnapshotInfo(snapshotDesc, initialOutputSnapshotDir, outputFs);
1032      if (filesUser != null || filesGroup != null) {
1033        outputFs.setOwner(new Path(initialOutputSnapshotDir,
1034          SnapshotDescriptionUtils.SNAPSHOTINFO_FILE), filesUser, filesGroup);
1035      }
1036      if (filesMode > 0) {
1037        outputFs.setPermission(new Path(initialOutputSnapshotDir,
1038          SnapshotDescriptionUtils.SNAPSHOTINFO_FILE), new FsPermission((short)filesMode));
1039      }
1040    }
1041
1042    // Step 2 - Start MR Job to copy files
1043    // The snapshot references must be copied before the files otherwise the files gets removed
1044    // by the HFileArchiver, since they have no references.
1045    try {
1046      runCopyJob(inputRoot, outputRoot, snapshotName, snapshotDir, verifyChecksum,
1047                 filesUser, filesGroup, filesMode, mappers, bandwidthMB);
1048
1049      LOG.info("Finalize the Snapshot Export");
1050      if (!skipTmp) {
1051        // Step 3 - Rename fs2:/.snapshot/.tmp/<snapshot> fs2:/.snapshot/<snapshot>
1052        if (!outputFs.rename(snapshotTmpDir, outputSnapshotDir)) {
1053          throw new ExportSnapshotException("Unable to rename snapshot directory from=" +
1054            snapshotTmpDir + " to=" + outputSnapshotDir);
1055        }
1056      }
1057
1058      // Step 4 - Verify snapshot integrity
1059      if (verifyTarget) {
1060        LOG.info("Verify snapshot integrity");
1061        verifySnapshot(destConf, outputFs, outputRoot, outputSnapshotDir);
1062      }
1063
1064      LOG.info("Export Completed: " + targetName);
1065      return 0;
1066    } catch (Exception e) {
1067      LOG.error("Snapshot export failed", e);
1068      if (!skipTmp) {
1069        outputFs.delete(snapshotTmpDir, true);
1070      }
1071      outputFs.delete(outputSnapshotDir, true);
1072      return 1;
1073    } finally {
1074      IOUtils.closeStream(inputFs);
1075      IOUtils.closeStream(outputFs);
1076    }
1077  }
1078
1079  @Override
1080  protected void printUsage() {
1081    super.printUsage();
1082    System.out.println("\n"
1083        + "Examples:\n"
1084        + "  hbase snapshot export \\\n"
1085        + "    --snapshot MySnapshot --copy-to hdfs://srv2:8082/hbase \\\n"
1086        + "    --chuser MyUser --chgroup MyGroup --chmod 700 --mappers 16\n"
1087        + "\n"
1088        + "  hbase snapshot export \\\n"
1089        + "    --snapshot MySnapshot --copy-from hdfs://srv2:8082/hbase \\\n"
1090        + "    --copy-to hdfs://srv1:50070/hbase");
1091  }
1092
1093  @Override protected void addOptions() {
1094    addRequiredOption(Options.SNAPSHOT);
1095    addOption(Options.COPY_TO);
1096    addOption(Options.COPY_FROM);
1097    addOption(Options.TARGET_NAME);
1098    addOption(Options.NO_CHECKSUM_VERIFY);
1099    addOption(Options.NO_TARGET_VERIFY);
1100    addOption(Options.OVERWRITE);
1101    addOption(Options.CHUSER);
1102    addOption(Options.CHGROUP);
1103    addOption(Options.CHMOD);
1104    addOption(Options.MAPPERS);
1105    addOption(Options.BANDWIDTH);
1106  }
1107
1108  public static void main(String[] args) {
1109    new ExportSnapshot().doStaticMain(args);
1110  }
1111}