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.mapreduce; 019 020import static org.apache.hadoop.hbase.regionserver.HStoreFile.BULKLOAD_TASK_KEY; 021import static org.apache.hadoop.hbase.regionserver.HStoreFile.BULKLOAD_TIME_KEY; 022import static org.apache.hadoop.hbase.regionserver.HStoreFile.EXCLUDE_FROM_MINOR_COMPACTION_KEY; 023import static org.apache.hadoop.hbase.regionserver.HStoreFile.MAJOR_COMPACTION_KEY; 024 025import java.io.IOException; 026import java.io.UnsupportedEncodingException; 027import java.net.InetSocketAddress; 028import java.net.URLDecoder; 029import java.net.URLEncoder; 030import java.util.ArrayList; 031import java.util.Arrays; 032import java.util.List; 033import java.util.Map; 034import java.util.Set; 035import java.util.TreeMap; 036import java.util.TreeSet; 037import java.util.UUID; 038import java.util.function.Function; 039import java.util.stream.Collectors; 040 041import org.apache.commons.lang3.StringUtils; 042import org.apache.hadoop.conf.Configuration; 043import org.apache.hadoop.fs.FileSystem; 044import org.apache.hadoop.fs.Path; 045import org.apache.hadoop.hbase.Cell; 046import org.apache.hadoop.hbase.CellComparator; 047import org.apache.hadoop.hbase.CellUtil; 048import org.apache.hadoop.hbase.HConstants; 049import org.apache.hadoop.hbase.HRegionLocation; 050import org.apache.hadoop.hbase.HTableDescriptor; 051import org.apache.hadoop.hbase.KeyValue; 052import org.apache.hadoop.hbase.PrivateCellUtil; 053import org.apache.hadoop.hbase.TableName; 054import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor; 055import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 056import org.apache.hadoop.hbase.client.Connection; 057import org.apache.hadoop.hbase.client.ConnectionFactory; 058import org.apache.hadoop.hbase.client.Put; 059import org.apache.hadoop.hbase.client.RegionLocator; 060import org.apache.hadoop.hbase.client.Table; 061import org.apache.hadoop.hbase.client.TableDescriptor; 062import org.apache.hadoop.hbase.fs.HFileSystem; 063import org.apache.hadoop.hbase.io.ImmutableBytesWritable; 064import org.apache.hadoop.hbase.io.compress.Compression; 065import org.apache.hadoop.hbase.io.compress.Compression.Algorithm; 066import org.apache.hadoop.hbase.io.encoding.DataBlockEncoding; 067import org.apache.hadoop.hbase.io.hfile.CacheConfig; 068import org.apache.hadoop.hbase.io.hfile.HFile; 069import org.apache.hadoop.hbase.io.hfile.HFileContext; 070import org.apache.hadoop.hbase.io.hfile.HFileContextBuilder; 071import org.apache.hadoop.hbase.io.hfile.HFileWriterImpl; 072import org.apache.hadoop.hbase.regionserver.BloomType; 073import org.apache.hadoop.hbase.regionserver.HStore; 074import org.apache.hadoop.hbase.regionserver.StoreFileWriter; 075import org.apache.hadoop.hbase.util.Bytes; 076import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; 077import org.apache.hadoop.hbase.util.FSUtils; 078import org.apache.hadoop.hbase.util.MapReduceExtendedCell; 079import org.apache.hadoop.io.NullWritable; 080import org.apache.hadoop.io.SequenceFile; 081import org.apache.hadoop.io.Text; 082import org.apache.hadoop.mapreduce.Job; 083import org.apache.hadoop.mapreduce.OutputCommitter; 084import org.apache.hadoop.mapreduce.OutputFormat; 085import org.apache.hadoop.mapreduce.RecordWriter; 086import org.apache.hadoop.mapreduce.TaskAttemptContext; 087import org.apache.hadoop.mapreduce.lib.output.FileOutputCommitter; 088import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; 089import org.apache.hadoop.mapreduce.lib.partition.TotalOrderPartitioner; 090import org.apache.yetus.audience.InterfaceAudience; 091import org.slf4j.Logger; 092import org.slf4j.LoggerFactory; 093 094import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting; 095 096/** 097 * Writes HFiles. Passed Cells must arrive in order. 098 * Writes current time as the sequence id for the file. Sets the major compacted 099 * attribute on created @{link {@link HFile}s. Calling write(null,null) will forcibly roll 100 * all HFiles being written. 101 * <p> 102 * Using this class as part of a MapReduce job is best done 103 * using {@link #configureIncrementalLoad(Job, TableDescriptor, RegionLocator)}. 104 */ 105@InterfaceAudience.Public 106public class HFileOutputFormat2 107 extends FileOutputFormat<ImmutableBytesWritable, Cell> { 108 private static final Logger LOG = LoggerFactory.getLogger(HFileOutputFormat2.class); 109 static class TableInfo { 110 private TableDescriptor tableDesctiptor; 111 private RegionLocator regionLocator; 112 113 public TableInfo(TableDescriptor tableDesctiptor, RegionLocator regionLocator) { 114 this.tableDesctiptor = tableDesctiptor; 115 this.regionLocator = regionLocator; 116 } 117 118 /** 119 * The modification for the returned HTD doesn't affect the inner TD. 120 * @return A clone of inner table descriptor 121 * @deprecated use {@link #getTableDescriptor} 122 */ 123 @Deprecated 124 public HTableDescriptor getHTableDescriptor() { 125 return new HTableDescriptor(tableDesctiptor); 126 } 127 128 public TableDescriptor getTableDescriptor() { 129 return tableDesctiptor; 130 } 131 132 public RegionLocator getRegionLocator() { 133 return regionLocator; 134 } 135 } 136 137 protected static final byte[] tableSeparator = Bytes.toBytes(";"); 138 139 protected static byte[] combineTableNameSuffix(byte[] tableName, byte[] suffix) { 140 return Bytes.add(tableName, tableSeparator, suffix); 141 } 142 143 // The following constants are private since these are used by 144 // HFileOutputFormat2 to internally transfer data between job setup and 145 // reducer run using conf. 146 // These should not be changed by the client. 147 static final String COMPRESSION_FAMILIES_CONF_KEY = 148 "hbase.hfileoutputformat.families.compression"; 149 static final String BLOOM_TYPE_FAMILIES_CONF_KEY = 150 "hbase.hfileoutputformat.families.bloomtype"; 151 static final String BLOCK_SIZE_FAMILIES_CONF_KEY = 152 "hbase.mapreduce.hfileoutputformat.blocksize"; 153 static final String DATABLOCK_ENCODING_FAMILIES_CONF_KEY = 154 "hbase.mapreduce.hfileoutputformat.families.datablock.encoding"; 155 156 // This constant is public since the client can modify this when setting 157 // up their conf object and thus refer to this symbol. 158 // It is present for backwards compatibility reasons. Use it only to 159 // override the auto-detection of datablock encoding. 160 public static final String DATABLOCK_ENCODING_OVERRIDE_CONF_KEY = 161 "hbase.mapreduce.hfileoutputformat.datablock.encoding"; 162 163 /** 164 * Keep locality while generating HFiles for bulkload. See HBASE-12596 165 */ 166 public static final String LOCALITY_SENSITIVE_CONF_KEY = 167 "hbase.bulkload.locality.sensitive.enabled"; 168 private static final boolean DEFAULT_LOCALITY_SENSITIVE = true; 169 static final String OUTPUT_TABLE_NAME_CONF_KEY = 170 "hbase.mapreduce.hfileoutputformat.table.name"; 171 static final String MULTI_TABLE_HFILEOUTPUTFORMAT_CONF_KEY = 172 "hbase.mapreduce.use.multi.table.hfileoutputformat"; 173 174 public static final String STORAGE_POLICY_PROPERTY = HStore.BLOCK_STORAGE_POLICY_KEY; 175 public static final String STORAGE_POLICY_PROPERTY_CF_PREFIX = STORAGE_POLICY_PROPERTY + "."; 176 177 @Override 178 public RecordWriter<ImmutableBytesWritable, Cell> getRecordWriter( 179 final TaskAttemptContext context) throws IOException, InterruptedException { 180 return createRecordWriter(context, this.getOutputCommitter(context)); 181 } 182 183 protected static byte[] getTableNameSuffixedWithFamily(byte[] tableName, byte[] family) { 184 return combineTableNameSuffix(tableName, family); 185 } 186 187 static <V extends Cell> RecordWriter<ImmutableBytesWritable, V> 188 createRecordWriter(final TaskAttemptContext context, final OutputCommitter committer) 189 throws IOException { 190 191 // Get the path of the temporary output file 192 final Path outputDir = ((FileOutputCommitter)committer).getWorkPath(); 193 final Configuration conf = context.getConfiguration(); 194 final boolean writeMultipleTables = conf.getBoolean(MULTI_TABLE_HFILEOUTPUTFORMAT_CONF_KEY, false) ; 195 final String writeTableNames = conf.get(OUTPUT_TABLE_NAME_CONF_KEY); 196 if (writeTableNames==null || writeTableNames.isEmpty()) { 197 throw new IllegalArgumentException("Configuration parameter " + OUTPUT_TABLE_NAME_CONF_KEY 198 + " cannot be empty"); 199 } 200 final FileSystem fs = outputDir.getFileSystem(conf); 201 // These configs. are from hbase-*.xml 202 final long maxsize = conf.getLong(HConstants.HREGION_MAX_FILESIZE, 203 HConstants.DEFAULT_MAX_FILE_SIZE); 204 // Invented config. Add to hbase-*.xml if other than default compression. 205 final String defaultCompressionStr = conf.get("hfile.compression", 206 Compression.Algorithm.NONE.getName()); 207 final Algorithm defaultCompression = HFileWriterImpl 208 .compressionByName(defaultCompressionStr); 209 final boolean compactionExclude = conf.getBoolean( 210 "hbase.mapreduce.hfileoutputformat.compaction.exclude", false); 211 212 final Set<String> allTableNames = Arrays.stream(writeTableNames.split( 213 Bytes.toString(tableSeparator))).collect(Collectors.toSet()); 214 215 // create a map from column family to the compression algorithm 216 final Map<byte[], Algorithm> compressionMap = createFamilyCompressionMap(conf); 217 final Map<byte[], BloomType> bloomTypeMap = createFamilyBloomTypeMap(conf); 218 final Map<byte[], Integer> blockSizeMap = createFamilyBlockSizeMap(conf); 219 220 String dataBlockEncodingStr = conf.get(DATABLOCK_ENCODING_OVERRIDE_CONF_KEY); 221 final Map<byte[], DataBlockEncoding> datablockEncodingMap 222 = createFamilyDataBlockEncodingMap(conf); 223 final DataBlockEncoding overriddenEncoding; 224 if (dataBlockEncodingStr != null) { 225 overriddenEncoding = DataBlockEncoding.valueOf(dataBlockEncodingStr); 226 } else { 227 overriddenEncoding = null; 228 } 229 230 return new RecordWriter<ImmutableBytesWritable, V>() { 231 // Map of families to writers and how much has been output on the writer. 232 private final Map<byte[], WriterLength> writers = 233 new TreeMap<>(Bytes.BYTES_COMPARATOR); 234 private byte[] previousRow = HConstants.EMPTY_BYTE_ARRAY; 235 private final long now = EnvironmentEdgeManager.currentTime(); 236 private boolean rollRequested = false; 237 238 @Override 239 public void write(ImmutableBytesWritable row, V cell) 240 throws IOException { 241 Cell kv = cell; 242 // null input == user explicitly wants to flush 243 if (row == null && kv == null) { 244 rollWriters(null); 245 return; 246 } 247 248 byte[] rowKey = CellUtil.cloneRow(kv); 249 int length = (PrivateCellUtil.estimatedSerializedSizeOf(kv)) - Bytes.SIZEOF_INT; 250 byte[] family = CellUtil.cloneFamily(kv); 251 byte[] tableNameBytes = null; 252 if (writeMultipleTables) { 253 tableNameBytes = MultiTableHFileOutputFormat.getTableName(row.get()); 254 if (!allTableNames.contains(Bytes.toString(tableNameBytes))) { 255 throw new IllegalArgumentException("TableName '" + Bytes.toString(tableNameBytes) + 256 "' not" + " expected"); 257 } 258 } else { 259 tableNameBytes = Bytes.toBytes(writeTableNames); 260 } 261 byte[] tableAndFamily = getTableNameSuffixedWithFamily(tableNameBytes, family); 262 WriterLength wl = this.writers.get(tableAndFamily); 263 264 // If this is a new column family, verify that the directory exists 265 if (wl == null) { 266 Path writerPath = null; 267 if (writeMultipleTables) { 268 writerPath = new Path(outputDir, new Path(Bytes.toString(tableNameBytes), Bytes 269 .toString(family))); 270 } 271 else { 272 writerPath = new Path(outputDir, Bytes.toString(family)); 273 } 274 fs.mkdirs(writerPath); 275 configureStoragePolicy(conf, fs, tableAndFamily, writerPath); 276 } 277 278 if (wl != null && wl.written + length >= maxsize) { 279 this.rollRequested = true; 280 } 281 282 // This can only happen once a row is finished though 283 if (rollRequested && Bytes.compareTo(this.previousRow, rowKey) != 0) { 284 rollWriters(wl); 285 } 286 287 // create a new WAL writer, if necessary 288 if (wl == null || wl.writer == null) { 289 if (conf.getBoolean(LOCALITY_SENSITIVE_CONF_KEY, DEFAULT_LOCALITY_SENSITIVE)) { 290 HRegionLocation loc = null; 291 292 String tableName = Bytes.toString(tableNameBytes); 293 if (tableName != null) { 294 try (Connection connection = ConnectionFactory.createConnection(conf); 295 RegionLocator locator = 296 connection.getRegionLocator(TableName.valueOf(tableName))) { 297 loc = locator.getRegionLocation(rowKey); 298 } catch (Throwable e) { 299 LOG.warn("There's something wrong when locating rowkey: " + 300 Bytes.toString(rowKey) + " for tablename: " + tableName, e); 301 loc = null; 302 } } 303 304 if (null == loc) { 305 if (LOG.isTraceEnabled()) { 306 LOG.trace("failed to get region location, so use default writer for rowkey: " + 307 Bytes.toString(rowKey)); 308 } 309 wl = getNewWriter(tableNameBytes, family, conf, null); 310 } else { 311 if (LOG.isDebugEnabled()) { 312 LOG.debug("first rowkey: [" + Bytes.toString(rowKey) + "]"); 313 } 314 InetSocketAddress initialIsa = 315 new InetSocketAddress(loc.getHostname(), loc.getPort()); 316 if (initialIsa.isUnresolved()) { 317 if (LOG.isTraceEnabled()) { 318 LOG.trace("failed to resolve bind address: " + loc.getHostname() + ":" 319 + loc.getPort() + ", so use default writer"); 320 } 321 wl = getNewWriter(tableNameBytes, family, conf, null); 322 } else { 323 if (LOG.isDebugEnabled()) { 324 LOG.debug("use favored nodes writer: " + initialIsa.getHostString()); 325 } 326 wl = getNewWriter(tableNameBytes, family, conf, new InetSocketAddress[] { initialIsa 327 }); 328 } 329 } 330 } else { 331 wl = getNewWriter(tableNameBytes, family, conf, null); 332 } 333 } 334 335 // we now have the proper WAL writer. full steam ahead 336 PrivateCellUtil.updateLatestStamp(cell, this.now); 337 wl.writer.append(kv); 338 wl.written += length; 339 340 // Copy the row so we know when a row transition. 341 this.previousRow = rowKey; 342 } 343 344 private void rollWriters(WriterLength writerLength) throws IOException { 345 if (writerLength != null) { 346 closeWriter(writerLength); 347 } else { 348 for (WriterLength wl : this.writers.values()) { 349 closeWriter(wl); 350 } 351 } 352 this.rollRequested = false; 353 } 354 355 private void closeWriter(WriterLength wl) throws IOException { 356 if (wl.writer != null) { 357 LOG.info( 358 "Writer=" + wl.writer.getPath() + ((wl.written == 0)? "": ", wrote=" + wl.written)); 359 close(wl.writer); 360 } 361 wl.writer = null; 362 wl.written = 0; 363 } 364 365 /* 366 * Create a new StoreFile.Writer. 367 * @param family 368 * @return A WriterLength, containing a new StoreFile.Writer. 369 * @throws IOException 370 */ 371 @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="BX_UNBOXING_IMMEDIATELY_REBOXED", 372 justification="Not important") 373 private WriterLength getNewWriter(byte[] tableName, byte[] family, Configuration 374 conf, InetSocketAddress[] favoredNodes) throws IOException { 375 byte[] tableAndFamily = getTableNameSuffixedWithFamily(tableName, family); 376 Path familydir = new Path(outputDir, Bytes.toString(family)); 377 if (writeMultipleTables) { 378 familydir = new Path(outputDir, 379 new Path(Bytes.toString(tableName), Bytes.toString(family))); 380 } 381 WriterLength wl = new WriterLength(); 382 Algorithm compression = compressionMap.get(tableAndFamily); 383 compression = compression == null ? defaultCompression : compression; 384 BloomType bloomType = bloomTypeMap.get(tableAndFamily); 385 bloomType = bloomType == null ? BloomType.NONE : bloomType; 386 Integer blockSize = blockSizeMap.get(tableAndFamily); 387 blockSize = blockSize == null ? HConstants.DEFAULT_BLOCKSIZE : blockSize; 388 DataBlockEncoding encoding = overriddenEncoding; 389 encoding = encoding == null ? datablockEncodingMap.get(tableAndFamily) : encoding; 390 encoding = encoding == null ? DataBlockEncoding.NONE : encoding; 391 Configuration tempConf = new Configuration(conf); 392 tempConf.setFloat(HConstants.HFILE_BLOCK_CACHE_SIZE_KEY, 0.0f); 393 HFileContextBuilder contextBuilder = new HFileContextBuilder() 394 .withCompression(compression) 395 .withChecksumType(HStore.getChecksumType(conf)) 396 .withBytesPerCheckSum(HStore.getBytesPerChecksum(conf)) 397 .withBlockSize(blockSize); 398 399 if (HFile.getFormatVersion(conf) >= HFile.MIN_FORMAT_VERSION_WITH_TAGS) { 400 contextBuilder.withIncludesTags(true); 401 } 402 403 contextBuilder.withDataBlockEncoding(encoding); 404 HFileContext hFileContext = contextBuilder.build(); 405 if (null == favoredNodes) { 406 wl.writer = 407 new StoreFileWriter.Builder(conf, new CacheConfig(tempConf), fs) 408 .withOutputDir(familydir).withBloomType(bloomType) 409 .withComparator(CellComparator.getInstance()).withFileContext(hFileContext).build(); 410 } else { 411 wl.writer = 412 new StoreFileWriter.Builder(conf, new CacheConfig(tempConf), new HFileSystem(fs)) 413 .withOutputDir(familydir).withBloomType(bloomType) 414 .withComparator(CellComparator.getInstance()).withFileContext(hFileContext) 415 .withFavoredNodes(favoredNodes).build(); 416 } 417 418 this.writers.put(tableAndFamily, wl); 419 return wl; 420 } 421 422 private void close(final StoreFileWriter w) throws IOException { 423 if (w != null) { 424 w.appendFileInfo(BULKLOAD_TIME_KEY, 425 Bytes.toBytes(System.currentTimeMillis())); 426 w.appendFileInfo(BULKLOAD_TASK_KEY, 427 Bytes.toBytes(context.getTaskAttemptID().toString())); 428 w.appendFileInfo(MAJOR_COMPACTION_KEY, 429 Bytes.toBytes(true)); 430 w.appendFileInfo(EXCLUDE_FROM_MINOR_COMPACTION_KEY, 431 Bytes.toBytes(compactionExclude)); 432 w.appendTrackedTimestampsToMetadata(); 433 w.close(); 434 } 435 } 436 437 @Override 438 public void close(TaskAttemptContext c) 439 throws IOException, InterruptedException { 440 for (WriterLength wl: this.writers.values()) { 441 close(wl.writer); 442 } 443 } 444 }; 445 } 446 447 /** 448 * Configure block storage policy for CF after the directory is created. 449 */ 450 static void configureStoragePolicy(final Configuration conf, final FileSystem fs, 451 byte[] tableAndFamily, Path cfPath) { 452 if (null == conf || null == fs || null == tableAndFamily || null == cfPath) { 453 return; 454 } 455 456 String policy = 457 conf.get(STORAGE_POLICY_PROPERTY_CF_PREFIX + Bytes.toString(tableAndFamily), 458 conf.get(STORAGE_POLICY_PROPERTY)); 459 FSUtils.setStoragePolicy(fs, cfPath, policy); 460 } 461 462 /* 463 * Data structure to hold a Writer and amount of data written on it. 464 */ 465 static class WriterLength { 466 long written = 0; 467 StoreFileWriter writer = null; 468 } 469 470 /** 471 * Return the start keys of all of the regions in this table, 472 * as a list of ImmutableBytesWritable. 473 */ 474 private static List<ImmutableBytesWritable> getRegionStartKeys(List<RegionLocator> regionLocators, 475 boolean writeMultipleTables) 476 throws IOException { 477 478 ArrayList<ImmutableBytesWritable> ret = new ArrayList<>(); 479 for(RegionLocator regionLocator : regionLocators) 480 { 481 TableName tableName = regionLocator.getName(); 482 LOG.info("Looking up current regions for table " + tableName); 483 byte[][] byteKeys = regionLocator.getStartKeys(); 484 for (byte[] byteKey : byteKeys) { 485 byte[] fullKey = byteKey; //HFileOutputFormat2 use case 486 if (writeMultipleTables) 487 { 488 //MultiTableHFileOutputFormat use case 489 fullKey = combineTableNameSuffix(tableName.getName(), byteKey); 490 } 491 if (LOG.isDebugEnabled()) { 492 LOG.debug("SplitPoint startkey for table [" + tableName + "]: [" + Bytes.toStringBinary 493 (fullKey) + "]"); 494 } 495 ret.add(new ImmutableBytesWritable(fullKey)); 496 } 497 } 498 return ret; 499 } 500 501 /** 502 * Write out a {@link SequenceFile} that can be read by 503 * {@link TotalOrderPartitioner} that contains the split points in startKeys. 504 */ 505 @SuppressWarnings("deprecation") 506 private static void writePartitions(Configuration conf, Path partitionsPath, 507 List<ImmutableBytesWritable> startKeys, boolean writeMultipleTables) throws IOException { 508 LOG.info("Writing partition information to " + partitionsPath); 509 if (startKeys.isEmpty()) { 510 throw new IllegalArgumentException("No regions passed"); 511 } 512 513 // We're generating a list of split points, and we don't ever 514 // have keys < the first region (which has an empty start key) 515 // so we need to remove it. Otherwise we would end up with an 516 // empty reducer with index 0 517 TreeSet<ImmutableBytesWritable> sorted = new TreeSet<>(startKeys); 518 ImmutableBytesWritable first = sorted.first(); 519 if (writeMultipleTables) { 520 first = new ImmutableBytesWritable(MultiTableHFileOutputFormat.getSuffix(sorted.first 521 ().get())); 522 } 523 if (!first.equals(HConstants.EMPTY_BYTE_ARRAY)) { 524 throw new IllegalArgumentException( 525 "First region of table should have empty start key. Instead has: " 526 + Bytes.toStringBinary(first.get())); 527 } 528 sorted.remove(sorted.first()); 529 530 // Write the actual file 531 FileSystem fs = partitionsPath.getFileSystem(conf); 532 SequenceFile.Writer writer = SequenceFile.createWriter( 533 fs, conf, partitionsPath, ImmutableBytesWritable.class, 534 NullWritable.class); 535 536 try { 537 for (ImmutableBytesWritable startKey : sorted) { 538 writer.append(startKey, NullWritable.get()); 539 } 540 } finally { 541 writer.close(); 542 } 543 } 544 545 /** 546 * Configure a MapReduce Job to perform an incremental load into the given 547 * table. This 548 * <ul> 549 * <li>Inspects the table to configure a total order partitioner</li> 550 * <li>Uploads the partitions file to the cluster and adds it to the DistributedCache</li> 551 * <li>Sets the number of reduce tasks to match the current number of regions</li> 552 * <li>Sets the output key/value class to match HFileOutputFormat2's requirements</li> 553 * <li>Sets the reducer up to perform the appropriate sorting (either KeyValueSortReducer or 554 * PutSortReducer)</li> 555 * </ul> 556 * The user should be sure to set the map output value class to either KeyValue or Put before 557 * running this function. 558 */ 559 public static void configureIncrementalLoad(Job job, Table table, RegionLocator regionLocator) 560 throws IOException { 561 configureIncrementalLoad(job, table.getDescriptor(), regionLocator); 562 } 563 564 /** 565 * Configure a MapReduce Job to perform an incremental load into the given 566 * table. This 567 * <ul> 568 * <li>Inspects the table to configure a total order partitioner</li> 569 * <li>Uploads the partitions file to the cluster and adds it to the DistributedCache</li> 570 * <li>Sets the number of reduce tasks to match the current number of regions</li> 571 * <li>Sets the output key/value class to match HFileOutputFormat2's requirements</li> 572 * <li>Sets the reducer up to perform the appropriate sorting (either KeyValueSortReducer or 573 * PutSortReducer)</li> 574 * </ul> 575 * The user should be sure to set the map output value class to either KeyValue or Put before 576 * running this function. 577 */ 578 public static void configureIncrementalLoad(Job job, TableDescriptor tableDescriptor, 579 RegionLocator regionLocator) throws IOException { 580 ArrayList<TableInfo> singleTableInfo = new ArrayList<>(); 581 singleTableInfo.add(new TableInfo(tableDescriptor, regionLocator)); 582 configureIncrementalLoad(job, singleTableInfo, HFileOutputFormat2.class); 583 } 584 585 static void configureIncrementalLoad(Job job, List<TableInfo> multiTableInfo, 586 Class<? extends OutputFormat<?, ?>> cls) throws IOException { 587 Configuration conf = job.getConfiguration(); 588 job.setOutputKeyClass(ImmutableBytesWritable.class); 589 job.setOutputValueClass(MapReduceExtendedCell.class); 590 job.setOutputFormatClass(cls); 591 592 if (multiTableInfo.stream().distinct().count() != multiTableInfo.size()) { 593 throw new IllegalArgumentException("Duplicate entries found in TableInfo argument"); 594 } 595 boolean writeMultipleTables = false; 596 if (MultiTableHFileOutputFormat.class.equals(cls)) { 597 writeMultipleTables = true; 598 conf.setBoolean(MULTI_TABLE_HFILEOUTPUTFORMAT_CONF_KEY, true); 599 } 600 // Based on the configured map output class, set the correct reducer to properly 601 // sort the incoming values. 602 // TODO it would be nice to pick one or the other of these formats. 603 if (KeyValue.class.equals(job.getMapOutputValueClass()) 604 || MapReduceExtendedCell.class.equals(job.getMapOutputValueClass())) { 605 job.setReducerClass(CellSortReducer.class); 606 } else if (Put.class.equals(job.getMapOutputValueClass())) { 607 job.setReducerClass(PutSortReducer.class); 608 } else if (Text.class.equals(job.getMapOutputValueClass())) { 609 job.setReducerClass(TextSortReducer.class); 610 } else { 611 LOG.warn("Unknown map output value type:" + job.getMapOutputValueClass()); 612 } 613 614 conf.setStrings("io.serializations", conf.get("io.serializations"), 615 MutationSerialization.class.getName(), ResultSerialization.class.getName(), 616 CellSerialization.class.getName()); 617 618 if (conf.getBoolean(LOCALITY_SENSITIVE_CONF_KEY, DEFAULT_LOCALITY_SENSITIVE)) { 619 LOG.info("bulkload locality sensitive enabled"); 620 } 621 622 /* Now get the region start keys for every table required */ 623 List<String> allTableNames = new ArrayList<>(multiTableInfo.size()); 624 List<RegionLocator> regionLocators = new ArrayList<>( multiTableInfo.size()); 625 List<TableDescriptor> tableDescriptors = new ArrayList<>( multiTableInfo.size()); 626 627 for( TableInfo tableInfo : multiTableInfo ) 628 { 629 regionLocators.add(tableInfo.getRegionLocator()); 630 allTableNames.add(tableInfo.getRegionLocator().getName().getNameAsString()); 631 tableDescriptors.add(tableInfo.getTableDescriptor()); 632 } 633 // Record tablenames for creating writer by favored nodes, and decoding compression, block size and other attributes of columnfamily per table 634 conf.set(OUTPUT_TABLE_NAME_CONF_KEY, StringUtils.join(allTableNames, Bytes 635 .toString(tableSeparator))); 636 List<ImmutableBytesWritable> startKeys = getRegionStartKeys(regionLocators, writeMultipleTables); 637 // Use table's region boundaries for TOP split points. 638 LOG.info("Configuring " + startKeys.size() + " reduce partitions " + 639 "to match current region count for all tables"); 640 job.setNumReduceTasks(startKeys.size()); 641 642 configurePartitioner(job, startKeys, writeMultipleTables); 643 // Set compression algorithms based on column families 644 645 conf.set(COMPRESSION_FAMILIES_CONF_KEY, serializeColumnFamilyAttribute(compressionDetails, 646 tableDescriptors)); 647 conf.set(BLOCK_SIZE_FAMILIES_CONF_KEY, serializeColumnFamilyAttribute(blockSizeDetails, 648 tableDescriptors)); 649 conf.set(BLOOM_TYPE_FAMILIES_CONF_KEY, serializeColumnFamilyAttribute(bloomTypeDetails, 650 tableDescriptors)); 651 conf.set(DATABLOCK_ENCODING_FAMILIES_CONF_KEY, 652 serializeColumnFamilyAttribute(dataBlockEncodingDetails, tableDescriptors)); 653 654 TableMapReduceUtil.addDependencyJars(job); 655 TableMapReduceUtil.initCredentials(job); 656 LOG.info("Incremental output configured for tables: " + StringUtils.join(allTableNames, ",")); 657 } 658 659 public static void configureIncrementalLoadMap(Job job, TableDescriptor tableDescriptor) throws 660 IOException { 661 Configuration conf = job.getConfiguration(); 662 663 job.setOutputKeyClass(ImmutableBytesWritable.class); 664 job.setOutputValueClass(MapReduceExtendedCell.class); 665 job.setOutputFormatClass(HFileOutputFormat2.class); 666 667 ArrayList<TableDescriptor> singleTableDescriptor = new ArrayList<>(1); 668 singleTableDescriptor.add(tableDescriptor); 669 670 conf.set(OUTPUT_TABLE_NAME_CONF_KEY, tableDescriptor.getTableName().getNameAsString()); 671 // Set compression algorithms based on column families 672 conf.set(COMPRESSION_FAMILIES_CONF_KEY, 673 serializeColumnFamilyAttribute(compressionDetails, singleTableDescriptor)); 674 conf.set(BLOCK_SIZE_FAMILIES_CONF_KEY, 675 serializeColumnFamilyAttribute(blockSizeDetails, singleTableDescriptor)); 676 conf.set(BLOOM_TYPE_FAMILIES_CONF_KEY, 677 serializeColumnFamilyAttribute(bloomTypeDetails, singleTableDescriptor)); 678 conf.set(DATABLOCK_ENCODING_FAMILIES_CONF_KEY, 679 serializeColumnFamilyAttribute(dataBlockEncodingDetails, singleTableDescriptor)); 680 681 TableMapReduceUtil.addDependencyJars(job); 682 TableMapReduceUtil.initCredentials(job); 683 LOG.info("Incremental table " + tableDescriptor.getTableName() + " output configured."); 684 } 685 686 /** 687 * Runs inside the task to deserialize column family to compression algorithm 688 * map from the configuration. 689 * 690 * @param conf to read the serialized values from 691 * @return a map from column family to the configured compression algorithm 692 */ 693 @VisibleForTesting 694 static Map<byte[], Algorithm> createFamilyCompressionMap(Configuration 695 conf) { 696 Map<byte[], String> stringMap = createFamilyConfValueMap(conf, 697 COMPRESSION_FAMILIES_CONF_KEY); 698 Map<byte[], Algorithm> compressionMap = new TreeMap<>(Bytes.BYTES_COMPARATOR); 699 for (Map.Entry<byte[], String> e : stringMap.entrySet()) { 700 Algorithm algorithm = HFileWriterImpl.compressionByName(e.getValue()); 701 compressionMap.put(e.getKey(), algorithm); 702 } 703 return compressionMap; 704 } 705 706 /** 707 * Runs inside the task to deserialize column family to bloom filter type 708 * map from the configuration. 709 * 710 * @param conf to read the serialized values from 711 * @return a map from column family to the the configured bloom filter type 712 */ 713 @VisibleForTesting 714 static Map<byte[], BloomType> createFamilyBloomTypeMap(Configuration conf) { 715 Map<byte[], String> stringMap = createFamilyConfValueMap(conf, 716 BLOOM_TYPE_FAMILIES_CONF_KEY); 717 Map<byte[], BloomType> bloomTypeMap = new TreeMap<>(Bytes.BYTES_COMPARATOR); 718 for (Map.Entry<byte[], String> e : stringMap.entrySet()) { 719 BloomType bloomType = BloomType.valueOf(e.getValue()); 720 bloomTypeMap.put(e.getKey(), bloomType); 721 } 722 return bloomTypeMap; 723 } 724 725 /** 726 * Runs inside the task to deserialize column family to block size 727 * map from the configuration. 728 * 729 * @param conf to read the serialized values from 730 * @return a map from column family to the configured block size 731 */ 732 @VisibleForTesting 733 static Map<byte[], Integer> createFamilyBlockSizeMap(Configuration conf) { 734 Map<byte[], String> stringMap = createFamilyConfValueMap(conf, 735 BLOCK_SIZE_FAMILIES_CONF_KEY); 736 Map<byte[], Integer> blockSizeMap = new TreeMap<>(Bytes.BYTES_COMPARATOR); 737 for (Map.Entry<byte[], String> e : stringMap.entrySet()) { 738 Integer blockSize = Integer.parseInt(e.getValue()); 739 blockSizeMap.put(e.getKey(), blockSize); 740 } 741 return blockSizeMap; 742 } 743 744 /** 745 * Runs inside the task to deserialize column family to data block encoding 746 * type map from the configuration. 747 * 748 * @param conf to read the serialized values from 749 * @return a map from column family to HFileDataBlockEncoder for the 750 * configured data block type for the family 751 */ 752 @VisibleForTesting 753 static Map<byte[], DataBlockEncoding> createFamilyDataBlockEncodingMap( 754 Configuration conf) { 755 Map<byte[], String> stringMap = createFamilyConfValueMap(conf, 756 DATABLOCK_ENCODING_FAMILIES_CONF_KEY); 757 Map<byte[], DataBlockEncoding> encoderMap = new TreeMap<>(Bytes.BYTES_COMPARATOR); 758 for (Map.Entry<byte[], String> e : stringMap.entrySet()) { 759 encoderMap.put(e.getKey(), DataBlockEncoding.valueOf((e.getValue()))); 760 } 761 return encoderMap; 762 } 763 764 765 /** 766 * Run inside the task to deserialize column family to given conf value map. 767 * 768 * @param conf to read the serialized values from 769 * @param confName conf key to read from the configuration 770 * @return a map of column family to the given configuration value 771 */ 772 private static Map<byte[], String> createFamilyConfValueMap( 773 Configuration conf, String confName) { 774 Map<byte[], String> confValMap = new TreeMap<>(Bytes.BYTES_COMPARATOR); 775 String confVal = conf.get(confName, ""); 776 for (String familyConf : confVal.split("&")) { 777 String[] familySplit = familyConf.split("="); 778 if (familySplit.length != 2) { 779 continue; 780 } 781 try { 782 confValMap.put(Bytes.toBytes(URLDecoder.decode(familySplit[0], "UTF-8")), 783 URLDecoder.decode(familySplit[1], "UTF-8")); 784 } catch (UnsupportedEncodingException e) { 785 // will not happen with UTF-8 encoding 786 throw new AssertionError(e); 787 } 788 } 789 return confValMap; 790 } 791 792 /** 793 * Configure <code>job</code> with a TotalOrderPartitioner, partitioning against 794 * <code>splitPoints</code>. Cleans up the partitions file after job exists. 795 */ 796 static void configurePartitioner(Job job, List<ImmutableBytesWritable> splitPoints, boolean 797 writeMultipleTables) 798 throws IOException { 799 Configuration conf = job.getConfiguration(); 800 // create the partitions file 801 FileSystem fs = FileSystem.get(conf); 802 String hbaseTmpFsDir = 803 conf.get(HConstants.TEMPORARY_FS_DIRECTORY_KEY, 804 HConstants.DEFAULT_TEMPORARY_HDFS_DIRECTORY); 805 Path partitionsPath = new Path(hbaseTmpFsDir, "partitions_" + UUID.randomUUID()); 806 fs.makeQualified(partitionsPath); 807 writePartitions(conf, partitionsPath, splitPoints, writeMultipleTables); 808 fs.deleteOnExit(partitionsPath); 809 810 // configure job to use it 811 job.setPartitionerClass(TotalOrderPartitioner.class); 812 TotalOrderPartitioner.setPartitionFile(conf, partitionsPath); 813 } 814 815 @edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE") 816 @VisibleForTesting 817 static String serializeColumnFamilyAttribute(Function<ColumnFamilyDescriptor, String> fn, List<TableDescriptor> allTables) 818 throws UnsupportedEncodingException { 819 StringBuilder attributeValue = new StringBuilder(); 820 int i = 0; 821 for (TableDescriptor tableDescriptor : allTables) { 822 if (tableDescriptor == null) { 823 // could happen with mock table instance 824 // CODEREVIEW: Can I set an empty string in conf if mock table instance? 825 return ""; 826 } 827 for (ColumnFamilyDescriptor familyDescriptor : tableDescriptor.getColumnFamilies()) { 828 if (i++ > 0) { 829 attributeValue.append('&'); 830 } 831 attributeValue.append(URLEncoder.encode( 832 Bytes.toString(combineTableNameSuffix(tableDescriptor.getTableName().getName(), familyDescriptor.getName())), 833 "UTF-8")); 834 attributeValue.append('='); 835 attributeValue.append(URLEncoder.encode(fn.apply(familyDescriptor), "UTF-8")); 836 } 837 } 838 // Get rid of the last ampersand 839 return attributeValue.toString(); 840 } 841 842 /** 843 * Serialize column family to compression algorithm map to configuration. 844 * Invoked while configuring the MR job for incremental load. 845 * 846 * @param tableDescriptor to read the properties from 847 * @param conf to persist serialized values into 848 * @throws IOException 849 * on failure to read column family descriptors 850 */ 851 @VisibleForTesting 852 static Function<ColumnFamilyDescriptor, String> compressionDetails = familyDescriptor -> 853 familyDescriptor.getCompressionType().getName(); 854 855 /** 856 * Serialize column family to block size map to configuration. Invoked while 857 * configuring the MR job for incremental load. 858 * 859 * @param tableDescriptor 860 * to read the properties from 861 * @param conf 862 * to persist serialized values into 863 * 864 * @throws IOException 865 * on failure to read column family descriptors 866 */ 867 @VisibleForTesting 868 static Function<ColumnFamilyDescriptor, String> blockSizeDetails = familyDescriptor -> String 869 .valueOf(familyDescriptor.getBlocksize()); 870 871 /** 872 * Serialize column family to bloom type map to configuration. Invoked while 873 * configuring the MR job for incremental load. 874 * 875 * @param tableDescriptor 876 * to read the properties from 877 * @param conf 878 * to persist serialized values into 879 * 880 * @throws IOException 881 * on failure to read column family descriptors 882 */ 883 @VisibleForTesting 884 static Function<ColumnFamilyDescriptor, String> bloomTypeDetails = familyDescriptor -> { 885 String bloomType = familyDescriptor.getBloomFilterType().toString(); 886 if (bloomType == null) { 887 bloomType = ColumnFamilyDescriptorBuilder.DEFAULT_BLOOMFILTER.name(); 888 } 889 return bloomType; 890 }; 891 892 /** 893 * Serialize column family to data block encoding map to configuration. 894 * Invoked while configuring the MR job for incremental load. 895 * 896 * @param tableDescriptor 897 * to read the properties from 898 * @param conf 899 * to persist serialized values into 900 * @throws IOException 901 * on failure to read column family descriptors 902 */ 903 @VisibleForTesting 904 static Function<ColumnFamilyDescriptor, String> dataBlockEncodingDetails = familyDescriptor -> { 905 DataBlockEncoding encoding = familyDescriptor.getDataBlockEncoding(); 906 if (encoding == null) { 907 encoding = DataBlockEncoding.NONE; 908 } 909 return encoding.toString(); 910 }; 911 912}