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 java.io.IOException;
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027import org.apache.hadoop.conf.Configuration;
028import org.apache.hadoop.fs.FileStatus;
029import org.apache.hadoop.fs.FileSystem;
030import org.apache.hadoop.fs.Path;
031import org.apache.hadoop.fs.PathFilter;
032import org.apache.hadoop.hbase.HConstants;
033import org.apache.hadoop.hbase.TableName;
034import org.apache.hadoop.hbase.backup.util.BackupUtils;
035import org.apache.hadoop.hbase.client.Connection;
036import org.apache.hadoop.hbase.procedure2.store.wal.WALProcedureStore;
037import org.apache.hadoop.hbase.util.CommonFSUtils;
038import org.apache.hadoop.hbase.wal.AbstractFSWALProvider;
039import org.apache.yetus.audience.InterfaceAudience;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042
043/**
044 * After a full backup was created, the incremental backup will only store the changes made after
045 * the last full or incremental backup. Creating the backup copies the logfiles in .logs and
046 * .oldlogs since the last backup timestamp.
047 */
048@InterfaceAudience.Private
049public class IncrementalBackupManager extends BackupManager {
050  public static final Logger LOG = LoggerFactory.getLogger(IncrementalBackupManager.class);
051
052  public IncrementalBackupManager(Connection conn, Configuration conf) throws IOException {
053    super(conn, conf);
054  }
055
056  /**
057   * Obtain the list of logs that need to be copied out for this incremental backup. The list is set
058   * in BackupInfo.
059   * @return The new map of RS log time stamps for this incremental backup, as computed by
060   *         {@link BackupUtils#computeLogBoundaries}: the included logs are covered, the logs held
061   *         back for a later backup (for example, logs still being split) are pending, and a host
062   *         that still has logs keeps its previous boundary if this backup gives it no new one.
063   * @throws IOException exception
064   */
065  public Map<String, Long> getIncrBackupLogFileMap() throws IOException {
066    List<String> logList;
067    Map<String, Long> previousTimestampMins =
068      BackupUtils.getRSLogTimestampMins(readLogTimestampMap());
069
070    // get all new log files from .logs and .oldlogs after last TS and before new timestamp
071    if (previousTimestampMins.isEmpty()) {
072      throw new IOException("Cannot read any previous back up timestamps from backup system table. "
073        + "In order to create an incremental backup, at least one full backup is needed.");
074    }
075
076    Map<String, Long> previousLogRollByHost = readRegionServerLastLogRollResult();
077    LOG.info("Execute roll log procedure for incremental backup ...");
078    BackupUtils.logRoll(conn, backupInfo.getBackupRootDir(), conf);
079
080    Map<String, Long> rolledHosts =
081      BackupUtils.getRolledHosts(previousLogRollByHost, readRegionServerLastLogRollResult());
082
083    LogFileSelection selection = getLogFilesForNewBackup(previousTimestampMins, rolledHosts, conf);
084    logList = excludeProcV2WALs(selection.included());
085    backupInfo.setIncrBackupFileList(logList);
086
087    Map<String, Long> newTimestamps = BackupUtils.computeLogBoundaries(rolledHosts,
088      previousTimestampMins, selection.hostsWithLogs(), logList, selection.heldBack());
089    LOG.debug("Log boundaries for incremental backup {}: {}", backupInfo.getBackupId(),
090      newTimestamps);
091    return newTimestamps;
092  }
093
094  private record LogFileSelection(List<String> included, List<String> heldBack,
095    Set<String> hostsWithLogs) {
096  }
097
098  private List<String> excludeProcV2WALs(List<String> logList) {
099    List<String> list = new ArrayList<>();
100    for (int i = 0; i < logList.size(); i++) {
101      Path p = new Path(logList.get(i));
102      String name = p.getName();
103
104      if (name.startsWith(WALProcedureStore.LOG_PREFIX)) {
105        continue;
106      }
107
108      list.add(logList.get(i));
109    }
110    return list;
111  }
112
113  /**
114   * Gather all log files that either: 1) are newer than the older timestamps, but not newer than
115   * the newest timestamps, or 2) are archived logs whose host name does not occur in the newest
116   * timestamps.
117   * @param olderTimestamps  the timestamp for each region server of the last backup.
118   * @param newestTimestamps the timestamp for each region server that the backup should lead to.
119   * @param conf             the Hadoop and Hbase configuration
120   * @return the log files to be backed up, the log files held back for a later backup, and the
121   *         hosts that have any log files, including ones not backed up
122   * @throws IOException exception
123   */
124  private LogFileSelection getLogFilesForNewBackup(Map<String, Long> olderTimestamps,
125    Map<String, Long> newestTimestamps, Configuration conf) throws IOException {
126    LOG.debug("In getLogFilesForNewBackup()\n" + "olderTimestamps: " + olderTimestamps
127      + "\n newestTimestamps: " + newestTimestamps);
128
129    long prevBackupStartTs = Collections.min(olderTimestamps.values());
130    Path walRootDir = CommonFSUtils.getWALRootDir(conf);
131    Path logDir = new Path(walRootDir, HConstants.HREGION_LOGDIR_NAME);
132    Path oldLogDir = new Path(walRootDir, HConstants.HREGION_OLDLOGDIR_NAME);
133    FileSystem fs = walRootDir.getFileSystem(conf);
134    NewestLogFilter pathFilter = new NewestLogFilter();
135
136    List<String> resultLogFiles = new ArrayList<>();
137    List<String> newestLogs = new ArrayList<>();
138    Set<String> hostsWithLogs = new HashSet<>();
139
140    /*
141     * The old region servers and timestamps info we kept in backup system table may be out of sync
142     * if new region server is added or existing one lost. We'll deal with it here when processing
143     * the logs. If data in backup system table has more hosts, just ignore it. If the .logs
144     * directory includes more hosts, the additional hosts will not have old timestamps to compare
145     * with. We'll just use all the logs in that directory. We always write up-to-date region server
146     * and timestamp info to backup system table at the end of successful backup.
147     */
148    FileStatus[] rss;
149    Path p;
150    String host;
151    Long oldTimeStamp;
152    String currentLogFile;
153    long currentLogTS;
154
155    // Get the files in .logs.
156    rss = fs.listStatus(logDir);
157    for (FileStatus rs : rss) {
158      p = rs.getPath();
159      host = BackupUtils.parseHostNameFromLogFile(p);
160      if (host == null) {
161        continue;
162      }
163      hostsWithLogs.add(host);
164      FileStatus[] logs;
165      oldTimeStamp = olderTimestamps.get(host);
166      // It is possible that there is no old timestamp in backup system table for this host if
167      // this region server is newly added after our last backup.
168      if (oldTimeStamp == null) {
169        logs = fs.listStatus(p);
170      } else {
171        pathFilter.setLastBackupTS(oldTimeStamp);
172        logs = fs.listStatus(p, pathFilter);
173      }
174      for (FileStatus log : logs) {
175        LOG.debug("currentLogFile: " + log.getPath().toString());
176        if (AbstractFSWALProvider.isMetaFile(log.getPath())) {
177          if (LOG.isDebugEnabled()) {
178            LOG.debug("Skip {} log file: {}", TableName.META_TABLE_NAME, log.getPath().getName());
179          }
180          continue;
181        }
182        currentLogFile = log.getPath().toString();
183        resultLogFiles.add(currentLogFile);
184        currentLogTS = BackupUtils.getCreationTime(log.getPath());
185
186        // A host without a roll result did not take part in this backup's log roll: it is offline
187        // (for example a dead server whose logs are still being split), or it started after the
188        // roll. Its logs are held back, like logs newer than the roll result of a host that took
189        // part. BackupUtils#computeLogBoundaries keeps the boundary of the host below held-back
190        // logs, so a later backup includes them.
191        Long ts = newestTimestamps.get(host);
192        if (ts == null) {
193          LOG.debug("Holding back log {} of host {}, which did not take part in the log roll",
194            log.getPath(), host);
195        }
196        if (ts == null || currentLogTS > ts) {
197          newestLogs.add(currentLogFile);
198        }
199      }
200    }
201
202    // Include the .oldlogs files too.
203    List<String> oldlogs = BackupUtils.getFiles(fs, oldLogDir, new ArrayList<>(), path -> true);
204    for (String oldlog : oldlogs) {
205      p = new Path(oldlog);
206      currentLogFile = oldlog;
207      if (AbstractFSWALProvider.isMetaFile(p)) {
208        if (LOG.isDebugEnabled()) {
209          LOG.debug("Skip .meta log file: " + currentLogFile);
210        }
211        continue;
212      }
213      host = BackupUtils.parseHostFromOldLog(p);
214      if (host == null) {
215        continue;
216      }
217      hostsWithLogs.add(host);
218      currentLogTS = BackupUtils.getCreationTime(p);
219      oldTimeStamp = olderTimestamps.get(host);
220      /*
221       * It is possible that there is no old timestamp in backup system table for this host. At the
222       * time of our last backup operation, this rs did not exist. The reason can be one of the two:
223       * 1. The rs already left/crashed. Its logs were moved to .oldlogs. 2. The rs was added after
224       * our last backup.
225       */
226      if (oldTimeStamp == null) {
227        if (currentLogTS < prevBackupStartTs) {
228          // This log file is really old, its region server was before our last backup.
229          continue;
230        } else {
231          resultLogFiles.add(currentLogFile);
232        }
233      } else if (currentLogTS > oldTimeStamp) {
234        resultLogFiles.add(currentLogFile);
235      }
236
237      Long newTimestamp = newestTimestamps.get(host);
238      if (newTimestamp != null && currentLogTS > newTimestamp) {
239        newestLogs.add(currentLogFile);
240      }
241    }
242    // Held-back logs are newer than the roll result of their host, or belong to a host that did not
243    // take part in the log roll. A later backup includes them.
244    resultLogFiles.removeAll(newestLogs);
245    return new LogFileSelection(resultLogFiles, newestLogs, hostsWithLogs);
246  }
247
248  static class NewestLogFilter implements PathFilter {
249    private Long lastBackupTS = 0L;
250
251    public NewestLogFilter() {
252    }
253
254    protected void setLastBackupTS(Long ts) {
255      this.lastBackupTS = ts;
256    }
257
258    @Override
259    public boolean accept(Path path) {
260      // skip meta table log -- ts.meta file
261      if (AbstractFSWALProvider.isMetaFile(path)) {
262        if (LOG.isDebugEnabled()) {
263          LOG.debug("Skip .meta log file: " + path.getName());
264        }
265        return false;
266      }
267      long timestamp;
268      try {
269        timestamp = BackupUtils.getCreationTime(path);
270        return timestamp > lastBackupTS;
271      } catch (Exception e) {
272        LOG.warn("Cannot read timestamp of log file " + path);
273        return false;
274      }
275    }
276  }
277}