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.mob.mapreduce;
20  
21  import java.io.IOException;
22  import java.util.ArrayList;
23  import java.util.HashMap;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Set;
28  import java.util.UUID;
29  
30  import org.apache.commons.logging.Log;
31  import org.apache.commons.logging.LogFactory;
32  import org.apache.hadoop.classification.InterfaceAudience;
33  import org.apache.hadoop.conf.Configuration;
34  import org.apache.hadoop.fs.FSDataOutputStream;
35  import org.apache.hadoop.fs.FileStatus;
36  import org.apache.hadoop.fs.FileSystem;
37  import org.apache.hadoop.fs.Path;
38  import org.apache.hadoop.fs.PathFilter;
39  import org.apache.hadoop.hbase.Cell;
40  import org.apache.hadoop.hbase.HColumnDescriptor;
41  import org.apache.hadoop.hbase.HConstants;
42  import org.apache.hadoop.hbase.InvalidFamilyOperationException;
43  import org.apache.hadoop.hbase.KeyValue;
44  import org.apache.hadoop.hbase.KeyValueUtil;
45  import org.apache.hadoop.hbase.TableName;
46  import org.apache.hadoop.hbase.client.HBaseAdmin;
47  import org.apache.hadoop.hbase.client.HTable;
48  import org.apache.hadoop.hbase.io.HFileLink;
49  import org.apache.hadoop.hbase.io.hfile.CacheConfig;
50  import org.apache.hadoop.hbase.mapreduce.TableInputFormat;
51  import org.apache.hadoop.hbase.mob.MobConstants;
52  import org.apache.hadoop.hbase.mob.MobFile;
53  import org.apache.hadoop.hbase.mob.MobFileName;
54  import org.apache.hadoop.hbase.mob.MobUtils;
55  import org.apache.hadoop.hbase.mob.mapreduce.SweepJob.DummyMobAbortable;
56  import org.apache.hadoop.hbase.mob.mapreduce.SweepJob.SweepCounter;
57  import org.apache.hadoop.hbase.regionserver.BloomType;
58  import org.apache.hadoop.hbase.regionserver.DefaultMemStore;
59  import org.apache.hadoop.hbase.regionserver.StoreFile;
60  import org.apache.hadoop.hbase.regionserver.StoreFileScanner;
61  import org.apache.hadoop.hbase.util.Bytes;
62  import org.apache.hadoop.hbase.util.FSUtils;
63  import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher;
64  import org.apache.hadoop.io.IOUtils;
65  import org.apache.hadoop.io.SequenceFile;
66  import org.apache.hadoop.io.SequenceFile.CompressionType;
67  import org.apache.hadoop.io.Text;
68  import org.apache.hadoop.io.Writable;
69  import org.apache.hadoop.mapreduce.Reducer;
70  import org.apache.zookeeper.KeeperException;
71  
72  /**
73   * The reducer of a sweep job.
74   * This reducer merges the small mob files into bigger ones, and write visited
75   * names of mob files to a sequence file which is used by the sweep job to delete
76   * the unused mob files.
77   * The key of the input is a file name, the value is a collection of KeyValue where
78   * the KeyValue is the actual cell (its format is valueLength + fileName) in HBase.
79   * In this reducer, we could know how many cells exist in HBase for a mob file.
80   * If the existCellSize/mobFileSize < compactionRatio, this mob
81   * file needs to be merged.
82   */
83  @InterfaceAudience.Private
84  public class SweepReducer extends Reducer<Text, KeyValue, Writable, Writable> {
85  
86    private static final Log LOG = LogFactory.getLog(SweepReducer.class);
87  
88    private SequenceFile.Writer writer = null;
89    private MemStoreWrapper memstore;
90    private Configuration conf;
91    private FileSystem fs;
92  
93    private Path familyDir;
94    private CacheConfig cacheConfig;
95    private long compactionBegin;
96    private HTable table;
97    private HColumnDescriptor family;
98    private long mobCompactionDelay;
99    private Path mobTableDir;
100 
101   @Override
102   protected void setup(Context context) throws IOException, InterruptedException {
103     this.conf = context.getConfiguration();
104     this.fs = FileSystem.get(conf);
105     // the MOB_SWEEP_JOB_DELAY is ONE_DAY by default. Its value is only changed when testing.
106     mobCompactionDelay = conf.getLong(SweepJob.MOB_SWEEP_JOB_DELAY, SweepJob.ONE_DAY);
107     String tableName = conf.get(TableInputFormat.INPUT_TABLE);
108     String familyName = conf.get(TableInputFormat.SCAN_COLUMN_FAMILY);
109     TableName tn = TableName.valueOf(tableName);
110     this.familyDir = MobUtils.getMobFamilyPath(conf, tn, familyName);
111     HBaseAdmin admin = new HBaseAdmin(this.conf);
112     try {
113       family = admin.getTableDescriptor(tn).getFamily(Bytes.toBytes(familyName));
114       if (family == null) {
115         // this column family might be removed, directly return.
116         throw new InvalidFamilyOperationException("Column family '" + familyName
117             + "' does not exist. It might be removed.");
118       }
119     } finally {
120       try {
121         admin.close();
122       } catch (IOException e) {
123         LOG.warn("Fail to close the HBaseAdmin", e);
124       }
125     }
126     // disable the block cache.
127     Configuration copyOfConf = new Configuration(conf);
128     copyOfConf.setFloat(HConstants.HFILE_BLOCK_CACHE_SIZE_KEY, 0f);
129     this.cacheConfig = new CacheConfig(copyOfConf);
130 
131     table = new HTable(this.conf, Bytes.toBytes(tableName));
132     table.setAutoFlush(false, false);
133 
134     table.setWriteBufferSize(1 * 1024 * 1024); // 1MB
135     memstore = new MemStoreWrapper(context, fs, table, family, new DefaultMemStore(), cacheConfig);
136 
137     // The start time of the sweep tool.
138     // Only the mob files whose creation time is older than startTime-oneDay will be handled by the
139     // reducer since it brings inconsistency to handle the latest mob files.
140     this.compactionBegin = conf.getLong(MobConstants.MOB_SWEEP_TOOL_COMPACTION_START_DATE, 0);
141     mobTableDir = FSUtils.getTableDir(MobUtils.getMobHome(conf), tn);
142   }
143 
144   private SweepPartition createPartition(SweepPartitionId id, Context context) throws IOException {
145     return new SweepPartition(id, context);
146   }
147 
148   @Override
149   public void run(Context context) throws IOException, InterruptedException {
150     String jobId = context.getConfiguration().get(SweepJob.SWEEP_JOB_ID);
151     String owner = context.getConfiguration().get(SweepJob.SWEEP_JOB_SERVERNAME);
152     String sweeperNode = context.getConfiguration().get(SweepJob.SWEEP_JOB_TABLE_NODE);
153     ZooKeeperWatcher zkw = new ZooKeeperWatcher(context.getConfiguration(), jobId,
154         new DummyMobAbortable());
155     FSDataOutputStream fout = null;
156     try {
157       SweepJobNodeTracker tracker = new SweepJobNodeTracker(zkw, sweeperNode, owner);
158       tracker.start();
159       setup(context);
160       // create a sequence contains all the visited file names in this reducer.
161       String dir = this.conf.get(SweepJob.WORKING_VISITED_DIR_KEY);
162       Path nameFilePath = new Path(dir, UUID.randomUUID().toString()
163           .replace("-", MobConstants.EMPTY_STRING));
164       fout = fs.create(nameFilePath, true);
165       writer = SequenceFile.createWriter(context.getConfiguration(), fout, String.class,
166           String.class, CompressionType.NONE, null);
167       SweepPartitionId id;
168       SweepPartition partition = null;
169       // the mob files which have the same start key and date are in the same partition.
170       while (context.nextKey()) {
171         Text key = context.getCurrentKey();
172         String keyString = key.toString();
173         id = SweepPartitionId.create(keyString);
174         if (null == partition || !id.equals(partition.getId())) {
175           // It's the first mob file in the current partition.
176           if (null != partition) {
177             // this mob file is in different partitions with the previous mob file.
178             // directly close.
179             partition.close();
180           }
181           // create a new one
182           partition = createPartition(id, context);
183         }
184         if (partition != null) {
185           // run the partition
186           partition.execute(key, context.getValues());
187         }
188       }
189       if (null != partition) {
190         partition.close();
191       }
192       writer.hflush();
193     } catch (KeeperException e) {
194       throw new IOException(e);
195     } finally {
196       cleanup(context);
197       zkw.close();
198       if (writer != null) {
199         IOUtils.closeStream(writer);
200       }
201       if (fout != null) {
202         IOUtils.closeStream(fout);
203       }
204       if (table != null) {
205         try {
206           table.close();
207         } catch (IOException e) {
208           LOG.warn(e);
209         }
210       }
211     }
212 
213   }
214 
215   /**
216    * The mob files which have the same start key and date are in the same partition.
217    * The files in the same partition are merged together into bigger ones.
218    */
219   public class SweepPartition {
220 
221     private final SweepPartitionId id;
222     private final Context context;
223     private boolean memstoreUpdated = false;
224     private boolean mergeSmall = false;
225     private final Map<String, MobFileStatus> fileStatusMap = new HashMap<String, MobFileStatus>();
226     private final List<Path> toBeDeleted = new ArrayList<Path>();
227 
228     public SweepPartition(SweepPartitionId id, Context context) throws IOException {
229       this.id = id;
230       this.context = context;
231       memstore.setPartitionId(id);
232       init();
233     }
234 
235     public SweepPartitionId getId() {
236       return this.id;
237     }
238 
239     /**
240      * Prepares the map of files.
241      *
242      * @throws IOException
243      */
244     private void init() throws IOException {
245       FileStatus[] fileStats = listStatus(familyDir, id.getStartKey());
246       if (null == fileStats) {
247         return;
248       }
249 
250       int smallFileCount = 0;
251       float compactionRatio = conf.getFloat(MobConstants.MOB_SWEEP_TOOL_COMPACTION_RATIO,
252           MobConstants.DEFAULT_SWEEP_TOOL_MOB_COMPACTION_RATIO);
253       long compactionMergeableSize = conf.getLong(
254           MobConstants.MOB_SWEEP_TOOL_COMPACTION_MERGEABLE_SIZE,
255           MobConstants.DEFAULT_SWEEP_TOOL_MOB_COMPACTION_MERGEABLE_SIZE);
256       // list the files. Just merge the hfiles, don't merge the hfile links.
257       // prepare the map of mob files. The key is the file name, the value is the file status.
258       for (FileStatus fileStat : fileStats) {
259         MobFileStatus mobFileStatus = null;
260         if (!HFileLink.isHFileLink(fileStat.getPath())) {
261           mobFileStatus = new MobFileStatus(fileStat, compactionRatio, compactionMergeableSize);
262           if (mobFileStatus.needMerge()) {
263             smallFileCount++;
264           }
265           // key is file name (not hfile name), value is hfile status.
266           fileStatusMap.put(fileStat.getPath().getName(), mobFileStatus);
267         }
268       }
269       if (smallFileCount >= 2) {
270         // merge the files only when there're more than 1 files in the same partition.
271         this.mergeSmall = true;
272       }
273     }
274 
275     /**
276      * Flushes the data into mob files and store files, and archives the small
277      * files after they're merged.
278      * @throws IOException
279      */
280     public void close() throws IOException {
281       if (null == id) {
282         return;
283       }
284       // flush remain key values into mob files
285       if (memstoreUpdated) {
286         memstore.flushMemStore();
287       }
288       List<StoreFile> storeFiles = new ArrayList<StoreFile>(toBeDeleted.size());
289       // delete samll files after compaction
290       for (Path path : toBeDeleted) {
291         LOG.info("[In Partition close] Delete the file " + path + " in partition close");
292         storeFiles.add(new StoreFile(fs, path, conf, cacheConfig, BloomType.NONE));
293       }
294       if (!storeFiles.isEmpty()) {
295         try {
296           MobUtils.removeMobFiles(conf, fs, table.getName(), mobTableDir, family.getName(),
297               storeFiles);
298           context.getCounter(SweepCounter.FILE_TO_BE_MERGE_OR_CLEAN).increment(storeFiles.size());
299         } catch (IOException e) {
300           LOG.error("Fail to archive the store files " + storeFiles, e);
301         }
302         storeFiles.clear();
303       }
304       fileStatusMap.clear();
305     }
306 
307     /**
308      * Merges the small mob files into bigger ones.
309      * @param fileName The current mob file name.
310      * @param values The collection of KeyValues in this mob file.
311      * @throws IOException
312      */
313     public void execute(Text fileName, Iterable<KeyValue> values) throws IOException {
314       if (null == values) {
315         return;
316       }
317       MobFileName mobFileName = MobFileName.create(fileName.toString());
318       LOG.info("[In reducer] The file name: " + fileName.toString());
319       MobFileStatus mobFileStat = fileStatusMap.get(mobFileName.getFileName());
320       if (null == mobFileStat) {
321         LOG.info("[In reducer] Cannot find the file, probably this record is obsolete");
322         return;
323       }
324       // only handle the files that are older then one day.
325       if (compactionBegin - mobFileStat.getFileStatus().getModificationTime()
326           <= mobCompactionDelay) {
327         return;
328       }
329       // write the hfile name
330       writer.append(mobFileName.getFileName(), MobConstants.EMPTY_STRING);
331       Set<KeyValue> kvs = new HashSet<KeyValue>();
332       for (KeyValue kv : values) {
333         if (kv.getValueLength() > Bytes.SIZEOF_INT) {
334           mobFileStat.addValidSize(Bytes.toInt(kv.getValueArray(), kv.getValueOffset(),
335               Bytes.SIZEOF_INT));
336         }
337         kvs.add(kv.createKeyOnly(false));
338       }
339       // If the mob file is a invalid one or a small one, merge it into new/bigger ones.
340       if (mobFileStat.needClean() || (mergeSmall && mobFileStat.needMerge())) {
341         context.getCounter(SweepCounter.INPUT_FILE_COUNT).increment(1);
342         MobFile file = MobFile.create(fs,
343             new Path(familyDir, mobFileName.getFileName()), conf, cacheConfig);
344         StoreFileScanner scanner = null;
345         file.open();
346         try {
347           scanner = file.getScanner();
348           scanner.seek(KeyValueUtil.createFirstOnRow(HConstants.EMPTY_BYTE_ARRAY));
349           Cell cell;
350           while (null != (cell = scanner.next())) {
351             KeyValue kv = KeyValueUtil.ensureKeyValue(cell);
352             KeyValue keyOnly = kv.createKeyOnly(false);
353             if (kvs.contains(keyOnly)) {
354               // write the KeyValue existing in HBase to the memstore.
355               memstore.addToMemstore(kv);
356               memstoreUpdated = true;
357             }
358           }
359         } finally {
360           if (scanner != null) {
361             scanner.close();
362           }
363           file.close();
364         }
365         toBeDeleted.add(mobFileStat.getFileStatus().getPath());
366       }
367     }
368 
369     /**
370      * Lists the files with the same prefix.
371      * @param p The file path.
372      * @param prefix The prefix.
373      * @return The files with the same prefix.
374      * @throws IOException
375      */
376     private FileStatus[] listStatus(Path p, String prefix) throws IOException {
377       return fs.listStatus(p, new PathPrefixFilter(prefix));
378     }
379   }
380 
381   static class PathPrefixFilter implements PathFilter {
382 
383     private final String prefix;
384 
385     public PathPrefixFilter(String prefix) {
386       this.prefix = prefix;
387     }
388 
389     public boolean accept(Path path) {
390       return path.getName().startsWith(prefix, 0);
391     }
392 
393   }
394 
395   /**
396    * The sweep partition id.
397    * It consists of the start key and date.
398    * The start key is a hex string of the checksum of a region start key.
399    * The date is the latest timestamp of cells in a mob file.
400    */
401   public static class SweepPartitionId {
402     private String date;
403     private String startKey;
404 
405     public SweepPartitionId(MobFileName fileName) {
406       this.date = fileName.getDate();
407       this.startKey = fileName.getStartKey();
408     }
409 
410     public SweepPartitionId(String date, String startKey) {
411       this.date = date;
412       this.startKey = startKey;
413     }
414 
415     public static SweepPartitionId create(String key) {
416       return new SweepPartitionId(MobFileName.create(key));
417     }
418 
419     @Override
420     public boolean equals(Object anObject) {
421       if (this == anObject) {
422         return true;
423       }
424       if (anObject instanceof SweepPartitionId) {
425         SweepPartitionId another = (SweepPartitionId) anObject;
426         if (this.date.equals(another.getDate()) && this.startKey.equals(another.getStartKey())) {
427           return true;
428         }
429       }
430       return false;
431     }
432 
433     public String getDate() {
434       return this.date;
435     }
436 
437     public String getStartKey() {
438       return this.startKey;
439     }
440 
441     public void setDate(String date) {
442       this.date = date;
443     }
444 
445     public void setStartKey(String startKey) {
446       this.startKey = startKey;
447     }
448   }
449 
450   /**
451    * The mob file status used in the sweep reduecer.
452    */
453   private static class MobFileStatus {
454     private FileStatus fileStatus;
455     private int validSize;
456     private long size;
457 
458     private float compactionRatio = MobConstants.DEFAULT_SWEEP_TOOL_MOB_COMPACTION_RATIO;
459     private long compactionMergeableSize =
460         MobConstants.DEFAULT_SWEEP_TOOL_MOB_COMPACTION_MERGEABLE_SIZE;
461 
462     /**
463      * @param fileStatus The current FileStatus.
464      * @param compactionRatio compactionRatio the invalid ratio.
465      * If there're too many cells deleted in a mob file, it's regarded as invalid,
466      * and needs to be written to a new one.
467      * If existingCellSize/fileSize < compactionRatio, it's regarded as a invalid one.
468      * @param compactionMergeableSize compactionMergeableSize If the size of a mob file is less
469      * than this value, it's regarded as a small file and needs to be merged
470      */
471     public MobFileStatus(FileStatus fileStatus, float compactionRatio,
472         long compactionMergeableSize) {
473       this.fileStatus = fileStatus;
474       this.size = fileStatus.getLen();
475       validSize = 0;
476       this.compactionRatio = compactionRatio;
477       this.compactionMergeableSize = compactionMergeableSize;
478     }
479 
480     /**
481      * Add size to this file.
482      * @param size The size to be added.
483      */
484     public void addValidSize(int size) {
485       this.validSize += size;
486     }
487 
488     /**
489      * Whether the mob files need to be cleaned.
490      * If there're too many cells deleted in this mob file, it needs to be cleaned.
491      * @return True if it needs to be cleaned.
492      */
493     public boolean needClean() {
494       return validSize < compactionRatio * size;
495     }
496 
497     /**
498      * Whether the mob files need to be merged.
499      * If this mob file is too small, it needs to be merged.
500      * @return True if it needs to be merged.
501      */
502     public boolean needMerge() {
503       return this.size < compactionMergeableSize;
504     }
505 
506     /**
507      * Gets the file status.
508      * @return The file status.
509      */
510     public FileStatus getFileStatus() {
511       return fileStatus;
512     }
513   }
514 }