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}