diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/BlockCacheFactory.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/BlockCacheFactory.java index c3dd8947ce87..c4269b169bd3 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/BlockCacheFactory.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/BlockCacheFactory.java @@ -166,7 +166,7 @@ public static CacheEngine createFirstLevelCacheEngine(final Configuration c) { return CacheEngines .fromBlockCache(new TinyLfuBlockCache(cacheSize, blockSize, ForkJoinPool.commonPool(), c)); } else if (policy.equalsIgnoreCase("AdaptiveLRU")) { - return CacheEngines.fromBlockCache(new LruAdaptiveBlockCache(cacheSize, blockSize, true, c)); + return new LruCacheEngine(cacheSize, blockSize, true, c); } else { throw new IllegalArgumentException("Unknown policy: " + policy); } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/AdaptiveLruCachePlacementAdmissionPolicy.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/AdaptiveLruCachePlacementAdmissionPolicy.java new file mode 100644 index 000000000000..cf446a6608a4 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/AdaptiveLruCachePlacementAdmissionPolicy.java @@ -0,0 +1,308 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.io.hfile.cache; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.io.hfile.BlockCacheKey; +import org.apache.hadoop.hbase.io.hfile.Cacheable; +import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Placement and admission policy implementing AdaptiveLRU behavior using a native + * {@link LruCacheEngine}. + *

+ * The policy preserves the adaptive admission behavior of the legacy {@code LruAdaptiveBlockCache}. + * Under sustained LRU eviction pressure, it reduces the percentage of data blocks admitted to the + * first-level cache. When eviction pressure decreases, data-block admission is gradually restored. + *

+ *

+ * Non-data blocks are not filtered by the adaptive algorithm. + *

+ */ +@InterfaceAudience.Private +public final class AdaptiveLruCachePlacementAdmissionPolicy + implements CachePlacementAdmissionPolicy { + + private static final Logger LOG = + LoggerFactory.getLogger(AdaptiveLruCachePlacementAdmissionPolicy.class); + + private static final String LRU_CACHE_HEAVY_EVICTION_COUNT_LIMIT = + "hbase.lru.cache.heavy.eviction.count.limit"; + + private static final int DEFAULT_LRU_CACHE_HEAVY_EVICTION_COUNT_LIMIT = Integer.MAX_VALUE; + + private static final String LRU_CACHE_HEAVY_EVICTION_MB_SIZE_LIMIT = + "hbase.lru.cache.heavy.eviction.mb.size.limit"; + + private static final long DEFAULT_LRU_CACHE_HEAVY_EVICTION_MB_SIZE_LIMIT = 500L; + + private static final String LRU_CACHE_HEAVY_EVICTION_OVERHEAD_COEFFICIENT = + "hbase.lru.cache.heavy.eviction.overhead.coefficient"; + + private static final float DEFAULT_LRU_CACHE_HEAVY_EVICTION_OVERHEAD_COEFFICIENT = 0.01f; + + private static final long EVICTION_EVALUATION_PERIOD_MS = 10_000L; + + private static final int MIN_CACHE_DATA_BLOCK_PERCENT = 1; + + private static final int MAX_CACHE_DATA_BLOCK_PERCENT = 100; + + private static final int MAX_ADMISSION_REDUCTION_PER_PERIOD = 15; + + private static final CachePlacementAdmissionPolicy DELEGATE = + DefaultHBaseCachePlacementAdmissionPolicy.INSTANCE; + + private final int heavyEvictionCountLimit; + + private final long heavyEvictionMbSizeLimit; + + private final float heavyEvictionOverheadCoefficient; + + private volatile int cacheDataBlockPercent = MAX_CACHE_DATA_BLOCK_PERCENT; + + private long freedSumMb; + + private int heavyEvictionCount; + + private long evaluationStartTime; + + /** + * Creates an AdaptiveLRU placement/admission policy from configuration. + * @param conf cache configuration + * @throws NullPointerException if {@code conf} is {@code null} + */ + public AdaptiveLruCachePlacementAdmissionPolicy(Configuration conf) { + Objects.requireNonNull(conf, "conf must not be null"); + + this.heavyEvictionCountLimit = Math.max(conf.getInt(LRU_CACHE_HEAVY_EVICTION_COUNT_LIMIT, + DEFAULT_LRU_CACHE_HEAVY_EVICTION_COUNT_LIMIT), 0); + + this.heavyEvictionMbSizeLimit = Math.max(conf.getLong(LRU_CACHE_HEAVY_EVICTION_MB_SIZE_LIMIT, + DEFAULT_LRU_CACHE_HEAVY_EVICTION_MB_SIZE_LIMIT), 1L); + + float configuredCoefficient = conf.getFloat(LRU_CACHE_HEAVY_EVICTION_OVERHEAD_COEFFICIENT, + DEFAULT_LRU_CACHE_HEAVY_EVICTION_OVERHEAD_COEFFICIENT); + configuredCoefficient = Math.min(configuredCoefficient, 1.0f); + configuredCoefficient = Math.max(configuredCoefficient, 0.001f); + this.heavyEvictionOverheadCoefficient = configuredCoefficient; + + this.evaluationStartTime = EnvironmentEdgeManager.currentTime(); + } + + /** + * Decides whether a block should be admitted into the cache. + *

+ * In a single-tier topology, adaptive rejection means the data block is not cached. In a tiered + * topology, admission remains enabled because adaptive filtering applies specifically to the L1 + * placement decision rather than to the entire cache hierarchy. + *

+ * @param cacheKey block cache key + * @param block block to cache + * @param context cache write context + * @param priority admission priority + * @param topologyView read-only topology view + * @return admission decision + */ + @Override + public AdmissionDecision shouldAdmit(BlockCacheKey cacheKey, Cacheable block, + CacheWriteContext context, AdmissionPriority priority, CacheTopologyView topologyView) { + Objects.requireNonNull(cacheKey, "cacheKey must not be null"); + Objects.requireNonNull(block, "block must not be null"); + Objects.requireNonNull(context, "context must not be null"); + Objects.requireNonNull(priority, "priority must not be null"); + Objects.requireNonNull(topologyView, "topologyView must not be null"); + + AdmissionDecision decision = + DELEGATE.shouldAdmit(cacheKey, block, context, priority, topologyView); + if (!decision.isAdmitted()) { + return decision; + } + + if ( + topologyView.getType() == CacheTopologyType.SINGLE_TIER + && !shouldCacheInFirstLevel(cacheKey, block) + ) { + return AdmissionDecision.reject("Rejected by AdaptiveLRU data-block admission"); + } + + return decision; + } + + /** + * Selects the target tier or tiers for an admitted block. + *

+ * The default HBase placement decision is preserved unless AdaptiveLRU rejects a data block from + * L1. In a tiered topology this removes L1 from the target set while retaining other selected + * tiers such as L2. + *

+ * @param cacheKey block cache key + * @param block block to cache + * @param context cache write context + * @param topologyView read-only topology view + * @return cache tier decision + */ + @Override + public TierDecision selectTier(BlockCacheKey cacheKey, Cacheable block, CacheWriteContext context, + CacheTopologyView topologyView) { + Objects.requireNonNull(cacheKey, "cacheKey must not be null"); + Objects.requireNonNull(block, "block must not be null"); + Objects.requireNonNull(context, "context must not be null"); + Objects.requireNonNull(topologyView, "topologyView must not be null"); + + TierDecision decision = DELEGATE.selectTier(cacheKey, block, context, topologyView); + + if (!decision.getTiers().contains(CacheTier.L1) || shouldCacheInFirstLevel(cacheKey, block)) { + return decision; + } + + List tiers = new ArrayList<>(); + for (CacheTier tier : decision.getTiers()) { + if (tier != CacheTier.L1) { + tiers.add(tier); + } + } + if (tiers.isEmpty()) { + return TierDecision.none(); + } + return TierDecision.multiple(tiers); + } + + /** + * Selects the cached representation for an admitted block. + * @param cacheKey block cache key + * @param block block to cache + * @param context cache write context + * @param topologyView read-only topology view + * @return representation decision + */ + @Override + public RepresentationDecision selectRepresentation(BlockCacheKey cacheKey, Cacheable block, + CacheWriteContext context, CacheTopologyView topologyView) { + return DELEGATE.selectRepresentation(cacheKey, block, context, topologyView); + } + + /** + * Decides whether a cache hit should trigger promotion. + * @param cacheKey block cache key + * @param block cached block + * @param sourceTier tier containing the block + * @param context cache request context + * @param topologyView read-only topology view + * @return promotion decision + */ + @Override + public PromotionDecision shouldPromote(BlockCacheKey cacheKey, Cacheable block, + CacheTier sourceTier, CacheRequestContext context, CacheTopologyView topologyView) { + return DELEGATE.shouldPromote(cacheKey, block, sourceTier, context, topologyView); + } + + /** + * Processes cache pressure reported by the first-level LRU engine. + *

+ * Pressure from L2 is ignored because AdaptiveLRU controls admission into the first-level cache. + * The algorithm intentionally preserves the behavior of the legacy {@code LruAdaptiveBlockCache}, + * including its approximately ten-second evaluation window and megabyte-based pressure + * accounting. + *

+ * @param sourceTier tier whose engine experienced cache pressure + * @param bytesToFree amount of cache pressure observed, in bytes + * @param topologyView read-only topology view + */ + @Override + public synchronized void onEvictionPressure(CacheTier sourceTier, long bytesToFree, + CacheTopologyView topologyView) { + Objects.requireNonNull(sourceTier, "sourceTier must not be null"); + Objects.requireNonNull(topologyView, "topologyView must not be null"); + + if (sourceTier != CacheTier.SINGLE && sourceTier != CacheTier.L1) { + return; + } + + freedSumMb += Math.max(bytesToFree, 0L) / 1024L / 1024L; + + long stopTime = EnvironmentEdgeManager.currentTime(); + if (stopTime - evaluationStartTime <= EVICTION_EVALUATION_PERIOD_MS - 1L) { + return; + } + + int freedDataOverheadPercent = (int) (freedSumMb * 100L / heavyEvictionMbSizeLimit) - 100; + + if (freedSumMb > heavyEvictionMbSizeLimit) { + heavyEvictionCount++; + + if (heavyEvictionCount > heavyEvictionCountLimit) { + int change = (int) (freedDataOverheadPercent * heavyEvictionOverheadCoefficient); + change = Math.min(MAX_ADMISSION_REDUCTION_PER_PERIOD, change); + change = Math.max(0, change); + + cacheDataBlockPercent -= change; + cacheDataBlockPercent = Math.max(MIN_CACHE_DATA_BLOCK_PERCENT, cacheDataBlockPercent); + } + } else if (freedSumMb >= heavyEvictionMbSizeLimit * 0.1) { + int change = (int) (-freedDataOverheadPercent * 0.1 + 1); + cacheDataBlockPercent += change; + cacheDataBlockPercent = Math.min(MAX_CACHE_DATA_BLOCK_PERCENT, cacheDataBlockPercent); + } else { + heavyEvictionCount = 0; + cacheDataBlockPercent = MAX_CACHE_DATA_BLOCK_PERCENT; + } + + LOG.info( + "BlockCache evicted (MB): {}, overhead (%): {}, heavy eviction counter: {}, " + + "current caching DataBlock (%): {}", + freedSumMb, freedDataOverheadPercent, heavyEvictionCount, cacheDataBlockPercent); + + freedSumMb = 0L; + evaluationStartTime = stopTime; + } + + /** + * Returns whether the block should be cached in the first-level AdaptiveLRU cache. + *

+ * Non-data blocks are always accepted. Data blocks use the same deterministic offset-based + * filtering used by the legacy AdaptiveLRU implementation. + *

+ * @param cacheKey block cache key + * @param block block being evaluated + * @return {@code true} if the block should be cached in the first-level cache + */ + private boolean shouldCacheInFirstLevel(BlockCacheKey cacheKey, Cacheable block) { + if (!block.getBlockType().isData() || cacheDataBlockPercent == MAX_CACHE_DATA_BLOCK_PERCENT) { + return true; + } + + return cacheKey.getOffset() % 100 < cacheDataBlockPercent; + } + + /** + * Returns the current percentage of data blocks admitted to the first-level cache. + *

+ * This method is intended for tests and diagnostics. + *

+ * @return current data-block admission percentage + */ + int getCacheDataBlockPercent() { + return cacheDataBlockPercent; + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheAccessServices.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheAccessServices.java index 050ab87f3dde..3e26024309f6 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheAccessServices.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheAccessServices.java @@ -108,7 +108,7 @@ public static CacheAccessService fromConfiguration(Configuration conf, return disabled(); } - CachePlacementAdmissionPolicy policy = DefaultHBaseCachePlacementAdmissionPolicy.INSTANCE; + CachePlacementAdmissionPolicy policy = createPlacementAdmissionPolicy(conf); boolean useExternal = conf.getBoolean(BlockCacheFactory.EXTERNAL_BLOCKCACHE_KEY, BlockCacheFactory.EXTERNAL_BLOCKCACHE_DEFAULT); @@ -183,4 +183,22 @@ public static Optional> asCachedBlockIterable(CacheAccessS } return Optional.empty(); } + + /** + * Creates the placement/admission policy associated with the configured first-level cache. + * @param conf cache configuration + * @return configured cache placement/admission policy + */ + private static CachePlacementAdmissionPolicy createPlacementAdmissionPolicy(Configuration conf) { + Objects.requireNonNull(conf, "conf must not be null"); + + String blockCachePolicy = conf.get(BlockCacheFactory.BLOCKCACHE_POLICY_KEY, + BlockCacheFactory.BLOCKCACHE_POLICY_DEFAULT); + + if (blockCachePolicy.equalsIgnoreCase("AdaptiveLRU")) { + return new AdaptiveLruCachePlacementAdmissionPolicy(conf); + } + + return DefaultHBaseCachePlacementAdmissionPolicy.INSTANCE; + } } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheEvictionListener.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheEvictionListener.java index 248082877b20..23aff26e16b0 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheEvictionListener.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheEvictionListener.java @@ -22,14 +22,16 @@ import org.apache.yetus.audience.InterfaceAudience; /** - * Listener for cache-engine eviction events. + * Listener for capacity-driven cache-engine eviction events. *

- * The listener is intended for topology-level handling of capacity-driven evictions. In particular, - * an exclusive tiered topology may use an eviction from L1 as a request to demote the block to L2. + * Per-block eviction events may be used by a cache topology for operations such as L1-to-L2 + * demotion. Eviction-pressure events may be used by placement/admission policies to adjust future + * cache decisions. *

*

- * The supplied block remains valid for the duration of the callback. An implementation that needs - * to retain the block after the callback returns must establish its own reference. + * A block supplied to {@link #onEviction(CacheEngine, BlockCacheKey, Cacheable)} remains valid for + * the duration of the callback. An implementation that needs to retain the block after the callback + * returns must establish its own reference. *

*/ @InterfaceAudience.Private @@ -42,4 +44,19 @@ public interface CacheEvictionListener { * @param block evicted block */ void onEviction(CacheEngine sourceEngine, BlockCacheKey cacheKey, Cacheable block); + + /** + * Handles cache pressure observed during a capacity-driven eviction pass. + *

+ * {@code bytesToFree} is the amount of space the engine determined should be freed to reach its + * target size. It does not necessarily equal the exact number of bytes actually evicted. + *

+ *

+ * The default implementation does nothing. + *

+ * @param sourceEngine engine that performed the eviction pass + * @param bytesToFree amount of cache pressure observed, in bytes + */ + default void onEvictionPressure(CacheEngine sourceEngine, long bytesToFree) { + } } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CachePlacementAdmissionPolicy.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CachePlacementAdmissionPolicy.java index 7ce89fdceeba..ca8f6e69879f 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CachePlacementAdmissionPolicy.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CachePlacementAdmissionPolicy.java @@ -85,4 +85,18 @@ RepresentationDecision selectRepresentation(BlockCacheKey cacheKey, Cacheable bl */ PromotionDecision shouldPromote(BlockCacheKey cacheKey, Cacheable block, CacheTier sourceTier, CacheRequestContext context, CacheTopologyView topologyView); + + /** + * Notifies this policy about cache pressure observed during an engine eviction pass. + *

+ * Stateful admission and placement policies may use this feedback to adjust future admission or + * placement decisions. The default implementation does nothing. + *

+ * @param sourceTier tier whose engine experienced cache pressure + * @param bytesToFree amount of cache pressure observed, in bytes + * @param topologyView read-only topology view + */ + default void onEvictionPressure(CacheTier sourceTier, long bytesToFree, + CacheTopologyView topologyView) { + } } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/LruCacheEngine.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/LruCacheEngine.java index 53ad44e45d57..4646f60a588f 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/LruCacheEngine.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/LruCacheEngine.java @@ -804,11 +804,14 @@ void evict() { return; } + long bytesToFree = 0L; + boolean evictionPassCompleted = false; + try { evictionInProgress = true; long currentSize = size.get(); - long bytesToFree = currentSize - minSize(); + bytesToFree = currentSize - minSize(); if (LOG.isTraceEnabled()) { LOG.trace("LRU cache eviction started; Attempting to free " @@ -816,6 +819,7 @@ void evict() { } if (bytesToFree <= 0) { + evictionPassCompleted = true; return; } @@ -908,10 +912,27 @@ void evict() { + StringUtils.byteDesc(single) + ", multi=" + StringUtils.byteDesc(multi) + ", memory=" + StringUtils.byteDesc(memory)); } + + evictionPassCompleted = true; } finally { stats.evict(); evictionInProgress = false; evictionLock.unlock(); + + if (evictionPassCompleted) { + notifyEvictionPressure(Math.max(bytesToFree, 0L)); + } + } + } + + /** + * Notifies the configured eviction listener about pressure observed during an eviction pass. + * @param bytesToFree amount of cache pressure observed, in bytes + */ + private void notifyEvictionPressure(long bytesToFree) { + CacheEvictionListener listener = evictionListener; + if (listener != null) { + listener.onEvictionPressure(this, bytesToFree); } } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/TopologyBackedCacheAccessService.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/TopologyBackedCacheAccessService.java index 5c5034172cf0..fde0a33812b8 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/TopologyBackedCacheAccessService.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/TopologyBackedCacheAccessService.java @@ -79,11 +79,48 @@ public TopologyBackedCacheAccessService(CacheTopology topology, Objects.requireNonNull(topology.getView(), "topology view must not be null"); for (CacheTier tier : topology.getTiers()) { topology.getEngine(tier) - .ifPresent(engine -> engine.setEvictionListener(this::handleEngineEviction)); + .ifPresent(engine -> engine.setEvictionListener(createEvictionListener(tier))); } this.persistenceCoordinator = new CachePersistenceCoordinator(topology, policy); } + /** + * Creates an eviction listener associated with the specified cache tier. + *

+ * Per-block eviction events are forwarded to the topology for topology-specific handling. + * Eviction-pressure events are forwarded to the placement/admission policy. + *

+ * @param tier tier associated with the cache engine + * @return eviction listener for the tier + */ + private CacheEvictionListener createEvictionListener(CacheTier tier) { + Objects.requireNonNull(tier, "tier must not be null"); + + return new CacheEvictionListener() { + + /** + * Handles a capacity-driven block eviction. + * @param sourceEngine engine that evicted the block + * @param cacheKey key identifying the evicted block + * @param block evicted block + */ + @Override + public void onEviction(CacheEngine sourceEngine, BlockCacheKey cacheKey, Cacheable block) { + handleEngineEviction(sourceEngine, cacheKey, block); + } + + /** + * Handles cache pressure reported by an eviction pass. + * @param sourceEngine engine that performed the eviction pass + * @param bytesToFree amount of cache pressure observed, in bytes + */ + @Override + public void onEvictionPressure(CacheEngine sourceEngine, long bytesToFree) { + policy.onEvictionPressure(tier, bytesToFree, topologyView); + } + }; + } + /** * Returns the topology used by this service. *

diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestCacheConfig.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestCacheConfig.java index 52d5bf704c3a..e750a8965cbd 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestCacheConfig.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestCacheConfig.java @@ -42,6 +42,7 @@ import org.apache.hadoop.hbase.io.ByteBuffAllocator; import org.apache.hadoop.hbase.io.hfile.BlockType.BlockCategory; import org.apache.hadoop.hbase.io.hfile.bucket.BucketCache; +import org.apache.hadoop.hbase.io.hfile.cache.AdaptiveLruCachePlacementAdmissionPolicy; import org.apache.hadoop.hbase.io.hfile.cache.BlockCacheBackedCacheEngine; import org.apache.hadoop.hbase.io.hfile.cache.CacheAccessService; import org.apache.hadoop.hbase.io.hfile.cache.CacheAccessServiceTestFactory; @@ -499,4 +500,38 @@ public void testDeregistersCacheAccessServiceAsConfigurationChild() { verify(manager).deregisterObserver(cacheAccessService); } + + /** + * Verifies that AdaptiveLRU uses the native LRU engine together with the adaptive placement and + * admission policy in the CacheAccessService construction path. + */ + @Test + public void testAdaptiveLruCacheConfigUsesNativeEngineAndAdaptivePolicy() { + this.conf.set(BlockCacheFactory.BLOCKCACHE_POLICY_KEY, "AdaptiveLRU"); + + CacheAccessService service = CacheAccessServiceTestFactory.fromConfiguration(this.conf); + + assertInstanceOf(TopologyBackedCacheAccessService.class, service); + + CacheEngine engine = CacheAccessServiceTestFactory.getCacheEngine(service, CacheTier.SINGLE); + assertInstanceOf(LruCacheEngine.class, engine); + + TopologyBackedCacheAccessService topologyBackedService = + (TopologyBackedCacheAccessService) service; + assertInstanceOf(AdaptiveLruCachePlacementAdmissionPolicy.class, + topologyBackedService.getPolicy()); + } + + /** + * Verifies that the legacy BlockCache construction path continues to create + * LruAdaptiveBlockCache. + */ + @Test + public void testLegacyAdaptiveLruBlockCacheConfigurationIsPreserved() { + this.conf.set(BlockCacheFactory.BLOCKCACHE_POLICY_KEY, "AdaptiveLRU"); + + BlockCache blockCache = BlockCacheFactory.createBlockCache(this.conf); + + assertInstanceOf(LruAdaptiveBlockCache.class, blockCache); + } } diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestAdaptiveLruCachePlacementAdmissionPolicy.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestAdaptiveLruCachePlacementAdmissionPolicy.java new file mode 100644 index 000000000000..de2920188b70 --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestAdaptiveLruCachePlacementAdmissionPolicy.java @@ -0,0 +1,335 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.io.hfile.cache; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.io.hfile.BlockCacheKey; +import org.apache.hadoop.hbase.io.hfile.BlockType; +import org.apache.hadoop.hbase.io.hfile.Cacheable; +import org.apache.hadoop.hbase.testclassification.IOTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; +import org.apache.hadoop.hbase.util.ManualEnvironmentEdge; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag(IOTests.TAG) +@Tag(SmallTests.TAG) +public class TestAdaptiveLruCachePlacementAdmissionPolicy { + + private static final String HEAVY_EVICTION_COUNT_LIMIT = + "hbase.lru.cache.heavy.eviction.count.limit"; + + private static final String HEAVY_EVICTION_MB_SIZE_LIMIT = + "hbase.lru.cache.heavy.eviction.mb.size.limit"; + + private static final String HEAVY_EVICTION_OVERHEAD_COEFFICIENT = + "hbase.lru.cache.heavy.eviction.overhead.coefficient"; + + private static final long MB = 1024L * 1024L; + + private static final long EVALUATION_PERIOD_MS = 10_000L; + + private ManualEnvironmentEdge environmentEdge; + + private long currentTime; + + /** + * Installs a manually controlled clock for AdaptiveLRU evaluation windows. + */ + @BeforeEach + void setUp() { + currentTime = 1_000L; + environmentEdge = new ManualEnvironmentEdge(); + environmentEdge.setValue(currentTime); + EnvironmentEdgeManager.injectEdge(environmentEdge); + } + + /** + * Restores the normal environment clock after each test. + */ + @AfterEach + void tearDown() { + EnvironmentEdgeManager.reset(); + } + + /** + * Verifies that AdaptiveLRU initially admits all data blocks. + */ + @Test + void testInitialDataBlockAdmissionIsOneHundredPercent() { + AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(0, 200L, 0.01f); + + assertEquals(100, policy.getCacheDataBlockPercent()); + } + + /** + * Verifies that sustained eviction pressure reduces data-block admission. + */ + @Test + void testHeavyEvictionPressureReducesDataBlockAdmission() { + AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(0, 200L, 0.01f); + + applyPressure(policy, CacheTier.SINGLE, singleTierView(), 3_200L); + + assertEquals(85, policy.getCacheDataBlockPercent()); + } + + /** + * Verifies that the configured heavy-eviction count delays adaptive admission changes. + */ + @Test + void testHeavyEvictionCountLimitDelaysAdaptation() { + AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(1, 200L, 0.01f); + CacheTopologyView topologyView = singleTierView(); + + applyPressure(policy, CacheTier.SINGLE, topologyView, 3_200L); + assertEquals(100, policy.getCacheDataBlockPercent()); + + applyPressure(policy, CacheTier.SINGLE, topologyView, 3_200L); + assertEquals(85, policy.getCacheDataBlockPercent()); + } + + /** + * Verifies that admission increases again when eviction pressure decreases. + */ + @Test + void testAdmissionRecoversWhenEvictionPressureDrops() { + AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(0, 200L, 0.01f); + CacheTopologyView topologyView = singleTierView(); + + applyPressure(policy, CacheTier.SINGLE, topologyView, 3_200L); + assertEquals(85, policy.getCacheDataBlockPercent()); + + applyPressure(policy, CacheTier.SINGLE, topologyView, 20L); + assertEquals(95, policy.getCacheDataBlockPercent()); + + applyPressure(policy, CacheTier.SINGLE, topologyView, 0L); + assertEquals(100, policy.getCacheDataBlockPercent()); + } + + /** + * Verifies that pressure from L2 does not affect AdaptiveLRU admission. + */ + @Test + void testSecondLevelEvictionPressureIsIgnored() { + AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(0, 200L, 0.01f); + + applyPressure(policy, CacheTier.L2, tieredExclusiveView(), 3_200L); + + assertEquals(100, policy.getCacheDataBlockPercent()); + } + + /** + * Verifies the deterministic offset-based data-block admission filter for a single-tier cache. + */ + @Test + void testSingleTierDataBlockAdmissionUsesBlockOffset() { + AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(0, 200L, 0.01f); + CacheTopologyView topologyView = singleTierView(); + Cacheable dataBlock = block(BlockType.DATA); + CacheWriteContext context = mock(CacheWriteContext.class); + + applyPressure(policy, CacheTier.SINGLE, topologyView, 3_200L); + assertEquals(85, policy.getCacheDataBlockPercent()); + + AdmissionDecision admitted = policy.shouldAdmit(new BlockCacheKey("file", 84L), dataBlock, + context, AdmissionPriority.NORMAL, topologyView); + + AdmissionDecision rejected = policy.shouldAdmit(new BlockCacheKey("file", 85L), dataBlock, + context, AdmissionPriority.NORMAL, topologyView); + + assertTrue(admitted.isAdmitted()); + assertFalse(rejected.isAdmitted()); + } + + /** + * Verifies that AdaptiveLRU does not filter metadata blocks. + */ + @Test + void testMetadataBlocksAreNotAdaptivelyRejected() { + AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(0, 200L, 0.01f); + CacheTopologyView topologyView = singleTierView(); + Cacheable indexBlock = block(BlockType.ROOT_INDEX); + CacheWriteContext context = mock(CacheWriteContext.class); + + applyPressure(policy, CacheTier.SINGLE, topologyView, 3_200L); + + AdmissionDecision decision = policy.shouldAdmit(new BlockCacheKey("file", 99L), indexBlock, + context, AdmissionPriority.NORMAL, topologyView); + + assertTrue(decision.isAdmitted()); + } + + /** + * Verifies that inclusive topology removes only L1 when AdaptiveLRU rejects a data block there. + */ + @Test + void testInclusiveTopologyRetainsL2WhenDataBlockIsRejectedFromL1() { + AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(0, 200L, 0.01f); + CacheTopologyView topologyView = tieredInclusiveView(); + Cacheable dataBlock = block(BlockType.DATA); + CacheWriteContext context = mock(CacheWriteContext.class); + + applyPressure(policy, CacheTier.L1, topologyView, 3_200L); + assertEquals(85, policy.getCacheDataBlockPercent()); + + BlockCacheKey cacheKey = new BlockCacheKey("file", 99L); + + AdmissionDecision admission = + policy.shouldAdmit(cacheKey, dataBlock, context, AdmissionPriority.NORMAL, topologyView); + TierDecision placement = policy.selectTier(cacheKey, dataBlock, context, topologyView); + + assertTrue(admission.isAdmitted()); + assertEquals(List.of(CacheTier.L2), placement.getTiers()); + } + + /** + * Verifies that exclusive topology continues to place data blocks in L2 even when AdaptiveLRU + * would reject them from L1. + */ + @Test + void testExclusiveTopologyKeepsDefaultDataPlacementInL2() { + AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(0, 200L, 0.01f); + CacheTopologyView topologyView = tieredExclusiveView(); + Cacheable dataBlock = block(BlockType.DATA); + CacheWriteContext context = mock(CacheWriteContext.class); + + applyPressure(policy, CacheTier.L1, topologyView, 3_200L); + assertEquals(85, policy.getCacheDataBlockPercent()); + + BlockCacheKey cacheKey = new BlockCacheKey("file", 99L); + + AdmissionDecision admission = + policy.shouldAdmit(cacheKey, dataBlock, context, AdmissionPriority.NORMAL, topologyView); + TierDecision placement = policy.selectTier(cacheKey, dataBlock, context, topologyView); + + assertTrue(admission.isAdmitted()); + assertEquals(List.of(CacheTier.L2), placement.getTiers()); + } + + /** + * Verifies that AdaptiveLRU returns no placement when the default placement contains only L1 and + * the data block is rejected from the first-level cache. + */ + @Test + void testAdaptiveRejectionReturnsNoTierWhenOnlyL1IsAvailable() { + AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(0, 200L, 0.01f); + CacheTopologyView topologyView = mock(CacheTopologyView.class); + CacheEngineView l1 = mock(CacheEngineView.class); + Cacheable dataBlock = block(BlockType.DATA); + CacheWriteContext context = mock(CacheWriteContext.class); + + when(topologyView.getType()).thenReturn(CacheTopologyType.TIERED_EXCLUSIVE); + when(topologyView.getEngine(CacheTier.L1)).thenReturn(Optional.of(l1)); + when(topologyView.getEngine(CacheTier.L2)).thenReturn(Optional.empty()); + + applyPressure(policy, CacheTier.L1, topologyView, 3_200L); + + TierDecision decision = + policy.selectTier(new BlockCacheKey("file", 99L), dataBlock, context, topologyView); + + assertTrue(decision.isEmpty()); + } + + /** + * Creates an AdaptiveLRU policy with explicitly controlled adaptation parameters. + * @param heavyEvictionCountLimit number of heavy periods tolerated before adaptation + * @param heavyEvictionMbSizeLimit target eviction volume in megabytes + * @param heavyEvictionOverheadCoefficient coefficient controlling admission adjustment speed + * @return configured AdaptiveLRU policy + */ + private AdaptiveLruCachePlacementAdmissionPolicy createPolicy(int heavyEvictionCountLimit, + long heavyEvictionMbSizeLimit, float heavyEvictionOverheadCoefficient) { + Configuration conf = new Configuration(false); + conf.setInt(HEAVY_EVICTION_COUNT_LIMIT, heavyEvictionCountLimit); + conf.setLong(HEAVY_EVICTION_MB_SIZE_LIMIT, heavyEvictionMbSizeLimit); + conf.setFloat(HEAVY_EVICTION_OVERHEAD_COEFFICIENT, heavyEvictionOverheadCoefficient); + return new AdaptiveLruCachePlacementAdmissionPolicy(conf); + } + + /** + * Advances one evaluation period and reports the specified eviction pressure. + * @param policy policy receiving the pressure event + * @param sourceTier tier reporting pressure + * @param topologyView topology view associated with the policy + * @param pressureMb cache pressure in megabytes + */ + private void applyPressure(AdaptiveLruCachePlacementAdmissionPolicy policy, CacheTier sourceTier, + CacheTopologyView topologyView, long pressureMb) { + currentTime += EVALUATION_PERIOD_MS; + environmentEdge.setValue(currentTime); + policy.onEvictionPressure(sourceTier, pressureMb * MB, topologyView); + } + + /** + * Creates a mocked single-tier topology view. + * @return single-tier topology view + */ + private CacheTopologyView singleTierView() { + CacheTopologyView topologyView = mock(CacheTopologyView.class); + when(topologyView.getType()).thenReturn(CacheTopologyType.SINGLE_TIER); + return topologyView; + } + + /** + * Creates a mocked tiered-inclusive topology view. + * @return tiered-inclusive topology view + */ + private CacheTopologyView tieredInclusiveView() { + CacheTopologyView topologyView = mock(CacheTopologyView.class); + when(topologyView.getType()).thenReturn(CacheTopologyType.TIERED_INCLUSIVE); + return topologyView; + } + + /** + * Creates a mocked tiered-exclusive topology view with both tiers available. + * @return tiered-exclusive topology view + */ + private CacheTopologyView tieredExclusiveView() { + CacheTopologyView topologyView = mock(CacheTopologyView.class); + CacheEngineView l1 = mock(CacheEngineView.class); + CacheEngineView l2 = mock(CacheEngineView.class); + + when(topologyView.getType()).thenReturn(CacheTopologyType.TIERED_EXCLUSIVE); + when(topologyView.getEngine(CacheTier.L1)).thenReturn(Optional.of(l1)); + when(topologyView.getEngine(CacheTier.L2)).thenReturn(Optional.of(l2)); + return topologyView; + } + + /** + * Creates a cacheable block with the specified type. + * @param blockType block type + * @return mocked cacheable block + */ + private Cacheable block(BlockType blockType) { + Cacheable block = mock(Cacheable.class); + when(block.getBlockType()).thenReturn(blockType); + return block; + } +} diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestLruCacheEngine.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestLruCacheEngine.java new file mode 100644 index 000000000000..89ef91a62e97 --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestLruCacheEngine.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.io.hfile.cache; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.testclassification.IOTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link LruCacheEngine}. + */ +@Tag(IOTests.TAG) +@Tag(SmallTests.TAG) +public class TestLruCacheEngine { + + private static final String LRU_MIN_FACTOR_CONFIG_NAME = "hbase.lru.blockcache.min.factor"; + + /** + * Verifies that an eviction pass with no excess occupancy reports zero cache pressure. + */ + @Test + void testEvictionPassReportsZeroPressureWhenBelowTargetSize() { + LruCacheEngine engine = new LruCacheEngine(1024L * 1024L, 64L * 1024L, false); + CacheEvictionListener listener = mock(CacheEvictionListener.class); + + try { + engine.setEvictionListener(listener); + + engine.evict(); + + verify(listener).onEvictionPressure(engine, 0L); + } finally { + engine.shutdown(); + } + } + + /** + * Verifies that an eviction pass reports the number of bytes the engine determines should be + * freed to reach its configured minimum target size. + */ + @Test + void testEvictionPassReportsRequestedPressure() { + Configuration conf = new Configuration(false); + conf.setFloat(LRU_MIN_FACTOR_CONFIG_NAME, 0.5f); + + LruCacheEngine engine = new LruCacheEngine(16L * 1024L * 1024L, 64L * 1024L, false, conf); + CacheEvictionListener listener = mock(CacheEvictionListener.class); + + try { + long currentSize = engine.getCurrentSize(); + long newMaxSize = Math.max(1L, currentSize / 2L); + long expectedBytesToFree = currentSize - (long) Math.floor(newMaxSize * 0.5f); + + assertTrue(currentSize > 0); + assertTrue(expectedBytesToFree > 0); + + engine.setEvictionListener(listener); + engine.setMaxSize(newMaxSize); + + verify(listener).onEvictionPressure(engine, expectedBytesToFree); + } finally { + engine.shutdown(); + } + } +} diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessService.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessService.java index f8019f45f7f6..23feb2b54a45 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessService.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessService.java @@ -45,6 +45,7 @@ import org.apache.hadoop.hbase.testclassification.SmallTests; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; /** * Tests for {@link TopologyBackedCacheAccessService}. @@ -629,6 +630,61 @@ public void testRestorePersistentState() throws IOException { verify(input).close(); } + /** + * Verifies that cache-pressure feedback from an engine is forwarded to the policy with the + * logical tier associated with that engine. + */ + @Test + void testEvictionPressureIsForwardedToPolicyWithSourceTier() { + CacheTopology topology = mock(CacheTopology.class); + CacheTopologyView topologyView = mock(CacheTopologyView.class); + CachePlacementAdmissionPolicy policy = mock(CachePlacementAdmissionPolicy.class); + CacheEngine l1 = mock(CacheEngine.class); + CacheEngine l2 = mock(CacheEngine.class); + + when(topology.getView()).thenReturn(topologyView); + when(topology.getTiers()).thenReturn(List.of(CacheTier.L1, CacheTier.L2)); + when(topology.getEngine(CacheTier.L1)).thenReturn(Optional.of(l1)); + when(topology.getEngine(CacheTier.L2)).thenReturn(Optional.of(l2)); + + new TopologyBackedCacheAccessService(topology, policy); + + ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(CacheEvictionListener.class); + verify(l1).setEvictionListener(listenerCaptor.capture()); + + listenerCaptor.getValue().onEvictionPressure(l1, 1234L); + + verify(policy).onEvictionPressure(CacheTier.L1, 1234L, topologyView); + } + + /** + * Verifies that concrete block eviction events continue to be handled by the topology. + */ + @Test + void testBlockEvictionIsStillForwardedToTopology() { + BlockCacheKey cacheKey = new BlockCacheKey(HFILE_NAME, BLOCK_OFFSET); + Cacheable block = mock(Cacheable.class); + CacheTopology topology = mock(CacheTopology.class); + CacheTopologyView topologyView = mock(CacheTopologyView.class); + CachePlacementAdmissionPolicy policy = mock(CachePlacementAdmissionPolicy.class); + CacheEngine l1 = mock(CacheEngine.class); + + when(topology.getView()).thenReturn(topologyView); + when(topology.getTiers()).thenReturn(List.of(CacheTier.L1)); + when(topology.getEngine(CacheTier.L1)).thenReturn(Optional.of(l1)); + + new TopologyBackedCacheAccessService(topology, policy); + + ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(CacheEvictionListener.class); + verify(l1).setEvictionListener(listenerCaptor.capture()); + + listenerCaptor.getValue().onEviction(l1, cacheKey, block); + + verify(topology).handleEviction(cacheKey, block, l1); + } + private static CacheRequestContext requestContext() { return CacheRequestContext.newBuilder().withCaching(true).withRepeat(false) .withUpdateCacheMetrics(true).withBlockType(BlockType.DATA).build();