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.mapreduce.HFileOutputFormat2.MULTI_TABLE_HFILEOUTPUTFORMAT_CONF_DEFAULT; 021import static org.apache.hadoop.hbase.mapreduce.HFileOutputFormat2.MULTI_TABLE_HFILEOUTPUTFORMAT_CONF_KEY; 022 023import java.io.IOException; 024import java.text.ParseException; 025import java.text.SimpleDateFormat; 026import java.util.ArrayList; 027import java.util.Collections; 028import java.util.HashSet; 029import java.util.List; 030import java.util.Map; 031import java.util.Set; 032import java.util.TreeMap; 033import org.apache.hadoop.conf.Configuration; 034import org.apache.hadoop.conf.Configured; 035import org.apache.hadoop.fs.Path; 036import org.apache.hadoop.hbase.Cell; 037import org.apache.hadoop.hbase.CellUtil; 038import org.apache.hadoop.hbase.ExtendedCell; 039import org.apache.hadoop.hbase.HBaseConfiguration; 040import org.apache.hadoop.hbase.KeyValue; 041import org.apache.hadoop.hbase.PrivateCellUtil; 042import org.apache.hadoop.hbase.TableName; 043import org.apache.hadoop.hbase.client.Connection; 044import org.apache.hadoop.hbase.client.ConnectionFactory; 045import org.apache.hadoop.hbase.client.Delete; 046import org.apache.hadoop.hbase.client.Mutation; 047import org.apache.hadoop.hbase.client.Put; 048import org.apache.hadoop.hbase.client.RegionLocator; 049import org.apache.hadoop.hbase.client.Table; 050import org.apache.hadoop.hbase.io.ImmutableBytesWritable; 051import org.apache.hadoop.hbase.mapreduce.HFileOutputFormat2.TableInfo; 052import org.apache.hadoop.hbase.regionserver.wal.WALCellCodec; 053import org.apache.hadoop.hbase.snapshot.SnapshotRegionLocator; 054import org.apache.hadoop.hbase.util.Bytes; 055import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; 056import org.apache.hadoop.hbase.util.MapReduceExtendedCell; 057import org.apache.hadoop.hbase.wal.WALEdit; 058import org.apache.hadoop.hbase.wal.WALEditInternalHelper; 059import org.apache.hadoop.hbase.wal.WALKey; 060import org.apache.hadoop.io.WritableComparable; 061import org.apache.hadoop.mapreduce.Job; 062import org.apache.hadoop.mapreduce.Mapper; 063import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; 064import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; 065import org.apache.hadoop.util.Tool; 066import org.apache.hadoop.util.ToolRunner; 067import org.apache.yetus.audience.InterfaceAudience; 068import org.slf4j.Logger; 069import org.slf4j.LoggerFactory; 070 071/** 072 * A tool to replay WAL files as a M/R job. The WAL can be replayed for a set of tables or all 073 * tables, and a time range can be provided (in milliseconds). The WAL is filtered to the passed set 074 * of tables and the output can optionally be mapped to another set of tables. WAL replay can also 075 * generate HFiles for later bulk importing, in that case the WAL is replayed for a single table 076 * only. 077 */ 078@InterfaceAudience.Public 079public class WALPlayer extends Configured implements Tool { 080 private static final Logger LOG = LoggerFactory.getLogger(WALPlayer.class); 081 final static String NAME = "WALPlayer"; 082 public final static String BULK_OUTPUT_CONF_KEY = "wal.bulk.output"; 083 public final static String TABLES_KEY = "wal.input.tables"; 084 public final static String TABLE_MAP_KEY = "wal.input.tablesmap"; 085 public final static String INPUT_FILES_SEPARATOR_KEY = "wal.input.separator"; 086 public final static String IGNORE_MISSING_FILES = "wal.input.ignore.missing.files"; 087 public final static String MULTI_TABLES_SUPPORT = "wal.multi.tables.support"; 088 089 /** 090 * Configuration flag that controls how the WALPlayer handles empty input WAL files. 091 * <p> 092 * If set to {@code true}, the WALPlayer will silently ignore empty files that cannot be parsed as 093 * valid WAL files. This is useful in scenarios where such files are expected (e.g., due to 094 * partial writes or cleanup operations). 095 * </p> 096 * <p> 097 * If set to {@code false} (default), the WALPlayer will throw an exception when it encounters an 098 * empty or un-parsable WAL file. This is useful for catching unexpected data issues early. 099 * </p> 100 * <p> 101 * Default value: {@link #DEFAULT_IGNORE_EMPTY_FILES} ({@code false}) 102 * </p> 103 */ 104 public final static String IGNORE_EMPTY_FILES = "wal.input.ignore.empty.files"; 105 public final static boolean DEFAULT_IGNORE_EMPTY_FILES = false; 106 107 protected static final String tableSeparator = ";"; 108 109 private final static String JOB_NAME_CONF_KEY = "mapreduce.job.name"; 110 111 public WALPlayer() { 112 } 113 114 protected WALPlayer(final Configuration c) { 115 super(c); 116 } 117 118 /** 119 * A mapper that just writes out KeyValues. This one can be used together with 120 * {@link CellSortReducer} 121 */ 122 static class WALKeyValueMapper extends Mapper<WALKey, WALEdit, WritableComparable<?>, Cell> { 123 private Set<String> tableSet = new HashSet<String>(); 124 private boolean multiTableSupport = false; 125 private boolean diskBasedSortingEnabled = false; 126 127 @Override 128 public void map(WALKey key, WALEdit value, Context context) throws IOException { 129 try { 130 // skip all other tables 131 TableName table = key.getTableName(); 132 if (tableSet.contains(table.getNameAsString())) { 133 for (Cell cell : value.getCells()) { 134 if (WALEdit.isMetaEditFamily(cell)) { 135 continue; 136 } 137 138 // Set sequenceId from WALKey, since it is not included by WALCellCodec. The sequenceId 139 // on WALKey is the same value that was on the cells in the WALEdit. This enables 140 // CellSortReducer to use sequenceId to disambiguate duplicate cell timestamps. 141 // See HBASE-27649 142 PrivateCellUtil.setSequenceId(cell, key.getSequenceId()); 143 144 byte[] outKey = multiTableSupport 145 ? Bytes.add(table.getName(), Bytes.toBytes(tableSeparator), CellUtil.cloneRow(cell)) 146 : CellUtil.cloneRow(cell); 147 ExtendedCell extendedCell = PrivateCellUtil.ensureExtendedCell(cell); 148 context.write(wrapKey(outKey, extendedCell), new MapReduceExtendedCell(extendedCell)); 149 } 150 } 151 } catch (InterruptedException e) { 152 LOG.error("Interrupted while emitting Cell", e); 153 Thread.currentThread().interrupt(); 154 } 155 } 156 157 @Override 158 public void setup(Context context) throws IOException { 159 Configuration conf = context.getConfiguration(); 160 String[] tables = conf.getStrings(TABLES_KEY); 161 this.multiTableSupport = conf.getBoolean(MULTI_TABLES_SUPPORT, false); 162 this.diskBasedSortingEnabled = HFileOutputFormat2.diskBasedSortingEnabled(conf); 163 Collections.addAll(tableSet, tables); 164 } 165 166 private WritableComparable<?> wrapKey(byte[] key, ExtendedCell cell) { 167 if (this.diskBasedSortingEnabled) { 168 // Important to build a new cell with the updated key to maintain multi-table support 169 KeyValue kv = new KeyValue(key, 0, key.length, cell.getFamilyArray(), 170 cell.getFamilyOffset(), cell.getFamilyLength(), cell.getQualifierArray(), 171 cell.getQualifierOffset(), cell.getQualifierLength(), cell.getTimestamp(), 172 KeyValue.Type.codeToType(PrivateCellUtil.getTypeByte(cell)), null, 0, 0); 173 kv.setSequenceId(cell.getSequenceId()); 174 return new KeyOnlyCellComparable(kv); 175 } else { 176 return new ImmutableBytesWritable(key); 177 } 178 } 179 } 180 181 /** 182 * Enum for map metrics. Keep it out here rather than inside in the Map inner-class so we can find 183 * associated properties. 184 */ 185 protected static enum Counter { 186 /** Number of aggregated writes */ 187 PUTS, 188 /** Number of aggregated deletes */ 189 DELETES, 190 CELLS_READ, 191 CELLS_WRITTEN, 192 WALEDITS 193 } 194 195 /** 196 * A mapper that writes out {@link Mutation} to be directly applied to a running HBase instance. 197 */ 198 protected static class WALMapper 199 extends Mapper<WALKey, WALEdit, ImmutableBytesWritable, Mutation> { 200 private Map<TableName, TableName> tables = new TreeMap<>(); 201 202 @Override 203 public void map(WALKey key, WALEdit value, Context context) throws IOException { 204 context.getCounter(Counter.WALEDITS).increment(1); 205 try { 206 if (tables.isEmpty() || tables.containsKey(key.getTableName())) { 207 TableName targetTable = 208 tables.isEmpty() ? key.getTableName() : tables.get(key.getTableName()); 209 ImmutableBytesWritable tableOut = new ImmutableBytesWritable(targetTable.getName()); 210 Put put = null; 211 Delete del = null; 212 ExtendedCell lastCell = null; 213 for (ExtendedCell cell : WALEditInternalHelper.getExtendedCells(value)) { 214 context.getCounter(Counter.CELLS_READ).increment(1); 215 // Filtering WAL meta marker entries. 216 if (WALEdit.isMetaEditFamily(cell)) { 217 continue; 218 } 219 // Allow a subclass filter out this cell. 220 if (filter(context, cell)) { 221 // A WALEdit may contain multiple operations (HBASE-3584) and/or 222 // multiple rows (HBASE-5229). 223 // Aggregate as much as possible into a single Put/Delete 224 // operation before writing to the context. 225 if ( 226 lastCell == null || lastCell.getTypeByte() != cell.getTypeByte() 227 || !CellUtil.matchingRows(lastCell, cell) 228 ) { 229 // row or type changed, write out aggregate KVs. 230 if (put != null) { 231 context.write(tableOut, put); 232 context.getCounter(Counter.PUTS).increment(1); 233 } 234 if (del != null) { 235 context.write(tableOut, del); 236 context.getCounter(Counter.DELETES).increment(1); 237 } 238 if (CellUtil.isDelete(cell)) { 239 del = new Delete(CellUtil.cloneRow(cell)); 240 } else { 241 put = new Put(CellUtil.cloneRow(cell)); 242 } 243 } 244 if (CellUtil.isDelete(cell)) { 245 del.add(cell); 246 } else { 247 put.add(cell); 248 } 249 context.getCounter(Counter.CELLS_WRITTEN).increment(1); 250 } 251 lastCell = cell; 252 } 253 // write residual KVs 254 if (put != null) { 255 context.write(tableOut, put); 256 context.getCounter(Counter.PUTS).increment(1); 257 } 258 if (del != null) { 259 context.getCounter(Counter.DELETES).increment(1); 260 context.write(tableOut, del); 261 } 262 } 263 } catch (InterruptedException e) { 264 LOG.error("Interrupted while writing results", e); 265 Thread.currentThread().interrupt(); 266 } 267 } 268 269 protected boolean filter(Context context, final Cell cell) { 270 return true; 271 } 272 273 @Override 274 protected void 275 cleanup(Mapper<WALKey, WALEdit, ImmutableBytesWritable, Mutation>.Context context) 276 throws IOException, InterruptedException { 277 super.cleanup(context); 278 } 279 280 @SuppressWarnings("checkstyle:EmptyBlock") 281 @Override 282 public void setup(Context context) throws IOException { 283 String[] tableMap = context.getConfiguration().getStrings(TABLE_MAP_KEY); 284 String[] tablesToUse = context.getConfiguration().getStrings(TABLES_KEY); 285 if (tableMap == null) { 286 tableMap = tablesToUse; 287 } 288 if (tablesToUse == null) { 289 // Then user wants all tables. 290 } else if (tablesToUse.length != tableMap.length) { 291 // this can only happen when WALMapper is used directly by a class other than WALPlayer 292 throw new IOException("Incorrect table mapping specified ."); 293 } 294 int i = 0; 295 if (tablesToUse != null) { 296 for (String table : tablesToUse) { 297 tables.put(TableName.valueOf(table), TableName.valueOf(tableMap[i++])); 298 } 299 } 300 } 301 } 302 303 void setupTime(Configuration conf, String option) throws IOException { 304 String val = conf.get(option); 305 if (null == val) { 306 return; 307 } 308 long ms; 309 try { 310 // first try to parse in user friendly form 311 ms = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SS").parse(val).getTime(); 312 } catch (ParseException pe) { 313 try { 314 // then see if just a number of ms's was specified 315 ms = Long.parseLong(val); 316 } catch (NumberFormatException nfe) { 317 throw new IOException( 318 option + " must be specified either in the form 2001-02-20T16:35:06.99 " 319 + "or as number of milliseconds"); 320 } 321 } 322 conf.setLong(option, ms); 323 } 324 325 /** 326 * Sets up the actual job. 327 * @param args The command line parameters. 328 * @return The newly created job. 329 * @throws IOException When setting up the job fails. 330 */ 331 public Job createSubmittableJob(String[] args) throws IOException { 332 Configuration conf = getConf(); 333 setupTime(conf, WALInputFormat.START_TIME_KEY); 334 setupTime(conf, WALInputFormat.END_TIME_KEY); 335 String inputDirs = args[0]; 336 String[] tables = args.length == 1 ? new String[] {} : args[1].split(","); 337 String[] tableMap; 338 if (args.length > 2) { 339 tableMap = args[2].split(","); 340 if (tableMap.length != tables.length) { 341 throw new IOException("The same number of tables and mapping must be provided."); 342 } 343 } else { 344 // if no mapping is specified, map each table to itself 345 tableMap = tables; 346 } 347 conf.setStrings(TABLES_KEY, tables); 348 conf.setStrings(TABLE_MAP_KEY, tableMap); 349 conf.set(FileInputFormat.INPUT_DIR, inputDirs); 350 Job job = Job.getInstance(conf, 351 conf.get(JOB_NAME_CONF_KEY, NAME + "_" + EnvironmentEdgeManager.currentTime())); 352 job.setJarByClass(WALPlayer.class); 353 354 job.setInputFormatClass(WALInputFormat.class); 355 boolean diskBasedSortingEnabled = HFileOutputFormat2.diskBasedSortingEnabled(conf); 356 if (diskBasedSortingEnabled) { 357 job.setMapOutputKeyClass(KeyOnlyCellComparable.class); 358 job.setSortComparatorClass(KeyOnlyCellComparable.KeyOnlyCellComparator.class); 359 } else { 360 job.setMapOutputKeyClass(ImmutableBytesWritable.class); 361 } 362 363 String hfileOutPath = conf.get(BULK_OUTPUT_CONF_KEY); 364 if (hfileOutPath != null) { 365 LOG.debug("add incremental job :" + hfileOutPath + " from " + inputDirs); 366 367 // WALPlayer needs ExtendedCellSerialization so that sequenceId can be propagated when 368 // sorting cells in CellSortReducer 369 job.getConfiguration().setBoolean(HFileOutputFormat2.EXTENDED_CELL_SERIALIZATION_ENABLED_KEY, 370 true); 371 372 // the bulk HFile case 373 List<TableName> tableNames = getTableNameList(tables); 374 375 job.setMapperClass(WALKeyValueMapper.class); 376 if (diskBasedSortingEnabled) { 377 job.setReducerClass(PreSortedCellsReducer.class); 378 } else { 379 job.setReducerClass(CellSortReducer.class); 380 } 381 Path outputDir = new Path(hfileOutPath); 382 FileOutputFormat.setOutputPath(job, outputDir); 383 job.setMapOutputValueClass(MapReduceExtendedCell.class); 384 try (Connection conn = ConnectionFactory.createConnection(conf)) { 385 if ( 386 conf.getBoolean(MULTI_TABLE_HFILEOUTPUTFORMAT_CONF_KEY, 387 MULTI_TABLE_HFILEOUTPUTFORMAT_CONF_DEFAULT) 388 ) { 389 // The HFiles will be output to something like this for each table: 390 // .../BULK_OUTPUT_CONF_KEY/namespace/table/columnFamily 391 List<TableInfo> tableInfoList = new ArrayList<TableInfo>(); 392 for (TableName tableName : tableNames) { 393 Table table = conn.getTable(tableName); 394 RegionLocator regionLocator = getRegionLocator(tableName, conf, conn); 395 tableInfoList.add(new TableInfo(table.getDescriptor(), regionLocator)); 396 } 397 MultiTableHFileOutputFormat.configureIncrementalLoad(job, tableInfoList); 398 } else { 399 // The HFiles will be output to something like: .../BULK_OUTPUT_CONF_KEY/columnFamily 400 // This is useful for scenarios where we are running the WALPlayer consecutively on just 401 // one table at a time, and BULK_OUTPUT_CONF_KEY is already set to a "namespace/table" 402 // directory path for each table. 403 if (tableNames.size() != 1) { 404 throw new IOException("Expected table names list to have only one table since " 405 + MULTI_TABLE_HFILEOUTPUTFORMAT_CONF_KEY + " is set to false. Got the following " 406 + "list of tables instead: " + tableNames); 407 } 408 TableName tableName = tableNames.get(0); 409 Table table = conn.getTable(tableName); 410 RegionLocator regionLocator = getRegionLocator(tableName, conf, conn); 411 HFileOutputFormat2.configureIncrementalLoad(job, table.getDescriptor(), regionLocator); 412 } 413 } 414 TableMapReduceUtil.addDependencyJarsForClasses(job.getConfiguration(), 415 org.apache.hbase.thirdparty.com.google.common.base.Preconditions.class); 416 } else { 417 // output to live cluster 418 job.setMapperClass(WALMapper.class); 419 job.setOutputFormatClass(MultiTableOutputFormat.class); 420 TableMapReduceUtil.addDependencyJars(job); 421 TableMapReduceUtil.initCredentials(job); 422 // No reducers. 423 job.setNumReduceTasks(0); 424 } 425 String codecCls = WALCellCodec.getWALCellCodecClass(conf).getName(); 426 try { 427 TableMapReduceUtil.addDependencyJarsForClasses(job.getConfiguration(), 428 Class.forName(codecCls)); 429 } catch (Exception e) { 430 throw new IOException("Cannot determine wal codec class " + codecCls, e); 431 } 432 return job; 433 } 434 435 private List<TableName> getTableNameList(String[] tables) { 436 List<TableName> list = new ArrayList<TableName>(); 437 for (String name : tables) { 438 list.add(TableName.valueOf(name)); 439 } 440 return list; 441 } 442 443 /** 444 * Print usage 445 * @param errorMsg Error message. Can be null. 446 */ 447 private void usage(final String errorMsg) { 448 if (errorMsg != null && errorMsg.length() > 0) { 449 System.err.println("ERROR: " + errorMsg); 450 } 451 System.err.println("Usage: " + NAME + " [options] <WAL inputdir> [<tables> <tableMappings>]"); 452 System.err.println(" <WAL inputdir> directory of WALs to replay."); 453 System.err.println(" <tables> comma separated list of tables. If no tables specified,"); 454 System.err.println(" all are imported (even hbase:meta if present)."); 455 System.err.println( 456 " <tableMappings> WAL entries can be mapped to a new set of tables by " + "passing"); 457 System.err 458 .println(" <tableMappings>, a comma separated list of target " + "tables."); 459 System.err 460 .println(" If specified, each table in <tables> must have a " + "mapping."); 461 System.err.println("To generate HFiles to bulk load instead of loading HBase directly, pass:"); 462 System.err.println(" -D" + BULK_OUTPUT_CONF_KEY + "=/path/for/output"); 463 System.err.println(" Only one table can be specified, and no mapping allowed!"); 464 System.err.println("To specify a time range, pass:"); 465 System.err.println(" -D" + WALInputFormat.START_TIME_KEY + "=[date|ms]"); 466 System.err.println(" -D" + WALInputFormat.END_TIME_KEY + "=[date|ms]"); 467 System.err.println(" The start and the end date of timerange (inclusive). The dates can be"); 468 System.err 469 .println(" expressed in milliseconds-since-epoch or yyyy-MM-dd'T'HH:mm:ss.SS " + "format."); 470 System.err.println(" E.g. 1234567890120 or 2009-02-13T23:32:30.12"); 471 System.err.println("Other options:"); 472 System.err.println(" -D" + JOB_NAME_CONF_KEY + "=jobName"); 473 System.err.println(" Use the specified mapreduce job name for the wal player"); 474 System.err.println(" -Dwal.input.separator=' '"); 475 System.err.println(" Change WAL filename separator (WAL dir names use default ','.)"); 476 System.err.println("For performance also consider the following options:\n" 477 + " -Dmapreduce.map.speculative=false\n" + " -Dmapreduce.reduce.speculative=false"); 478 } 479 480 /** 481 * Main entry point. 482 * @param args The command line parameters. 483 * @throws Exception When running the job fails. 484 */ 485 public static void main(String[] args) throws Exception { 486 int ret = ToolRunner.run(new WALPlayer(HBaseConfiguration.create()), args); 487 System.exit(ret); 488 } 489 490 @Override 491 public int run(String[] args) throws Exception { 492 if (args.length < 1) { 493 usage("Wrong number of arguments: " + args.length); 494 System.exit(-1); 495 } 496 Job job = createSubmittableJob(args); 497 return job.waitForCompletion(true) ? 0 : 1; 498 } 499 500 private static RegionLocator getRegionLocator(TableName tableName, Configuration conf, 501 Connection conn) throws IOException { 502 if (SnapshotRegionLocator.shouldUseSnapshotRegionLocator(conf, tableName)) { 503 return SnapshotRegionLocator.create(conf, tableName); 504 } 505 506 return conn.getRegionLocator(tableName); 507 } 508}