001/** 002 * 003 * Licensed to the Apache Software Foundation (ASF) under one 004 * or more contributor license agreements. See the NOTICE file 005 * distributed with this work for additional information 006 * regarding copyright ownership. The ASF licenses this file 007 * to you under the Apache License, Version 2.0 (the 008 * "License"); you may not use this file except in compliance 009 * with the License. You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 */ 019package org.apache.hadoop.hbase.master; 020 021import java.io.FileNotFoundException; 022import java.io.IOException; 023import java.util.Comparator; 024import java.util.HashSet; 025import java.util.Map; 026import java.util.TreeMap; 027import java.util.concurrent.atomic.AtomicBoolean; 028import java.util.concurrent.atomic.AtomicInteger; 029 030import org.apache.hadoop.fs.FileSystem; 031import org.apache.hadoop.fs.Path; 032import org.apache.hadoop.hbase.HConstants; 033import org.apache.hadoop.hbase.MetaTableAccessor; 034import org.apache.hadoop.hbase.ScheduledChore; 035import org.apache.hadoop.hbase.TableName; 036import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor; 037import org.apache.hadoop.hbase.client.Connection; 038import org.apache.hadoop.hbase.client.RegionInfo; 039import org.apache.hadoop.hbase.client.Result; 040import org.apache.hadoop.hbase.client.TableDescriptor; 041import org.apache.hadoop.hbase.master.assignment.AssignmentManager; 042import org.apache.hadoop.hbase.master.assignment.GCMergedRegionsProcedure; 043import org.apache.hadoop.hbase.master.assignment.GCRegionProcedure; 044import org.apache.hadoop.hbase.master.procedure.MasterProcedureEnv; 045import org.apache.hadoop.hbase.procedure2.ProcedureExecutor; 046import org.apache.hadoop.hbase.regionserver.HRegionFileSystem; 047import org.apache.hadoop.hbase.util.Bytes; 048import org.apache.hadoop.hbase.util.FSUtils; 049import org.apache.hadoop.hbase.util.Pair; 050import org.apache.hadoop.hbase.util.PairOfSameType; 051import org.apache.hadoop.hbase.util.Threads; 052import org.apache.hadoop.hbase.util.Triple; 053import org.apache.yetus.audience.InterfaceAudience; 054import org.slf4j.Logger; 055import org.slf4j.LoggerFactory; 056 057/** 058 * A janitor for the catalog tables. Scans the <code>hbase:meta</code> catalog 059 * table on a period looking for unused regions to garbage collect. 060 */ 061@InterfaceAudience.Private 062public class CatalogJanitor extends ScheduledChore { 063 private static final Logger LOG = LoggerFactory.getLogger(CatalogJanitor.class.getName()); 064 065 private final AtomicBoolean alreadyRunning = new AtomicBoolean(false); 066 private final AtomicBoolean enabled = new AtomicBoolean(true); 067 private final MasterServices services; 068 private final Connection connection; 069 // PID of the last Procedure launched herein. Keep around for Tests. 070 071 CatalogJanitor(final MasterServices services) { 072 super("CatalogJanitor-" + services.getServerName().toShortString(), services, 073 services.getConfiguration().getInt("hbase.catalogjanitor.interval", 300000)); 074 this.services = services; 075 this.connection = services.getConnection(); 076 } 077 078 @Override 079 protected boolean initialChore() { 080 try { 081 if (this.enabled.get()) scan(); 082 } catch (IOException e) { 083 LOG.warn("Failed initial scan of catalog table", e); 084 return false; 085 } 086 return true; 087 } 088 089 /** 090 * @param enabled 091 */ 092 public boolean setEnabled(final boolean enabled) { 093 boolean alreadyEnabled = this.enabled.getAndSet(enabled); 094 // If disabling is requested on an already enabled chore, we could have an active 095 // scan still going on, callers might not be aware of that and do further action thinkng 096 // that no action would be from this chore. In this case, the right action is to wait for 097 // the active scan to complete before exiting this function. 098 if (!enabled && alreadyEnabled) { 099 while (alreadyRunning.get()) { 100 Threads.sleepWithoutInterrupt(100); 101 } 102 } 103 return alreadyEnabled; 104 } 105 106 boolean getEnabled() { 107 return this.enabled.get(); 108 } 109 110 @Override 111 protected void chore() { 112 try { 113 AssignmentManager am = this.services.getAssignmentManager(); 114 if (this.enabled.get() && !this.services.isInMaintenanceMode() && am != null && 115 am.isMetaLoaded() && !am.hasRegionsInTransition()) { 116 scan(); 117 } else { 118 LOG.warn("CatalogJanitor is disabled! Enabled=" + this.enabled.get() + 119 ", maintenanceMode=" + this.services.isInMaintenanceMode() + ", am=" + am + 120 ", metaLoaded=" + (am != null && am.isMetaLoaded()) + ", hasRIT=" + 121 (am != null && am.hasRegionsInTransition())); 122 } 123 } catch (IOException e) { 124 LOG.warn("Failed scan of catalog table", e); 125 } 126 } 127 128 /** 129 * Scans hbase:meta and returns a number of scanned rows, and a map of merged 130 * regions, and an ordered map of split parents. 131 * @return triple of scanned rows, map of merged regions and map of split 132 * parent regioninfos 133 * @throws IOException 134 */ 135 Triple<Integer, Map<RegionInfo, Result>, Map<RegionInfo, Result>> 136 getMergedRegionsAndSplitParents() throws IOException { 137 return getMergedRegionsAndSplitParents(null); 138 } 139 140 /** 141 * Scans hbase:meta and returns a number of scanned rows, and a map of merged 142 * regions, and an ordered map of split parents. if the given table name is 143 * null, return merged regions and split parents of all tables, else only the 144 * specified table 145 * @param tableName null represents all tables 146 * @return triple of scanned rows, and map of merged regions, and map of split 147 * parent regioninfos 148 * @throws IOException 149 */ 150 Triple<Integer, Map<RegionInfo, Result>, Map<RegionInfo, Result>> 151 getMergedRegionsAndSplitParents(final TableName tableName) throws IOException { 152 final boolean isTableSpecified = (tableName != null); 153 // TODO: Only works with single hbase:meta region currently. Fix. 154 final AtomicInteger count = new AtomicInteger(0); 155 // Keep Map of found split parents. There are candidates for cleanup. 156 // Use a comparator that has split parents come before its daughters. 157 final Map<RegionInfo, Result> splitParents = new TreeMap<>(new SplitParentFirstComparator()); 158 final Map<RegionInfo, Result> mergedRegions = new TreeMap<>(RegionInfo.COMPARATOR); 159 // This visitor collects split parents and counts rows in the hbase:meta table 160 161 MetaTableAccessor.Visitor visitor = new MetaTableAccessor.Visitor() { 162 @Override 163 public boolean visit(Result r) throws IOException { 164 if (r == null || r.isEmpty()) return true; 165 count.incrementAndGet(); 166 RegionInfo info = MetaTableAccessor.getRegionInfo(r); 167 if (info == null) return true; // Keep scanning 168 if (isTableSpecified 169 && info.getTable().compareTo(tableName) > 0) { 170 // Another table, stop scanning 171 return false; 172 } 173 if (LOG.isTraceEnabled()) LOG.trace("" + info + " IS-SPLIT_PARENT=" + info.isSplitParent()); 174 if (info.isSplitParent()) splitParents.put(info, r); 175 if (r.getValue(HConstants.CATALOG_FAMILY, HConstants.MERGEA_QUALIFIER) != null) { 176 mergedRegions.put(info, r); 177 } 178 // Returning true means "keep scanning" 179 return true; 180 } 181 }; 182 183 // Run full scan of hbase:meta catalog table passing in our custom visitor with 184 // the start row 185 MetaTableAccessor.scanMetaForTableRegions(this.connection, visitor, tableName); 186 187 return new Triple<>(count.get(), mergedRegions, splitParents); 188 } 189 190 /** 191 * If merged region no longer holds reference to the merge regions, archive 192 * merge region on hdfs and perform deleting references in hbase:meta 193 * @param mergedRegion 194 * @return true if we delete references in merged region on hbase:meta and archive 195 * the files on the file system 196 * @throws IOException 197 */ 198 boolean cleanMergeRegion(final RegionInfo mergedRegion, 199 final RegionInfo regionA, final RegionInfo regionB) throws IOException { 200 FileSystem fs = this.services.getMasterFileSystem().getFileSystem(); 201 Path rootdir = this.services.getMasterFileSystem().getRootDir(); 202 Path tabledir = FSUtils.getTableDir(rootdir, mergedRegion.getTable()); 203 TableDescriptor htd = getTableDescriptor(mergedRegion.getTable()); 204 HRegionFileSystem regionFs = null; 205 try { 206 regionFs = HRegionFileSystem.openRegionFromFileSystem( 207 this.services.getConfiguration(), fs, tabledir, mergedRegion, true); 208 } catch (IOException e) { 209 LOG.warn("Merged region does not exist: " + mergedRegion.getEncodedName()); 210 } 211 if (regionFs == null || !regionFs.hasReferences(htd)) { 212 LOG.debug("Deleting region " + regionA.getShortNameToLog() + " and " 213 + regionB.getShortNameToLog() 214 + " from fs because merged region no longer holds references"); 215 ProcedureExecutor<MasterProcedureEnv> pe = this.services.getMasterProcedureExecutor(); 216 pe.submitProcedure(new GCMergedRegionsProcedure(pe.getEnvironment(), 217 mergedRegion, regionA, regionB)); 218 // Remove from in-memory states 219 this.services.getAssignmentManager().getRegionStates().deleteRegion(regionA); 220 this.services.getAssignmentManager().getRegionStates().deleteRegion(regionB); 221 this.services.getServerManager().removeRegion(regionA); 222 this.services.getServerManager().removeRegion(regionB); 223 return true; 224 } 225 return false; 226 } 227 228 /** 229 * Run janitorial scan of catalog <code>hbase:meta</code> table looking for 230 * garbage to collect. 231 * @return number of archiving jobs started. 232 * @throws IOException 233 */ 234 int scan() throws IOException { 235 int result = 0; 236 237 try { 238 if (!alreadyRunning.compareAndSet(false, true)) { 239 LOG.debug("CatalogJanitor already running"); 240 return result; 241 } 242 Triple<Integer, Map<RegionInfo, Result>, Map<RegionInfo, Result>> scanTriple = 243 getMergedRegionsAndSplitParents(); 244 /** 245 * clean merge regions first 246 */ 247 Map<RegionInfo, Result> mergedRegions = scanTriple.getSecond(); 248 for (Map.Entry<RegionInfo, Result> e : mergedRegions.entrySet()) { 249 if (this.services.isInMaintenanceMode()) { 250 // Stop cleaning if the master is in maintenance mode 251 break; 252 } 253 254 PairOfSameType<RegionInfo> p = MetaTableAccessor.getMergeRegions(e.getValue()); 255 RegionInfo regionA = p.getFirst(); 256 RegionInfo regionB = p.getSecond(); 257 if (regionA == null || regionB == null) { 258 LOG.warn("Unexpected references regionA=" 259 + (regionA == null ? "null" : regionA.getShortNameToLog()) 260 + ",regionB=" 261 + (regionB == null ? "null" : regionB.getShortNameToLog()) 262 + " in merged region " + e.getKey().getShortNameToLog()); 263 } else { 264 if (cleanMergeRegion(e.getKey(), regionA, regionB)) { 265 result++; 266 } 267 } 268 } 269 /** 270 * clean split parents 271 */ 272 Map<RegionInfo, Result> splitParents = scanTriple.getThird(); 273 274 // Now work on our list of found parents. See if any we can clean up. 275 // regions whose parents are still around 276 HashSet<String> parentNotCleaned = new HashSet<>(); 277 for (Map.Entry<RegionInfo, Result> e : splitParents.entrySet()) { 278 if (this.services.isInMaintenanceMode()) { 279 // Stop cleaning if the master is in maintenance mode 280 break; 281 } 282 283 if (!parentNotCleaned.contains(e.getKey().getEncodedName()) && 284 cleanParent(e.getKey(), e.getValue())) { 285 result++; 286 } else { 287 // We could not clean the parent, so it's daughters should not be 288 // cleaned either (HBASE-6160) 289 PairOfSameType<RegionInfo> daughters = 290 MetaTableAccessor.getDaughterRegions(e.getValue()); 291 parentNotCleaned.add(daughters.getFirst().getEncodedName()); 292 parentNotCleaned.add(daughters.getSecond().getEncodedName()); 293 } 294 } 295 return result; 296 } finally { 297 alreadyRunning.set(false); 298 } 299 } 300 301 /** 302 * Compare HRegionInfos in a way that has split parents sort BEFORE their 303 * daughters. 304 */ 305 static class SplitParentFirstComparator implements Comparator<RegionInfo> { 306 Comparator<byte[]> rowEndKeyComparator = new Bytes.RowEndKeyComparator(); 307 @Override 308 public int compare(RegionInfo left, RegionInfo right) { 309 // This comparator differs from the one RegionInfo in that it sorts 310 // parent before daughters. 311 if (left == null) return -1; 312 if (right == null) return 1; 313 // Same table name. 314 int result = left.getTable().compareTo(right.getTable()); 315 if (result != 0) return result; 316 // Compare start keys. 317 result = Bytes.compareTo(left.getStartKey(), right.getStartKey()); 318 if (result != 0) return result; 319 // Compare end keys, but flip the operands so parent comes first 320 result = rowEndKeyComparator.compare(right.getEndKey(), left.getEndKey()); 321 322 return result; 323 } 324 } 325 326 /** 327 * If daughters no longer hold reference to the parents, delete the parent. 328 * @param parent RegionInfo of split offlined parent 329 * @param rowContent Content of <code>parent</code> row in 330 * <code>metaRegionName</code> 331 * @return True if we removed <code>parent</code> from meta table and from 332 * the filesystem. 333 * @throws IOException 334 */ 335 boolean cleanParent(final RegionInfo parent, Result rowContent) 336 throws IOException { 337 // Check whether it is a merged region and not clean reference 338 // No necessary to check MERGEB_QUALIFIER because these two qualifiers will 339 // be inserted/deleted together 340 if (rowContent.getValue(HConstants.CATALOG_FAMILY, HConstants.MERGEA_QUALIFIER) != null) { 341 // wait cleaning merge region first 342 return false; 343 } 344 // Run checks on each daughter split. 345 PairOfSameType<RegionInfo> daughters = MetaTableAccessor.getDaughterRegions(rowContent); 346 Pair<Boolean, Boolean> a = checkDaughterInFs(parent, daughters.getFirst()); 347 Pair<Boolean, Boolean> b = checkDaughterInFs(parent, daughters.getSecond()); 348 if (hasNoReferences(a) && hasNoReferences(b)) { 349 String daughterA = daughters.getFirst() != null? 350 daughters.getFirst().getShortNameToLog(): "null"; 351 String daughterB = daughters.getSecond() != null? 352 daughters.getSecond().getShortNameToLog(): "null"; 353 LOG.debug("Deleting region " + parent.getShortNameToLog() + 354 " because daughters -- " + daughterA + ", " + daughterB + 355 " -- no longer hold references"); 356 ProcedureExecutor<MasterProcedureEnv> pe = this.services.getMasterProcedureExecutor(); 357 pe.submitProcedure(new GCRegionProcedure(pe.getEnvironment(), parent)); 358 // Remove from in-memory states 359 this.services.getAssignmentManager().getRegionStates().deleteRegion(parent); 360 this.services.getServerManager().removeRegion(parent); 361 return true; 362 } 363 return false; 364 } 365 366 /** 367 * @param p A pair where the first boolean says whether or not the daughter 368 * region directory exists in the filesystem and then the second boolean says 369 * whether the daughter has references to the parent. 370 * @return True the passed <code>p</code> signifies no references. 371 */ 372 private boolean hasNoReferences(final Pair<Boolean, Boolean> p) { 373 return !p.getFirst() || !p.getSecond(); 374 } 375 376 /** 377 * Checks if a daughter region -- either splitA or splitB -- still holds 378 * references to parent. 379 * @param parent Parent region 380 * @param daughter Daughter region 381 * @return A pair where the first boolean says whether or not the daughter 382 * region directory exists in the filesystem and then the second boolean says 383 * whether the daughter has references to the parent. 384 * @throws IOException 385 */ 386 Pair<Boolean, Boolean> checkDaughterInFs(final RegionInfo parent, final RegionInfo daughter) 387 throws IOException { 388 if (daughter == null) { 389 return new Pair<>(Boolean.FALSE, Boolean.FALSE); 390 } 391 392 FileSystem fs = this.services.getMasterFileSystem().getFileSystem(); 393 Path rootdir = this.services.getMasterFileSystem().getRootDir(); 394 Path tabledir = FSUtils.getTableDir(rootdir, daughter.getTable()); 395 396 Path daughterRegionDir = new Path(tabledir, daughter.getEncodedName()); 397 398 HRegionFileSystem regionFs = null; 399 400 try { 401 if (!FSUtils.isExists(fs, daughterRegionDir)) { 402 return new Pair<>(Boolean.FALSE, Boolean.FALSE); 403 } 404 } catch (IOException ioe) { 405 LOG.error("Error trying to determine if daughter region exists, " + 406 "assuming exists and has references", ioe); 407 return new Pair<>(Boolean.TRUE, Boolean.TRUE); 408 } 409 410 boolean references = false; 411 TableDescriptor parentDescriptor = getTableDescriptor(parent.getTable()); 412 try { 413 regionFs = HRegionFileSystem.openRegionFromFileSystem( 414 this.services.getConfiguration(), fs, tabledir, daughter, true); 415 416 for (ColumnFamilyDescriptor family: parentDescriptor.getColumnFamilies()) { 417 if ((references = regionFs.hasReferences(family.getNameAsString()))) { 418 break; 419 } 420 } 421 } catch (IOException e) { 422 LOG.error("Error trying to determine referenced files from : " + daughter.getEncodedName() 423 + ", to: " + parent.getEncodedName() + " assuming has references", e); 424 return new Pair<>(Boolean.TRUE, Boolean.TRUE); 425 } 426 return new Pair<>(Boolean.TRUE, Boolean.valueOf(references)); 427 } 428 429 private TableDescriptor getTableDescriptor(final TableName tableName) 430 throws FileNotFoundException, IOException { 431 return this.services.getTableDescriptors().get(tableName); 432 } 433 434 /** 435 * Checks if the specified region has merge qualifiers, if so, try to clean 436 * them 437 * @param region 438 * @return true if the specified region doesn't have merge qualifier now 439 * @throws IOException 440 */ 441 public boolean cleanMergeQualifier(final RegionInfo region) 442 throws IOException { 443 // Get merge regions if it is a merged region and already has merge 444 // qualifier 445 Pair<RegionInfo, RegionInfo> mergeRegions = MetaTableAccessor 446 .getRegionsFromMergeQualifier(this.services.getConnection(), 447 region.getRegionName()); 448 if (mergeRegions == null 449 || (mergeRegions.getFirst() == null && mergeRegions.getSecond() == null)) { 450 // It doesn't have merge qualifier, no need to clean 451 return true; 452 } 453 // It shouldn't happen, we must insert/delete these two qualifiers together 454 if (mergeRegions.getFirst() == null || mergeRegions.getSecond() == null) { 455 LOG.error("Merged region " + region.getRegionNameAsString() 456 + " has only one merge qualifier in META."); 457 return false; 458 } 459 return cleanMergeRegion(region, mergeRegions.getFirst(), 460 mergeRegions.getSecond()); 461 } 462}