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.List;
26  import java.util.Map;
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.conf.Configuration;
32  import org.apache.hadoop.hbase.Cell;
33  import org.apache.hadoop.hbase.CellUtil;
34  import org.apache.hadoop.hbase.HConstants;
35  import org.apache.hadoop.hbase.KeyValue;
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.StoreFileScanner;
49  import org.apache.hadoop.hbase.regionserver.StoreScanner;
50  import org.apache.hadoop.hbase.regionserver.TimeRangeTracker;
51  import org.apache.hadoop.hbase.security.User;
52  import org.apache.hadoop.hbase.util.Bytes;
53  import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
54  import org.apache.hadoop.hbase.util.Writables;
55  import org.apache.hadoop.util.StringUtils.TraditionalBinaryPrefix;
56  
57  /**
58   * A compactor is a compaction algorithm associated a given policy. Base class also contains
59   * reusable parts for implementing compactors (what is common and what isn't is evolving).
60   */
61  @InterfaceAudience.Private
62  public abstract class Compactor {
63    private static final Log LOG = LogFactory.getLog(Compactor.class);
64    protected CompactionProgress progress;
65    protected Configuration conf;
66    protected Store store;
67  
68    protected int compactionKVMax;
69    protected Compression.Algorithm compactionCompression;
70    
71    /** specify how many days to keep MVCC values during major compaction **/ 
72    protected int keepSeqIdPeriod;
73  
74    //TODO: depending on Store is not good but, realistically, all compactors currently do.
75    Compactor(final Configuration conf, final Store store) {
76      this.conf = conf;
77      this.store = store;
78      this.compactionKVMax =
79        this.conf.getInt(HConstants.COMPACTION_KV_MAX, HConstants.COMPACTION_KV_MAX_DEFAULT);
80      this.compactionCompression = (this.store.getFamily() == null) ?
81          Compression.Algorithm.NONE : this.store.getFamily().getCompactionCompression();
82      this.keepSeqIdPeriod = Math.max(this.conf.getInt(HConstants.KEEP_SEQID_PERIOD, 
83        HConstants.MIN_KEEP_SEQID_PERIOD), HConstants.MIN_KEEP_SEQID_PERIOD);
84    }
85  
86    public interface CellSink {
87      void append(Cell cell) throws IOException;
88    }
89  
90    public CompactionProgress getProgress() {
91      return this.progress;
92    }
93  
94    /** The sole reason this class exists is that java has no ref/out/pointer parameters. */
95    protected static class FileDetails {
96      /** Maximum key count after compaction (for blooms) */
97      public long maxKeyCount = 0;
98      /** Earliest put timestamp if major compaction */
99      public long earliestPutTs = HConstants.LATEST_TIMESTAMP;
100     /** Latest put timestamp */
101     public long latestPutTs = HConstants.LATEST_TIMESTAMP;
102     /** The last key in the files we're compacting. */
103     public long maxSeqId = 0;
104     /** Latest memstore read point found in any of the involved files */
105     public long maxMVCCReadpoint = 0;
106     /** Max tags length**/
107     public int maxTagsLength = 0;
108     /** Min SeqId to keep during a major compaction **/
109     public long minSeqIdToKeep = 0;
110   }
111 
112   /**
113    * Extracts some details about the files to compact that are commonly needed by compactors.
114    * @param filesToCompact Files.
115    * @param allFiles Whether all files are included for compaction
116    * @return The result.
117    */
118   protected FileDetails getFileDetails(
119       Collection<StoreFile> filesToCompact, boolean allFiles) throws IOException {
120     FileDetails fd = new FileDetails();
121     long oldestHFileTimeStampToKeepMVCC = System.currentTimeMillis() - 
122       (1000L * 60 * 60 * 24 * this.keepSeqIdPeriod);  
123 
124     for (StoreFile file : filesToCompact) {
125       if(allFiles && (file.getModificationTimeStamp() < oldestHFileTimeStampToKeepMVCC)) {
126         // when isAllFiles is true, all files are compacted so we can calculate the smallest 
127         // MVCC value to keep
128         if(fd.minSeqIdToKeep < file.getMaxMemstoreTS()) {
129           fd.minSeqIdToKeep = file.getMaxMemstoreTS();
130         }
131       }
132       long seqNum = file.getMaxSequenceId();
133       fd.maxSeqId = Math.max(fd.maxSeqId, seqNum);
134       StoreFile.Reader r = file.getReader();
135       if (r == null) {
136         LOG.warn("Null reader for " + file.getPath());
137         continue;
138       }
139       // NOTE: use getEntries when compacting instead of getFilterEntries, otherwise under-sized
140       // blooms can cause progress to be miscalculated or if the user switches bloom
141       // type (e.g. from ROW to ROWCOL)
142       long keyCount = r.getEntries();
143       fd.maxKeyCount += keyCount;
144       // calculate the latest MVCC readpoint in any of the involved store files
145       Map<byte[], byte[]> fileInfo = r.loadFileInfo();
146       byte tmp[] = null;
147       // Get and set the real MVCCReadpoint for bulk loaded files, which is the
148       // SeqId number.
149       if (r.isBulkLoaded()) {
150         fd.maxMVCCReadpoint = Math.max(fd.maxMVCCReadpoint, r.getSequenceID());
151       }
152       else {
153         tmp = fileInfo.get(HFileWriterV2.MAX_MEMSTORE_TS_KEY);
154         if (tmp != null) {
155           fd.maxMVCCReadpoint = Math.max(fd.maxMVCCReadpoint, Bytes.toLong(tmp));
156         }
157       }
158       tmp = fileInfo.get(FileInfo.MAX_TAGS_LEN);
159       if (tmp != null) {
160         fd.maxTagsLength = Math.max(fd.maxTagsLength, Bytes.toInt(tmp));
161       }
162       // If required, calculate the earliest put timestamp of all involved storefiles.
163       // This is used to remove family delete marker during compaction.
164       long earliestPutTs = 0;
165       if (allFiles) {
166         tmp = fileInfo.get(StoreFile.EARLIEST_PUT_TS);
167         if (tmp == null) {
168           // There's a file with no information, must be an old one
169           // assume we have very old puts
170           fd.earliestPutTs = earliestPutTs = HConstants.OLDEST_TIMESTAMP;
171         } else {
172           earliestPutTs = Bytes.toLong(tmp);
173           fd.earliestPutTs = Math.min(fd.earliestPutTs, earliestPutTs);
174         }
175       }
176       tmp = fileInfo.get(StoreFile.TIMERANGE_KEY);
177       TimeRangeTracker trt = new TimeRangeTracker();
178       if (tmp == null) {
179         fd.latestPutTs = HConstants.LATEST_TIMESTAMP;
180       } else {
181         Writables.copyWritable(tmp, trt);
182         fd.latestPutTs = trt.getMaximumTimestamp();
183       }
184       if (LOG.isDebugEnabled()) {
185         LOG.debug("Compacting " + file +
186           ", keycount=" + keyCount +
187           ", bloomtype=" + r.getBloomFilterType().toString() +
188           ", size=" + TraditionalBinaryPrefix.long2String(r.length(), "", 1) +
189           ", encoding=" + r.getHFileReader().getDataBlockEncoding() +
190           ", seqNum=" + seqNum +
191           (allFiles ? ", earliestPutTs=" + earliestPutTs: ""));
192       }
193     }
194     return fd;
195   }
196 
197   /**
198    * Creates file scanners for compaction.
199    * @param filesToCompact Files.
200    * @return Scanners.
201    */
202   protected List<StoreFileScanner> createFileScanners(
203       final Collection<StoreFile> filesToCompact,
204       long smallestReadPoint,
205       boolean useDropBehind) throws IOException {
206     return StoreFileScanner.getScannersForStoreFiles(filesToCompact,
207         /* cache blocks = */ false,
208         /* use pread = */ false,
209         /* is compaction */ true,
210         /* use Drop Behind */ useDropBehind,
211       smallestReadPoint);
212   }
213 
214   protected long getSmallestReadPoint() {
215     return store.getSmallestReadPoint();
216   }
217 
218   /**
219    * Calls coprocessor, if any, to create compaction scanner - before normal scanner creation.
220    * @param request Compaction request.
221    * @param scanType Scan type.
222    * @param earliestPutTs Earliest put ts.
223    * @param scanners File scanners for compaction files.
224    * @return Scanner override by coprocessor; null if not overriding.
225    */
226   protected InternalScanner preCreateCoprocScanner(final CompactionRequest request,
227       ScanType scanType, long earliestPutTs,  List<StoreFileScanner> scanners) throws IOException {
228     return preCreateCoprocScanner(request, scanType, earliestPutTs, scanners, null);
229   }
230 
231   protected InternalScanner preCreateCoprocScanner(final CompactionRequest request,
232       final ScanType scanType, final long earliestPutTs, final List<StoreFileScanner> scanners,
233       User user) throws IOException {
234     if (store.getCoprocessorHost() == null) return null;
235     if (user == null) {
236       return store.getCoprocessorHost().preCompactScannerOpen(store, scanners, scanType,
237         earliestPutTs, request);
238     } else {
239       try {
240         return user.getUGI().doAs(new PrivilegedExceptionAction<InternalScanner>() {
241           @Override
242           public InternalScanner run() throws Exception {
243             return store.getCoprocessorHost().preCompactScannerOpen(store, scanners,
244               scanType, earliestPutTs, request);
245           }
246         });
247       } catch (InterruptedException ie) {
248         InterruptedIOException iioe = new InterruptedIOException();
249         iioe.initCause(ie);
250         throw iioe;
251       }
252     }
253   }
254 
255   /**
256    * Calls coprocessor, if any, to create scanners - after normal scanner creation.
257    * @param request Compaction request.
258    * @param scanType Scan type.
259    * @param scanner The default scanner created for compaction.
260    * @return Scanner scanner to use (usually the default); null if compaction should not proceed.
261    */
262    protected InternalScanner postCreateCoprocScanner(final CompactionRequest request,
263       final ScanType scanType, final InternalScanner scanner, User user) throws IOException {
264      if (store.getCoprocessorHost() == null) return scanner;
265      if (user == null) {
266        return store.getCoprocessorHost().preCompact(store, scanner, scanType, request);
267      } else {
268        try {
269          return user.getUGI().doAs(new PrivilegedExceptionAction<InternalScanner>() {
270            @Override
271            public InternalScanner run() throws Exception {
272              return store.getCoprocessorHost().preCompact(store, scanner, scanType, request);
273            }
274          });
275        } catch (InterruptedException ie) {
276          InterruptedIOException iioe = new InterruptedIOException();
277          iioe.initCause(ie);
278          throw iioe;
279        }
280      }
281   }
282 
283   /**
284    * Used to prevent compaction name conflict when multiple compactions running parallel on the
285    * same store.
286    */
287   private static final AtomicInteger NAME_COUNTER = new AtomicInteger(0);
288 
289   private String generateCompactionName() {
290     int counter;
291     for (;;) {
292       counter = NAME_COUNTER.get();
293       int next = counter == Integer.MAX_VALUE ? 0 : counter + 1;
294       if (NAME_COUNTER.compareAndSet(counter, next)) {
295         break;
296       }
297     }
298     return store.getRegionInfo().getRegionNameAsString() + "#"
299         + store.getFamily().getNameAsString() + "#" + counter;
300   }
301   /**
302    * Performs the compaction.
303    * @param fd File details
304    * @param scanner Where to read from.
305    * @param writer Where to write to.
306    * @param smallestReadPoint Smallest read point.
307    * @param cleanSeqId When true, remove seqId(used to be mvcc) value which is <= smallestReadPoint
308    * @param major Is a major compaction.
309    * @return Whether compaction ended; false if it was interrupted for some reason.
310    */
311   protected boolean performCompaction(FileDetails fd, InternalScanner scanner, CellSink writer,
312       long smallestReadPoint, boolean cleanSeqId,
313       CompactionThroughputController throughputController, boolean major) throws IOException {
314     long bytesWritten = 0;
315     long bytesWrittenProgress = 0;
316 
317     // Since scanner.next() can return 'false' but still be delivering data,
318     // we have to use a do/while loop.
319     List<Cell> cells = new ArrayList<Cell>();
320     long closeCheckInterval = HStore.getCloseCheckInterval();
321     long lastMillis = 0;
322     if (LOG.isDebugEnabled()) {
323       lastMillis = EnvironmentEdgeManager.currentTime();
324     }
325     String compactionName = generateCompactionName();
326     long now = 0;
327     boolean hasMore;
328     ScannerContext scannerContext =
329         ScannerContext.newBuilder().setBatchLimit(compactionKVMax).build();
330 
331     throughputController.start(compactionName);
332     try {
333       do {
334         hasMore = scanner.next(cells, scannerContext);
335         if (LOG.isDebugEnabled()) {
336           now = EnvironmentEdgeManager.currentTime();
337         }
338         // output to writer:
339         for (Cell c : cells) {
340           if (cleanSeqId && c.getSequenceId() <= smallestReadPoint) {
341             CellUtil.setSequenceId(c, 0);
342           }
343           writer.append(c);
344           int len = KeyValueUtil.length(c);
345           ++progress.currentCompactedKVs;
346           progress.totalCompactedSize += len;
347           if (LOG.isDebugEnabled()) {
348             bytesWrittenProgress += len;
349           }
350           throughputController.control(compactionName, len);
351           // check periodically to see if a system stop is requested
352           if (closeCheckInterval > 0) {
353             bytesWritten += len;
354             if (bytesWritten > closeCheckInterval) {
355               bytesWritten = 0;
356               if (!store.areWritesEnabled()) {
357                 progress.cancel();
358                 return false;
359               }
360             }
361           }
362         }
363         // Log the progress of long running compactions every minute if
364         // logging at DEBUG level
365         if (LOG.isDebugEnabled()) {
366           if ((now - lastMillis) >= 60 * 1000) {
367             LOG.debug("Compaction progress: "
368                 + compactionName
369                 + " "
370                 + progress
371                 + String.format(", rate=%.2f kB/sec", (bytesWrittenProgress / 1024.0)
372                     / ((now - lastMillis) / 1000.0)) + ", throughputController is "
373                 + throughputController);
374             lastMillis = now;
375             bytesWrittenProgress = 0;
376           }
377         }
378         cells.clear();
379       } while (hasMore);
380     } catch (InterruptedException e) {
381       progress.cancel();
382       throw new InterruptedIOException("Interrupted while control throughput of compacting "
383           + compactionName);
384     } finally {
385       throughputController.finish(compactionName);
386     }
387     progress.complete();
388     return true;
389   }
390 
391   /**
392    * @param store store
393    * @param scanners Store file scanners.
394    * @param scanType Scan type.
395    * @param smallestReadPoint Smallest MVCC read point.
396    * @param earliestPutTs Earliest put across all files.
397    * @return A compaction scanner.
398    */
399   protected InternalScanner createScanner(Store store, List<StoreFileScanner> scanners,
400       ScanType scanType, long smallestReadPoint, long earliestPutTs) throws IOException {
401     Scan scan = new Scan();
402     scan.setMaxVersions(store.getFamily().getMaxVersions());
403     return new StoreScanner(store, store.getScanInfo(), scan, scanners,
404         scanType, smallestReadPoint, earliestPutTs);
405   }
406 
407   /**
408    * @param store The store.
409    * @param scanners Store file scanners.
410    * @param smallestReadPoint Smallest MVCC read point.
411    * @param earliestPutTs Earliest put across all files.
412    * @param dropDeletesFromRow Drop deletes starting with this row, inclusive. Can be null.
413    * @param dropDeletesToRow Drop deletes ending with this row, exclusive. Can be null.
414    * @return A compaction scanner.
415    */
416   protected InternalScanner createScanner(Store store, List<StoreFileScanner> scanners,
417      long smallestReadPoint, long earliestPutTs, byte[] dropDeletesFromRow,
418      byte[] dropDeletesToRow) throws IOException {
419     Scan scan = new Scan();
420     scan.setMaxVersions(store.getFamily().getMaxVersions());
421     return new StoreScanner(store, store.getScanInfo(), scan, scanners, smallestReadPoint,
422         earliestPutTs, dropDeletesFromRow, dropDeletesToRow);
423   }
424 
425   /**
426    * Resets the sequence id.
427    * @param smallestReadPoint The smallest mvcc readPoint across all the scanners in this region.
428    * @param cleanSeqId Should clean the sequence id.
429    * @param kv The current KeyValue.
430    */
431   protected void resetSeqId(long smallestReadPoint, boolean cleanSeqId, KeyValue kv) {
432     if (cleanSeqId && kv.getSequenceId() <= smallestReadPoint) {
433       kv.setSequenceId(0);
434     }
435   }
436 
437   /**
438    * Appends the metadata and closes the writer.
439    * @param writer The current store writer.
440    * @param fd The file details.
441    * @param isMajor Is a major compaction.
442    * @throws IOException
443    */
444   protected void appendMetadataAndCloseWriter(StoreFile.Writer writer, FileDetails fd,
445       boolean isMajor) throws IOException {
446     writer.appendMetadata(fd.maxSeqId, isMajor);
447     writer.close();
448   }
449 }