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.assertEquals; 022import static org.junit.jupiter.api.Assertions.assertFalse; 023import static org.junit.jupiter.api.Assertions.assertNotNull; 024import static org.junit.jupiter.api.Assertions.assertNull; 025import static org.junit.jupiter.api.Assertions.assertTrue; 026 027import java.io.IOException; 028import java.util.ArrayList; 029import java.util.List; 030import java.util.Map; 031import org.apache.hadoop.fs.Path; 032import org.apache.hadoop.hbase.HBaseTestingUtil; 033import org.apache.hadoop.hbase.ServerName; 034import org.apache.hadoop.hbase.SingleProcessHBaseCluster; 035import org.apache.hadoop.hbase.TableName; 036import org.apache.hadoop.hbase.backup.impl.BackupAdminImpl; 037import org.apache.hadoop.hbase.backup.impl.BackupSystemTable; 038import org.apache.hadoop.hbase.backup.impl.FullTableBackupClient; 039import org.apache.hadoop.hbase.backup.impl.TableBackupClient; 040import org.apache.hadoop.hbase.backup.util.BackupUtils; 041import org.apache.hadoop.hbase.client.Admin; 042import org.apache.hadoop.hbase.client.Connection; 043import org.apache.hadoop.hbase.client.ConnectionFactory; 044import org.apache.hadoop.hbase.client.Delete; 045import org.apache.hadoop.hbase.client.Get; 046import org.apache.hadoop.hbase.client.Put; 047import org.apache.hadoop.hbase.client.RegionInfo; 048import org.apache.hadoop.hbase.client.ResultScanner; 049import org.apache.hadoop.hbase.client.Scan; 050import org.apache.hadoop.hbase.client.Table; 051import org.apache.hadoop.hbase.master.HMaster; 052import org.apache.hadoop.hbase.master.procedure.ServerCrashProcedure; 053import org.apache.hadoop.hbase.regionserver.HRegion; 054import org.apache.hadoop.hbase.regionserver.HRegionServer; 055import org.apache.hadoop.hbase.testclassification.LargeTests; 056import org.apache.hadoop.hbase.util.Bytes; 057import org.junit.jupiter.api.BeforeAll; 058import org.junit.jupiter.api.Tag; 059import org.junit.jupiter.api.Test; 060import org.slf4j.Logger; 061import org.slf4j.LoggerFactory; 062 063import org.apache.hbase.thirdparty.com.google.common.collect.Lists; 064 065/** 066 * Tests that WAL files from offline/inactive RegionServers are handled correctly during backup. 067 * Specifically verifies that WALs from an offline RS are: 068 * <ol> 069 * <li>Backed up once in the first backup after the RS goes offline</li> 070 * <li>NOT re-backed up in subsequent backups</li> 071 * </ol> 072 */ 073@Tag(LargeTests.TAG) 074public class TestBackupOfflineRS extends TestBackupBase { 075 076 private static final Logger LOG = LoggerFactory.getLogger(TestBackupOfflineRS.class); 077 078 @BeforeAll 079 public static void setUp() throws Exception { 080 TEST_UTIL = new HBaseTestingUtil(); 081 conf1 = TEST_UTIL.getConfiguration(); 082 conf1.setInt("hbase.regionserver.info.port", -1); 083 conf1.setInt(HMaster.HBASE_MASTER_CLEANER_INTERVAL, Integer.MAX_VALUE); 084 autoRestoreOnFailure = true; 085 useSecondCluster = false; 086 setUpHelper(); 087 TEST_UTIL.getMiniHBaseCluster().startRegionServer(); 088 TEST_UTIL.waitTableAvailable(table1); 089 } 090 091 private static Runnable afterSnapshotHook; 092 093 /** Full backup client that runs {@link #afterSnapshotHook} once the table snapshots exist. */ 094 public static class FullTableBackupClientWithHook extends FullTableBackupClient { 095 @Override 096 protected void snapshotCopy(BackupInfo backupInfo) throws IOException { 097 afterSnapshotHook.run(); 098 super.snapshotCopy(backupInfo); 099 } 100 } 101 102 private static void stopRegionServerAndWait(ServerName serverName) throws Exception { 103 SingleProcessHBaseCluster cluster = TEST_UTIL.getMiniHBaseCluster(); 104 HMaster master = cluster.getMaster(); 105 cluster.stopRegionServer(serverName); 106 cluster.waitForRegionServerToStop(serverName, 60_000); 107 TEST_UTIL.waitFor(60_000, 108 () -> master.getProcedures().stream().filter(ServerCrashProcedure.class::isInstance) 109 .map(ServerCrashProcedure.class::cast) 110 .anyMatch(scp -> scp.getServerName().equals(serverName) && scp.isFinished())); 111 TEST_UTIL.waitUntilNoRegionsInTransition(60_000); 112 } 113 114 private static void restore(String backupId, TableName sourceTable, TableName restoredTable) 115 throws Exception { 116 try (Connection conn = ConnectionFactory.createConnection(conf1); 117 BackupAdminImpl backupAdmin = new BackupAdminImpl(conn)) { 118 backupAdmin.restore(BackupUtils.createRestoreRequest(BACKUP_ROOT_DIR, backupId, false, 119 new TableName[] { sourceTable }, new TableName[] { restoredTable }, true)); 120 } 121 } 122 123 private static void restoreAndAssertAllRowsPresent(String backupId, String restoredTableName, 124 String message) throws Exception { 125 TableName restoredTable = TableName.valueOf(restoredTableName); 126 restore(backupId, table1, restoredTable); 127 assertEquals(TEST_UTIL.countRows(table1), TEST_UTIL.countRows(restoredTable), message); 128 } 129 130 private static Long boundary(BackupSystemTable sysTable, String host) throws IOException { 131 return sysTable.readLogTimestampMap(BACKUP_ROOT_DIR).get(table1).get(host); 132 } 133 134 private static List<String> walsOfHost(BackupSystemTable sysTable, String backupId, String host) 135 throws IOException { 136 List<String> wals = sysTable.readBackupInfo(backupId).getIncrBackupFileList(); 137 if (wals == null) { 138 return List.of(); 139 } 140 return wals.stream() 141 .filter(wal -> host.equals(BackupUtils.parseHostNameFromLogFile(new Path(wal)))).toList(); 142 } 143 144 private static void moveRegionAndWait(TableName table, HRegionServer destination) 145 throws Exception { 146 try (Admin admin = TEST_UTIL.getConnection().getAdmin()) { 147 RegionInfo region = admin.getRegions(table).get(0); 148 admin.move(region.getEncodedNameAsBytes(), destination.getServerName()); 149 } 150 TEST_UTIL.waitFor(60_000, () -> !destination.getRegions(table).isEmpty()); 151 TEST_UTIL.waitUntilAllRegionsAssigned(table); 152 } 153 154 /** 155 * Tests that when a full backup is taken while an RS is offline (with WALs in oldlogs), the 156 * offline host's timestamps are recorded so subsequent incremental backups don't reinclude those 157 * WALs. 158 */ 159 @Test 160 public void testBackupWithOfflineRS() throws Exception { 161 LOG.info("Starting testBackupWithOfflineRS"); 162 163 SingleProcessHBaseCluster cluster = TEST_UTIL.getMiniHBaseCluster(); 164 List<TableName> tables = Lists.newArrayList(table1); 165 166 HRegionServer rsBeforeStop = cluster.startRegionServerAndWait(10000).getRegionServer(); 167 moveRegionAndWait(table1, rsBeforeStop); 168 169 LOG.info("Inserting data to generate WAL entries"); 170 try (Connection conn = ConnectionFactory.createConnection(conf1)) { 171 insertIntoTable(conn, table1, famName, 2, 100); 172 } 173 174 String offlineHost = 175 rsBeforeStop.getServerName().getHostname() + ":" + rsBeforeStop.getServerName().getPort(); 176 LOG.info("Stopping RS: {}", offlineHost); 177 178 stopRegionServerAndWait(rsBeforeStop.getServerName()); 179 180 LOG.info("Taking full backup (with offline RS WALs in oldlogs)"); 181 String fullBackupId = fullTableBackup(tables); 182 assertTrue(checkSucceeded(fullBackupId), "Full backup should succeed"); 183 184 try (BackupSystemTable sysTable = new BackupSystemTable(TEST_UTIL.getConnection())) { 185 Map<TableName, Map<String, Long>> timestamps = sysTable.readLogTimestampMap(BACKUP_ROOT_DIR); 186 Map<String, Long> rsTimestamps = timestamps.get(table1); 187 LOG.info("RS timestamps after full backup: {}", rsTimestamps); 188 189 Long tsAfterFullBackup = rsTimestamps.get(offlineHost); 190 assertNotNull(tsAfterFullBackup, 191 "Offline host should have timestamp recorded in trslm after full backup"); 192 193 LOG.info("Taking incremental backup (should NOT include offline RS WALs)"); 194 String incrBackupId = incrementalTableBackup(tables); 195 assertTrue(checkSucceeded(incrBackupId), "Incremental backup should succeed"); 196 197 assertEquals(tsAfterFullBackup, boundary(sysTable, offlineHost), 198 "Incremental backup moved the boundary of the offline host"); 199 List<String> offlineHostWals = walsOfHost(sysTable, incrBackupId, offlineHost); 200 assertTrue(offlineHostWals.isEmpty(), 201 "Incremental backup included WALs of the offline host that the full backup covers: " 202 + offlineHostWals); 203 } 204 } 205 206 /** 207 * Tests that WALs written to an RS after a full backup are correctly included in the subsequent 208 * incremental backup, even if that RS has gone offline before the incremental runs. 209 */ 210 @Test 211 public void testRSGoesOfflineAfterFullBackupBeforeIncremental() throws Exception { 212 SingleProcessHBaseCluster cluster = TEST_UTIL.getMiniHBaseCluster(); 213 List<TableName> tables = Lists.newArrayList(table1); 214 215 HRegionServer rsBeforeStop = cluster.startRegionServerAndWait(10000).getRegionServer(); 216 217 try (Connection conn = ConnectionFactory.createConnection(conf1)) { 218 insertIntoTable(conn, table1, famName, 3, 50); 219 } 220 221 String fullBackupId = fullTableBackup(tables); 222 assertTrue(checkSucceeded(fullBackupId), "Full backup should succeed"); 223 224 moveRegionAndWait(table1, rsBeforeStop); 225 try (Connection conn = ConnectionFactory.createConnection(conf1)) { 226 insertIntoTable(conn, table1, famName, 4, 50); 227 } 228 229 rsBeforeStop.getWalRoller().requestRollAll(); 230 rsBeforeStop.getWalRoller().waitUntilWalRollFinished(); 231 String offlineHost = 232 rsBeforeStop.getServerName().getHostname() + ":" + rsBeforeStop.getServerName().getPort(); 233 234 stopRegionServerAndWait(rsBeforeStop.getServerName()); 235 236 String incrBackupId = incrementalTableBackup(tables); 237 assertTrue(checkSucceeded(incrBackupId), "Incremental backup should succeed"); 238 239 restoreAndAssertAllRowsPresent(incrBackupId, "table1_rs_offline_after_full_backup", 240 "Restored table should contain the rows written to the RS that went offline"); 241 242 try (BackupSystemTable sysTable = new BackupSystemTable(TEST_UTIL.getConnection())) { 243 Long boundaryAfterIncr1 = boundary(sysTable, offlineHost); 244 assertNotNull(boundaryAfterIncr1, 245 "Offline RS should have a timestamp boundary after the incremental backed up its WALs"); 246 assertFalse(walsOfHost(sysTable, incrBackupId, offlineHost).isEmpty(), 247 "First incremental after the RS went offline should include its WALs"); 248 Long staleLogRoll = 249 sysTable.readRegionServerLastLogRollResult(BACKUP_ROOT_DIR).get(offlineHost); 250 251 String incrBackupId2 = incrementalTableBackup(tables); 252 assertTrue(checkSucceeded(incrBackupId2), "Second incremental backup should succeed"); 253 Long boundaryAfterIncr2 = boundary(sysTable, offlineHost); 254 255 String incrBackupId3 = incrementalTableBackup(tables); 256 assertTrue(checkSucceeded(incrBackupId3), "Third incremental backup should succeed"); 257 Long boundaryAfterIncr3 = boundary(sysTable, offlineHost); 258 259 String incrBackupId4 = incrementalTableBackup(tables); 260 assertTrue(checkSucceeded(incrBackupId4), "Fourth incremental backup should succeed"); 261 Long boundaryAfterIncr4 = boundary(sysTable, offlineHost); 262 263 String message = "Incremental backup moved the boundary of the offline RS, which would back " 264 + "up its WALs again (last log roll = " + staleLogRoll + ")"; 265 List<String> walsBackedUpAgain = new ArrayList<>(); 266 for (String laterBackupId : List.of(incrBackupId2, incrBackupId3, incrBackupId4)) { 267 walsBackedUpAgain.addAll(walsOfHost(sysTable, laterBackupId, offlineHost)); 268 } 269 assertAll(() -> assertEquals(boundaryAfterIncr1, boundaryAfterIncr2, message), 270 () -> assertEquals(boundaryAfterIncr1, boundaryAfterIncr3, message), 271 () -> assertEquals(boundaryAfterIncr1, boundaryAfterIncr4, message), 272 () -> assertTrue(walsBackedUpAgain.isEmpty(), 273 "Later incremental backups included WALs of the offline RS again: " + walsBackedUpAgain)); 274 } 275 } 276 277 /** 278 * Tests that a brand-new RS that comes online and goes offline before any backup correctly has 279 * its WALs covered by the full backup. 280 */ 281 @Test 282 public void testTransientRSBeforeFullBackup() throws Exception { 283 SingleProcessHBaseCluster cluster = TEST_UTIL.getMiniHBaseCluster(); 284 List<TableName> tables = Lists.newArrayList(table1); 285 286 HRegionServer transientRS = cluster.startRegionServerAndWait(10000).getRegionServer(); 287 moveRegionAndWait(table1, transientRS); 288 try (Connection conn = ConnectionFactory.createConnection(conf1)) { 289 insertIntoTable(conn, table1, famName, 5, 50); 290 } 291 transientRS.getWalRoller().requestRollAll(); 292 transientRS.getWalRoller().waitUntilWalRollFinished(); 293 String transientHost = 294 transientRS.getServerName().getHostname() + ":" + transientRS.getServerName().getPort(); 295 296 stopRegionServerAndWait(transientRS.getServerName()); 297 298 String fullBackupId = fullTableBackup(tables); 299 assertTrue(checkSucceeded(fullBackupId), "Full backup should succeed"); 300 301 try (BackupSystemTable sysTable = new BackupSystemTable(TEST_UTIL.getConnection())) { 302 Long boundaryAfterFullBackup = boundary(sysTable, transientHost); 303 assertNotNull(boundaryAfterFullBackup, 304 "Transient RS that went offline before full backup should have its WAL boundary recorded"); 305 306 String incrBackupId = incrementalTableBackup(tables); 307 assertTrue(checkSucceeded(incrBackupId), 308 "Incremental backup after transient RS should succeed"); 309 310 assertEquals(boundaryAfterFullBackup, boundary(sysTable, transientHost), 311 "Incremental backup moved the boundary of the transient RS"); 312 } 313 } 314 315 /** 316 * Tests that WALs from an RS that comes online and goes offline between a full backup and an 317 * incremental backup are correctly included in the incremental backup and not re-included in 318 * subsequent incremental backups. 319 */ 320 @Test 321 public void testTransientRSAfterFullBackupBeforeIncremental() throws Exception { 322 SingleProcessHBaseCluster cluster = TEST_UTIL.getMiniHBaseCluster(); 323 List<TableName> tables = Lists.newArrayList(table1); 324 325 try (Connection conn = ConnectionFactory.createConnection(conf1)) { 326 insertIntoTable(conn, table1, famName, 6, 50); 327 } 328 329 String fullBackupId = fullTableBackup(tables); 330 assertTrue(checkSucceeded(fullBackupId), "Full backup should succeed"); 331 332 HRegionServer transientRS = cluster.startRegionServerAndWait(10000).getRegionServer(); 333 moveRegionAndWait(table1, transientRS); 334 try (Connection conn = ConnectionFactory.createConnection(conf1)) { 335 insertIntoTable(conn, table1, famName, 7, 50); 336 } 337 transientRS.getWalRoller().requestRollAll(); 338 transientRS.getWalRoller().waitUntilWalRollFinished(); 339 String transientHost = 340 transientRS.getServerName().getHostname() + ":" + transientRS.getServerName().getPort(); 341 342 stopRegionServerAndWait(transientRS.getServerName()); 343 344 String incrBackupId = incrementalTableBackup(tables); 345 assertTrue(checkSucceeded(incrBackupId), "Incremental backup should succeed"); 346 347 restoreAndAssertAllRowsPresent(incrBackupId, "table1_transient_rs_after_full_backup", 348 "Restored table should contain the rows written to the transient RS"); 349 350 try (BackupSystemTable sysTable = new BackupSystemTable(TEST_UTIL.getConnection())) { 351 Long boundaryAfterIncr1 = boundary(sysTable, transientHost); 352 assertNotNull(boundaryAfterIncr1, 353 "Transient RS should have a timestamp boundary after the incremental backed up its WALs"); 354 355 String incrBackupId2 = incrementalTableBackup(tables); 356 assertTrue(checkSucceeded(incrBackupId2), "Second incremental backup should succeed"); 357 358 assertEquals(boundaryAfterIncr1, boundary(sysTable, transientHost), 359 "Second incremental backup moved the boundary of the transient RS"); 360 } 361 } 362 363 /** 364 * Tests that edits written during a full backup, after its log roll and table snapshot, to an RS 365 * that started after that log roll, are included in the next incremental backup. 366 */ 367 @Test 368 public void testRSStartedDuringFullBackup() throws Exception { 369 SingleProcessHBaseCluster cluster = TEST_UTIL.getMiniHBaseCluster(); 370 List<TableName> tables = Lists.newArrayList(table1); 371 372 afterSnapshotHook = () -> { 373 try { 374 HRegionServer newRS = cluster.startRegionServerAndWait(10000).getRegionServer(); 375 moveRegionAndWait(table1, newRS); 376 try (Connection conn = ConnectionFactory.createConnection(conf1)) { 377 insertIntoTable(conn, table1, famName, 8, 50).close(); 378 } 379 stopRegionServerAndWait(newRS.getServerName()); 380 } catch (Exception e) { 381 throw new RuntimeException(e); 382 } 383 }; 384 conf1.set(TableBackupClient.BACKUP_CLIENT_IMPL_CLASS, 385 FullTableBackupClientWithHook.class.getName()); 386 String fullBackupId; 387 try { 388 fullBackupId = fullTableBackup(tables); 389 } finally { 390 conf1.unset(TableBackupClient.BACKUP_CLIENT_IMPL_CLASS); 391 } 392 assertTrue(checkSucceeded(fullBackupId), "Full backup should succeed"); 393 394 String incrBackupId = incrementalTableBackup(tables); 395 assertTrue(checkSucceeded(incrBackupId), "Incremental backup should succeed"); 396 397 restoreAndAssertAllRowsPresent(incrBackupId, "table1_rs_started_during_full_backup", 398 "Restored table should contain all rows, including those written to the new RS"); 399 } 400 401 /** 402 * Tests that a row deleted before a full backup is not brought back by the WALs of a region 403 * server that went offline between two backups. The row and its delete marker are compacted away 404 * before the full backup, so only the offline RS's old WAL, which still holds the put, could 405 * bring it back. 406 */ 407 @Test 408 public void testDeletedRowIsNotResurrectedByOfflineRSWALs() throws Exception { 409 SingleProcessHBaseCluster cluster = TEST_UTIL.getMiniHBaseCluster(); 410 List<TableName> tables = Lists.newArrayList(table1); 411 byte[] deletedRow = Bytes.toBytes("row-deleted"); 412 413 HRegionServer offlineRS = cluster.startRegionServerAndWait(10000).getRegionServer(); 414 HRegionServer liveRS = cluster.startRegionServerAndWait(10000).getRegionServer(); 415 416 try (Table table = TEST_UTIL.getConnection().getTable(table1)) { 417 moveRegionAndWait(table1, offlineRS); 418 419 String firstFullBackupId = fullTableBackup(tables); 420 assertTrue(checkSucceeded(firstFullBackupId), "First full backup should succeed"); 421 422 for (int i = 0; i < 10; i++) { 423 table.put(new Put(Bytes.toBytes("row-kept-" + i)).addColumn(famName, qualName, 424 Bytes.toBytes("value"))); 425 } 426 table.put(new Put(deletedRow).addColumn(famName, qualName, Bytes.toBytes("value"))); 427 for (HRegion region : offlineRS.getRegions(table1)) { 428 region.flush(true); 429 } 430 431 moveRegionAndWait(table1, liveRS); 432 433 table.delete(new Delete(deletedRow)); 434 for (HRegion region : liveRS.getRegions(table1)) { 435 region.flush(true); 436 region.compact(true); 437 } 438 439 assertTrue(table.get(new Get(deletedRow)).isEmpty(), 440 "Deleted row should be gone from the source table"); 441 Scan rawScan = new Scan().withStartRow(deletedRow).withStopRow(deletedRow, true).setRaw(true); 442 try (ResultScanner scanner = table.getScanner(rawScan)) { 443 assertNull(scanner.next(), 444 "Major compaction should have purged the deleted row and its delete marker"); 445 } 446 447 stopRegionServerAndWait(offlineRS.getServerName()); 448 449 String secondFullBackupId = fullTableBackup(tables); 450 assertTrue(checkSucceeded(secondFullBackupId), "Second full backup should succeed"); 451 String incrBackupId = incrementalTableBackup(tables); 452 assertTrue(checkSucceeded(incrBackupId), "Incremental backup should succeed"); 453 454 TableName restoredTable = TableName.valueOf("table1_deleted_row_not_resurrected"); 455 restore(incrBackupId, table1, restoredTable); 456 try (Table restored = TEST_UTIL.getConnection().getTable(restoredTable)) { 457 assertTrue(restored.get(new Get(deletedRow)).isEmpty(), 458 "Deleted row was brought back by the WALs of the offline RS"); 459 } 460 assertEquals(TEST_UTIL.countRows(table1), TEST_UTIL.countRows(restoredTable), 461 "Restored table should match the source table"); 462 } 463 } 464}