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 java.io.FileNotFoundException;
22  import java.io.IOException;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.concurrent.ThreadPoolExecutor;
29  import java.util.concurrent.TimeUnit;
30  
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  import org.apache.hadoop.hbase.classification.InterfaceAudience;
34  import org.apache.hadoop.conf.Configuration;
35  import org.apache.hadoop.fs.FSDataInputStream;
36  import org.apache.hadoop.fs.FSDataOutputStream;
37  import org.apache.hadoop.fs.FileStatus;
38  import org.apache.hadoop.fs.FileSystem;
39  import org.apache.hadoop.fs.Path;
40  import org.apache.hadoop.hbase.HColumnDescriptor;
41  import org.apache.hadoop.hbase.HRegionInfo;
42  import org.apache.hadoop.hbase.HTableDescriptor;
43  import org.apache.hadoop.hbase.errorhandling.ForeignExceptionSnare;
44  import org.apache.hadoop.hbase.mob.MobUtils;
45  import org.apache.hadoop.hbase.protobuf.ProtobufUtil;
46  import org.apache.hadoop.hbase.protobuf.generated.HBaseProtos.SnapshotDescription;
47  import org.apache.hadoop.hbase.protobuf.generated.SnapshotProtos.SnapshotDataManifest;
48  import org.apache.hadoop.hbase.protobuf.generated.SnapshotProtos.SnapshotRegionManifest;
49  import org.apache.hadoop.hbase.regionserver.HRegion;
50  import org.apache.hadoop.hbase.regionserver.HRegionFileSystem;
51  import org.apache.hadoop.hbase.regionserver.Store;
52  import org.apache.hadoop.hbase.regionserver.StoreFile;
53  import org.apache.hadoop.hbase.regionserver.StoreFileInfo;
54  import org.apache.hadoop.hbase.util.Bytes;
55  import org.apache.hadoop.hbase.util.FSTableDescriptors;
56  import org.apache.hadoop.hbase.util.FSUtils;
57  import org.apache.hadoop.hbase.util.Threads;
58  
59  /**
60   * Utility class to help read/write the Snapshot Manifest.
61   *
62   * The snapshot format is transparent for the users of this class,
63   * once the snapshot is written, it will never be modified.
64   * On open() the snapshot will be loaded to the current in-memory format.
65   */
66  @InterfaceAudience.Private
67  public class SnapshotManifest {
68    private static final Log LOG = LogFactory.getLog(SnapshotManifest.class);
69  
70    private static final String DATA_MANIFEST_NAME = "data.manifest";
71  
72    private List<SnapshotRegionManifest> regionManifests;
73    private SnapshotDescription desc;
74    private HTableDescriptor htd;
75  
76    private final ForeignExceptionSnare monitor;
77    private final Configuration conf;
78    private final Path workingDir;
79    private final FileSystem fs;
80  
81    private SnapshotManifest(final Configuration conf, final FileSystem fs,
82        final Path workingDir, final SnapshotDescription desc,
83        final ForeignExceptionSnare monitor) {
84      this.monitor = monitor;
85      this.desc = desc;
86      this.workingDir = workingDir;
87      this.conf = conf;
88      this.fs = fs;
89    }
90  
91    /**
92     * Return a SnapshotManifest instance, used for writing a snapshot.
93     *
94     * There are two usage pattern:
95     *  - The Master will create a manifest, add the descriptor, offline regions
96     *    and consolidate the snapshot by writing all the pending stuff on-disk.
97     *      manifest = SnapshotManifest.create(...)
98     *      manifest.addRegion(tableDir, hri)
99     *      manifest.consolidate()
100    *  - The RegionServer will create a single region manifest
101    *      manifest = SnapshotManifest.create(...)
102    *      manifest.addRegion(region)
103    */
104   public static SnapshotManifest create(final Configuration conf, final FileSystem fs,
105       final Path workingDir, final SnapshotDescription desc,
106       final ForeignExceptionSnare monitor) {
107     return new SnapshotManifest(conf, fs, workingDir, desc, monitor);
108   }
109 
110   /**
111    * Return a SnapshotManifest instance with the information already loaded in-memory.
112    *    SnapshotManifest manifest = SnapshotManifest.open(...)
113    *    HTableDescriptor htd = manifest.getTableDescriptor()
114    *    for (SnapshotRegionManifest regionManifest: manifest.getRegionManifests())
115    *      hri = regionManifest.getRegionInfo()
116    *      for (regionManifest.getFamilyFiles())
117    *        ...
118    */
119   public static SnapshotManifest open(final Configuration conf, final FileSystem fs,
120       final Path workingDir, final SnapshotDescription desc) throws IOException {
121     SnapshotManifest manifest = new SnapshotManifest(conf, fs, workingDir, desc, null);
122     manifest.load();
123     return manifest;
124   }
125 
126 
127   /**
128    * Add the table descriptor to the snapshot manifest
129    */
130   public void addTableDescriptor(final HTableDescriptor htd) throws IOException {
131     this.htd = htd;
132   }
133 
134   interface RegionVisitor<TRegion, TFamily> {
135     TRegion regionOpen(final HRegionInfo regionInfo) throws IOException;
136     void regionClose(final TRegion region) throws IOException;
137 
138     TFamily familyOpen(final TRegion region, final byte[] familyName) throws IOException;
139     void familyClose(final TRegion region, final TFamily family) throws IOException;
140 
141     void storeFile(final TRegion region, final TFamily family, final StoreFileInfo storeFile)
142       throws IOException;
143   }
144 
145   private RegionVisitor createRegionVisitor(final SnapshotDescription desc) throws IOException {
146     switch (getSnapshotFormat(desc)) {
147       case SnapshotManifestV1.DESCRIPTOR_VERSION:
148         return new SnapshotManifestV1.ManifestBuilder(conf, fs, workingDir);
149       case SnapshotManifestV2.DESCRIPTOR_VERSION:
150         return new SnapshotManifestV2.ManifestBuilder(conf, fs, workingDir);
151       default:
152         throw new CorruptedSnapshotException("Invalid Snapshot version: "+ desc.getVersion(), desc);
153     }
154   }
155 
156   public void addMobRegion(HRegionInfo regionInfo, HColumnDescriptor[] hcds) throws IOException {
157     // 0. Get the ManifestBuilder/RegionVisitor
158     RegionVisitor visitor = createRegionVisitor(desc);
159 
160     // 1. dump region meta info into the snapshot directory
161     LOG.debug("Storing '" + regionInfo + "' region-info for snapshot.");
162     Object regionData = visitor.regionOpen(regionInfo);
163     monitor.rethrowException();
164 
165     // 2. iterate through all the stores in the region
166     LOG.debug("Creating references for hfiles");
167 
168     Path mobRegionPath = MobUtils.getMobRegionPath(conf, regionInfo.getTable());
169     for (HColumnDescriptor hcd : hcds) {
170       // 2.1. build the snapshot reference for the store if it's a mob store
171       if (!hcd.isMobEnabled()) {
172         continue;
173       }
174       Object familyData = visitor.familyOpen(regionData, hcd.getName());
175       monitor.rethrowException();
176 
177       Path storePath = MobUtils.getMobFamilyPath(mobRegionPath, hcd.getNameAsString());
178       if (!fs.exists(storePath)) {
179         continue;
180       }
181       FileStatus[] stats = fs.listStatus(storePath);
182       if (stats == null) {
183         continue;
184       }
185       List<StoreFileInfo> storeFiles = new ArrayList<StoreFileInfo>();
186       for (FileStatus stat : stats) {
187         storeFiles.add(new StoreFileInfo(conf, fs, stat));
188       }
189       if (LOG.isDebugEnabled()) {
190         LOG.debug("Adding snapshot references for " + storeFiles + " hfiles");
191       }
192 
193       // 2.2. iterate through all the mob files and create "references".
194       for (int i = 0, sz = storeFiles.size(); i < sz; i++) {
195         StoreFileInfo storeFile = storeFiles.get(i);
196         monitor.rethrowException();
197 
198         // create "reference" to this store file.
199         if (LOG.isDebugEnabled()) {
200           LOG.debug("Adding reference for file (" + (i + 1) + "/" + sz + "): "
201             + storeFile.getPath());
202         }
203         visitor.storeFile(regionData, familyData, storeFile);
204       }
205       visitor.familyClose(regionData, familyData);
206     }
207     visitor.regionClose(regionData);
208   }
209 
210   /**
211    * Creates a 'manifest' for the specified region, by reading directly from the HRegion object.
212    * This is used by the "online snapshot" when the table is enabled.
213    */
214   public void addRegion(final HRegion region) throws IOException {
215     // 0. Get the ManifestBuilder/RegionVisitor
216     RegionVisitor visitor = createRegionVisitor(desc);
217 
218     // 1. dump region meta info into the snapshot directory
219     LOG.debug("Storing '" + region + "' region-info for snapshot.");
220     Object regionData = visitor.regionOpen(region.getRegionInfo());
221     monitor.rethrowException();
222 
223     // 2. iterate through all the stores in the region
224     LOG.debug("Creating references for hfiles");
225 
226     for (Store store : region.getStores()) {
227       // 2.1. build the snapshot reference for the store
228       Object familyData = visitor.familyOpen(regionData, store.getFamily().getName());
229       monitor.rethrowException();
230 
231       List<StoreFile> storeFiles = new ArrayList<StoreFile>(store.getStorefiles());
232       if (LOG.isDebugEnabled()) {
233         LOG.debug("Adding snapshot references for " + storeFiles  + " hfiles");
234       }
235 
236       // 2.2. iterate through all the store's files and create "references".
237       for (int i = 0, sz = storeFiles.size(); i < sz; i++) {
238         StoreFile storeFile = storeFiles.get(i);
239         monitor.rethrowException();
240 
241         // create "reference" to this store file.
242         LOG.debug("Adding reference for file (" + (i+1) + "/" + sz + "): " + storeFile.getPath());
243         visitor.storeFile(regionData, familyData, storeFile.getFileInfo());
244       }
245       visitor.familyClose(regionData, familyData);
246     }
247     visitor.regionClose(regionData);
248   }
249 
250   /**
251    * Creates a 'manifest' for the specified region, by reading directly from the disk.
252    * This is used by the "offline snapshot" when the table is disabled.
253    */
254   public void addRegion(final Path tableDir, final HRegionInfo regionInfo) throws IOException {
255     // 0. Get the ManifestBuilder/RegionVisitor
256     RegionVisitor visitor = createRegionVisitor(desc);
257 
258     boolean isMobRegion = MobUtils.isMobRegionInfo(regionInfo);
259     try {
260       // Open the RegionFS
261       HRegionFileSystem regionFs = HRegionFileSystem.openRegionFromFileSystem(conf, fs,
262             tableDir, regionInfo, true);
263       monitor.rethrowException();
264 
265       // 1. dump region meta info into the snapshot directory
266       LOG.debug("Storing region-info for snapshot.");
267       Object regionData = visitor.regionOpen(regionInfo);
268       monitor.rethrowException();
269 
270       // 2. iterate through all the stores in the region
271       LOG.debug("Creating references for hfiles");
272 
273       // This ensures that we have an atomic view of the directory as long as we have < ls limit
274       // (batch size of the files in a directory) on the namenode. Otherwise, we get back the files
275       // in batches and may miss files being added/deleted. This could be more robust (iteratively
276       // checking to see if we have all the files until we are sure), but the limit is currently
277       // 1000 files/batch, far more than the number of store files under a single column family.
278       Collection<String> familyNames = regionFs.getFamilies();
279       if (familyNames != null) {
280         for (String familyName: familyNames) {
281           Object familyData = visitor.familyOpen(regionData, Bytes.toBytes(familyName));
282           monitor.rethrowException();
283 
284           Collection<StoreFileInfo> storeFiles = null;
285           if (isMobRegion) {
286             Path regionPath = MobUtils.getMobRegionPath(conf, regionInfo.getTable());
287             Path storePath = MobUtils.getMobFamilyPath(regionPath, familyName);
288             if (!fs.exists(storePath)) {
289               continue;
290             }
291             FileStatus[] stats = fs.listStatus(storePath);
292             if (stats == null) {
293               continue;
294             }
295             storeFiles = new ArrayList<StoreFileInfo>();
296             for (FileStatus stat : stats) {
297               storeFiles.add(new StoreFileInfo(conf, fs, stat));
298             }
299           } else {
300             storeFiles = regionFs.getStoreFiles(familyName);
301           }
302           if (storeFiles == null) {
303             if (LOG.isDebugEnabled()) {
304               LOG.debug("No files under family: " + familyName);
305             }
306             continue;
307           }
308 
309           // 2.1. build the snapshot reference for the store
310           if (LOG.isDebugEnabled()) {
311             LOG.debug("Adding snapshot references for " + storeFiles  + " hfiles");
312           }
313 
314           // 2.2. iterate through all the store's files and create "references".
315           int i = 0;
316           int sz = storeFiles.size();
317           for (StoreFileInfo storeFile: storeFiles) {
318             monitor.rethrowException();
319 
320             // create "reference" to this store file.
321             LOG.debug("Adding reference for file (" + (++i) + "/" + sz + "): "
322                 + storeFile.getPath());
323             visitor.storeFile(regionData, familyData, storeFile);
324           }
325           visitor.familyClose(regionData, familyData);
326         }
327       }
328       visitor.regionClose(regionData);
329     } catch (IOException e) {
330       // the mob directory might not be created yet, so do nothing when it is a mob region
331       if (!isMobRegion) {
332         throw e;
333       }
334     }
335   }
336 
337   /**
338    * Load the information in the SnapshotManifest. Called by SnapshotManifest.open()
339    *
340    * If the format is v2 and there is no data-manifest, means that we are loading an
341    * in-progress snapshot. Since we support rolling-upgrades, we loook for v1 and v2
342    * regions format.
343    */
344   private void load() throws IOException {
345     switch (getSnapshotFormat(desc)) {
346       case SnapshotManifestV1.DESCRIPTOR_VERSION: {
347         this.htd = FSTableDescriptors.getTableDescriptorFromFs(fs, workingDir);
348         ThreadPoolExecutor tpool = createExecutor("SnapshotManifestLoader");
349         try {
350           this.regionManifests =
351             SnapshotManifestV1.loadRegionManifests(conf, tpool, fs, workingDir, desc);
352         } finally {
353           tpool.shutdown();
354         }
355         break;
356       }
357       case SnapshotManifestV2.DESCRIPTOR_VERSION: {
358         SnapshotDataManifest dataManifest = readDataManifest();
359         if (dataManifest != null) {
360           htd = HTableDescriptor.convert(dataManifest.getTableSchema());
361           regionManifests = dataManifest.getRegionManifestsList();
362         } else {
363           // Compatibility, load the v1 regions
364           // This happens only when the snapshot is in-progress and the cache wants to refresh.
365           List<SnapshotRegionManifest> v1Regions, v2Regions;
366           ThreadPoolExecutor tpool = createExecutor("SnapshotManifestLoader");
367           try {
368             v1Regions = SnapshotManifestV1.loadRegionManifests(conf, tpool, fs, workingDir, desc);
369             v2Regions = SnapshotManifestV2.loadRegionManifests(conf, tpool, fs, workingDir, desc);
370           } finally {
371             tpool.shutdown();
372           }
373           if (v1Regions != null && v2Regions != null) {
374             regionManifests =
375               new ArrayList<SnapshotRegionManifest>(v1Regions.size() + v2Regions.size());
376             regionManifests.addAll(v1Regions);
377             regionManifests.addAll(v2Regions);
378           } else if (v1Regions != null) {
379             regionManifests = v1Regions;
380           } else /* if (v2Regions != null) */ {
381             regionManifests = v2Regions;
382           }
383         }
384         break;
385       }
386       default:
387         throw new CorruptedSnapshotException("Invalid Snapshot version: "+ desc.getVersion(), desc);
388     }
389   }
390 
391   /**
392    * Get the current snapshot working dir
393    */
394   public Path getSnapshotDir() {
395     return this.workingDir;
396   }
397 
398   /**
399    * Get the SnapshotDescription
400    */
401   public SnapshotDescription getSnapshotDescription() {
402     return this.desc;
403   }
404 
405   /**
406    * Get the table descriptor from the Snapshot
407    */
408   public HTableDescriptor getTableDescriptor() {
409     return this.htd;
410   }
411 
412   /**
413    * Get all the Region Manifest from the snapshot
414    */
415   public List<SnapshotRegionManifest> getRegionManifests() {
416     return this.regionManifests;
417   }
418 
419   /**
420    * Get all the Region Manifest from the snapshot.
421    * This is an helper to get a map with the region encoded name
422    */
423   public Map<String, SnapshotRegionManifest> getRegionManifestsMap() {
424     if (regionManifests == null || regionManifests.size() == 0) return null;
425 
426     HashMap<String, SnapshotRegionManifest> regionsMap =
427         new HashMap<String, SnapshotRegionManifest>(regionManifests.size());
428     for (SnapshotRegionManifest manifest: regionManifests) {
429       String regionName = getRegionNameFromManifest(manifest);
430       regionsMap.put(regionName, manifest);
431     }
432     return regionsMap;
433   }
434 
435   public void consolidate() throws IOException {
436     if (getSnapshotFormat(desc) == SnapshotManifestV1.DESCRIPTOR_VERSION) {
437       Path rootDir = FSUtils.getRootDir(conf);
438       LOG.info("Using old Snapshot Format");
439       // write a copy of descriptor to the snapshot directory
440       new FSTableDescriptors(conf, fs, rootDir)
441         .createTableDescriptorForTableDirectory(workingDir, htd, false);
442     } else {
443       LOG.debug("Convert to Single Snapshot Manifest");
444       convertToV2SingleManifest();
445     }
446   }
447 
448   /*
449    * In case of rolling-upgrade, we try to read all the formats and build
450    * the snapshot with the latest format.
451    */
452   private void convertToV2SingleManifest() throws IOException {
453     // Try to load v1 and v2 regions
454     List<SnapshotRegionManifest> v1Regions, v2Regions;
455     ThreadPoolExecutor tpool = createExecutor("SnapshotManifestLoader");
456     try {
457       v1Regions = SnapshotManifestV1.loadRegionManifests(conf, tpool, fs, workingDir, desc);
458       v2Regions = SnapshotManifestV2.loadRegionManifests(conf, tpool, fs, workingDir, desc);
459     } finally {
460       tpool.shutdown();
461     }
462 
463     SnapshotDataManifest.Builder dataManifestBuilder = SnapshotDataManifest.newBuilder();
464     dataManifestBuilder.setTableSchema(htd.convert());
465 
466     if (v1Regions != null && v1Regions.size() > 0) {
467       dataManifestBuilder.addAllRegionManifests(v1Regions);
468     }
469     if (v2Regions != null && v2Regions.size() > 0) {
470       dataManifestBuilder.addAllRegionManifests(v2Regions);
471     }
472 
473     // Write the v2 Data Manifest.
474     // Once the data-manifest is written, the snapshot can be considered complete.
475     // Currently snapshots are written in a "temporary" directory and later
476     // moved to the "complated" snapshot directory.
477     SnapshotDataManifest dataManifest = dataManifestBuilder.build();
478     writeDataManifest(dataManifest);
479     this.regionManifests = dataManifest.getRegionManifestsList();
480 
481     // Remove the region manifests. Everything is now in the data-manifest.
482     // The delete operation is "relaxed", unless we get an exception we keep going.
483     // The extra files in the snapshot directory will not give any problem,
484     // since they have the same content as the data manifest, and even by re-reading
485     // them we will get the same information.
486     if (v1Regions != null && v1Regions.size() > 0) {
487       for (SnapshotRegionManifest regionManifest: v1Regions) {
488         SnapshotManifestV1.deleteRegionManifest(fs, workingDir, regionManifest);
489       }
490     }
491     if (v2Regions != null && v2Regions.size() > 0) {
492       for (SnapshotRegionManifest regionManifest: v2Regions) {
493         SnapshotManifestV2.deleteRegionManifest(fs, workingDir, regionManifest);
494       }
495     }
496   }
497 
498   /*
499    * Write the SnapshotDataManifest file
500    */
501   private void writeDataManifest(final SnapshotDataManifest manifest)
502       throws IOException {
503     FSDataOutputStream stream = fs.create(new Path(workingDir, DATA_MANIFEST_NAME));
504     try {
505       manifest.writeTo(stream);
506     } finally {
507       stream.close();
508     }
509   }
510 
511   /*
512    * Read the SnapshotDataManifest file
513    */
514   private SnapshotDataManifest readDataManifest() throws IOException {
515     FSDataInputStream in = null;
516     try {
517       in = fs.open(new Path(workingDir, DATA_MANIFEST_NAME));
518       return SnapshotDataManifest.parseFrom(in);
519     } catch (FileNotFoundException e) {
520       return null;
521     } finally {
522       if (in != null) in.close();
523     }
524   }
525 
526   private ThreadPoolExecutor createExecutor(final String name) {
527     return createExecutor(conf, name);
528   }
529 
530   public static ThreadPoolExecutor createExecutor(final Configuration conf, final String name) {
531     int maxThreads = conf.getInt("hbase.snapshot.thread.pool.max", 8);
532     return Threads.getBoundedCachedThreadPool(maxThreads, 30L, TimeUnit.SECONDS,
533               Threads.getNamedThreadFactory(name));
534   }
535 
536   /**
537    * Extract the region encoded name from the region manifest
538    */
539   static String getRegionNameFromManifest(final SnapshotRegionManifest manifest) {
540     byte[] regionName = HRegionInfo.createRegionName(
541             ProtobufUtil.toTableName(manifest.getRegionInfo().getTableName()),
542             manifest.getRegionInfo().getStartKey().toByteArray(),
543             manifest.getRegionInfo().getRegionId(), true);
544     return HRegionInfo.encodeRegionName(regionName);
545   }
546 
547   /*
548    * Return the snapshot format
549    */
550   private static int getSnapshotFormat(final SnapshotDescription desc) {
551     return desc.hasVersion() ? desc.getVersion() : SnapshotManifestV1.DESCRIPTOR_VERSION;
552   }
553 }