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 java.io.IOException;
040import java.util.ArrayList;
041import java.util.HashMap;
042import java.util.List;
043import java.util.Map;
044import java.util.UUID;
045import java.util.stream.Collectors;
046import org.apache.hadoop.hbase.TableName;
047import org.apache.hadoop.hbase.backup.BackupCopyJob;
048import org.apache.hadoop.hbase.backup.BackupInfo;
049import org.apache.hadoop.hbase.backup.BackupInfo.BackupPhase;
050import org.apache.hadoop.hbase.backup.BackupInfo.BackupState;
051import org.apache.hadoop.hbase.backup.BackupRequest;
052import org.apache.hadoop.hbase.backup.BackupRestoreFactory;
053import org.apache.hadoop.hbase.backup.BackupType;
054import org.apache.hadoop.hbase.backup.util.BackupUtils;
055import org.apache.hadoop.hbase.client.Admin;
056import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
057import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
058import org.apache.hadoop.hbase.client.Connection;
059import org.apache.hadoop.hbase.client.TableDescriptor;
060import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
061import org.apache.hadoop.hbase.replication.ReplicationException;
062import org.apache.hadoop.hbase.replication.ReplicationPeerConfig;
063import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
064import org.apache.yetus.audience.InterfaceAudience;
065import org.slf4j.Logger;
066import org.slf4j.LoggerFactory;
067
068/**
069 * Full table backup implementation
070 */
071@InterfaceAudience.Private
072public class FullTableBackupClient extends TableBackupClient {
073  private static final Logger LOG = LoggerFactory.getLogger(FullTableBackupClient.class);
074
075  public FullTableBackupClient() {
076  }
077
078  public FullTableBackupClient(final Connection conn, final String backupId, BackupRequest request)
079    throws IOException {
080    super(conn, backupId, request);
081  }
082
083  /**
084   * Do snapshot copy.
085   * @param backupInfo backup info
086   * @throws IOException exception
087   */
088  protected void snapshotCopy(BackupInfo backupInfo) throws IOException {
089    LOG.info("Snapshot copy is starting.");
090
091    // set overall backup phase: snapshot_copy
092    backupInfo.setPhase(BackupPhase.SNAPSHOTCOPY);
093
094    // call ExportSnapshot to copy files based on hbase snapshot for backup
095    // ExportSnapshot only support single snapshot export, need loop for multiple tables case
096    BackupCopyJob copyService = BackupRestoreFactory.getBackupCopyJob(conf);
097
098    // number of snapshots matches number of tables
099    float numOfSnapshots = backupInfo.getSnapshotNames().size();
100
101    LOG.debug("There are " + (int) numOfSnapshots + " snapshots to be copied.");
102
103    for (TableName table : backupInfo.getTables()) {
104      // Currently we simply set the sub copy tasks by counting the table snapshot number, we can
105      // calculate the real files' size for the percentage in the future.
106      // backupCopier.setSubTaskPercntgInWholeTask(1f / numOfSnapshots);
107      int res;
108      ArrayList<String> argsList = new ArrayList<>();
109      argsList.add("-snapshot");
110      argsList.add(backupInfo.getSnapshotName(table));
111      argsList.add("-copy-to");
112      argsList.add(backupInfo.getTableBackupDir(table));
113      if (backupInfo.getBandwidth() > -1) {
114        argsList.add("-bandwidth");
115        argsList.add(String.valueOf(backupInfo.getBandwidth()));
116      }
117      if (backupInfo.getWorkers() > -1) {
118        argsList.add("-mappers");
119        argsList.add(String.valueOf(backupInfo.getWorkers()));
120      }
121      if (backupInfo.getNoChecksumVerify()) {
122        argsList.add("-no-checksum-verify");
123      }
124
125      String[] args = argsList.toArray(new String[0]);
126
127      String jobname = "Full-Backup_" + backupInfo.getBackupId() + "_" + table.getNameAsString();
128      if (LOG.isDebugEnabled()) {
129        LOG.debug("Setting snapshot copy job name to : " + jobname);
130      }
131      conf.set(JOB_NAME_CONF_KEY, jobname);
132
133      LOG.debug("Copy snapshot " + args[1] + " to " + args[3]);
134      res = copyService.copy(backupInfo, backupManager, conf, BackupType.FULL, args);
135
136      // if one snapshot export failed, do not continue for remained snapshots
137      if (res != 0) {
138        LOG.error("Exporting Snapshot " + args[1] + " failed with return code: " + res + ".");
139
140        throw new IOException("Failed of exporting snapshot " + args[1] + " to " + args[3]
141          + " with reason code " + res);
142      }
143
144      conf.unset(JOB_NAME_CONF_KEY);
145      LOG.info("Snapshot copy " + args[1] + " finished.");
146    }
147  }
148
149  /**
150   * Backup request execution.
151   * @throws IOException if the execution of the backup fails
152   */
153  @Override
154  public void execute() throws IOException {
155    try (Admin admin = conn.getAdmin()) {
156      beginBackup(backupManager, backupInfo);
157
158      // Gather the bulk loads being tracked by the system, which can be deleted (since their data
159      // will be part of the snapshot being taken). We gather this list before taking the actual
160      // snapshots for the same reason as the log rolls.
161      List<BulkLoad> bulkLoadsToDelete = backupManager.readBulkloadRows(tableList);
162
163      if (backupInfo.isContinuousBackupEnabled()) {
164        handleContinuousBackup(admin);
165      } else {
166        handleNonContinuousBackup(admin);
167      }
168
169      backupManager
170        .deleteBulkLoadedRows(bulkLoadsToDelete.stream().map(BulkLoad::getRowKey).toList());
171
172      completeBackup(conn, backupInfo, BackupType.FULL, conf);
173    } catch (Exception e) {
174      failBackup(conn, backupInfo, backupManager, e, "Unexpected BackupException : ",
175        BackupType.FULL, conf);
176      throw new IOException(e);
177    }
178  }
179
180  private void handleContinuousBackup(Admin admin) throws IOException {
181    backupInfo.setPhase(BackupInfo.BackupPhase.SETUP_WAL_REPLICATION);
182    long startTimestamp = startContinuousWALBackup(admin);
183    backupManager.addContinuousBackupTableSet(backupInfo.getTables(), startTimestamp);
184
185    // Updating the start time of this backup to reflect the actual beginning of the full backup.
186    // So far, we have only set up continuous WAL replication, but the full backup has not yet
187    // started.
188    // Setting the correct start time is crucial for Point-In-Time Recovery (PITR).
189    // When selecting a backup for PITR, we must ensure that the backup started **on or after** the
190    // starting time of the WALs. If WAL streaming began later, we couldn't guarantee that WALs
191    // exist for the entire period between the backup's start time and the desired PITR timestamp.
192    backupInfo.setStartTs(startTimestamp);
193
194    performBackupSnapshots(admin);
195
196    // set overall backup status: complete. Here we make sure to complete the backup.
197    // After this checkpoint, even if entering cancel process, will let the backup finished
198    backupInfo.setState(BackupState.COMPLETE);
199
200    if (!conf.getBoolean(REPLICATION_BULKLOAD_ENABLE_KEY, false)) {
201      System.out.println("WARNING: Bulkload replication is not enabled. "
202        + "Since continuous backup is using HBase replication, bulk loaded files won't be backed up as part of continuous backup. "
203        + "To ensure bulk-loaded files are backed up, enable bulkload replication "
204        + "(hbase.replication.bulkload.enabled=true) and configure a unique cluster ID using "
205        + "hbase.replication.cluster.id. This cluster ID is required by the replication framework "
206        + "to uniquely identify clusters, even if continuous backup itself does not directly rely on it.");
207    }
208  }
209
210  private void handleNonContinuousBackup(Admin admin) throws IOException {
211    performLogRoll();
212    performBackupSnapshots(admin);
213    backupManager.addIncrementalBackupTableSet(backupInfo.getTables());
214
215    // set overall backup status: complete. Here we make sure to complete the backup.
216    // After this checkpoint, even if entering cancel process, will let the backup finished
217    backupInfo.setState(BackupState.COMPLETE);
218
219    updateBackupMetadata();
220  }
221
222  private void performLogRoll() throws IOException {
223    // We roll log here before we do the snapshot. It is possible there is duplicate data
224    // in the log that is already in the snapshot. But if we do it after the snapshot, we
225    // could have data loss.
226    // A better approach is to do the roll log on each RS in the same global procedure as
227    // the snapshot.
228    LOG.info("Execute roll log procedure for full backup ...");
229    BackupUtils.logRoll(conn, backupInfo.getBackupRootDir(), conf);
230    newTimestamps = backupManager.readRegionServerLastLogRollResult();
231  }
232
233  private void performBackupSnapshots(Admin admin) throws IOException {
234    backupInfo.setPhase(BackupPhase.SNAPSHOT);
235    performSnapshots(admin);
236    LOG.debug("Performing snapshot copy for backup ID: {}", backupInfo.getBackupId());
237    snapshotCopy(backupInfo);
238  }
239
240  private void performSnapshots(Admin admin) throws IOException {
241    backupInfo.setPhase(BackupPhase.SNAPSHOT);
242
243    for (TableName tableName : tableList) {
244      String snapshotName = String.format("snapshot_%d_%s_%s", EnvironmentEdgeManager.currentTime(),
245        tableName.getNamespaceAsString(), tableName.getQualifierAsString());
246      snapshotTable(admin, tableName, snapshotName);
247      backupInfo.setSnapshotName(tableName, snapshotName);
248    }
249  }
250
251  private void updateBackupMetadata() throws IOException {
252    // The table list in backupInfo is good for both full backup and incremental backup.
253    // For incremental backup, it contains the incremental backup table set.
254    backupManager.writeRegionServerLogTimestamp(backupInfo.getTables(), newTimestamps);
255    Map<TableName, Map<String, Long>> timestampMap = backupManager.readLogTimestampMap();
256    backupInfo.setTableSetTimestampMap(timestampMap);
257  }
258
259  private long startContinuousWALBackup(Admin admin) throws IOException {
260    enableTableReplication(admin);
261    if (continuousBackupReplicationPeerExists(admin)) {
262      updateContinuousBackupReplicationPeer(admin);
263    } else {
264      addContinuousBackupReplicationPeer(admin);
265    }
266    LOG.info("Continuous WAL Backup setup completed.");
267    return EnvironmentEdgeManager.getDelegate().currentTime();
268  }
269
270  private void enableTableReplication(Admin admin) throws IOException {
271    for (TableName table : tableList) {
272      TableDescriptor tableDescriptor = admin.getDescriptor(table);
273      TableDescriptorBuilder tableDescriptorBuilder =
274        TableDescriptorBuilder.newBuilder(tableDescriptor);
275
276      for (ColumnFamilyDescriptor cfDescriptor : tableDescriptor.getColumnFamilies()) {
277        if (cfDescriptor.getScope() != REPLICATION_SCOPE_GLOBAL) {
278          ColumnFamilyDescriptor newCfDescriptor = ColumnFamilyDescriptorBuilder
279            .newBuilder(cfDescriptor).setScope(REPLICATION_SCOPE_GLOBAL).build();
280
281          tableDescriptorBuilder.modifyColumnFamily(newCfDescriptor);
282        }
283      }
284
285      admin.modifyTable(tableDescriptorBuilder.build());
286      LOG.info("Enabled Global replication scope for table: {}", table);
287    }
288  }
289
290  private void updateContinuousBackupReplicationPeer(Admin admin) throws IOException {
291    Map<TableName, List<String>> tableMap = tableList.stream()
292      .collect(Collectors.toMap(tableName -> tableName, tableName -> new ArrayList<>()));
293
294    try {
295      if (!admin.isReplicationPeerEnabled(CONTINUOUS_BACKUP_REPLICATION_PEER)) {
296        admin.enableReplicationPeer(CONTINUOUS_BACKUP_REPLICATION_PEER);
297      }
298      admin.appendReplicationPeerTableCFs(CONTINUOUS_BACKUP_REPLICATION_PEER, tableMap);
299      LOG.info("Updated replication peer {} with table and column family map.",
300        CONTINUOUS_BACKUP_REPLICATION_PEER);
301    } catch (ReplicationException e) {
302      LOG.error("Error while updating the replication peer: {}. Error: {}",
303        CONTINUOUS_BACKUP_REPLICATION_PEER, e.getMessage(), e);
304      throw new IOException("Error while updating the continuous backup replication peer.", e);
305    }
306  }
307
308  private void addContinuousBackupReplicationPeer(Admin admin) throws IOException {
309    String backupWalDir = conf.get(CONF_CONTINUOUS_BACKUP_WAL_DIR);
310
311    if (backupWalDir == null || backupWalDir.isEmpty()) {
312      String errorMsg = "WAL Directory is not specified for continuous backup.";
313      LOG.error(errorMsg);
314      throw new IOException(errorMsg);
315    }
316
317    Map<String, String> additionalArgs = new HashMap<>();
318    additionalArgs.put(CONF_PEER_UUID, UUID.randomUUID().toString());
319    additionalArgs.put(CONF_BACKUP_ROOT_DIR, backupWalDir);
320    additionalArgs.put(OFFSET_UPDATE_INTERVAL_MS_KEY,
321      String.valueOf(conf.getLong(CONTINUOUS_BACKUP_OFFSET_UPDATE_INTERVAL_MS,
322        DEFAULT_CONTINUOUS_BACKUP_OFFSET_UPDATE_INTERVAL_MS)));
323
324    additionalArgs.put(OFFSET_UPDATE_SIZE_THRESHOLD_KEY,
325      String.valueOf(conf.getLong(CONTINUOUS_BACKUP_OFFSET_UPDATE_SIZE_THRESHOLD,
326        DEFAULT_CONTINUOUS_BACKUP_OFFSET_UPDATE_SIZE_THRESHOLD)));
327
328    Map<TableName, List<String>> tableMap = tableList.stream()
329      .collect(Collectors.toMap(tableName -> tableName, tableName -> new ArrayList<>()));
330
331    ReplicationPeerConfig peerConfig = ReplicationPeerConfig.newBuilder()
332      .setReplicationEndpointImpl(DEFAULT_CONTINUOUS_BACKUP_REPLICATION_ENDPOINT)
333      .setReplicateAllUserTables(false).setTableCFsMap(tableMap).putAllConfiguration(additionalArgs)
334      .build();
335
336    try {
337      admin.addReplicationPeer(CONTINUOUS_BACKUP_REPLICATION_PEER, peerConfig, true);
338      LOG.info("Successfully added replication peer with ID: {}",
339        CONTINUOUS_BACKUP_REPLICATION_PEER);
340    } catch (IOException e) {
341      LOG.error("Failed to add replication peer with ID: {}. Error: {}",
342        CONTINUOUS_BACKUP_REPLICATION_PEER, e.getMessage(), e);
343      throw e;
344    }
345  }
346
347  private boolean continuousBackupReplicationPeerExists(Admin admin) throws IOException {
348    return admin.listReplicationPeers().stream()
349      .anyMatch(peer -> peer.getPeerId().equals(CONTINUOUS_BACKUP_REPLICATION_PEER));
350  }
351
352  protected void snapshotTable(Admin admin, TableName tableName, String snapshotName)
353    throws IOException {
354    int maxAttempts = conf.getInt(BACKUP_MAX_ATTEMPTS_KEY, DEFAULT_BACKUP_MAX_ATTEMPTS);
355    int pause = conf.getInt(BACKUP_ATTEMPTS_PAUSE_MS_KEY, DEFAULT_BACKUP_ATTEMPTS_PAUSE_MS);
356    int attempts = 0;
357
358    while (attempts++ < maxAttempts) {
359      try {
360        admin.snapshot(snapshotName, tableName);
361        return;
362      } catch (IOException ee) {
363        LOG.warn("Snapshot attempt " + attempts + " failed for table " + tableName
364          + ", sleeping for " + pause + "ms", ee);
365        if (attempts < maxAttempts) {
366          try {
367            Thread.sleep(pause);
368          } catch (InterruptedException e) {
369            Thread.currentThread().interrupt();
370            break;
371          }
372        }
373      }
374    }
375    throw new IOException("Failed to snapshot table " + tableName);
376  }
377}