001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase.mob; 019 020import static org.apache.hadoop.hbase.regionserver.ScanType.COMPACT_DROP_DELETES; 021import static org.apache.hadoop.hbase.regionserver.ScanType.COMPACT_RETAIN_DELETES; 022 023import java.io.FileNotFoundException; 024import java.io.IOException; 025import java.io.InterruptedIOException; 026import java.util.ArrayList; 027import java.util.Date; 028import java.util.HashMap; 029import java.util.List; 030import java.util.Map.Entry; 031import java.util.Optional; 032import java.util.function.Consumer; 033import org.apache.hadoop.conf.Configuration; 034import org.apache.hadoop.fs.FileStatus; 035import org.apache.hadoop.fs.FileSystem; 036import org.apache.hadoop.fs.Path; 037import org.apache.hadoop.hbase.DoNotRetryIOException; 038import org.apache.hadoop.hbase.ExtendedCell; 039import org.apache.hadoop.hbase.KeyValue; 040import org.apache.hadoop.hbase.KeyValueUtil; 041import org.apache.hadoop.hbase.PrivateCellUtil; 042import org.apache.hadoop.hbase.TableName; 043import org.apache.hadoop.hbase.regionserver.CellSink; 044import org.apache.hadoop.hbase.regionserver.HMobStore; 045import org.apache.hadoop.hbase.regionserver.HStore; 046import org.apache.hadoop.hbase.regionserver.HStoreFile; 047import org.apache.hadoop.hbase.regionserver.InternalScanner; 048import org.apache.hadoop.hbase.regionserver.KeyValueScanner; 049import org.apache.hadoop.hbase.regionserver.ScanInfo; 050import org.apache.hadoop.hbase.regionserver.ScanType; 051import org.apache.hadoop.hbase.regionserver.ScannerContext; 052import org.apache.hadoop.hbase.regionserver.ShipperListener; 053import org.apache.hadoop.hbase.regionserver.StoreFileScanner; 054import org.apache.hadoop.hbase.regionserver.StoreFileWriter; 055import org.apache.hadoop.hbase.regionserver.StoreScanner; 056import org.apache.hadoop.hbase.regionserver.compactions.CloseChecker; 057import org.apache.hadoop.hbase.regionserver.compactions.CompactionProgress; 058import org.apache.hadoop.hbase.regionserver.compactions.CompactionRequestImpl; 059import org.apache.hadoop.hbase.regionserver.compactions.DefaultCompactor; 060import org.apache.hadoop.hbase.regionserver.throttle.ThroughputControlUtil; 061import org.apache.hadoop.hbase.regionserver.throttle.ThroughputController; 062import org.apache.hadoop.hbase.security.User; 063import org.apache.hadoop.hbase.util.Bytes; 064import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; 065import org.apache.yetus.audience.InterfaceAudience; 066import org.slf4j.Logger; 067import org.slf4j.LoggerFactory; 068 069import org.apache.hbase.thirdparty.com.google.common.collect.HashMultimap; 070import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableSetMultimap; 071import org.apache.hbase.thirdparty.com.google.common.collect.Lists; 072import org.apache.hbase.thirdparty.com.google.common.collect.SetMultimap; 073 074/** 075 * Compact passed set of files in the mob-enabled column family. 076 */ 077@InterfaceAudience.Private 078public class DefaultMobStoreCompactor extends DefaultCompactor { 079 080 private static final Logger LOG = LoggerFactory.getLogger(DefaultMobStoreCompactor.class); 081 protected long mobSizeThreshold; 082 protected HMobStore mobStore; 083 protected boolean ioOptimizedMode = false; 084 protected final boolean cacheMobBlocksOnCompaction; 085 086 /* 087 * MOB file reference set thread local variable. It contains set of a MOB file names, which newly 088 * compacted store file has references to. This variable is populated during compaction and the 089 * content of it is written into meta section of a newly created store file at the final step of 090 * compaction process. 091 */ 092 static ThreadLocal<SetMultimap<TableName, String>> mobRefSet = 093 ThreadLocal.withInitial(HashMultimap::create); 094 095 /* 096 * Is it user or system-originated request. 097 */ 098 099 static ThreadLocal<Boolean> userRequest = new ThreadLocal<Boolean>() { 100 @Override 101 protected Boolean initialValue() { 102 return Boolean.FALSE; 103 } 104 }; 105 106 /* 107 * Disable IO mode. IO mode can be forcefully disabled if compactor finds old MOB file 108 * (pre-distributed compaction). This means that migration has not been completed yet. During data 109 * migration (upgrade) process only general compaction is allowed. 110 */ 111 112 static ThreadLocal<Boolean> disableIO = new ThreadLocal<Boolean>() { 113 114 @Override 115 protected Boolean initialValue() { 116 return Boolean.FALSE; 117 } 118 }; 119 120 /* 121 * Map : MOB file name - file length Can be expensive for large amount of MOB files. 122 */ 123 static ThreadLocal<HashMap<String, Long>> mobLengthMap = 124 new ThreadLocal<HashMap<String, Long>>() { 125 @Override 126 protected HashMap<String, Long> initialValue() { 127 return new HashMap<String, Long>(); 128 } 129 }; 130 131 private final InternalScannerFactory scannerFactory = new InternalScannerFactory() { 132 133 @Override 134 public ScanType getScanType(CompactionRequestImpl request) { 135 return request.isAllFiles() ? COMPACT_DROP_DELETES : COMPACT_RETAIN_DELETES; 136 } 137 138 @Override 139 public InternalScanner createScanner(ScanInfo scanInfo, List<StoreFileScanner> scanners, 140 ScanType scanType, FileDetails fd, long smallestReadPoint) throws IOException { 141 return new StoreScanner(store, scanInfo, scanners, scanType, smallestReadPoint, 142 fd.earliestPutTs); 143 } 144 }; 145 146 private final CellSinkFactory<StoreFileWriter> writerFactory = 147 new CellSinkFactory<StoreFileWriter>() { 148 @Override 149 public StoreFileWriter createWriter(InternalScanner scanner, 150 org.apache.hadoop.hbase.regionserver.compactions.Compactor.FileDetails fd, 151 boolean shouldDropBehind, boolean major, Consumer<Path> writerCreationTracker) 152 throws IOException { 153 // make this writer with tags always because of possible new cells with tags. 154 return store.getStoreEngine() 155 .createWriter(createParams(fd, shouldDropBehind, major, writerCreationTracker) 156 .includeMVCCReadpoint(true).includesTag(true)); 157 } 158 }; 159 160 public DefaultMobStoreCompactor(Configuration conf, HStore store) { 161 super(conf, store); 162 // The mob cells reside in the mob-enabled column family which is held by HMobStore. 163 // During the compaction, the compactor reads the cells from the mob files and 164 // probably creates new mob files. All of these operations are included in HMobStore, 165 // so we need to cast the Store to HMobStore. 166 if (!(store instanceof HMobStore)) { 167 throw new IllegalArgumentException("The store " + store + " is not a HMobStore"); 168 } 169 this.mobStore = (HMobStore) store; 170 this.mobSizeThreshold = store.getColumnFamilyDescriptor().getMobThreshold(); 171 this.ioOptimizedMode = 172 conf.get(MobConstants.MOB_COMPACTION_TYPE_KEY, MobConstants.DEFAULT_MOB_COMPACTION_TYPE) 173 .equals(MobConstants.OPTIMIZED_MOB_COMPACTION_TYPE); 174 this.cacheMobBlocksOnCompaction = conf.getBoolean(MobConstants.MOB_COMPACTION_READ_CACHE_BLOCKS, 175 MobConstants.DEFAULT_MOB_COMPACTION_READ_CACHE_BLOCKS); 176 } 177 178 /** 179 * Resolves a MOB reference cell to its backing MOB value and returns an independent, 180 * heap-resident copy of the resolved cell. 181 * <p> 182 * A MOB cell resolved from a MOB file is backed by a {@code StoreFileScanner}; closing the 183 * {@link MobCell} closes that scanner and may release/recycle the NIO buffers referenced by the 184 * returned cell. We close the {@link MobCell} here to avoid leaking scanners/buffers while 185 * compacting many reference cells. 186 * <p> 187 * The {@link KeyValueUtil#copyToNewKeyValue(ExtendedCell)} call is required by this ownership 188 * model: HFile writers and encoders may retain references to appended cells (e.g. 189 * {@code lastCell}, {@code firstCellInBlock}, and the data block encoder's {@code prevCell}) 190 * until {@code beforeShipped()}. Returning the scanner-backed cell directly would let those later 191 * reads access released buffers. Removing this copy would require changing the caller to retain 192 * each {@link MobCell} and close it only after the writers have shipped their retained 193 * references. 194 */ 195 protected ExtendedCell resolveMobCell(ExtendedCell reference) throws IOException { 196 try (MobCell mobCell = mobStore.resolve(reference, cacheMobBlocksOnCompaction, false)) { 197 return KeyValueUtil.copyToNewKeyValue(mobCell.getCell()); 198 } 199 } 200 201 @Override 202 public List<Path> compact(CompactionRequestImpl request, 203 ThroughputController throughputController, User user) throws IOException { 204 String tableName = store.getTableName().toString(); 205 String regionName = store.getRegionInfo().getRegionNameAsString(); 206 String familyName = store.getColumnFamilyName(); 207 LOG.info( 208 "MOB compaction: major={} isAll={} priority={} throughput controller={}" 209 + " table={} cf={} region={}", 210 request.isMajor(), request.isAllFiles(), request.getPriority(), throughputController, 211 tableName, familyName, regionName); 212 if (request.getPriority() == HStore.PRIORITY_USER) { 213 userRequest.set(Boolean.TRUE); 214 } else { 215 userRequest.set(Boolean.FALSE); 216 } 217 LOG.debug("MOB compaction table={} cf={} region={} files: {}", tableName, familyName, 218 regionName, request.getFiles()); 219 // Check if I/O optimized MOB compaction 220 if (ioOptimizedMode) { 221 if (request.isMajor() && request.getPriority() == HStore.PRIORITY_USER) { 222 try { 223 final SetMultimap<TableName, String> mobRefs = request.getFiles().stream().map(file -> { 224 byte[] value = file.getMetadataValue(HStoreFile.MOB_FILE_REFS); 225 ImmutableSetMultimap.Builder<TableName, String> builder; 226 if (value == null) { 227 builder = ImmutableSetMultimap.builder(); 228 } else { 229 try { 230 builder = MobUtils.deserializeMobFileRefs(value); 231 } catch (RuntimeException exception) { 232 throw new RuntimeException("failure getting mob references for hfile " + file, 233 exception); 234 } 235 } 236 return builder; 237 }).reduce((a, b) -> a.putAll(b.build())).orElseGet(ImmutableSetMultimap::builder).build(); 238 // reset disableIO 239 disableIO.set(Boolean.FALSE); 240 if (!mobRefs.isEmpty()) { 241 calculateMobLengthMap(mobRefs); 242 } 243 LOG.info( 244 "Table={} cf={} region={}. I/O optimized MOB compaction. " 245 + "Total referenced MOB files: {}", 246 tableName, familyName, regionName, mobRefs.size()); 247 } catch (RuntimeException exception) { 248 throw new IOException("Failed to get list of referenced hfiles for request " + request, 249 exception); 250 } 251 } 252 } 253 254 return compact(request, scannerFactory, writerFactory, throughputController, user); 255 } 256 257 /** 258 * @param mobRefs multimap of original table name -> mob hfile 259 */ 260 private void calculateMobLengthMap(SetMultimap<TableName, String> mobRefs) throws IOException { 261 FileSystem fs = store.getFileSystem(); 262 HashMap<String, Long> map = mobLengthMap.get(); 263 map.clear(); 264 for (Entry<TableName, String> reference : mobRefs.entries()) { 265 final TableName table = reference.getKey(); 266 final String mobfile = reference.getValue(); 267 if (MobFileName.isOldMobFileName(mobfile)) { 268 disableIO.set(Boolean.TRUE); 269 } 270 List<Path> locations = mobStore.getLocations(table); 271 for (Path p : locations) { 272 try { 273 FileStatus st = fs.getFileStatus(new Path(p, mobfile)); 274 long size = st.getLen(); 275 LOG.debug("Referenced MOB file={} size={}", mobfile, size); 276 map.put(mobfile, size); 277 break; 278 } catch (FileNotFoundException exception) { 279 LOG.debug("Mob file {} was not in location {}. May have other locations to try.", mobfile, 280 p); 281 } 282 } 283 if (!map.containsKey(mobfile)) { 284 throw new FileNotFoundException("Could not find mob file " + mobfile + " in the list of " 285 + "expected locations: " + locations); 286 } 287 } 288 } 289 290 /** 291 * Performs compaction on a column family with the mob flag enabled. This works only when MOB 292 * compaction is explicitly requested (by User), or by Master There are two modes of a MOB 293 * compaction:<br> 294 * <p> 295 * <ul> 296 * <li>1. Full mode - when all MOB data for a region is compacted into a single MOB file. 297 * <li>2. I/O optimized mode - for use cases with no or infrequent updates/deletes of a <br> 298 * MOB data. The main idea behind i/o optimized compaction is to limit maximum size of a MOB file 299 * produced during compaction and to limit I/O write/read amplification. 300 * </ul> 301 * The basic algorithm of compaction is the following: <br> 302 * 1. If the Put cell has a mob reference tag, the cell's value is the path of the mob file. 303 * <ol> 304 * <li>If the value size of a cell is larger than the threshold, this cell is regarded as a mob, 305 * directly copy the (with mob tag) cell into the new store file.</li> 306 * <li>Otherwise, retrieve the mob cell from the mob file, and writes a copy of the cell into the 307 * new store file.</li> 308 * </ol> 309 * 2. If the Put cell doesn't have a reference tag. 310 * <ol> 311 * <li>If the value size of a cell is larger than the threshold, this cell is regarded as a mob, 312 * write this cell to a mob file, and write the path of this mob file to the store file.</li> 313 * <li>Otherwise, directly write this cell into the store file.</li> 314 * </ol> 315 * @param fd File details 316 * @param scanner Where to read from. 317 * @param writer Where to write to. 318 * @param smallestReadPoint Smallest read point. 319 * @param cleanSeqId When true, remove seqId(used to be mvcc) value which is <= 320 * smallestReadPoint 321 * @param throughputController The compaction throughput controller. 322 * @param request compaction request. 323 * @param progress Progress reporter. 324 * @return Whether compaction ended; false if it was interrupted for any reason. 325 */ 326 @Override 327 protected boolean performCompaction(FileDetails fd, InternalScanner scanner, CellSink writer, 328 long smallestReadPoint, boolean cleanSeqId, ThroughputController throughputController, 329 CompactionRequestImpl request, CompactionProgress progress) throws IOException { 330 long bytesWrittenProgressForLog = 0; 331 long bytesWrittenProgressForShippedCall = 0; 332 // Clear old mob references 333 mobRefSet.get().clear(); 334 boolean isUserRequest = userRequest.get(); 335 boolean major = request.isAllFiles(); 336 boolean compactMOBs = major && isUserRequest; 337 boolean discardMobMiss = conf.getBoolean(MobConstants.MOB_UNSAFE_DISCARD_MISS_KEY, 338 MobConstants.DEFAULT_MOB_DISCARD_MISS); 339 if (discardMobMiss) { 340 LOG.warn("{}=true. This is unsafe setting recommended only when first upgrading to a version" 341 + " with the distributed mob compaction feature on a cluster that has experienced MOB data " 342 + "corruption.", MobConstants.MOB_UNSAFE_DISCARD_MISS_KEY); 343 } 344 long maxMobFileSize = conf.getLong(MobConstants.MOB_COMPACTION_MAX_FILE_SIZE_KEY, 345 MobConstants.DEFAULT_MOB_COMPACTION_MAX_FILE_SIZE); 346 boolean ioOptimizedMode = this.ioOptimizedMode && !disableIO.get(); 347 LOG.info( 348 "Compact MOB={} optimized configured={} optimized enabled={} maximum MOB file size={}" 349 + " major={} store={}", 350 compactMOBs, this.ioOptimizedMode, ioOptimizedMode, maxMobFileSize, major, getStoreInfo()); 351 // Since scanner.next() can return 'false' but still be delivering data, 352 // we have to use a do/while loop. 353 List<ExtendedCell> cells = new ArrayList<>(); 354 // Limit to "hbase.hstore.compaction.kv.max" (default 10) to avoid OOME 355 long currentTime = EnvironmentEdgeManager.currentTime(); 356 long lastMillis = 0; 357 if (LOG.isDebugEnabled()) { 358 lastMillis = currentTime; 359 } 360 CloseChecker closeChecker = new CloseChecker(conf, currentTime); 361 String compactionName = ThroughputControlUtil.getNameForThrottling(store, "compaction"); 362 long now = 0; 363 boolean hasMore; 364 byte[] fileName = null; 365 StoreFileWriter mobFileWriter = null; 366 /* 367 * mobCells are used only to decide if we need to commit or abort current MOB output file. 368 */ 369 long mobCells = 0; 370 long cellsCountCompactedToMob = 0, cellsCountCompactedFromMob = 0; 371 long cellsSizeCompactedToMob = 0, cellsSizeCompactedFromMob = 0; 372 boolean finished = false; 373 374 ScannerContext scannerContext = ScannerContext.newBuilder().setBatchLimit(compactionKVMax) 375 .setSizeLimit(ScannerContext.LimitScope.BETWEEN_CELLS, Long.MAX_VALUE, Long.MAX_VALUE, 376 compactScannerSizeLimit) 377 .build(); 378 throughputController.start(compactionName); 379 KeyValueScanner kvs = (scanner instanceof KeyValueScanner) ? (KeyValueScanner) scanner : null; 380 long shippedCallSizeLimit = Math.min(compactScannerSizeLimit, 381 (long) request.getFiles().size() * this.store.getColumnFamilyDescriptor().getBlocksize()); 382 383 ExtendedCell mobCell = null; 384 List<String> committedMobWriterFileNames = new ArrayList<>(); 385 try { 386 387 mobFileWriter = newMobWriter(fd, major, request.getWriterCreationTracker()); 388 fileName = Bytes.toBytes(mobFileWriter.getPath().getName()); 389 390 do { 391 hasMore = scanner.next(cells, scannerContext); 392 currentTime = EnvironmentEdgeManager.currentTime(); 393 if (LOG.isDebugEnabled()) { 394 now = currentTime; 395 } 396 if (closeChecker.isTimeLimit(store, currentTime)) { 397 progress.cancel(); 398 return false; 399 } 400 for (ExtendedCell c : cells) { 401 if (compactMOBs) { 402 if (MobUtils.isMobReferenceCell(c)) { 403 String fName = MobUtils.getMobFileName(c); 404 // Added to support migration 405 try { 406 mobCell = resolveMobCell(c); 407 } catch (DoNotRetryIOException e) { 408 if ( 409 discardMobMiss && e.getCause() != null 410 && e.getCause() instanceof FileNotFoundException 411 ) { 412 LOG.error("Missing MOB cell: file={} not found cell={}", fName, c); 413 continue; 414 } else { 415 throw e; 416 } 417 } 418 419 if (discardMobMiss && mobCell.getValueLength() == 0) { 420 LOG.error("Missing MOB cell value: file={} mob cell={} cell={}", fName, mobCell, c); 421 continue; 422 } else if (mobCell.getValueLength() == 0) { 423 String errMsg = 424 String.format("Found 0 length MOB cell in a file=%s mob cell=%s " + " cell=%s", 425 fName, mobCell, c); 426 throw new IOException(errMsg); 427 } 428 429 if (mobCell.getValueLength() > mobSizeThreshold) { 430 // put the mob data back to the MOB store file 431 PrivateCellUtil.setSequenceId(mobCell, c.getSequenceId()); 432 if (!ioOptimizedMode) { 433 mobFileWriter.append(mobCell); 434 mobCells++; 435 writer.append( 436 MobUtils.createMobRefCell(mobCell, fileName, this.mobStore.getRefCellTags())); 437 } else { 438 // I/O optimized mode 439 // Check if MOB cell origin file size is 440 // greater than threshold 441 Long size = mobLengthMap.get().get(fName); 442 if (size == null) { 443 // FATAL error (we should never get here though), abort compaction 444 // This error means that meta section of store file does not contain 445 // MOB file, which has references in at least one cell from this store file 446 String msg = String.format( 447 "Found an unexpected MOB file during compaction %s, aborting compaction %s", 448 fName, getStoreInfo()); 449 throw new IOException(msg); 450 } 451 // Can not be null 452 if (size < maxMobFileSize) { 453 // If MOB cell origin file is below threshold 454 // it is get compacted 455 mobFileWriter.append(mobCell); 456 // Update number of mobCells in a current mob writer 457 mobCells++; 458 writer.append( 459 MobUtils.createMobRefCell(mobCell, fileName, this.mobStore.getRefCellTags())); 460 // Update total size of the output (we do not take into account 461 // file compression yet) 462 long len = mobFileWriter.getPos(); 463 if (len > maxMobFileSize) { 464 LOG.debug("Closing output MOB File, length={} file={}, store={}", len, 465 mobFileWriter.getPath().getName(), getStoreInfo()); 466 mobFileWriter = switchToNewMobWriter(mobFileWriter, fd, mobCells, major, 467 request, committedMobWriterFileNames); 468 fileName = Bytes.toBytes(mobFileWriter.getPath().getName()); 469 mobCells = 0; 470 } 471 } else { 472 // We leave large MOB file as is (is not compacted), 473 // then we update set of MOB file references 474 // and append mob cell directly to the store's writer 475 Optional<TableName> refTable = MobUtils.getTableName(c); 476 if (refTable.isPresent()) { 477 mobRefSet.get().put(refTable.get(), fName); 478 writer.append(c); 479 } else { 480 throw new IOException(String.format("MOB cell did not contain a tablename " 481 + "tag. should not be possible. see ref guide on mob troubleshooting. " 482 + "store=%s cell=%s", getStoreInfo(), c)); 483 } 484 } 485 } 486 } else { 487 // If MOB value is less than threshold, append it directly to a store file 488 PrivateCellUtil.setSequenceId(mobCell, c.getSequenceId()); 489 writer.append(mobCell); 490 cellsCountCompactedFromMob++; 491 cellsSizeCompactedFromMob += mobCell.getValueLength(); 492 } 493 } else { 494 // Not a MOB reference cell 495 int size = c.getValueLength(); 496 if (size > mobSizeThreshold) { 497 // This MOB cell comes from a regular store file 498 // therefore we store it into original mob output 499 mobFileWriter.append(c); 500 writer 501 .append(MobUtils.createMobRefCell(c, fileName, this.mobStore.getRefCellTags())); 502 mobCells++; 503 cellsCountCompactedToMob++; 504 cellsSizeCompactedToMob += c.getValueLength(); 505 if (ioOptimizedMode) { 506 // Update total size of the output (we do not take into account 507 // file compression yet) 508 long len = mobFileWriter.getPos(); 509 if (len > maxMobFileSize) { 510 mobFileWriter = switchToNewMobWriter(mobFileWriter, fd, mobCells, major, 511 request, committedMobWriterFileNames); 512 fileName = Bytes.toBytes(mobFileWriter.getPath().getName()); 513 mobCells = 0; 514 } 515 } 516 } else { 517 // Not a MOB cell, write it directly to a store file 518 writer.append(c); 519 } 520 } 521 } else if (c.getTypeByte() != KeyValue.Type.Put.getCode()) { 522 // Not a major compaction or major with MOB disabled 523 // If the kv type is not put, directly write the cell 524 // to the store file. 525 writer.append(c); 526 } else if (MobUtils.isMobReferenceCell(c)) { 527 // Not a major MOB compaction, Put MOB reference 528 if (MobUtils.hasValidMobRefCellValue(c)) { 529 // We do not check mobSizeThreshold during normal compaction, 530 // leaving it to a MOB compaction run 531 Optional<TableName> refTable = MobUtils.getTableName(c); 532 if (refTable.isPresent()) { 533 mobRefSet.get().put(refTable.get(), MobUtils.getMobFileName(c)); 534 writer.append(c); 535 } else { 536 throw new IOException(String.format("MOB cell did not contain a tablename " 537 + "tag. should not be possible. see ref guide on mob troubleshooting. " 538 + "store=%s cell=%s", getStoreInfo(), c)); 539 } 540 } else { 541 String errMsg = String.format("Corrupted MOB reference: %s", c.toString()); 542 throw new IOException(errMsg); 543 } 544 } else if (c.getValueLength() <= mobSizeThreshold) { 545 // If the value size of a cell is not larger than the threshold, directly write it to 546 // the store file. 547 writer.append(c); 548 } else { 549 // If the value size of a cell is larger than the threshold, it's regarded as a mob, 550 // write this cell to a mob file, and write the path to the store file. 551 mobCells++; 552 // append the original keyValue in the mob file. 553 mobFileWriter.append(c); 554 ExtendedCell reference = 555 MobUtils.createMobRefCell(c, fileName, this.mobStore.getRefCellTags()); 556 // write the cell whose value is the path of a mob file to the store file. 557 writer.append(reference); 558 cellsCountCompactedToMob++; 559 cellsSizeCompactedToMob += c.getValueLength(); 560 if (ioOptimizedMode) { 561 long len = mobFileWriter.getPos(); 562 if (len > maxMobFileSize) { 563 mobFileWriter = switchToNewMobWriter(mobFileWriter, fd, mobCells, major, request, 564 committedMobWriterFileNames); 565 fileName = Bytes.toBytes(mobFileWriter.getPath().getName()); 566 mobCells = 0; 567 } 568 } 569 } 570 571 int len = c.getSerializedSize(); 572 ++progress.currentCompactedKVs; 573 progress.totalCompactedSize += len; 574 bytesWrittenProgressForShippedCall += len; 575 if (LOG.isDebugEnabled()) { 576 bytesWrittenProgressForLog += len; 577 } 578 throughputController.control(compactionName, len); 579 if (closeChecker.isSizeLimit(store, len)) { 580 progress.cancel(); 581 return false; 582 } 583 if (kvs != null && bytesWrittenProgressForShippedCall > shippedCallSizeLimit) { 584 ((ShipperListener) writer).beforeShipped(); 585 kvs.shipped(); 586 scannerContext.clearBlockSizeProgress(); 587 bytesWrittenProgressForShippedCall = 0; 588 } 589 } 590 // Log the progress of long running compactions every minute if 591 // logging at DEBUG level 592 if (LOG.isDebugEnabled()) { 593 if ((now - lastMillis) >= COMPACTION_PROGRESS_LOG_INTERVAL) { 594 String rate = String.format("%.2f", 595 (bytesWrittenProgressForLog / 1024.0) / ((now - lastMillis) / 1000.0)); 596 LOG.debug("Compaction progress: {} {}, rate={} KB/sec, throughputController is {}", 597 compactionName, progress, rate, throughputController); 598 lastMillis = now; 599 bytesWrittenProgressForLog = 0; 600 } 601 } 602 cells.clear(); 603 } while (hasMore); 604 // Commit last MOB writer 605 commitOrAbortMobWriter(mobFileWriter, fd.maxSeqId, mobCells, major); 606 finished = true; 607 } catch (InterruptedException e) { 608 progress.cancel(); 609 throw new InterruptedIOException( 610 "Interrupted while control throughput of compacting " + compactionName); 611 } catch (IOException t) { 612 String msg = "Mob compaction failed for region: " + store.getRegionInfo().getEncodedName(); 613 throw new IOException(msg, t); 614 } finally { 615 // Clone last cell in the final because writer will append last cell when committing. If 616 // don't clone here and once the scanner get closed, then the memory of last cell will be 617 // released. (HBASE-22582) 618 ((ShipperListener) writer).beforeShipped(); 619 throughputController.finish(compactionName); 620 if (!finished && mobFileWriter != null) { 621 // Remove all MOB references because compaction failed 622 clearThreadLocals(); 623 // Abort writer 624 LOG.debug("Aborting writer for {} because of a compaction failure, Store {}", 625 mobFileWriter.getPath(), getStoreInfo()); 626 abortWriter(mobFileWriter); 627 deleteCommittedMobFiles(committedMobWriterFileNames); 628 } 629 } 630 631 mobStore.updateCellsCountCompactedFromMob(cellsCountCompactedFromMob); 632 mobStore.updateCellsCountCompactedToMob(cellsCountCompactedToMob); 633 mobStore.updateCellsSizeCompactedFromMob(cellsSizeCompactedFromMob); 634 mobStore.updateCellsSizeCompactedToMob(cellsSizeCompactedToMob); 635 progress.complete(); 636 return true; 637 } 638 639 protected String getStoreInfo() { 640 return String.format("[table=%s family=%s region=%s]", store.getTableName().getNameAsString(), 641 store.getColumnFamilyName(), store.getRegionInfo().getEncodedName()); 642 } 643 644 private void clearThreadLocals() { 645 mobRefSet.get().clear(); 646 HashMap<String, Long> map = mobLengthMap.get(); 647 if (map != null) { 648 map.clear(); 649 } 650 } 651 652 private StoreFileWriter newMobWriter(FileDetails fd, boolean major, 653 Consumer<Path> writerCreationTracker) throws IOException { 654 try { 655 StoreFileWriter mobFileWriter = mobStore.getStoreEngine().requireWritingToTmpDirFirst() 656 ? mobStore.createWriterInTmp(new Date(fd.latestPutTs), fd.maxKeyCount, 657 major ? majorCompactionCompression : minorCompactionCompression, 658 store.getRegionInfo().getStartKey(), true) 659 : mobStore.createWriter(new Date(fd.latestPutTs), fd.maxKeyCount, 660 major ? majorCompactionCompression : minorCompactionCompression, 661 store.getRegionInfo().getStartKey(), true, writerCreationTracker); 662 LOG.debug("New MOB writer created={} store={}", mobFileWriter.getPath().getName(), 663 getStoreInfo()); 664 // Add reference we get for compact MOB 665 mobRefSet.get().put(store.getTableName(), mobFileWriter.getPath().getName()); 666 return mobFileWriter; 667 } catch (IOException e) { 668 // Bailing out 669 throw new IOException(String.format("Failed to create mob writer, store=%s", getStoreInfo()), 670 e); 671 } 672 } 673 674 private void commitOrAbortMobWriter(StoreFileWriter mobFileWriter, long maxSeqId, long mobCells, 675 boolean major) throws IOException { 676 // Commit or abort major mob writer 677 // If IOException happens during below operation, some 678 // MOB files can be committed partially, but corresponding 679 // store file won't be committed, therefore these MOB files 680 // become orphans and will be deleted during next MOB cleaning chore cycle 681 682 if (mobFileWriter != null) { 683 LOG.debug("Commit or abort size={} mobCells={} major={} file={}, store={}", 684 mobFileWriter.getPos(), mobCells, major, mobFileWriter.getPath().getName(), getStoreInfo()); 685 Path path = 686 MobUtils.getMobFamilyPath(conf, store.getTableName(), store.getColumnFamilyName()); 687 if (mobCells > 0) { 688 // If the mob file is not empty, commit it. 689 mobFileWriter.appendMetadata(maxSeqId, major, mobCells); 690 mobFileWriter.close(); 691 mobStore.commitFile(mobFileWriter.getPath(), path); 692 } else { 693 // If the mob file is empty, delete it instead of committing. 694 LOG.debug("Aborting writer for {} because there are no MOB cells, store={}", 695 mobFileWriter.getPath(), getStoreInfo()); 696 // Remove MOB file from reference set 697 mobRefSet.get().remove(store.getTableName(), mobFileWriter.getPath().getName()); 698 abortWriter(mobFileWriter); 699 } 700 } else { 701 LOG.debug("Mob file writer is null, skipping commit/abort, store=", getStoreInfo()); 702 } 703 } 704 705 @Override 706 protected List<Path> commitWriter(StoreFileWriter writer, FileDetails fd, 707 CompactionRequestImpl request) throws IOException { 708 List<Path> newFiles = Lists.newArrayList(writer.getPath()); 709 writer.appendMetadata(fd.maxSeqId, request.isAllFiles(), request.getFiles()); 710 writer.appendMobMetadata(mobRefSet.get()); 711 writer.close(); 712 clearThreadLocals(); 713 return newFiles; 714 } 715 716 private StoreFileWriter switchToNewMobWriter(StoreFileWriter mobFileWriter, FileDetails fd, 717 long mobCells, boolean major, CompactionRequestImpl request, 718 List<String> committedMobWriterFileNames) throws IOException { 719 commitOrAbortMobWriter(mobFileWriter, fd.maxSeqId, mobCells, major); 720 committedMobWriterFileNames.add(mobFileWriter.getPath().getName()); 721 return newMobWriter(fd, major, request.getWriterCreationTracker()); 722 } 723 724 private void deleteCommittedMobFiles(List<String> fileNames) { 725 if (fileNames.isEmpty()) { 726 return; 727 } 728 Path mobColumnFamilyPath = 729 MobUtils.getMobFamilyPath(conf, store.getTableName(), store.getColumnFamilyName()); 730 for (String fileName : fileNames) { 731 if (fileName == null) { 732 continue; 733 } 734 Path path = new Path(mobColumnFamilyPath, fileName); 735 try { 736 if (store.getFileSystem().exists(path)) { 737 store.getFileSystem().delete(path, false); 738 } 739 } catch (IOException e) { 740 LOG.warn("Failed to delete the mob file {} for an failed mob compaction.", path, e); 741 } 742 } 743 744 } 745 746}