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 static org.apache.hadoop.util.ToolRunner.run; 021import static org.junit.Assert.assertEquals; 022import static org.junit.Assert.assertFalse; 023import static org.junit.Assert.assertTrue; 024 025import java.io.IOException; 026import java.util.ArrayList; 027import java.util.HashMap; 028import java.util.HashSet; 029import java.util.List; 030import java.util.Map; 031import java.util.Objects; 032import java.util.Optional; 033import java.util.Set; 034import java.util.stream.Collectors; 035import org.apache.hadoop.conf.Configuration; 036import org.apache.hadoop.fs.FileStatus; 037import org.apache.hadoop.fs.FileSystem; 038import org.apache.hadoop.fs.Path; 039import org.apache.hadoop.hbase.HBaseClassTestRule; 040import org.apache.hadoop.hbase.HBaseTestingUtility; 041import org.apache.hadoop.hbase.HConstants; 042import org.apache.hadoop.hbase.TableName; 043import org.apache.hadoop.hbase.client.Admin; 044import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 045import org.apache.hadoop.hbase.client.Put; 046import org.apache.hadoop.hbase.client.RegionInfo; 047import org.apache.hadoop.hbase.client.Table; 048import org.apache.hadoop.hbase.client.TableDescriptorBuilder; 049import org.apache.hadoop.hbase.master.snapshot.SnapshotManager; 050import org.apache.hadoop.hbase.regionserver.StoreFileInfo; 051import org.apache.hadoop.hbase.testclassification.LargeTests; 052import org.apache.hadoop.hbase.testclassification.VerySlowMapReduceTests; 053import org.apache.hadoop.hbase.util.Bytes; 054import org.apache.hadoop.hbase.util.CommonFSUtils; 055import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; 056import org.apache.hadoop.hbase.util.Pair; 057import org.junit.After; 058import org.junit.AfterClass; 059import org.junit.Before; 060import org.junit.BeforeClass; 061import org.junit.ClassRule; 062import org.junit.Ignore; 063import org.junit.Rule; 064import org.junit.Test; 065import org.junit.experimental.categories.Category; 066import org.junit.rules.TestName; 067import org.slf4j.Logger; 068import org.slf4j.LoggerFactory; 069 070import org.apache.hbase.thirdparty.com.google.common.collect.Lists; 071 072import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotDescription; 073import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotRegionManifest; 074 075/** 076 * Test Export Snapshot Tool 077 */ 078@Ignore // HBASE-24493 079@Category({ VerySlowMapReduceTests.class, LargeTests.class }) 080public class TestExportSnapshot { 081 082 @ClassRule 083 public static final HBaseClassTestRule CLASS_RULE = 084 HBaseClassTestRule.forClass(TestExportSnapshot.class); 085 086 private static final Logger LOG = LoggerFactory.getLogger(TestExportSnapshot.class); 087 088 protected final static HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility(); 089 090 protected final static byte[] FAMILY = Bytes.toBytes("cf"); 091 092 @Rule 093 public final TestName testName = new TestName(); 094 095 protected TableName tableName; 096 private byte[] emptySnapshotName; 097 private byte[] snapshotName; 098 private int tableNumFiles; 099 private Admin admin; 100 101 public static void setUpBaseConf(Configuration conf) { 102 conf.setBoolean(SnapshotManager.HBASE_SNAPSHOT_ENABLED, true); 103 conf.setInt("hbase.regionserver.msginterval", 100); 104 // If a single node has enough failures (default 3), resource manager will blacklist it. 105 // With only 2 nodes and tests injecting faults, we don't want that. 106 conf.setInt("mapreduce.job.maxtaskfailures.per.tracker", 100); 107 } 108 109 @BeforeClass 110 public static void setUpBeforeClass() throws Exception { 111 setUpBaseConf(TEST_UTIL.getConfiguration()); 112 TEST_UTIL.startMiniCluster(1); 113 TEST_UTIL.startMiniMapReduceCluster(); 114 } 115 116 @AfterClass 117 public static void tearDownAfterClass() throws Exception { 118 TEST_UTIL.shutdownMiniMapReduceCluster(); 119 TEST_UTIL.shutdownMiniCluster(); 120 } 121 122 /** 123 * Create a table and take a snapshot of the table used by the export test. 124 */ 125 @Before 126 public void setUp() throws Exception { 127 this.admin = TEST_UTIL.getAdmin(); 128 129 tableName = TableName.valueOf("testtb-" + testName.getMethodName()); 130 snapshotName = Bytes.toBytes("snaptb0-" + testName.getMethodName()); 131 emptySnapshotName = Bytes.toBytes("emptySnaptb0-" + testName.getMethodName()); 132 133 // create Table 134 createTable(this.tableName); 135 136 // Take an empty snapshot 137 admin.snapshot(emptySnapshotName, tableName); 138 139 // Add some rows 140 SnapshotTestingUtils.loadData(TEST_UTIL, tableName, 50, FAMILY); 141 tableNumFiles = admin.getTableRegions(tableName).size(); 142 143 // take a snapshot 144 admin.snapshot(snapshotName, tableName); 145 } 146 147 protected void createTable(TableName tableName) throws Exception { 148 SnapshotTestingUtils.createPreSplitTable(TEST_UTIL, tableName, 2, FAMILY); 149 } 150 151 protected interface RegionPredicate { 152 boolean evaluate(final RegionInfo regionInfo); 153 } 154 155 protected RegionPredicate getBypassRegionPredicate() { 156 return null; 157 } 158 159 @After 160 public void tearDown() throws Exception { 161 TEST_UTIL.deleteTable(tableName); 162 SnapshotTestingUtils.deleteAllSnapshots(TEST_UTIL.getAdmin()); 163 SnapshotTestingUtils.deleteArchiveDirectory(TEST_UTIL); 164 } 165 166 /** 167 * Verify if exported snapshot and copied files matches the original one. 168 */ 169 @Test 170 public void testExportFileSystemState() throws Exception { 171 testExportFileSystemState(tableName, snapshotName, snapshotName, tableNumFiles); 172 } 173 174 @Test 175 public void testExportFileSystemStateWithMergeRegion() throws Exception { 176 // disable compaction 177 admin.compactionSwitch(false, 178 admin.getRegionServers().stream().map(a -> a.getServerName()).collect(Collectors.toList())); 179 // create Table 180 TableName tableName0 = TableName.valueOf("testtb-" + testName.getMethodName() + "-1"); 181 byte[] snapshotName0 = Bytes.toBytes("snaptb0-" + testName.getMethodName() + "-1"); 182 admin.createTable( 183 TableDescriptorBuilder.newBuilder(tableName0) 184 .setColumnFamilies( 185 Lists.newArrayList(ColumnFamilyDescriptorBuilder.newBuilder(FAMILY).build())) 186 .build(), 187 new byte[][] { Bytes.toBytes("2") }); 188 // put some data 189 try (Table table = admin.getConnection().getTable(tableName0)) { 190 table.put(new Put(Bytes.toBytes("1")).addColumn(FAMILY, null, Bytes.toBytes("1"))); 191 table.put(new Put(Bytes.toBytes("2")).addColumn(FAMILY, null, Bytes.toBytes("2"))); 192 } 193 List<RegionInfo> regions = admin.getRegions(tableName0); 194 assertEquals(2, regions.size()); 195 tableNumFiles = regions.size(); 196 // merge region 197 admin.mergeRegionsAsync(new byte[][] { regions.get(0).getEncodedNameAsBytes(), 198 regions.get(1).getEncodedNameAsBytes() }, true).get(); 199 // take a snapshot 200 admin.snapshot(snapshotName0, tableName0); 201 // export snapshot and verify 202 testExportFileSystemState(tableName0, snapshotName0, snapshotName0, tableNumFiles); 203 // delete table 204 TEST_UTIL.deleteTable(tableName0); 205 } 206 207 @Test 208 public void testExportFileSystemStateWithSkipTmp() throws Exception { 209 TEST_UTIL.getConfiguration().setBoolean(ExportSnapshot.CONF_SKIP_TMP, true); 210 try { 211 testExportFileSystemState(tableName, snapshotName, snapshotName, tableNumFiles); 212 } finally { 213 TEST_UTIL.getConfiguration().setBoolean(ExportSnapshot.CONF_SKIP_TMP, false); 214 } 215 } 216 217 @Test 218 public void testEmptyExportFileSystemState() throws Exception { 219 testExportFileSystemState(tableName, emptySnapshotName, emptySnapshotName, 0); 220 } 221 222 @Test 223 public void testConsecutiveExports() throws Exception { 224 Path copyDir = getLocalDestinationDir(TEST_UTIL); 225 testExportFileSystemState(tableName, snapshotName, snapshotName, tableNumFiles, copyDir, false); 226 testExportFileSystemState(tableName, snapshotName, snapshotName, tableNumFiles, copyDir, true); 227 removeExportDir(copyDir); 228 } 229 230 @Test 231 public void testExportWithTargetName() throws Exception { 232 final byte[] targetName = Bytes.toBytes("testExportWithTargetName"); 233 testExportFileSystemState(tableName, snapshotName, targetName, tableNumFiles); 234 } 235 236 @Test 237 public void testExportWithResetTtl() throws Exception { 238 String name = "testExportWithResetTtl"; 239 TableName tableName = TableName.valueOf(name); 240 String snapshotNameStr = "snaptb-" + name; 241 byte[] snapshotName = Bytes.toBytes(snapshotNameStr); 242 Long ttl = 100000L; 243 244 try { 245 // create Table 246 createTable(tableName); 247 SnapshotTestingUtils.loadData(TEST_UTIL, tableName, 50, FAMILY); 248 int tableNumFiles = admin.getRegions(tableName).size(); 249 // take a snapshot with TTL 250 Map<String, Object> props = new HashMap<>(); 251 props.put("TTL", ttl); 252 admin.snapshot(snapshotNameStr, tableName, props); 253 Optional<Long> ttlOpt = 254 admin.listSnapshots().stream().filter(s -> s.getName().equals(snapshotNameStr)) 255 .map(org.apache.hadoop.hbase.client.SnapshotDescription::getTtl).findAny(); 256 assertTrue(ttlOpt.isPresent()); 257 assertEquals(ttl, ttlOpt.get()); 258 259 testExportFileSystemState(tableName, snapshotName, snapshotName, tableNumFiles, 260 getHdfsDestinationDir(), false, true); 261 } catch (Exception e) { 262 throw e; 263 } finally { 264 TEST_UTIL.deleteTable(tableName); 265 } 266 } 267 268 private void testExportFileSystemState(final TableName tableName, final byte[] snapshotName, 269 final byte[] targetName, int filesExpected) throws Exception { 270 testExportFileSystemState(tableName, snapshotName, targetName, filesExpected, 271 getHdfsDestinationDir(), false); 272 } 273 274 protected void testExportFileSystemState(final TableName tableName, final byte[] snapshotName, 275 final byte[] targetName, int filesExpected, Path copyDir, boolean overwrite) throws Exception { 276 testExportFileSystemState(tableName, snapshotName, targetName, filesExpected, copyDir, 277 overwrite, false); 278 } 279 280 protected void testExportFileSystemState(final TableName tableName, final byte[] snapshotName, 281 final byte[] targetName, int filesExpected, Path copyDir, boolean overwrite, boolean resetTtl) 282 throws Exception { 283 testExportFileSystemState(TEST_UTIL.getConfiguration(), tableName, snapshotName, targetName, 284 filesExpected, TEST_UTIL.getDefaultRootDirPath(), copyDir, overwrite, resetTtl, 285 getBypassRegionPredicate(), true); 286 } 287 288 /** 289 * Creates destination directory, runs ExportSnapshot() tool, and runs some verifications. 290 */ 291 protected static void testExportFileSystemState(final Configuration conf, 292 final TableName tableName, final byte[] snapshotName, final byte[] targetName, 293 final int filesExpected, final Path srcDir, Path rawTgtDir, final boolean overwrite, 294 final boolean resetTtl, final RegionPredicate bypassregionPredicate, boolean success) 295 throws Exception { 296 FileSystem tgtFs = rawTgtDir.getFileSystem(conf); 297 FileSystem srcFs = srcDir.getFileSystem(conf); 298 Path tgtDir = rawTgtDir.makeQualified(tgtFs.getUri(), tgtFs.getWorkingDirectory()); 299 LOG.info("tgtFsUri={}, tgtDir={}, rawTgtDir={}, srcFsUri={}, srcDir={}", tgtFs.getUri(), tgtDir, 300 rawTgtDir, srcFs.getUri(), srcDir); 301 List<String> opts = new ArrayList<>(); 302 opts.add("--snapshot"); 303 opts.add(Bytes.toString(snapshotName)); 304 opts.add("--copy-to"); 305 opts.add(tgtDir.toString()); 306 if (targetName != snapshotName) { 307 opts.add("--target"); 308 opts.add(Bytes.toString(targetName)); 309 } 310 if (overwrite) { 311 opts.add("--overwrite"); 312 } 313 if (resetTtl) { 314 opts.add("--reset-ttl"); 315 } 316 317 // Export Snapshot 318 int res = run(conf, new ExportSnapshot(), opts.toArray(new String[opts.size()])); 319 assertEquals("success " + success + ", res=" + res, success ? 0 : 1, res); 320 if (!success) { 321 final Path targetDir = new Path(HConstants.SNAPSHOT_DIR_NAME, Bytes.toString(targetName)); 322 assertFalse(tgtDir.toString() + " " + targetDir.toString(), 323 tgtFs.exists(new Path(tgtDir, targetDir))); 324 return; 325 } 326 LOG.info("Exported snapshot"); 327 328 // Verify File-System state 329 FileStatus[] rootFiles = tgtFs.listStatus(tgtDir); 330 assertEquals(filesExpected > 0 ? 2 : 1, rootFiles.length); 331 for (FileStatus fileStatus : rootFiles) { 332 String name = fileStatus.getPath().getName(); 333 assertTrue(fileStatus.toString(), fileStatus.isDirectory()); 334 assertTrue(name.toString(), name.equals(HConstants.SNAPSHOT_DIR_NAME) 335 || name.equals(HConstants.HFILE_ARCHIVE_DIRECTORY)); 336 } 337 LOG.info("Verified filesystem state"); 338 339 // Compare the snapshot metadata and verify the hfiles 340 final Path snapshotDir = new Path(HConstants.SNAPSHOT_DIR_NAME, Bytes.toString(snapshotName)); 341 final Path targetDir = new Path(HConstants.SNAPSHOT_DIR_NAME, Bytes.toString(targetName)); 342 verifySnapshotDir(srcFs, new Path(srcDir, snapshotDir), tgtFs, new Path(tgtDir, targetDir)); 343 Set<String> snapshotFiles = verifySnapshot(conf, tgtFs, tgtDir, tableName, 344 Bytes.toString(targetName), resetTtl, bypassregionPredicate); 345 assertEquals(filesExpected, snapshotFiles.size()); 346 } 347 348 /* 349 * verify if the snapshot folder on file-system 1 match the one on file-system 2 350 */ 351 protected static void verifySnapshotDir(final FileSystem fs1, final Path root1, 352 final FileSystem fs2, final Path root2) throws IOException { 353 assertEquals(listFiles(fs1, root1, root1), listFiles(fs2, root2, root2)); 354 } 355 356 /* 357 * Verify if the files exists 358 */ 359 protected static Set<String> verifySnapshot(final Configuration conf, final FileSystem fs, 360 final Path rootDir, final TableName tableName, final String snapshotName, 361 final boolean resetTtl, final RegionPredicate bypassregionPredicate) throws IOException { 362 final Path exportedSnapshot = 363 new Path(rootDir, new Path(HConstants.SNAPSHOT_DIR_NAME, snapshotName)); 364 final Set<String> snapshotFiles = new HashSet<>(); 365 final Path exportedArchive = new Path(rootDir, HConstants.HFILE_ARCHIVE_DIRECTORY); 366 SnapshotReferenceUtil.visitReferencedFiles(conf, fs, exportedSnapshot, 367 new SnapshotReferenceUtil.SnapshotVisitor() { 368 @Override 369 public void storeFile(final RegionInfo regionInfo, final String family, 370 final SnapshotRegionManifest.StoreFile storeFile) throws IOException { 371 if (bypassregionPredicate != null && bypassregionPredicate.evaluate(regionInfo)) { 372 return; 373 } 374 375 if (!storeFile.hasReference() && !StoreFileInfo.isReference(storeFile.getName())) { 376 String hfile = storeFile.getName(); 377 snapshotFiles.add(hfile); 378 verifyNonEmptyFile(new Path(exportedArchive, 379 new Path(CommonFSUtils.getTableDir(new Path("./"), tableName), 380 new Path(regionInfo.getEncodedName(), new Path(family, hfile))))); 381 } else { 382 Pair<String, String> referredToRegionAndFile = 383 StoreFileInfo.getReferredToRegionAndFile(storeFile.getName()); 384 String region = referredToRegionAndFile.getFirst(); 385 String hfile = referredToRegionAndFile.getSecond(); 386 snapshotFiles.add(hfile); 387 verifyNonEmptyFile(new Path(exportedArchive, 388 new Path(CommonFSUtils.getTableDir(new Path("./"), tableName), 389 new Path(region, new Path(family, hfile))))); 390 } 391 } 392 393 private void verifyNonEmptyFile(final Path path) throws IOException { 394 assertTrue(path + " should exists", fs.exists(path)); 395 assertTrue(path + " should not be empty", fs.getFileStatus(path).getLen() > 0); 396 } 397 }); 398 399 // Verify Snapshot description 400 SnapshotDescription desc = SnapshotDescriptionUtils.readSnapshotInfo(fs, exportedSnapshot); 401 assertTrue(desc.getName().equals(snapshotName)); 402 assertTrue(desc.getTable().equals(tableName.getNameAsString())); 403 if (resetTtl) { 404 assertEquals(HConstants.DEFAULT_SNAPSHOT_TTL, desc.getTtl()); 405 } 406 return snapshotFiles; 407 } 408 409 private static Set<String> listFiles(final FileSystem fs, final Path root, final Path dir) 410 throws IOException { 411 Set<String> files = new HashSet<>(); 412 LOG.debug("List files in {} in root {} at {}", fs, root, dir); 413 int rootPrefix = root.makeQualified(fs.getUri(), fs.getWorkingDirectory()).toString().length(); 414 FileStatus[] list = CommonFSUtils.listStatus(fs, dir); 415 if (list != null) { 416 for (FileStatus fstat : list) { 417 LOG.debug(Objects.toString(fstat.getPath())); 418 if (fstat.isDirectory()) { 419 files.addAll(listFiles(fs, root, fstat.getPath())); 420 } else { 421 files.add(fstat.getPath().makeQualified(fs).toString().substring(rootPrefix)); 422 } 423 } 424 } 425 return files; 426 } 427 428 private Path getHdfsDestinationDir() { 429 Path rootDir = TEST_UTIL.getHBaseCluster().getMaster().getMasterFileSystem().getRootDir(); 430 Path path = 431 new Path(new Path(rootDir, "export-test"), "export-" + EnvironmentEdgeManager.currentTime()); 432 LOG.info("HDFS export destination path: " + path); 433 return path; 434 } 435 436 static Path getLocalDestinationDir(HBaseTestingUtility htu) { 437 Path path = htu.getDataTestDir("local-export-" + EnvironmentEdgeManager.currentTime()); 438 try { 439 FileSystem fs = FileSystem.getLocal(htu.getConfiguration()); 440 LOG.info("Local export destination path: " + path); 441 return path.makeQualified(fs.getUri(), fs.getWorkingDirectory()); 442 } catch (IOException ioe) { 443 throw new RuntimeException(ioe); 444 } 445 } 446 447 private static void removeExportDir(final Path path) throws IOException { 448 FileSystem fs = FileSystem.get(path.toUri(), new Configuration()); 449 fs.delete(path, true); 450 } 451}