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.impl; 019 020import static org.apache.hadoop.hbase.backup.BackupInfo.withState; 021import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONF_CONTINUOUS_BACKUP_WAL_DIR; 022import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.JOB_NAME_CONF_KEY; 023import static org.apache.hadoop.hbase.backup.mapreduce.MapReduceBackupCopyJob.NUMBER_OF_LEVELS_TO_PRESERVE_KEY; 024import static org.apache.hadoop.hbase.mapreduce.HFileOutputFormat2.MULTI_TABLE_HFILEOUTPUTFORMAT_CONF_KEY; 025 026import java.io.IOException; 027import java.net.URI; 028import java.net.URISyntaxException; 029import java.util.ArrayList; 030import java.util.Arrays; 031import java.util.HashMap; 032import java.util.List; 033import java.util.Map; 034import java.util.Set; 035import java.util.stream.Collectors; 036import org.apache.commons.io.FilenameUtils; 037import org.apache.commons.lang3.StringUtils; 038import org.apache.hadoop.conf.Configuration; 039import org.apache.hadoop.fs.FileSystem; 040import org.apache.hadoop.fs.LocatedFileStatus; 041import org.apache.hadoop.fs.Path; 042import org.apache.hadoop.fs.RemoteIterator; 043import org.apache.hadoop.hbase.TableName; 044import org.apache.hadoop.hbase.backup.BackupCopyJob; 045import org.apache.hadoop.hbase.backup.BackupInfo; 046import org.apache.hadoop.hbase.backup.BackupInfo.BackupPhase; 047import org.apache.hadoop.hbase.backup.BackupRequest; 048import org.apache.hadoop.hbase.backup.BackupRestoreFactory; 049import org.apache.hadoop.hbase.backup.BackupType; 050import org.apache.hadoop.hbase.backup.HBackupFileSystem; 051import org.apache.hadoop.hbase.backup.mapreduce.MapReduceHFileSplitterJob; 052import org.apache.hadoop.hbase.backup.util.BackupUtils; 053import org.apache.hadoop.hbase.client.Admin; 054import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor; 055import org.apache.hadoop.hbase.client.Connection; 056import org.apache.hadoop.hbase.io.hfile.HFile; 057import org.apache.hadoop.hbase.mapreduce.HFileOutputFormat2; 058import org.apache.hadoop.hbase.mapreduce.WALInputFormat; 059import org.apache.hadoop.hbase.mapreduce.WALPlayer; 060import org.apache.hadoop.hbase.snapshot.SnapshotDescriptionUtils; 061import org.apache.hadoop.hbase.snapshot.SnapshotManifest; 062import org.apache.hadoop.hbase.snapshot.SnapshotRegionLocator; 063import org.apache.hadoop.hbase.snapshot.SnapshotTTLExpiredException; 064import org.apache.hadoop.hbase.util.CommonFSUtils; 065import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; 066import org.apache.hadoop.hbase.util.HFileArchiveUtil; 067import org.apache.hadoop.hbase.wal.AbstractFSWALProvider; 068import org.apache.hadoop.util.Tool; 069import org.apache.yetus.audience.InterfaceAudience; 070import org.slf4j.Logger; 071import org.slf4j.LoggerFactory; 072 073import org.apache.hbase.thirdparty.com.google.common.base.Strings; 074import org.apache.hbase.thirdparty.com.google.common.collect.Lists; 075 076import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil; 077import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos; 078 079/** 080 * Incremental backup implementation. See the {@link #execute() execute} method. 081 */ 082@InterfaceAudience.Private 083public class IncrementalTableBackupClient extends TableBackupClient { 084 private static final Logger LOG = LoggerFactory.getLogger(IncrementalTableBackupClient.class); 085 private static final String BULKLOAD_COLLECTOR_OUTPUT = "bulkload-collector-output"; 086 087 protected IncrementalTableBackupClient() { 088 } 089 090 public IncrementalTableBackupClient(final Connection conn, final String backupId, 091 BackupRequest request) throws IOException { 092 super(conn, backupId, request); 093 } 094 095 protected List<String> filterMissingFiles(List<String> incrBackupFileList) throws IOException { 096 List<String> list = new ArrayList<>(); 097 for (String file : incrBackupFileList) { 098 Path p = new Path(file); 099 if (fs.exists(p) || isActiveWalPath(p)) { 100 list.add(file); 101 } else { 102 LOG.warn("Can't find file: " + file); 103 } 104 } 105 return list; 106 } 107 108 /** 109 * Check if a given path belongs to active WAL directory 110 * @param p path 111 * @return true, if yes 112 */ 113 protected boolean isActiveWalPath(Path p) { 114 return !AbstractFSWALProvider.isArchivedLogFile(p); 115 } 116 117 protected static int getIndex(TableName tbl, List<TableName> sTableList) { 118 if (sTableList == null) { 119 return 0; 120 } 121 122 for (int i = 0; i < sTableList.size(); i++) { 123 if (tbl.equals(sTableList.get(i))) { 124 return i; 125 } 126 } 127 return -1; 128 } 129 130 /** 131 * Reads bulk load records from backup table, iterates through the records and forms the paths for 132 * bulk loaded hfiles. Copies the bulk loaded hfiles to the backup destination. This method does 133 * NOT clean up the entries in the bulk load system table. Those entries should not be cleaned 134 * until the backup is marked as complete. 135 * @param tablesToBackup list of tables to be backed up 136 */ 137 protected List<BulkLoad> handleBulkLoad(List<TableName> tablesToBackup, 138 Map<TableName, List<String>> tablesToWALFileList, Map<TableName, Long> tablesToPrevBackupTs) 139 throws IOException { 140 Map<TableName, MergeSplitBulkloadInfo> toBulkload = new HashMap<>(); 141 List<BulkLoad> bulkLoads = new ArrayList<>(); 142 143 FileSystem tgtFs; 144 try { 145 tgtFs = FileSystem.get(new URI(backupInfo.getBackupRootDir()), conf); 146 } catch (URISyntaxException use) { 147 throw new IOException("Unable to get FileSystem", use); 148 } 149 150 Path rootdir = CommonFSUtils.getRootDir(conf); 151 Path tgtRoot = new Path(new Path(backupInfo.getBackupRootDir()), backupId); 152 153 if (!backupInfo.isContinuousBackupEnabled()) { 154 bulkLoads = backupManager.readBulkloadRows(tablesToBackup); 155 for (BulkLoad bulkLoad : bulkLoads) { 156 TableName srcTable = bulkLoad.getTableName(); 157 if (!tablesToBackup.contains(srcTable)) { 158 LOG.debug("Skipping {} since it is not in tablesToBackup", srcTable); 159 continue; 160 } 161 162 MergeSplitBulkloadInfo bulkloadInfo = 163 toBulkload.computeIfAbsent(srcTable, MergeSplitBulkloadInfo::new); 164 String regionName = bulkLoad.getRegion(); 165 String fam = bulkLoad.getColumnFamily(); 166 String filename = FilenameUtils.getName(bulkLoad.getHfilePath()); 167 Path tblDir = CommonFSUtils.getTableDir(rootdir, srcTable); 168 Path p = new Path(tblDir, regionName + Path.SEPARATOR + fam + Path.SEPARATOR + filename); 169 String srcTableQualifier = srcTable.getQualifierAsString(); 170 String srcTableNs = srcTable.getNamespaceAsString(); 171 Path tgtFam = new Path(tgtRoot, srcTableNs + Path.SEPARATOR + srcTableQualifier 172 + Path.SEPARATOR + regionName + Path.SEPARATOR + fam); 173 if (!tgtFs.mkdirs(tgtFam)) { 174 throw new IOException("couldn't create " + tgtFam); 175 } 176 177 Path tgt = new Path(tgtFam, filename); 178 Path archiveDir = HFileArchiveUtil.getStoreArchivePath(conf, srcTable, regionName, fam); 179 Path archive = new Path(archiveDir, filename); 180 181 if (fs.exists(p)) { 182 if (LOG.isTraceEnabled()) { 183 LOG.trace("found bulk hfile {} in {} for {}", bulkLoad.getHfilePath(), p.getParent(), 184 srcTableQualifier); 185 LOG.trace("copying {} to {}", p, tgt); 186 } 187 bulkloadInfo.addActiveFile(p.toString()); 188 } else if (fs.exists(archive)) { 189 LOG.debug("copying archive {} to {}", archive, tgt); 190 bulkloadInfo.addArchiveFiles(archive.toString()); 191 } 192 } 193 194 for (MergeSplitBulkloadInfo bulkloadInfo : toBulkload.values()) { 195 mergeSplitAndCopyBulkloadedHFiles(bulkloadInfo.getActiveFiles(), 196 bulkloadInfo.getArchiveFiles(), bulkloadInfo.getSrcTable(), tgtFs); 197 } 198 } else { 199 // Continuous incremental backup: run BulkLoadCollectorJob over backed-up WALs 200 Path collectorOutput = new Path(getBulkOutputDir(), BULKLOAD_COLLECTOR_OUTPUT); 201 for (TableName table : tablesToBackup) { 202 long startTs = tablesToPrevBackupTs.getOrDefault(table, 0L); 203 long endTs = backupInfo.getIncrCommittedWalTs(); 204 List<String> walDirs = tablesToWALFileList.getOrDefault(table, new ArrayList<String>()); 205 206 List<Path> bulkloadPaths = BackupUtils.collectBulkFiles(conn, table, table, startTs, endTs, 207 collectorOutput, walDirs); 208 209 List<String> bulkLoadFiles = 210 bulkloadPaths.stream().map(Path::toString).collect(Collectors.toList()); 211 212 if (bulkLoadFiles.isEmpty()) { 213 LOG.info("No bulk-load files found for table {}", table); 214 continue; 215 } 216 217 mergeSplitAndCopyBulkloadedHFiles(bulkLoadFiles, table, tgtFs); 218 } 219 } 220 return bulkLoads; 221 } 222 223 private void mergeSplitAndCopyBulkloadedHFiles(List<String> activeFiles, 224 List<String> archiveFiles, TableName tn, FileSystem tgtFs) throws IOException { 225 int attempt = 1; 226 227 while (!activeFiles.isEmpty()) { 228 LOG.info("MergeSplit {} active bulk loaded files. Attempt={}", activeFiles.size(), attempt++); 229 // Active file can be archived during copy operation, 230 // we need to handle this properly 231 try { 232 mergeSplitAndCopyBulkloadedHFiles(activeFiles, tn, tgtFs); 233 break; 234 } catch (IOException e) { 235 int numActiveFiles = activeFiles.size(); 236 updateFileLists(activeFiles, archiveFiles); 237 if (activeFiles.size() < numActiveFiles) { 238 // We've archived some files, delete bulkloads directory 239 // and re-try 240 deleteBulkLoadDirectory(); 241 continue; 242 } 243 244 throw e; 245 } 246 } 247 248 if (!archiveFiles.isEmpty()) { 249 mergeSplitAndCopyBulkloadedHFiles(archiveFiles, tn, tgtFs); 250 } 251 } 252 253 private void mergeSplitAndCopyBulkloadedHFiles(List<String> files, TableName tn, FileSystem tgtFs) 254 throws IOException { 255 MapReduceHFileSplitterJob player = new MapReduceHFileSplitterJob(); 256 Configuration conf = new Configuration(this.conf); 257 conf.set(MapReduceHFileSplitterJob.BULK_OUTPUT_CONF_KEY, 258 getBulkOutputDirForTable(tn).toString()); 259 if (backupInfo.isContinuousBackupEnabled()) { 260 conf.setBoolean(MULTI_TABLE_HFILEOUTPUTFORMAT_CONF_KEY, false); 261 } 262 player.setConf(conf); 263 264 String inputDirs = StringUtils.join(files, ","); 265 String[] args = { inputDirs, tn.getNameWithNamespaceInclAsString() }; 266 267 int result; 268 269 try { 270 result = player.run(args); 271 } catch (Exception e) { 272 LOG.error("Failed to run MapReduceHFileSplitterJob", e); 273 // Delete the bulkload directory if we fail to run the HFile splitter job for any reason 274 // as it might be re-tried 275 deleteBulkLoadDirectory(); 276 throw new IOException(e); 277 } 278 279 if (result != 0) { 280 throw new IOException( 281 "Failed to run MapReduceHFileSplitterJob with invalid result: " + result); 282 } 283 284 incrementalCopyBulkloadHFiles(tgtFs, tn); 285 } 286 287 public void updateFileLists(List<String> activeFiles, List<String> archiveFiles) 288 throws IOException { 289 List<String> newlyArchived = new ArrayList<>(); 290 291 for (String spath : activeFiles) { 292 if (!fs.exists(new Path(spath))) { 293 newlyArchived.add(spath); 294 } 295 } 296 297 if (!newlyArchived.isEmpty()) { 298 String rootDir = CommonFSUtils.getRootDir(conf).toString(); 299 300 activeFiles.removeAll(newlyArchived); 301 for (String file : newlyArchived) { 302 String archivedFile = file.substring(rootDir.length() + 1); 303 Path archivedFilePath = new Path(HFileArchiveUtil.getArchivePath(conf), archivedFile); 304 archivedFile = archivedFilePath.toString(); 305 306 if (!fs.exists(archivedFilePath)) { 307 throw new IOException(String.format( 308 "File %s no longer exists, and no archived file %s exists for it", file, archivedFile)); 309 } 310 311 LOG.debug("Archived file {} has been updated", archivedFile); 312 archiveFiles.add(archivedFile); 313 } 314 } 315 316 LOG.debug(newlyArchived.size() + " files have been archived."); 317 } 318 319 /** 320 * @throws IOException If the execution of the backup fails 321 * @throws ColumnFamilyMismatchException If the column families of the current table do not match 322 * the column families for the last full backup. In which 323 * case, a full backup should be taken 324 */ 325 @Override 326 public void execute() throws IOException, ColumnFamilyMismatchException { 327 // tablesToWALFileList and tablesToPrevBackupTs are needed for "continuous" Incremental backup 328 Map<TableName, List<String>> tablesToWALFileList = new HashMap<>(); 329 Map<TableName, Long> tablesToPrevBackupTs = new HashMap<>(); 330 try { 331 Map<TableName, String> tablesToFullBackupIds = getFullBackupIds(); 332 verifyCfCompatibility(backupInfo.getTables(), tablesToFullBackupIds); 333 334 // case PREPARE_INCREMENTAL: 335 if (backupInfo.isContinuousBackupEnabled()) { 336 // committedWALsTs is needed only for Incremental backups with continuous backup 337 // since these do not depend on log roll ts 338 long committedWALsTs = BackupUtils.getReplicationCheckpoint(conn); 339 backupInfo.setIncrCommittedWalTs(committedWALsTs); 340 } 341 beginBackup(backupManager, backupInfo); 342 backupInfo.setPhase(BackupPhase.PREPARE_INCREMENTAL); 343 // Non-continuous Backup incremental backup is controlled by 'incremental backup table set' 344 // and not by user provided backup table list. This is an optimization to avoid copying 345 // the same set of WALs for incremental backups of different tables at different times 346 // HBASE-14038 347 // Continuous-incremental backup backs up user provided table list/set 348 Set<TableName> currentTableSet; 349 if (backupInfo.isContinuousBackupEnabled()) { 350 currentTableSet = backupInfo.getTables(); 351 } else { 352 currentTableSet = backupManager.getIncrementalBackupTableSet(); 353 newTimestamps = ((IncrementalBackupManager) backupManager).getIncrBackupLogFileMap(); 354 } 355 LOG.debug("For incremental backup, the current table set is {}", currentTableSet); 356 } catch (Exception e) { 357 // fail the overall backup and return 358 failBackup(conn, backupInfo, backupManager, e, "Unexpected Exception : ", 359 BackupType.INCREMENTAL, conf); 360 throw new IOException(e); 361 } 362 363 // case INCREMENTAL_COPY: 364 try { 365 // copy out the table and region info files for each table 366 BackupUtils.copyTableRegionInfo(conn, backupInfo, conf); 367 setupRegionLocator(); 368 // convert WAL to HFiles and copy them to .tmp under BACKUP_ROOT 369 convertWALsToHFiles(tablesToWALFileList, tablesToPrevBackupTs); 370 371 String[] bulkOutputFiles; 372 String backupDest = backupInfo.getBackupRootDir(); 373 if (backupInfo.isContinuousBackupEnabled()) { 374 // For the continuous backup case, the WALs have been converted to HFiles in a separate 375 // map-reduce job for each table. In order to prevent MR job failures due to HBASE-29891, 376 // these HFiles were sent to a different output directory for each table. This means 377 // continuous backups require a list of source directories and a different destination 378 // directory when copying HFiles to the incremental backup directory. 379 List<String> uniqueNamespaces = tablesToWALFileList.keySet().stream() 380 .map(TableName::getNamespaceAsString).distinct().toList(); 381 bulkOutputFiles = uniqueNamespaces.stream() 382 .map(ns -> new Path(getBulkOutputDir(), ns).toString()).toArray(String[]::new); 383 backupDest = backupDest + Path.SEPARATOR + backupId; 384 } else { 385 bulkOutputFiles = new String[] { getBulkOutputDir().toString() }; 386 } 387 incrementalCopyHFiles(bulkOutputFiles, backupDest); 388 } catch (Exception e) { 389 String msg = "Unexpected exception in incremental-backup: incremental copy " + backupId + " "; 390 // fail the overall backup and return 391 failBackup(conn, backupInfo, backupManager, e, msg, BackupType.INCREMENTAL, conf); 392 throw new IOException(e); 393 } 394 // case INCR_BACKUP_COMPLETE: 395 // set overall backup status: complete. Here we make sure to complete the backup. 396 // After this checkpoint, even if entering cancel process, will let the backup finished 397 try { 398 if (!backupInfo.isContinuousBackupEnabled()) { 399 // Set the previousTimestampMap which is before this current log roll to the manifest. 400 Map<TableName, Map<String, Long>> previousTimestampMap = 401 backupManager.readLogTimestampMap(); 402 backupInfo.setIncrTimestampMap(previousTimestampMap); 403 404 // The table list in backupInfo is good for both full backup and incremental backup. 405 // For incremental backup, it contains the incremental backup table set. 406 backupManager.writeRegionServerLogTimestamp(backupInfo.getTables(), newTimestamps); 407 } 408 409 Map<TableName, Map<String, Long>> newTableSetTimestampMap = 410 backupManager.readLogTimestampMap(); 411 412 List<BulkLoad> bulkLoads = 413 handleBulkLoad(backupInfo.getTableNames(), tablesToWALFileList, tablesToPrevBackupTs); 414 415 // backup complete 416 backupInfo.setTableSetTimestampMap(newTableSetTimestampMap); 417 completeBackup(conn, backupInfo, BackupType.INCREMENTAL, conf); 418 419 List<byte[]> bulkLoadedRows = Lists.transform(bulkLoads, BulkLoad::getRowKey); 420 backupManager.deleteBulkLoadedRows(bulkLoadedRows); 421 } catch (IOException e) { 422 failBackup(conn, backupInfo, backupManager, e, "Unexpected Exception : ", 423 BackupType.INCREMENTAL, conf); 424 throw new IOException(e); 425 } finally { 426 if (backupInfo.isContinuousBackupEnabled()) { 427 deleteTmpBackupDirectory(); 428 } 429 } 430 } 431 432 protected void incrementalCopyHFiles(String[] files, String backupDest) throws IOException { 433 boolean diskBasedSortingOriginalValue = HFileOutputFormat2.diskBasedSortingEnabled(conf); 434 try { 435 LOG.debug("Incremental copy HFiles is starting. dest={}", backupDest); 436 // set overall backup phase: incremental_copy 437 backupInfo.setPhase(BackupPhase.INCREMENTAL_COPY); 438 // get incremental backup file list and prepare parms for DistCp 439 String[] strArr = new String[files.length + 1]; 440 System.arraycopy(files, 0, strArr, 0, files.length); 441 strArr[strArr.length - 1] = backupDest; 442 443 String jobname = "Incremental_Backup-HFileCopy-" + backupInfo.getBackupId() + "-" 444 + System.currentTimeMillis(); 445 if (LOG.isDebugEnabled()) { 446 LOG.debug("Setting incremental copy HFiles job name to : " + jobname); 447 } 448 conf.set(JOB_NAME_CONF_KEY, jobname); 449 conf.setBoolean(HFileOutputFormat2.DISK_BASED_SORTING_ENABLED_KEY, true); 450 451 BackupCopyJob copyService = BackupRestoreFactory.getBackupCopyJob(conf); 452 int res = copyService.copy(backupInfo, backupManager, conf, BackupType.INCREMENTAL, strArr); 453 if (res != 0) { 454 LOG.error("Copy incremental HFile files failed with return code: " + res + "."); 455 throw new IOException( 456 "Failed copy from " + StringUtils.join(files, ',') + " to " + backupDest); 457 } 458 LOG.debug("Incremental copy HFiles from " + StringUtils.join(files, ',') + " to " + backupDest 459 + " finished."); 460 } finally { 461 deleteBulkLoadDirectory(); 462 conf.setBoolean(HFileOutputFormat2.DISK_BASED_SORTING_ENABLED_KEY, 463 diskBasedSortingOriginalValue); 464 } 465 } 466 467 protected void deleteBulkLoadDirectory() throws IOException { 468 // delete original bulk load directory on method exit 469 Path path = getBulkOutputDir(); 470 deleteDirectory(path); 471 } 472 473 protected void deleteTmpBackupDirectory() throws IOException { 474 Path path = getTmpBackupDir(); 475 deleteDirectory(path); 476 } 477 478 private void deleteDirectory(Path path) throws IOException { 479 FileSystem fs = FileSystem.get(path.toUri(), conf); 480 boolean result = fs.delete(path, true); 481 if (!result) { 482 LOG.warn("Could not delete {}", path); 483 } 484 } 485 486 protected void convertWALsToHFiles(Map<TableName, List<String>> tablesToWALFileList, 487 Map<TableName, Long> tablesToPrevBackupTs) throws IOException { 488 long previousBackupTs = 0L; 489 long currentBackupTs = 0L; 490 if (backupInfo.isContinuousBackupEnabled()) { 491 String walBackupDir = conf.get(CONF_CONTINUOUS_BACKUP_WAL_DIR); 492 if (Strings.isNullOrEmpty(walBackupDir)) { 493 throw new IOException( 494 "Incremental backup requires the WAL backup directory " + CONF_CONTINUOUS_BACKUP_WAL_DIR); 495 } 496 Path walBackupPath = new Path(walBackupDir); 497 Set<TableName> tableSet = backupInfo.getTables(); 498 currentBackupTs = backupInfo.getIncrCommittedWalTs(); 499 List<BackupInfo> backupInfos = 500 backupManager.getBackupHistory(withState(BackupInfo.BackupState.COMPLETE)); 501 for (TableName table : tableSet) { 502 for (BackupInfo backup : backupInfos) { 503 // find previous backup for this table 504 if (backup.getTables().contains(table)) { 505 LOG.info("Found previous backup of type {} with id {} for table {}", backup.getType(), 506 backup.getBackupId(), table.getNameAsString()); 507 List<String> walBackupFileList; 508 if (backup.getType() == BackupType.FULL) { 509 previousBackupTs = backup.getStartTs(); 510 } else { 511 previousBackupTs = backup.getIncrCommittedWalTs(); 512 } 513 walBackupFileList = 514 BackupUtils.getValidWalDirs(conf, walBackupPath, previousBackupTs, currentBackupTs); 515 tablesToWALFileList.put(table, walBackupFileList); 516 tablesToPrevBackupTs.put(table, previousBackupTs); 517 walToHFiles(walBackupFileList, Arrays.asList(table.getNameAsString()), 518 previousBackupTs); 519 break; 520 } 521 } 522 } 523 } else { 524 // get incremental backup file list and prepare parameters for DistCp 525 List<String> incrBackupFileList = backupInfo.getIncrBackupFileList(); 526 // Get list of tables in incremental backup set 527 Set<TableName> tableSet = backupManager.getIncrementalBackupTableSet(); 528 // filter missing files out (they have been copied by previous backups) 529 incrBackupFileList = filterMissingFiles(incrBackupFileList); 530 List<String> tableList = new ArrayList<String>(); 531 for (TableName table : tableSet) { 532 // Check if table exists 533 if (tableExists(table, conn)) { 534 tableList.add(table.getNameAsString()); 535 } else { 536 LOG.warn("Table " + table + " does not exists. Skipping in WAL converter"); 537 } 538 } 539 walToHFiles(incrBackupFileList, tableList, previousBackupTs); 540 } 541 } 542 543 protected boolean tableExists(TableName table, Connection conn) throws IOException { 544 try (Admin admin = conn.getAdmin()) { 545 return admin.tableExists(table); 546 } 547 } 548 549 protected void walToHFiles(List<String> dirPaths, List<String> tableList, long previousBackupTs) 550 throws IOException { 551 Tool player = new WALPlayer(); 552 Configuration conf = new Configuration(this.conf); 553 554 // Player reads all files in arbitrary directory structure and creates 555 // a Map task for each file. We use ';' as separator 556 // because WAL file names contains ',' 557 String dirs = StringUtils.join(dirPaths, ';'); 558 String jobname = "Incremental_Backup-" + backupId + "-" + System.currentTimeMillis(); 559 560 setBulkOutputPath(conf, tableList); 561 conf.set(WALPlayer.INPUT_FILES_SEPARATOR_KEY, ";"); 562 conf.setBoolean(WALPlayer.MULTI_TABLES_SUPPORT, true); 563 conf.set(JOB_NAME_CONF_KEY, jobname); 564 conf.setBoolean(HFileOutputFormat2.DISK_BASED_SORTING_ENABLED_KEY, true); 565 if (backupInfo.isContinuousBackupEnabled()) { 566 conf.set(WALInputFormat.START_TIME_KEY, Long.toString(previousBackupTs)); 567 conf.set(WALInputFormat.END_TIME_KEY, Long.toString(backupInfo.getIncrCommittedWalTs())); 568 // We do not want a multi-table HFile format here because continuous backups run the WALPlayer 569 // individually on each table in the backup set. 570 conf.setBoolean(MULTI_TABLE_HFILEOUTPUTFORMAT_CONF_KEY, false); 571 } 572 String[] playerArgs = { dirs, StringUtils.join(tableList, ",") }; 573 574 try { 575 player.setConf(conf); 576 int result = player.run(playerArgs); 577 if (result != 0) { 578 throw new IOException("WAL Player failed"); 579 } 580 } catch (IOException e) { 581 throw e; 582 } catch (Exception ee) { 583 throw new IOException("Can not convert from directory " + dirs 584 + " (check Hadoop, HBase and WALPlayer M/R job logs) ", ee); 585 } 586 } 587 588 private void setBulkOutputPath(Configuration conf, List<String> tableList) { 589 Path bulkOutputPath = getBulkOutputDir(); 590 if (backupInfo.isContinuousBackupEnabled()) { 591 if (tableList.size() != 1) { 592 // Continuous backups run the WALPlayer job on one table at a time, so the list of tables 593 // should have only one element. 594 throw new RuntimeException( 595 "Expected table list to have only one element, but got: " + tableList); 596 } 597 bulkOutputPath = getTmpBackupDirForTable(TableName.valueOf(tableList.get(0))); 598 } 599 conf.set(WALPlayer.BULK_OUTPUT_CONF_KEY, bulkOutputPath.toString()); 600 } 601 602 private void incrementalCopyBulkloadHFiles(FileSystem tgtFs, TableName tn) throws IOException { 603 Path bulkOutDir = getBulkOutputDirForTable(tn); 604 605 if (tgtFs.exists(bulkOutDir)) { 606 conf.setInt(NUMBER_OF_LEVELS_TO_PRESERVE_KEY, 2); 607 LOG.debug( 608 "{} has been set to {}. This affects what source files are actually copied in the " 609 + "next Incremental copy HFiles job", 610 NUMBER_OF_LEVELS_TO_PRESERVE_KEY, conf.get(NUMBER_OF_LEVELS_TO_PRESERVE_KEY)); 611 Path tgtPath = getTargetDirForTable(tn); 612 try { 613 RemoteIterator<LocatedFileStatus> locatedFiles = tgtFs.listFiles(bulkOutDir, true); 614 List<String> files = new ArrayList<>(); 615 while (locatedFiles.hasNext()) { 616 LocatedFileStatus file = locatedFiles.next(); 617 if (file.isFile() && HFile.isHFileFormat(tgtFs, file.getPath())) { 618 files.add(file.getPath().toString()); 619 } 620 } 621 incrementalCopyHFiles(files.toArray(files.toArray(new String[0])), tgtPath.toString()); 622 } finally { 623 conf.unset(NUMBER_OF_LEVELS_TO_PRESERVE_KEY); 624 LOG.debug("{} has been unset", NUMBER_OF_LEVELS_TO_PRESERVE_KEY); 625 } 626 } 627 } 628 629 /** 630 * Creates a path to the bulk load output directory for a table. This directory will look like: 631 * .../backupRoot/.tmp/backupId/namespace/table/data 632 * @param table The table whose HFiles are being bulk loaded 633 * @return A Path object representing the directory 634 */ 635 protected Path getBulkOutputDirForTable(TableName table) { 636 Path tablePath = getTmpBackupDirForTable(table); 637 return new Path(tablePath, "data"); 638 } 639 640 /** 641 * Creates a path to a table's directory within the temporary directory. This directory will look 642 * like: .../backupRoot/.tmp/backupId/namespace/table 643 * @param table The table whose HFiles are being bulk loaded 644 * @return A Path object representing the directory 645 */ 646 protected Path getTmpBackupDirForTable(TableName table) { 647 Path tablePath = getBulkOutputDir(); 648 tablePath = new Path(tablePath, table.getNamespaceAsString()); 649 return new Path(tablePath, table.getQualifierAsString()); 650 } 651 652 /** 653 * Creates a path to a temporary backup directory. This directory will look like: 654 * .../backupRoot/.tmp/backupId 655 * @return A Path object representing the directory 656 */ 657 protected Path getBulkOutputDir() { 658 return new Path(getTmpBackupDir(), backupId); 659 } 660 661 /** 662 * Creates a path to the backup root directory's temporary subdirectory. The directory will look 663 * like: .../backupRoot/.tmp 664 * @return A Path object representing the directory 665 */ 666 protected Path getTmpBackupDir() { 667 return new Path(backupInfo.getBackupRootDir(), ".tmp"); 668 } 669 670 /** 671 * Creates a path to a destination directory for bulk loaded HFiles. This directory will look 672 * like: .../backupRoot/backupID/namespace/table 673 * @param table The table whose HFiles are being bulk loaded 674 * @return A Path object representing the directory 675 */ 676 private Path getTargetDirForTable(TableName table) { 677 Path path = new Path(backupInfo.getBackupRootDir() + Path.SEPARATOR + backupInfo.getBackupId()); 678 path = new Path(path, table.getNamespaceAsString()); 679 path = new Path(path, table.getQualifierAsString()); 680 return path; 681 } 682 683 private void setupRegionLocator() throws IOException { 684 Map<TableName, String> fullBackupIds = getFullBackupIds(); 685 try (BackupAdminImpl backupAdmin = new BackupAdminImpl(conn)) { 686 687 for (TableName tableName : backupInfo.getTables()) { 688 String fullBackupId = fullBackupIds.get(tableName); 689 BackupInfo fullBackupInfo = backupAdmin.getBackupInfo(fullBackupId); 690 String snapshotName = fullBackupInfo.getSnapshotName(tableName); 691 Path root = HBackupFileSystem.getTableBackupPath(tableName, 692 new Path(fullBackupInfo.getBackupRootDir()), fullBackupId); 693 String manifestDir = 694 SnapshotDescriptionUtils.getCompletedSnapshotDir(snapshotName, root).toString(); 695 SnapshotRegionLocator.setSnapshotManifestDir(conf, manifestDir, tableName); 696 } 697 } 698 } 699 700 private Map<TableName, String> getFullBackupIds() throws IOException { 701 // Ancestors are stored from newest to oldest, so we can iterate backwards 702 // in order to populate our backupId map with the most recent full backup 703 // for a given table 704 List<BackupManifest.BackupImage> images = getAncestors(backupInfo); 705 Map<TableName, String> results = new HashMap<>(); 706 for (int i = images.size() - 1; i >= 0; i--) { 707 BackupManifest.BackupImage image = images.get(i); 708 if (image.getType() != BackupType.FULL) { 709 continue; 710 } 711 712 for (TableName tn : image.getTableNames()) { 713 results.put(tn, image.getBackupId()); 714 } 715 } 716 return results; 717 } 718 719 /** 720 * Verifies that the current table descriptor CFs matches the descriptor CFs of the last full 721 * backup for the tables. This ensures CF compatibility across incremental backups. If a mismatch 722 * is detected, a full table backup should be taken, rather than an incremental one 723 */ 724 private void verifyCfCompatibility(Set<TableName> tables, 725 Map<TableName, String> tablesToFullBackupId) throws IOException, ColumnFamilyMismatchException { 726 ColumnFamilyMismatchException.ColumnFamilyMismatchExceptionBuilder exBuilder = 727 ColumnFamilyMismatchException.newBuilder(); 728 try (Admin admin = conn.getAdmin(); BackupAdminImpl backupAdmin = new BackupAdminImpl(conn)) { 729 for (TableName tn : tables) { 730 String backupId = tablesToFullBackupId.get(tn); 731 BackupInfo fullBackupInfo = backupAdmin.getBackupInfo(backupId); 732 733 ColumnFamilyDescriptor[] currentCfs = admin.getDescriptor(tn).getColumnFamilies(); 734 String snapshotName = fullBackupInfo.getSnapshotName(tn); 735 Path root = HBackupFileSystem.getTableBackupPath(tn, 736 new Path(fullBackupInfo.getBackupRootDir()), fullBackupInfo.getBackupId()); 737 Path manifestDir = SnapshotDescriptionUtils.getCompletedSnapshotDir(snapshotName, root); 738 739 FileSystem fs; 740 try { 741 fs = FileSystem.get(new URI(fullBackupInfo.getBackupRootDir()), conf); 742 } catch (URISyntaxException e) { 743 throw new IOException("Unable to get fs for backup " + fullBackupInfo.getBackupId(), e); 744 } 745 746 SnapshotProtos.SnapshotDescription snapshotDescription = 747 SnapshotDescriptionUtils.readSnapshotInfo(fs, manifestDir); 748 SnapshotManifest manifest = 749 SnapshotManifest.open(conf, fs, manifestDir, snapshotDescription); 750 if ( 751 SnapshotDescriptionUtils.isExpiredSnapshot(snapshotDescription.getTtl(), 752 snapshotDescription.getCreationTime(), EnvironmentEdgeManager.currentTime()) 753 ) { 754 throw new SnapshotTTLExpiredException( 755 ProtobufUtil.createSnapshotDesc(snapshotDescription)); 756 } 757 758 ColumnFamilyDescriptor[] backupCfs = manifest.getTableDescriptor().getColumnFamilies(); 759 if (!areCfsCompatible(currentCfs, backupCfs)) { 760 exBuilder.addMismatchedTable(tn, currentCfs, backupCfs); 761 } 762 } 763 } 764 765 ColumnFamilyMismatchException ex = exBuilder.build(); 766 if (!ex.getMismatchedTables().isEmpty()) { 767 throw ex; 768 } 769 } 770 771 private static boolean areCfsCompatible(ColumnFamilyDescriptor[] currentCfs, 772 ColumnFamilyDescriptor[] backupCfs) { 773 if (currentCfs.length != backupCfs.length) { 774 return false; 775 } 776 777 for (int i = 0; i < backupCfs.length; i++) { 778 String currentCf = currentCfs[i].getNameAsString(); 779 String backupCf = backupCfs[i].getNameAsString(); 780 781 if (!currentCf.equals(backupCf)) { 782 return false; 783 } 784 } 785 786 return true; 787 } 788}