View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  package org.apache.hadoop.hbase.snapshot;
20  
21  import static org.junit.Assert.assertEquals;
22  import static org.junit.Assert.assertTrue;
23  
24  import java.io.IOException;
25  import java.net.URI;
26  import java.util.ArrayList;
27  import java.util.Arrays;
28  import java.util.HashSet;
29  import java.util.List;
30  import java.util.Set;
31  
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  import org.apache.hadoop.conf.Configuration;
35  import org.apache.hadoop.fs.FSDataOutputStream;
36  import org.apache.hadoop.fs.FileStatus;
37  import org.apache.hadoop.fs.FileSystem;
38  import org.apache.hadoop.fs.FileUtil;
39  import org.apache.hadoop.fs.Path;
40  import org.apache.hadoop.hbase.HBaseTestingUtility;
41  import org.apache.hadoop.hbase.HColumnDescriptor;
42  import org.apache.hadoop.hbase.HConstants;
43  import org.apache.hadoop.hbase.HRegionInfo;
44  import org.apache.hadoop.hbase.HTableDescriptor;
45  import org.apache.hadoop.hbase.MediumTests;
46  import org.apache.hadoop.hbase.TableName;
47  import org.apache.hadoop.hbase.client.HBaseAdmin;
48  import org.apache.hadoop.hbase.client.HTable;
49  import org.apache.hadoop.hbase.master.snapshot.SnapshotManager;
50  import org.apache.hadoop.hbase.protobuf.generated.HBaseProtos.SnapshotDescription;
51  import org.apache.hadoop.hbase.regionserver.HRegionFileSystem;
52  import org.apache.hadoop.hbase.util.Bytes;
53  import org.apache.hadoop.hbase.util.FSUtils;
54  import org.apache.hadoop.hbase.util.Pair;
55  import org.junit.After;
56  import org.junit.AfterClass;
57  import org.junit.Before;
58  import org.junit.BeforeClass;
59  import org.junit.Test;
60  import org.junit.experimental.categories.Category;
61  
62  /**
63   * Test Export Snapshot Tool
64   */
65  @Category(MediumTests.class)
66  public class TestExportSnapshot {
67    private final Log LOG = LogFactory.getLog(getClass());
68  
69    protected final static HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
70  
71    private final static byte[] FAMILY = Bytes.toBytes("cf");
72  
73    private byte[] emptySnapshotName;
74    private byte[] snapshotName;
75    private TableName tableName;
76    private HBaseAdmin admin;
77  
78    public static void setUpBaseConf(Configuration conf) {
79      conf.setBoolean(SnapshotManager.HBASE_SNAPSHOT_ENABLED, true);
80      conf.setInt("hbase.regionserver.msginterval", 100);
81      conf.setInt("hbase.client.pause", 250);
82      conf.setInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER, 6);
83      conf.setBoolean("hbase.master.enabletable.roundrobin", true);
84      conf.setInt("mapreduce.map.max.attempts", 10);
85      conf.setInt("mapred.map.max.attempts", 10);
86    }
87  
88    @BeforeClass
89    public static void setUpBeforeClass() throws Exception {
90      setUpBaseConf(TEST_UTIL.getConfiguration());
91      TEST_UTIL.startMiniCluster(3);
92      TEST_UTIL.startMiniMapReduceCluster();
93    }
94  
95    @AfterClass
96    public static void tearDownAfterClass() throws Exception {
97      TEST_UTIL.shutdownMiniMapReduceCluster();
98      TEST_UTIL.shutdownMiniCluster();
99    }
100 
101   /**
102    * Create a table and take a snapshot of the table used by the export test.
103    */
104   @Before
105   public void setUp() throws Exception {
106     this.admin = TEST_UTIL.getHBaseAdmin();
107 
108     long tid = System.currentTimeMillis();
109     tableName = TableName.valueOf("testtb-" + tid);
110     snapshotName = Bytes.toBytes("snaptb0-" + tid);
111     emptySnapshotName = Bytes.toBytes("emptySnaptb0-" + tid);
112 
113     // create Table
114     SnapshotTestingUtils.createTable(TEST_UTIL, tableName, FAMILY);
115 
116     // Take an empty snapshot
117     admin.snapshot(emptySnapshotName, tableName);
118 
119     // Add some rows
120     HTable table = new HTable(TEST_UTIL.getConfiguration(), tableName);
121     SnapshotTestingUtils.loadData(TEST_UTIL, tableName, 500, FAMILY);
122 
123     // take a snapshot
124     admin.snapshot(snapshotName, tableName);
125   }
126 
127   @After
128   public void tearDown() throws Exception {
129     TEST_UTIL.deleteTable(tableName);
130     SnapshotTestingUtils.deleteAllSnapshots(TEST_UTIL.getHBaseAdmin());
131     SnapshotTestingUtils.deleteArchiveDirectory(TEST_UTIL);
132   }
133 
134 
135   /**
136    * Verfy the result of getBalanceSplits() method.
137    * The result are groups of files, used as input list for the "export" mappers.
138    * All the groups should have similar amount of data.
139    *
140    * The input list is a pair of file path and length.
141    * The getBalanceSplits() function sort it by length,
142    * and assign to each group a file, going back and forth through the groups.
143    */
144   @Test
145   public void testBalanceSplit() throws Exception {
146     // Create a list of files
147     List<Pair<Path, Long>> files = new ArrayList<Pair<Path, Long>>();
148     for (long i = 0; i <= 20; i++) {
149       files.add(new Pair<Path, Long>(new Path("file-" + i), i));
150     }
151 
152     // Create 5 groups (total size 210)
153     //    group 0: 20, 11, 10,  1 (total size: 42)
154     //    group 1: 19, 12,  9,  2 (total size: 42)
155     //    group 2: 18, 13,  8,  3 (total size: 42)
156     //    group 3: 17, 12,  7,  4 (total size: 42)
157     //    group 4: 16, 11,  6,  5 (total size: 42)
158     List<List<Path>> splits = ExportSnapshot.getBalancedSplits(files, 5);
159     assertEquals(5, splits.size());
160     assertEquals(Arrays.asList(new Path("file-20"), new Path("file-11"),
161       new Path("file-10"), new Path("file-1"), new Path("file-0")), splits.get(0));
162     assertEquals(Arrays.asList(new Path("file-19"), new Path("file-12"),
163       new Path("file-9"), new Path("file-2")), splits.get(1));
164     assertEquals(Arrays.asList(new Path("file-18"), new Path("file-13"),
165       new Path("file-8"), new Path("file-3")), splits.get(2));
166     assertEquals(Arrays.asList(new Path("file-17"), new Path("file-14"),
167       new Path("file-7"), new Path("file-4")), splits.get(3));
168     assertEquals(Arrays.asList(new Path("file-16"), new Path("file-15"),
169       new Path("file-6"), new Path("file-5")), splits.get(4));
170   }
171 
172   /**
173    * Verify if exported snapshot and copied files matches the original one.
174    */
175   @Test
176   public void testExportFileSystemState() throws Exception {
177     testExportFileSystemState(tableName, snapshotName, snapshotName, 2);
178   }
179 
180   @Test
181   public void testExportFileSystemStateWithSkipTmp() throws Exception {
182     TEST_UTIL.getConfiguration().setBoolean(ExportSnapshot.CONF_SKIP_TMP, true);
183     testExportFileSystemState(tableName, snapshotName, snapshotName, 2);
184   }
185 
186   @Test
187   public void testEmptyExportFileSystemState() throws Exception {
188     testExportFileSystemState(tableName, emptySnapshotName, emptySnapshotName, 1);
189   }
190 
191   @Test
192   public void testConsecutiveExports() throws Exception {
193     Path copyDir = getLocalDestinationDir();
194     testExportFileSystemState(tableName, snapshotName, snapshotName, 2, copyDir, false);
195     testExportFileSystemState(tableName, snapshotName, snapshotName, 2, copyDir, true);
196     removeExportDir(copyDir);
197   }
198 
199   @Test
200   public void testExportWithTargetName() throws Exception {
201     final byte[] targetName = Bytes.toBytes("testExportWithTargetName");
202     testExportFileSystemState(tableName, snapshotName, targetName, 2);
203   }
204 
205   /**
206    * Mock a snapshot with files in the archive dir,
207    * two regions, and one reference file.
208    */
209   @Test
210   public void testSnapshotWithRefsExportFileSystemState() throws Exception {
211     Configuration conf = TEST_UTIL.getConfiguration();
212 
213     final TableName tableWithRefsName =
214         TableName.valueOf("tableWithRefs");
215     final String snapshotName = "tableWithRefs";
216     final String TEST_FAMILY = Bytes.toString(FAMILY);
217     final String TEST_HFILE = "abc";
218 
219     final SnapshotDescription sd = SnapshotDescription.newBuilder()
220         .setName(snapshotName)
221         .setTable(tableWithRefsName.getNameAsString()).build();
222 
223     FileSystem fs = TEST_UTIL.getHBaseCluster().getMaster().getMasterFileSystem().getFileSystem();
224     Path rootDir = TEST_UTIL.getHBaseCluster().getMaster().getMasterFileSystem().getRootDir();
225     Path archiveDir = new Path(rootDir, HConstants.HFILE_ARCHIVE_DIRECTORY);
226 
227     // First region, simple with one plain hfile.
228     HRegionInfo hri = new HRegionInfo(tableWithRefsName);
229     HRegionFileSystem r0fs = HRegionFileSystem.createRegionOnFileSystem(conf,
230       fs, FSUtils.getTableDir(archiveDir, hri.getTable()), hri);
231     Path storeFile = new Path(rootDir, TEST_HFILE);
232     FSDataOutputStream out = fs.create(storeFile);
233     out.write(Bytes.toBytes("Test Data"));
234     out.close();
235     r0fs.commitStoreFile(TEST_FAMILY, storeFile);
236 
237     // Second region, used to test the split case.
238     // This region contains a reference to the hfile in the first region.
239     hri = new HRegionInfo(tableWithRefsName);
240     HRegionFileSystem r1fs = HRegionFileSystem.createRegionOnFileSystem(conf,
241       fs, new Path(archiveDir, hri.getTable().getNameAsString()), hri);
242     storeFile = new Path(rootDir, TEST_HFILE + '.' + r0fs.getRegionInfo().getEncodedName());
243     out = fs.create(storeFile);
244     out.write(Bytes.toBytes("Test Data"));
245     out.close();
246     r1fs.commitStoreFile(TEST_FAMILY, storeFile);
247 
248     Path tableDir = FSUtils.getTableDir(archiveDir, tableWithRefsName);
249     Path snapshotDir = SnapshotDescriptionUtils.getCompletedSnapshotDir(snapshotName, rootDir);
250     FileUtil.copy(fs, tableDir, fs, snapshotDir, false, conf);
251     SnapshotDescriptionUtils.writeSnapshotInfo(sd, snapshotDir, fs);
252 
253     byte[] name = Bytes.toBytes(snapshotName);
254     testExportFileSystemState(tableWithRefsName, name, name, 2);
255   }
256 
257   private void testExportFileSystemState(final TableName tableName, final byte[] snapshotName,
258       final byte[] targetName, int filesExpected) throws Exception {
259     Path copyDir = getHdfsDestinationDir();
260     testExportFileSystemState(tableName, snapshotName, targetName, filesExpected, copyDir, false);
261     removeExportDir(copyDir);
262   }
263 
264   /**
265    * Test ExportSnapshot
266    */
267   private void testExportFileSystemState(final TableName tableName, final byte[] snapshotName,
268       final byte[] targetName, int filesExpected, Path copyDir, boolean overwrite)
269       throws Exception {
270     URI hdfsUri = FileSystem.get(TEST_UTIL.getConfiguration()).getUri();
271     FileSystem fs = FileSystem.get(copyDir.toUri(), new Configuration());
272     copyDir = copyDir.makeQualified(fs);
273 
274     List<String> opts = new ArrayList<String>();
275     opts.add("-snapshot");
276     opts.add(Bytes.toString(snapshotName));
277     opts.add("-copy-to");
278     opts.add(copyDir.toString());
279     if (targetName != snapshotName) {
280       opts.add("-target");
281       opts.add(Bytes.toString(targetName));
282     }
283     if (overwrite) opts.add("-overwrite");
284 
285     // Export Snapshot
286     int res = ExportSnapshot.innerMain(TEST_UTIL.getConfiguration(),
287         opts.toArray(new String[opts.size()]));
288     assertEquals(0, res);
289 
290     // Verify File-System state
291     FileStatus[] rootFiles = fs.listStatus(copyDir);
292     assertEquals(filesExpected, rootFiles.length);
293     for (FileStatus fileStatus: rootFiles) {
294       String name = fileStatus.getPath().getName();
295       assertTrue(fileStatus.isDir());
296       assertTrue(name.equals(HConstants.SNAPSHOT_DIR_NAME) ||
297                  name.equals(HConstants.HFILE_ARCHIVE_DIRECTORY));
298     }
299 
300     // compare the snapshot metadata and verify the hfiles
301     final FileSystem hdfs = FileSystem.get(hdfsUri, TEST_UTIL.getConfiguration());
302     final Path snapshotDir = new Path(HConstants.SNAPSHOT_DIR_NAME, Bytes.toString(snapshotName));
303     final Path targetDir = new Path(HConstants.SNAPSHOT_DIR_NAME, Bytes.toString(targetName));
304     verifySnapshot(hdfs, new Path(TEST_UTIL.getDefaultRootDirPath(), snapshotDir),
305         fs, new Path(copyDir, targetDir));
306     verifyArchive(fs, copyDir, tableName, Bytes.toString(targetName));
307     FSUtils.logFileSystemState(hdfs, snapshotDir, LOG);
308   }
309 
310   /**
311    * Check that ExportSnapshot will return a failure if something fails.
312    */
313   @Test
314   public void testExportFailure() throws Exception {
315     assertEquals(1, runExportAndInjectFailures(snapshotName, false));
316   }
317 
318   /**
319    * Check that ExportSnapshot will succede if something fails but the retry succede.
320    */
321   @Test
322   public void testExportRetry() throws Exception {
323     assertEquals(0, runExportAndInjectFailures(snapshotName, true));
324   }
325 
326   /*
327    * Execute the ExportSnapshot job injecting failures
328    */
329   private int runExportAndInjectFailures(final byte[] snapshotName, boolean retry)
330       throws Exception {
331     Path copyDir = getLocalDestinationDir();
332     URI hdfsUri = FileSystem.get(TEST_UTIL.getConfiguration()).getUri();
333     FileSystem fs = FileSystem.get(copyDir.toUri(), new Configuration());
334     copyDir = copyDir.makeQualified(fs);
335 
336     Configuration conf = new Configuration(TEST_UTIL.getConfiguration());
337     conf.setBoolean(ExportSnapshot.CONF_TEST_FAILURE, true);
338     conf.setBoolean(ExportSnapshot.CONF_TEST_RETRY, retry);
339 
340     // Export Snapshot
341     Path sourceDir = TEST_UTIL.getHBaseCluster().getMaster().getMasterFileSystem().getRootDir();
342     int res = ExportSnapshot.innerMain(conf, new String[] {
343       "-snapshot", Bytes.toString(snapshotName),
344       "-copy-from", sourceDir.toString(),
345       "-copy-to", copyDir.toString()
346     });
347     return res;
348   }
349 
350   /*
351    * verify if the snapshot folder on file-system 1 match the one on file-system 2
352    */
353   private void verifySnapshot(final FileSystem fs1, final Path root1,
354       final FileSystem fs2, final Path root2) throws IOException {
355     Set<String> s = new HashSet<String>();
356     assertEquals(listFiles(fs1, root1, root1), listFiles(fs2, root2, root2));
357   }
358 
359   /*
360    * Verify if the files exists
361    */
362   private void verifyArchive(final FileSystem fs, final Path rootDir,
363       final TableName tableName, final String snapshotName) throws IOException {
364     final Path exportedSnapshot = new Path(rootDir,
365       new Path(HConstants.SNAPSHOT_DIR_NAME, snapshotName));
366     final Path exportedArchive = new Path(rootDir, HConstants.HFILE_ARCHIVE_DIRECTORY);
367     LOG.debug(listFiles(fs, exportedArchive, exportedArchive));
368     SnapshotReferenceUtil.visitReferencedFiles(fs, exportedSnapshot,
369         new SnapshotReferenceUtil.FileVisitor() {
370         public void storeFile (final String region, final String family, final String hfile)
371             throws IOException {
372           verifyNonEmptyFile(new Path(exportedArchive,
373             new Path(FSUtils.getTableDir(new Path("./"), tableName),
374                 new Path(region, new Path(family, hfile)))));
375         }
376 
377         public void recoveredEdits (final String region, final String logfile)
378             throws IOException {
379           verifyNonEmptyFile(new Path(exportedSnapshot,
380             new Path(tableName.getNameAsString(), new Path(region, logfile))));
381         }
382 
383         public void logFile (final String server, final String logfile)
384             throws IOException {
385           verifyNonEmptyFile(new Path(exportedSnapshot, new Path(server, logfile)));
386         }
387 
388         private void verifyNonEmptyFile(final Path path) throws IOException {
389           assertTrue(path + " should exists", fs.exists(path));
390           assertTrue(path + " should not be empty", fs.getFileStatus(path).getLen() > 0);
391         }
392     });
393 
394     // Verify Snapshot description
395     SnapshotDescription desc = SnapshotDescriptionUtils.readSnapshotInfo(fs, exportedSnapshot);
396     assertTrue(desc.getName().equals(snapshotName));
397     assertTrue(desc.getTable().equals(tableName.getNameAsString()));
398   }
399 
400   private Set<String> listFiles(final FileSystem fs, final Path root, final Path dir)
401       throws IOException {
402     Set<String> files = new HashSet<String>();
403     int rootPrefix = root.toString().length();
404     FileStatus[] list = FSUtils.listStatus(fs, dir);
405     if (list != null) {
406       for (FileStatus fstat: list) {
407         LOG.debug(fstat.getPath());
408         if (fstat.isDir()) {
409           files.addAll(listFiles(fs, root, fstat.getPath()));
410         } else {
411           files.add(fstat.getPath().toString().substring(rootPrefix));
412         }
413       }
414     }
415     return files;
416   }
417 
418   private Path getHdfsDestinationDir() {
419     Path rootDir = TEST_UTIL.getHBaseCluster().getMaster().getMasterFileSystem().getRootDir();
420     Path path = new Path(new Path(rootDir, "export-test"), "export-" + System.currentTimeMillis());
421     LOG.info("HDFS export destination path: " + path);
422     return path;
423   }
424 
425   private Path getLocalDestinationDir() {
426     Path path = TEST_UTIL.getDataTestDir("local-export-" + System.currentTimeMillis());
427     LOG.info("Local export destination path: " + path);
428     return path;
429   }
430 
431   private void removeExportDir(final Path path) throws IOException {
432     FileSystem fs = FileSystem.get(path.toUri(), new Configuration());
433     FSUtils.logFileSystemState(fs, path, LOG);
434     fs.delete(path, true);
435   }
436 }