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.apache.hadoop.hbase.backup.BackupRestoreConstants.CONF_CONTINUOUS_BACKUP_WAL_DIR; 021import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONTINUOUS_BACKUP_OFFSET_UPDATE_INTERVAL_MS; 022import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONTINUOUS_BACKUP_OFFSET_UPDATE_SIZE_THRESHOLD; 023import static org.apache.hadoop.hbase.replication.regionserver.ReplicationMarkerChore.REPLICATION_MARKER_ENABLED_DEFAULT; 024import static org.apache.hadoop.hbase.replication.regionserver.ReplicationMarkerChore.REPLICATION_MARKER_ENABLED_KEY; 025import static org.junit.jupiter.api.Assertions.assertEquals; 026import static org.junit.jupiter.api.Assertions.assertFalse; 027import static org.junit.jupiter.api.Assertions.assertNotNull; 028import static org.junit.jupiter.api.Assertions.assertTrue; 029 030import java.io.IOException; 031import java.nio.ByteBuffer; 032import java.util.ArrayList; 033import java.util.HashSet; 034import java.util.LinkedHashMap; 035import java.util.List; 036import java.util.Map; 037import java.util.Objects; 038import java.util.Set; 039import org.apache.hadoop.fs.FileStatus; 040import org.apache.hadoop.fs.FileSystem; 041import org.apache.hadoop.fs.Path; 042import org.apache.hadoop.hbase.TableName; 043import org.apache.hadoop.hbase.backup.impl.BackupAdminImpl; 044import org.apache.hadoop.hbase.backup.impl.BackupManifest; 045import org.apache.hadoop.hbase.backup.impl.BackupSystemTable; 046import org.apache.hadoop.hbase.backup.util.BackupUtils; 047import org.apache.hadoop.hbase.client.Put; 048import org.apache.hadoop.hbase.client.Result; 049import org.apache.hadoop.hbase.client.ResultScanner; 050import org.apache.hadoop.hbase.client.Scan; 051import org.apache.hadoop.hbase.client.Table; 052import org.apache.hadoop.hbase.testclassification.LargeTests; 053import org.apache.hadoop.hbase.tool.BulkLoadHFiles; 054import org.apache.hadoop.hbase.util.Bytes; 055import org.apache.hadoop.hbase.util.CommonFSUtils; 056import org.apache.hadoop.hbase.util.HFileTestUtil; 057import org.junit.jupiter.api.AfterEach; 058import org.junit.jupiter.api.BeforeEach; 059import org.junit.jupiter.api.Tag; 060import org.junit.jupiter.api.Test; 061import org.slf4j.Logger; 062import org.slf4j.LoggerFactory; 063 064import org.apache.hbase.thirdparty.com.google.common.collect.Sets; 065 066@Tag(LargeTests.TAG) 067public class TestIncrementalBackupWithContinuous extends TestBackupBase { 068 069 private static final Logger LOG = 070 LoggerFactory.getLogger(TestIncrementalBackupWithContinuous.class); 071 072 private static final int ROWS_IN_BULK_LOAD = 100; 073 private static final String backupWalDirName = "TestContinuousBackupWalDir"; 074 075 private FileSystem fs; 076 077 @BeforeEach 078 public void beforeTest() throws IOException { 079 Path root = TEST_UTIL.getDataTestDirOnTestFS(); 080 Path backupWalDir = new Path(root, backupWalDirName); 081 conf1.set(CONF_CONTINUOUS_BACKUP_WAL_DIR, backupWalDir.toString()); 082 conf1.setBoolean(REPLICATION_MARKER_ENABLED_KEY, true); 083 conf1.setLong(CONTINUOUS_BACKUP_OFFSET_UPDATE_INTERVAL_MS, 2000L); // 2 seconds 084 conf1.setLong(CONTINUOUS_BACKUP_OFFSET_UPDATE_SIZE_THRESHOLD, 1024L); // 1 KB 085 fs = FileSystem.get(conf1); 086 } 087 088 @AfterEach 089 public void afterTest() throws IOException { 090 Path root = TEST_UTIL.getDataTestDirOnTestFS(); 091 Path backupWalDir = new Path(root, backupWalDirName); 092 if (fs.exists(backupWalDir)) { 093 fs.delete(backupWalDir, true); 094 } 095 conf1.unset(CONTINUOUS_BACKUP_OFFSET_UPDATE_INTERVAL_MS); 096 conf1.unset(CONTINUOUS_BACKUP_OFFSET_UPDATE_SIZE_THRESHOLD); 097 conf1.unset(CONF_CONTINUOUS_BACKUP_WAL_DIR); 098 conf1.setBoolean(REPLICATION_MARKER_ENABLED_KEY, REPLICATION_MARKER_ENABLED_DEFAULT); 099 deleteContinuousBackupReplicationPeerIfExists(TEST_UTIL.getAdmin()); 100 } 101 102 @Test 103 public void testContinuousBackupWithIncrementalBackupSuccess() throws Exception { 104 String methodName = Thread.currentThread().getStackTrace()[1].getMethodName(); 105 TableName tableName = TableName.valueOf("table_" + methodName); 106 Table t1 = TEST_UTIL.createTable(tableName, famName); 107 108 try (BackupSystemTable backupSystemTable = new BackupSystemTable(TEST_UTIL.getConnection())) { 109 int before = backupSystemTable.getBackupHistory().size(); 110 111 // Run continuous backup 112 LOG.info("Running full backup with continuous backup enabled on table: {}", tableName); 113 String backup1 = backupTables(BackupType.FULL, List.of(tableName), BACKUP_ROOT_DIR, true); 114 LOG.info("Full backup complete with ID {} for table: {}", backup1, tableName); 115 assertTrue(checkSucceeded(backup1)); 116 117 // Verify backup history increased and all the backups are succeeded 118 LOG.info("Verify backup history increased and all the backups are succeeded"); 119 List<BackupInfo> backups = backupSystemTable.getBackupHistory(); 120 assertEquals(before + 1, backups.size(), "Backup history should increase"); 121 122 // Verify backup manifest contains the correct tables 123 LOG.info("Verify backup manifest contains the correct tables"); 124 BackupManifest manifest = getLatestBackupManifest(backups); 125 assertEquals(Sets.newHashSet(tableName), new HashSet<>(manifest.getTableList()), 126 "Backup should contain the expected tables"); 127 128 loadTable(t1); 129 Thread.sleep(10000); 130 131 // Run incremental backup 132 LOG.info("Run incremental backup now on table: {}", tableName); 133 before = backupSystemTable.getBackupHistory().size(); 134 String backup2 = 135 backupTables(BackupType.INCREMENTAL, List.of(tableName), BACKUP_ROOT_DIR, true); 136 assertTrue(checkSucceeded(backup2)); 137 LOG.info("Incremental backup completed for table: {}", tableName); 138 139 // Verify the temporary backup directory was deleted 140 Path backupTmpDir = new Path(BACKUP_ROOT_DIR, ".tmp"); 141 assertFalse(fs.exists(backupTmpDir), 142 "Temporary backup directory " + backupTmpDir + " should have been deleted"); 143 144 // Verify backup history increased and all the backups are succeeded 145 backups = backupSystemTable.getBackupHistory(); 146 assertEquals(before + 1, backups.size(), "Backup history should increase"); 147 148 String originalTableChecksum = TEST_UTIL.checksumRows(t1); 149 150 LOG.info("Truncating table: {}", tableName); 151 TEST_UTIL.truncateTable(tableName); 152 153 // Restore incremental backup 154 TableName[] tables = new TableName[] { tableName }; 155 BackupAdminImpl client = new BackupAdminImpl(TEST_UTIL.getConnection()); 156 LOG.info("Restoring table: {}", tableName); 157 // In the restore request, the original table is both the "from table" and the "to table". 158 // This means the table is being restored "into itself". It is not being restored into 159 // separate table. 160 client.restore( 161 BackupUtils.createRestoreRequest(BACKUP_ROOT_DIR, backup2, false, tables, tables, true)); 162 163 LOG.info("Verifying data integrity for restored table: {}", tableName); 164 verifyRestoredTableDataIntegrity(tables[0], originalTableChecksum, NB_ROWS_IN_BATCH); 165 } 166 } 167 168 @Test 169 public void testMultiTableContinuousBackupWithIncrementalBackupSuccess() throws Exception { 170 String methodName = Thread.currentThread().getStackTrace()[1].getMethodName(); 171 List<Table> tables = new ArrayList<>(); 172 List<TableName> tableNames = new ArrayList<>(); 173 tableNames.add(TableName.valueOf("table_" + methodName + "_0")); 174 tableNames.add(TableName.valueOf("table_" + methodName + "_1")); 175 tableNames.add(TableName.valueOf("ns1", "ns1_table_" + methodName + "_0")); 176 tableNames.add(TableName.valueOf("ns1", "ns1_table_" + methodName + "_1")); 177 tableNames.add(TableName.valueOf("sameTableNameDifferentNamespace")); 178 tableNames.add(TableName.valueOf("ns3", "sameTableNameDifferentNamespace")); 179 180 for (TableName table : tableNames) { 181 LOG.info("Creating table: {}", table); 182 tables.add(TEST_UTIL.createTable(table, famName)); 183 } 184 185 try (BackupSystemTable backupSystemTable = new BackupSystemTable(TEST_UTIL.getConnection())) { 186 int before = backupSystemTable.getBackupHistory().size(); 187 188 // Run continuous backup on multiple tables 189 LOG.info("Running full backup with continuous backup enabled on tables: {}", tableNames); 190 String backup1 = backupTables(BackupType.FULL, tableNames, BACKUP_ROOT_DIR, true); 191 LOG.info("Full backup complete with ID {} for tables: {}", backup1, tableNames); 192 assertTrue(checkSucceeded(backup1)); 193 194 // Verify backup history increased and all backups have succeeded 195 LOG.info("Verify backup history increased and all backups have succeeded"); 196 List<BackupInfo> backups = backupSystemTable.getBackupHistory(); 197 assertEquals(before + 1, backups.size(), "Backup history should increase"); 198 199 // Verify backup manifest contains the correct tables 200 LOG.info("Verify backup manifest contains the correct tables"); 201 BackupManifest manifest = getLatestBackupManifest(backups); 202 assertEquals(Sets.newHashSet(tableNames), new HashSet<>(manifest.getTableList()), 203 "Backup should contain the expected tables"); 204 205 loadTables(tables); 206 Thread.sleep(10000); 207 208 // Run incremental backup 209 LOG.info("Running incremental backup on tables: {}", tableNames); 210 before = backupSystemTable.getBackupHistory().size(); 211 String backup2 = backupTables(BackupType.INCREMENTAL, tableNames, BACKUP_ROOT_DIR, true); 212 assertTrue(checkSucceeded(backup2)); 213 LOG.info("Incremental backup completed with ID {} for tables: {}", backup2, tableNames); 214 215 // Verify backup history increased and all the backups are succeeded 216 backups = backupSystemTable.getBackupHistory(); 217 assertEquals(before + 1, backups.size(), "Backup history should increase"); 218 219 // We need to get each table's original row checksum before truncating each table 220 LinkedHashMap<TableName, String> originalTableChecksums = new LinkedHashMap<>(); 221 for (Table table : tables) { 222 LOG.info("Getting row checksum for table: {}", table); 223 originalTableChecksums.put(table.getName(), TEST_UTIL.checksumRows(table)); 224 } 225 226 for (TableName tableName : tableNames) { 227 LOG.info("Truncating table: {}", tableName); 228 TEST_UTIL.truncateTable(tableName); 229 } 230 231 // Restore incremental backup 232 TableName[] tableNamesArray = tableNames.toArray(new TableName[0]); 233 BackupAdminImpl client = new BackupAdminImpl(TEST_UTIL.getConnection()); 234 LOG.info("Restoring tables: {}", tableNames); 235 // In the restore request, the original tables are both the list of "from tables" and the 236 // list of "to tables". This means the tables are being restored "into themselves". They are 237 // not being restored into separate tables. 238 client.restore(BackupUtils.createRestoreRequest(BACKUP_ROOT_DIR, backup2, false, 239 tableNamesArray, tableNamesArray, true)); 240 241 for (TableName tableName : originalTableChecksums.keySet()) { 242 LOG.info("Verifying data integrity for restored table: {}", tableName); 243 verifyRestoredTableDataIntegrity(tableName, originalTableChecksums.get(tableName), 244 NB_ROWS_IN_BATCH); 245 } 246 } 247 } 248 249 @Test 250 public void testIncrementalBackupCopyingBulkloadTillIncrCommittedWalTs() throws Exception { 251 String methodName = Thread.currentThread().getStackTrace()[1].getMethodName(); 252 TableName tableName1 = TableName.valueOf("table_" + methodName); 253 TEST_UTIL.createTable(tableName1, famName); 254 255 try (BackupSystemTable systemTable = new BackupSystemTable(TEST_UTIL.getConnection())) { 256 // The test starts with no data, and no bulk loaded rows. 257 int expectedRowCount = 0; 258 assertEquals(expectedRowCount, TEST_UTIL.countRows(tableName1)); 259 assertTrue(systemTable.readBulkloadRows(List.of(tableName1)).isEmpty()); 260 261 // Create continuous backup, bulk loads are now being tracked 262 String backup1 = backupTables(BackupType.FULL, List.of(tableName1), BACKUP_ROOT_DIR, true); 263 assertTrue(checkSucceeded(backup1)); 264 265 loadTable(TEST_UTIL.getConnection().getTable(tableName1)); 266 expectedRowCount = expectedRowCount + NB_ROWS_IN_BATCH; 267 performBulkLoad("bulkPreIncr", methodName, tableName1); 268 expectedRowCount += ROWS_IN_BULK_LOAD; 269 assertEquals(expectedRowCount, TEST_UTIL.countRows(tableName1)); 270 assertTrue(systemTable.readBulkloadRows(List.of(tableName1)).isEmpty()); 271 loadTable(TEST_UTIL.getConnection().getTable(tableName1)); 272 Thread.sleep(15000); 273 274 performBulkLoad("bulkPostIncr", methodName, tableName1); 275 assertTrue(systemTable.readBulkloadRows(List.of(tableName1)).isEmpty()); 276 277 // Incremental backup 278 String backup2 = 279 backupTables(BackupType.INCREMENTAL, List.of(tableName1), BACKUP_ROOT_DIR, true); 280 assertTrue(checkSucceeded(backup2)); 281 assertTrue(systemTable.readBulkloadRows(List.of(tableName1)).isEmpty()); 282 283 TEST_UTIL.truncateTable(tableName1); 284 // Restore incremental backup 285 TableName[] tables = new TableName[] { tableName1 }; 286 BackupAdminImpl client = new BackupAdminImpl(TEST_UTIL.getConnection()); 287 client.restore( 288 BackupUtils.createRestoreRequest(BACKUP_ROOT_DIR, backup2, false, tables, tables, true)); 289 assertEquals(expectedRowCount, TEST_UTIL.countRows(tableName1)); 290 } 291 } 292 293 private void performBulkLoad(String keyPrefix, String testDir, TableName tableName) 294 throws IOException { 295 FileSystem fs = TEST_UTIL.getTestFileSystem(); 296 Path baseDirectory = TEST_UTIL.getDataTestDirOnTestFS(testDir); 297 Path hfilePath = 298 new Path(baseDirectory, Bytes.toString(famName) + Path.SEPARATOR + "hfile_" + keyPrefix); 299 300 HFileTestUtil.createHFile(TEST_UTIL.getConfiguration(), fs, hfilePath, famName, qualName, 301 Bytes.toBytes(keyPrefix), Bytes.toBytes(keyPrefix + "z"), ROWS_IN_BULK_LOAD); 302 303 listFiles(fs, baseDirectory, baseDirectory); 304 305 Map<BulkLoadHFiles.LoadQueueItem, ByteBuffer> result = 306 BulkLoadHFiles.create(TEST_UTIL.getConfiguration()).bulkLoad(tableName, baseDirectory); 307 assertFalse(result.isEmpty()); 308 } 309 310 private static Set<String> listFiles(final FileSystem fs, final Path root, final Path dir) 311 throws IOException { 312 Set<String> files = new HashSet<>(); 313 FileStatus[] list = CommonFSUtils.listStatus(fs, dir); 314 if (list != null) { 315 for (FileStatus fstat : list) { 316 if (fstat.isDirectory()) { 317 LOG.info("Found directory {}", Objects.toString(fstat.getPath())); 318 files.addAll(listFiles(fs, root, fstat.getPath())); 319 } else { 320 LOG.info("Found file {}", Objects.toString(fstat.getPath())); 321 String file = fstat.getPath().makeQualified(fs).toString(); 322 files.add(file); 323 } 324 } 325 } 326 return files; 327 } 328 329 protected static void loadTable(Table table) throws Exception { 330 Put p; // 100 + 1 row to t1_syncup 331 for (int i = 0; i < NB_ROWS_IN_BATCH; i++) { 332 p = new Put(Bytes.toBytes("rowLoad" + i)); 333 p.addColumn(famName, qualName, Bytes.toBytes("val" + i)); 334 table.put(p); 335 } 336 } 337 338 protected static void loadTables(List<Table> tables) throws Exception { 339 for (Table table : tables) { 340 LOG.info("Loading data into table: {}", table); 341 loadTable(table); 342 } 343 } 344 345 private void verifyRestoredTableDataIntegrity(TableName restoredTableName, 346 String originalTableChecksum, int expectedRowCount) throws Exception { 347 try (Table restoredTable = TEST_UTIL.getConnection().getTable(restoredTableName); 348 ResultScanner scanner = restoredTable.getScanner(new Scan())) { 349 350 // Verify the checksum for the original table (before it was truncated) matches the checksum 351 // of the restored table. 352 String restoredTableChecksum = TEST_UTIL.checksumRows(restoredTable); 353 assertEquals(originalTableChecksum, restoredTableChecksum, 354 "The restored table's row checksum did not match the original table's checksum"); 355 356 // Verify the data in the restored table is the same as when it was originally loaded 357 // into the table. 358 int count = 0; 359 for (Result result : scanner) { 360 // The data has a numerical match between its row key and value (such as rowLoad1 and 361 // value1) 362 // We can use this to ensure a row key has the expected value. 363 String rowKey = Bytes.toString(result.getRow()); 364 int index = Integer.parseInt(rowKey.replace("rowLoad", "")); 365 366 // Verify the Value 367 byte[] actualValue = result.getValue(famName, qualName); 368 assertNotNull(actualValue, "Value missing for row key: " + rowKey); 369 String expectedValue = "val" + index; 370 assertEquals(expectedValue, Bytes.toString(actualValue), 371 "Value mismatch for row key: " + rowKey); 372 373 count++; 374 } 375 assertEquals(expectedRowCount, count); 376 } 377 } 378}