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