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