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