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