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