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.HConstants.REPLICATION_BULKLOAD_ENABLE_KEY; 021import static org.apache.hadoop.hbase.HConstants.REPLICATION_SCOPE_GLOBAL; 022import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.BACKUP_ATTEMPTS_PAUSE_MS_KEY; 023import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.BACKUP_MAX_ATTEMPTS_KEY; 024import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONF_CONTINUOUS_BACKUP_WAL_DIR; 025import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONTINUOUS_BACKUP_OFFSET_UPDATE_INTERVAL_MS; 026import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONTINUOUS_BACKUP_OFFSET_UPDATE_SIZE_THRESHOLD; 027import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONTINUOUS_BACKUP_REPLICATION_PEER; 028import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.DEFAULT_BACKUP_ATTEMPTS_PAUSE_MS; 029import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.DEFAULT_BACKUP_MAX_ATTEMPTS; 030import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.DEFAULT_CONTINUOUS_BACKUP_OFFSET_UPDATE_INTERVAL_MS; 031import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.DEFAULT_CONTINUOUS_BACKUP_OFFSET_UPDATE_SIZE_THRESHOLD; 032import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.DEFAULT_CONTINUOUS_BACKUP_REPLICATION_ENDPOINT; 033import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.JOB_NAME_CONF_KEY; 034import static org.apache.hadoop.hbase.backup.replication.ContinuousBackupReplicationEndpoint.CONF_BACKUP_ROOT_DIR; 035import static org.apache.hadoop.hbase.backup.replication.ContinuousBackupReplicationEndpoint.CONF_PEER_UUID; 036import static org.apache.hadoop.hbase.replication.ReplicationUtils.OFFSET_UPDATE_INTERVAL_MS_KEY; 037import static org.apache.hadoop.hbase.replication.ReplicationUtils.OFFSET_UPDATE_SIZE_THRESHOLD_KEY; 038 039import com.google.errorprone.annotations.RestrictedApi; 040import java.io.IOException; 041import java.util.ArrayList; 042import java.util.HashMap; 043import java.util.HashSet; 044import java.util.List; 045import java.util.Map; 046import java.util.Set; 047import java.util.UUID; 048import java.util.stream.Collectors; 049import org.apache.hadoop.fs.FileStatus; 050import org.apache.hadoop.fs.FileSystem; 051import org.apache.hadoop.fs.Path; 052import org.apache.hadoop.hbase.HConstants; 053import org.apache.hadoop.hbase.ServerName; 054import org.apache.hadoop.hbase.TableName; 055import org.apache.hadoop.hbase.backup.BackupCopyJob; 056import org.apache.hadoop.hbase.backup.BackupInfo; 057import org.apache.hadoop.hbase.backup.BackupInfo.BackupPhase; 058import org.apache.hadoop.hbase.backup.BackupInfo.BackupState; 059import org.apache.hadoop.hbase.backup.BackupRequest; 060import org.apache.hadoop.hbase.backup.BackupRestoreFactory; 061import org.apache.hadoop.hbase.backup.BackupType; 062import org.apache.hadoop.hbase.backup.util.BackupUtils; 063import org.apache.hadoop.hbase.client.Admin; 064import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor; 065import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 066import org.apache.hadoop.hbase.client.Connection; 067import org.apache.hadoop.hbase.client.TableDescriptor; 068import org.apache.hadoop.hbase.client.TableDescriptorBuilder; 069import org.apache.hadoop.hbase.replication.ReplicationException; 070import org.apache.hadoop.hbase.replication.ReplicationPeerConfig; 071import org.apache.hadoop.hbase.util.CommonFSUtils; 072import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; 073import org.apache.hadoop.hbase.wal.AbstractFSWALProvider; 074import org.apache.yetus.audience.InterfaceAudience; 075import org.slf4j.Logger; 076import org.slf4j.LoggerFactory; 077 078/** 079 * Full table backup implementation 080 */ 081@InterfaceAudience.Private 082public class FullTableBackupClient extends TableBackupClient { 083 private static final Logger LOG = LoggerFactory.getLogger(FullTableBackupClient.class); 084 085 public FullTableBackupClient() { 086 } 087 088 public FullTableBackupClient(final Connection conn, final String backupId, BackupRequest request) 089 throws IOException { 090 super(conn, backupId, request); 091 } 092 093 /** 094 * Do snapshot copy. 095 * @param backupInfo backup info 096 * @throws IOException exception 097 */ 098 protected void snapshotCopy(BackupInfo backupInfo) throws IOException { 099 LOG.info("Snapshot copy is starting."); 100 101 // set overall backup phase: snapshot_copy 102 backupInfo.setPhase(BackupPhase.SNAPSHOTCOPY); 103 104 // call ExportSnapshot to copy files based on hbase snapshot for backup 105 // ExportSnapshot only support single snapshot export, need loop for multiple tables case 106 BackupCopyJob copyService = BackupRestoreFactory.getBackupCopyJob(conf); 107 108 // number of snapshots matches number of tables 109 float numOfSnapshots = backupInfo.getSnapshotNames().size(); 110 111 LOG.debug("There are " + (int) numOfSnapshots + " snapshots to be copied."); 112 113 for (TableName table : backupInfo.getTables()) { 114 // Currently we simply set the sub copy tasks by counting the table snapshot number, we can 115 // calculate the real files' size for the percentage in the future. 116 // backupCopier.setSubTaskPercntgInWholeTask(1f / numOfSnapshots); 117 int res; 118 ArrayList<String> argsList = new ArrayList<>(); 119 argsList.add("-snapshot"); 120 argsList.add(backupInfo.getSnapshotName(table)); 121 argsList.add("-copy-to"); 122 argsList.add(backupInfo.getTableBackupDir(table)); 123 if (backupInfo.getBandwidth() > -1) { 124 argsList.add("-bandwidth"); 125 argsList.add(String.valueOf(backupInfo.getBandwidth())); 126 } 127 if (backupInfo.getWorkers() > -1) { 128 argsList.add("-mappers"); 129 argsList.add(String.valueOf(backupInfo.getWorkers())); 130 } 131 if (backupInfo.getNoChecksumVerify()) { 132 argsList.add("-no-checksum-verify"); 133 } 134 135 String[] args = argsList.toArray(new String[0]); 136 137 String jobname = "Full-Backup_" + backupInfo.getBackupId() + "_" + table.getNameAsString(); 138 if (LOG.isDebugEnabled()) { 139 LOG.debug("Setting snapshot copy job name to : " + jobname); 140 } 141 conf.set(JOB_NAME_CONF_KEY, jobname); 142 143 LOG.debug("Copy snapshot " + args[1] + " to " + args[3]); 144 res = copyService.copy(backupInfo, backupManager, conf, BackupType.FULL, args); 145 146 // if one snapshot export failed, do not continue for remained snapshots 147 if (res != 0) { 148 LOG.error("Exporting Snapshot " + args[1] + " failed with return code: " + res + "."); 149 150 throw new IOException("Failed of exporting snapshot " + args[1] + " to " + args[3] 151 + " with reason code " + res); 152 } 153 154 conf.unset(JOB_NAME_CONF_KEY); 155 LOG.info("Snapshot copy " + args[1] + " finished."); 156 } 157 } 158 159 /** 160 * Backup request execution. 161 * @throws IOException if the execution of the backup fails 162 */ 163 @Override 164 public void execute() throws IOException { 165 try (Admin admin = conn.getAdmin()) { 166 beginBackup(backupManager, backupInfo); 167 168 // Gather the bulk loads being tracked by the system, which can be deleted (since their data 169 // will be part of the snapshot being taken). We gather this list before taking the actual 170 // snapshots for the same reason as the log rolls. 171 List<BulkLoad> bulkLoadsToDelete = backupManager.readBulkloadRows(tableList); 172 173 if (backupInfo.isContinuousBackupEnabled()) { 174 handleContinuousBackup(admin); 175 } else { 176 handleNonContinuousBackup(admin); 177 } 178 179 backupManager 180 .deleteBulkLoadedRows(bulkLoadsToDelete.stream().map(BulkLoad::getRowKey).toList()); 181 182 completeBackup(conn, backupInfo, BackupType.FULL, conf); 183 } catch (Exception e) { 184 failBackup(conn, backupInfo, backupManager, e, "Unexpected BackupException : ", 185 BackupType.FULL, conf); 186 throw new IOException(e); 187 } 188 } 189 190 private void handleContinuousBackup(Admin admin) throws IOException { 191 backupInfo.setPhase(BackupInfo.BackupPhase.SETUP_WAL_REPLICATION); 192 long startTimestamp = startContinuousWALBackup(admin); 193 backupManager.addContinuousBackupTableSet(backupInfo.getTables(), startTimestamp); 194 195 // Updating the start time of this backup to reflect the actual beginning of the full backup. 196 // So far, we have only set up continuous WAL replication, but the full backup has not yet 197 // started. 198 // Setting the correct start time is crucial for Point-In-Time Recovery (PITR). 199 // When selecting a backup for PITR, we must ensure that the backup started **on or after** the 200 // starting time of the WALs. If WAL streaming began later, we couldn't guarantee that WALs 201 // exist for the entire period between the backup's start time and the desired PITR timestamp. 202 backupInfo.setStartTs(startTimestamp); 203 204 performBackupSnapshots(admin); 205 206 // set overall backup status: complete. Here we make sure to complete the backup. 207 // After this checkpoint, even if entering cancel process, will let the backup finished 208 backupInfo.setState(BackupState.COMPLETE); 209 210 if (!conf.getBoolean(REPLICATION_BULKLOAD_ENABLE_KEY, false)) { 211 System.out.println("WARNING: Bulkload replication is not enabled. " 212 + "Since continuous backup is using HBase replication, bulk loaded files won't be backed up as part of continuous backup. " 213 + "To ensure bulk-loaded files are backed up, enable bulkload replication " 214 + "(hbase.replication.bulkload.enabled=true) and configure a unique cluster ID using " 215 + "hbase.replication.cluster.id. This cluster ID is required by the replication framework " 216 + "to uniquely identify clusters, even if continuous backup itself does not directly rely on it."); 217 } 218 } 219 220 private void handleNonContinuousBackup(Admin admin) throws IOException { 221 Map<String, Long> previousLogRollsByHost = backupManager.readRegionServerLastLogRollResult(); 222 Map<String, Long> latestLogRollsByHost = performLogRoll(); 223 Path walRootDir = CommonFSUtils.getWALRootDir(conf); 224 newTimestamps = computeLogBoundaries(walRootDir.getFileSystem(conf), walRootDir, 225 BackupUtils.getRolledHosts(previousLogRollsByHost, latestLogRollsByHost), admin); 226 227 performBackupSnapshots(admin); 228 backupManager.addIncrementalBackupTableSet(backupInfo.getTables()); 229 230 // set overall backup status: complete. Here we make sure to complete the backup. 231 // After this checkpoint, even if entering cancel process, will let the backup finished 232 backupInfo.setState(BackupState.COMPLETE); 233 234 updateBackupMetadata(); 235 } 236 237 private Map<String, Long> performLogRoll() throws IOException { 238 // We roll log here before we do the snapshot. It is possible there is duplicate data 239 // in the log that is already in the snapshot. But if we do it after the snapshot, we 240 // could have data loss. 241 // A better approach is to do the roll log on each RS in the same global procedure as 242 // the snapshot. 243 LOG.info("Execute roll log procedure for full backup ..."); 244 BackupUtils.logRoll(conn, backupInfo.getBackupRootDir(), conf); 245 return backupManager.readRegionServerLastLogRollResult(); 246 } 247 248 private void performBackupSnapshots(Admin admin) throws IOException { 249 backupInfo.setPhase(BackupPhase.SNAPSHOT); 250 performSnapshots(admin); 251 LOG.debug("Performing snapshot copy for backup ID: {}", backupInfo.getBackupId()); 252 snapshotCopy(backupInfo); 253 } 254 255 private void performSnapshots(Admin admin) throws IOException { 256 backupInfo.setPhase(BackupPhase.SNAPSHOT); 257 258 for (TableName tableName : tableList) { 259 String snapshotName = String.format("snapshot_%d_%s_%s", EnvironmentEdgeManager.currentTime(), 260 tableName.getNamespaceAsString(), tableName.getQualifierAsString()); 261 snapshotTable(admin, tableName, snapshotName); 262 backupInfo.setSnapshotName(tableName, snapshotName); 263 } 264 } 265 266 /** 267 * Computes the per-host log boundaries stored by this full backup, so that the next incremental 268 * backup includes exactly the WALs whose edits may be missing from the snapshot. It must run 269 * right after the log roll, before the snapshot is taken, so that a host starting later gets no 270 * boundary and has all of its WALs included in the next incremental backup. 271 * <p> 272 * Boundaries are computed by {@link BackupUtils#computeLogBoundaries}: 273 * <ul> 274 * <li>A host that took part in this backup's log roll gets its roll result. WALs created after 275 * the roll will be included in the next incremental backup, even though some of their edits may 276 * already be in the snapshot, which is safe because deletes are replayed along with the 277 * puts.</li> 278 * <li>For any other host, its archived WALs and the WALs of dead servers are covered by the 279 * snapshot, so the next incremental backup does not replay them.</li> 280 * <li>The WALs of a live server that did not take part in the roll (for example one that started 281 * during it) are pending, because they can still receive edits that are not in the snapshot.</li> 282 * </ul> 283 * If the master's server list is incomplete (for example right after a master failover), the 284 * backup fails instead of treating live servers as dead. 285 */ 286 @RestrictedApi( 287 explanation = "Package-private for test visibility only. Do not use outside tests.", 288 link = "", 289 allowedOnPath = "(.*/src/test/.*|.*/org/apache/hadoop/hbase/backup/impl/FullTableBackupClient.java)") 290 static Map<String, Long> computeLogBoundaries(FileSystem fs, Path walRootDir, 291 Map<String, Long> rolledHosts, Admin admin) throws IOException { 292 Path logDir = new Path(walRootDir, HConstants.HREGION_LOGDIR_NAME); 293 Path oldLogDir = new Path(walRootDir, HConstants.HREGION_OLDLOGDIR_NAME); 294 295 Map<ServerName, List<String>> logsByServer = new HashMap<>(); 296 for (FileStatus serverLogDir : fs.listStatus(logDir)) { 297 ServerName serverName = 298 AbstractFSWALProvider.getServerNameFromWALDirectoryName(serverLogDir.getPath()); 299 if (serverName == null) { 300 continue; 301 } 302 List<String> logs = logsByServer.computeIfAbsent(serverName, k -> new ArrayList<>()); 303 for (FileStatus log : fs.listStatus(serverLogDir.getPath())) { 304 if (!AbstractFSWALProvider.isMetaFile(log.getPath())) { 305 logs.add(log.getPath().toString()); 306 } 307 } 308 } 309 310 // The live servers are read only after the WAL directories are listed: a region server creates 311 // its WAL directory only after the master registers it, so every live server whose directory 312 // was listed is found. If the live servers do not include every host that took part in the 313 // roll, the master's server list is incomplete, or a host stopped after the roll. 314 Set<ServerName> live = new HashSet<>(admin.getRegionServers()); 315 Set<String> liveAddresses = 316 live.stream().map(sn -> sn.getAddress().toString()).collect(Collectors.toSet()); 317 if (!liveAddresses.containsAll(rolledHosts.keySet())) { 318 throw new IOException("Live region servers " + live + " do not include every host that took" 319 + " part in this backup's log roll " + rolledHosts.keySet() + ". The master's server list" 320 + " may be incomplete, for example during a master failover, or a region server stopped" 321 + " after the log roll, so the backup is failed to be retried."); 322 } 323 324 List<String> coveredLogs = new ArrayList<>(); 325 List<String> pendingLogs = new ArrayList<>(); 326 for (Map.Entry<ServerName, List<String>> entry : logsByServer.entrySet()) { 327 if (live.contains(entry.getKey())) { 328 pendingLogs.addAll(entry.getValue()); 329 } else { 330 coveredLogs.addAll(entry.getValue()); 331 } 332 } 333 334 if (fs.exists(oldLogDir)) { 335 BackupUtils.getFiles(fs, oldLogDir, coveredLogs, 336 path -> !AbstractFSWALProvider.isMetaFile(path)); 337 } 338 339 return BackupUtils.computeLogBoundaries(rolledHosts, coveredLogs, pendingLogs); 340 } 341 342 private void updateBackupMetadata() throws IOException { 343 // The table list in backupInfo is good for both full backup and incremental backup. 344 // For incremental backup, it contains the incremental backup table set. 345 backupManager.writeRegionServerLogTimestamp(backupInfo.getTables(), newTimestamps); 346 Map<TableName, Map<String, Long>> timestampMap = backupManager.readLogTimestampMap(); 347 backupInfo.setTableSetTimestampMap(timestampMap); 348 } 349 350 private long startContinuousWALBackup(Admin admin) throws IOException { 351 enableTableReplication(admin); 352 if (continuousBackupReplicationPeerExists(admin)) { 353 updateContinuousBackupReplicationPeer(admin); 354 } else { 355 addContinuousBackupReplicationPeer(admin); 356 } 357 LOG.info("Continuous WAL Backup setup completed."); 358 return EnvironmentEdgeManager.getDelegate().currentTime(); 359 } 360 361 private void enableTableReplication(Admin admin) throws IOException { 362 for (TableName table : tableList) { 363 TableDescriptor tableDescriptor = admin.getDescriptor(table); 364 TableDescriptorBuilder tableDescriptorBuilder = 365 TableDescriptorBuilder.newBuilder(tableDescriptor); 366 367 for (ColumnFamilyDescriptor cfDescriptor : tableDescriptor.getColumnFamilies()) { 368 if (cfDescriptor.getScope() != REPLICATION_SCOPE_GLOBAL) { 369 ColumnFamilyDescriptor newCfDescriptor = ColumnFamilyDescriptorBuilder 370 .newBuilder(cfDescriptor).setScope(REPLICATION_SCOPE_GLOBAL).build(); 371 372 tableDescriptorBuilder.modifyColumnFamily(newCfDescriptor); 373 } 374 } 375 376 admin.modifyTable(tableDescriptorBuilder.build()); 377 LOG.info("Enabled Global replication scope for table: {}", table); 378 } 379 } 380 381 private void updateContinuousBackupReplicationPeer(Admin admin) throws IOException { 382 Map<TableName, List<String>> tableMap = tableList.stream() 383 .collect(Collectors.toMap(tableName -> tableName, tableName -> new ArrayList<>())); 384 385 try { 386 if (!admin.isReplicationPeerEnabled(CONTINUOUS_BACKUP_REPLICATION_PEER)) { 387 admin.enableReplicationPeer(CONTINUOUS_BACKUP_REPLICATION_PEER); 388 } 389 admin.appendReplicationPeerTableCFs(CONTINUOUS_BACKUP_REPLICATION_PEER, tableMap); 390 LOG.info("Updated replication peer {} with table and column family map.", 391 CONTINUOUS_BACKUP_REPLICATION_PEER); 392 } catch (ReplicationException e) { 393 LOG.error("Error while updating the replication peer: {}. Error: {}", 394 CONTINUOUS_BACKUP_REPLICATION_PEER, e.getMessage(), e); 395 throw new IOException("Error while updating the continuous backup replication peer.", e); 396 } 397 } 398 399 private void addContinuousBackupReplicationPeer(Admin admin) throws IOException { 400 String backupWalDir = conf.get(CONF_CONTINUOUS_BACKUP_WAL_DIR); 401 402 if (backupWalDir == null || backupWalDir.isEmpty()) { 403 String errorMsg = "WAL Directory is not specified for continuous backup."; 404 LOG.error(errorMsg); 405 throw new IOException(errorMsg); 406 } 407 408 Map<String, String> additionalArgs = new HashMap<>(); 409 additionalArgs.put(CONF_PEER_UUID, UUID.randomUUID().toString()); 410 additionalArgs.put(CONF_BACKUP_ROOT_DIR, backupWalDir); 411 additionalArgs.put(OFFSET_UPDATE_INTERVAL_MS_KEY, 412 String.valueOf(conf.getLong(CONTINUOUS_BACKUP_OFFSET_UPDATE_INTERVAL_MS, 413 DEFAULT_CONTINUOUS_BACKUP_OFFSET_UPDATE_INTERVAL_MS))); 414 415 additionalArgs.put(OFFSET_UPDATE_SIZE_THRESHOLD_KEY, 416 String.valueOf(conf.getLong(CONTINUOUS_BACKUP_OFFSET_UPDATE_SIZE_THRESHOLD, 417 DEFAULT_CONTINUOUS_BACKUP_OFFSET_UPDATE_SIZE_THRESHOLD))); 418 419 Map<TableName, List<String>> tableMap = tableList.stream() 420 .collect(Collectors.toMap(tableName -> tableName, tableName -> new ArrayList<>())); 421 422 ReplicationPeerConfig peerConfig = ReplicationPeerConfig.newBuilder() 423 .setReplicationEndpointImpl(DEFAULT_CONTINUOUS_BACKUP_REPLICATION_ENDPOINT) 424 .setReplicateAllUserTables(false).setTableCFsMap(tableMap).putAllConfiguration(additionalArgs) 425 .build(); 426 427 try { 428 admin.addReplicationPeer(CONTINUOUS_BACKUP_REPLICATION_PEER, peerConfig, true); 429 LOG.info("Successfully added replication peer with ID: {}", 430 CONTINUOUS_BACKUP_REPLICATION_PEER); 431 } catch (IOException e) { 432 LOG.error("Failed to add replication peer with ID: {}. Error: {}", 433 CONTINUOUS_BACKUP_REPLICATION_PEER, e.getMessage(), e); 434 throw e; 435 } 436 } 437 438 private boolean continuousBackupReplicationPeerExists(Admin admin) throws IOException { 439 return admin.listReplicationPeers().stream() 440 .anyMatch(peer -> peer.getPeerId().equals(CONTINUOUS_BACKUP_REPLICATION_PEER)); 441 } 442 443 protected void snapshotTable(Admin admin, TableName tableName, String snapshotName) 444 throws IOException { 445 int maxAttempts = conf.getInt(BACKUP_MAX_ATTEMPTS_KEY, DEFAULT_BACKUP_MAX_ATTEMPTS); 446 int pause = conf.getInt(BACKUP_ATTEMPTS_PAUSE_MS_KEY, DEFAULT_BACKUP_ATTEMPTS_PAUSE_MS); 447 int attempts = 0; 448 449 while (attempts++ < maxAttempts) { 450 try { 451 admin.snapshot(snapshotName, tableName); 452 return; 453 } catch (IOException ee) { 454 LOG.warn("Snapshot attempt " + attempts + " failed for table " + tableName 455 + ", sleeping for " + pause + "ms", ee); 456 if (attempts < maxAttempts) { 457 try { 458 Thread.sleep(pause); 459 } catch (InterruptedException e) { 460 Thread.currentThread().interrupt(); 461 break; 462 } 463 } 464 } 465 } 466 throw new IOException("Failed to snapshot table " + tableName); 467 } 468}