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.backup.mapreduce; 019 020import static org.apache.hadoop.hbase.mapreduce.WALPlayer.TABLES_KEY; 021import static org.apache.hadoop.hbase.mapreduce.WALPlayer.TABLE_MAP_KEY; 022 023import java.io.IOException; 024import java.text.ParseException; 025import java.text.SimpleDateFormat; 026import java.util.List; 027import java.util.Map; 028import java.util.TreeMap; 029import org.apache.hadoop.conf.Configuration; 030import org.apache.hadoop.conf.Configured; 031import org.apache.hadoop.fs.Path; 032import org.apache.hadoop.hbase.HBaseConfiguration; 033import org.apache.hadoop.hbase.TableName; 034import org.apache.hadoop.hbase.backup.util.BackupFileSystemManager; 035import org.apache.hadoop.hbase.backup.util.BulkLoadProcessor; 036import org.apache.hadoop.hbase.mapreduce.TableMapReduceUtil; 037import org.apache.hadoop.hbase.mapreduce.WALInputFormat; 038import org.apache.hadoop.hbase.regionserver.wal.WALCellCodec; 039import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; 040import org.apache.hadoop.hbase.wal.WALEdit; 041import org.apache.hadoop.hbase.wal.WALKey; 042import org.apache.hadoop.io.NullWritable; 043import org.apache.hadoop.io.Text; 044import org.apache.hadoop.mapreduce.Job; 045import org.apache.hadoop.mapreduce.Mapper; 046import org.apache.hadoop.mapreduce.Reducer; 047import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; 048import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; 049import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat; 050import org.apache.hadoop.util.Tool; 051import org.apache.hadoop.util.ToolRunner; 052import org.apache.yetus.audience.InterfaceAudience; 053import org.slf4j.Logger; 054import org.slf4j.LoggerFactory; 055 056/** 057 * MapReduce job that scans WAL backups and extracts referenced bulk-load store-file paths. 058 * <p> 059 * This job is intended to be used when you want a list of HFiles / store-files referenced by WAL 060 * bulk-load descriptors. It emits a de-duplicated list of full paths (one per line) by default 061 * using the {@link DedupReducer}. 062 * </p> 063 * <p> 064 * Usage (CLI): 065 * {@code BulkLoadCollector <WAL inputdir> <bulk-files-output-dir> [<tables> [<tableMappings>]]} 066 * </p> 067 */ 068@InterfaceAudience.Private 069public class BulkLoadCollectorJob extends Configured implements Tool { 070 private static final Logger LOG = LoggerFactory.getLogger(BulkLoadCollectorJob.class); 071 072 public static final String NAME = "BulkLoadCollector"; 073 public static final String DEFAULT_REDUCERS = "1"; 074 075 public BulkLoadCollectorJob() { 076 } 077 078 public BulkLoadCollectorJob(final Configuration c) { 079 super(c); 080 } 081 082 /** 083 * Mapper that extracts relative bulk-load paths from a WAL entry (via {@code BulkLoadProcessor}), 084 * resolves them to full paths (via 085 * {@code BackupFileSystemManager#resolveBulkLoadFullPath(Path, Path)}), and emits each full path 086 * as the map key (Text). Uses the same table-filtering semantics as other WAL mappers: if no 087 * tables are configured, all tables are processed; otherwise only the configured table set is 088 * processed. Map output: (Text fullPathString, NullWritable) 089 */ 090 public static class BulkLoadCollectorMapper extends Mapper<WALKey, WALEdit, Text, NullWritable> { 091 private final Map<TableName, TableName> tables = new TreeMap<>(); 092 private final Text out = new Text(); 093 094 @Override 095 protected void map(WALKey key, WALEdit value, Context context) 096 throws IOException, InterruptedException { 097 if (key == null) { 098 if (LOG.isTraceEnabled()) LOG.trace("map: received null WALKey, skipping"); 099 return; 100 } 101 if (value == null) { 102 if (LOG.isTraceEnabled()) 103 LOG.trace("map: received null WALEdit for table={}, skipping", safeTable(key)); 104 return; 105 } 106 107 TableName tname = key.getTableName(); 108 109 // table filtering 110 if (!(tables.isEmpty() || tables.containsKey(tname))) { 111 if (LOG.isTraceEnabled()) { 112 LOG.trace("map: skipping table={} because it is not in configured table list", tname); 113 } 114 return; 115 } 116 117 // Extract relative store-file paths referenced by this WALEdit. 118 // Delegates parsing to BulkLoadProcessor so parsing logic is centralized. 119 List<Path> relativePaths = BulkLoadProcessor.processBulkLoadFiles(key, value); 120 if (relativePaths.isEmpty()) return; 121 122 // Determine WAL input path for this split (used to compute date/prefix for full path) 123 Path walInputPath; 124 try { 125 walInputPath = 126 new Path(((WALInputFormat.WALSplit) context.getInputSplit()).getLogFileName()); 127 } catch (ClassCastException cce) { 128 String splitClass = 129 (context.getInputSplit() == null) ? "null" : context.getInputSplit().getClass().getName(); 130 LOG.warn( 131 "map: unexpected InputSplit type (not WALSplit) - cannot determine WAL input path; context.getInputSplit() class={}", 132 splitClass, cce); 133 throw new IOException("Unexpected InputSplit type: expected WALSplit but got " + splitClass, 134 cce); 135 } 136 137 if (LOG.isTraceEnabled()) { 138 LOG.trace("map: walInputPath={} table={} relativePathsCount={}", walInputPath, tname, 139 relativePaths.size()); 140 } 141 142 // Build full path for each relative path and emit it. 143 for (Path rel : relativePaths) { 144 Path full = BackupFileSystemManager.resolveBulkLoadFullPath(walInputPath, rel); 145 out.set(full.toString()); 146 context.write(out, NullWritable.get()); 147 context.getCounter("BulkCollector", "StoreFilesEmitted").increment(1); 148 } 149 } 150 151 @Override 152 protected void setup(Context context) throws IOException { 153 String[] tableMap = context.getConfiguration().getStrings(TABLE_MAP_KEY); 154 String[] tablesToUse = context.getConfiguration().getStrings(TABLES_KEY); 155 if (tableMap == null) { 156 tableMap = tablesToUse; 157 } 158 if (tablesToUse == null) { 159 // user requested all tables; tables map remains empty to indicate "all" 160 return; 161 } 162 163 if (tablesToUse.length != tableMap.length) { 164 throw new IOException("Incorrect table mapping specified."); 165 } 166 167 int i = 0; 168 for (String table : tablesToUse) { 169 TableName from = TableName.valueOf(table); 170 TableName to = TableName.valueOf(tableMap[i++]); 171 tables.put(from, to); 172 if (LOG.isDebugEnabled()) { 173 LOG.debug("setup: configuring mapping {} -> {}", from, to); 174 } 175 } 176 } 177 178 private String safeTable(WALKey key) { 179 try { 180 return key == null ? "<null>" : key.getTableName().toString(); 181 } catch (Exception e) { 182 return "<error>"; 183 } 184 } 185 } 186 187 /** 188 * Reducer that deduplicates full-path keys emitted by the mappers. It writes each unique key 189 * exactly once. Reduce input: (Text fullPathString, Iterable<NullWritable>) Reduce output: (Text 190 * fullPathString, NullWritable) 191 */ 192 public static class DedupReducer extends Reducer<Text, NullWritable, Text, NullWritable> { 193 @Override 194 protected void reduce(Text key, Iterable<NullWritable> values, Context ctx) 195 throws IOException, InterruptedException { 196 // Write the unique path once. 197 ctx.write(key, NullWritable.get()); 198 } 199 } 200 201 /** 202 * Create and configure a Job instance for bulk-file collection. 203 * @param args CLI args expected to be: inputDirs bulkFilesOut [tables] [tableMap] 204 * @throws IOException on misconfiguration 205 */ 206 public Job createSubmittableJob(String[] args) throws IOException { 207 Configuration conf = getConf(); 208 209 setupTime(conf, WALInputFormat.START_TIME_KEY); 210 setupTime(conf, WALInputFormat.END_TIME_KEY); 211 212 if (args == null || args.length < 2) { 213 throw new IOException( 214 "Usage: <WAL inputdir> <bulk-files-output-dir> [<tables> [<tableMappings>]]"); 215 } 216 217 String inputDirs = args[0]; 218 String bulkFilesOut = args[1]; 219 220 // tables are optional (args[2]) 221 String[] tables = (args.length == 2) ? new String[] {} : args[2].split(","); 222 String[] tableMap; 223 if (args.length > 3) { 224 tableMap = args[3].split(","); 225 if (tableMap.length != tables.length) { 226 throw new IOException("The same number of tables and mapping must be provided."); 227 } 228 } else { 229 // if no mapping is specified, map each table to itself 230 tableMap = tables; 231 } 232 233 LOG.info("createSubmittableJob: inputDirs='{}' bulkFilesOut='{}' tables={} tableMap={}", 234 inputDirs, bulkFilesOut, String.join(",", tables), String.join(",", tableMap)); 235 236 conf.setStrings(TABLES_KEY, tables); 237 conf.setStrings(TABLE_MAP_KEY, tableMap); 238 conf.set(FileInputFormat.INPUT_DIR, inputDirs); 239 240 // create and return the actual Job configured for bulk-file discovery 241 return BulkLoadCollectorJob.createSubmittableJob(conf, inputDirs, bulkFilesOut); 242 } 243 244 /** 245 * Low-level job wiring. Creates the Job instance and sets input, mapper, reducer and output. 246 * @param conf configuration used for the job 247 * @param inputDirs WAL input directories (comma-separated) 248 * @param bulkFilesOut output directory to write discovered full-paths 249 * @throws IOException on invalid args 250 */ 251 private static Job createSubmittableJob(Configuration conf, String inputDirs, String bulkFilesOut) 252 throws IOException { 253 if (bulkFilesOut == null || bulkFilesOut.isEmpty()) { 254 throw new IOException("bulkFilesOut (output dir) must be provided."); 255 } 256 if (inputDirs == null || inputDirs.isEmpty()) { 257 throw new IOException("inputDirs (WAL input dir) must be provided."); 258 } 259 260 Job job = Job.getInstance(conf, NAME + "_" + EnvironmentEdgeManager.currentTime()); 261 job.setJarByClass(BulkLoadCollectorJob.class); 262 263 // Input: use same WALInputFormat used by WALPlayer so we parse WALs consistently 264 job.setInputFormatClass(WALInputFormat.class); 265 FileInputFormat.setInputDirRecursive(job, true); 266 FileInputFormat.setInputPaths(job, inputDirs); 267 268 // Mapper: extract and emit full bulk-load file paths (Text, NullWritable) 269 job.setMapperClass(BulkLoadCollectorMapper.class); 270 job.setMapOutputKeyClass(Text.class); 271 job.setMapOutputValueClass(NullWritable.class); 272 273 // Reducer: deduplicate the full-path keys 274 job.setReducerClass(DedupReducer.class); 275 // default to a single reducer (single deduped file); callers can set mapreduce.job.reduces 276 int reducers = conf.getInt("mapreduce.job.reduces", Integer.parseInt(DEFAULT_REDUCERS)); 277 job.setNumReduceTasks(reducers); 278 279 // Output: write plain text lines (one path per line) 280 job.setOutputFormatClass(TextOutputFormat.class); 281 FileOutputFormat.setOutputPath(job, new Path(bulkFilesOut)); 282 283 LOG.info("createSubmittableJob: created job name='{}' reducers={}", job.getJobName(), reducers); 284 285 String codecCls = WALCellCodec.getWALCellCodecClass(conf).getName(); 286 try { 287 TableMapReduceUtil.addDependencyJarsForClasses(job.getConfiguration(), 288 Class.forName(codecCls)); 289 } catch (Exception e) { 290 throw new IOException("Cannot determine wal codec class " + codecCls, e); 291 } 292 return job; 293 } 294 295 /** 296 * Parse a time option. Supports the user-friendly ISO-like format 297 * {@code yyyy-MM-dd'T'HH:mm:ss.SS} or milliseconds since epoch. If the option is not present, 298 * this method is a no-op. 299 * @param conf configuration containing option 300 * @param option key to read (e.g. WALInputFormat.START_TIME_KEY) 301 * @throws IOException on parse failure 302 */ 303 private void setupTime(Configuration conf, String option) throws IOException { 304 String val = conf.get(option); 305 if (val == null) { 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 a number (milliseconds) 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 * CLI entry point. 327 * @param args job arguments (see {@link #usage(String)}) 328 * @throws Exception on job failure 329 */ 330 public static void main(String[] args) throws Exception { 331 int ret = ToolRunner.run(new BulkLoadCollectorJob(HBaseConfiguration.create()), args); 332 System.exit(ret); 333 } 334 335 @Override 336 public int run(String[] args) throws Exception { 337 if (args.length < 2) { 338 usage("Wrong number of arguments: " + args.length); 339 System.exit(-1); 340 } 341 342 Job job = createSubmittableJob(args); 343 return job.waitForCompletion(true) ? 0 : 1; 344 } 345 346 /** 347 * Print usage/help for the BulkLoadCollectorJob CLI/driver. 348 * <p> 349 * 350 * <pre> 351 * args layout: 352 * args[0] = input directory (required) 353 * args[1] = output directory (required) 354 * args[2] = tables (comma-separated) (optional) 355 * args[3] = tableMappings (comma-separated) (optional; must match tables length) 356 * </pre> 357 */ 358 private void usage(final String errorMsg) { 359 if (errorMsg != null && !errorMsg.isEmpty()) { 360 System.err.println("ERROR: " + errorMsg); 361 } 362 363 System.err.println( 364 "Usage: " + NAME + " <WAL inputdir> <bulk-files-output-dir> [<tables> [<tableMappings>]]"); 365 System.err.println( 366 " <WAL inputdir> directory of WALs to scan (comma-separated list accepted)"); 367 System.err.println( 368 " <bulk-files-output-dir> directory to write discovered store-file paths (output)"); 369 System.err.println( 370 " <tables> optional comma-separated list of tables to include; if omitted, all tables are processed"); 371 System.err.println( 372 " <tableMappings> optional comma-separated list of mapped target tables; must match number of tables"); 373 374 System.err.println(); 375 System.err.println("Time range options (either milliseconds or yyyy-MM-dd'T'HH:mm:ss.SS):"); 376 System.err.println(" -D" + WALInputFormat.START_TIME_KEY + "=[date|ms]"); 377 System.err.println(" -D" + WALInputFormat.END_TIME_KEY + "=[date|ms]"); 378 379 System.err.println(); 380 System.err.println("Configuration alternatives (can be provided via -D):"); 381 System.err 382 .println(" -D" + TABLES_KEY + "=<comma-separated-tables> (alternative to arg[2])"); 383 System.err 384 .println(" -D" + TABLE_MAP_KEY + "=<comma-separated-mappings> (alternative to arg[3])"); 385 System.err.println( 386 " -Dmapreduce.job.reduces=<N> (number of reducers; default 1)"); 387 System.err.println(); 388 389 System.err.println("Performance hints:"); 390 System.err.println(" For large inputs consider disabling speculative execution:"); 391 System.err 392 .println(" -Dmapreduce.map.speculative=false -Dmapreduce.reduce.speculative=false"); 393 394 System.err.println(); 395 System.err.println("Example:"); 396 System.err.println( 397 " " + NAME + " /wals/input /out/bulkfiles ns:tbl1,ns:tbl2 ns:tbl1_mapped,ns:tbl2_mapped"); 398 } 399}