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