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