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.HConstants.BUCKET_CACHE_SIZE_KEY; 021import static org.apache.hadoop.hbase.io.hfile.bucket.BucketCache.DEFAULT_ERROR_TOLERATION_DURATION; 022import static org.junit.Assert.assertEquals; 023import static org.junit.Assert.assertFalse; 024import static org.junit.Assert.assertNotNull; 025import static org.junit.Assert.assertNull; 026import static org.junit.Assert.assertTrue; 027import static org.junit.Assert.fail; 028 029import java.io.IOException; 030import java.util.ArrayList; 031import java.util.HashMap; 032import java.util.HashSet; 033import java.util.List; 034import java.util.Map; 035import java.util.Optional; 036import java.util.Random; 037import java.util.Set; 038import org.apache.hadoop.conf.Configuration; 039import org.apache.hadoop.fs.FileSystem; 040import org.apache.hadoop.fs.Path; 041import org.apache.hadoop.hbase.HBaseClassTestRule; 042import org.apache.hadoop.hbase.HBaseTestingUtil; 043import org.apache.hadoop.hbase.HConstants; 044import org.apache.hadoop.hbase.KeyValue; 045import org.apache.hadoop.hbase.TableName; 046import org.apache.hadoop.hbase.Waiter; 047import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor; 048import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 049import org.apache.hadoop.hbase.client.RegionInfo; 050import org.apache.hadoop.hbase.client.RegionInfoBuilder; 051import org.apache.hadoop.hbase.client.TableDescriptor; 052import org.apache.hadoop.hbase.client.TableDescriptorBuilder; 053import org.apache.hadoop.hbase.fs.HFileSystem; 054import org.apache.hadoop.hbase.io.encoding.DataBlockEncoding; 055import org.apache.hadoop.hbase.io.hfile.BlockCache; 056import org.apache.hadoop.hbase.io.hfile.BlockCacheFactory; 057import org.apache.hadoop.hbase.io.hfile.BlockCacheKey; 058import org.apache.hadoop.hbase.io.hfile.BlockType; 059import org.apache.hadoop.hbase.io.hfile.BlockType.BlockCategory; 060import org.apache.hadoop.hbase.io.hfile.CacheConfig; 061import org.apache.hadoop.hbase.io.hfile.CacheTestUtils; 062import org.apache.hadoop.hbase.io.hfile.HFileBlock; 063import org.apache.hadoop.hbase.io.hfile.HFileContextBuilder; 064import org.apache.hadoop.hbase.io.hfile.bucket.BucketCache; 065import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTracker; 066import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTrackerFactory; 067import org.apache.hadoop.hbase.testclassification.RegionServerTests; 068import org.apache.hadoop.hbase.testclassification.SmallTests; 069import org.apache.hadoop.hbase.util.Bytes; 070import org.apache.hadoop.hbase.util.CommonFSUtils; 071import org.apache.hadoop.hbase.util.Pair; 072import org.junit.BeforeClass; 073import org.junit.ClassRule; 074import org.junit.Test; 075import org.junit.experimental.categories.Category; 076import org.slf4j.Logger; 077import org.slf4j.LoggerFactory; 078 079/** 080 * This class is used to test the functionality of the DataTieringManager. 081 * 082 * The mock online regions are stored in {@link TestCustomCellDataTieringManager#testOnlineRegions}. 083 * For all tests, the setup of 084 * {@link TestCustomCellDataTieringManager#testOnlineRegions} occurs only once. 085 * Please refer to {@link TestCustomCellDataTieringManager#setupOnlineRegions()} for the structure. 086 * Additionally, a list of all store files is 087 * maintained in {@link TestCustomCellDataTieringManager#hStoreFiles}. 088 * The characteristics of these store files are listed below: 089 * @formatter:off 090 * ## HStoreFile Information 091 * | HStoreFile | Region | Store | DataTiering | isHot | 092 * |------------------|--------------------|---------------------|-----------------------|-------| 093 * | hStoreFile0 | region1 | hStore11 | CUSTOM_CELL_VALUE | true | 094 * | hStoreFile1 | region1 | hStore12 | NONE | true | 095 * | hStoreFile2 | region2 | hStore21 | CUSTOM_CELL_VALUE | true | 096 * | hStoreFile3 | region2 | hStore22 | CUSTOM_CELL_VALUE | false | 097 * @formatter:on 098 */ 099 100@Category({ RegionServerTests.class, SmallTests.class }) 101public class TestCustomCellDataTieringManager { 102 103 @ClassRule 104 public static final HBaseClassTestRule CLASS_RULE = 105 HBaseClassTestRule.forClass(TestCustomCellDataTieringManager.class); 106 107 private static final Logger LOG = LoggerFactory.getLogger(TestCustomCellDataTieringManager.class); 108 private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil(); 109 private static final long DAY = 24 * 60 * 60 * 1000; 110 private static Configuration defaultConf; 111 private static FileSystem fs; 112 private BlockCache blockCache; 113 private static CacheConfig cacheConf; 114 private static Path testDir; 115 private static final Map<String, HRegion> testOnlineRegions = new HashMap<>(); 116 117 private static DataTieringManager dataTieringManager; 118 private static final List<HStoreFile> hStoreFiles = new ArrayList<>(); 119 120 /** 121 * Represents the current lexicographically increasing string used as a row key when writing 122 * HFiles. It is incremented each time {@link #nextString()} is called to generate unique row 123 * keys. 124 */ 125 private static String rowKeyString; 126 127 @BeforeClass 128 public static void setupBeforeClass() throws Exception { 129 testDir = TEST_UTIL.getDataTestDir(TestCustomCellDataTieringManager.class.getSimpleName()); 130 defaultConf = TEST_UTIL.getConfiguration(); 131 updateCommonConfigurations(); 132 DataTieringManager.instantiate(defaultConf, testOnlineRegions); 133 dataTieringManager = DataTieringManager.getInstance(); 134 rowKeyString = ""; 135 } 136 137 private static void updateCommonConfigurations() { 138 defaultConf.setBoolean(DataTieringManager.GLOBAL_DATA_TIERING_ENABLED_KEY, true); 139 defaultConf.setStrings(HConstants.BUCKET_CACHE_IOENGINE_KEY, "offheap"); 140 defaultConf.setLong(BUCKET_CACHE_SIZE_KEY, 32); 141 } 142 143 @FunctionalInterface 144 interface DataTieringMethodCallerWithPath { 145 boolean call(DataTieringManager manager, Path path) throws DataTieringException; 146 } 147 148 @FunctionalInterface 149 interface DataTieringMethodCallerWithKey { 150 boolean call(DataTieringManager manager, BlockCacheKey key) throws DataTieringException; 151 } 152 153 @Test 154 public void testDataTieringEnabledWithKey() throws IOException { 155 initializeTestEnvironment(); 156 DataTieringMethodCallerWithKey methodCallerWithKey = DataTieringManager::isDataTieringEnabled; 157 158 // Test with valid key 159 BlockCacheKey key = new BlockCacheKey(hStoreFiles.get(0).getPath(), 0, true, BlockType.DATA); 160 testDataTieringMethodWithKeyNoException(methodCallerWithKey, key, true); 161 162 // Test with another valid key 163 key = new BlockCacheKey(hStoreFiles.get(1).getPath(), 0, true, BlockType.DATA); 164 testDataTieringMethodWithKeyNoException(methodCallerWithKey, key, false); 165 166 } 167 168 @Test 169 public void testDataTieringEnabledWithPath() throws IOException { 170 initializeTestEnvironment(); 171 DataTieringMethodCallerWithPath methodCallerWithPath = DataTieringManager::isDataTieringEnabled; 172 173 // Test with valid path 174 Path hFilePath = hStoreFiles.get(1).getPath(); 175 testDataTieringMethodWithPathNoException(methodCallerWithPath, hFilePath, false); 176 177 // Test with another valid path 178 hFilePath = hStoreFiles.get(3).getPath(); 179 testDataTieringMethodWithPathNoException(methodCallerWithPath, hFilePath, true); 180 181 // Test with an incorrect path 182 hFilePath = new Path("incorrectPath"); 183 testDataTieringMethodWithPathExpectingException(methodCallerWithPath, hFilePath, 184 new DataTieringException("Incorrect HFile Path: " + hFilePath)); 185 186 // Test with a non-existing HRegion path 187 Path basePath = hStoreFiles.get(0).getPath().getParent().getParent().getParent(); 188 hFilePath = new Path(basePath, "incorrectRegion/cf1/filename"); 189 testDataTieringMethodWithPathExpectingException(methodCallerWithPath, hFilePath, 190 new DataTieringException("HRegion corresponding to incorrectRegion doesn't exist")); 191 192 // Test with a non-existing HStore path 193 basePath = hStoreFiles.get(0).getPath().getParent().getParent(); 194 hFilePath = new Path(basePath, "incorrectCf/filename"); 195 testDataTieringMethodWithPathExpectingException(methodCallerWithPath, hFilePath, 196 new DataTieringException( 197 "HStore corresponding to " + basePath.getName() + "/incorrectCf doesn't exist")); 198 } 199 200 @Test 201 public void testHotDataWithKey() throws IOException { 202 initializeTestEnvironment(); 203 DataTieringMethodCallerWithKey methodCallerWithKey = DataTieringManager::isHotData; 204 205 // Test with valid key 206 BlockCacheKey key = new BlockCacheKey(hStoreFiles.get(0).getPath(), 0, true, BlockType.DATA); 207 testDataTieringMethodWithKeyNoException(methodCallerWithKey, key, true); 208 209 // Test with another valid key 210 key = new BlockCacheKey(hStoreFiles.get(3).getPath(), 0, true, BlockType.DATA); 211 testDataTieringMethodWithKeyNoException(methodCallerWithKey, key, false); 212 } 213 214 @Test 215 public void testPrefetchWhenDataTieringEnabled() throws IOException { 216 setPrefetchBlocksOnOpen(); 217 this.blockCache = initializeTestEnvironment(); 218 // Evict blocks from cache by closing the files and passing evict on close. 219 // Then initialize the reader again. Since Prefetch on open is set to true, it should prefetch 220 // those blocks. 221 for (HStoreFile file : hStoreFiles) { 222 file.closeStoreFile(true); 223 file.initReader(); 224 } 225 226 // Since we have one cold file among four files, only three should get prefetched. 227 Optional<Map<String, Pair<String, Long>>> fullyCachedFiles = blockCache.getFullyCachedFiles(); 228 assertTrue("We should get the fully cached files from the cache", fullyCachedFiles.isPresent()); 229 Waiter.waitFor(defaultConf, 10000, () -> fullyCachedFiles.get().size() == 3); 230 assertEquals("Number of fully cached files are incorrect", 3, fullyCachedFiles.get().size()); 231 } 232 233 private void setPrefetchBlocksOnOpen() { 234 defaultConf.setBoolean(CacheConfig.PREFETCH_BLOCKS_ON_OPEN_KEY, true); 235 } 236 237 @Test 238 public void testColdDataFiles() throws IOException { 239 initializeTestEnvironment(); 240 Set<BlockCacheKey> allCachedBlocks = new HashSet<>(); 241 for (HStoreFile file : hStoreFiles) { 242 allCachedBlocks.add(new BlockCacheKey(file.getPath(), 0, true, BlockType.DATA)); 243 } 244 245 // Verify hStoreFile3 is identified as cold data 246 DataTieringMethodCallerWithKey methodCallerWithKey = DataTieringManager::isHotData; 247 Path hFilePath = hStoreFiles.get(3).getPath(); 248 testDataTieringMethodWithKeyNoException(methodCallerWithKey, 249 new BlockCacheKey(hFilePath, 0, true, BlockType.DATA), false); 250 251 // Verify all the other files in hStoreFiles are hot data 252 for (int i = 0; i < hStoreFiles.size() - 1; i++) { 253 hFilePath = hStoreFiles.get(i).getPath(); 254 testDataTieringMethodWithKeyNoException(methodCallerWithKey, 255 new BlockCacheKey(hFilePath, 0, true, BlockType.DATA), true); 256 } 257 258 try { 259 Set<String> coldFilePaths = dataTieringManager.getColdDataFiles(allCachedBlocks); 260 assertEquals(1, coldFilePaths.size()); 261 } catch (DataTieringException e) { 262 fail("Unexpected DataTieringException: " + e.getMessage()); 263 } 264 } 265 266 @Test 267 public void testCacheCompactedBlocksOnWriteDataTieringDisabled() throws IOException { 268 setCacheCompactBlocksOnWrite(); 269 this.blockCache = initializeTestEnvironment(); 270 HRegion region = createHRegion("table3", this.blockCache); 271 testCacheCompactedBlocksOnWrite(region, true); 272 } 273 274 @Test 275 public void testCacheCompactedBlocksOnWriteWithHotData() throws IOException { 276 setCacheCompactBlocksOnWrite(); 277 this.blockCache = initializeTestEnvironment(); 278 HRegion region = 279 createHRegion("table3", getConfWithCustomCellDataTieringEnabled(5 * DAY), this.blockCache); 280 testCacheCompactedBlocksOnWrite(region, true); 281 } 282 283 @Test 284 public void testCacheCompactedBlocksOnWriteWithColdData() throws IOException { 285 setCacheCompactBlocksOnWrite(); 286 this.blockCache = initializeTestEnvironment(); 287 HRegion region = 288 createHRegion("table3", getConfWithCustomCellDataTieringEnabled(DAY), this.blockCache); 289 testCacheCompactedBlocksOnWrite(region, false); 290 } 291 292 private void setCacheCompactBlocksOnWrite() { 293 defaultConf.setBoolean(CacheConfig.CACHE_COMPACTED_BLOCKS_ON_WRITE_KEY, true); 294 } 295 296 private void testCacheCompactedBlocksOnWrite(HRegion region, boolean expectDataBlocksCached) 297 throws IOException { 298 HStore hStore = createHStore(region, "cf1"); 299 createTestFilesForCompaction(hStore); 300 hStore.refreshStoreFiles(); 301 302 region.stores.put(Bytes.toBytes("cf1"), hStore); 303 testOnlineRegions.put(region.getRegionInfo().getEncodedName(), region); 304 305 long initialStoreFilesCount = hStore.getStorefilesCount(); 306 long initialCacheDataBlockCount = blockCache.getDataBlockCount(); 307 assertEquals(3, initialStoreFilesCount); 308 assertEquals(0, initialCacheDataBlockCount); 309 310 region.compact(true); 311 312 long compactedStoreFilesCount = hStore.getStorefilesCount(); 313 long compactedCacheDataBlockCount = blockCache.getDataBlockCount(); 314 assertEquals(1, compactedStoreFilesCount); 315 assertEquals(expectDataBlocksCached, compactedCacheDataBlockCount > 0); 316 } 317 318 private void createTestFilesForCompaction(HStore hStore) throws IOException { 319 long currentTime = System.currentTimeMillis(); 320 Path storeDir = hStore.getStoreContext().getFamilyStoreDirectoryPath(); 321 Configuration configuration = hStore.getReadOnlyConfiguration(); 322 323 HRegionFileSystem regionFS = hStore.getHRegion().getRegionFileSystem(); 324 325 createHStoreFile(storeDir, configuration, currentTime - 2 * DAY, regionFS); 326 createHStoreFile(storeDir, configuration, currentTime - 3 * DAY, regionFS); 327 createHStoreFile(storeDir, configuration, currentTime - 4 * DAY, regionFS); 328 } 329 330 @Test 331 public void testPickColdDataFiles() throws IOException { 332 initializeTestEnvironment(); 333 Map<String, String> coldDataFiles = dataTieringManager.getColdFilesList(); 334 assertEquals(1, coldDataFiles.size()); 335 // hStoreFiles[3] is the cold file. 336 assert (coldDataFiles.containsKey(hStoreFiles.get(3).getFileInfo().getActiveFileName())); 337 } 338 339 /* 340 * Verify that two cold blocks(both) are evicted when bucket reaches its capacity. The hot file 341 * remains in the cache. 342 */ 343 @Test 344 public void testBlockEvictions() throws Exception { 345 initializeTestEnvironment(); 346 long capacitySize = 40 * 1024; 347 int writeThreads = 3; 348 int writerQLen = 64; 349 int[] bucketSizes = new int[] { 8 * 1024 + 1024 }; 350 351 // Setup: Create a bucket cache with lower capacity 352 BucketCache bucketCache = 353 new BucketCache("file:" + testDir + "/bucket.cache", capacitySize, 8192, bucketSizes, 354 writeThreads, writerQLen, null, DEFAULT_ERROR_TOLERATION_DURATION, defaultConf); 355 356 // Create three Cache keys with cold data files and a block with hot data. 357 // hStoreFiles.get(3) is a cold data file, while hStoreFiles.get(0) is a hot file. 358 Set<BlockCacheKey> cacheKeys = new HashSet<>(); 359 cacheKeys.add(new BlockCacheKey(hStoreFiles.get(3).getPath(), 0, true, BlockType.DATA)); 360 cacheKeys.add(new BlockCacheKey(hStoreFiles.get(3).getPath(), 8192, true, BlockType.DATA)); 361 cacheKeys.add(new BlockCacheKey(hStoreFiles.get(0).getPath(), 0, true, BlockType.DATA)); 362 363 // Create dummy data to be cached and fill the cache completely. 364 CacheTestUtils.HFileBlockPair[] blocks = CacheTestUtils.generateHFileBlocks(8192, 3); 365 366 int blocksIter = 0; 367 for (BlockCacheKey key : cacheKeys) { 368 bucketCache.cacheBlock(key, blocks[blocksIter++].getBlock()); 369 // Ensure that the block is persisted to the file. 370 Waiter.waitFor(defaultConf, 10000, 100, () -> (bucketCache.getBackingMap().containsKey(key))); 371 } 372 373 // Verify that the bucket cache contains 3 blocks. 374 assertEquals(3, bucketCache.getBackingMap().keySet().size()); 375 376 // Add an additional block into cache with hot data which should trigger the eviction 377 BlockCacheKey newKey = new BlockCacheKey(hStoreFiles.get(2).getPath(), 0, true, BlockType.DATA); 378 CacheTestUtils.HFileBlockPair[] newBlock = CacheTestUtils.generateHFileBlocks(8192, 1); 379 380 bucketCache.cacheBlock(newKey, newBlock[0].getBlock()); 381 Waiter.waitFor(defaultConf, 10000, 100, 382 () -> (bucketCache.getBackingMap().containsKey(newKey))); 383 384 // Verify that the bucket cache now contains 2 hot blocks blocks only. 385 // Both cold blocks of 8KB will be evicted to make room for 1 block of 8KB + an additional 386 // space. 387 validateBlocks(bucketCache.getBackingMap().keySet(), 2, 2, 0); 388 } 389 390 /* 391 * Verify that two cold blocks(both) are evicted when bucket reaches its capacity, but one cold 392 * block remains in the cache since the required space is freed. 393 */ 394 @Test 395 public void testBlockEvictionsAllColdBlocks() throws Exception { 396 initializeTestEnvironment(); 397 long capacitySize = 40 * 1024; 398 int writeThreads = 3; 399 int writerQLen = 64; 400 int[] bucketSizes = new int[] { 8 * 1024 + 1024 }; 401 402 // Setup: Create a bucket cache with lower capacity 403 BucketCache bucketCache = 404 new BucketCache("file:" + testDir + "/bucket.cache", capacitySize, 8192, bucketSizes, 405 writeThreads, writerQLen, null, DEFAULT_ERROR_TOLERATION_DURATION, defaultConf); 406 407 // Create three Cache keys with three cold data blocks. 408 // hStoreFiles.get(3) is a cold data file. 409 Set<BlockCacheKey> cacheKeys = new HashSet<>(); 410 cacheKeys.add(new BlockCacheKey(hStoreFiles.get(3).getPath(), 0, true, BlockType.DATA)); 411 cacheKeys.add(new BlockCacheKey(hStoreFiles.get(3).getPath(), 8192, true, BlockType.DATA)); 412 cacheKeys.add(new BlockCacheKey(hStoreFiles.get(3).getPath(), 16384, true, BlockType.DATA)); 413 414 // Create dummy data to be cached and fill the cache completely. 415 CacheTestUtils.HFileBlockPair[] blocks = CacheTestUtils.generateHFileBlocks(8192, 3); 416 417 int blocksIter = 0; 418 for (BlockCacheKey key : cacheKeys) { 419 bucketCache.cacheBlock(key, blocks[blocksIter++].getBlock()); 420 // Ensure that the block is persisted to the file. 421 Waiter.waitFor(defaultConf, 10000, 100, () -> (bucketCache.getBackingMap().containsKey(key))); 422 } 423 424 // Verify that the bucket cache contains 3 blocks. 425 assertEquals(3, bucketCache.getBackingMap().keySet().size()); 426 427 // Add an additional block into cache with hot data which should trigger the eviction 428 BlockCacheKey newKey = new BlockCacheKey(hStoreFiles.get(2).getPath(), 0, true, BlockType.DATA); 429 CacheTestUtils.HFileBlockPair[] newBlock = CacheTestUtils.generateHFileBlocks(8192, 1); 430 431 bucketCache.cacheBlock(newKey, newBlock[0].getBlock()); 432 Waiter.waitFor(defaultConf, 10000, 100, 433 () -> (bucketCache.getBackingMap().containsKey(newKey))); 434 435 // Verify that the bucket cache now contains 1 cold block and a newly added hot block. 436 validateBlocks(bucketCache.getBackingMap().keySet(), 2, 1, 1); 437 } 438 439 /* 440 * Verify that a hot block evicted along with a cold block when bucket reaches its capacity. 441 */ 442 @Test 443 public void testBlockEvictionsHotBlocks() throws Exception { 444 initializeTestEnvironment(); 445 long capacitySize = 40 * 1024; 446 int writeThreads = 3; 447 int writerQLen = 64; 448 int[] bucketSizes = new int[] { 8 * 1024 + 1024 }; 449 450 // Setup: Create a bucket cache with lower capacity 451 BucketCache bucketCache = 452 new BucketCache("file:" + testDir + "/bucket.cache", capacitySize, 8192, bucketSizes, 453 writeThreads, writerQLen, null, DEFAULT_ERROR_TOLERATION_DURATION, defaultConf); 454 455 // Create three Cache keys with two hot data blocks and one cold data block 456 // hStoreFiles.get(0) is a hot data file and hStoreFiles.get(3) is a cold data file. 457 Set<BlockCacheKey> cacheKeys = new HashSet<>(); 458 cacheKeys.add(new BlockCacheKey(hStoreFiles.get(0).getPath(), 0, true, BlockType.DATA)); 459 cacheKeys.add(new BlockCacheKey(hStoreFiles.get(0).getPath(), 8192, true, BlockType.DATA)); 460 cacheKeys.add(new BlockCacheKey(hStoreFiles.get(3).getPath(), 0, true, BlockType.DATA)); 461 462 // Create dummy data to be cached and fill the cache completely. 463 CacheTestUtils.HFileBlockPair[] blocks = CacheTestUtils.generateHFileBlocks(8192, 3); 464 465 int blocksIter = 0; 466 for (BlockCacheKey key : cacheKeys) { 467 bucketCache.cacheBlock(key, blocks[blocksIter++].getBlock()); 468 // Ensure that the block is persisted to the file. 469 Waiter.waitFor(defaultConf, 10000, 100, () -> (bucketCache.getBackingMap().containsKey(key))); 470 } 471 472 // Verify that the bucket cache contains 3 blocks. 473 assertEquals(3, bucketCache.getBackingMap().keySet().size()); 474 475 // Add an additional block which should evict the only cold block with an additional hot block. 476 BlockCacheKey newKey = new BlockCacheKey(hStoreFiles.get(2).getPath(), 0, true, BlockType.DATA); 477 CacheTestUtils.HFileBlockPair[] newBlock = CacheTestUtils.generateHFileBlocks(8192, 1); 478 479 bucketCache.cacheBlock(newKey, newBlock[0].getBlock()); 480 Waiter.waitFor(defaultConf, 10000, 100, 481 () -> (bucketCache.getBackingMap().containsKey(newKey))); 482 483 // Verify that the bucket cache now contains 2 hot blocks. 484 // Only one of the older hot blocks is retained and other one is the newly added hot block. 485 validateBlocks(bucketCache.getBackingMap().keySet(), 2, 2, 0); 486 } 487 488 @Test 489 public void testFeatureKeyDisabled() throws Exception { 490 DataTieringManager.resetForTestingOnly(); 491 defaultConf.setBoolean(DataTieringManager.GLOBAL_DATA_TIERING_ENABLED_KEY, false); 492 initializeTestEnvironment(); 493 494 try { 495 assertFalse(DataTieringManager.instantiate(defaultConf, testOnlineRegions)); 496 // Verify that the DataaTieringManager instance is not instantiated in the 497 // instantiate call above. 498 assertNull(DataTieringManager.getInstance()); 499 500 // Also validate that data temperature is not honoured. 501 long capacitySize = 40 * 1024; 502 int writeThreads = 3; 503 int writerQLen = 64; 504 int[] bucketSizes = new int[] { 8 * 1024 + 1024 }; 505 506 // Setup: Create a bucket cache with lower capacity 507 BucketCache bucketCache = 508 new BucketCache("file:" + testDir + "/bucket.cache", capacitySize, 8192, bucketSizes, 509 writeThreads, writerQLen, null, DEFAULT_ERROR_TOLERATION_DURATION, defaultConf); 510 511 // Create three Cache keys with two hot data blocks and one cold data block 512 // hStoreFiles.get(0) is a hot data file and hStoreFiles.get(3) is a cold data file. 513 List<BlockCacheKey> cacheKeys = new ArrayList<>(); 514 cacheKeys.add(new BlockCacheKey(hStoreFiles.get(0).getPath(), 0, true, BlockType.DATA)); 515 cacheKeys.add(new BlockCacheKey(hStoreFiles.get(0).getPath(), 8192, true, BlockType.DATA)); 516 cacheKeys.add(new BlockCacheKey(hStoreFiles.get(3).getPath(), 0, true, BlockType.DATA)); 517 518 // Create dummy data to be cached and fill the cache completely. 519 CacheTestUtils.HFileBlockPair[] blocks = CacheTestUtils.generateHFileBlocks(8192, 3); 520 521 int blocksIter = 0; 522 for (BlockCacheKey key : cacheKeys) { 523 LOG.info("Adding {}", key); 524 bucketCache.cacheBlock(key, blocks[blocksIter++].getBlock()); 525 // Ensure that the block is persisted to the file. 526 Waiter.waitFor(defaultConf, 10000, 100, 527 () -> (bucketCache.getBackingMap().containsKey(key))); 528 } 529 530 // Verify that the bucket cache contains 3 blocks. 531 assertEquals(3, bucketCache.getBackingMap().keySet().size()); 532 533 // Add an additional hot block, which triggers eviction. 534 BlockCacheKey newKey = 535 new BlockCacheKey(hStoreFiles.get(2).getPath(), 0, true, BlockType.DATA); 536 CacheTestUtils.HFileBlockPair[] newBlock = CacheTestUtils.generateHFileBlocks(8192, 1); 537 538 bucketCache.cacheBlock(newKey, newBlock[0].getBlock()); 539 Waiter.waitFor(defaultConf, 10000, 100, 540 () -> (bucketCache.getBackingMap().containsKey(newKey))); 541 542 // Verify that the bucket still contains the only cold block and one newly added hot block. 543 // The older hot blocks are evicted and data-tiering mechanism does not kick in to evict 544 // the cold block. 545 validateBlocks(bucketCache.getBackingMap().keySet(), 2, 1, 1); 546 } finally { 547 DataTieringManager.resetForTestingOnly(); 548 defaultConf.setBoolean(DataTieringManager.GLOBAL_DATA_TIERING_ENABLED_KEY, true); 549 assertTrue(DataTieringManager.instantiate(defaultConf, testOnlineRegions)); 550 } 551 } 552 553 @Test 554 public void testCacheConfigShouldCacheFile() throws Exception { 555 initializeTestEnvironment(); 556 // Verify that the API shouldCacheFileBlock returns the result correctly. 557 // hStoreFiles[0], hStoreFiles[1], hStoreFiles[2] are hot files. 558 // hStoreFiles[3] is a cold file. 559 assertTrue(cacheConf.shouldCacheBlockOnRead(BlockCategory.DATA, 560 hStoreFiles.get(0).getFileInfo().getHFileInfo(), hStoreFiles.get(0).getFileInfo().getConf())); 561 assertTrue(cacheConf.shouldCacheBlockOnRead(BlockCategory.DATA, 562 hStoreFiles.get(1).getFileInfo().getHFileInfo(), hStoreFiles.get(1).getFileInfo().getConf())); 563 assertTrue(cacheConf.shouldCacheBlockOnRead(BlockCategory.DATA, 564 hStoreFiles.get(2).getFileInfo().getHFileInfo(), hStoreFiles.get(2).getFileInfo().getConf())); 565 assertFalse(cacheConf.shouldCacheBlockOnRead(BlockCategory.DATA, 566 hStoreFiles.get(3).getFileInfo().getHFileInfo(), hStoreFiles.get(3).getFileInfo().getConf())); 567 } 568 569 @Test 570 public void testCacheOnReadColdFile() throws Exception { 571 this.blockCache = initializeTestEnvironment(); 572 // hStoreFiles[3] is a cold file. the blocks should not get loaded after a readBlock call. 573 HStoreFile hStoreFile = hStoreFiles.get(3); 574 BlockCacheKey cacheKey = new BlockCacheKey(hStoreFile.getPath(), 0, true, BlockType.DATA); 575 testCacheOnRead(hStoreFile, cacheKey, -1, false); 576 } 577 578 @Test 579 public void testCacheOnReadHotFile() throws Exception { 580 this.blockCache = initializeTestEnvironment(); 581 // hStoreFiles[0] is a hot file. the blocks should get loaded after a readBlock call. 582 HStoreFile hStoreFile = hStoreFiles.get(0); 583 BlockCacheKey cacheKey = 584 new BlockCacheKey(hStoreFiles.get(0).getPath(), 0, true, BlockType.DATA); 585 testCacheOnRead(hStoreFile, cacheKey, -1, true); 586 } 587 588 private void testCacheOnRead(HStoreFile hStoreFile, BlockCacheKey key, long onDiskBlockSize, 589 boolean expectedCached) throws Exception { 590 // Execute the read block API which will try to cache the block if the block is a hot block. 591 hStoreFile.getReader().getHFileReader().readBlock(key.getOffset(), onDiskBlockSize, true, false, 592 false, false, key.getBlockType(), DataBlockEncoding.NONE); 593 // Validate that the hot block gets cached and cold block is not cached. 594 HFileBlock block = (HFileBlock) blockCache.getBlock(key, false, false, false); 595 if (expectedCached) { 596 assertNotNull(block); 597 } else { 598 assertNull(block); 599 } 600 } 601 602 private void validateBlocks(Set<BlockCacheKey> keys, int expectedTotalKeys, int expectedHotBlocks, 603 int expectedColdBlocks) { 604 int numHotBlocks = 0, numColdBlocks = 0; 605 606 Waiter.waitFor(defaultConf, 10000, 100, () -> (expectedTotalKeys == keys.size())); 607 int iter = 0; 608 for (BlockCacheKey key : keys) { 609 try { 610 if (dataTieringManager.isHotData(key)) { 611 numHotBlocks++; 612 } else { 613 numColdBlocks++; 614 } 615 } catch (Exception e) { 616 LOG.debug("Error validating priority for key {}", key, e); 617 fail(e.getMessage()); 618 } 619 } 620 assertEquals(expectedHotBlocks, numHotBlocks); 621 assertEquals(expectedColdBlocks, numColdBlocks); 622 } 623 624 private void testDataTieringMethodWithPath(DataTieringMethodCallerWithPath caller, Path path, 625 boolean expectedResult, DataTieringException exception) { 626 try { 627 boolean value = caller.call(dataTieringManager, path); 628 if (exception != null) { 629 fail("Expected DataTieringException to be thrown"); 630 } 631 assertEquals(expectedResult, value); 632 } catch (DataTieringException e) { 633 if (exception == null) { 634 fail("Unexpected DataTieringException: " + e.getMessage()); 635 } 636 assertEquals(exception.getMessage(), e.getMessage()); 637 } 638 } 639 640 private void testDataTieringMethodWithKey(DataTieringMethodCallerWithKey caller, 641 BlockCacheKey key, boolean expectedResult, DataTieringException exception) { 642 try { 643 boolean value = caller.call(dataTieringManager, key); 644 if (exception != null) { 645 fail("Expected DataTieringException to be thrown"); 646 } 647 assertEquals(expectedResult, value); 648 } catch (DataTieringException e) { 649 if (exception == null) { 650 fail("Unexpected DataTieringException: " + e.getMessage()); 651 } 652 assertEquals(exception.getMessage(), e.getMessage()); 653 } 654 } 655 656 private void testDataTieringMethodWithPathExpectingException( 657 DataTieringMethodCallerWithPath caller, Path path, DataTieringException exception) { 658 testDataTieringMethodWithPath(caller, path, false, exception); 659 } 660 661 private void testDataTieringMethodWithPathNoException(DataTieringMethodCallerWithPath caller, 662 Path path, boolean expectedResult) { 663 testDataTieringMethodWithPath(caller, path, expectedResult, null); 664 } 665 666 private void testDataTieringMethodWithKeyExpectingException(DataTieringMethodCallerWithKey caller, 667 BlockCacheKey key, DataTieringException exception) { 668 testDataTieringMethodWithKey(caller, key, false, exception); 669 } 670 671 private void testDataTieringMethodWithKeyNoException(DataTieringMethodCallerWithKey caller, 672 BlockCacheKey key, boolean expectedResult) { 673 testDataTieringMethodWithKey(caller, key, expectedResult, null); 674 } 675 676 private static BlockCache initializeTestEnvironment() throws IOException { 677 BlockCache blockCache = setupFileSystemAndCache(); 678 setupOnlineRegions(blockCache); 679 return blockCache; 680 } 681 682 private static BlockCache setupFileSystemAndCache() throws IOException { 683 fs = HFileSystem.get(defaultConf); 684 BlockCache blockCache = BlockCacheFactory.createBlockCache(defaultConf); 685 cacheConf = new CacheConfig(defaultConf, blockCache); 686 return blockCache; 687 } 688 689 private static void setupOnlineRegions(BlockCache blockCache) throws IOException { 690 testOnlineRegions.clear(); 691 hStoreFiles.clear(); 692 long day = 24 * 60 * 60 * 1000; 693 long currentTime = System.currentTimeMillis(); 694 695 HRegion region1 = createHRegion("table1", blockCache); 696 697 HStore hStore11 = createHStore(region1, "cf1", getConfWithCustomCellDataTieringEnabled(day)); 698 hStoreFiles.add(createHStoreFile(hStore11.getStoreContext().getFamilyStoreDirectoryPath(), 699 hStore11.getReadOnlyConfiguration(), currentTime, region1.getRegionFileSystem())); 700 hStore11.refreshStoreFiles(); 701 HStore hStore12 = createHStore(region1, "cf2"); 702 hStoreFiles.add(createHStoreFile(hStore12.getStoreContext().getFamilyStoreDirectoryPath(), 703 hStore12.getReadOnlyConfiguration(), currentTime - day, region1.getRegionFileSystem())); 704 hStore12.refreshStoreFiles(); 705 706 region1.stores.put(Bytes.toBytes("cf1"), hStore11); 707 region1.stores.put(Bytes.toBytes("cf2"), hStore12); 708 709 HRegion region2 = createHRegion("table2", 710 getConfWithCustomCellDataTieringEnabled((long) (2.5 * day)), blockCache); 711 712 HStore hStore21 = createHStore(region2, "cf1"); 713 hStoreFiles.add(createHStoreFile(hStore21.getStoreContext().getFamilyStoreDirectoryPath(), 714 hStore21.getReadOnlyConfiguration(), currentTime - 2 * day, region2.getRegionFileSystem())); 715 hStore21.refreshStoreFiles(); 716 HStore hStore22 = createHStore(region2, "cf2"); 717 hStoreFiles.add(createHStoreFile(hStore22.getStoreContext().getFamilyStoreDirectoryPath(), 718 hStore22.getReadOnlyConfiguration(), currentTime - 3 * day, region2.getRegionFileSystem())); 719 hStore22.refreshStoreFiles(); 720 721 region2.stores.put(Bytes.toBytes("cf1"), hStore21); 722 region2.stores.put(Bytes.toBytes("cf2"), hStore22); 723 724 for (HStoreFile file : hStoreFiles) { 725 file.initReader(); 726 } 727 728 testOnlineRegions.put(region1.getRegionInfo().getEncodedName(), region1); 729 testOnlineRegions.put(region2.getRegionInfo().getEncodedName(), region2); 730 } 731 732 private static HRegion createHRegion(String table, BlockCache blockCache) throws IOException { 733 return createHRegion(table, defaultConf, blockCache); 734 } 735 736 private static HRegion createHRegion(String table, Configuration conf, BlockCache blockCache) 737 throws IOException { 738 TableName tableName = TableName.valueOf(table); 739 740 TableDescriptor htd = TableDescriptorBuilder.newBuilder(tableName) 741 .setValue(DataTieringManager.DATATIERING_KEY, conf.get(DataTieringManager.DATATIERING_KEY)) 742 .setValue(DataTieringManager.DATATIERING_HOT_DATA_AGE_KEY, 743 conf.get(DataTieringManager.DATATIERING_HOT_DATA_AGE_KEY)) 744 .build(); 745 RegionInfo hri = RegionInfoBuilder.newBuilder(tableName).build(); 746 747 Configuration testConf = new Configuration(conf); 748 CommonFSUtils.setRootDir(testConf, testDir); 749 HRegionFileSystem regionFs = HRegionFileSystem.createRegionOnFileSystem(testConf, fs, 750 CommonFSUtils.getTableDir(testDir, hri.getTable()), hri); 751 752 HRegion region = new HRegion(regionFs, null, conf, htd, null); 753 // Manually sets the BlockCache for the HRegion instance. 754 // This is necessary because the region server is not started within this method, 755 // and therefore the BlockCache needs to be explicitly configured. 756 region.setBlockCache(blockCache); 757 return region; 758 } 759 760 private static HStore createHStore(HRegion region, String columnFamily) throws IOException { 761 return createHStore(region, columnFamily, defaultConf); 762 } 763 764 private static HStore createHStore(HRegion region, String columnFamily, Configuration conf) 765 throws IOException { 766 ColumnFamilyDescriptor columnFamilyDescriptor = 767 ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(columnFamily)) 768 .setValue(DataTieringManager.DATATIERING_KEY, conf.get(DataTieringManager.DATATIERING_KEY)) 769 .setValue(DataTieringManager.DATATIERING_HOT_DATA_AGE_KEY, 770 conf.get(DataTieringManager.DATATIERING_HOT_DATA_AGE_KEY)) 771 .build(); 772 773 return new HStore(region, columnFamilyDescriptor, conf, false); 774 } 775 776 private static HStoreFile createHStoreFile(Path storeDir, Configuration conf, long timestamp, 777 HRegionFileSystem regionFs) throws IOException { 778 String columnFamily = storeDir.getName(); 779 780 StoreFileWriter storeFileWriter = new StoreFileWriter.Builder(conf, cacheConf, fs) 781 .withOutputDir(storeDir).withFileContext(new HFileContextBuilder().build()).build(); 782 783 writeStoreFileRandomData(storeFileWriter, Bytes.toBytes(columnFamily), timestamp); 784 785 StoreContext storeContext = StoreContext.getBuilder().withRegionFileSystem(regionFs).build(); 786 StoreFileTracker sft = StoreFileTrackerFactory.create(conf, true, storeContext); 787 return new HStoreFile(fs, storeFileWriter.getPath(), conf, cacheConf, BloomType.NONE, true, 788 sft); 789 } 790 791 private static Configuration getConfWithCustomCellDataTieringEnabled(long hotDataAge) { 792 Configuration conf = new Configuration(defaultConf); 793 conf.set(DataTieringManager.DATATIERING_KEY, DataTieringType.CUSTOM.name()); 794 conf.set(DataTieringManager.DATATIERING_HOT_DATA_AGE_KEY, String.valueOf(hotDataAge)); 795 return conf; 796 } 797 798 /** 799 * Writes random data to a store file with rows arranged in lexicographically increasing order. 800 * Each row is generated using the {@link #nextString()} method, ensuring that each subsequent row 801 * is lexicographically larger than the previous one. 802 */ 803 private static void writeStoreFileRandomData(final StoreFileWriter writer, byte[] columnFamily, 804 long timestamp) throws IOException { 805 int cellsPerFile = 10; 806 byte[] qualifier = Bytes.toBytes("qualifier"); 807 byte[] value = generateRandomBytes(4 * 1024); 808 try { 809 for (int i = 0; i < cellsPerFile; i++) { 810 byte[] row = Bytes.toBytes(nextString()); 811 writer.append(new KeyValue(row, columnFamily, qualifier, timestamp, value)); 812 } 813 } finally { 814 writer.appendTrackedTimestampsToMetadata(); 815 TimeRangeTracker timeRangeTracker = TimeRangeTracker.create(TimeRangeTracker.Type.NON_SYNC); 816 timeRangeTracker.setMin(timestamp); 817 timeRangeTracker.setMax(timestamp); 818 writer.appendCustomCellTimestampsToMetadata(timeRangeTracker); 819 writer.close(); 820 } 821 } 822 823 private static byte[] generateRandomBytes(int sizeInBytes) { 824 Random random = new Random(); 825 byte[] randomBytes = new byte[sizeInBytes]; 826 random.nextBytes(randomBytes); 827 return randomBytes; 828 } 829 830 /** 831 * Returns the lexicographically larger string every time it's called. 832 */ 833 private static String nextString() { 834 if (rowKeyString == null || rowKeyString.isEmpty()) { 835 rowKeyString = "a"; 836 } 837 char lastChar = rowKeyString.charAt(rowKeyString.length() - 1); 838 if (lastChar < 'z') { 839 rowKeyString = rowKeyString.substring(0, rowKeyString.length() - 1) + (char) (lastChar + 1); 840 } else { 841 rowKeyString = rowKeyString + "a"; 842 } 843 return rowKeyString; 844 } 845}