View Javadoc

1   /**
2    * Copyright 2008 The Apache Software Foundation
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS,
16   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17   * See the License for the specific language governing permissions and
18   * limitations under the License.
19   */
20  package org.apache.hadoop.hbase.master;
21  
22  import java.io.FileNotFoundException;
23  import java.io.IOException;
24  import java.util.Comparator;
25  import java.util.Map;
26  import java.util.TreeMap;
27  import java.util.concurrent.atomic.AtomicInteger;
28  
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  import org.apache.hadoop.fs.FileStatus;
32  import org.apache.hadoop.fs.FileSystem;
33  import org.apache.hadoop.fs.Path;
34  import org.apache.hadoop.fs.PathFilter;
35  import org.apache.hadoop.hbase.Chore;
36  import org.apache.hadoop.hbase.HColumnDescriptor;
37  import org.apache.hadoop.hbase.HConstants;
38  import org.apache.hadoop.hbase.HRegionInfo;
39  import org.apache.hadoop.hbase.HTableDescriptor;
40  import org.apache.hadoop.hbase.Server;
41  import org.apache.hadoop.hbase.TableExistsException;
42  import org.apache.hadoop.hbase.catalog.MetaEditor;
43  import org.apache.hadoop.hbase.catalog.MetaReader;
44  import org.apache.hadoop.hbase.client.Result;
45  import org.apache.hadoop.hbase.regionserver.HRegion;
46  import org.apache.hadoop.hbase.regionserver.Store;
47  import org.apache.hadoop.hbase.regionserver.StoreFile;
48  import org.apache.hadoop.hbase.util.Bytes;
49  import org.apache.hadoop.hbase.util.FSUtils;
50  import org.apache.hadoop.hbase.util.Pair;
51  import org.apache.hadoop.hbase.util.Writables;
52  
53  
54  /**
55   * A janitor for the catalog tables.  Scans the <code>.META.</code> catalog
56   * table on a period looking for unused regions to garbage collect.
57   */
58  class CatalogJanitor extends Chore {
59    private static final Log LOG = LogFactory.getLog(CatalogJanitor.class.getName());
60    private final Server server;
61    private final MasterServices services;
62    private boolean enabled = true;
63  
64    CatalogJanitor(final Server server, final MasterServices services) {
65      super(server.getServerName() + "-CatalogJanitor",
66        server.getConfiguration().getInt("hbase.catalogjanitor.interval", 300000),
67        server);
68      this.server = server;
69      this.services = services;
70    }
71  
72    @Override
73    protected boolean initialChore() {
74      try {
75        if (this.enabled) scan();
76      } catch (IOException e) {
77        LOG.warn("Failed initial scan of catalog table", e);
78        return false;
79      }
80      return true;
81    }
82  
83    /**
84     * @param enabled
85     */
86    public void setEnabled(final boolean enabled) {
87      this.enabled = enabled;
88    }
89  
90    @Override
91    protected void chore() {
92      try {
93        scan();
94      } catch (IOException e) {
95        LOG.warn("Failed scan of catalog table", e);
96      }
97    }
98  
99    /**
100    * Run janitorial scan of catalog <code>.META.</code> table looking for
101    * garbage to collect.
102    * @throws IOException
103    */
104   void scan() throws IOException {
105     // TODO: Only works with single .META. region currently.  Fix.
106     final AtomicInteger count = new AtomicInteger(0);
107     // Keep Map of found split parents.  There are candidates for cleanup.
108     // Use a comparator that has split parents come before its daughters.
109     final Map<HRegionInfo, Result> splitParents =
110       new TreeMap<HRegionInfo, Result>(new SplitParentFirstComparator());
111     // This visitor collects split parents and counts rows in the .META. table
112     MetaReader.Visitor visitor = new MetaReader.Visitor() {
113       @Override
114       public boolean visit(Result r) throws IOException {
115         if (r == null || r.isEmpty()) return true;
116         count.incrementAndGet();
117         HRegionInfo info = getHRegionInfo(r);
118         if (info == null) return true; // Keep scanning
119         if (info.isSplitParent()) splitParents.put(info, r);
120         // Returning true means "keep scanning"
121         return true;
122       }
123     };
124     // Run full scan of .META. catalog table passing in our custom visitor
125     MetaReader.fullScan(this.server.getCatalogTracker(), visitor);
126     // Now work on our list of found parents. See if any we can clean up.
127     int cleaned = 0;
128     for (Map.Entry<HRegionInfo, Result> e : splitParents.entrySet()) {
129       if (cleanParent(e.getKey(), e.getValue())) cleaned++;
130     }
131     if (cleaned != 0) {
132       LOG.info("Scanned " + count.get() + " catalog row(s) and gc'd " + cleaned +
133         " unreferenced parent region(s)");
134     } else if (LOG.isDebugEnabled()) {
135       LOG.debug("Scanned " + count.get() + " catalog row(s) and gc'd " + cleaned +
136       " unreferenced parent region(s)");
137     }
138   }
139 
140   /**
141    * Compare HRegionInfos in a way that has split parents sort BEFORE their
142    * daughters.
143    */
144   static class SplitParentFirstComparator implements Comparator<HRegionInfo> {
145     @Override
146     public int compare(HRegionInfo left, HRegionInfo right) {
147       // This comparator differs from the one HRegionInfo in that it sorts
148       // parent before daughters.
149       if (left == null) return -1;
150       if (right == null) return 1;
151       // Same table name.
152       int result = Bytes.compareTo(left.getTableName(),
153           right.getTableName());
154       if (result != 0) return result;
155       // Compare start keys.
156       result = Bytes.compareTo(left.getStartKey(), right.getStartKey());
157       if (result != 0) return result;
158       // Compare end keys.
159       result = Bytes.compareTo(left.getEndKey(), right.getEndKey());
160       if (result != 0) return -result; // Flip the result so parent comes first.
161       return result;
162     }
163   }
164 
165   /**
166    * Get HRegionInfo from passed Map of row values.
167    * @param result Map to do lookup in.
168    * @return Null if not found (and logs fact that expected COL_REGIONINFO
169    * was missing) else deserialized {@link HRegionInfo}
170    * @throws IOException
171    */
172   static HRegionInfo getHRegionInfo(final Result result)
173   throws IOException {
174     byte [] bytes =
175       result.getValue(HConstants.CATALOG_FAMILY, HConstants.REGIONINFO_QUALIFIER);
176     if (bytes == null) {
177       LOG.warn("REGIONINFO_QUALIFIER is empty in " + result);
178       return null;
179     }
180     return Writables.getHRegionInfo(bytes);
181   }
182 
183   /**
184    * If daughters no longer hold reference to the parents, delete the parent.
185    * @param server HRegionInterface of meta server to talk to 
186    * @param parent HRegionInfo of split offlined parent
187    * @param rowContent Content of <code>parent</code> row in
188    * <code>metaRegionName</code>
189    * @return True if we removed <code>parent</code> from meta table and from
190    * the filesystem.
191    * @throws IOException
192    */
193   boolean cleanParent(final HRegionInfo parent, Result rowContent)
194   throws IOException {
195     boolean result = false;
196     // Run checks on each daughter split.
197     HRegionInfo a_region = getDaughterRegionInfo(rowContent, HConstants.SPLITA_QUALIFIER);
198     HRegionInfo b_region = getDaughterRegionInfo(rowContent, HConstants.SPLITB_QUALIFIER);
199     Pair<Boolean, Boolean> a =
200       checkDaughterInFs(parent, a_region, HConstants.SPLITA_QUALIFIER);
201     Pair<Boolean, Boolean> b =
202       checkDaughterInFs(parent, b_region, HConstants.SPLITB_QUALIFIER);
203     if (hasNoReferences(a) && hasNoReferences(b)) {
204       LOG.debug("Deleting region " + parent.getRegionNameAsString() +
205         " because daughter splits no longer hold references");
206 	  // wipe out daughter references from parent region
207       removeDaughtersFromParent(parent);
208 
209       // This latter regionOffline should not be necessary but is done for now
210       // until we let go of regionserver to master heartbeats.  See HBASE-3368.
211       if (this.services.getAssignmentManager() != null) {
212         // The mock used in testing catalogjanitor returns null for getAssignmnetManager.
213         // Allow for null result out of getAssignmentManager.
214         this.services.getAssignmentManager().regionOffline(parent);
215       }
216       FileSystem fs = this.services.getMasterFileSystem().getFileSystem();
217       Path rootdir = this.services.getMasterFileSystem().getRootDir();
218       HRegion.deleteRegion(fs, rootdir, parent);
219       MetaEditor.deleteRegion(this.server.getCatalogTracker(), parent);
220       result = true;
221     }
222     return result;
223   }
224 
225   /**
226    * @param p A pair where the first boolean says whether or not the daughter
227    * region directory exists in the filesystem and then the second boolean says
228    * whether the daughter has references to the parent.
229    * @return True the passed <code>p</code> signifies no references.
230    */
231   private boolean hasNoReferences(final Pair<Boolean, Boolean> p) {
232     return !p.getFirst() || !p.getSecond();
233   }
234 
235   /**
236    * Get daughter HRegionInfo out of parent info:splitA/info:splitB columns.
237    * @param result
238    * @param which Whether "info:splitA" or "info:splitB" column
239    * @return Deserialized content of the info:splitA or info:splitB as a
240    * HRegionInfo
241    * @throws IOException
242    */
243   private HRegionInfo getDaughterRegionInfo(final Result result,
244     final byte [] which)
245   throws IOException {
246     byte [] bytes = result.getValue(HConstants.CATALOG_FAMILY, which);
247     return Writables.getHRegionInfoOrNull(bytes);
248   }
249 
250   /**
251    * Remove mention of daughters from parent row.
252    * @param parent
253    * @throws IOException
254    */
255   private void removeDaughtersFromParent(final HRegionInfo parent)
256   throws IOException {
257     MetaEditor.deleteDaughtersReferencesInParent(this.server.getCatalogTracker(), parent);
258   }
259 
260   /**
261    * Checks if a daughter region -- either splitA or splitB -- still holds
262    * references to parent.
263    * @param parent Parent region name. 
264    * @param split Which column family.
265    * @param qualifier Which of the daughters to look at, splitA or splitB.
266    * @return A pair where the first boolean says whether or not the daughter
267    * region directory exists in the filesystem and then the second boolean says
268    * whether the daughter has references to the parent.
269    * @throws IOException
270    */
271   Pair<Boolean, Boolean> checkDaughterInFs(final HRegionInfo parent,
272     final HRegionInfo split,
273     final byte [] qualifier)
274   throws IOException {
275     boolean references = false;
276     boolean exists = false;
277     if (split == null)  {
278       return new Pair<Boolean, Boolean>(Boolean.FALSE, Boolean.FALSE);
279     }
280     FileSystem fs = this.services.getMasterFileSystem().getFileSystem();
281     Path rootdir = this.services.getMasterFileSystem().getRootDir();
282     Path tabledir = new Path(rootdir, split.getTableNameAsString());
283     Path regiondir = new Path(tabledir, split.getEncodedName());
284     exists = fs.exists(regiondir);
285     if (!exists) {
286       LOG.warn("Daughter regiondir does not exist: " + regiondir.toString());
287       return new Pair<Boolean, Boolean>(exists, Boolean.FALSE);
288     }
289     HTableDescriptor parentDescriptor = getTableDescriptor(parent.getTableName());
290 
291     for (HColumnDescriptor family: parentDescriptor.getFamilies()) {
292       Path p = Store.getStoreHomedir(tabledir, split.getEncodedName(),
293         family.getName());
294       if (!fs.exists(p)) continue;
295       // Look for reference files.  Call listStatus with anonymous instance of PathFilter.
296       FileStatus [] ps = FSUtils.listStatus(fs, p,
297           new PathFilter () {
298             public boolean accept(Path path) {
299               return StoreFile.isReference(path);
300             }
301           }
302       );
303 
304       if (ps != null && ps.length > 0) {
305         references = true;
306         break;
307       }
308     }
309     return new Pair<Boolean, Boolean>(Boolean.valueOf(exists),
310       Boolean.valueOf(references));
311   }
312 
313   private HTableDescriptor getTableDescriptor(byte[] tableName)
314   throws TableExistsException, FileNotFoundException, IOException {
315     return this.services.getTableDescriptors().get(Bytes.toString(tableName));
316   }
317 }