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 Path bulkLoadOutputDir = new Path(backupTmpDir, backup2); 142 assertFalse(fs.exists(bulkLoadOutputDir), 143 "Bulk load output directory " + bulkLoadOutputDir + " should have been deleted"); 144 145 // Verify backup history increased and all the backups are succeeded 146 backups = backupSystemTable.getBackupHistory(); 147 assertEquals(before + 1, backups.size(), "Backup history should increase"); 148 149 String originalTableChecksum = TEST_UTIL.checksumRows(t1); 150 151 LOG.info("Truncating table: {}", tableName); 152 TEST_UTIL.truncateTable(tableName); 153 154 // Restore incremental backup 155 TableName[] tables = new TableName[] { tableName }; 156 BackupAdminImpl client = new BackupAdminImpl(TEST_UTIL.getConnection()); 157 LOG.info("Restoring table: {}", tableName); 158 // In the restore request, the original table is both the "from table" and the "to table". 159 // This means the table is being restored "into itself". It is not being restored into 160 // separate table. 161 client.restore( 162 BackupUtils.createRestoreRequest(BACKUP_ROOT_DIR, backup2, false, tables, tables, true)); 163 164 LOG.info("Verifying data integrity for restored table: {}", tableName); 165 verifyRestoredTableDataIntegrity(tables[0], originalTableChecksum, NB_ROWS_IN_BATCH); 166 } 167 } 168 169 @Test 170 public void testMultiTableContinuousBackupWithIncrementalBackupSuccess() throws Exception { 171 String methodName = Thread.currentThread().getStackTrace()[1].getMethodName(); 172 List<Table> tables = new ArrayList<>(); 173 List<TableName> tableNames = new ArrayList<>(); 174 tableNames.add(TableName.valueOf("table_" + methodName + "_0")); 175 tableNames.add(TableName.valueOf("table_" + methodName + "_1")); 176 tableNames.add(TableName.valueOf("ns1", "ns1_table_" + methodName + "_0")); 177 tableNames.add(TableName.valueOf("ns1", "ns1_table_" + methodName + "_1")); 178 tableNames.add(TableName.valueOf("sameTableNameDifferentNamespace")); 179 tableNames.add(TableName.valueOf("ns3", "sameTableNameDifferentNamespace")); 180 181 for (TableName table : tableNames) { 182 LOG.info("Creating table: {}", table); 183 tables.add(TEST_UTIL.createTable(table, famName)); 184 } 185 186 try (BackupSystemTable backupSystemTable = new BackupSystemTable(TEST_UTIL.getConnection())) { 187 int before = backupSystemTable.getBackupHistory().size(); 188 189 // Run continuous backup on multiple tables 190 LOG.info("Running full backup with continuous backup enabled on tables: {}", tableNames); 191 String backup1 = backupTables(BackupType.FULL, tableNames, BACKUP_ROOT_DIR, true); 192 LOG.info("Full backup complete with ID {} for tables: {}", backup1, tableNames); 193 assertTrue(checkSucceeded(backup1)); 194 195 // Verify backup history increased and all backups have succeeded 196 LOG.info("Verify backup history increased and all backups have succeeded"); 197 List<BackupInfo> backups = backupSystemTable.getBackupHistory(); 198 assertEquals(before + 1, backups.size(), "Backup history should increase"); 199 200 // Verify backup manifest contains the correct tables 201 LOG.info("Verify backup manifest contains the correct tables"); 202 BackupManifest manifest = getLatestBackupManifest(backups); 203 assertEquals(Sets.newHashSet(tableNames), new HashSet<>(manifest.getTableList()), 204 "Backup should contain the expected tables"); 205 206 loadTables(tables); 207 Thread.sleep(10000); 208 209 // Run incremental backup 210 LOG.info("Running incremental backup on tables: {}", tableNames); 211 before = backupSystemTable.getBackupHistory().size(); 212 String backup2 = backupTables(BackupType.INCREMENTAL, tableNames, BACKUP_ROOT_DIR, true); 213 assertTrue(checkSucceeded(backup2)); 214 LOG.info("Incremental backup completed with ID {} for tables: {}", backup2, tableNames); 215 216 // Verify backup history increased and all the backups are succeeded 217 backups = backupSystemTable.getBackupHistory(); 218 assertEquals(before + 1, backups.size(), "Backup history should increase"); 219 220 // We need to get each table's original row checksum before truncating each table 221 LinkedHashMap<TableName, String> originalTableChecksums = new LinkedHashMap<>(); 222 for (Table table : tables) { 223 LOG.info("Getting row checksum for table: {}", table); 224 originalTableChecksums.put(table.getName(), TEST_UTIL.checksumRows(table)); 225 } 226 227 for (TableName tableName : tableNames) { 228 LOG.info("Truncating table: {}", tableName); 229 TEST_UTIL.truncateTable(tableName); 230 } 231 232 // Restore incremental backup 233 TableName[] tableNamesArray = tableNames.toArray(new TableName[0]); 234 BackupAdminImpl client = new BackupAdminImpl(TEST_UTIL.getConnection()); 235 LOG.info("Restoring tables: {}", tableNames); 236 // In the restore request, the original tables are both the list of "from tables" and the 237 // list of "to tables". This means the tables are being restored "into themselves". They are 238 // not being restored into separate tables. 239 client.restore(BackupUtils.createRestoreRequest(BACKUP_ROOT_DIR, backup2, false, 240 tableNamesArray, tableNamesArray, true)); 241 242 for (TableName tableName : originalTableChecksums.keySet()) { 243 LOG.info("Verifying data integrity for restored table: {}", tableName); 244 verifyRestoredTableDataIntegrity(tableName, originalTableChecksums.get(tableName), 245 NB_ROWS_IN_BATCH); 246 } 247 } 248 } 249 250 @Test 251 public void testIncrementalBackupCopyingBulkloadTillIncrCommittedWalTs() throws Exception { 252 String methodName = Thread.currentThread().getStackTrace()[1].getMethodName(); 253 TableName tableName1 = TableName.valueOf("table_" + methodName); 254 TEST_UTIL.createTable(tableName1, famName); 255 256 try (BackupSystemTable systemTable = new BackupSystemTable(TEST_UTIL.getConnection())) { 257 // The test starts with no data, and no bulk loaded rows. 258 int expectedRowCount = 0; 259 assertEquals(expectedRowCount, TEST_UTIL.countRows(tableName1)); 260 assertTrue(systemTable.readBulkloadRows(List.of(tableName1)).isEmpty()); 261 262 // Create continuous backup, bulk loads are now being tracked 263 String backup1 = backupTables(BackupType.FULL, List.of(tableName1), BACKUP_ROOT_DIR, true); 264 assertTrue(checkSucceeded(backup1)); 265 266 loadTable(TEST_UTIL.getConnection().getTable(tableName1)); 267 expectedRowCount = expectedRowCount + NB_ROWS_IN_BATCH; 268 performBulkLoad("bulkPreIncr", methodName, tableName1); 269 expectedRowCount += ROWS_IN_BULK_LOAD; 270 assertEquals(expectedRowCount, TEST_UTIL.countRows(tableName1)); 271 assertTrue(systemTable.readBulkloadRows(List.of(tableName1)).isEmpty()); 272 loadTable(TEST_UTIL.getConnection().getTable(tableName1)); 273 Thread.sleep(15000); 274 275 performBulkLoad("bulkPostIncr", methodName, tableName1); 276 assertTrue(systemTable.readBulkloadRows(List.of(tableName1)).isEmpty()); 277 278 // Incremental backup 279 String backup2 = 280 backupTables(BackupType.INCREMENTAL, List.of(tableName1), BACKUP_ROOT_DIR, true); 281 assertTrue(checkSucceeded(backup2)); 282 assertTrue(systemTable.readBulkloadRows(List.of(tableName1)).isEmpty()); 283 284 TEST_UTIL.truncateTable(tableName1); 285 // Restore incremental backup 286 TableName[] tables = new TableName[] { tableName1 }; 287 BackupAdminImpl client = new BackupAdminImpl(TEST_UTIL.getConnection()); 288 client.restore( 289 BackupUtils.createRestoreRequest(BACKUP_ROOT_DIR, backup2, false, tables, tables, true)); 290 assertEquals(expectedRowCount, TEST_UTIL.countRows(tableName1)); 291 } 292 } 293 294 private void performBulkLoad(String keyPrefix, String testDir, TableName tableName) 295 throws IOException { 296 FileSystem fs = TEST_UTIL.getTestFileSystem(); 297 Path baseDirectory = TEST_UTIL.getDataTestDirOnTestFS(testDir); 298 Path hfilePath = 299 new Path(baseDirectory, Bytes.toString(famName) + Path.SEPARATOR + "hfile_" + keyPrefix); 300 301 HFileTestUtil.createHFile(TEST_UTIL.getConfiguration(), fs, hfilePath, famName, qualName, 302 Bytes.toBytes(keyPrefix), Bytes.toBytes(keyPrefix + "z"), ROWS_IN_BULK_LOAD); 303 304 listFiles(fs, baseDirectory, baseDirectory); 305 306 Map<BulkLoadHFiles.LoadQueueItem, ByteBuffer> result = 307 BulkLoadHFiles.create(TEST_UTIL.getConfiguration()).bulkLoad(tableName, baseDirectory); 308 assertFalse(result.isEmpty()); 309 } 310 311 private static Set<String> listFiles(final FileSystem fs, final Path root, final Path dir) 312 throws IOException { 313 Set<String> files = new HashSet<>(); 314 FileStatus[] list = CommonFSUtils.listStatus(fs, dir); 315 if (list != null) { 316 for (FileStatus fstat : list) { 317 if (fstat.isDirectory()) { 318 LOG.info("Found directory {}", Objects.toString(fstat.getPath())); 319 files.addAll(listFiles(fs, root, fstat.getPath())); 320 } else { 321 LOG.info("Found file {}", Objects.toString(fstat.getPath())); 322 String file = fstat.getPath().makeQualified(fs).toString(); 323 files.add(file); 324 } 325 } 326 } 327 return files; 328 } 329 330 protected static void loadTable(Table table) throws Exception { 331 Put p; // 100 + 1 row to t1_syncup 332 for (int i = 0; i < NB_ROWS_IN_BATCH; i++) { 333 p = new Put(Bytes.toBytes("rowLoad" + i)); 334 p.addColumn(famName, qualName, Bytes.toBytes("val" + i)); 335 table.put(p); 336 } 337 } 338 339 protected static void loadTables(List<Table> tables) throws Exception { 340 for (Table table : tables) { 341 LOG.info("Loading data into table: {}", table); 342 loadTable(table); 343 } 344 } 345 346 private void verifyRestoredTableDataIntegrity(TableName restoredTableName, 347 String originalTableChecksum, int expectedRowCount) throws Exception { 348 try (Table restoredTable = TEST_UTIL.getConnection().getTable(restoredTableName); 349 ResultScanner scanner = restoredTable.getScanner(new Scan())) { 350 351 // Verify the checksum for the original table (before it was truncated) matches the checksum 352 // of the restored table. 353 String restoredTableChecksum = TEST_UTIL.checksumRows(restoredTable); 354 assertEquals(originalTableChecksum, restoredTableChecksum, 355 "The restored table's row checksum did not match the original table's checksum"); 356 357 // Verify the data in the restored table is the same as when it was originally loaded 358 // into the table. 359 int count = 0; 360 for (Result result : scanner) { 361 // The data has a numerical match between its row key and value (such as rowLoad1 and 362 // value1) 363 // We can use this to ensure a row key has the expected value. 364 String rowKey = Bytes.toString(result.getRow()); 365 int index = Integer.parseInt(rowKey.replace("rowLoad", "")); 366 367 // Verify the Value 368 byte[] actualValue = result.getValue(famName, qualName); 369 assertNotNull(actualValue, "Value missing for row key: " + rowKey); 370 String expectedValue = "val" + index; 371 assertEquals(expectedValue, Bytes.toString(actualValue), 372 "Value mismatch for row key: " + rowKey); 373 374 count++; 375 } 376 assertEquals(expectedRowCount, count); 377 } 378 } 379}