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.util; 019 020import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONF_CONTINUOUS_BACKUP_WAL_DIR; 021import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONTINUOUS_BACKUP_REPLICATION_PEER; 022import static org.apache.hadoop.hbase.backup.replication.ContinuousBackupReplicationEndpoint.ONE_DAY_IN_MILLISECONDS; 023import static org.apache.hadoop.hbase.backup.util.BackupFileSystemManager.WALS_DIR; 024import static org.apache.hadoop.hbase.replication.regionserver.ReplicationMarkerChore.REPLICATION_MARKER_ENABLED_DEFAULT; 025import static org.apache.hadoop.hbase.replication.regionserver.ReplicationMarkerChore.REPLICATION_MARKER_ENABLED_KEY; 026 027import java.io.FileNotFoundException; 028import java.io.IOException; 029import java.net.URLDecoder; 030import java.text.ParseException; 031import java.text.SimpleDateFormat; 032import java.time.ZoneOffset; 033import java.util.ArrayList; 034import java.util.Collection; 035import java.util.Collections; 036import java.util.Comparator; 037import java.util.Date; 038import java.util.HashMap; 039import java.util.List; 040import java.util.Map; 041import java.util.Map.Entry; 042import java.util.Set; 043import java.util.TimeZone; 044import java.util.TreeSet; 045import java.util.function.Predicate; 046import java.util.stream.Collectors; 047import java.util.stream.Stream; 048import org.apache.hadoop.conf.Configuration; 049import org.apache.hadoop.fs.FSDataOutputStream; 050import org.apache.hadoop.fs.FileStatus; 051import org.apache.hadoop.fs.FileSystem; 052import org.apache.hadoop.fs.LocatedFileStatus; 053import org.apache.hadoop.fs.Path; 054import org.apache.hadoop.fs.PathFilter; 055import org.apache.hadoop.fs.RemoteIterator; 056import org.apache.hadoop.fs.permission.FsPermission; 057import org.apache.hadoop.hbase.HBaseConfiguration; 058import org.apache.hadoop.hbase.HConstants; 059import org.apache.hadoop.hbase.MetaTableAccessor; 060import org.apache.hadoop.hbase.ServerName; 061import org.apache.hadoop.hbase.TableName; 062import org.apache.hadoop.hbase.backup.BackupInfo; 063import org.apache.hadoop.hbase.backup.BackupRestoreConstants; 064import org.apache.hadoop.hbase.backup.HBackupFileSystem; 065import org.apache.hadoop.hbase.backup.RestoreRequest; 066import org.apache.hadoop.hbase.backup.impl.BackupManifest; 067import org.apache.hadoop.hbase.backup.impl.BackupManifest.BackupImage; 068import org.apache.hadoop.hbase.backup.impl.BackupSystemTable; 069import org.apache.hadoop.hbase.backup.master.LogRollMasterProcedureManager; 070import org.apache.hadoop.hbase.client.Admin; 071import org.apache.hadoop.hbase.client.Connection; 072import org.apache.hadoop.hbase.client.RegionInfo; 073import org.apache.hadoop.hbase.client.TableDescriptor; 074import org.apache.hadoop.hbase.master.region.MasterRegionFactory; 075import org.apache.hadoop.hbase.replication.ReplicationException; 076import org.apache.hadoop.hbase.replication.ReplicationGroupOffset; 077import org.apache.hadoop.hbase.replication.ReplicationQueueId; 078import org.apache.hadoop.hbase.replication.ReplicationQueueStorage; 079import org.apache.hadoop.hbase.replication.ReplicationStorageFactory; 080import org.apache.hadoop.hbase.tool.BulkLoadHFiles; 081import org.apache.hadoop.hbase.util.CommonFSUtils; 082import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; 083import org.apache.hadoop.hbase.util.FSTableDescriptors; 084import org.apache.hadoop.hbase.util.FSUtils; 085import org.apache.hadoop.hbase.wal.AbstractFSWALProvider; 086import org.apache.yetus.audience.InterfaceAudience; 087import org.slf4j.Logger; 088import org.slf4j.LoggerFactory; 089 090import org.apache.hbase.thirdparty.com.google.common.base.Splitter; 091import org.apache.hbase.thirdparty.com.google.common.base.Strings; 092import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableMap; 093import org.apache.hbase.thirdparty.com.google.common.collect.Iterables; 094import org.apache.hbase.thirdparty.com.google.common.collect.Iterators; 095 096/** 097 * A collection for methods used by multiple classes to backup HBase tables. 098 */ 099@InterfaceAudience.Private 100public final class BackupUtils { 101 private static final Logger LOG = LoggerFactory.getLogger(BackupUtils.class); 102 public static final String LOGNAME_SEPARATOR = "."; 103 public static final int MILLISEC_IN_HOUR = 3600000; 104 public static final String DATE_FORMAT = "yyyy-MM-dd"; 105 106 private BackupUtils() { 107 throw new AssertionError("Instantiating utility class..."); 108 } 109 110 /** 111 * Loop through the RS log timestamp map for the tables, for each RS, find the min timestamp value 112 * for the RS among the tables. 113 * @param rsLogTimestampMap timestamp map 114 * @return the min timestamp of each RS 115 */ 116 public static Map<String, Long> 117 getRSLogTimestampMins(Map<TableName, Map<String, Long>> rsLogTimestampMap) { 118 return rsLogTimestampMap.values().stream().flatMap(map -> map.entrySet().stream()) 119 .collect(Collectors.toMap(Entry::getKey, Entry::getValue, Math::min)); 120 } 121 122 /** 123 * copy out Table RegionInfo into incremental backup image need to consider move this logic into 124 * HBackupFileSystem 125 * @param conn connection 126 * @param backupInfo backup info 127 * @param conf configuration 128 * @throws IOException exception 129 */ 130 public static void copyTableRegionInfo(Connection conn, BackupInfo backupInfo, Configuration conf) 131 throws IOException { 132 Path rootDir = CommonFSUtils.getRootDir(conf); 133 FileSystem fs = rootDir.getFileSystem(conf); 134 135 // for each table in the table set, copy out the table info and region 136 // info files in the correct directory structure 137 try (Admin admin = conn.getAdmin()) { 138 for (TableName table : backupInfo.getTables()) { 139 if (!admin.tableExists(table)) { 140 LOG.warn("Table " + table + " does not exists, skipping it."); 141 continue; 142 } 143 TableDescriptor orig = FSTableDescriptors.getTableDescriptorFromFs(fs, rootDir, table); 144 145 // write a copy of descriptor to the target directory 146 Path target = new Path(backupInfo.getTableBackupDir(table)); 147 FileSystem targetFs = target.getFileSystem(conf); 148 try (FSTableDescriptors descriptors = 149 new FSTableDescriptors(targetFs, CommonFSUtils.getRootDir(conf))) { 150 descriptors.createTableDescriptorForTableDirectory(target, orig, false); 151 } 152 LOG.debug("Attempting to copy table info for:" + table + " target: " + target 153 + " descriptor: " + orig); 154 LOG.debug("Finished copying tableinfo."); 155 List<RegionInfo> regions = MetaTableAccessor.getTableRegions(conn, table); 156 // For each region, write the region info to disk 157 LOG.debug("Starting to write region info for table " + table); 158 for (RegionInfo regionInfo : regions) { 159 Path regionDir = FSUtils 160 .getRegionDirFromTableDir(new Path(backupInfo.getTableBackupDir(table)), regionInfo); 161 regionDir = new Path(backupInfo.getTableBackupDir(table), regionDir.getName()); 162 writeRegioninfoOnFilesystem(conf, targetFs, regionDir, regionInfo); 163 } 164 LOG.debug("Finished writing region info for table " + table); 165 } 166 } 167 } 168 169 /** 170 * Write the .regioninfo file on-disk. 171 */ 172 public static void writeRegioninfoOnFilesystem(final Configuration conf, final FileSystem fs, 173 final Path regionInfoDir, RegionInfo regionInfo) throws IOException { 174 final byte[] content = RegionInfo.toDelimitedByteArray(regionInfo); 175 Path regionInfoFile = new Path(regionInfoDir, "." + HConstants.REGIONINFO_QUALIFIER_STR); 176 // First check to get the permissions 177 FsPermission perms = CommonFSUtils.getFilePermissions(fs, conf, HConstants.DATA_FILE_UMASK_KEY); 178 // Write the RegionInfo file content 179 FSDataOutputStream out = FSUtils.create(conf, fs, regionInfoFile, perms, null); 180 try { 181 out.write(content); 182 } finally { 183 out.close(); 184 } 185 } 186 187 /** 188 * Parses hostname:port from WAL file path 189 * @param p path to WAL file 190 * @return hostname:port 191 */ 192 public static String parseHostNameFromLogFile(Path p) { 193 try { 194 if (AbstractFSWALProvider.isArchivedLogFile(p)) { 195 return BackupUtils.parseHostFromOldLog(p); 196 } else { 197 ServerName sname = AbstractFSWALProvider.getServerNameFromWALDirectoryName(p); 198 if (sname != null) { 199 return sname.getAddress().toString(); 200 } else { 201 LOG.error("Skip log file (can't parse): " + p); 202 return null; 203 } 204 } 205 } catch (Exception e) { 206 LOG.error("Skip log file (can't parse): " + p, e); 207 return null; 208 } 209 } 210 211 /** 212 * Returns the log roll result of every region server that took part in the log roll that happened 213 * between reading {@code previousLogRolls} and {@code latestLogRolls}. A region server that did 214 * not take part (for example because it is offline) keeps its old roll result, which is never 215 * removed, so it is recognized by its roll result not changing. 216 * @param previousLogRolls roll results by host, read before the log roll 217 * @param latestLogRolls roll results by host, read after the log roll 218 * @return roll results by host, for the hosts that took part in the log roll 219 */ 220 public static Map<String, Long> getRolledHosts(Map<String, Long> previousLogRolls, 221 Map<String, Long> latestLogRolls) { 222 return latestLogRolls.entrySet().stream() 223 .filter(entry -> !entry.getValue().equals(previousLogRolls.get(entry.getKey()))) 224 .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); 225 } 226 227 /** 228 * Computes the per-host log boundaries stored by a backup that has no previous boundaries to 229 * carry forward, such as a full backup. See 230 * {@link #computeLogBoundaries(Map, Map, Set, Collection, Collection)}. 231 */ 232 public static Map<String, Long> computeLogBoundaries(Map<String, Long> rolledHosts, 233 Collection<String> coveredLogs, Collection<String> pendingLogs) throws IOException { 234 return computeLogBoundaries(rolledHosts, Collections.emptyMap(), Collections.emptySet(), 235 coveredLogs, pendingLogs); 236 } 237 238 /** 239 * Computes the per-host log boundaries stored by a backup. Everything up to a host's boundary is 240 * covered by backups, so later incremental backups only include that host's logs that are newer. 241 * <ul> 242 * <li>A host that took part in the backup's log roll gets its roll result.</li> 243 * <li>Any other host gets the creation time of its newest covered log, capped to just below its 244 * oldest pending log, so that pending logs are included in a later backup.</li> 245 * <li>A host without covered or pending logs keeps its previous boundary while it still has logs, 246 * and otherwise gets no boundary.</li> 247 * </ul> 248 * Capping is needed because a host's logs can still be pending while some of its newer logs are 249 * already covered, for example when a dead server's logs are split and archived out of order. As 250 * a result, the next backup includes the covered logs that are newer than the cap a second time: 251 * losing the pending logs is worse than repeating edits that are already backed up. A host is 252 * also kept while any of its logs are pending, even if none are covered, because without a 253 * boundary the next backup would treat it as unknown and skip its older logs. For the same reason 254 * a host keeps its previous boundary while it still has logs: without it, the next backup would 255 * include its logs that are already covered, which can bring back deleted data. 256 * @param rolledHosts roll results of the hosts that took part in the log roll 257 * @param previousBoundaries boundaries stored by the previous backup 258 * @param hostsWithLogs hosts that still have logs, whether or not this backup includes them 259 * @param coveredLogs logs whose edits are included in the backup 260 * @param pendingLogs logs whose edits are not to be included in the backup 261 * @return boundaries by host 262 */ 263 public static Map<String, Long> computeLogBoundaries(Map<String, Long> rolledHosts, 264 Map<String, Long> previousBoundaries, Set<String> hostsWithLogs, Collection<String> coveredLogs, 265 Collection<String> pendingLogs) throws IOException { 266 Map<String, Long> otherHosts = new HashMap<>(); 267 for (String log : coveredLogs) { 268 Path path = new Path(log); 269 String host = parseHostNameFromLogFile(path); 270 if (host != null && !rolledHosts.containsKey(host)) { 271 otherHosts.merge(host, getCreationTime(path), Math::max); 272 } 273 } 274 for (String log : pendingLogs) { 275 Path path = new Path(log); 276 String host = parseHostNameFromLogFile(path); 277 if (host != null && !rolledHosts.containsKey(host)) { 278 otherHosts.merge(host, getCreationTime(path) - 1, Math::min); 279 } 280 } 281 Map<String, Long> boundaries = new HashMap<>(rolledHosts); 282 boundaries.putAll(otherHosts); 283 for (Entry<String, Long> previous : previousBoundaries.entrySet()) { 284 if (hostsWithLogs.contains(previous.getKey())) { 285 boundaries.putIfAbsent(previous.getKey(), previous.getValue()); 286 } 287 } 288 return boundaries; 289 } 290 291 /** 292 * Returns WAL file name 293 * @param walFileName WAL file name 294 * @return WAL file name 295 */ 296 public static String getUniqueWALFileNamePart(String walFileName) { 297 return getUniqueWALFileNamePart(new Path(walFileName)); 298 } 299 300 /** 301 * Returns WAL file name 302 * @param p WAL file path 303 * @return WAL file name 304 */ 305 public static String getUniqueWALFileNamePart(Path p) { 306 return p.getName(); 307 } 308 309 /** 310 * Get the total length of files under the given directory recursively. 311 * @param fs The hadoop file system 312 * @param dir The target directory 313 * @return the total length of files 314 * @throws IOException exception 315 */ 316 public static long getFilesLength(FileSystem fs, Path dir) throws IOException { 317 long totalLength = 0; 318 FileStatus[] files = CommonFSUtils.listStatus(fs, dir); 319 if (files != null) { 320 for (FileStatus fileStatus : files) { 321 if (fileStatus.isDirectory()) { 322 totalLength += getFilesLength(fs, fileStatus.getPath()); 323 } else { 324 totalLength += fileStatus.getLen(); 325 } 326 } 327 } 328 return totalLength; 329 } 330 331 /** 332 * Get list of all old WAL files (WALs and archive) 333 * @param c configuration 334 * @param hostTimestampMap {host,timestamp} map 335 * @return list of WAL files 336 * @throws IOException exception 337 */ 338 public static List<String> getWALFilesOlderThan(final Configuration c, 339 final HashMap<String, Long> hostTimestampMap) throws IOException { 340 Path walRootDir = CommonFSUtils.getWALRootDir(c); 341 Path logDir = new Path(walRootDir, HConstants.HREGION_LOGDIR_NAME); 342 Path oldLogDir = new Path(walRootDir, HConstants.HREGION_OLDLOGDIR_NAME); 343 List<String> logFiles = new ArrayList<>(); 344 345 PathFilter filter = p -> { 346 try { 347 if (AbstractFSWALProvider.isMetaFile(p)) { 348 return false; 349 } 350 String host = parseHostNameFromLogFile(p); 351 if (host == null) { 352 return false; 353 } 354 Long oldTimestamp = hostTimestampMap.get(host); 355 Long currentLogTS = BackupUtils.getCreationTime(p); 356 return currentLogTS <= oldTimestamp; 357 } catch (Exception e) { 358 LOG.warn("Can not parse" + p, e); 359 return false; 360 } 361 }; 362 FileSystem walFs = CommonFSUtils.getWALFileSystem(c); 363 logFiles = BackupUtils.getFiles(walFs, logDir, logFiles, filter); 364 logFiles = BackupUtils.getFiles(walFs, oldLogDir, logFiles, filter); 365 return logFiles; 366 } 367 368 public static TableName[] parseTableNames(String tables) { 369 if (tables == null) { 370 return null; 371 } 372 return Splitter.on(BackupRestoreConstants.TABLENAME_DELIMITER_IN_COMMAND).splitToStream(tables) 373 .map(TableName::valueOf).toArray(TableName[]::new); 374 } 375 376 /** 377 * Check whether the backup path exist 378 * @param backupStr backup 379 * @param conf configuration 380 * @return Yes if path exists 381 * @throws IOException exception 382 */ 383 public static boolean checkPathExist(String backupStr, Configuration conf) throws IOException { 384 boolean isExist = false; 385 Path backupPath = new Path(backupStr); 386 FileSystem fileSys = backupPath.getFileSystem(conf); 387 String targetFsScheme = fileSys.getUri().getScheme(); 388 if (LOG.isTraceEnabled()) { 389 LOG.trace("Schema of given url: " + backupStr + " is: " + targetFsScheme); 390 } 391 if (fileSys.exists(backupPath)) { 392 isExist = true; 393 } 394 return isExist; 395 } 396 397 /** 398 * Check target path first, confirm it doesn't exist before backup 399 * @param backupRootPath backup destination path 400 * @param conf configuration 401 * @throws IOException exception 402 */ 403 public static void checkTargetDir(String backupRootPath, Configuration conf) throws IOException { 404 boolean targetExists; 405 try { 406 targetExists = checkPathExist(backupRootPath, conf); 407 } catch (IOException e) { 408 String expMsg = e.getMessage(); 409 String newMsg = null; 410 if (expMsg.contains("No FileSystem for scheme")) { 411 newMsg = 412 "Unsupported filesystem scheme found in the backup target url. Error Message: " + expMsg; 413 LOG.error(newMsg); 414 throw new IOException(newMsg); 415 } else { 416 throw e; 417 } 418 } 419 420 if (targetExists) { 421 LOG.info("Using existing backup root dir: " + backupRootPath); 422 } else { 423 LOG.info("Backup root dir " + backupRootPath + " does not exist. Will be created."); 424 } 425 } 426 427 /** 428 * Get the min value for all the Values a map. 429 * @param map map 430 * @return the min value 431 */ 432 public static <T> Long getMinValue(Map<T, Long> map) { 433 Long minTimestamp = null; 434 if (map != null) { 435 ArrayList<Long> timestampList = new ArrayList<>(map.values()); 436 Collections.sort(timestampList); 437 // The min among all the RS log timestamps will be kept in backup system table table. 438 minTimestamp = timestampList.get(0); 439 } 440 return minTimestamp; 441 } 442 443 /** 444 * Parses host name:port from archived WAL path 445 * @param p path 446 * @return host name 447 */ 448 public static String parseHostFromOldLog(Path p) { 449 // Skip master wals 450 if (p.getName().endsWith(MasterRegionFactory.ARCHIVED_WAL_SUFFIX)) { 451 return null; 452 } 453 Path parent = p.getParent(); 454 if (parent != null && ServerName.isFullServerName(parent.getName())) { 455 return ServerName.valueOf(parent.getName()).getAddress().toString(); 456 } 457 try { 458 String urlDecodedName = URLDecoder.decode(p.getName(), "UTF8"); 459 Iterable<String> nameSplitsOnComma = Splitter.on(",").split(urlDecodedName); 460 String host = Iterables.get(nameSplitsOnComma, 0); 461 String port = Iterables.get(nameSplitsOnComma, 1); 462 return host + ":" + port; 463 } catch (Exception e) { 464 LOG.warn("Skip log file (can't parse): {}", p); 465 return null; 466 } 467 } 468 469 /** 470 * Given the log file, parse the timestamp from the file name. The timestamp is the last number. 471 * @param p a path to the log file 472 * @return the timestamp 473 * @throws IOException exception 474 */ 475 public static Long getCreationTime(Path p) throws IOException { 476 int idx = p.getName().lastIndexOf(LOGNAME_SEPARATOR); 477 if (idx < 0) { 478 throw new IOException("Cannot parse timestamp from path " + p); 479 } 480 String ts = p.getName().substring(idx + 1); 481 return Long.parseLong(ts); 482 } 483 484 public static List<String> getFiles(FileSystem fs, Path rootDir, List<String> files, 485 PathFilter filter) throws IOException { 486 RemoteIterator<LocatedFileStatus> it = fs.listFiles(rootDir, true); 487 488 while (it.hasNext()) { 489 LocatedFileStatus lfs = it.next(); 490 if (lfs.isDirectory()) { 491 continue; 492 } 493 // apply filter 494 if (filter.accept(lfs.getPath())) { 495 files.add(lfs.getPath().toString()); 496 } 497 } 498 return files; 499 } 500 501 public static void cleanupBackupData(BackupInfo context, Configuration conf) throws IOException { 502 cleanupHLogDir(context, conf); 503 cleanupTargetDir(context, conf); 504 } 505 506 /** 507 * Clean up directories which are generated when DistCp copying hlogs 508 * @param backupInfo backup info 509 * @param conf configuration 510 * @throws IOException exception 511 */ 512 private static void cleanupHLogDir(BackupInfo backupInfo, Configuration conf) throws IOException { 513 String logDir = backupInfo.getHLogTargetDir(); 514 if (logDir == null) { 515 LOG.warn("No log directory specified for " + backupInfo.getBackupId()); 516 return; 517 } 518 519 Path rootPath = new Path(logDir).getParent(); 520 FileSystem fs = FileSystem.get(rootPath.toUri(), conf); 521 FileStatus[] files = listStatus(fs, rootPath, null); 522 if (files == null) { 523 return; 524 } 525 for (FileStatus file : files) { 526 LOG.debug("Delete log files: " + file.getPath().getName()); 527 fs.delete(file.getPath(), true); 528 } 529 } 530 531 private static void cleanupTargetDir(BackupInfo backupInfo, Configuration conf) { 532 try { 533 // clean up the data at target directory 534 LOG.debug("Trying to cleanup up target dir : " + backupInfo.getBackupId()); 535 String targetDir = backupInfo.getBackupRootDir(); 536 if (targetDir == null) { 537 LOG.warn("No target directory specified for " + backupInfo.getBackupId()); 538 return; 539 } 540 541 FileSystem outputFs = FileSystem.get(new Path(backupInfo.getBackupRootDir()).toUri(), conf); 542 543 for (TableName table : backupInfo.getTables()) { 544 Path targetDirPath = new Path( 545 getTableBackupDir(backupInfo.getBackupRootDir(), backupInfo.getBackupId(), table)); 546 if (outputFs.delete(targetDirPath, true)) { 547 LOG.info("Cleaning up backup data at " + targetDirPath.toString() + " done."); 548 } else { 549 LOG.info("No data has been found in " + targetDirPath.toString() + "."); 550 } 551 552 Path tableDir = targetDirPath.getParent(); 553 FileStatus[] backups = listStatus(outputFs, tableDir, null); 554 if (backups == null || backups.length == 0) { 555 outputFs.delete(tableDir, true); 556 LOG.debug(tableDir.toString() + " is empty, remove it."); 557 } 558 } 559 outputFs.delete(new Path(targetDir, backupInfo.getBackupId()), true); 560 } catch (IOException e1) { 561 LOG.error("Cleaning up backup data of " + backupInfo.getBackupId() + " at " 562 + backupInfo.getBackupRootDir() + " failed due to " + e1.getMessage() + "."); 563 } 564 } 565 566 /** 567 * Given the backup root dir, backup id and the table name, return the backup image location, 568 * which is also where the backup manifest file is. return value look like: 569 * "hdfs://backup.hbase.org:9000/user/biadmin/backup1/backup_1396650096738/default/t1_dn/" 570 * @param backupRootDir backup root directory 571 * @param backupId backup id 572 * @param tableName table name 573 * @return backupPath String for the particular table 574 */ 575 public static String getTableBackupDir(String backupRootDir, String backupId, 576 TableName tableName) { 577 return backupRootDir + Path.SEPARATOR + backupId + Path.SEPARATOR 578 + tableName.getNamespaceAsString() + Path.SEPARATOR + tableName.getQualifierAsString() 579 + Path.SEPARATOR; 580 } 581 582 /** 583 * Calls fs.listStatus() and treats FileNotFoundException as non-fatal This accommodates 584 * differences between hadoop versions, where hadoop 1 does not throw a FileNotFoundException, and 585 * return an empty FileStatus[] while Hadoop 2 will throw FileNotFoundException. 586 * @param fs file system 587 * @param dir directory 588 * @param filter path filter 589 * @return null if dir is empty or doesn't exist, otherwise FileStatus array 590 */ 591 public static FileStatus[] listStatus(final FileSystem fs, final Path dir, 592 final PathFilter filter) throws IOException { 593 FileStatus[] status = null; 594 try { 595 status = filter == null ? fs.listStatus(dir) : fs.listStatus(dir, filter); 596 } catch (FileNotFoundException fnfe) { 597 // if directory doesn't exist, return null 598 if (LOG.isTraceEnabled()) { 599 LOG.trace(dir + " doesn't exist"); 600 } 601 } 602 603 if (status == null || status.length < 1) { 604 return null; 605 } 606 607 return status; 608 } 609 610 /** 611 * Return the 'path' component of a Path. In Hadoop, Path is a URI. This method returns the 'path' 612 * component of a Path's URI: e.g. If a Path is 613 * <code>hdfs://example.org:9000/hbase_trunk/TestTable/compaction.dir</code>, this method returns 614 * <code>/hbase_trunk/TestTable/compaction.dir</code>. This method is useful if you want to print 615 * out a Path without qualifying Filesystem instance. 616 * @param p file system Path whose 'path' component we are to return. 617 * @return Path portion of the Filesystem 618 */ 619 public static String getPath(Path p) { 620 return p.toUri().getPath(); 621 } 622 623 /** 624 * Given the backup root dir and the backup id, return the log file location for an incremental 625 * backup. 626 * @param backupRootDir backup root directory 627 * @param backupId backup id 628 * @return logBackupDir: ".../user/biadmin/backup1/WALs/backup_1396650096738" 629 */ 630 public static String getLogBackupDir(String backupRootDir, String backupId) { 631 return backupRootDir + Path.SEPARATOR + backupId + Path.SEPARATOR 632 + HConstants.HREGION_LOGDIR_NAME; 633 } 634 635 /** 636 * Loads all backup history as stored in files on the given backup root path. 637 * @return all backup history, from newest (most recent) to oldest (least recent) 638 */ 639 private static List<BackupInfo> getHistory(Configuration conf, Path backupRootPath) 640 throws IOException { 641 // Get all (n) history from backup root destination 642 643 FileSystem fs = FileSystem.get(backupRootPath.toUri(), conf); 644 RemoteIterator<LocatedFileStatus> it; 645 try { 646 it = fs.listLocatedStatus(backupRootPath); 647 } catch (FileNotFoundException e) { 648 return Collections.emptyList(); 649 } 650 651 List<BackupInfo> infos = new ArrayList<>(); 652 while (it.hasNext()) { 653 LocatedFileStatus lfs = it.next(); 654 655 if (!lfs.isDirectory()) { 656 continue; 657 } 658 659 String backupId = lfs.getPath().getName(); 660 try { 661 BackupInfo info = loadBackupInfo(backupRootPath, backupId, fs); 662 infos.add(info); 663 } catch (IOException e) { 664 LOG.error("Can not load backup info from: " + lfs.getPath(), e); 665 } 666 } 667 // Sort 668 infos.sort(Comparator.<BackupInfo> naturalOrder().reversed()); 669 return infos; 670 } 671 672 /** 673 * Loads all backup history as stored in files on the given backup root path, and returns the 674 * first n entries matching all given filters. 675 * @return (subset of) backup history, from newest (most recent) to oldest (least recent) 676 */ 677 public static List<BackupInfo> getHistory(Configuration conf, int n, Path backupRootPath, 678 BackupInfo.Filter... filters) throws IOException { 679 List<BackupInfo> infos = getHistory(conf, backupRootPath); 680 681 Predicate<BackupInfo> combinedPredicate = Stream.of(filters) 682 .map(filter -> (Predicate<BackupInfo>) filter).reduce(Predicate::and).orElse(x -> true); 683 684 return infos.stream().filter(combinedPredicate).limit(n).toList(); 685 } 686 687 public static BackupInfo loadBackupInfo(Path backupRootPath, String backupId, FileSystem fs) 688 throws IOException { 689 Path backupPath = new Path(backupRootPath, backupId); 690 691 RemoteIterator<LocatedFileStatus> it = fs.listFiles(backupPath, true); 692 while (it.hasNext()) { 693 LocatedFileStatus lfs = it.next(); 694 if (lfs.getPath().getName().equals(BackupManifest.MANIFEST_FILE_NAME)) { 695 // Load BackupManifest 696 BackupManifest manifest = new BackupManifest(fs, lfs.getPath().getParent()); 697 BackupInfo info = manifest.toBackupInfo(); 698 return info; 699 } 700 } 701 return null; 702 } 703 704 /** 705 * Create restore request. 706 * @param backupRootDir backup root dir 707 * @param backupId backup id 708 * @param check check only 709 * @param fromTables table list from 710 * @param toTables table list to 711 * @param isOverwrite overwrite data 712 * @return request obkect 713 */ 714 public static RestoreRequest createRestoreRequest(String backupRootDir, String backupId, 715 boolean check, TableName[] fromTables, TableName[] toTables, boolean isOverwrite) { 716 return createRestoreRequest(backupRootDir, backupId, check, fromTables, toTables, isOverwrite, 717 false); 718 } 719 720 public static RestoreRequest createRestoreRequest(String backupRootDir, String backupId, 721 boolean check, TableName[] fromTables, TableName[] toTables, boolean isOverwrite, 722 boolean isKeepOriginalSplits) { 723 RestoreRequest.Builder builder = new RestoreRequest.Builder(); 724 RestoreRequest request = builder.withBackupRootDir(backupRootDir).withBackupId(backupId) 725 .withCheck(check).withFromTables(fromTables).withToTables(toTables).withOverwrite(isOverwrite) 726 .withKeepOriginalSplits(isKeepOriginalSplits).build(); 727 return request; 728 } 729 730 public static boolean validate(List<TableName> tables, BackupManifest backupManifest, 731 Configuration conf) throws IOException { 732 boolean isValid = true; 733 734 for (TableName table : tables) { 735 TreeSet<BackupImage> imageSet = new TreeSet<>(); 736 737 ArrayList<BackupImage> depList = backupManifest.getDependentListByTable(table); 738 if (depList != null && !depList.isEmpty()) { 739 imageSet.addAll(depList); 740 } 741 742 LOG.info("Dependent image(s) from old to new:"); 743 for (BackupImage image : imageSet) { 744 String imageDir = 745 HBackupFileSystem.getTableBackupDir(image.getRootDir(), image.getBackupId(), table); 746 if (!BackupUtils.checkPathExist(imageDir, conf)) { 747 LOG.error("ERROR: backup image does not exist: " + imageDir); 748 isValid = false; 749 break; 750 } 751 LOG.info("Backup image: " + image.getBackupId() + " for '" + table + "' is available"); 752 } 753 } 754 return isValid; 755 } 756 757 public static Path getBulkOutputDir(Path restoreRootDir, String tableName, Configuration conf, 758 boolean deleteOnExit) throws IOException { 759 FileSystem fs = restoreRootDir.getFileSystem(conf); 760 Path path = new Path(restoreRootDir, 761 "bulk_output-" + tableName + "-" + EnvironmentEdgeManager.currentTime()); 762 if (deleteOnExit) { 763 fs.deleteOnExit(path); 764 } 765 return path; 766 } 767 768 public static Path getBulkOutputDir(Path restoreRootDir, String tableName, Configuration conf) 769 throws IOException { 770 return getBulkOutputDir(restoreRootDir, tableName, conf, true); 771 } 772 773 public static Path getBulkOutputDir(String tableName, Configuration conf, boolean deleteOnExit) 774 throws IOException { 775 FileSystem fs = FileSystem.get(conf); 776 return getBulkOutputDir(getTmpRestoreOutputDir(fs, conf), tableName, conf, deleteOnExit); 777 } 778 779 /** 780 * Build temporary output path 781 * @param fs filesystem for default output dir 782 * @param conf configuration 783 * @return output path 784 */ 785 public static Path getTmpRestoreOutputDir(FileSystem fs, Configuration conf) { 786 String tmp = 787 conf.get(HConstants.TEMPORARY_FS_DIRECTORY_KEY, fs.getHomeDirectory() + "/hbase-staging"); 788 return new Path(tmp); 789 } 790 791 public static String getFileNameCompatibleString(TableName table) { 792 return table.getNamespaceAsString() + "-" + table.getQualifierAsString(); 793 } 794 795 public static boolean failed(int result) { 796 return result != 0; 797 } 798 799 public static boolean succeeded(int result) { 800 return result == 0; 801 } 802 803 public static BulkLoadHFiles createLoader(Configuration config) { 804 // set configuration for restore: 805 // LoadIncrementalHFile needs more time 806 // <name>hbase.rpc.timeout</name> <value>600000</value> 807 // calculates 808 Configuration conf = new Configuration(config); 809 conf.setInt(HConstants.HBASE_RPC_TIMEOUT_KEY, MILLISEC_IN_HOUR); 810 811 // By default, it is 32 and loader will fail if # of files in any region exceed this 812 // limit. Bad for snapshot restore. 813 conf.setInt(BulkLoadHFiles.MAX_FILES_PER_REGION_PER_FAMILY, Integer.MAX_VALUE); 814 conf.set(BulkLoadHFiles.IGNORE_UNMATCHED_CF_CONF_KEY, "yes"); 815 return BulkLoadHFiles.create(conf); 816 } 817 818 public static String findMostRecentBackupId(String[] backupIds) { 819 long recentTimestamp = Long.MIN_VALUE; 820 for (String backupId : backupIds) { 821 long ts = Long.parseLong(Iterators.get(Splitter.on('_').split(backupId).iterator(), 1)); 822 if (ts > recentTimestamp) { 823 recentTimestamp = ts; 824 } 825 } 826 return BackupRestoreConstants.BACKUPID_PREFIX + recentTimestamp; 827 } 828 829 /** 830 * roll WAL writer for all region servers and record the newest log roll result 831 */ 832 public static void logRoll(Connection conn, String backupRootDir, Configuration conf) 833 throws IOException { 834 boolean legacy = conf.getBoolean("hbase.backup.logroll.legacy.used", false); 835 if (legacy) { 836 logRollV1(conn, backupRootDir); 837 } else { 838 logRollV2(conn, backupRootDir); 839 } 840 } 841 842 private static void logRollV1(Connection conn, String backupRootDir) throws IOException { 843 try (Admin admin = conn.getAdmin()) { 844 admin.execProcedure(LogRollMasterProcedureManager.ROLLLOG_PROCEDURE_SIGNATURE, 845 LogRollMasterProcedureManager.ROLLLOG_PROCEDURE_NAME, 846 ImmutableMap.of("backupRoot", backupRootDir)); 847 } 848 } 849 850 private static void logRollV2(Connection conn, String backupRootDir) throws IOException { 851 BackupSystemTable backupSystemTable = new BackupSystemTable(conn); 852 HashMap<String, Long> lastLogRollResult = 853 backupSystemTable.readRegionServerLastLogRollResult(backupRootDir); 854 try (Admin admin = conn.getAdmin()) { 855 Map<ServerName, Long> newLogRollResult = admin.rollAllWALWriters(); 856 857 for (Map.Entry<ServerName, Long> entry : newLogRollResult.entrySet()) { 858 ServerName serverName = entry.getKey(); 859 long newHighestWALFilenum = entry.getValue(); 860 861 String address = serverName.getAddress().toString(); 862 Long lastHighestWALFilenum = lastLogRollResult.get(address); 863 if (lastHighestWALFilenum != null && lastHighestWALFilenum > newHighestWALFilenum) { 864 LOG.warn("Won't update last roll log result for server {}: current = {}, new = {}", 865 serverName, lastHighestWALFilenum, newHighestWALFilenum); 866 } else { 867 backupSystemTable.writeRegionServerLastLogRollResult(address, newHighestWALFilenum, 868 backupRootDir); 869 if (LOG.isDebugEnabled()) { 870 LOG.debug("updated last roll log result for {} from {} to {}", serverName, 871 lastHighestWALFilenum, newHighestWALFilenum); 872 } 873 } 874 } 875 } 876 } 877 878 /** 879 * Calculates the replication checkpoint timestamp used for continuous backup. 880 * <p> 881 * A replication checkpoint is the earliest timestamp across all region servers such that every 882 * WAL entry before that point is known to be replicated to the target system. This is essential 883 * for features like Point-in-Time Restore (PITR) and incremental backups, where we want to 884 * confidently restore data to a consistent state without missing updates. 885 * <p> 886 * The checkpoint is calculated using a combination of: 887 * <ul> 888 * <li>The start timestamps of WAL files currently being replicated for each server.</li> 889 * <li>The latest successfully replicated timestamp recorded by the replication marker chore.</li> 890 * </ul> 891 * <p> 892 * We combine these two sources to handle the following challenges: 893 * <ul> 894 * <li><b>Stale WAL start times:</b> If replication traffic is low or WALs are long-lived, the 895 * replication offset may point to the same WAL for a long time, resulting in stale timestamps 896 * that underestimate progress. This could delay PITR unnecessarily.</li> 897 * <li><b>Limitations of marker-only tracking:</b> The replication marker chore stores the last 898 * successfully replicated timestamp per region server in a system table. However, this data may 899 * become stale if the server goes offline or region ownership changes. For example, if a region 900 * initially belonged to rs1 and was later moved to rs4 due to re-balancing, rs1’s marker would 901 * persist even though it no longer holds any regions. Relying solely on these stale markers could 902 * lead to incorrect or outdated checkpoints.</li> 903 * </ul> 904 * <p> 905 * To handle these limitations, the method: 906 * <ol> 907 * <li>Verifies that the continuous backup peer exists to ensure replication is enabled.</li> 908 * <li>Retrieves WAL replication queue information for the peer, collecting WAL start times per 909 * region server. This gives us a lower bound for replication progress.</li> 910 * <li>Reads the marker chore's replicated timestamps from the backup system table.</li> 911 * <li>For servers found in both sources, if the marker timestamp is more recent than the WAL's 912 * start timestamp, we use the marker (since replication has progressed beyond the WAL).</li> 913 * <li>We discard marker entries for region servers that are not present in WAL queues, assuming 914 * those servers are no longer relevant (e.g., decommissioned or reassigned).</li> 915 * <li>The checkpoint is the minimum of all chosen timestamps — i.e., the slowest replicating 916 * region server.</li> 917 * <li>Finally, we persist the updated marker information to include any newly participating 918 * region servers.</li> 919 * </ol> 920 * <p> 921 * Note: If the replication marker chore is disabled, we fall back to using only the WAL start 922 * times. This ensures correctness but may lead to conservative checkpoint estimates during idle 923 * periods. 924 * @param conn the HBase connection 925 * @return the calculated replication checkpoint timestamp 926 * @throws IOException if reading replication queues or updating the backup system table fails 927 */ 928 public static long getReplicationCheckpoint(Connection conn) throws IOException { 929 Configuration conf = conn.getConfiguration(); 930 long checkpoint = EnvironmentEdgeManager.getDelegate().currentTime(); 931 932 // Step 1: Ensure the continuous backup replication peer exists 933 if (!continuousBackupReplicationPeerExists(conn.getAdmin())) { 934 String msg = "Replication peer '" + CONTINUOUS_BACKUP_REPLICATION_PEER 935 + "' not found. Continuous backup not enabled."; 936 LOG.error(msg); 937 throw new IOException(msg); 938 } 939 940 // Step 2: Get all replication queues for the continuous backup peer 941 ReplicationQueueStorage queueStorage = 942 ReplicationStorageFactory.getReplicationQueueStorage(conn, conf); 943 944 List<ReplicationQueueId> queueIds; 945 try { 946 queueIds = queueStorage.listAllQueueIds(CONTINUOUS_BACKUP_REPLICATION_PEER); 947 } catch (ReplicationException e) { 948 String msg = "Failed to retrieve replication queue IDs for peer '" 949 + CONTINUOUS_BACKUP_REPLICATION_PEER + "'"; 950 LOG.error(msg, e); 951 throw new IOException(msg, e); 952 } 953 954 if (queueIds.isEmpty()) { 955 String msg = "Replication peer '" + CONTINUOUS_BACKUP_REPLICATION_PEER + "' has no queues. " 956 + "This may indicate that continuous backup replication is not initialized correctly."; 957 LOG.error(msg); 958 throw new IOException(msg); 959 } 960 961 // Step 3: Build a map of ServerName -> WAL start timestamp (lowest seen per server) 962 Map<ServerName, Long> serverToCheckpoint = new HashMap<>(); 963 for (ReplicationQueueId queueId : queueIds) { 964 Map<String, ReplicationGroupOffset> offsets; 965 try { 966 offsets = queueStorage.getOffsets(queueId); 967 } catch (ReplicationException e) { 968 String msg = "Failed to fetch WAL offsets for replication queue: " + queueId; 969 LOG.error(msg, e); 970 throw new IOException(msg, e); 971 } 972 973 for (ReplicationGroupOffset offset : offsets.values()) { 974 String walFile = offset.getWal(); 975 long ts = AbstractFSWALProvider.getTimestamp(walFile); // WAL creation time 976 ServerName server = queueId.getServerName(); 977 // Store the minimum timestamp per server (ts - 1 to avoid edge boundary issues) 978 serverToCheckpoint.merge(server, ts - 1, Math::min); 979 } 980 } 981 982 // Step 4: If replication markers are enabled, overlay fresher timestamps from backup system 983 // table 984 boolean replicationMarkerEnabled = 985 conf.getBoolean(REPLICATION_MARKER_ENABLED_KEY, REPLICATION_MARKER_ENABLED_DEFAULT); 986 if (replicationMarkerEnabled) { 987 try (BackupSystemTable backupSystemTable = new BackupSystemTable(conn)) { 988 Map<ServerName, Long> markerTimestamps = backupSystemTable.getBackupCheckpointTimestamps(); 989 990 for (Map.Entry<ServerName, Long> entry : markerTimestamps.entrySet()) { 991 ServerName server = entry.getKey(); 992 long markerTs = entry.getValue(); 993 994 // If marker timestamp is newer, override 995 if (serverToCheckpoint.containsKey(server)) { 996 long current = serverToCheckpoint.get(server); 997 if (markerTs > current) { 998 serverToCheckpoint.put(server, markerTs); 999 } 1000 } else { 1001 // This server is no longer active (e.g., RS moved or removed); skip 1002 if (LOG.isDebugEnabled()) { 1003 LOG.debug("Skipping replication marker timestamp for inactive server: {}", server); 1004 } 1005 } 1006 } 1007 1008 // Step 5: Persist current server timestamps into backup system table 1009 for (Map.Entry<ServerName, Long> entry : serverToCheckpoint.entrySet()) { 1010 backupSystemTable.updateBackupCheckpointTimestamp(entry.getKey(), entry.getValue()); 1011 } 1012 } 1013 } else { 1014 LOG.warn( 1015 "Replication marker chore is disabled. Using WAL-based timestamps only for checkpoint calculation."); 1016 } 1017 1018 // Step 6: Calculate final checkpoint as minimum timestamp across all active servers 1019 for (long ts : serverToCheckpoint.values()) { 1020 checkpoint = Math.min(checkpoint, ts); 1021 } 1022 1023 return checkpoint; 1024 } 1025 1026 private static boolean continuousBackupReplicationPeerExists(Admin admin) throws IOException { 1027 return admin.listReplicationPeers().stream() 1028 .anyMatch(peer -> peer.getPeerId().equals(CONTINUOUS_BACKUP_REPLICATION_PEER)); 1029 } 1030 1031 /** 1032 * Convert dayInMillis to "yyyy-MM-dd" format 1033 */ 1034 public static String formatToDateString(long dayInMillis) { 1035 SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT); 1036 dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); 1037 return dateFormat.format(new Date(dayInMillis)); 1038 } 1039 1040 /** 1041 * Fetches bulkload filepaths based on the given time range from backup WAL directory. 1042 */ 1043 public static List<Path> collectBulkFiles(Connection conn, TableName sourceTable, 1044 TableName targetTable, long startTime, long endTime, Path restoreRootDir, List<String> walDirs) 1045 throws IOException { 1046 1047 if (walDirs.isEmpty()) { 1048 String walBackupDir = conn.getConfiguration().get(CONF_CONTINUOUS_BACKUP_WAL_DIR); 1049 if (Strings.isNullOrEmpty(walBackupDir)) { 1050 throw new IOException( 1051 "WAL backup directory is not configured " + CONF_CONTINUOUS_BACKUP_WAL_DIR); 1052 } 1053 Path walDirPath = new Path(walBackupDir); 1054 walDirs = 1055 BackupUtils.getValidWalDirs(conn.getConfiguration(), walDirPath, startTime, endTime); 1056 } 1057 1058 if (walDirs.isEmpty()) { 1059 LOG.warn("No valid WAL directories found for range {} - {}. Skipping bulk-file collection.", 1060 startTime, endTime); 1061 return Collections.emptyList(); 1062 } 1063 1064 LOG.info( 1065 "Starting WAL bulk-file collection for source: {}, target: {}, time range: {} - {}, WAL " 1066 + "backup dir: {}, restore root: {}", 1067 sourceTable, targetTable, startTime, endTime, walDirs, restoreRootDir); 1068 String walDirsCsv = String.join(",", walDirs); 1069 1070 return BulkFilesCollector.collectFromWalDirs(HBaseConfiguration.create(conn.getConfiguration()), 1071 walDirsCsv, restoreRootDir, sourceTable, targetTable, startTime, endTime); 1072 } 1073 1074 /** 1075 * Fetches valid WAL directories based on the given time range. 1076 */ 1077 public static List<String> getValidWalDirs(Configuration conf, Path walBackupDir, long startTime, 1078 long endTime) throws IOException { 1079 FileSystem backupFs = FileSystem.get(walBackupDir.toUri(), conf); 1080 FileStatus[] dayDirs = backupFs.listStatus(new Path(walBackupDir, WALS_DIR)); 1081 1082 List<String> validDirs = new ArrayList<>(); 1083 SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT); 1084 dateFormat.setTimeZone(TimeZone.getTimeZone(ZoneOffset.UTC)); 1085 1086 for (FileStatus dayDir : dayDirs) { 1087 if (!dayDir.isDirectory()) { 1088 continue; // Skip files, only process directories 1089 } 1090 1091 String dirName = dayDir.getPath().getName(); 1092 try { 1093 Date dirDate = dateFormat.parse(dirName); 1094 long dirStartTime = dirDate.getTime(); // Start of that day (00:00:00) 1095 long dirEndTime = dirStartTime + ONE_DAY_IN_MILLISECONDS - 1; // End time of day (23:59:59) 1096 1097 // Check if this day's WAL files overlap with the required time range 1098 if (dirEndTime >= startTime && dirStartTime <= endTime) { 1099 validDirs.add(dayDir.getPath().toString()); 1100 } 1101 } catch (ParseException e) { 1102 LOG.warn("Skipping invalid directory name: {}", dirName, e); 1103 } 1104 } 1105 return validDirs; 1106 } 1107}