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  package org.apache.hadoop.hbase.regionserver.compactions;
19  
20  import java.io.IOException;
21  import java.io.InterruptedIOException;
22  import java.security.PrivilegedExceptionAction;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.Collections;
26  import java.util.List;
27  import java.util.Map;
28  
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  import org.apache.hadoop.conf.Configuration;
32  import org.apache.hadoop.fs.Path;
33  import org.apache.hadoop.hbase.Cell;
34  import org.apache.hadoop.hbase.CellUtil;
35  import org.apache.hadoop.hbase.HConstants;
36  import org.apache.hadoop.hbase.KeyValueUtil;
37  import org.apache.hadoop.hbase.classification.InterfaceAudience;
38  import org.apache.hadoop.hbase.client.Scan;
39  import org.apache.hadoop.hbase.io.compress.Compression;
40  import org.apache.hadoop.hbase.io.hfile.HFile.FileInfo;
41  import org.apache.hadoop.hbase.io.hfile.HFileWriterV2;
42  import org.apache.hadoop.hbase.regionserver.HStore;
43  import org.apache.hadoop.hbase.regionserver.InternalScanner;
44  import org.apache.hadoop.hbase.regionserver.ScanType;
45  import org.apache.hadoop.hbase.regionserver.ScannerContext;
46  import org.apache.hadoop.hbase.regionserver.Store;
47  import org.apache.hadoop.hbase.regionserver.StoreFile;
48  import org.apache.hadoop.hbase.regionserver.StoreFile.Writer;
49  import org.apache.hadoop.hbase.regionserver.StoreFileScanner;
50  import org.apache.hadoop.hbase.regionserver.StoreScanner;
51  import org.apache.hadoop.hbase.regionserver.compactions.Compactor.CellSink;
52  import org.apache.hadoop.hbase.regionserver.throttle.ThroughputControlUtil;
53  import org.apache.hadoop.hbase.regionserver.throttle.ThroughputController;
54  import org.apache.hadoop.hbase.security.User;
55  import org.apache.hadoop.hbase.util.Bytes;
56  import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
57  import org.apache.hadoop.util.StringUtils.TraditionalBinaryPrefix;
58  
59  import com.google.common.io.Closeables;
60  
61  /**
62   * A compactor is a compaction algorithm associated a given policy. Base class also contains
63   * reusable parts for implementing compactors (what is common and what isn't is evolving).
64   */
65  @InterfaceAudience.Private
66  public abstract class Compactor<T extends CellSink> {
67    private static final Log LOG = LogFactory.getLog(Compactor.class);
68  
69    protected volatile CompactionProgress progress;
70  
71    protected final Configuration conf;
72    protected final Store store;
73  
74    protected final int compactionKVMax;
75    protected final Compression.Algorithm compactionCompression;
76  
77    /** specify how many days to keep MVCC values during major compaction **/ 
78    protected int keepSeqIdPeriod;
79  
80    // Configs that drive whether we drop page cache behind compactions
81    protected static final String  MAJOR_COMPACTION_DROP_CACHE =
82        "hbase.regionserver.majorcompaction.pagecache.drop";
83    protected static final String MINOR_COMPACTION_DROP_CACHE =
84        "hbase.regionserver.minorcompaction.pagecache.drop";
85  
86    private boolean dropCacheMajor;
87    private boolean dropCacheMinor;
88  
89    //TODO: depending on Store is not good but, realistically, all compactors currently do.
90    Compactor(final Configuration conf, final Store store) {
91      this.conf = conf;
92      this.store = store;
93      this.compactionKVMax =
94        this.conf.getInt(HConstants.COMPACTION_KV_MAX, HConstants.COMPACTION_KV_MAX_DEFAULT);
95      this.compactionCompression = (this.store.getFamily() == null) ?
96          Compression.Algorithm.NONE : this.store.getFamily().getCompactionCompression();
97      this.keepSeqIdPeriod = Math.max(this.conf.getInt(HConstants.KEEP_SEQID_PERIOD, 
98        HConstants.MIN_KEEP_SEQID_PERIOD), HConstants.MIN_KEEP_SEQID_PERIOD);
99      this.dropCacheMajor = conf.getBoolean(MAJOR_COMPACTION_DROP_CACHE, true);
100     this.dropCacheMinor = conf.getBoolean(MINOR_COMPACTION_DROP_CACHE, true);
101   }
102 
103   public interface CellSink {
104     void append(Cell cell) throws IOException;
105   }
106 
107   protected interface CellSinkFactory<S> {
108     S createWriter(InternalScanner scanner, FileDetails fd, boolean shouldDropBehind)
109         throws IOException;
110   }
111 
112   public CompactionProgress getProgress() {
113     return this.progress;
114   }
115 
116   /** The sole reason this class exists is that java has no ref/out/pointer parameters. */
117   protected static class FileDetails {
118     /** Maximum key count after compaction (for blooms) */
119     public long maxKeyCount = 0;
120     /** Earliest put timestamp if major compaction */
121     public long earliestPutTs = HConstants.LATEST_TIMESTAMP;
122     /** The last key in the files we're compacting. */
123     public long maxSeqId = 0;
124     /** Latest memstore read point found in any of the involved files */
125     public long maxMVCCReadpoint = 0;
126     /** Max tags length**/
127     public int maxTagsLength = 0;
128     /** Min SeqId to keep during a major compaction **/
129     public long minSeqIdToKeep = 0;
130   }
131 
132   /**
133    * Extracts some details about the files to compact that are commonly needed by compactors.
134    * @param filesToCompact Files.
135    * @param allFiles Whether all files are included for compaction
136    * @return The result.
137    */
138   protected FileDetails getFileDetails(
139       Collection<StoreFile> filesToCompact, boolean allFiles) throws IOException {
140     FileDetails fd = new FileDetails();
141     long oldestHFileTimeStampToKeepMVCC = System.currentTimeMillis() - 
142       (1000L * 60 * 60 * 24 * this.keepSeqIdPeriod);  
143 
144     for (StoreFile file : filesToCompact) {
145       if(allFiles && (file.getModificationTimeStamp() < oldestHFileTimeStampToKeepMVCC)) {
146         // when isAllFiles is true, all files are compacted so we can calculate the smallest 
147         // MVCC value to keep
148         if(fd.minSeqIdToKeep < file.getMaxMemstoreTS()) {
149           fd.minSeqIdToKeep = file.getMaxMemstoreTS();
150         }
151       }
152       long seqNum = file.getMaxSequenceId();
153       fd.maxSeqId = Math.max(fd.maxSeqId, seqNum);
154       StoreFile.Reader r = file.getReader();
155       if (r == null) {
156         LOG.warn("Null reader for " + file.getPath());
157         continue;
158       }
159       // NOTE: use getEntries when compacting instead of getFilterEntries, otherwise under-sized
160       // blooms can cause progress to be miscalculated or if the user switches bloom
161       // type (e.g. from ROW to ROWCOL)
162       long keyCount = r.getEntries();
163       fd.maxKeyCount += keyCount;
164       // calculate the latest MVCC readpoint in any of the involved store files
165       Map<byte[], byte[]> fileInfo = r.loadFileInfo();
166       byte[] tmp = null;
167       // Get and set the real MVCCReadpoint for bulk loaded files, which is the
168       // SeqId number.
169       if (r.isBulkLoaded()) {
170         fd.maxMVCCReadpoint = Math.max(fd.maxMVCCReadpoint, r.getSequenceID());
171       }
172       else {
173         tmp = fileInfo.get(HFileWriterV2.MAX_MEMSTORE_TS_KEY);
174         if (tmp != null) {
175           fd.maxMVCCReadpoint = Math.max(fd.maxMVCCReadpoint, Bytes.toLong(tmp));
176         }
177       }
178       tmp = fileInfo.get(FileInfo.MAX_TAGS_LEN);
179       if (tmp != null) {
180         fd.maxTagsLength = Math.max(fd.maxTagsLength, Bytes.toInt(tmp));
181       }
182       // If required, calculate the earliest put timestamp of all involved storefiles.
183       // This is used to remove family delete marker during compaction.
184       long earliestPutTs = 0;
185       if (allFiles) {
186         tmp = fileInfo.get(StoreFile.EARLIEST_PUT_TS);
187         if (tmp == null) {
188           // There's a file with no information, must be an old one
189           // assume we have very old puts
190           fd.earliestPutTs = earliestPutTs = HConstants.OLDEST_TIMESTAMP;
191         } else {
192           earliestPutTs = Bytes.toLong(tmp);
193           fd.earliestPutTs = Math.min(fd.earliestPutTs, earliestPutTs);
194         }
195       }
196       if (LOG.isDebugEnabled()) {
197         LOG.debug("Compacting " + file +
198           ", keycount=" + keyCount +
199           ", bloomtype=" + r.getBloomFilterType().toString() +
200           ", size=" + TraditionalBinaryPrefix.long2String(r.length(), "", 1) +
201           ", encoding=" + r.getHFileReader().getDataBlockEncoding() +
202           ", compression=" + compactionCompression +
203           ", seqNum=" + seqNum +
204           (allFiles ? ", earliestPutTs=" + earliestPutTs: ""));
205       }
206     }
207     return fd;
208   }
209 
210   /**
211    * Creates file scanners for compaction.
212    * @param filesToCompact Files.
213    * @return Scanners.
214    */
215   protected List<StoreFileScanner> createFileScanners(
216       final Collection<StoreFile> filesToCompact,
217       long smallestReadPoint,
218       boolean useDropBehind) throws IOException {
219     return StoreFileScanner.getScannersForStoreFiles(filesToCompact,
220         /* cache blocks = */ false,
221         /* use pread = */ false,
222         /* is compaction */ true,
223         /* use Drop Behind */ useDropBehind,
224       smallestReadPoint);
225   }
226 
227   protected long getSmallestReadPoint() {
228     return store.getSmallestReadPoint();
229   }
230 
231   protected interface InternalScannerFactory {
232 
233     ScanType getScanType(CompactionRequest request);
234 
235     InternalScanner createScanner(List<StoreFileScanner> scanners, ScanType scanType,
236         FileDetails fd, long smallestReadPoint) throws IOException;
237   }
238 
239   protected final InternalScannerFactory defaultScannerFactory = new InternalScannerFactory() {
240 
241     @Override
242     public ScanType getScanType(CompactionRequest request) {
243       return request.isAllFiles() ? ScanType.COMPACT_DROP_DELETES
244           : ScanType.COMPACT_RETAIN_DELETES;
245     }
246 
247     @Override
248     public InternalScanner createScanner(List<StoreFileScanner> scanners, ScanType scanType,
249         FileDetails fd, long smallestReadPoint) throws IOException {
250       return Compactor.this.createScanner(store, scanners, scanType, smallestReadPoint,
251         fd.earliestPutTs);
252     }
253   };
254 
255   /**
256    * Creates a writer for a new file in a temporary directory.
257    * @param fd The file details.
258    * @return Writer for a new StoreFile in the tmp dir.
259    * @throws IOException if creation failed
260    */
261   protected Writer createTmpWriter(FileDetails fd, boolean shouldDropBehind) throws IOException {
262     // When all MVCC readpoints are 0, don't write them.
263     // See HBASE-8166, HBASE-12600, and HBASE-13389.
264     return store.createWriterInTmp(fd.maxKeyCount, this.compactionCompression,
265     /* isCompaction = */true,
266     /* includeMVCCReadpoint = */fd.maxMVCCReadpoint > 0,
267     /* includesTags = */fd.maxTagsLength > 0, shouldDropBehind);
268   }
269 
270   protected List<Path> compact(final CompactionRequest request,
271       InternalScannerFactory scannerFactory, CellSinkFactory<T> sinkFactory,
272       ThroughputController throughputController, User user) throws IOException {
273     FileDetails fd = getFileDetails(request.getFiles(), request.isAllFiles());
274     this.progress = new CompactionProgress(fd.maxKeyCount);
275 
276     // Find the smallest read point across all the Scanners.
277     long smallestReadPoint = getSmallestReadPoint();
278 
279     List<StoreFileScanner> scanners;
280     Collection<StoreFile> readersToClose;
281     T writer = null;
282     boolean dropCache;
283     if (request.isMajor() || request.isAllFiles()) {
284       dropCache = this.dropCacheMajor;
285     } else {
286       dropCache = this.dropCacheMinor;
287     }
288 
289     if (this.conf.getBoolean("hbase.regionserver.compaction.private.readers", true)) {
290       // clone all StoreFiles, so we'll do the compaction on a independent copy of StoreFiles,
291       // HFiles, and their readers
292       readersToClose = new ArrayList<StoreFile>(request.getFiles().size());
293       for (StoreFile f : request.getFiles()) {
294         readersToClose.add(f.cloneForReader());
295       }
296       scanners = createFileScanners(readersToClose, smallestReadPoint, dropCache);
297     } else {
298       readersToClose = Collections.emptyList();
299       scanners = createFileScanners(request.getFiles(), smallestReadPoint, dropCache);
300     }
301     InternalScanner scanner = null;
302     boolean finished = false;
303     try {
304       /* Include deletes, unless we are doing a major compaction */
305       ScanType scanType = scannerFactory.getScanType(request);
306       scanner = preCreateCoprocScanner(request, scanType, fd.earliestPutTs, scanners);
307       if (scanner == null) {
308         scanner = scannerFactory.createScanner(scanners, scanType, fd, smallestReadPoint);
309       }
310       scanner = postCreateCoprocScanner(request, scanType, scanner, user);
311       if (scanner == null) {
312         // NULL scanner returned from coprocessor hooks means skip normal processing.
313         return new ArrayList<Path>();
314       }
315       boolean cleanSeqId = false;
316       if (fd.minSeqIdToKeep > 0) {
317         smallestReadPoint = Math.min(fd.minSeqIdToKeep, smallestReadPoint);
318         cleanSeqId = true;
319       }
320       writer = sinkFactory.createWriter(scanner, fd, dropCache);
321       finished =
322           performCompaction(scanner, writer, smallestReadPoint, cleanSeqId, throughputController);
323       if (!finished) {
324         throw new InterruptedIOException("Aborting compaction of store " + store + " in region "
325             + store.getRegionInfo().getRegionNameAsString() + " because it was interrupted.");
326       }
327     } finally {
328       Closeables.close(scanner, true);
329       for (StoreFile f : readersToClose) {
330         try {
331           f.closeReader(true);
332         } catch (IOException e) {
333           LOG.warn("Exception closing " + f, e);
334         }
335       }
336       if (!finished && writer != null) {
337         abortWriter(writer);
338       }
339     }
340     assert finished : "We should have exited the method on all error paths";
341     assert writer != null : "Writer should be non-null if no error";
342     return commitWriter(writer, fd, request);
343   }
344 
345   protected abstract List<Path> commitWriter(T writer, FileDetails fd, CompactionRequest request)
346       throws IOException;
347 
348   protected abstract void abortWriter(T writer) throws IOException;
349 
350   /**
351    * Calls coprocessor, if any, to create compaction scanner - before normal scanner creation.
352    * @param request Compaction request.
353    * @param scanType Scan type.
354    * @param earliestPutTs Earliest put ts.
355    * @param scanners File scanners for compaction files.
356    * @return Scanner override by coprocessor; null if not overriding.
357    */
358   protected InternalScanner preCreateCoprocScanner(final CompactionRequest request,
359       ScanType scanType, long earliestPutTs,  List<StoreFileScanner> scanners) throws IOException {
360     return preCreateCoprocScanner(request, scanType, earliestPutTs, scanners, null);
361   }
362 
363   protected InternalScanner preCreateCoprocScanner(final CompactionRequest request,
364       final ScanType scanType, final long earliestPutTs, final List<StoreFileScanner> scanners,
365       User user) throws IOException {
366     if (store.getCoprocessorHost() == null) {
367       return null;
368     }
369     if (user == null) {
370       return store.getCoprocessorHost().preCompactScannerOpen(store, scanners, scanType,
371         earliestPutTs, request);
372     } else {
373       try {
374         return user.getUGI().doAs(new PrivilegedExceptionAction<InternalScanner>() {
375           @Override
376           public InternalScanner run() throws Exception {
377             return store.getCoprocessorHost().preCompactScannerOpen(store, scanners,
378               scanType, earliestPutTs, request);
379           }
380         });
381       } catch (InterruptedException ie) {
382         InterruptedIOException iioe = new InterruptedIOException();
383         iioe.initCause(ie);
384         throw iioe;
385       }
386     }
387   }
388 
389   /**
390    * Calls coprocessor, if any, to create scanners - after normal scanner creation.
391    * @param request Compaction request.
392    * @param scanType Scan type.
393    * @param scanner The default scanner created for compaction.
394    * @return Scanner scanner to use (usually the default); null if compaction should not proceed.
395    */
396   protected InternalScanner postCreateCoprocScanner(final CompactionRequest request,
397       final ScanType scanType, final InternalScanner scanner, User user) throws IOException {
398     if (store.getCoprocessorHost() == null) {
399       return scanner;
400     }
401     if (user == null) {
402       return store.getCoprocessorHost().preCompact(store, scanner, scanType, request);
403     } else {
404       try {
405         return user.getUGI().doAs(new PrivilegedExceptionAction<InternalScanner>() {
406           @Override
407           public InternalScanner run() throws Exception {
408             return store.getCoprocessorHost().preCompact(store, scanner, scanType, request);
409           }
410         });
411       } catch (InterruptedException ie) {
412         InterruptedIOException iioe = new InterruptedIOException();
413         iioe.initCause(ie);
414         throw iioe;
415       }
416     }
417   }
418 
419   /**
420    * Performs the compaction.
421    * @param scanner Where to read from.
422    * @param writer Where to write to.
423    * @param smallestReadPoint Smallest read point.
424    * @param cleanSeqId When true, remove seqId(used to be mvcc) value which is &lt;=
425    *          smallestReadPoint
426    * @return Whether compaction ended; false if it was interrupted for some reason.
427    */
428   protected boolean performCompaction(InternalScanner scanner, CellSink writer,
429       long smallestReadPoint, boolean cleanSeqId,
430       ThroughputController throughputController) throws IOException {
431     long bytesWritten = 0;
432     long bytesWrittenProgress = 0;
433     // Since scanner.next() can return 'false' but still be delivering data,
434     // we have to use a do/while loop.
435     List<Cell> cells = new ArrayList<Cell>();
436     long closeCheckInterval = HStore.getCloseCheckInterval();
437     long lastMillis = 0;
438     if (LOG.isDebugEnabled()) {
439       lastMillis = EnvironmentEdgeManager.currentTime();
440     }
441     String compactionName = ThroughputControlUtil.getNameForThrottling(store, "compaction");
442     long now = 0;
443     boolean hasMore;
444     ScannerContext scannerContext =
445         ScannerContext.newBuilder().setBatchLimit(compactionKVMax).build();
446 
447     throughputController.start(compactionName);
448     try {
449       do {
450         hasMore = scanner.next(cells, scannerContext);
451         if (LOG.isDebugEnabled()) {
452           now = EnvironmentEdgeManager.currentTime();
453         }
454         // output to writer:
455         Cell lastCleanCell = null;
456         long lastCleanCellSeqId = 0;
457         for (Cell c : cells) {
458           if (cleanSeqId && c.getSequenceId() <= smallestReadPoint) {
459             lastCleanCell = c;
460             lastCleanCellSeqId = c.getSequenceId();
461             CellUtil.setSequenceId(c, 0);
462           } else {
463             lastCleanCell = null;
464             lastCleanCellSeqId = 0;
465           }
466           writer.append(c);
467           int len = KeyValueUtil.length(c);
468           ++progress.currentCompactedKVs;
469           progress.totalCompactedSize += len;
470           if (LOG.isDebugEnabled()) {
471             bytesWrittenProgress += len;
472           }
473           throughputController.control(compactionName, len);
474           // check periodically to see if a system stop is requested
475           if (closeCheckInterval > 0) {
476             bytesWritten += len;
477             if (bytesWritten > closeCheckInterval) {
478               bytesWritten = 0;
479               if (!store.areWritesEnabled()) {
480                 progress.cancel();
481                 return false;
482               }
483             }
484           }
485         }
486         if (lastCleanCell != null) {
487           // HBASE-16931, set back sequence id to avoid affecting scan order unexpectedly
488           CellUtil.setSequenceId(lastCleanCell, lastCleanCellSeqId);
489         }
490         // Log the progress of long running compactions every minute if
491         // logging at DEBUG level
492         if (LOG.isDebugEnabled()) {
493           if ((now - lastMillis) >= 60 * 1000) {
494             LOG.debug("Compaction progress: "
495                 + compactionName
496                 + " "
497                 + progress
498                 + String.format(", rate=%.2f kB/sec", (bytesWrittenProgress / 1024.0)
499                     / ((now - lastMillis) / 1000.0)) + ", throughputController is "
500                 + throughputController);
501             lastMillis = now;
502             bytesWrittenProgress = 0;
503           }
504         }
505         cells.clear();
506       } while (hasMore);
507     } catch (InterruptedException e) {
508       progress.cancel();
509       throw new InterruptedIOException("Interrupted while control throughput of compacting "
510           + compactionName);
511     } finally {
512       throughputController.finish(compactionName);
513     }
514     progress.complete();
515     return true;
516   }
517 
518   /**
519    * @param store store
520    * @param scanners Store file scanners.
521    * @param scanType Scan type.
522    * @param smallestReadPoint Smallest MVCC read point.
523    * @param earliestPutTs Earliest put across all files.
524    * @return A compaction scanner.
525    */
526   protected InternalScanner createScanner(Store store, List<StoreFileScanner> scanners,
527       ScanType scanType, long smallestReadPoint, long earliestPutTs) throws IOException {
528     Scan scan = new Scan();
529     scan.setMaxVersions(store.getFamily().getMaxVersions());
530     return new StoreScanner(store, store.getScanInfo(), scan, scanners,
531         scanType, smallestReadPoint, earliestPutTs);
532   }
533 
534   /**
535    * @param store The store.
536    * @param scanners Store file scanners.
537    * @param smallestReadPoint Smallest MVCC read point.
538    * @param earliestPutTs Earliest put across all files.
539    * @param dropDeletesFromRow Drop deletes starting with this row, inclusive. Can be null.
540    * @param dropDeletesToRow Drop deletes ending with this row, exclusive. Can be null.
541    * @return A compaction scanner.
542    */
543   protected InternalScanner createScanner(Store store, List<StoreFileScanner> scanners,
544      long smallestReadPoint, long earliestPutTs, byte[] dropDeletesFromRow,
545      byte[] dropDeletesToRow) throws IOException {
546     Scan scan = new Scan();
547     scan.setMaxVersions(store.getFamily().getMaxVersions());
548     return new StoreScanner(store, store.getScanInfo(), scan, scanners, smallestReadPoint,
549         earliestPutTs, dropDeletesFromRow, dropDeletesToRow);
550   }
551 }