View Javadoc

1   /**
2    *
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  package org.apache.hadoop.hbase.master;
20  
21  import java.io.FileNotFoundException;
22  import java.io.IOException;
23  import java.util.Comparator;
24  import java.util.HashSet;
25  import java.util.Map;
26  import java.util.TreeMap;
27  import java.util.concurrent.atomic.AtomicBoolean;
28  import java.util.concurrent.atomic.AtomicInteger;
29  
30  import org.apache.commons.logging.Log;
31  import org.apache.commons.logging.LogFactory;
32  import org.apache.hadoop.fs.FileSystem;
33  import org.apache.hadoop.fs.Path;
34  import org.apache.hadoop.hbase.HColumnDescriptor;
35  import org.apache.hadoop.hbase.HConstants;
36  import org.apache.hadoop.hbase.HRegionInfo;
37  import org.apache.hadoop.hbase.HTableDescriptor;
38  import org.apache.hadoop.hbase.MetaTableAccessor;
39  import org.apache.hadoop.hbase.ScheduledChore;
40  import org.apache.hadoop.hbase.Server;
41  import org.apache.hadoop.hbase.TableName;
42  import org.apache.hadoop.hbase.backup.HFileArchiver;
43  import org.apache.hadoop.hbase.classification.InterfaceAudience;
44  import org.apache.hadoop.hbase.client.Connection;
45  import org.apache.hadoop.hbase.client.MetaScanner;
46  import org.apache.hadoop.hbase.client.MetaScanner.MetaScannerVisitor;
47  import org.apache.hadoop.hbase.client.Result;
48  import org.apache.hadoop.hbase.regionserver.HRegionFileSystem;
49  import org.apache.hadoop.hbase.util.Bytes;
50  import org.apache.hadoop.hbase.util.FSUtils;
51  import org.apache.hadoop.hbase.util.Pair;
52  import org.apache.hadoop.hbase.util.PairOfSameType;
53  import org.apache.hadoop.hbase.util.Threads;
54  import org.apache.hadoop.hbase.util.Triple;
55  
56  /**
57   * A janitor for the catalog tables.  Scans the <code>hbase:meta</code> catalog
58   * table on a period looking for unused regions to garbage collect.
59   */
60  @InterfaceAudience.Private
61  public class CatalogJanitor extends ScheduledChore {
62    private static final Log LOG = LogFactory.getLog(CatalogJanitor.class.getName());
63    private final Server server;
64    private final MasterServices services;
65    private AtomicBoolean enabled = new AtomicBoolean(true);
66    private AtomicBoolean alreadyRunning = new AtomicBoolean(false);
67    private final Connection connection;
68  
69    CatalogJanitor(final Server server, final MasterServices services) {
70      super("CatalogJanitor-" + server.getServerName().toShortString(), server, server
71          .getConfiguration().getInt("hbase.catalogjanitor.interval", 300000));
72      this.server = server;
73      this.services = services;
74      this.connection = server.getConnection();
75    }
76  
77    @Override
78    protected boolean initialChore() {
79      try {
80        if (this.enabled.get()) scan();
81      } catch (IOException e) {
82        LOG.warn("Failed initial scan of catalog table", e);
83        return false;
84      }
85      return true;
86    }
87  
88    /**
89     * @param enabled
90     */
91    public boolean setEnabled(final boolean enabled) {
92      boolean alreadyEnabled = this.enabled.getAndSet(enabled);
93      // If disabling is requested on an already enabled chore, we could have an active
94      // scan still going on, callers might not be aware of that and do further action thinkng
95      // that no action would be from this chore.  In this case, the right action is to wait for
96      // the active scan to complete before exiting this function.
97      if (!enabled && alreadyEnabled) {
98        while (alreadyRunning.get()) {
99          Threads.sleepWithoutInterrupt(100);
100       }
101     }
102     return alreadyEnabled;
103   }
104 
105   boolean getEnabled() {
106     return this.enabled.get();
107   }
108 
109   @Override
110   protected void chore() {
111     try {
112       AssignmentManager am = this.services.getAssignmentManager();
113       if (this.enabled.get()
114           && !this.services.isInMaintenanceMode()
115           && am != null
116           && am.isFailoverCleanupDone()
117           && am.getRegionStates().getRegionsInTransition().size() == 0) {
118         scan();
119       } else {
120         LOG.warn("CatalogJanitor is disabled! Enabled=" + this.enabled.get() +
121           ", maintenanceMode=" + this.services.isInMaintenanceMode() +
122           ", am=" + am + ", failoverCleanupDone=" + (am != null && am.isFailoverCleanupDone()) +
123           ", hasRIT=" + (am != null && am.getRegionStates().getRegionsInTransition().size() == 0));
124       }
125     } catch (IOException e) {
126       LOG.warn("Failed scan of catalog table", e);
127     }
128   }
129 
130   /**
131    * Scans hbase:meta and returns a number of scanned rows, and a map of merged
132    * regions, and an ordered map of split parents.
133    * @return triple of scanned rows, map of merged regions and map of split
134    *         parent regioninfos
135    * @throws IOException
136    */
137   Triple<Integer, Map<HRegionInfo, Result>, Map<HRegionInfo, Result>> getMergedRegionsAndSplitParents()
138       throws IOException {
139     return getMergedRegionsAndSplitParents(null);
140   }
141 
142   /**
143    * Scans hbase:meta and returns a number of scanned rows, and a map of merged
144    * regions, and an ordered map of split parents. if the given table name is
145    * null, return merged regions and split parents of all tables, else only the
146    * specified table
147    * @param tableName null represents all tables
148    * @return triple of scanned rows, and map of merged regions, and map of split
149    *         parent regioninfos
150    * @throws IOException
151    */
152   Triple<Integer, Map<HRegionInfo, Result>, Map<HRegionInfo, Result>> getMergedRegionsAndSplitParents(
153       final TableName tableName) throws IOException {
154     final boolean isTableSpecified = (tableName != null);
155     // TODO: Only works with single hbase:meta region currently.  Fix.
156     final AtomicInteger count = new AtomicInteger(0);
157     // Keep Map of found split parents.  There are candidates for cleanup.
158     // Use a comparator that has split parents come before its daughters.
159     final Map<HRegionInfo, Result> splitParents =
160       new TreeMap<HRegionInfo, Result>(new SplitParentFirstComparator());
161     final Map<HRegionInfo, Result> mergedRegions = new TreeMap<HRegionInfo, Result>();
162     // This visitor collects split parents and counts rows in the hbase:meta table
163 
164     MetaScannerVisitor visitor = new MetaScanner.MetaScannerVisitorBase() {
165       @Override
166       public boolean processRow(Result r) throws IOException {
167         if (r == null || r.isEmpty()) return true;
168         count.incrementAndGet();
169         HRegionInfo info = HRegionInfo.getHRegionInfo(r);
170         if (info == null) return true; // Keep scanning
171         if (isTableSpecified
172             && info.getTable().compareTo(tableName) > 0) {
173           // Another table, stop scanning
174           return false;
175         }
176         if (info.isSplitParent()) splitParents.put(info, r);
177         if (r.getValue(HConstants.CATALOG_FAMILY, HConstants.MERGEA_QUALIFIER) != null) {
178           mergedRegions.put(info, r);
179         }
180         // Returning true means "keep scanning"
181         return true;
182       }
183     };
184 
185     // Run full scan of hbase:meta catalog table passing in our custom visitor with
186     // the start row
187     MetaScanner.metaScan(this.connection, visitor, tableName);
188 
189     return new Triple<Integer, Map<HRegionInfo, Result>, Map<HRegionInfo, Result>>(
190         count.get(), mergedRegions, splitParents);
191   }
192 
193   /**
194    * If merged region no longer holds reference to the merge regions, archive
195    * merge region on hdfs and perform deleting references in hbase:meta
196    * @param mergedRegion
197    * @param regionA
198    * @param regionB
199    * @return true if we delete references in merged region on hbase:meta and archive
200    *         the files on the file system
201    * @throws IOException
202    */
203   boolean cleanMergeRegion(final HRegionInfo mergedRegion,
204       final HRegionInfo regionA, final HRegionInfo regionB) throws IOException {
205     FileSystem fs = this.services.getMasterFileSystem().getFileSystem();
206     Path rootdir = this.services.getMasterFileSystem().getRootDir();
207     Path tabledir = FSUtils.getTableDir(rootdir, mergedRegion.getTable());
208     HTableDescriptor htd = getTableDescriptor(mergedRegion.getTable());
209     HRegionFileSystem regionFs = null;
210     try {
211       regionFs = HRegionFileSystem.openRegionFromFileSystem(
212           this.services.getConfiguration(), fs, tabledir, mergedRegion, true);
213     } catch (IOException e) {
214       LOG.warn("Merged region does not exist: " + mergedRegion.getEncodedName());
215     }
216     if (regionFs == null || !regionFs.hasReferences(htd)) {
217       LOG.debug("Deleting region " + regionA.getRegionNameAsString() + " and "
218           + regionB.getRegionNameAsString()
219           + " from fs because merged region no longer holds references");
220       HFileArchiver.archiveRegion(this.services.getConfiguration(), fs, regionA);
221       HFileArchiver.archiveRegion(this.services.getConfiguration(), fs, regionB);
222       MetaTableAccessor.deleteMergeQualifiers(services.getConnection(), mergedRegion);
223       services.getAssignmentManager().getRegionStates().deleteRegion(regionA);
224       services.getAssignmentManager().getRegionStates().deleteRegion(regionB);
225       services.getServerManager().removeRegion(regionA);
226       services.getServerManager().removeRegion(regionB);
227       return true;
228     }
229     return false;
230   }
231 
232   /**
233    * Run janitorial scan of catalog <code>hbase:meta</code> table looking for
234    * garbage to collect.
235    * @return number of cleaned regions
236    * @throws IOException
237    */
238   int scan() throws IOException {
239     try {
240       if (!alreadyRunning.compareAndSet(false, true)) {
241         LOG.debug("CatalogJanitor already running");
242         // -1 indicates previous scan is in progress
243         return -1;
244       }
245       Triple<Integer, Map<HRegionInfo, Result>, Map<HRegionInfo, Result>> scanTriple =
246         getMergedRegionsAndSplitParents();
247       int count = scanTriple.getFirst();
248       /**
249        * clean merge regions first
250        */
251       int mergeCleaned = 0;
252       Map<HRegionInfo, Result> mergedRegions = scanTriple.getSecond();
253       for (Map.Entry<HRegionInfo, Result> e : mergedRegions.entrySet()) {
254         if (this.services.isInMaintenanceMode()) {
255           // Stop cleaning if the master is in maintenance mode
256           break;
257         }
258 
259         HRegionInfo regionA = HRegionInfo.getHRegionInfo(e.getValue(),
260             HConstants.MERGEA_QUALIFIER);
261         HRegionInfo regionB = HRegionInfo.getHRegionInfo(e.getValue(),
262             HConstants.MERGEB_QUALIFIER);
263         if (regionA == null || regionB == null) {
264           LOG.warn("Unexpected references regionA="
265               + (regionA == null ? "null" : regionA.getRegionNameAsString())
266               + ",regionB="
267               + (regionB == null ? "null" : regionB.getRegionNameAsString())
268               + " in merged region " + e.getKey().getRegionNameAsString());
269         } else {
270           if (cleanMergeRegion(e.getKey(), regionA, regionB)) {
271             mergeCleaned++;
272           }
273         }
274       }
275       /**
276        * clean split parents
277        */
278       Map<HRegionInfo, Result> splitParents = scanTriple.getThird();
279 
280       // Now work on our list of found parents. See if any we can clean up.
281       int splitCleaned = 0;
282       // regions whose parents are still around
283       HashSet<String> parentNotCleaned = new HashSet<String>();
284       for (Map.Entry<HRegionInfo, Result> e : splitParents.entrySet()) {
285         if (this.services.isInMaintenanceMode()) {
286           // Stop cleaning if the master is in maintenance mode
287           break;
288         }
289 
290         if (!parentNotCleaned.contains(e.getKey().getEncodedName()) &&
291             cleanParent(e.getKey(), e.getValue())) {
292           splitCleaned++;
293         } else {
294           // We could not clean the parent, so it's daughters should not be cleaned either (HBASE-6160)
295           PairOfSameType<HRegionInfo> daughters = HRegionInfo.getDaughterRegions(e.getValue());
296           parentNotCleaned.add(daughters.getFirst().getEncodedName());
297           parentNotCleaned.add(daughters.getSecond().getEncodedName());
298         }
299       }
300       if ((mergeCleaned + splitCleaned) != 0) {
301         LOG.info("Scanned " + count + " catalog row(s), gc'd " + mergeCleaned
302             + " unreferenced merged region(s) and " + splitCleaned
303             + " unreferenced parent region(s)");
304       } else if (LOG.isTraceEnabled()) {
305         LOG.trace("Scanned " + count + " catalog row(s), gc'd " + mergeCleaned
306             + " unreferenced merged region(s) and " + splitCleaned
307             + " unreferenced parent region(s)");
308       }
309       return mergeCleaned + splitCleaned;
310     } finally {
311       alreadyRunning.set(false);
312     }
313   }
314 
315   /**
316    * Compare HRegionInfos in a way that has split parents sort BEFORE their
317    * daughters.
318    */
319   static class SplitParentFirstComparator implements Comparator<HRegionInfo> {
320     Comparator<byte[]> rowEndKeyComparator = new Bytes.RowEndKeyComparator();
321     @Override
322     public int compare(HRegionInfo left, HRegionInfo right) {
323       // This comparator differs from the one HRegionInfo in that it sorts
324       // parent before daughters.
325       if (left == null) return -1;
326       if (right == null) return 1;
327       // Same table name.
328       int result = left.getTable().compareTo(right.getTable());
329       if (result != 0) return result;
330       // Compare start keys.
331       result = Bytes.compareTo(left.getStartKey(), right.getStartKey());
332       if (result != 0) return result;
333       // Compare end keys, but flip the operands so parent comes first
334       result = rowEndKeyComparator.compare(right.getEndKey(), left.getEndKey());
335 
336       return result;
337     }
338   }
339 
340   /**
341    * If daughters no longer hold reference to the parents, delete the parent.
342    * @param parent HRegionInfo of split offlined parent
343    * @param rowContent Content of <code>parent</code> row in
344    * <code>metaRegionName</code>
345    * @return True if we removed <code>parent</code> from meta table and from
346    * the filesystem.
347    * @throws IOException
348    */
349   boolean cleanParent(final HRegionInfo parent, Result rowContent)
350   throws IOException {
351     boolean result = false;
352     // Check whether it is a merged region and not clean reference
353     // No necessary to check MERGEB_QUALIFIER because these two qualifiers will
354     // be inserted/deleted together
355     if (rowContent.getValue(HConstants.CATALOG_FAMILY,
356         HConstants.MERGEA_QUALIFIER) != null) {
357       // wait cleaning merge region first
358       return result;
359     }
360     // Run checks on each daughter split.
361     PairOfSameType<HRegionInfo> daughters = HRegionInfo.getDaughterRegions(rowContent);
362     Pair<Boolean, Boolean> a = checkDaughterInFs(parent, daughters.getFirst());
363     Pair<Boolean, Boolean> b = checkDaughterInFs(parent, daughters.getSecond());
364     if (hasNoReferences(a) && hasNoReferences(b)) {
365       LOG.debug("Deleting region " + parent.getRegionNameAsString() +
366         " because daughter splits no longer hold references");
367       FileSystem fs = this.services.getMasterFileSystem().getFileSystem();
368       if (LOG.isTraceEnabled()) LOG.trace("Archiving parent region: " + parent);
369       HFileArchiver.archiveRegion(this.services.getConfiguration(), fs, parent);
370       MetaTableAccessor.deleteRegion(this.connection, parent);
371       if (services.getAssignmentManager().getRegionStates() != null)
372         services.getAssignmentManager().getRegionStates().deleteRegion(parent);
373       services.getServerManager().removeRegion(parent);
374       result = true;
375     }
376     return result;
377   }
378 
379   /**
380    * @param p A pair where the first boolean says whether or not the daughter
381    * region directory exists in the filesystem and then the second boolean says
382    * whether the daughter has references to the parent.
383    * @return True the passed <code>p</code> signifies no references.
384    */
385   private boolean hasNoReferences(final Pair<Boolean, Boolean> p) {
386     return !p.getFirst() || !p.getSecond();
387   }
388 
389   /**
390    * Checks if a daughter region -- either splitA or splitB -- still holds
391    * references to parent.
392    * @param parent Parent region
393    * @param daughter Daughter region
394    * @return A pair where the first boolean says whether or not the daughter
395    * region directory exists in the filesystem and then the second boolean says
396    * whether the daughter has references to the parent.
397    * @throws IOException
398    */
399   Pair<Boolean, Boolean> checkDaughterInFs(final HRegionInfo parent, final HRegionInfo daughter)
400   throws IOException {
401     if (daughter == null)  {
402       return new Pair<Boolean, Boolean>(Boolean.FALSE, Boolean.FALSE);
403     }
404 
405     FileSystem fs = this.services.getMasterFileSystem().getFileSystem();
406     Path rootdir = this.services.getMasterFileSystem().getRootDir();
407     Path tabledir = FSUtils.getTableDir(rootdir, daughter.getTable());
408 
409     Path daughterRegionDir = new Path(tabledir, daughter.getEncodedName());
410 
411     HRegionFileSystem regionFs = null;
412 
413     try {
414       if (!FSUtils.isExists(fs, daughterRegionDir)) {
415         return new Pair<Boolean, Boolean>(Boolean.FALSE, Boolean.FALSE);
416       }
417     } catch (IOException ioe) {
418       LOG.error("Error trying to determine if daughter region exists, " +
419                "assuming exists and has references", ioe);
420       return new Pair<Boolean, Boolean>(Boolean.TRUE, Boolean.TRUE);
421     }
422 
423     boolean references = false;
424     HTableDescriptor parentDescriptor = getTableDescriptor(parent.getTable());
425     try {
426       regionFs = HRegionFileSystem.openRegionFromFileSystem(
427           this.services.getConfiguration(), fs, tabledir, daughter, true);
428       
429       for (HColumnDescriptor family: parentDescriptor.getFamilies()) {
430         if ((references = regionFs.hasReferences(family.getNameAsString()))) {
431           break;
432         }
433       }
434     } catch (IOException e) {
435       LOG.error("Error trying to determine referenced files from : " + daughter.getEncodedName()
436           + ", to: " + parent.getEncodedName() + " assuming has references", e);
437       return new Pair<Boolean, Boolean>(Boolean.TRUE, Boolean.TRUE);
438     }
439     return new Pair<Boolean, Boolean>(Boolean.TRUE, Boolean.valueOf(references));
440   }
441 
442   private HTableDescriptor getTableDescriptor(final TableName tableName)
443       throws FileNotFoundException, IOException {
444     return this.services.getTableDescriptors().get(tableName);
445   }
446 
447   /**
448    * Checks if the specified region has merge qualifiers, if so, try to clean
449    * them
450    * @param region
451    * @return true if the specified region doesn't have merge qualifier now
452    * @throws IOException
453    */
454   public boolean cleanMergeQualifier(final HRegionInfo region)
455       throws IOException {
456     // Get merge regions if it is a merged region and already has merge
457     // qualifier
458     Pair<HRegionInfo, HRegionInfo> mergeRegions = MetaTableAccessor
459         .getRegionsFromMergeQualifier(this.services.getConnection(),
460           region.getRegionName());
461     if (mergeRegions == null
462         || (mergeRegions.getFirst() == null && mergeRegions.getSecond() == null)) {
463       // It doesn't have merge qualifier, no need to clean
464       return true;
465     }
466     // It shouldn't happen, we must insert/delete these two qualifiers together
467     if (mergeRegions.getFirst() == null || mergeRegions.getSecond() == null) {
468       LOG.error("Merged region " + region.getRegionNameAsString()
469           + " has only one merge qualifier in META.");
470       return false;
471     }
472     return cleanMergeRegion(region, mergeRegions.getFirst(),
473         mergeRegions.getSecond());
474   }
475 }