View Javadoc

1   /**
2    *
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  package org.apache.hadoop.hbase.regionserver.compactions;
20  
21  import com.google.common.annotations.VisibleForTesting;
22  import com.google.common.collect.Iterators;
23  import com.google.common.collect.Lists;
24  import com.google.common.collect.PeekingIterator;
25  import com.google.common.math.LongMath;
26  
27  import java.io.IOException;
28  import java.util.ArrayList;
29  import java.util.Collection;
30  import java.util.Collections;
31  import java.util.List;
32  
33  import org.apache.commons.logging.Log;
34  import org.apache.commons.logging.LogFactory;
35  import org.apache.hadoop.conf.Configuration;
36  import org.apache.hadoop.hbase.HBaseInterfaceAudience;
37  import org.apache.hadoop.hbase.HDFSBlocksDistribution;
38  import org.apache.hadoop.hbase.classification.InterfaceAudience;
39  import org.apache.hadoop.hbase.regionserver.RSRpcServices;
40  import org.apache.hadoop.hbase.regionserver.StoreConfigInformation;
41  import org.apache.hadoop.hbase.regionserver.StoreFile;
42  import org.apache.hadoop.hbase.regionserver.StoreUtils;
43  import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
44  import org.apache.hadoop.hbase.util.Pair;
45  import org.apache.hadoop.hbase.util.ReflectionUtils;
46  
47  /**
48   * HBASE-15181 This is a simple implementation of date-based tiered compaction similar to
49   * Cassandra's for the following benefits:
50   * <ol>
51   * <li>Improve date-range-based scan by structuring store files in date-based tiered layout.</li>
52   * <li>Reduce compaction overhead.</li>
53   * <li>Improve TTL efficiency.</li>
54   * </ol>
55   * Perfect fit for the use cases that:
56   * <ol>
57   * <li>has mostly date-based data write and scan and a focus on the most recent data.</li>
58   * </ol>
59   * Out-of-order writes are handled gracefully. Time range overlapping among store files is tolerated
60   * and the performance impact is minimized. Configuration can be set at hbase-site or overridden at
61   * per-table or per-column-family level by hbase shell. Design spec is at
62   * https://docs.google.com/document/d/1_AmlNb2N8Us1xICsTeGDLKIqL6T-oHoRLZ323MG_uy8/
63   */
64  @InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.CONFIG)
65  public class DateTieredCompactionPolicy extends SortedCompactionPolicy {
66  
67    private static final Log LOG = LogFactory.getLog(DateTieredCompactionPolicy.class);
68  
69    private final RatioBasedCompactionPolicy compactionPolicyPerWindow;
70  
71    private final CompactionWindowFactory windowFactory;
72  
73    public DateTieredCompactionPolicy(Configuration conf, StoreConfigInformation storeConfigInfo)
74        throws IOException {
75      super(conf, storeConfigInfo);
76      try {
77        compactionPolicyPerWindow = ReflectionUtils.instantiateWithCustomCtor(
78          comConf.getCompactionPolicyForDateTieredWindow(),
79          new Class[] { Configuration.class, StoreConfigInformation.class },
80          new Object[] { conf, storeConfigInfo });
81      } catch (Exception e) {
82        throw new IOException("Unable to load configured compaction policy '"
83            + comConf.getCompactionPolicyForDateTieredWindow() + "'", e);
84      }
85      try {
86        windowFactory = ReflectionUtils.instantiateWithCustomCtor(
87          comConf.getDateTieredCompactionWindowFactory(),
88          new Class[] { CompactionConfiguration.class }, new Object[] { comConf });
89      } catch (Exception e) {
90        throw new IOException("Unable to load configured window factory '"
91            + comConf.getDateTieredCompactionWindowFactory() + "'", e);
92      }
93    }
94  
95    /**
96     * Heuristics for guessing whether we need minor compaction.
97     */
98    @Override
99    @VisibleForTesting
100   public boolean needsCompaction(final Collection<StoreFile> storeFiles,
101       final List<StoreFile> filesCompacting) {
102     ArrayList<StoreFile> candidates = new ArrayList<StoreFile>(storeFiles);
103     try {
104       return !selectMinorCompaction(candidates, false, true).getFiles().isEmpty();
105     } catch (Exception e) {
106       LOG.error("Can not check for compaction: ", e);
107       return false;
108     }
109   }
110 
111   public boolean shouldPerformMajorCompaction(final Collection<StoreFile> filesToCompact)
112     throws IOException {
113     long mcTime = getNextMajorCompactTime(filesToCompact);
114     if (filesToCompact == null || mcTime == 0) {
115       if (LOG.isDebugEnabled()) {
116         LOG.debug("filesToCompact: " + filesToCompact + " mcTime: " + mcTime);
117       }
118       return false;
119     }
120 
121     // TODO: Use better method for determining stamp of last major (HBASE-2990)
122     long lowTimestamp = StoreUtils.getLowestTimestamp(filesToCompact);
123     long now = EnvironmentEdgeManager.currentTime();
124     if (lowTimestamp <= 0L || lowTimestamp >= (now - mcTime)) {
125       if (LOG.isDebugEnabled()) {
126         LOG.debug("lowTimestamp: " + lowTimestamp + " lowTimestamp: " + lowTimestamp + " now: " +
127             now + " mcTime: " + mcTime); 
128       }
129       return false;
130     }
131 
132     long cfTTL = this.storeConfigInfo.getStoreFileTtl();
133     HDFSBlocksDistribution hdfsBlocksDistribution = new HDFSBlocksDistribution();
134     List<Long> boundaries = getCompactBoundariesForMajor(filesToCompact, now);
135     boolean[] filesInWindow = new boolean[boundaries.size()];
136 
137     for (StoreFile file: filesToCompact) {
138       Long minTimestamp = file.getMinimumTimestamp();
139       long oldest = (minTimestamp == null) ? Long.MIN_VALUE : now - minTimestamp.longValue();
140       if (cfTTL != Long.MAX_VALUE && oldest >= cfTTL) {
141         LOG.debug("Major compaction triggered on store " + this
142           + "; for TTL maintenance");
143         return true;
144       }
145       if (!file.isMajorCompaction() || file.isBulkLoadResult()) {
146         LOG.debug("Major compaction triggered on store " + this
147           + ", because there are new files and time since last major compaction "
148           + (now - lowTimestamp) + "ms");
149         return true;
150       }
151 
152       int lowerWindowIndex = Collections.binarySearch(boundaries,
153         minTimestamp == null ? (Long)Long.MAX_VALUE : minTimestamp);
154       int upperWindowIndex = Collections.binarySearch(boundaries,
155         file.getMaximumTimestamp() == null ? (Long)Long.MAX_VALUE : file.getMaximumTimestamp());
156       // Handle boundary conditions and negative values of binarySearch
157       lowerWindowIndex = (lowerWindowIndex < 0) ? Math.abs(lowerWindowIndex + 2) : lowerWindowIndex;
158       upperWindowIndex = (upperWindowIndex < 0) ? Math.abs(upperWindowIndex + 2) : upperWindowIndex;
159       if (lowerWindowIndex != upperWindowIndex) {
160         LOG.debug("Major compaction triggered on store " + this + "; because file "
161           + file.getPath() + " has data with timestamps cross window boundaries");
162         return true;
163       } else if (filesInWindow[upperWindowIndex]) {
164         LOG.debug("Major compaction triggered on store " + this +
165           "; because there are more than one file in some windows");
166         return true;
167       } else {
168         filesInWindow[upperWindowIndex] = true;
169       }
170       hdfsBlocksDistribution.add(file.getHDFSBlockDistribution());
171     }
172 
173     float blockLocalityIndex = hdfsBlocksDistribution
174         .getBlockLocalityIndex(RSRpcServices.getHostname(comConf.conf, false));
175     if (blockLocalityIndex < comConf.getMinLocalityToForceCompact()) {
176       LOG.debug("Major compaction triggered on store " + this
177         + "; to make hdfs blocks local, current blockLocalityIndex is "
178         + blockLocalityIndex + " (min " + comConf.getMinLocalityToForceCompact() + ")");
179       return true;
180     }
181 
182     LOG.debug("Skipping major compaction of " + this +
183       ", because the files are already major compacted");
184     return false;
185   }
186 
187   @Override
188   protected CompactionRequest createCompactionRequest(ArrayList<StoreFile> candidateSelection,
189     boolean tryingMajor, boolean mayUseOffPeak, boolean mayBeStuck) throws IOException {
190     CompactionRequest result = tryingMajor ? selectMajorCompaction(candidateSelection)
191       : selectMinorCompaction(candidateSelection, mayUseOffPeak, mayBeStuck);
192     if (LOG.isDebugEnabled()) {
193       LOG.debug("Generated compaction request: " + result);
194     }
195     return result;
196   }
197 
198   public CompactionRequest selectMajorCompaction(ArrayList<StoreFile> candidateSelection) {
199     long now = EnvironmentEdgeManager.currentTime();
200     return new DateTieredCompactionRequest(candidateSelection,
201       this.getCompactBoundariesForMajor(candidateSelection, now));
202   }
203 
204   /**
205    * We receive store files sorted in ascending order by seqId then scan the list of files. If the
206    * current file has a maxTimestamp older than last known maximum, treat this file as it carries
207    * the last known maximum. This way both seqId and timestamp are in the same order. If files carry
208    * the same maxTimestamps, they are ordered by seqId. We then reverse the list so they are ordered
209    * by seqId and maxTimestamp in descending order and build the time windows. All the out-of-order
210    * data into the same compaction windows, guaranteeing contiguous compaction based on sequence id.
211    */
212   public CompactionRequest selectMinorCompaction(ArrayList<StoreFile> candidateSelection,
213       boolean mayUseOffPeak, boolean mayBeStuck) throws IOException {
214     long now = EnvironmentEdgeManager.currentTime();
215     long oldestToCompact = getOldestToCompact(comConf.getDateTieredMaxStoreFileAgeMillis(), now);
216 
217     List<Pair<StoreFile, Long>> storefileMaxTimestampPairs =
218         Lists.newArrayListWithCapacity(candidateSelection.size());
219     long maxTimestampSeen = Long.MIN_VALUE;
220     for (StoreFile storeFile : candidateSelection) {
221       // if there is out-of-order data,
222       // we put them in the same window as the last file in increasing order
223       maxTimestampSeen = Math.max(maxTimestampSeen,
224         storeFile.getMaximumTimestamp() == null? Long.MIN_VALUE : storeFile.getMaximumTimestamp());
225       storefileMaxTimestampPairs.add(new Pair<StoreFile, Long>(storeFile, maxTimestampSeen));
226     }
227     Collections.reverse(storefileMaxTimestampPairs);
228 
229     CompactionWindow window = getIncomingWindow(now);
230     int minThreshold = comConf.getDateTieredIncomingWindowMin();
231     PeekingIterator<Pair<StoreFile, Long>> it =
232         Iterators.peekingIterator(storefileMaxTimestampPairs.iterator());
233     while (it.hasNext()) {
234       if (window.compareToTimestamp(oldestToCompact) < 0) {
235         break;
236       }
237       int compResult = window.compareToTimestamp(it.peek().getSecond());
238       if (compResult > 0) {
239         // If the file is too old for the window, switch to the next window
240         window = window.nextEarlierWindow();
241         minThreshold = comConf.getMinFilesToCompact();
242       } else {
243         // The file is within the target window
244         ArrayList<StoreFile> fileList = Lists.newArrayList();
245         // Add all files in the same window. For incoming window
246         // we tolerate files with future data although it is sub-optimal
247         while (it.hasNext() && window.compareToTimestamp(it.peek().getSecond()) <= 0) {
248           fileList.add(it.next().getFirst());
249         }
250         if (fileList.size() >= minThreshold) {
251           if (LOG.isDebugEnabled()) {
252             LOG.debug("Processing files: " + fileList + " for window: " + window);
253           }
254           DateTieredCompactionRequest request = generateCompactionRequest(fileList, window,
255             mayUseOffPeak, mayBeStuck, minThreshold);
256           if (request != null) {
257             return request;
258           }
259         }
260       }
261     }
262     // A non-null file list is expected by HStore
263     return new CompactionRequest(Collections.<StoreFile> emptyList());
264   }
265 
266   private DateTieredCompactionRequest generateCompactionRequest(ArrayList<StoreFile> storeFiles,
267       CompactionWindow window, boolean mayUseOffPeak, boolean mayBeStuck, int minThreshold)
268       throws IOException {
269     // The files has to be in ascending order for ratio-based compaction to work right
270     // and removeExcessFile to exclude youngest files.
271     Collections.reverse(storeFiles);
272 
273     // Compact everything in the window if have more files than comConf.maxBlockingFiles
274     compactionPolicyPerWindow.setMinThreshold(minThreshold);
275     ArrayList<StoreFile> storeFileSelection = mayBeStuck ? storeFiles
276       : compactionPolicyPerWindow.applyCompactionPolicy(storeFiles, mayUseOffPeak, false);
277     if (storeFileSelection != null && !storeFileSelection.isEmpty()) {
278       // If there is any file in the window excluded from compaction,
279       // only one file will be output from compaction.
280       boolean singleOutput = storeFiles.size() != storeFileSelection.size() ||
281         comConf.useDateTieredSingleOutputForMinorCompaction();
282       List<Long> boundaries = getCompactionBoundariesForMinor(window, singleOutput);
283       DateTieredCompactionRequest result = new DateTieredCompactionRequest(storeFileSelection,
284         boundaries);
285       return result;
286     }
287     return null;
288   }
289 
290   /**
291    * Return a list of boundaries for multiple compaction output
292    *   in ascending order.
293    */
294   private List<Long> getCompactBoundariesForMajor(Collection<StoreFile> filesToCompact, long now) {
295     long minTimestamp = Long.MAX_VALUE;
296     for (StoreFile file : filesToCompact) {
297       minTimestamp =
298         Math.min(minTimestamp,
299           file.getMinimumTimestamp() == null ? Long.MAX_VALUE : file.getMinimumTimestamp());
300     }
301 
302     List<Long> boundaries = new ArrayList<Long>();
303 
304     // Add startMillis of all windows between now and min timestamp
305     for (CompactionWindow window = getIncomingWindow(now);
306         window.compareToTimestamp(minTimestamp) > 0;
307         window = window.nextEarlierWindow()) {
308       boundaries.add(window.startMillis());
309     }
310     boundaries.add(Long.MIN_VALUE);
311     Collections.reverse(boundaries);
312     return boundaries;
313   }
314 
315   /**
316    * @return a list of boundaries for multiple compaction output from minTimestamp to maxTimestamp.
317    */
318   private static List<Long> getCompactionBoundariesForMinor(CompactionWindow window,
319       boolean singleOutput) {
320     List<Long> boundaries = new ArrayList<Long>();
321     boundaries.add(Long.MIN_VALUE);
322     if (!singleOutput) {
323       boundaries.add(window.startMillis());
324     }
325     return boundaries;
326   }
327 
328   private CompactionWindow getIncomingWindow(long now) {
329     return windowFactory.newIncomingWindow(now);
330   }
331 
332   private static long getOldestToCompact(long maxAgeMillis, long now) {
333     try {
334       return LongMath.checkedSubtract(now, maxAgeMillis);
335     } catch (ArithmeticException ae) {
336       LOG.warn("Value for " + CompactionConfiguration.DATE_TIERED_MAX_AGE_MILLIS_KEY + ": "
337           + maxAgeMillis + ". All the files will be eligible for minor compaction.");
338       return Long.MIN_VALUE;
339     }
340   }
341 }