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; 019 020import static org.junit.jupiter.api.Assertions.assertAll; 021import static org.junit.jupiter.api.Assertions.assertFalse; 022import static org.junit.jupiter.api.Assertions.assertNotNull; 023import static org.junit.jupiter.api.Assertions.assertTrue; 024 025import java.util.ArrayList; 026import java.util.List; 027import java.util.Map; 028import org.apache.hadoop.conf.Configuration; 029import org.apache.hadoop.fs.FileSystem; 030import org.apache.hadoop.fs.Path; 031import org.apache.hadoop.hbase.HBaseTestingUtil; 032import org.apache.hadoop.hbase.ServerName; 033import org.apache.hadoop.hbase.TableName; 034import org.apache.hadoop.hbase.backup.impl.BackupAdminImpl; 035import org.apache.hadoop.hbase.backup.impl.IncrementalBackupManager; 036import org.apache.hadoop.hbase.backup.util.BackupUtils; 037import org.apache.hadoop.hbase.client.Connection; 038import org.apache.hadoop.hbase.client.ConnectionFactory; 039import org.apache.hadoop.hbase.regionserver.HRegionServer; 040import org.apache.hadoop.hbase.testclassification.LargeTests; 041import org.apache.hadoop.hbase.util.CommonFSUtils; 042import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; 043import org.apache.hadoop.hbase.util.JVMClusterUtil; 044import org.apache.hadoop.hbase.wal.AbstractFSWALProvider; 045import org.junit.jupiter.api.BeforeAll; 046import org.junit.jupiter.api.Tag; 047import org.junit.jupiter.api.Test; 048 049@Tag(LargeTests.TAG) 050public class TestIncrementalBackupManager extends TestBackupBase { 051 052 @BeforeAll 053 public static void setUp() throws Exception { 054 TEST_UTIL = new HBaseTestingUtil(); 055 conf1 = TEST_UTIL.getConfiguration(); 056 conf1.setBoolean(AbstractFSWALProvider.SEPARATE_OLDLOGDIR, true); 057 autoRestoreOnFailure = true; 058 useSecondCluster = false; 059 setUpHelper(); 060 } 061 062 @Test 063 public void testCollectWALFilesFromRegionServerDirectories() throws Exception { 064 testCollectWALFiles(true); 065 } 066 067 @Test 068 public void testCollectWALFilesFromFlatOldWALDirectory() throws Exception { 069 testCollectWALFiles(false); 070 } 071 072 private void testCollectWALFiles(boolean separateOldLogDir) throws Exception { 073 Configuration testConf = new Configuration(conf1); 074 testConf.setBoolean(AbstractFSWALProvider.SEPARATE_OLDLOGDIR, separateOldLogDir); 075 List<TableName> tables = List.of(table1); 076 try (Connection conn = ConnectionFactory.createConnection(testConf); 077 BackupAdminImpl backupAdmin = new BackupAdminImpl(conn)) { 078 String fullBackupId = 079 backupAdmin.backupTables(createBackupRequest(BackupType.FULL, tables, BACKUP_ROOT_DIR)); 080 assertTrue(checkSucceeded(fullBackupId)); 081 082 try (IncrementalBackupManager manager = new IncrementalBackupManager(conn, testConf)) { 083 BackupInfo backupInfo = manager.createBackupInfo("backup_test", BackupType.INCREMENTAL, 084 tables, BACKUP_ROOT_DIR, -1, -1, false, false); 085 Map<String, Long> previousTimestamps = 086 BackupUtils.getRSLogTimestampMins(manager.readLogTimestampMap()); 087 ServerName serverName = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0).getServerName(); 088 Long previousTimestamp = previousTimestamps.get(serverName.getAddress().toString()); 089 assertNotNull(previousTimestamp); 090 091 TEST_UTIL.waitFor(30_000, 092 () -> EnvironmentEdgeManager.currentTime() > previousTimestamp + 1); 093 Path walRootDir = CommonFSUtils.getWALRootDir(conf1); 094 Path archiveDir = new Path(walRootDir, 095 AbstractFSWALProvider.getWALArchiveDirectoryName(testConf, serverName.toString())); 096 String walName = (separateOldLogDir ? "wal" : serverName.toString()) 097 + BackupUtils.LOGNAME_SEPARATOR + (previousTimestamp + 1); 098 Path archivedWAL = new Path(archiveDir, walName); 099 FileSystem fs = walRootDir.getFileSystem(conf1); 100 fs.mkdirs(archiveDir); 101 fs.create(archivedWAL).close(); 102 103 manager.getIncrBackupLogFileMap(); 104 105 assertTrue(backupInfo.getIncrBackupFileList().contains(archivedWAL.toString())); 106 } 107 } 108 } 109 110 /** 111 * WALs can be archived out of order, so a region server that took part in the log roll can have 112 * an archived WAL newer than its roll result while an older WAL is still in the WALs directory. 113 * The newer archived WAL must be deferred to a later backup instead of being backed up twice, and 114 * the older WAL must still end up in a backup. 115 */ 116 @Test 117 public void testOutOfOrderArchivedWALDoesNotSkipOlderWAL() throws Exception { 118 List<TableName> tables = List.of(table1); 119 HRegionServer rs = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0); 120 ServerName serverName = rs.getServerName(); 121 Path walRootDir = CommonFSUtils.getWALRootDir(conf1); 122 FileSystem fs = walRootDir.getFileSystem(conf1); 123 124 try (Connection conn = ConnectionFactory.createConnection(conf1); 125 BackupAdminImpl backupAdmin = new BackupAdminImpl(conn)) { 126 String fullBackupId = 127 backupAdmin.backupTables(createBackupRequest(BackupType.FULL, tables, BACKUP_ROOT_DIR)); 128 assertTrue(checkSucceeded(fullBackupId)); 129 130 long olderWALTs = EnvironmentEdgeManager.currentTime() + 1; 131 long newerWALTs = olderWALTs + 1; 132 Path walDir = 133 new Path(walRootDir, AbstractFSWALProvider.getWALDirectoryName(serverName.toString())); 134 Path olderWAL = 135 new Path(walDir, serverName.toString() + BackupUtils.LOGNAME_SEPARATOR + olderWALTs); 136 Path archiveDir = new Path(walRootDir, 137 AbstractFSWALProvider.getWALArchiveDirectoryName(conf1, serverName.toString())); 138 Path newerArchivedWAL = 139 new Path(archiveDir, "wal" + BackupUtils.LOGNAME_SEPARATOR + newerWALTs); 140 fs.create(olderWAL).close(); 141 fs.mkdirs(archiveDir); 142 fs.create(newerArchivedWAL).close(); 143 144 try { 145 List<String> firstBackupFiles; 146 try (IncrementalBackupManager manager = new IncrementalBackupManager(conn, conf1)) { 147 BackupInfo backupInfo = manager.createBackupInfo("backup_incr_1", BackupType.INCREMENTAL, 148 tables, BACKUP_ROOT_DIR, -1, -1, false, false); 149 Map<String, Long> boundaries = manager.getIncrBackupLogFileMap(); 150 manager.writeRegionServerLogTimestamp(backupInfo.getTables(), boundaries); 151 firstBackupFiles = backupInfo.getIncrBackupFileList(); 152 } 153 assertFalse(firstBackupFiles.contains(newerArchivedWAL.toString()), 154 "Archived WAL newer than the roll result should be deferred to a later backup: " 155 + firstBackupFiles); 156 157 TEST_UTIL.waitFor(30_000, () -> EnvironmentEdgeManager.currentTime() > newerWALTs); 158 rs.getWalRoller().requestRollAll(); 159 rs.getWalRoller().waitUntilWalRollFinished(); 160 161 List<String> secondBackupFiles; 162 try (IncrementalBackupManager manager = new IncrementalBackupManager(conn, conf1)) { 163 BackupInfo backupInfo = manager.createBackupInfo("backup_incr_2", BackupType.INCREMENTAL, 164 tables, BACKUP_ROOT_DIR, -1, -1, false, false); 165 manager.getIncrBackupLogFileMap(); 166 secondBackupFiles = backupInfo.getIncrBackupFileList(); 167 } 168 169 assertTrue( 170 firstBackupFiles.contains(olderWAL.toString()) 171 || secondBackupFiles.contains(olderWAL.toString()), 172 "WAL " + olderWAL + " was not included in any backup. First backup: " + firstBackupFiles 173 + ", second backup: " + secondBackupFiles); 174 } finally { 175 fs.delete(olderWAL, false); 176 fs.delete(newerArchivedWAL, false); 177 } 178 } 179 } 180 181 /** 182 * A dead region server's WALs stay in its -splitting directory until WAL splitting finishes. An 183 * incremental backup that runs during the split must not lose them once they are archived. 184 */ 185 @Test 186 public void testWALOfDeadServerStillSplittingIsBackedUpAfterArchiving() throws Exception { 187 List<TableName> tables = List.of(table1); 188 ServerName deadServer = ServerName.valueOf("deadhost", 16020, 1001L); 189 Path walRootDir = CommonFSUtils.getWALRootDir(conf1); 190 FileSystem fs = walRootDir.getFileSystem(conf1); 191 Path splittingDir = splittingDir(walRootDir, deadServer); 192 Path archiveDir = new Path(walRootDir, 193 AbstractFSWALProvider.getWALArchiveDirectoryName(conf1, deadServer.toString())); 194 195 try (Connection conn = ConnectionFactory.createConnection(conf1); 196 BackupAdminImpl backupAdmin = new BackupAdminImpl(conn)) { 197 String fullBackupId = 198 backupAdmin.backupTables(createBackupRequest(BackupType.FULL, tables, BACKUP_ROOT_DIR)); 199 assertTrue(checkSucceeded(fullBackupId)); 200 201 long walTs = EnvironmentEdgeManager.currentTime() + 1; 202 Path splittingWAL = new Path(splittingDir, walName(deadServer, walTs)); 203 Path archivedWAL = archivedWAL(archiveDir, walTs); 204 fs.create(splittingWAL).close(); 205 206 try { 207 // Make the live servers' roll results, and so the next boundaries, newer than the WAL. 208 TEST_UTIL.waitFor(30_000, () -> EnvironmentEdgeManager.currentTime() > walTs); 209 rollAllLiveRegionServers(); 210 211 List<String> firstBackupFiles = runIncrementalBackup(conn, tables, "backup_split_1"); 212 213 fs.mkdirs(archiveDir); 214 assertTrue(fs.rename(splittingWAL, archivedWAL)); 215 fs.delete(splittingDir, true); 216 217 List<String> secondBackupFiles = runIncrementalBackup(conn, tables, "backup_split_2"); 218 219 assertTrue( 220 firstBackupFiles.contains(splittingWAL.toString()) 221 || secondBackupFiles.contains(archivedWAL.toString()), 222 "WAL of the dead server was not included in any backup. First backup: " + firstBackupFiles 223 + ", second backup: " + secondBackupFiles); 224 } finally { 225 fs.delete(splittingDir, true); 226 fs.delete(archivedWAL, false); 227 } 228 } 229 } 230 231 /** 232 * WAL splitting archives a dead region server's WALs in parallel, so a newer WAL can be archived 233 * while older ones are still in the -splitting directory. Backing up the newer WAL must not move 234 * the boundary past the older ones. 235 */ 236 @Test 237 public void testOlderWALsOfDeadServerAreNotSkippedWhenNewerWALIsArchivedFirst() throws Exception { 238 List<TableName> tables = List.of(table1); 239 ServerName deadServer = ServerName.valueOf("deadhost", 16020, 2002L); 240 Path walRootDir = CommonFSUtils.getWALRootDir(conf1); 241 FileSystem fs = walRootDir.getFileSystem(conf1); 242 Path splittingDir = splittingDir(walRootDir, deadServer); 243 Path archiveDir = new Path(walRootDir, 244 AbstractFSWALProvider.getWALArchiveDirectoryName(conf1, deadServer.toString())); 245 246 try (Connection conn = ConnectionFactory.createConnection(conf1); 247 BackupAdminImpl backupAdmin = new BackupAdminImpl(conn)) { 248 String fullBackupId = 249 backupAdmin.backupTables(createBackupRequest(BackupType.FULL, tables, BACKUP_ROOT_DIR)); 250 assertTrue(checkSucceeded(fullBackupId)); 251 252 long oldestTs = EnvironmentEdgeManager.currentTime() + 1; 253 long middleTs = oldestTs + 1; 254 long newestTs = oldestTs + 2; 255 Path oldestSplittingWAL = new Path(splittingDir, walName(deadServer, oldestTs)); 256 Path middleSplittingWAL = new Path(splittingDir, walName(deadServer, middleTs)); 257 Path oldestArchivedWAL = archivedWAL(archiveDir, oldestTs); 258 Path middleArchivedWAL = archivedWAL(archiveDir, middleTs); 259 Path newestArchivedWAL = archivedWAL(archiveDir, newestTs); 260 fs.create(oldestSplittingWAL).close(); 261 fs.create(middleSplittingWAL).close(); 262 fs.mkdirs(archiveDir); 263 fs.create(newestArchivedWAL).close(); 264 265 try { 266 TEST_UTIL.waitFor(30_000, () -> EnvironmentEdgeManager.currentTime() > newestTs); 267 268 List<String> firstBackupFiles = runIncrementalBackup(conn, tables, "backup_order_1"); 269 270 assertTrue(fs.rename(oldestSplittingWAL, oldestArchivedWAL)); 271 assertTrue(fs.rename(middleSplittingWAL, middleArchivedWAL)); 272 fs.delete(splittingDir, true); 273 274 List<String> secondBackupFiles = runIncrementalBackup(conn, tables, "backup_order_2"); 275 276 assertAll( 277 () -> assertTrue( 278 firstBackupFiles.contains(oldestSplittingWAL.toString()) 279 || secondBackupFiles.contains(oldestArchivedWAL.toString()), 280 "Oldest WAL of the dead server was not included in any backup. First backup: " 281 + firstBackupFiles + ", second backup: " + secondBackupFiles), 282 () -> assertTrue( 283 firstBackupFiles.contains(middleSplittingWAL.toString()) 284 || secondBackupFiles.contains(middleArchivedWAL.toString()), 285 "Middle WAL of the dead server was not included in any backup. First backup: " 286 + firstBackupFiles + ", second backup: " + secondBackupFiles)); 287 } finally { 288 fs.delete(splittingDir, true); 289 fs.delete(oldestArchivedWAL, false); 290 fs.delete(middleArchivedWAL, false); 291 fs.delete(newestArchivedWAL, false); 292 } 293 } 294 } 295 296 /** 297 * A dead region server can keep an old WAL, already covered by its boundary, in its -splitting 298 * directory across several incremental backups. Its boundary must survive those backups, or a 299 * later backup would include that WAL again and could bring back deleted data. 300 */ 301 @Test 302 public void testDeadServerKeepsBoundaryWhileOldWALIsStillSplitting() throws Exception { 303 List<TableName> tables = List.of(table1); 304 ServerName deadServer = ServerName.valueOf("deadhost", 16020, 3003L); 305 Path walRootDir = CommonFSUtils.getWALRootDir(conf1); 306 FileSystem fs = walRootDir.getFileSystem(conf1); 307 Path splittingDir = splittingDir(walRootDir, deadServer); 308 Path archiveDir = new Path(walRootDir, 309 AbstractFSWALProvider.getWALArchiveDirectoryName(conf1, deadServer.toString())); 310 311 try (Connection conn = ConnectionFactory.createConnection(conf1); 312 BackupAdminImpl backupAdmin = new BackupAdminImpl(conn)) { 313 String fullBackupId = 314 backupAdmin.backupTables(createBackupRequest(BackupType.FULL, tables, BACKUP_ROOT_DIR)); 315 assertTrue(checkSucceeded(fullBackupId)); 316 317 long stuckTs = EnvironmentEdgeManager.currentTime() + 1; 318 long archivedTs = stuckTs + 1; 319 Path archivedWAL = archivedWAL(archiveDir, archivedTs); 320 Path stuckSplittingWAL = new Path(splittingDir, walName(deadServer, stuckTs)); 321 Path stuckArchivedWAL = archivedWAL(archiveDir, stuckTs); 322 fs.mkdirs(archiveDir); 323 fs.create(archivedWAL).close(); 324 325 try { 326 TEST_UTIL.waitFor(30_000, () -> EnvironmentEdgeManager.currentTime() > archivedTs); 327 List<String> firstBackupFiles = runIncrementalBackup(conn, tables, "backup_stuck_1"); 328 assertTrue(firstBackupFiles.contains(archivedWAL.toString()), 329 "Archived WAL of the dead server should be in the first backup: " + firstBackupFiles); 330 331 fs.create(stuckSplittingWAL).close(); 332 List<String> laterBackupFiles = new ArrayList<>(); 333 laterBackupFiles.addAll(runIncrementalBackup(conn, tables, "backup_stuck_2")); 334 laterBackupFiles.addAll(runIncrementalBackup(conn, tables, "backup_stuck_3")); 335 336 assertTrue(fs.rename(stuckSplittingWAL, stuckArchivedWAL)); 337 fs.delete(splittingDir, true); 338 laterBackupFiles.addAll(runIncrementalBackup(conn, tables, "backup_stuck_4")); 339 340 assertFalse( 341 laterBackupFiles.contains(stuckSplittingWAL.toString()) 342 || laterBackupFiles.contains(stuckArchivedWAL.toString()), 343 "WAL already covered by the dead server's boundary was backed up again: " 344 + laterBackupFiles); 345 } finally { 346 fs.delete(splittingDir, true); 347 fs.delete(archivedWAL, false); 348 fs.delete(stuckArchivedWAL, false); 349 } 350 } 351 } 352 353 private static Path splittingDir(Path walRootDir, ServerName serverName) { 354 return new Path(walRootDir, AbstractFSWALProvider.getWALDirectoryName(serverName.toString()) 355 + AbstractFSWALProvider.SPLITTING_EXT); 356 } 357 358 private static Path archivedWAL(Path archiveDir, long ts) { 359 return new Path(archiveDir, "wal" + BackupUtils.LOGNAME_SEPARATOR + ts); 360 } 361 362 private static String walName(ServerName serverName, long ts) { 363 return serverName.toString() + BackupUtils.LOGNAME_SEPARATOR + ts; 364 } 365 366 private static void rollAllLiveRegionServers() throws Exception { 367 for (JVMClusterUtil.RegionServerThread rst : TEST_UTIL.getMiniHBaseCluster() 368 .getLiveRegionServerThreads()) { 369 rst.getRegionServer().getWalRoller().requestRollAll(); 370 rst.getRegionServer().getWalRoller().waitUntilWalRollFinished(); 371 } 372 } 373 374 private static List<String> runIncrementalBackup(Connection conn, List<TableName> tables, 375 String backupId) throws Exception { 376 try (IncrementalBackupManager manager = new IncrementalBackupManager(conn, conf1)) { 377 BackupInfo backupInfo = manager.createBackupInfo(backupId, BackupType.INCREMENTAL, tables, 378 BACKUP_ROOT_DIR, -1, -1, false, false); 379 Map<String, Long> boundaries = manager.getIncrBackupLogFileMap(); 380 manager.writeRegionServerLogTimestamp(backupInfo.getTables(), boundaries); 381 return backupInfo.getIncrBackupFileList(); 382 } 383 } 384}