001/**
002 *
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *     http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 */
019package org.apache.hadoop.hbase.mapreduce;
020
021import java.io.IOException;
022import java.util.HashMap;
023import java.util.Map;
024import java.util.Random;
025
026import org.apache.hadoop.conf.Configured;
027import org.apache.hadoop.fs.FileSystem;
028import org.apache.hadoop.fs.Path;
029import org.apache.hadoop.hbase.HBaseConfiguration;
030import org.apache.hadoop.hbase.HConstants;
031import org.apache.hadoop.hbase.TableName;
032import org.apache.yetus.audience.InterfaceAudience;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035import org.apache.hadoop.hbase.client.Admin;
036import org.apache.hadoop.hbase.client.Connection;
037import org.apache.hadoop.hbase.client.ConnectionFactory;
038import org.apache.hadoop.hbase.client.Scan;
039import org.apache.hadoop.hbase.tool.LoadIncrementalHFiles;
040import org.apache.hadoop.hbase.util.Bytes;
041import org.apache.hadoop.mapreduce.Job;
042import org.apache.hadoop.util.Tool;
043import org.apache.hadoop.util.ToolRunner;
044
045/**
046 * Tool used to copy a table to another one which can be on a different setup.
047 * It is also configurable with a start and time as well as a specification
048 * of the region server implementation if different from the local cluster.
049 */
050@InterfaceAudience.Public
051public class CopyTable extends Configured implements Tool {
052  private static final Logger LOG = LoggerFactory.getLogger(CopyTable.class);
053
054  final static String NAME = "copytable";
055  long startTime = 0;
056  long endTime = HConstants.LATEST_TIMESTAMP;
057  int batch = Integer.MAX_VALUE;
058  int cacheRow = -1;
059  int versions = -1;
060  String tableName = null;
061  String startRow = null;
062  String stopRow = null;
063  String dstTableName = null;
064  String peerAddress = null;
065  String families = null;
066  boolean allCells = false;
067  static boolean shuffle = false;
068
069  boolean bulkload = false;
070  Path bulkloadDir = null;
071
072  private final static String JOB_NAME_CONF_KEY = "mapreduce.job.name";
073
074  /**
075   * Sets up the actual job.
076   *
077   * @param args  The command line parameters.
078   * @return The newly created job.
079   * @throws IOException When setting up the job fails.
080   */
081  public Job createSubmittableJob(String[] args)
082  throws IOException {
083    if (!doCommandLine(args)) {
084      return null;
085    }
086
087    Job job = Job.getInstance(getConf(), getConf().get(JOB_NAME_CONF_KEY, NAME + "_" + tableName));
088    job.setJarByClass(CopyTable.class);
089    Scan scan = new Scan();
090
091    scan.setBatch(batch);
092    scan.setCacheBlocks(false);
093
094    if (cacheRow > 0) {
095      scan.setCaching(cacheRow);
096    } else {
097      scan.setCaching(getConf().getInt(HConstants.HBASE_CLIENT_SCANNER_CACHING, 100));
098    }
099
100    scan.setTimeRange(startTime, endTime);
101
102    if (allCells) {
103      scan.setRaw(true);
104    }
105    if (shuffle) {
106      job.getConfiguration().set(TableInputFormat.SHUFFLE_MAPS, "true");
107    }
108    if (versions >= 0) {
109      scan.setMaxVersions(versions);
110    }
111
112    if (startRow != null) {
113      scan.setStartRow(Bytes.toBytesBinary(startRow));
114    }
115
116    if (stopRow != null) {
117      scan.setStopRow(Bytes.toBytesBinary(stopRow));
118    }
119
120    if(families != null) {
121      String[] fams = families.split(",");
122      Map<String,String> cfRenameMap = new HashMap<>();
123      for(String fam : fams) {
124        String sourceCf;
125        if(fam.contains(":")) {
126            // fam looks like "sourceCfName:destCfName"
127            String[] srcAndDest = fam.split(":", 2);
128            sourceCf = srcAndDest[0];
129            String destCf = srcAndDest[1];
130            cfRenameMap.put(sourceCf, destCf);
131        } else {
132            // fam is just "sourceCf"
133            sourceCf = fam;
134        }
135        scan.addFamily(Bytes.toBytes(sourceCf));
136      }
137      Import.configureCfRenaming(job.getConfiguration(), cfRenameMap);
138    }
139    job.setNumReduceTasks(0);
140
141    if (bulkload) {
142      TableMapReduceUtil.initTableMapperJob(tableName, scan, Import.CellImporter.class, null,
143        null, job);
144
145      // We need to split the inputs by destination tables so that output of Map can be bulk-loaded.
146      TableInputFormat.configureSplitTable(job, TableName.valueOf(dstTableName));
147
148      FileSystem fs = FileSystem.get(getConf());
149      Random rand = new Random();
150      Path root = new Path(fs.getWorkingDirectory(), "copytable");
151      fs.mkdirs(root);
152      while (true) {
153        bulkloadDir = new Path(root, "" + rand.nextLong());
154        if (!fs.exists(bulkloadDir)) {
155          break;
156        }
157      }
158
159      System.out.println("HFiles will be stored at " + this.bulkloadDir);
160      HFileOutputFormat2.setOutputPath(job, bulkloadDir);
161      try (Connection conn = ConnectionFactory.createConnection(getConf());
162          Admin admin = conn.getAdmin()) {
163        HFileOutputFormat2.configureIncrementalLoadMap(job,
164            admin.getDescriptor((TableName.valueOf(dstTableName))));
165      }
166    } else {
167      TableMapReduceUtil.initTableMapperJob(tableName, scan,
168        Import.Importer.class, null, null, job);
169
170      TableMapReduceUtil.initTableReducerJob(dstTableName, null, job, null, peerAddress, null,
171        null);
172    }
173
174    return job;
175  }
176
177  /*
178   * @param errorMsg Error message.  Can be null.
179   */
180  private static void printUsage(final String errorMsg) {
181    if (errorMsg != null && errorMsg.length() > 0) {
182      System.err.println("ERROR: " + errorMsg);
183    }
184    System.err.println("Usage: CopyTable [general options] [--starttime=X] [--endtime=Y] " +
185        "[--new.name=NEW] [--peer.adr=ADR] <tablename>");
186    System.err.println();
187    System.err.println("Options:");
188    System.err.println(" rs.class     hbase.regionserver.class of the peer cluster");
189    System.err.println("              specify if different from current cluster");
190    System.err.println(" rs.impl      hbase.regionserver.impl of the peer cluster");
191    System.err.println(" startrow     the start row");
192    System.err.println(" stoprow      the stop row");
193    System.err.println(" starttime    beginning of the time range (unixtime in millis)");
194    System.err.println("              without endtime means from starttime to forever");
195    System.err.println(" endtime      end of the time range.  Ignored if no starttime specified.");
196    System.err.println(" versions     number of cell versions to copy");
197    System.err.println(" new.name     new table's name");
198    System.err.println(" peer.adr     Address of the peer cluster given in the format");
199    System.err.println("              hbase.zookeeper.quorum:hbase.zookeeper.client"
200        + ".port:zookeeper.znode.parent");
201    System.err.println(" families     comma-separated list of families to copy");
202    System.err.println("              To copy from cf1 to cf2, give sourceCfName:destCfName. ");
203    System.err.println("              To keep the same name, just give \"cfName\"");
204    System.err.println(" all.cells    also copy delete markers and deleted cells");
205    System.err.println(" bulkload     Write input into HFiles and bulk load to the destination "
206        + "table");
207    System.err.println();
208    System.err.println("Args:");
209    System.err.println(" tablename    Name of the table to copy");
210    System.err.println();
211    System.err.println("Examples:");
212    System.err.println(" To copy 'TestTable' to a cluster that uses replication for a 1 hour window:");
213    System.err.println(" $ hbase " +
214        "org.apache.hadoop.hbase.mapreduce.CopyTable --starttime=1265875194289 --endtime=1265878794289 " +
215        "--peer.adr=server1,server2,server3:2181:/hbase --families=myOldCf:myNewCf,cf2,cf3 TestTable ");
216    System.err.println("For performance consider the following general option:\n"
217        + "  It is recommended that you set the following to >=100. A higher value uses more memory but\n"
218        + "  decreases the round trip time to the server and may increase performance.\n"
219        + "    -Dhbase.client.scanner.caching=100\n"
220        + "  The following should always be set to false, to prevent writing data twice, which may produce \n"
221        + "  inaccurate results.\n"
222        + "    -Dmapreduce.map.speculative=false");
223  }
224
225  private boolean doCommandLine(final String[] args) {
226    // Process command-line args. TODO: Better cmd-line processing
227    // (but hopefully something not as painful as cli options).
228    if (args.length < 1) {
229      printUsage(null);
230      return false;
231    }
232    try {
233      for (int i = 0; i < args.length; i++) {
234        String cmd = args[i];
235        if (cmd.equals("-h") || cmd.startsWith("--h")) {
236          printUsage(null);
237          return false;
238        }
239
240        final String startRowArgKey = "--startrow=";
241        if (cmd.startsWith(startRowArgKey)) {
242          startRow = cmd.substring(startRowArgKey.length());
243          continue;
244        }
245
246        final String stopRowArgKey = "--stoprow=";
247        if (cmd.startsWith(stopRowArgKey)) {
248          stopRow = cmd.substring(stopRowArgKey.length());
249          continue;
250        }
251
252        final String startTimeArgKey = "--starttime=";
253        if (cmd.startsWith(startTimeArgKey)) {
254          startTime = Long.parseLong(cmd.substring(startTimeArgKey.length()));
255          continue;
256        }
257
258        final String endTimeArgKey = "--endtime=";
259        if (cmd.startsWith(endTimeArgKey)) {
260          endTime = Long.parseLong(cmd.substring(endTimeArgKey.length()));
261          continue;
262        }
263
264        final String batchArgKey = "--batch=";
265        if (cmd.startsWith(batchArgKey)) {
266          batch = Integer.parseInt(cmd.substring(batchArgKey.length()));
267          continue;
268        }
269
270        final String cacheRowArgKey = "--cacheRow=";
271        if (cmd.startsWith(cacheRowArgKey)) {
272          cacheRow = Integer.parseInt(cmd.substring(cacheRowArgKey.length()));
273          continue;
274        }
275
276        final String versionsArgKey = "--versions=";
277        if (cmd.startsWith(versionsArgKey)) {
278          versions = Integer.parseInt(cmd.substring(versionsArgKey.length()));
279          continue;
280        }
281
282        final String newNameArgKey = "--new.name=";
283        if (cmd.startsWith(newNameArgKey)) {
284          dstTableName = cmd.substring(newNameArgKey.length());
285          continue;
286        }
287
288        final String peerAdrArgKey = "--peer.adr=";
289        if (cmd.startsWith(peerAdrArgKey)) {
290          peerAddress = cmd.substring(peerAdrArgKey.length());
291          continue;
292        }
293
294        final String familiesArgKey = "--families=";
295        if (cmd.startsWith(familiesArgKey)) {
296          families = cmd.substring(familiesArgKey.length());
297          continue;
298        }
299
300        if (cmd.startsWith("--all.cells")) {
301          allCells = true;
302          continue;
303        }
304
305        if (cmd.startsWith("--bulkload")) {
306          bulkload = true;
307          continue;
308        }
309
310        if (cmd.startsWith("--shuffle")) {
311          shuffle = true;
312          continue;
313        }
314
315        if (i == args.length-1) {
316          tableName = cmd;
317        } else {
318          printUsage("Invalid argument '" + cmd + "'");
319          return false;
320        }
321      }
322      if (dstTableName == null && peerAddress == null) {
323        printUsage("At least a new table name or a " +
324            "peer address must be specified");
325        return false;
326      }
327      if ((endTime != 0) && (startTime > endTime)) {
328        printUsage("Invalid time range filter: starttime=" + startTime + " >  endtime=" + endTime);
329        return false;
330      }
331
332      if (bulkload && peerAddress != null) {
333        printUsage("Remote bulkload is not supported!");
334        return false;
335      }
336
337      // set dstTableName if necessary
338      if (dstTableName == null) {
339        dstTableName = tableName;
340      }
341    } catch (Exception e) {
342      e.printStackTrace();
343      printUsage("Can't start because " + e.getMessage());
344      return false;
345    }
346    return true;
347  }
348
349  /**
350   * Main entry point.
351   *
352   * @param args  The command line parameters.
353   * @throws Exception When running the job fails.
354   */
355  public static void main(String[] args) throws Exception {
356    int ret = ToolRunner.run(HBaseConfiguration.create(), new CopyTable(), args);
357    System.exit(ret);
358  }
359
360  @Override
361  public int run(String[] args) throws Exception {
362    Job job = createSubmittableJob(args);
363    if (job == null) return 1;
364    if (!job.waitForCompletion(true)) {
365      LOG.info("Map-reduce job failed!");
366      if (bulkload) {
367        LOG.info("Files are not bulkloaded!");
368      }
369      return 1;
370    }
371    int code = 0;
372    if (bulkload) {
373      code = new LoadIncrementalHFiles(this.getConf()).run(new String[]{this.bulkloadDir.toString(),
374          this.dstTableName});
375      if (code == 0) {
376        // bulkloadDir is deleted only LoadIncrementalHFiles was successful so that one can rerun
377        // LoadIncrementalHFiles.
378        FileSystem fs = FileSystem.get(this.getConf());
379        if (!fs.delete(this.bulkloadDir, true)) {
380          LOG.error("Deleting folder " + bulkloadDir + " failed!");
381          code = 1;
382        }
383      }
384    }
385    return code;
386  }
387}