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 package org.apache.hadoop.hdfs.server.namenode;
19 
20 import static org.junit.Assert.assertEquals;
21 import static org.junit.Assert.assertTrue;
22 
23 import java.io.File;
24 import java.io.IOException;
25 import java.util.ArrayList;
26 import java.util.EnumSet;
27 import java.util.List;
28 import java.util.Random;
29 
30 import org.apache.commons.logging.impl.Log4JLogger;
31 import org.apache.hadoop.conf.Configuration;
32 import org.apache.hadoop.fs.FSDataOutputStream;
33 import org.apache.hadoop.fs.FileStatus;
34 import org.apache.hadoop.fs.Path;
35 import org.apache.hadoop.fs.permission.FsPermission;
36 import org.apache.hadoop.hdfs.DFSTestUtil;
37 import org.apache.hadoop.hdfs.DFSUtil;
38 import org.apache.hadoop.hdfs.DistributedFileSystem;
39 import org.apache.hadoop.hdfs.MiniDFSCluster;
40 import org.apache.hadoop.hdfs.client.HdfsDataOutputStream;
41 import org.apache.hadoop.hdfs.client.HdfsDataOutputStream.SyncFlag;
42 import org.apache.hadoop.hdfs.protocol.HdfsConstants.SafeModeAction;
43 import org.apache.hadoop.hdfs.protocol.SnapshottableDirectoryStatus;
44 import org.apache.hadoop.hdfs.server.namenode.NNStorage.NameNodeFile;
45 import org.apache.hadoop.hdfs.server.namenode.snapshot.DirectoryWithSnapshotFeature.DirectoryDiff;
46 import org.apache.hadoop.hdfs.server.namenode.snapshot.Snapshot;
47 import org.apache.hadoop.hdfs.server.namenode.snapshot.SnapshotTestHelper;
48 import org.apache.hadoop.hdfs.util.Canceler;
49 import org.apache.log4j.Level;
50 import org.junit.After;
51 import org.junit.Assert;
52 import org.junit.Before;
53 import org.junit.Test;
54 
55 /**
56  * Test FSImage save/load when Snapshot is supported
57  */
58 public class TestFSImageWithSnapshot {
59   {
SnapshotTestHelper.disableLogs()60     SnapshotTestHelper.disableLogs();
61     ((Log4JLogger)INode.LOG).getLogger().setLevel(Level.ALL);
62   }
63 
64   static final long seed = 0;
65   static final short REPLICATION = 3;
66   static final int BLOCKSIZE = 1024;
67   static final long txid = 1;
68 
69   private final Path dir = new Path("/TestSnapshot");
70   private static final String testDir =
71       System.getProperty("test.build.data", "build/test/data");
72 
73   Configuration conf;
74   MiniDFSCluster cluster;
75   FSNamesystem fsn;
76   DistributedFileSystem hdfs;
77 
78   @Before
setUp()79   public void setUp() throws Exception {
80     conf = new Configuration();
81     cluster = new MiniDFSCluster.Builder(conf).numDataNodes(REPLICATION)
82         .build();
83     cluster.waitActive();
84     fsn = cluster.getNamesystem();
85     hdfs = cluster.getFileSystem();
86   }
87 
88   @After
tearDown()89   public void tearDown() throws Exception {
90     if (cluster != null) {
91       cluster.shutdown();
92     }
93   }
94 
95   /**
96    * Create a temp fsimage file for testing.
97    * @param dir The directory where the fsimage file resides
98    * @param imageTxId The transaction id of the fsimage
99    * @return The file of the image file
100    */
getImageFile(String dir, long imageTxId)101   private File getImageFile(String dir, long imageTxId) {
102     return new File(dir, String.format("%s_%019d", NameNodeFile.IMAGE,
103         imageTxId));
104   }
105 
106   /**
107    * Create a temp file for dumping the fsdir
108    * @param dir directory for the temp file
109    * @param suffix suffix of of the temp file
110    * @return the temp file
111    */
getDumpTreeFile(String dir, String suffix)112   private File getDumpTreeFile(String dir, String suffix) {
113     return new File(dir, String.format("dumpTree_%s", suffix));
114   }
115 
116   /**
117    * Dump the fsdir tree to a temp file
118    * @param fileSuffix suffix of the temp file for dumping
119    * @return the temp file
120    */
dumpTree2File(String fileSuffix)121   private File dumpTree2File(String fileSuffix) throws IOException {
122     File file = getDumpTreeFile(testDir, fileSuffix);
123     SnapshotTestHelper.dumpTree2File(fsn.getFSDirectory(), file);
124     return file;
125   }
126 
127   /** Append a file without closing the output stream */
appendFileWithoutClosing(Path file, int length)128   private HdfsDataOutputStream appendFileWithoutClosing(Path file, int length)
129       throws IOException {
130     byte[] toAppend = new byte[length];
131     Random random = new Random();
132     random.nextBytes(toAppend);
133     HdfsDataOutputStream out = (HdfsDataOutputStream) hdfs.append(file);
134     out.write(toAppend);
135     return out;
136   }
137 
138   /** Save the fsimage to a temp file */
saveFSImageToTempFile()139   private File saveFSImageToTempFile() throws IOException {
140     SaveNamespaceContext context = new SaveNamespaceContext(fsn, txid,
141         new Canceler());
142     FSImageFormatProtobuf.Saver saver = new FSImageFormatProtobuf.Saver(context);
143     FSImageCompression compression = FSImageCompression.createCompression(conf);
144     File imageFile = getImageFile(testDir, txid);
145     fsn.readLock();
146     try {
147       saver.save(imageFile, compression);
148     } finally {
149       fsn.readUnlock();
150     }
151     return imageFile;
152   }
153 
154   /** Load the fsimage from a temp file */
loadFSImageFromTempFile(File imageFile)155   private void loadFSImageFromTempFile(File imageFile) throws IOException {
156     FSImageFormat.LoaderDelegator loader = FSImageFormat.newLoader(conf, fsn);
157     fsn.writeLock();
158     fsn.getFSDirectory().writeLock();
159     try {
160       loader.load(imageFile, false);
161       FSImage.updateCountForQuota(fsn.getBlockManager().getStoragePolicySuite(),
162           INodeDirectory.valueOf(fsn.getFSDirectory().getINode("/"), "/"));
163     } finally {
164       fsn.getFSDirectory().writeUnlock();
165       fsn.writeUnlock();
166     }
167   }
168 
169   /**
170    * Test when there is snapshot taken on root
171    */
172   @Test
testSnapshotOnRoot()173   public void testSnapshotOnRoot() throws Exception {
174     final Path root = new Path("/");
175     hdfs.allowSnapshot(root);
176     hdfs.createSnapshot(root, "s1");
177 
178     cluster.shutdown();
179     cluster = new MiniDFSCluster.Builder(conf).format(false)
180         .numDataNodes(REPLICATION).build();
181     cluster.waitActive();
182     fsn = cluster.getNamesystem();
183     hdfs = cluster.getFileSystem();
184 
185     // save namespace and restart cluster
186     hdfs.setSafeMode(SafeModeAction.SAFEMODE_ENTER);
187     hdfs.saveNamespace();
188     hdfs.setSafeMode(SafeModeAction.SAFEMODE_LEAVE);
189     cluster.shutdown();
190     cluster = new MiniDFSCluster.Builder(conf).format(false)
191         .numDataNodes(REPLICATION).build();
192     cluster.waitActive();
193     fsn = cluster.getNamesystem();
194     hdfs = cluster.getFileSystem();
195 
196     INodeDirectory rootNode = fsn.dir.getINode4Write(root.toString())
197         .asDirectory();
198     assertTrue("The children list of root should be empty",
199         rootNode.getChildrenList(Snapshot.CURRENT_STATE_ID).isEmpty());
200     // one snapshot on root: s1
201     List<DirectoryDiff> diffList = rootNode.getDiffs().asList();
202     assertEquals(1, diffList.size());
203     Snapshot s1 = rootNode.getSnapshot(DFSUtil.string2Bytes("s1"));
204     assertEquals(s1.getId(), diffList.get(0).getSnapshotId());
205 
206     // check SnapshotManager's snapshottable directory list
207     assertEquals(1, fsn.getSnapshotManager().getNumSnapshottableDirs());
208     SnapshottableDirectoryStatus[] sdirs = fsn.getSnapshotManager()
209         .getSnapshottableDirListing(null);
210     assertEquals(root, sdirs[0].getFullPath());
211 
212     // save namespace and restart cluster
213     hdfs.setSafeMode(SafeModeAction.SAFEMODE_ENTER);
214     hdfs.saveNamespace();
215     hdfs.setSafeMode(SafeModeAction.SAFEMODE_LEAVE);
216     cluster.shutdown();
217     cluster = new MiniDFSCluster.Builder(conf).format(false)
218         .numDataNodes(REPLICATION).build();
219     cluster.waitActive();
220     fsn = cluster.getNamesystem();
221     hdfs = cluster.getFileSystem();
222   }
223 
224   /**
225    * Testing steps:
226    * <pre>
227    * 1. Creating/modifying directories/files while snapshots are being taken.
228    * 2. Dump the FSDirectory tree of the namesystem.
229    * 3. Save the namesystem to a temp file (FSImage saving).
230    * 4. Restart the cluster and format the namesystem.
231    * 5. Load the namesystem from the temp file (FSImage loading).
232    * 6. Dump the FSDirectory again and compare the two dumped string.
233    * </pre>
234    */
235   @Test
testSaveLoadImage()236   public void testSaveLoadImage() throws Exception {
237     int s = 0;
238     // make changes to the namesystem
239     hdfs.mkdirs(dir);
240     SnapshotTestHelper.createSnapshot(hdfs, dir, "s" + ++s);
241     Path sub1 = new Path(dir, "sub1");
242     hdfs.mkdirs(sub1);
243     hdfs.setPermission(sub1, new FsPermission((short)0777));
244     Path sub11 = new Path(sub1, "sub11");
245     hdfs.mkdirs(sub11);
246     checkImage(s);
247 
248     hdfs.createSnapshot(dir, "s" + ++s);
249     Path sub1file1 = new Path(sub1, "sub1file1");
250     Path sub1file2 = new Path(sub1, "sub1file2");
251     DFSTestUtil.createFile(hdfs, sub1file1, BLOCKSIZE, REPLICATION, seed);
252     DFSTestUtil.createFile(hdfs, sub1file2, BLOCKSIZE, REPLICATION, seed);
253     checkImage(s);
254 
255     hdfs.createSnapshot(dir, "s" + ++s);
256     Path sub2 = new Path(dir, "sub2");
257     Path sub2file1 = new Path(sub2, "sub2file1");
258     Path sub2file2 = new Path(sub2, "sub2file2");
259     DFSTestUtil.createFile(hdfs, sub2file1, BLOCKSIZE, REPLICATION, seed);
260     DFSTestUtil.createFile(hdfs, sub2file2, BLOCKSIZE, REPLICATION, seed);
261     checkImage(s);
262 
263     hdfs.createSnapshot(dir, "s" + ++s);
264     hdfs.setReplication(sub1file1, (short) (REPLICATION - 1));
265     hdfs.delete(sub1file2, true);
266     hdfs.setOwner(sub2, "dr.who", "unknown");
267     hdfs.delete(sub2file1, true);
268     checkImage(s);
269 
270     hdfs.createSnapshot(dir, "s" + ++s);
271     Path sub1_sub2file2 = new Path(sub1, "sub2file2");
272     hdfs.rename(sub2file2, sub1_sub2file2);
273 
274     hdfs.rename(sub1file1, sub2file1);
275     checkImage(s);
276 
277     hdfs.rename(sub2file1, sub2file2);
278     checkImage(s);
279   }
280 
checkImage(int s)281   void checkImage(int s) throws IOException {
282     final String name = "s" + s;
283 
284     // dump the fsdir tree
285     File fsnBefore = dumpTree2File(name + "_before");
286 
287     // save the namesystem to a temp file
288     File imageFile = saveFSImageToTempFile();
289 
290     long numSdirBefore = fsn.getNumSnapshottableDirs();
291     long numSnapshotBefore = fsn.getNumSnapshots();
292     SnapshottableDirectoryStatus[] dirBefore = hdfs.getSnapshottableDirListing();
293 
294     // shutdown the cluster
295     cluster.shutdown();
296 
297     // dump the fsdir tree
298     File fsnBetween = dumpTree2File(name + "_between");
299     SnapshotTestHelper.compareDumpedTreeInFile(fsnBefore, fsnBetween, true);
300 
301     // restart the cluster, and format the cluster
302     cluster = new MiniDFSCluster.Builder(conf).format(true)
303         .numDataNodes(REPLICATION).build();
304     cluster.waitActive();
305     fsn = cluster.getNamesystem();
306     hdfs = cluster.getFileSystem();
307 
308     // load the namesystem from the temp file
309     loadFSImageFromTempFile(imageFile);
310 
311     // dump the fsdir tree again
312     File fsnAfter = dumpTree2File(name + "_after");
313 
314     // compare two dumped tree
315     SnapshotTestHelper.compareDumpedTreeInFile(fsnBefore, fsnAfter, true);
316 
317     long numSdirAfter = fsn.getNumSnapshottableDirs();
318     long numSnapshotAfter = fsn.getNumSnapshots();
319     SnapshottableDirectoryStatus[] dirAfter = hdfs.getSnapshottableDirListing();
320 
321     Assert.assertEquals(numSdirBefore, numSdirAfter);
322     Assert.assertEquals(numSnapshotBefore, numSnapshotAfter);
323     Assert.assertEquals(dirBefore.length, dirAfter.length);
324     List<String> pathListBefore = new ArrayList<String>();
325     for (SnapshottableDirectoryStatus sBefore : dirBefore) {
326       pathListBefore.add(sBefore.getFullPath().toString());
327     }
328     for (SnapshottableDirectoryStatus sAfter : dirAfter) {
329       Assert.assertTrue(pathListBefore.contains(sAfter.getFullPath().toString()));
330     }
331   }
332 
333   /**
334    * Test the fsimage saving/loading while file appending.
335    */
336   @Test (timeout=60000)
testSaveLoadImageWithAppending()337   public void testSaveLoadImageWithAppending() throws Exception {
338     Path sub1 = new Path(dir, "sub1");
339     Path sub1file1 = new Path(sub1, "sub1file1");
340     Path sub1file2 = new Path(sub1, "sub1file2");
341     DFSTestUtil.createFile(hdfs, sub1file1, BLOCKSIZE, REPLICATION, seed);
342     DFSTestUtil.createFile(hdfs, sub1file2, BLOCKSIZE, REPLICATION, seed);
343 
344     // 1. create snapshot s0
345     hdfs.allowSnapshot(dir);
346     hdfs.createSnapshot(dir, "s0");
347 
348     // 2. create snapshot s1 before appending sub1file1 finishes
349     HdfsDataOutputStream out = appendFileWithoutClosing(sub1file1, BLOCKSIZE);
350     out.hsync(EnumSet.of(SyncFlag.UPDATE_LENGTH));
351     // also append sub1file2
352     DFSTestUtil.appendFile(hdfs, sub1file2, BLOCKSIZE);
353     hdfs.createSnapshot(dir, "s1");
354     out.close();
355 
356     // 3. create snapshot s2 before appending finishes
357     out = appendFileWithoutClosing(sub1file1, BLOCKSIZE);
358     out.hsync(EnumSet.of(SyncFlag.UPDATE_LENGTH));
359     hdfs.createSnapshot(dir, "s2");
360     out.close();
361 
362     // 4. save fsimage before appending finishes
363     out = appendFileWithoutClosing(sub1file1, BLOCKSIZE);
364     out.hsync(EnumSet.of(SyncFlag.UPDATE_LENGTH));
365     // dump fsdir
366     File fsnBefore = dumpTree2File("before");
367     // save the namesystem to a temp file
368     File imageFile = saveFSImageToTempFile();
369 
370     // 5. load fsimage and compare
371     // first restart the cluster, and format the cluster
372     out.close();
373     cluster.shutdown();
374     cluster = new MiniDFSCluster.Builder(conf).format(true)
375         .numDataNodes(REPLICATION).build();
376     cluster.waitActive();
377     fsn = cluster.getNamesystem();
378     hdfs = cluster.getFileSystem();
379     // then load the fsimage
380     loadFSImageFromTempFile(imageFile);
381 
382     // dump the fsdir tree again
383     File fsnAfter = dumpTree2File("after");
384 
385     // compare two dumped tree
386     SnapshotTestHelper.compareDumpedTreeInFile(fsnBefore, fsnAfter, true);
387   }
388 
389   /**
390    * Test the fsimage loading while there is file under construction.
391    */
392   @Test (timeout=60000)
testLoadImageWithAppending()393   public void testLoadImageWithAppending() throws Exception {
394     Path sub1 = new Path(dir, "sub1");
395     Path sub1file1 = new Path(sub1, "sub1file1");
396     Path sub1file2 = new Path(sub1, "sub1file2");
397     DFSTestUtil.createFile(hdfs, sub1file1, BLOCKSIZE, REPLICATION, seed);
398     DFSTestUtil.createFile(hdfs, sub1file2, BLOCKSIZE, REPLICATION, seed);
399 
400     hdfs.allowSnapshot(dir);
401     hdfs.createSnapshot(dir, "s0");
402 
403     HdfsDataOutputStream out = appendFileWithoutClosing(sub1file1, BLOCKSIZE);
404     out.hsync(EnumSet.of(SyncFlag.UPDATE_LENGTH));
405 
406     // save namespace and restart cluster
407     hdfs.setSafeMode(SafeModeAction.SAFEMODE_ENTER);
408     hdfs.saveNamespace();
409     hdfs.setSafeMode(SafeModeAction.SAFEMODE_LEAVE);
410 
411     cluster.shutdown();
412     cluster = new MiniDFSCluster.Builder(conf).format(false)
413         .numDataNodes(REPLICATION).build();
414     cluster.waitActive();
415     fsn = cluster.getNamesystem();
416     hdfs = cluster.getFileSystem();
417   }
418 
419   /**
420    * Test fsimage loading when 1) there is an empty file loaded from fsimage,
421    * and 2) there is later an append operation to be applied from edit log.
422    */
423   @Test (timeout=60000)
testLoadImageWithEmptyFile()424   public void testLoadImageWithEmptyFile() throws Exception {
425     // create an empty file
426     Path file = new Path(dir, "file");
427     FSDataOutputStream out = hdfs.create(file);
428     out.close();
429 
430     // save namespace
431     hdfs.setSafeMode(SafeModeAction.SAFEMODE_ENTER);
432     hdfs.saveNamespace();
433     hdfs.setSafeMode(SafeModeAction.SAFEMODE_LEAVE);
434 
435     // append to the empty file
436     out = hdfs.append(file);
437     out.write(1);
438     out.close();
439 
440     // restart cluster
441     cluster.shutdown();
442     cluster = new MiniDFSCluster.Builder(conf).format(false)
443         .numDataNodes(REPLICATION).build();
444     cluster.waitActive();
445     hdfs = cluster.getFileSystem();
446 
447     FileStatus status = hdfs.getFileStatus(file);
448     assertEquals(1, status.getLen());
449   }
450 
451   /**
452    * Testing a special case with snapshots. When the following steps happen:
453    * <pre>
454    * 1. Take snapshot s1 on dir.
455    * 2. Create new dir and files under subsubDir, which is descendant of dir.
456    * 3. Take snapshot s2 on dir.
457    * 4. Delete subsubDir.
458    * 5. Delete snapshot s2.
459    * </pre>
460    * When we merge the diff from s2 to s1 (since we deleted s2), we need to make
461    * sure all the files/dirs created after s1 should be destroyed. Otherwise
462    * we may save these files/dirs to the fsimage, and cause FileNotFound
463    * Exception while loading fsimage.
464    */
465   @Test (timeout=300000)
testSaveLoadImageAfterSnapshotDeletion()466   public void testSaveLoadImageAfterSnapshotDeletion()
467       throws Exception {
468     // create initial dir and subdir
469     Path dir = new Path("/dir");
470     Path subDir = new Path(dir, "subdir");
471     Path subsubDir = new Path(subDir, "subsubdir");
472     hdfs.mkdirs(subsubDir);
473 
474     // take snapshots on subdir and dir
475     SnapshotTestHelper.createSnapshot(hdfs, dir, "s1");
476 
477     // create new dir under initial dir
478     Path newDir = new Path(subsubDir, "newdir");
479     Path newFile = new Path(newDir, "newfile");
480     hdfs.mkdirs(newDir);
481     DFSTestUtil.createFile(hdfs, newFile, BLOCKSIZE, REPLICATION, seed);
482 
483     // create another snapshot
484     SnapshotTestHelper.createSnapshot(hdfs, dir, "s2");
485 
486     // delete subsubdir
487     hdfs.delete(subsubDir, true);
488 
489     // delete snapshot s2
490     hdfs.deleteSnapshot(dir, "s2");
491 
492     // restart cluster
493     cluster.shutdown();
494     cluster = new MiniDFSCluster.Builder(conf).numDataNodes(REPLICATION)
495         .format(false).build();
496     cluster.waitActive();
497     fsn = cluster.getNamesystem();
498     hdfs = cluster.getFileSystem();
499 
500     // save namespace to fsimage
501     hdfs.setSafeMode(SafeModeAction.SAFEMODE_ENTER);
502     hdfs.saveNamespace();
503     hdfs.setSafeMode(SafeModeAction.SAFEMODE_LEAVE);
504 
505     cluster.shutdown();
506     cluster = new MiniDFSCluster.Builder(conf).format(false)
507         .numDataNodes(REPLICATION).build();
508     cluster.waitActive();
509     fsn = cluster.getNamesystem();
510     hdfs = cluster.getFileSystem();
511   }
512 }