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