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