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.regionserver; 019 020import static org.apache.hadoop.hbase.HBaseTestingUtil.START_KEY; 021import static org.apache.hadoop.hbase.HBaseTestingUtil.START_KEY_BYTES; 022import static org.apache.hadoop.hbase.HBaseTestingUtil.fam1; 023import static org.apache.hadoop.hbase.regionserver.Store.PRIORITY_USER; 024import static org.junit.jupiter.api.Assertions.assertEquals; 025import static org.junit.jupiter.api.Assertions.assertNotNull; 026import static org.junit.jupiter.api.Assertions.assertTrue; 027 028import java.io.IOException; 029import java.util.ArrayList; 030import java.util.Collection; 031import java.util.HashMap; 032import java.util.List; 033import java.util.Map; 034import java.util.Map.Entry; 035import java.util.stream.Stream; 036import org.apache.hadoop.conf.Configuration; 037import org.apache.hadoop.hbase.Cell; 038import org.apache.hadoop.hbase.CellUtil; 039import org.apache.hadoop.hbase.HBaseParameterizedTestTemplate; 040import org.apache.hadoop.hbase.HBaseTestingUtil; 041import org.apache.hadoop.hbase.HConstants; 042import org.apache.hadoop.hbase.HTestConst; 043import org.apache.hadoop.hbase.KeepDeletedCells; 044import org.apache.hadoop.hbase.KeyValue; 045import org.apache.hadoop.hbase.TableName; 046import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 047import org.apache.hadoop.hbase.client.Delete; 048import org.apache.hadoop.hbase.client.Get; 049import org.apache.hadoop.hbase.client.Result; 050import org.apache.hadoop.hbase.client.Scan; 051import org.apache.hadoop.hbase.client.Table; 052import org.apache.hadoop.hbase.client.TableDescriptor; 053import org.apache.hadoop.hbase.io.encoding.DataBlockEncoding; 054import org.apache.hadoop.hbase.io.hfile.HFileDataBlockEncoder; 055import org.apache.hadoop.hbase.io.hfile.HFileDataBlockEncoderImpl; 056import org.apache.hadoop.hbase.regionserver.compactions.CompactionLifeCycleTracker; 057import org.apache.hadoop.hbase.regionserver.compactions.CompactionRequestImpl; 058import org.apache.hadoop.hbase.regionserver.compactions.RatioBasedCompactionPolicy; 059import org.apache.hadoop.hbase.testclassification.LargeTests; 060import org.apache.hadoop.hbase.testclassification.RegionServerTests; 061import org.apache.hadoop.hbase.util.Bytes; 062import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; 063import org.apache.hadoop.hbase.wal.WAL; 064import org.junit.jupiter.api.AfterEach; 065import org.junit.jupiter.api.BeforeEach; 066import org.junit.jupiter.api.Tag; 067import org.junit.jupiter.api.TestInfo; 068import org.junit.jupiter.api.TestTemplate; 069import org.junit.jupiter.params.provider.Arguments; 070import org.slf4j.Logger; 071import org.slf4j.LoggerFactory; 072 073/** 074 * Test major compactions 075 */ 076@Tag(RegionServerTests.TAG) 077@Tag(LargeTests.TAG) 078@HBaseParameterizedTestTemplate(name = "{index}: compType={0}") 079public class TestMajorCompaction { 080 081 public static Stream<Arguments> parameters() { 082 return Stream.of("NONE", "BASIC", "EAGER").map(Arguments::of); 083 } 084 085 private static final Logger LOG = LoggerFactory.getLogger(TestMajorCompaction.class.getName()); 086 private static final HBaseTestingUtil UTIL = new HBaseTestingUtil(); 087 protected Configuration conf = UTIL.getConfiguration(); 088 089 private String name; 090 private final String compType; 091 092 private HRegion r = null; 093 private TableDescriptor htd = null; 094 private static final byte[] COLUMN_FAMILY = fam1; 095 private final byte[] STARTROW = Bytes.toBytes(START_KEY); 096 private static final byte[] COLUMN_FAMILY_TEXT = COLUMN_FAMILY; 097 private int compactionThreshold; 098 private byte[] secondRowBytes, thirdRowBytes; 099 private static final long MAX_FILES_TO_COMPACT = 10; 100 101 /** constructor */ 102 public TestMajorCompaction(String compType) { 103 super(); 104 this.compType = compType; 105 // Set cache flush size to 1MB 106 conf.setInt(HConstants.HREGION_MEMSTORE_FLUSH_SIZE, 1024 * 1024); 107 conf.setInt(HConstants.HREGION_MEMSTORE_BLOCK_MULTIPLIER, 100); 108 compactionThreshold = conf.getInt("hbase.hstore.compactionThreshold", 3); 109 conf.set(CompactingMemStore.COMPACTING_MEMSTORE_TYPE_KEY, String.valueOf(compType)); 110 111 secondRowBytes = START_KEY_BYTES.clone(); 112 // Increment the least significant character so we get to next row. 113 secondRowBytes[START_KEY_BYTES.length - 1]++; 114 thirdRowBytes = START_KEY_BYTES.clone(); 115 thirdRowBytes[START_KEY_BYTES.length - 1] = 116 (byte) (thirdRowBytes[START_KEY_BYTES.length - 1] + 2); 117 } 118 119 @BeforeEach 120 public void setUp(TestInfo testInfo) throws Exception { 121 this.name = testInfo.getTestMethod().get().getName(); 122 this.htd = UTIL.createTableDescriptor( 123 TableName.valueOf((name + "-" + compType).replace('[', 'i').replace(']', 'i')), 124 ColumnFamilyDescriptorBuilder.DEFAULT_MIN_VERSIONS, 3, HConstants.FOREVER, 125 ColumnFamilyDescriptorBuilder.DEFAULT_KEEP_DELETED); 126 this.r = UTIL.createLocalHRegion(htd, null, null); 127 } 128 129 @AfterEach 130 public void tearDown() throws Exception { 131 WAL wal = ((HRegion) r).getWAL(); 132 ((HRegion) r).close(); 133 wal.close(); 134 } 135 136 /** 137 * Test that on a major compaction, if all cells are expired or deleted, then we'll end up with no 138 * product. Make sure scanner over region returns right answer in this case - and that it just 139 * basically works. 140 * @throws IOException exception encountered 141 */ 142 @TestTemplate 143 public void testMajorCompactingToNoOutput() throws IOException { 144 testMajorCompactingWithDeletes(KeepDeletedCells.FALSE); 145 } 146 147 /** 148 * Test that on a major compaction,Deleted cells are retained if keep deleted cells is set to true 149 * @throws IOException exception encountered 150 */ 151 @TestTemplate 152 public void testMajorCompactingWithKeepDeletedCells() throws IOException { 153 testMajorCompactingWithDeletes(KeepDeletedCells.TRUE); 154 } 155 156 /** 157 * Run compaction and flushing memstore Assert deletes get cleaned up. 158 */ 159 @TestTemplate 160 public void testMajorCompaction() throws Exception { 161 majorCompaction(); 162 } 163 164 @TestTemplate 165 public void testDataBlockEncodingInCacheOnly() throws Exception { 166 majorCompactionWithDataBlockEncoding(true); 167 } 168 169 @TestTemplate 170 public void testDataBlockEncodingEverywhere() throws Exception { 171 majorCompactionWithDataBlockEncoding(false); 172 } 173 174 public void majorCompactionWithDataBlockEncoding(boolean inCacheOnly) throws Exception { 175 Map<HStore, HFileDataBlockEncoder> replaceBlockCache = new HashMap<>(); 176 for (HStore store : r.getStores()) { 177 HFileDataBlockEncoder blockEncoder = store.getDataBlockEncoder(); 178 replaceBlockCache.put(store, blockEncoder); 179 final DataBlockEncoding inCache = DataBlockEncoding.PREFIX; 180 final DataBlockEncoding onDisk = inCacheOnly ? DataBlockEncoding.NONE : inCache; 181 ((HStore) store).setDataBlockEncoderInTest(new HFileDataBlockEncoderImpl(onDisk)); 182 } 183 184 majorCompaction(); 185 186 // restore settings 187 for (Entry<HStore, HFileDataBlockEncoder> entry : replaceBlockCache.entrySet()) { 188 ((HStore) entry.getKey()).setDataBlockEncoderInTest(entry.getValue()); 189 } 190 } 191 192 private void majorCompaction() throws Exception { 193 createStoreFile(r); 194 for (int i = 0; i < compactionThreshold; i++) { 195 createStoreFile(r); 196 } 197 // Add more content. 198 HTestConst.addContent(new RegionAsTable(r), Bytes.toString(COLUMN_FAMILY)); 199 200 // Now there are about 5 versions of each column. 201 // Default is that there only 3 (MAXVERSIONS) versions allowed per column. 202 // 203 // Assert == 3 when we ask for versions. 204 Result result = r.get(new Get(STARTROW).addFamily(COLUMN_FAMILY_TEXT).readVersions(100)); 205 assertEquals(compactionThreshold, result.size()); 206 207 r.flush(true); 208 r.compact(true); 209 210 // look at the second row 211 // Increment the least significant character so we get to next row. 212 byte[] secondRowBytes = START_KEY_BYTES.clone(); 213 secondRowBytes[START_KEY_BYTES.length - 1]++; 214 215 // Always 3 versions if that is what max versions is. 216 result = r.get(new Get(secondRowBytes).addFamily(COLUMN_FAMILY_TEXT).readVersions(100)); 217 LOG.debug( 218 "Row " + Bytes.toStringBinary(secondRowBytes) + " after " + "initial compaction: " + result); 219 assertEquals(compactionThreshold, result.size(), 220 "Invalid number of versions of row " + Bytes.toStringBinary(secondRowBytes) + "."); 221 222 // Now add deletes to memstore and then flush it. 223 // That will put us over 224 // the compaction threshold of 3 store files. Compacting these store files 225 // should result in a compacted store file that has no references to the 226 // deleted row. 227 LOG.debug("Adding deletes to memstore and flushing"); 228 Delete delete = new Delete(secondRowBytes, EnvironmentEdgeManager.currentTime()); 229 byte[][] famAndQf = { COLUMN_FAMILY, null }; 230 delete.addFamily(famAndQf[0]); 231 r.delete(delete); 232 233 // Assert deleted. 234 result = r.get(new Get(secondRowBytes).addFamily(COLUMN_FAMILY_TEXT).readVersions(100)); 235 assertTrue(result.isEmpty(), "Second row should have been deleted"); 236 237 r.flush(true); 238 239 result = r.get(new Get(secondRowBytes).addFamily(COLUMN_FAMILY_TEXT).readVersions(100)); 240 assertTrue(result.isEmpty(), "Second row should have been deleted"); 241 242 // Add a bit of data and flush. Start adding at 'bbb'. 243 createSmallerStoreFile(this.r); 244 r.flush(true); 245 // Assert that the second row is still deleted. 246 result = r.get(new Get(secondRowBytes).addFamily(COLUMN_FAMILY_TEXT).readVersions(100)); 247 assertTrue(result.isEmpty(), "Second row should still be deleted"); 248 249 // Force major compaction. 250 r.compact(true); 251 assertEquals(1, r.getStore(COLUMN_FAMILY_TEXT).getStorefiles().size()); 252 253 result = r.get(new Get(secondRowBytes).addFamily(COLUMN_FAMILY_TEXT).readVersions(100)); 254 assertTrue(result.isEmpty(), "Second row should still be deleted"); 255 256 // Make sure the store files do have some 'aaa' keys in them -- exactly 3. 257 // Also, that compacted store files do not have any secondRowBytes because 258 // they were deleted. 259 verifyCounts(3, 0); 260 261 // Multiple versions allowed for an entry, so the delete isn't enough 262 // Lower TTL and expire to ensure that all our entries have been wiped 263 final int ttl = 1000; 264 for (HStore store : r.getStores()) { 265 ScanInfo old = store.getScanInfo(); 266 ScanInfo si = old.customize(old.getMaxVersions(), ttl, old.getKeepDeletedCells()); 267 store.setScanInfo(si); 268 } 269 Thread.sleep(1000); 270 271 r.compact(true); 272 int count = count(); 273 assertEquals(0, count, "Should not see anything after TTL has expired"); 274 } 275 276 @TestTemplate 277 public void testTimeBasedMajorCompaction() throws Exception { 278 // create 2 storefiles and force a major compaction to reset the time 279 int delay = 10 * 1000; // 10 sec 280 float jitterPct = 0.20f; // 20% 281 conf.setLong(HConstants.MAJOR_COMPACTION_PERIOD, delay); 282 conf.setFloat("hbase.hregion.majorcompaction.jitter", jitterPct); 283 284 HStore s = ((HStore) r.getStore(COLUMN_FAMILY)); 285 s.storeEngine.getCompactionPolicy().setConf(conf); 286 try { 287 createStoreFile(r); 288 createStoreFile(r); 289 r.compact(true); 290 291 // add one more file & verify that a regular compaction won't work 292 createStoreFile(r); 293 r.compact(false); 294 assertEquals(2, s.getStorefilesCount()); 295 296 // ensure that major compaction time is deterministic 297 RatioBasedCompactionPolicy c = 298 (RatioBasedCompactionPolicy) s.storeEngine.getCompactionPolicy(); 299 Collection<HStoreFile> storeFiles = s.getStorefiles(); 300 long mcTime = c.getNextMajorCompactTime(storeFiles); 301 for (int i = 0; i < 10; ++i) { 302 assertEquals(mcTime, c.getNextMajorCompactTime(storeFiles)); 303 } 304 305 // ensure that the major compaction time is within the variance 306 long jitter = Math.round(delay * jitterPct); 307 assertTrue(delay - jitter <= mcTime && mcTime <= delay + jitter); 308 309 // wait until the time-based compaction interval 310 Thread.sleep(mcTime); 311 312 // trigger a compaction request and ensure that it's upgraded to major 313 r.compact(false); 314 assertEquals(1, s.getStorefilesCount()); 315 } finally { 316 // reset the timed compaction settings 317 conf.setLong(HConstants.MAJOR_COMPACTION_PERIOD, 1000 * 60 * 60 * 24); 318 conf.setFloat("hbase.hregion.majorcompaction.jitter", 0.20F); 319 // run a major to reset the cache 320 createStoreFile(r); 321 r.compact(true); 322 assertEquals(1, s.getStorefilesCount()); 323 } 324 } 325 326 private void verifyCounts(int countRow1, int countRow2) throws Exception { 327 int count1 = 0; 328 int count2 = 0; 329 for (HStoreFile f : r.getStore(COLUMN_FAMILY_TEXT).getStorefiles()) { 330 try (StoreFileScanner scanner = f.getPreadScanner(false, Long.MAX_VALUE, 0, false)) { 331 scanner.seek(KeyValue.LOWESTKEY); 332 for (Cell cell;;) { 333 cell = scanner.next(); 334 if (cell == null) { 335 break; 336 } 337 byte[] row = CellUtil.cloneRow(cell); 338 if (Bytes.equals(row, STARTROW)) { 339 count1++; 340 } else if (Bytes.equals(row, secondRowBytes)) { 341 count2++; 342 } 343 } 344 } 345 } 346 assertEquals(countRow1, count1); 347 assertEquals(countRow2, count2); 348 } 349 350 private int count() throws IOException { 351 int count = 0; 352 for (HStoreFile f : r.getStore(COLUMN_FAMILY_TEXT).getStorefiles()) { 353 try (StoreFileScanner scanner = f.getPreadScanner(false, Long.MAX_VALUE, 0, false)) { 354 scanner.seek(KeyValue.LOWESTKEY); 355 while (scanner.next() != null) { 356 count++; 357 } 358 } 359 } 360 return count; 361 } 362 363 private void createStoreFile(final HRegion region) throws IOException { 364 createStoreFile(region, Bytes.toString(COLUMN_FAMILY)); 365 } 366 367 private void createStoreFile(final HRegion region, String family) throws IOException { 368 Table loader = new RegionAsTable(region); 369 HTestConst.addContent(loader, family); 370 region.flush(true); 371 } 372 373 private void createSmallerStoreFile(final HRegion region) throws IOException { 374 Table loader = new RegionAsTable(region); 375 HTestConst.addContent(loader, Bytes.toString(COLUMN_FAMILY), Bytes.toBytes("" + "bbb"), null); 376 region.flush(true); 377 } 378 379 /** 380 * Test for HBASE-5920 - Test user requested major compactions always occurring 381 */ 382 @TestTemplate 383 public void testNonUserMajorCompactionRequest() throws Exception { 384 HStore store = r.getStore(COLUMN_FAMILY); 385 createStoreFile(r); 386 for (int i = 0; i < MAX_FILES_TO_COMPACT + 1; i++) { 387 createStoreFile(r); 388 } 389 store.triggerMajorCompaction(); 390 391 CompactionRequestImpl request = store.requestCompaction().get().getRequest(); 392 assertNotNull(request, "Expected to receive a compaction request"); 393 assertEquals(false, request.isMajor(), 394 "System-requested major compaction should not occur if there are too many store files"); 395 } 396 397 /** 398 * Test for HBASE-5920 399 */ 400 @TestTemplate 401 public void testUserMajorCompactionRequest() throws IOException { 402 HStore store = r.getStore(COLUMN_FAMILY); 403 createStoreFile(r); 404 for (int i = 0; i < MAX_FILES_TO_COMPACT + 1; i++) { 405 createStoreFile(r); 406 } 407 store.triggerMajorCompaction(); 408 CompactionRequestImpl request = store 409 .requestCompaction(PRIORITY_USER, CompactionLifeCycleTracker.DUMMY, null).get().getRequest(); 410 assertNotNull(request, "Expected to receive a compaction request"); 411 assertTrue(request.isMajor(), "User-requested major compaction should always occur, " 412 + "even if there are too many store files"); 413 } 414 415 /** 416 * Test that on a major compaction, if all cells are expired or deleted, then we'll end up with no 417 * product. Make sure scanner over region returns right answer in this case - and that it just 418 * basically works. 419 */ 420 @TestTemplate 421 public void testMajorCompactingToNoOutputWithReverseScan() throws IOException { 422 createStoreFile(r); 423 for (int i = 0; i < compactionThreshold; i++) { 424 createStoreFile(r); 425 } 426 // Now delete everything. 427 Scan scan = new Scan(); 428 scan.setReversed(true); 429 InternalScanner s = r.getScanner(scan); 430 do { 431 List<Cell> results = new ArrayList<>(); 432 boolean result = s.next(results); 433 assertTrue(!results.isEmpty()); 434 r.delete(new Delete(CellUtil.cloneRow(results.get(0)))); 435 if (!result) { 436 break; 437 } 438 } while (true); 439 s.close(); 440 // Flush 441 r.flush(true); 442 // Major compact. 443 r.compact(true); 444 scan = new Scan(); 445 scan.setReversed(true); 446 s = r.getScanner(scan); 447 int counter = 0; 448 do { 449 List<Cell> results = new ArrayList<>(); 450 boolean result = s.next(results); 451 if (!result) { 452 break; 453 } 454 counter++; 455 } while (true); 456 s.close(); 457 assertEquals(0, counter); 458 } 459 460 private void testMajorCompactingWithDeletes(KeepDeletedCells keepDeletedCells) 461 throws IOException { 462 createStoreFile(r); 463 for (int i = 0; i < compactionThreshold; i++) { 464 createStoreFile(r); 465 } 466 // Now delete everything. 467 InternalScanner s = r.getScanner(new Scan()); 468 int originalCount = 0; 469 do { 470 List<Cell> results = new ArrayList<>(); 471 boolean result = s.next(results); 472 r.delete(new Delete(CellUtil.cloneRow(results.get(0)))); 473 if (!result) break; 474 originalCount++; 475 } while (true); 476 s.close(); 477 // Flush 478 r.flush(true); 479 480 for (HStore store : this.r.stores.values()) { 481 ScanInfo old = store.getScanInfo(); 482 ScanInfo si = old.customize(old.getMaxVersions(), old.getTtl(), keepDeletedCells); 483 store.setScanInfo(si); 484 } 485 // Major compact. 486 r.compact(true); 487 s = r.getScanner(new Scan().setRaw(true)); 488 int counter = 0; 489 do { 490 List<Cell> results = new ArrayList<>(); 491 boolean result = s.next(results); 492 if (!result) break; 493 counter++; 494 } while (true); 495 assertEquals(keepDeletedCells == KeepDeletedCells.TRUE ? originalCount : 0, counter); 496 497 } 498}