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.io.hfile.bucket; 019 020import static org.apache.hadoop.hbase.io.hfile.BlockCacheFactory.BLOCKCACHE_STATS_PERIODS; 021import static org.apache.hadoop.hbase.io.hfile.BlockCacheFactory.BLOCKCACHE_STATS_PERIOD_MINUTES_KEY; 022import static org.apache.hadoop.hbase.io.hfile.BlockCacheFactory.DEFAULT_BLOCKCACHE_STATS_PERIODS; 023import static org.apache.hadoop.hbase.io.hfile.BlockCacheFactory.DEFAULT_BLOCKCACHE_STATS_PERIOD_MINUTES; 024import static org.apache.hadoop.hbase.io.hfile.CacheConfig.BUCKETCACHE_PERSIST_INTERVAL_KEY; 025 026import java.io.File; 027import java.io.FileInputStream; 028import java.io.FileOutputStream; 029import java.io.IOException; 030import java.nio.ByteBuffer; 031import java.util.ArrayList; 032import java.util.Arrays; 033import java.util.Collections; 034import java.util.Comparator; 035import java.util.HashSet; 036import java.util.Iterator; 037import java.util.List; 038import java.util.Map; 039import java.util.NavigableSet; 040import java.util.Optional; 041import java.util.PriorityQueue; 042import java.util.Set; 043import java.util.concurrent.ArrayBlockingQueue; 044import java.util.concurrent.BlockingQueue; 045import java.util.concurrent.ConcurrentHashMap; 046import java.util.concurrent.ConcurrentMap; 047import java.util.concurrent.ConcurrentSkipListSet; 048import java.util.concurrent.Executors; 049import java.util.concurrent.ScheduledExecutorService; 050import java.util.concurrent.TimeUnit; 051import java.util.concurrent.atomic.AtomicBoolean; 052import java.util.concurrent.atomic.AtomicLong; 053import java.util.concurrent.atomic.LongAdder; 054import java.util.concurrent.locks.Lock; 055import java.util.concurrent.locks.ReentrantLock; 056import java.util.concurrent.locks.ReentrantReadWriteLock; 057import java.util.function.Consumer; 058import java.util.function.Function; 059import org.apache.commons.io.IOUtils; 060import org.apache.hadoop.conf.Configuration; 061import org.apache.hadoop.fs.Path; 062import org.apache.hadoop.hbase.HBaseConfiguration; 063import org.apache.hadoop.hbase.HBaseIOException; 064import org.apache.hadoop.hbase.TableName; 065import org.apache.hadoop.hbase.client.Admin; 066import org.apache.hadoop.hbase.io.ByteBuffAllocator; 067import org.apache.hadoop.hbase.io.ByteBuffAllocator.Recycler; 068import org.apache.hadoop.hbase.io.HeapSize; 069import org.apache.hadoop.hbase.io.hfile.BlockCache; 070import org.apache.hadoop.hbase.io.hfile.BlockCacheKey; 071import org.apache.hadoop.hbase.io.hfile.BlockCacheUtil; 072import org.apache.hadoop.hbase.io.hfile.BlockPriority; 073import org.apache.hadoop.hbase.io.hfile.BlockType; 074import org.apache.hadoop.hbase.io.hfile.CacheConfig; 075import org.apache.hadoop.hbase.io.hfile.CacheStats; 076import org.apache.hadoop.hbase.io.hfile.Cacheable; 077import org.apache.hadoop.hbase.io.hfile.CachedBlock; 078import org.apache.hadoop.hbase.io.hfile.CombinedBlockCache; 079import org.apache.hadoop.hbase.io.hfile.HFileBlock; 080import org.apache.hadoop.hbase.io.hfile.HFileContext; 081import org.apache.hadoop.hbase.io.hfile.HFileInfo; 082import org.apache.hadoop.hbase.nio.ByteBuff; 083import org.apache.hadoop.hbase.nio.RefCnt; 084import org.apache.hadoop.hbase.protobuf.ProtobufMagic; 085import org.apache.hadoop.hbase.regionserver.DataTieringManager; 086import org.apache.hadoop.hbase.regionserver.HRegion; 087import org.apache.hadoop.hbase.regionserver.StoreFileInfo; 088import org.apache.hadoop.hbase.util.Bytes; 089import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; 090import org.apache.hadoop.hbase.util.IdReadWriteLock; 091import org.apache.hadoop.hbase.util.IdReadWriteLockStrongRef; 092import org.apache.hadoop.hbase.util.IdReadWriteLockWithObjectPool; 093import org.apache.hadoop.hbase.util.IdReadWriteLockWithObjectPool.ReferenceType; 094import org.apache.hadoop.hbase.util.Pair; 095import org.apache.hadoop.hbase.util.Threads; 096import org.apache.hadoop.util.StringUtils; 097import org.apache.yetus.audience.InterfaceAudience; 098import org.slf4j.Logger; 099import org.slf4j.LoggerFactory; 100 101import org.apache.hbase.thirdparty.com.google.common.base.Preconditions; 102import org.apache.hbase.thirdparty.com.google.common.util.concurrent.ThreadFactoryBuilder; 103 104import org.apache.hadoop.hbase.shaded.protobuf.generated.BucketCacheProtos; 105 106/** 107 * BucketCache uses {@link BucketAllocator} to allocate/free blocks, and uses BucketCache#ramCache 108 * and BucketCache#backingMap in order to determine if a given element is in the cache. The bucket 109 * cache can use off-heap memory {@link ByteBufferIOEngine} or mmap 110 * {@link ExclusiveMemoryMmapIOEngine} or pmem {@link SharedMemoryMmapIOEngine} or local files 111 * {@link FileIOEngine} to store/read the block data. 112 * <p> 113 * Eviction is via a similar algorithm as used in 114 * {@link org.apache.hadoop.hbase.io.hfile.LruBlockCache} 115 * <p> 116 * BucketCache can be used as mainly a block cache (see 117 * {@link org.apache.hadoop.hbase.io.hfile.CombinedBlockCache}), combined with a BlockCache to 118 * decrease CMS GC and heap fragmentation. 119 * <p> 120 * It also can be used as a secondary cache (e.g. using a file on ssd/fusionio to store blocks) to 121 * enlarge cache space via a victim cache. 122 */ 123@InterfaceAudience.Private 124public class BucketCache implements BlockCache, HeapSize { 125 private static final Logger LOG = LoggerFactory.getLogger(BucketCache.class); 126 127 /** Priority buckets config */ 128 static final String SINGLE_FACTOR_CONFIG_NAME = "hbase.bucketcache.single.factor"; 129 static final String MULTI_FACTOR_CONFIG_NAME = "hbase.bucketcache.multi.factor"; 130 static final String MEMORY_FACTOR_CONFIG_NAME = "hbase.bucketcache.memory.factor"; 131 static final String EXTRA_FREE_FACTOR_CONFIG_NAME = "hbase.bucketcache.extrafreefactor"; 132 static final String ACCEPT_FACTOR_CONFIG_NAME = "hbase.bucketcache.acceptfactor"; 133 static final String MIN_FACTOR_CONFIG_NAME = "hbase.bucketcache.minfactor"; 134 static final String BACKING_MAP_PERSISTENCE_CHUNK_SIZE = 135 "hbase.bucketcache.persistence.chunksize"; 136 137 /** Use strong reference for offsetLock or not */ 138 private static final String STRONG_REF_KEY = "hbase.bucketcache.offsetlock.usestrongref"; 139 private static final boolean STRONG_REF_DEFAULT = false; 140 141 /** The cache age of blocks to check if the related file is present on any online regions. */ 142 static final String BLOCK_ORPHAN_GRACE_PERIOD = 143 "hbase.bucketcache.block.orphan.evictgraceperiod.seconds"; 144 145 static final long BLOCK_ORPHAN_GRACE_PERIOD_DEFAULT = 24 * 60 * 60 * 1000L; 146 147 /** Priority buckets */ 148 static final float DEFAULT_SINGLE_FACTOR = 0.25f; 149 static final float DEFAULT_MULTI_FACTOR = 0.50f; 150 static final float DEFAULT_MEMORY_FACTOR = 0.25f; 151 static final float DEFAULT_MIN_FACTOR = 0.85f; 152 153 static final float DEFAULT_EXTRA_FREE_FACTOR = 0.10f; 154 static final float DEFAULT_ACCEPT_FACTOR = 0.95f; 155 156 // Number of blocks to clear for each of the bucket size that is full 157 static final int DEFAULT_FREE_ENTIRE_BLOCK_FACTOR = 2; 158 159 /** Statistics thread */ 160 private static final int statThreadPeriod = 5 * 60; 161 162 final static int DEFAULT_WRITER_THREADS = 3; 163 final static int DEFAULT_WRITER_QUEUE_ITEMS = 64; 164 165 final static long DEFAULT_BACKING_MAP_PERSISTENCE_CHUNK_SIZE = 10000; 166 167 // Store/read block data 168 transient final IOEngine ioEngine; 169 170 // Store the block in this map before writing it to cache 171 transient final RAMCache ramCache; 172 173 // In this map, store the block's meta data like offset, length 174 transient Map<BlockCacheKey, BucketEntry> backingMap; 175 176 private AtomicBoolean backingMapValidated = new AtomicBoolean(false); 177 178 /** 179 * Map of hFile -> Region -> File size. This map is used to track all files completed prefetch, 180 * together with the region those belong to and the total cached size for the 181 * region.TestBlockEvictionOnRegionMovement 182 */ 183 transient final Map<String, Pair<String, Long>> fullyCachedFiles = new ConcurrentHashMap<>(); 184 /** 185 * Map of region -> total size of the region prefetched on this region server. This is the total 186 * size of hFiles for this region prefetched on this region server 187 */ 188 final Map<String, Long> regionCachedSize = new ConcurrentHashMap<>(); 189 190 private transient BucketCachePersister cachePersister; 191 192 /** 193 * Enum to represent the state of cache 194 */ 195 protected enum CacheState { 196 // Initializing: State when the cache is being initialised from persistence. 197 INITIALIZING, 198 // Enabled: State when cache is initialised and is ready. 199 ENABLED, 200 // Disabled: State when the cache is disabled. 201 DISABLED 202 } 203 204 /** 205 * Flag if the cache is enabled or not... We shut it off if there are IO errors for some time, so 206 * that Bucket IO exceptions/errors don't bring down the HBase server. 207 */ 208 private volatile CacheState cacheState; 209 210 /** The single cleanup thread shared by disable and explicit shutdown calls. */ 211 private volatile Thread cacheCleanupThread; 212 213 /** The thread restoring the persistent cache index during initialization. */ 214 private volatile Thread persistenceRetrieverThread; 215 216 /** 217 * A list of writer queues. We have a queue per {@link WriterThread} we have running. In other 218 * words, the work adding blocks to the BucketCache is divided up amongst the running 219 * WriterThreads. Its done by taking hash of the cache key modulo queue count. WriterThread when 220 * it runs takes whatever has been recently added and 'drains' the entries to the BucketCache. It 221 * then updates the ramCache and backingMap accordingly. 222 */ 223 transient final ArrayList<BlockingQueue<RAMQueueEntry>> writerQueues = new ArrayList<>(); 224 transient final WriterThread[] writerThreads; 225 226 /** Volatile boolean to track if free space is in process or not */ 227 private volatile boolean freeInProgress = false; 228 private transient final Lock freeSpaceLock = new ReentrantLock(); 229 230 private final LongAdder realCacheSize = new LongAdder(); 231 private final LongAdder heapSize = new LongAdder(); 232 /** Current number of cached elements */ 233 private final LongAdder blockNumber = new LongAdder(); 234 235 /** Cache access count (sequential ID) */ 236 private final AtomicLong accessCount = new AtomicLong(); 237 238 private final BucketCacheStats cacheStats; 239 private final String persistencePath; 240 static AtomicBoolean isCacheInconsistent = new AtomicBoolean(false); 241 private final long cacheCapacity; 242 /** Approximate block size */ 243 private final long blockSize; 244 245 /** Duration of IO errors tolerated before we disable cache, 1 min as default */ 246 private final int ioErrorsTolerationDuration; 247 // 1 min 248 public static final int DEFAULT_ERROR_TOLERATION_DURATION = 60 * 1000; 249 250 // Start time of first IO error when reading or writing IO Engine, it will be 251 // reset after a successful read/write. 252 private volatile long ioErrorStartTime = -1; 253 254 private transient Configuration conf; 255 256 /** 257 * A ReentrantReadWriteLock to lock on a particular block identified by offset. The purpose of 258 * this is to avoid freeing the block which is being read. 259 * <p> 260 */ 261 transient final IdReadWriteLock<Long> offsetLock; 262 263 transient NavigableSet<BlockCacheKey> blocksByHFile = new ConcurrentSkipListSet<>( 264 Comparator.comparing(BlockCacheKey::getHfileName).thenComparingLong(BlockCacheKey::getOffset)); 265 266 /** Statistics thread schedule pool (for heavy debugging, could remove) */ 267 private transient final ScheduledExecutorService scheduleThreadPool = 268 Executors.newScheduledThreadPool(1, 269 new ThreadFactoryBuilder().setNameFormat("BucketCacheStatsExecutor").setDaemon(true).build()); 270 271 // Allocate or free space for the block 272 private transient BucketAllocator bucketAllocator; 273 274 /** Acceptable size of cache (no evictions if size < acceptable) */ 275 private float acceptableFactor; 276 277 /** Minimum threshold of cache (when evicting, evict until size < min) */ 278 private float minFactor; 279 280 /** 281 * Free this floating point factor of extra blocks when evicting. For example free the number of 282 * blocks requested * (1 + extraFreeFactor) 283 */ 284 private float extraFreeFactor; 285 286 /** Single access bucket size */ 287 private float singleFactor; 288 289 /** Multiple access bucket size */ 290 private float multiFactor; 291 292 /** In-memory bucket size */ 293 private float memoryFactor; 294 295 private long bucketcachePersistInterval; 296 297 private static final String FILE_VERIFY_ALGORITHM = 298 "hbase.bucketcache.persistent.file.integrity.check.algorithm"; 299 private static final String DEFAULT_FILE_VERIFY_ALGORITHM = "MD5"; 300 301 static final String QUEUE_ADDITION_WAIT_TIME = "hbase.bucketcache.queue.addition.waittime"; 302 private static final long DEFAULT_QUEUE_ADDITION_WAIT_TIME = 0; 303 private long queueAdditionWaitTime; 304 /** 305 * Use {@link java.security.MessageDigest} class's encryption algorithms to check persistent file 306 * integrity, default algorithm is MD5 307 */ 308 private String algorithm; 309 310 private long persistenceChunkSize; 311 312 /* Tracing failed Bucket Cache allocations. */ 313 private long allocFailLogPrevTs; // time of previous log event for allocation failure. 314 private static final int ALLOCATION_FAIL_LOG_TIME_PERIOD = 60000; // Default 1 minute. 315 316 private transient Map<String, HRegion> onlineRegions; 317 318 private long orphanBlockGracePeriod = 0; 319 320 public BucketCache(String ioEngineName, long capacity, int blockSize, int[] bucketSizes, 321 int writerThreadNum, int writerQLen, String persistencePath) throws IOException { 322 this(ioEngineName, capacity, blockSize, bucketSizes, writerThreadNum, writerQLen, 323 persistencePath, DEFAULT_ERROR_TOLERATION_DURATION, HBaseConfiguration.create()); 324 } 325 326 public BucketCache(String ioEngineName, long capacity, int blockSize, int[] bucketSizes, 327 int writerThreadNum, int writerQLen, String persistencePath, int ioErrorsTolerationDuration, 328 Configuration conf) throws IOException { 329 this(ioEngineName, capacity, blockSize, bucketSizes, writerThreadNum, writerQLen, 330 persistencePath, ioErrorsTolerationDuration, conf, null); 331 } 332 333 public BucketCache(String ioEngineName, long capacity, int blockSize, int[] bucketSizes, 334 int writerThreadNum, int writerQLen, String persistencePath, int ioErrorsTolerationDuration, 335 Configuration conf, Map<String, HRegion> onlineRegions) throws IOException { 336 Preconditions.checkArgument(blockSize > 0, 337 "BucketCache capacity is set to " + blockSize + ", can not be less than 0"); 338 boolean useStrongRef = conf.getBoolean(STRONG_REF_KEY, STRONG_REF_DEFAULT); 339 if (useStrongRef) { 340 this.offsetLock = new IdReadWriteLockStrongRef<>(); 341 } else { 342 this.offsetLock = new IdReadWriteLockWithObjectPool<>(ReferenceType.SOFT); 343 } 344 this.conf = conf; 345 this.algorithm = conf.get(FILE_VERIFY_ALGORITHM, DEFAULT_FILE_VERIFY_ALGORITHM); 346 this.ioEngine = getIOEngineFromName(ioEngineName, capacity, persistencePath); 347 this.writerThreads = new WriterThread[writerThreadNum]; 348 this.onlineRegions = onlineRegions; 349 this.orphanBlockGracePeriod = 350 conf.getLong(BLOCK_ORPHAN_GRACE_PERIOD, BLOCK_ORPHAN_GRACE_PERIOD_DEFAULT); 351 long blockNumCapacity = capacity / blockSize; 352 if (blockNumCapacity >= Integer.MAX_VALUE) { 353 // Enough for about 32TB of cache! 354 throw new IllegalArgumentException("Cache capacity is too large, only support 32TB now"); 355 } 356 357 // these sets the dynamic configs 358 this.onConfigurationChange(conf); 359 this.cacheStats = 360 new BucketCacheStats(conf.getInt(BLOCKCACHE_STATS_PERIODS, DEFAULT_BLOCKCACHE_STATS_PERIODS), 361 conf.getInt(BLOCKCACHE_STATS_PERIOD_MINUTES_KEY, DEFAULT_BLOCKCACHE_STATS_PERIOD_MINUTES)); 362 363 LOG.info("Instantiating BucketCache with acceptableFactor: " + acceptableFactor 364 + ", minFactor: " + minFactor + ", extraFreeFactor: " + extraFreeFactor + ", singleFactor: " 365 + singleFactor + ", multiFactor: " + multiFactor + ", memoryFactor: " + memoryFactor 366 + ", useStrongRef: " + useStrongRef); 367 368 this.cacheCapacity = capacity; 369 this.persistencePath = persistencePath; 370 this.blockSize = blockSize; 371 this.ioErrorsTolerationDuration = ioErrorsTolerationDuration; 372 this.cacheState = CacheState.INITIALIZING; 373 374 this.allocFailLogPrevTs = 0; 375 376 for (int i = 0; i < writerThreads.length; ++i) { 377 writerQueues.add(new ArrayBlockingQueue<>(writerQLen)); 378 } 379 380 assert writerQueues.size() == writerThreads.length; 381 this.ramCache = new RAMCache(); 382 383 this.backingMap = new ConcurrentHashMap<>((int) blockNumCapacity); 384 instantiateWriterThreads(); 385 386 if (isCachePersistent()) { 387 if (ioEngine instanceof FileIOEngine) { 388 startBucketCachePersisterThread(); 389 } 390 startPersistenceRetriever(bucketSizes, capacity); 391 } else { 392 bucketAllocator = new BucketAllocator(capacity, bucketSizes); 393 this.cacheState = CacheState.ENABLED; 394 startWriterThreads(); 395 } 396 397 // Run the statistics thread periodically to print the cache statistics log 398 // TODO: Add means of turning this off. Bit obnoxious running thread just to make a log 399 // every five minutes. 400 this.scheduleThreadPool.scheduleAtFixedRate(new StatisticsThread(this), statThreadPeriod, 401 statThreadPeriod, TimeUnit.SECONDS); 402 LOG.info("Started bucket cache; ioengine=" + ioEngineName + ", capacity=" 403 + StringUtils.byteDesc(capacity) + ", blockSize=" + StringUtils.byteDesc(blockSize) 404 + ", writerThreadNum=" + writerThreadNum + ", writerQLen=" + writerQLen + ", persistencePath=" 405 + persistencePath + ", bucketAllocator=" + BucketAllocator.class.getName()); 406 } 407 408 private void startPersistenceRetriever(int[] bucketSizes, long capacity) { 409 Runnable persistentCacheRetriever = () -> { 410 try { 411 retrieveFromFile(bucketSizes); 412 LOG.info("Persistent bucket cache recovery from {} is complete.", persistencePath); 413 } catch (Throwable ex) { 414 LOG.warn("Can't restore from file[{}]. The bucket cache will be reset and rebuilt." 415 + " Exception seen: ", persistencePath, ex); 416 backingMap.clear(); 417 fullyCachedFiles.clear(); 418 backingMapValidated.set(true); 419 regionCachedSize.clear(); 420 try { 421 bucketAllocator = new BucketAllocator(capacity, bucketSizes); 422 } catch (BucketAllocatorException allocatorException) { 423 LOG.error("Exception during Bucket Allocation", allocatorException); 424 } 425 } finally { 426 synchronized (BucketCache.this) { 427 if (cacheState == CacheState.INITIALIZING) { 428 cacheState = CacheState.ENABLED; 429 startWriterThreads(); 430 } 431 } 432 } 433 }; 434 persistenceRetrieverThread = new Thread(persistentCacheRetriever, 435 "BucketCachePersistenceRetriever-" + System.identityHashCode(this)); 436 persistenceRetrieverThread.start(); 437 } 438 439 private void sanityCheckConfigs() { 440 Preconditions.checkArgument(acceptableFactor <= 1 && acceptableFactor >= 0, 441 ACCEPT_FACTOR_CONFIG_NAME + " must be between 0.0 and 1.0"); 442 Preconditions.checkArgument(minFactor <= 1 && minFactor >= 0, 443 MIN_FACTOR_CONFIG_NAME + " must be between 0.0 and 1.0"); 444 Preconditions.checkArgument(minFactor <= acceptableFactor, 445 MIN_FACTOR_CONFIG_NAME + " must be <= " + ACCEPT_FACTOR_CONFIG_NAME); 446 Preconditions.checkArgument(extraFreeFactor >= 0, 447 EXTRA_FREE_FACTOR_CONFIG_NAME + " must be greater than 0.0"); 448 Preconditions.checkArgument(singleFactor <= 1 && singleFactor >= 0, 449 SINGLE_FACTOR_CONFIG_NAME + " must be between 0.0 and 1.0"); 450 Preconditions.checkArgument(multiFactor <= 1 && multiFactor >= 0, 451 MULTI_FACTOR_CONFIG_NAME + " must be between 0.0 and 1.0"); 452 Preconditions.checkArgument(memoryFactor <= 1 && memoryFactor >= 0, 453 MEMORY_FACTOR_CONFIG_NAME + " must be between 0.0 and 1.0"); 454 Preconditions.checkArgument((singleFactor + multiFactor + memoryFactor) == 1, 455 SINGLE_FACTOR_CONFIG_NAME + ", " + MULTI_FACTOR_CONFIG_NAME + ", and " 456 + MEMORY_FACTOR_CONFIG_NAME + " segments must add up to 1.0"); 457 if (this.persistenceChunkSize <= 0) { 458 persistenceChunkSize = DEFAULT_BACKING_MAP_PERSISTENCE_CHUNK_SIZE; 459 } 460 } 461 462 /** 463 * Called by the constructor to instantiate the writer threads. 464 */ 465 private void instantiateWriterThreads() { 466 final String threadName = Thread.currentThread().getName(); 467 for (int i = 0; i < this.writerThreads.length; ++i) { 468 this.writerThreads[i] = new WriterThread(this.writerQueues.get(i)); 469 this.writerThreads[i].setName(threadName + "-BucketCacheWriter-" + i); 470 this.writerThreads[i].setDaemon(true); 471 } 472 } 473 474 /** 475 * Called by the constructor to start the writer threads. Used by tests that need to override 476 * starting the threads. 477 */ 478 protected void startWriterThreads() { 479 for (WriterThread thread : writerThreads) { 480 thread.start(); 481 } 482 } 483 484 void startBucketCachePersisterThread() { 485 LOG.info("Starting BucketCachePersisterThread"); 486 cachePersister = new BucketCachePersister(this, bucketcachePersistInterval); 487 cachePersister.setDaemon(true); 488 cachePersister.start(); 489 } 490 491 @Override 492 public boolean isCacheEnabled() { 493 return this.cacheState == CacheState.ENABLED; 494 } 495 496 @Override 497 public long getMaxSize() { 498 return this.cacheCapacity; 499 } 500 501 public String getIoEngine() { 502 return ioEngine.toString(); 503 } 504 505 /** 506 * Get the IOEngine from the IO engine name 507 * @return the IOEngine 508 */ 509 private IOEngine getIOEngineFromName(String ioEngineName, long capacity, String persistencePath) 510 throws IOException { 511 if (ioEngineName.startsWith("file:") || ioEngineName.startsWith("files:")) { 512 // In order to make the usage simple, we only need the prefix 'files:' in 513 // document whether one or multiple file(s), but also support 'file:' for 514 // the compatibility 515 String[] filePaths = 516 ioEngineName.substring(ioEngineName.indexOf(":") + 1).split(FileIOEngine.FILE_DELIMITER); 517 return new FileIOEngine(capacity, persistencePath != null, filePaths); 518 } else if (ioEngineName.startsWith("offheap")) { 519 return new ByteBufferIOEngine(capacity); 520 } else if (ioEngineName.startsWith("mmap:")) { 521 return new ExclusiveMemoryMmapIOEngine(ioEngineName.substring(5), capacity); 522 } else if (ioEngineName.startsWith("pmem:")) { 523 // This mode of bucket cache creates an IOEngine over a file on the persistent memory 524 // device. Since the persistent memory device has its own address space the contents 525 // mapped to this address space does not get swapped out like in the case of mmapping 526 // on to DRAM. Hence the cells created out of the hfile blocks in the pmem bucket cache 527 // can be directly referred to without having to copy them onheap. Once the RPC is done, 528 // the blocks can be returned back as in case of ByteBufferIOEngine. 529 return new SharedMemoryMmapIOEngine(ioEngineName.substring(5), capacity); 530 } else { 531 throw new IllegalArgumentException( 532 "Don't understand io engine name for cache- prefix with file:, files:, mmap: or offheap"); 533 } 534 } 535 536 public boolean isCachePersistenceEnabled() { 537 return persistencePath != null; 538 } 539 540 /** 541 * Cache the block with the specified name and buffer. 542 * @param cacheKey block's cache key 543 * @param buf block buffer 544 */ 545 @Override 546 public void cacheBlock(BlockCacheKey cacheKey, Cacheable buf) { 547 cacheBlock(cacheKey, buf, false); 548 } 549 550 /** 551 * Cache the block with the specified name and buffer. 552 * @param cacheKey block's cache key 553 * @param cachedItem block buffer 554 * @param inMemory if block is in-memory 555 */ 556 @Override 557 public void cacheBlock(BlockCacheKey cacheKey, Cacheable cachedItem, boolean inMemory) { 558 cacheBlockWithWait(cacheKey, cachedItem, inMemory, false); 559 } 560 561 /** 562 * Cache the block with the specified name and buffer. 563 * @param cacheKey block's cache key 564 * @param cachedItem block buffer 565 * @param inMemory if block is in-memory 566 */ 567 @Override 568 public void cacheBlock(BlockCacheKey cacheKey, Cacheable cachedItem, boolean inMemory, 569 boolean waitWhenCache) { 570 cacheBlockWithWait(cacheKey, cachedItem, inMemory, waitWhenCache && queueAdditionWaitTime > 0); 571 } 572 573 /** 574 * Cache the block to ramCache 575 * @param cacheKey block's cache key 576 * @param cachedItem block buffer 577 * @param inMemory if block is in-memory 578 * @param wait if true, blocking wait when queue is full 579 */ 580 public void cacheBlockWithWait(BlockCacheKey cacheKey, Cacheable cachedItem, boolean inMemory, 581 boolean wait) { 582 if (isCacheEnabled()) { 583 if (backingMap.containsKey(cacheKey) || ramCache.containsKey(cacheKey)) { 584 if (shouldReplaceExistingCacheBlock(cacheKey, cachedItem)) { 585 BucketEntry bucketEntry = backingMap.get(cacheKey); 586 if (bucketEntry != null && bucketEntry.isRpcRef()) { 587 // avoid replace when there are RPC refs for the bucket entry in bucket cache 588 return; 589 } 590 cacheBlockWithWaitInternal(cacheKey, cachedItem, inMemory, wait); 591 } 592 } else { 593 cacheBlockWithWaitInternal(cacheKey, cachedItem, inMemory, wait); 594 } 595 } 596 } 597 598 protected boolean shouldReplaceExistingCacheBlock(BlockCacheKey cacheKey, Cacheable newBlock) { 599 return BlockCacheUtil.shouldReplaceExistingCacheBlock(this, cacheKey, newBlock); 600 } 601 602 protected void cacheBlockWithWaitInternal(BlockCacheKey cacheKey, Cacheable cachedItem, 603 boolean inMemory, boolean wait) { 604 if (!isCacheEnabled()) { 605 return; 606 } 607 if (cacheKey.getBlockType() == null && cachedItem.getBlockType() != null) { 608 cacheKey.setBlockType(cachedItem.getBlockType()); 609 } 610 LOG.debug("Caching key={}, item={}, key heap size={}", cacheKey, cachedItem, 611 cacheKey.heapSize()); 612 // Stuff the entry into the RAM cache so it can get drained to the persistent store 613 RAMQueueEntry re = new RAMQueueEntry(cacheKey, cachedItem, accessCount.incrementAndGet(), 614 inMemory, isCachePersistent() && ioEngine instanceof FileIOEngine, wait); 615 /** 616 * Don't use ramCache.put(cacheKey, re) here. because there may be a existing entry with same 617 * key in ramCache, the heap size of bucket cache need to update if replacing entry from 618 * ramCache. But WriterThread will also remove entry from ramCache and update heap size, if 619 * using ramCache.put(), It's possible that the removed entry in WriterThread is not the correct 620 * one, then the heap size will mess up (HBASE-20789) 621 */ 622 if (ramCache.putIfAbsent(cacheKey, re) != null) { 623 return; 624 } 625 int queueNum = (cacheKey.hashCode() & 0x7FFFFFFF) % writerQueues.size(); 626 BlockingQueue<RAMQueueEntry> bq = writerQueues.get(queueNum); 627 boolean successfulAddition = false; 628 if (wait) { 629 try { 630 successfulAddition = bq.offer(re, queueAdditionWaitTime, TimeUnit.MILLISECONDS); 631 } catch (InterruptedException e) { 632 LOG.error("Thread interrupted: ", e); 633 Thread.currentThread().interrupt(); 634 } 635 } else { 636 successfulAddition = bq.offer(re); 637 } 638 if (!successfulAddition) { 639 LOG.debug("Failed to insert block {} into the cache writers queue", cacheKey); 640 ramCache.remove(cacheKey); 641 cacheStats.failInsert(); 642 } else { 643 this.blockNumber.increment(); 644 this.heapSize.add(cachedItem.heapSize()); 645 } 646 } 647 648 /** 649 * If the passed cache key relates to a reference (<hfile>.<parentEncRegion>), this 650 * method looks for the block from the referred file, in the cache. If present in the cache, the 651 * block for the referred file is returned, otherwise, this method returns null. It will also 652 * return null if the passed cache key doesn't relate to a reference. 653 * @param key the BlockCacheKey instance to look for in the cache. 654 * @return the cached block from the referred file, null if there's no such block in the cache or 655 * the passed key doesn't relate to a reference. 656 */ 657 public BucketEntry getBlockForReference(BlockCacheKey key) { 658 BlockCacheKey referredKey = getBlockKeyForReference(key); 659 BucketEntry foundEntry = referredKey != null ? backingMap.get(referredKey) : null; 660 if (referredKey != null) { 661 LOG.debug("Got a link/ref: {}. Related cacheKey: {}. Found entry: {}", key.getHfileName(), 662 referredKey, foundEntry); 663 } 664 return foundEntry; 665 } 666 667 private BlockCacheKey getBlockKeyForReference(BlockCacheKey key) { 668 if (!StoreFileInfo.isReference(key.getHfileName())) { 669 return null; 670 } 671 String referredFileName = 672 StoreFileInfo.getReferredToRegionAndFile(key.getHfileName()).getSecond(); 673 return referredFileName != null ? new BlockCacheKey(referredFileName, key.getOffset()) : null; 674 } 675 676 /** 677 * Get the buffer of the block with the specified key. 678 * @param key block's cache key 679 * @param caching true if the caller caches blocks on cache misses 680 * @param repeat Whether this is a repeat lookup for the same block 681 * @param updateCacheMetrics Whether we should update cache metrics or not 682 * @return buffer of specified cache key, or null if not in cache 683 */ 684 @Override 685 public Cacheable getBlock(BlockCacheKey key, boolean caching, boolean repeat, 686 boolean updateCacheMetrics) { 687 if (!isCacheEnabled()) { 688 cacheStats.miss(caching, key.isPrimary(), key.getBlockType()); 689 return null; 690 } 691 RAMQueueEntry re = ramCache.get(key); 692 if (re != null) { 693 if (updateCacheMetrics) { 694 cacheStats.hit(caching, key.isPrimary(), key.getBlockType()); 695 } 696 re.access(accessCount.incrementAndGet()); 697 return re.getData(); 698 } 699 BlockCacheKey backingMapLookupKey = key; 700 BucketEntry bucketEntry = backingMap.get(backingMapLookupKey); 701 LOG.debug("bucket entry for key {}: {}", key, 702 bucketEntry == null ? null : bucketEntry.offset()); 703 if (bucketEntry == null) { 704 backingMapLookupKey = getBlockKeyForReference(key); 705 if (backingMapLookupKey != null) { 706 bucketEntry = backingMap.get(backingMapLookupKey); 707 LOG.debug("Got a link/ref: {}. Related cacheKey: {}. Found entry: {}", key.getHfileName(), 708 backingMapLookupKey, bucketEntry); 709 } 710 } 711 if (bucketEntry != null) { 712 long start = System.nanoTime(); 713 ReentrantReadWriteLock lock = offsetLock.getLock(bucketEntry.offset()); 714 boolean inconsistentEntry = false; 715 try { 716 lock.readLock().lock(); 717 // We can not read here even if backingMap does contain the given key because its offset 718 // maybe changed. If we lock BlockCacheKey instead of offset, then we can only check 719 // existence here. 720 if (bucketEntry.equals(backingMap.get(backingMapLookupKey))) { 721 // Read the block from IOEngine based on the bucketEntry's offset and length, NOTICE: the 722 // block will use the refCnt of bucketEntry, which means if two HFileBlock mapping to 723 // the same BucketEntry, then all of the three will share the same refCnt. 724 Cacheable cachedBlock = ioEngine.read(bucketEntry); 725 if (ioEngine.usesSharedMemory()) { 726 // If IOEngine use shared memory, cachedBlock and BucketEntry will share the 727 // same RefCnt, do retain here, in order to count the number of RPC references 728 cachedBlock.retain(); 729 } 730 // Update the cache statistics. 731 if (updateCacheMetrics) { 732 cacheStats.hit(caching, key.isPrimary(), key.getBlockType()); 733 cacheStats.ioHit(System.nanoTime() - start); 734 } 735 bucketEntry.access(accessCount.incrementAndGet()); 736 if (this.ioErrorStartTime > 0) { 737 ioErrorStartTime = -1; 738 } 739 return cachedBlock; 740 } 741 } catch (HBaseIOException hioex) { 742 // FileIOEngine throws this when its cached time differs from the persisted index. A plain 743 // IOException still follows the configured tolerance policy below. 744 inconsistentEntry = true; 745 LOG.debug("Failed to fetch block for cache key: {}.", key, hioex); 746 } catch (IOException ioex) { 747 LOG.error("Failed reading block " + key + " from bucket cache", ioex); 748 checkIOErrorIsTolerated(); 749 } finally { 750 lock.readLock().unlock(); 751 } 752 if (inconsistentEntry) { 753 evictInconsistentEntry(backingMapLookupKey, bucketEntry); 754 } 755 } 756 if (!repeat && updateCacheMetrics) { 757 cacheStats.miss(caching, key.isPrimary(), key.getBlockType()); 758 } 759 return null; 760 } 761 762 private void evictInconsistentEntry(BlockCacheKey lookupKey, BucketEntry bucketEntry) { 763 BlockCacheKey storedKey = blocksByHFile.ceiling(lookupKey); 764 if (storedKey == null || !storedKey.equals(lookupKey)) { 765 return; 766 } 767 bucketEntry.withWriteLock(offsetLock, () -> { 768 if (backingMap.remove(storedKey, bucketEntry)) { 769 blockEvicted(storedKey, bucketEntry, true, false); 770 } 771 return null; 772 }); 773 } 774 775 /** 776 * This method is invoked after the bucketEntry is removed from {@link BucketCache#backingMap} 777 */ 778 void blockEvicted(BlockCacheKey cacheKey, BucketEntry bucketEntry, boolean decrementBlockNumber, 779 boolean evictedByEvictionProcess) { 780 bucketEntry.markAsEvicted(); 781 blocksByHFile.remove(cacheKey); 782 if (decrementBlockNumber) { 783 this.blockNumber.decrement(); 784 if (ioEngine.isPersistent()) { 785 fileNotFullyCached(cacheKey, bucketEntry); 786 } 787 } 788 if (evictedByEvictionProcess) { 789 cacheStats.evicted(bucketEntry.getCachedTime(), cacheKey.isPrimary()); 790 } 791 if (ioEngine.isPersistent()) { 792 setCacheInconsistent(true); 793 } 794 } 795 796 private void fileNotFullyCached(BlockCacheKey key, BucketEntry entry) { 797 // Update the updateRegionCachedSize before removing the file from fullyCachedFiles. 798 // This computation should happen even if the file is not in fullyCachedFiles map. 799 updateRegionCachedSize(key, (entry.getLength() * -1)); 800 fullyCachedFiles.remove(key.getHfileName()); 801 } 802 803 public void fileCacheCompleted(Path filePath, long size) { 804 Pair<String, Long> pair = new Pair<>(); 805 // sets the region name 806 String regionName = filePath.getParent().getParent().getName(); 807 pair.setFirst(regionName); 808 pair.setSecond(size); 809 fullyCachedFiles.put(filePath.getName(), pair); 810 } 811 812 private void updateRegionCachedSize(BlockCacheKey key, long cachedSize) { 813 if (key.getRegionName() != null) { 814 if (key.isArchived()) { 815 LOG.trace("Skipping region cached size update for archived file:{} from region: {}", 816 key.getHfileName(), key.getRegionName()); 817 } else { 818 String regionName = key.getRegionName(); 819 regionCachedSize.merge(regionName, cachedSize, 820 (previousSize, newBlockSize) -> previousSize + newBlockSize); 821 // If all the blocks for a region are evicted from the cache, 822 // remove the entry for that region from regionCachedSize map. 823 if (regionCachedSize.getOrDefault(regionName, 0L) <= 0) { 824 regionCachedSize.remove(regionName); 825 } 826 } 827 } 828 } 829 830 /** 831 * Free the {{@link BucketEntry} actually,which could only be invoked when the 832 * {@link BucketEntry#refCnt} becoming 0. 833 */ 834 void freeBucketEntry(BucketEntry bucketEntry) { 835 bucketAllocator.freeBlock(bucketEntry.offset(), bucketEntry.getLength()); 836 realCacheSize.add(-1 * bucketEntry.getLength()); 837 } 838 839 /** 840 * Try to evict the block from {@link BlockCache} by force. We'll call this in few cases:<br> 841 * 1. Close an HFile, and clear all cached blocks. <br> 842 * 2. Call {@link Admin#clearBlockCache(TableName)} to clear all blocks for a given table.<br> 843 * <p> 844 * Firstly, we'll try to remove the block from RAMCache,and then try to evict from backingMap. 845 * Here we evict the block from backingMap immediately, but only free the reference from bucket 846 * cache by calling {@link BucketEntry#markedAsEvicted}. If there're still some RPC referring this 847 * block, block can only be de-allocated when all of them release the block. 848 * <p> 849 * NOTICE: we need to grab the write offset lock firstly before releasing the reference from 850 * bucket cache. if we don't, we may read an {@link BucketEntry} with refCnt = 0 when 851 * {@link BucketCache#getBlock(BlockCacheKey, boolean, boolean, boolean)}, it's a memory leak. 852 * @param cacheKey Block to evict 853 * @return true to indicate whether we've evicted successfully or not. 854 */ 855 @Override 856 public boolean evictBlock(BlockCacheKey cacheKey) { 857 return doEvictBlock(cacheKey, null, false); 858 } 859 860 /** 861 * Evict the {@link BlockCacheKey} and {@link BucketEntry} from {@link BucketCache#backingMap} and 862 * {@link BucketCache#ramCache}. <br/> 863 * NOTE:When Evict from {@link BucketCache#backingMap},only the matched {@link BlockCacheKey} and 864 * {@link BucketEntry} could be removed. 865 * @param cacheKey {@link BlockCacheKey} to evict. 866 * @param bucketEntry {@link BucketEntry} matched {@link BlockCacheKey} to evict. 867 * @return true to indicate whether we've evicted successfully or not. 868 */ 869 private boolean doEvictBlock(BlockCacheKey cacheKey, BucketEntry bucketEntry, 870 boolean evictedByEvictionProcess) { 871 if (!isCacheEnabled()) { 872 return false; 873 } 874 boolean existedInRamCache = removeFromRamCache(cacheKey); 875 if (bucketEntry == null) { 876 bucketEntry = backingMap.get(cacheKey); 877 } 878 final BucketEntry bucketEntryToUse = bucketEntry; 879 880 if (bucketEntryToUse == null) { 881 if (existedInRamCache && evictedByEvictionProcess) { 882 cacheStats.evicted(0, cacheKey.isPrimary()); 883 } 884 return existedInRamCache; 885 } else { 886 return bucketEntryToUse.withWriteLock(offsetLock, () -> { 887 if (backingMap.remove(cacheKey, bucketEntryToUse)) { 888 LOG.debug("removed key {} from back map with offset lock {} in the evict process", 889 cacheKey, bucketEntryToUse.offset()); 890 blockEvicted(cacheKey, bucketEntryToUse, !existedInRamCache, evictedByEvictionProcess); 891 return true; 892 } 893 return false; 894 }); 895 } 896 } 897 898 /** 899 * <pre> 900 * Create the {@link Recycler} for {@link BucketEntry#refCnt},which would be used as 901 * {@link RefCnt#recycler} of {@link HFileBlock#buf} returned from {@link BucketCache#getBlock}. 902 * NOTE: for {@link BucketCache#getBlock},the {@link RefCnt#recycler} of {@link HFileBlock#buf} 903 * from {@link BucketCache#backingMap} and {@link BucketCache#ramCache} are different: 904 * 1.For {@link RefCnt#recycler} of {@link HFileBlock#buf} from {@link BucketCache#backingMap}, 905 * it is the return value of current {@link BucketCache#createRecycler} method. 906 * 907 * 2.For {@link RefCnt#recycler} of {@link HFileBlock#buf} from {@link BucketCache#ramCache}, 908 * it is {@link ByteBuffAllocator#putbackBuffer}. 909 * </pre> 910 */ 911 public Recycler createRecycler(final BucketEntry bucketEntry) { 912 return () -> { 913 freeBucketEntry(bucketEntry); 914 return; 915 }; 916 } 917 918 /** 919 * NOTE: This method is only for test. 920 */ 921 public boolean evictBlockIfNoRpcReferenced(BlockCacheKey blockCacheKey) { 922 BucketEntry bucketEntry = backingMap.get(blockCacheKey); 923 if (bucketEntry == null) { 924 return false; 925 } 926 return evictBucketEntryIfNoRpcReferenced(blockCacheKey, bucketEntry); 927 } 928 929 /** 930 * Evict {@link BlockCacheKey} and its corresponding {@link BucketEntry} only if 931 * {@link BucketEntry#isRpcRef} is false. <br/> 932 * NOTE:When evict from {@link BucketCache#backingMap},only the matched {@link BlockCacheKey} and 933 * {@link BucketEntry} could be removed. 934 * @param blockCacheKey {@link BlockCacheKey} to evict. 935 * @param bucketEntry {@link BucketEntry} matched {@link BlockCacheKey} to evict. 936 * @return true to indicate whether we've evicted successfully or not. 937 */ 938 boolean evictBucketEntryIfNoRpcReferenced(BlockCacheKey blockCacheKey, BucketEntry bucketEntry) { 939 if (!bucketEntry.isRpcRef()) { 940 return doEvictBlock(blockCacheKey, bucketEntry, true); 941 } 942 return false; 943 } 944 945 /** 946 * Since HBASE-29249, the following properties governin freeSpace behaviour and block priorities 947 * were made dynamically configurable: - hbase.bucketcache.acceptfactor - 948 * hbase.bucketcache.minfactor - hbase.bucketcache.extrafreefactor - 949 * hbase.bucketcache.single.factor - hbase.bucketcache.multi.factor - 950 * hbase.bucketcache.multi.factor - hbase.bucketcache.memory.factor The 951 * hbase.bucketcache.queue.addition.waittime property allows for introducing a delay in the 952 * publishing of blocks for the cache writer threads during prefetch reads only (client reads 953 * wouldn't get delayed). It has also been made dynamic configurable since HBASE-29249. The 954 * hbase.bucketcache.persist.intervalinmillis propperty determines the frequency for saving the 955 * persistent cache, and it has also been made dynamically configurable since HBASE-29249. The 956 * hbase.bucketcache.persistence.chunksize property determines the size of the persistent file 957 * splits (due to the limitation of maximum allowed protobuff size), and it has also been made 958 * dynamically configurable since HBASE-29249. 959 * @param config the new configuration to be updated. 960 */ 961 @Override 962 public void onConfigurationChange(Configuration config) { 963 this.acceptableFactor = conf.getFloat(ACCEPT_FACTOR_CONFIG_NAME, DEFAULT_ACCEPT_FACTOR); 964 this.minFactor = conf.getFloat(MIN_FACTOR_CONFIG_NAME, DEFAULT_MIN_FACTOR); 965 this.extraFreeFactor = conf.getFloat(EXTRA_FREE_FACTOR_CONFIG_NAME, DEFAULT_EXTRA_FREE_FACTOR); 966 this.singleFactor = conf.getFloat(SINGLE_FACTOR_CONFIG_NAME, DEFAULT_SINGLE_FACTOR); 967 this.multiFactor = conf.getFloat(MULTI_FACTOR_CONFIG_NAME, DEFAULT_MULTI_FACTOR); 968 this.memoryFactor = conf.getFloat(MEMORY_FACTOR_CONFIG_NAME, DEFAULT_MEMORY_FACTOR); 969 this.queueAdditionWaitTime = 970 conf.getLong(QUEUE_ADDITION_WAIT_TIME, DEFAULT_QUEUE_ADDITION_WAIT_TIME); 971 this.bucketcachePersistInterval = conf.getLong(BUCKETCACHE_PERSIST_INTERVAL_KEY, 1000); 972 this.persistenceChunkSize = 973 conf.getLong(BACKING_MAP_PERSISTENCE_CHUNK_SIZE, DEFAULT_BACKING_MAP_PERSISTENCE_CHUNK_SIZE); 974 sanityCheckConfigs(); 975 } 976 977 protected boolean removeFromRamCache(BlockCacheKey cacheKey) { 978 return ramCache.remove(cacheKey, re -> { 979 if (re != null) { 980 this.blockNumber.decrement(); 981 this.heapSize.add(-1 * re.getData().heapSize()); 982 } 983 }); 984 } 985 986 public boolean isCacheInconsistent() { 987 return isCacheInconsistent.get(); 988 } 989 990 public void setCacheInconsistent(boolean setCacheInconsistent) { 991 isCacheInconsistent.set(setCacheInconsistent); 992 } 993 994 protected void setCacheState(CacheState state) { 995 cacheState = state; 996 } 997 998 /* 999 * Statistics thread. Periodically output cache statistics to the log. 1000 */ 1001 private static class StatisticsThread extends Thread { 1002 private final BucketCache bucketCache; 1003 1004 public StatisticsThread(BucketCache bucketCache) { 1005 super("BucketCacheStatsThread"); 1006 setDaemon(true); 1007 this.bucketCache = bucketCache; 1008 } 1009 1010 @Override 1011 public void run() { 1012 bucketCache.logStats(); 1013 } 1014 } 1015 1016 public void logStats() { 1017 if (!isCacheInitialized("BucketCache::logStats")) { 1018 return; 1019 } 1020 1021 long totalSize = bucketAllocator.getTotalSize(); 1022 long usedSize = bucketAllocator.getUsedSize(); 1023 long freeSize = totalSize - usedSize; 1024 long cacheSize = getRealCacheSize(); 1025 LOG.info("failedBlockAdditions=" + cacheStats.getFailedInserts() + ", " + "totalSize=" 1026 + StringUtils.byteDesc(totalSize) + ", " + "freeSize=" + StringUtils.byteDesc(freeSize) + ", " 1027 + "usedSize=" + StringUtils.byteDesc(usedSize) + ", " + "cacheSize=" 1028 + StringUtils.byteDesc(cacheSize) + ", " + "accesses=" + cacheStats.getRequestCount() + ", " 1029 + "hits=" + cacheStats.getHitCount() + ", " + "IOhitsPerSecond=" 1030 + cacheStats.getIOHitsPerSecond() + ", " + "IOTimePerHit=" 1031 + String.format("%.2f", cacheStats.getIOTimePerHit()) + ", " + "hitRatio=" 1032 + (cacheStats.getHitCount() == 0 1033 ? "0," 1034 : (StringUtils.formatPercent(cacheStats.getHitRatio(), 2) + ", ")) 1035 + "cachingAccesses=" + cacheStats.getRequestCachingCount() + ", " + "cachingHits=" 1036 + cacheStats.getHitCachingCount() + ", " + "cachingHitsRatio=" 1037 + (cacheStats.getHitCachingCount() == 0 1038 ? "0," 1039 : (StringUtils.formatPercent(cacheStats.getHitCachingRatio(), 2) + ", ")) 1040 + "evictions=" + cacheStats.getEvictionCount() + ", " + "evicted=" 1041 + cacheStats.getEvictedCount() + ", " + "evictedPerRun=" + cacheStats.evictedPerEviction() 1042 + ", " + "allocationFailCount=" + cacheStats.getAllocationFailCount() + ", blocksCount=" 1043 + backingMap.size()); 1044 cacheStats.reset(); 1045 1046 bucketAllocator.logDebugStatistics(); 1047 } 1048 1049 public long getRealCacheSize() { 1050 return this.realCacheSize.sum(); 1051 } 1052 1053 public long acceptableSize() { 1054 if (!isCacheInitialized("BucketCache::acceptableSize")) { 1055 return 0; 1056 } 1057 return (long) Math.floor(bucketAllocator.getTotalSize() * acceptableFactor); 1058 } 1059 1060 long getPartitionSize(float partitionFactor) { 1061 if (!isCacheInitialized("BucketCache::getPartitionSize")) { 1062 return 0; 1063 } 1064 1065 return (long) Math.floor(bucketAllocator.getTotalSize() * partitionFactor * minFactor); 1066 } 1067 1068 /** 1069 * Return the count of bucketSizeinfos still need free space 1070 */ 1071 private int bucketSizesAboveThresholdCount(float minFactor) { 1072 if (!isCacheInitialized("BucketCache::bucketSizesAboveThresholdCount")) { 1073 return 0; 1074 } 1075 1076 BucketAllocator.IndexStatistics[] stats = bucketAllocator.getIndexStatistics(); 1077 int fullCount = 0; 1078 for (int i = 0; i < stats.length; i++) { 1079 long freeGoal = (long) Math.floor(stats[i].totalCount() * (1 - minFactor)); 1080 freeGoal = Math.max(freeGoal, 1); 1081 if (stats[i].freeCount() < freeGoal) { 1082 fullCount++; 1083 } 1084 } 1085 return fullCount; 1086 } 1087 1088 /** 1089 * This method will find the buckets that are minimally occupied and are not reference counted and 1090 * will free them completely without any constraint on the access times of the elements, and as a 1091 * process will completely free at most the number of buckets passed, sometimes it might not due 1092 * to changing refCounts 1093 * @param completelyFreeBucketsNeeded number of buckets to free 1094 **/ 1095 private void freeEntireBuckets(int completelyFreeBucketsNeeded) { 1096 if (!isCacheInitialized("BucketCache::freeEntireBuckets")) { 1097 return; 1098 } 1099 1100 if (completelyFreeBucketsNeeded != 0) { 1101 // First we will build a set where the offsets are reference counted, usually 1102 // this set is small around O(Handler Count) unless something else is wrong 1103 Set<Integer> inUseBuckets = new HashSet<>(); 1104 backingMap.forEach((k, be) -> { 1105 if (be.isRpcRef()) { 1106 inUseBuckets.add(bucketAllocator.getBucketIndex(be.offset())); 1107 } 1108 }); 1109 Set<Integer> candidateBuckets = 1110 bucketAllocator.getLeastFilledBuckets(inUseBuckets, completelyFreeBucketsNeeded); 1111 for (Map.Entry<BlockCacheKey, BucketEntry> entry : backingMap.entrySet()) { 1112 if (candidateBuckets.contains(bucketAllocator.getBucketIndex(entry.getValue().offset()))) { 1113 evictBucketEntryIfNoRpcReferenced(entry.getKey(), entry.getValue()); 1114 } 1115 } 1116 } 1117 } 1118 1119 private long calculateBytesToFree(StringBuilder msgBuffer) { 1120 long bytesToFreeWithoutExtra = 0; 1121 BucketAllocator.IndexStatistics[] stats = bucketAllocator.getIndexStatistics(); 1122 long[] bytesToFreeForBucket = new long[stats.length]; 1123 for (int i = 0; i < stats.length; i++) { 1124 bytesToFreeForBucket[i] = 0; 1125 long freeGoal = (long) Math.floor(stats[i].totalCount() * (1 - minFactor)); 1126 freeGoal = Math.max(freeGoal, 1); 1127 if (stats[i].freeCount() < freeGoal) { 1128 bytesToFreeForBucket[i] = stats[i].itemSize() * (freeGoal - stats[i].freeCount()); 1129 bytesToFreeWithoutExtra += bytesToFreeForBucket[i]; 1130 if (msgBuffer != null) { 1131 msgBuffer.append("Free for bucketSize(" + stats[i].itemSize() + ")=" 1132 + StringUtils.byteDesc(bytesToFreeForBucket[i]) + ", "); 1133 } 1134 } 1135 } 1136 if (msgBuffer != null) { 1137 msgBuffer.append("Free for total=" + StringUtils.byteDesc(bytesToFreeWithoutExtra) + ", "); 1138 } 1139 return bytesToFreeWithoutExtra; 1140 } 1141 1142 /** 1143 * Free the space if the used size reaches acceptableSize() or one size block couldn't be 1144 * allocated. When freeing the space, we use the LRU algorithm and ensure there must be some 1145 * blocks evicted 1146 * @param why Why we are being called 1147 */ 1148 void freeSpace(final String why) { 1149 if (!isCacheInitialized("BucketCache::freeSpace")) { 1150 return; 1151 } 1152 // Ensure only one freeSpace progress at a time 1153 if (!freeSpaceLock.tryLock()) { 1154 return; 1155 } 1156 try { 1157 freeInProgress = true; 1158 StringBuilder msgBuffer = LOG.isDebugEnabled() ? new StringBuilder() : null; 1159 long bytesToFreeWithoutExtra = calculateBytesToFree(msgBuffer); 1160 if (bytesToFreeWithoutExtra <= 0) { 1161 return; 1162 } 1163 long currentSize = bucketAllocator.getUsedSize(); 1164 long totalSize = bucketAllocator.getTotalSize(); 1165 if (LOG.isDebugEnabled() && msgBuffer != null) { 1166 LOG.debug("Free started because \"" + why + "\"; " + msgBuffer + " of current used=" 1167 + StringUtils.byteDesc(currentSize) + ", actual cacheSize=" 1168 + StringUtils.byteDesc(realCacheSize.sum()) + ", total=" 1169 + StringUtils.byteDesc(totalSize)); 1170 } 1171 long bytesToFreeWithExtra = 1172 (long) Math.floor(bytesToFreeWithoutExtra * (1 + extraFreeFactor)); 1173 // Instantiate priority buckets 1174 BucketEntryGroup bucketSingle = 1175 new BucketEntryGroup(bytesToFreeWithExtra, blockSize, getPartitionSize(singleFactor)); 1176 BucketEntryGroup bucketMulti = 1177 new BucketEntryGroup(bytesToFreeWithExtra, blockSize, getPartitionSize(multiFactor)); 1178 BucketEntryGroup bucketMemory = 1179 new BucketEntryGroup(bytesToFreeWithExtra, blockSize, getPartitionSize(memoryFactor)); 1180 1181 Set<String> allValidFiles = null; 1182 // We need the region/stores/files tree, in order to figure out if a block is "orphan" or not. 1183 // See further comments below for more details. 1184 if (onlineRegions != null) { 1185 allValidFiles = BlockCacheUtil.listAllFilesNames(onlineRegions); 1186 } 1187 // the cached time is recored in nanos, so we need to convert the grace period accordingly 1188 long orphanGracePeriodNanos = orphanBlockGracePeriod * 1000000; 1189 long bytesFreed = 0; 1190 // Check the list of files to determine the cold files which can be readily evicted. 1191 Map<String, String> coldFiles = null; 1192 1193 DataTieringManager dataTieringManager = DataTieringManager.getInstance(); 1194 if (dataTieringManager != null) { 1195 coldFiles = dataTieringManager.getColdFilesList(); 1196 } 1197 // Scan entire map putting bucket entry into appropriate bucket entry 1198 // group 1199 for (Map.Entry<BlockCacheKey, BucketEntry> bucketEntryWithKey : backingMap.entrySet()) { 1200 BlockCacheKey key = bucketEntryWithKey.getKey(); 1201 BucketEntry entry = bucketEntryWithKey.getValue(); 1202 // Under certain conditions, blocks for regions not on the current region server might 1203 // be hanging on the cache. For example, when using the persistent cache feature, if the 1204 // RS crashes, then if not the same regions are assigned back once its online again, blocks 1205 // for the previous online regions would be recovered and stay in the cache. These would be 1206 // "orphan" blocks, as the files these blocks belong to are not in any of the online 1207 // regions. 1208 // "Orphan" blocks are a pure waste of cache space and should be evicted first during 1209 // the freespace run. 1210 // Compactions and Flushes may cache blocks before its files are completely written. In 1211 // these cases the file won't be found in any of the online regions stores, but the block 1212 // shouldn't be evicted. To avoid this, we defined this 1213 // hbase.bucketcache.block.orphan.evictgraceperiod property, to account for a grace 1214 // period (default 24 hours) where a block should be checked if it's an orphan block. 1215 if ( 1216 allValidFiles != null 1217 && entry.getCachedTime() < (System.nanoTime() - orphanGracePeriodNanos) 1218 ) { 1219 if (!allValidFiles.contains(key.getHfileName())) { 1220 if (evictBucketEntryIfNoRpcReferenced(key, entry)) { 1221 // We calculate the freed bytes, but we don't stop if the goal was reached because 1222 // these are orphan blocks anyway, so let's leverage this run of freeSpace 1223 // to get rid of all orphans at once. 1224 bytesFreed += entry.getLength(); 1225 continue; 1226 } 1227 } 1228 } 1229 1230 if ( 1231 bytesFreed < bytesToFreeWithExtra && coldFiles != null 1232 && coldFiles.containsKey(bucketEntryWithKey.getKey().getHfileName()) 1233 ) { 1234 int freedBlockSize = bucketEntryWithKey.getValue().getLength(); 1235 if (evictBlockIfNoRpcReferenced(bucketEntryWithKey.getKey())) { 1236 bytesFreed += freedBlockSize; 1237 } 1238 continue; 1239 } 1240 1241 switch (entry.getPriority()) { 1242 case SINGLE: { 1243 bucketSingle.add(bucketEntryWithKey); 1244 break; 1245 } 1246 case MULTI: { 1247 bucketMulti.add(bucketEntryWithKey); 1248 break; 1249 } 1250 case MEMORY: { 1251 bucketMemory.add(bucketEntryWithKey); 1252 break; 1253 } 1254 } 1255 } 1256 1257 // Check if the cold file eviction is sufficient to create enough space. 1258 bytesToFreeWithExtra -= bytesFreed; 1259 if (bytesToFreeWithExtra <= 0) { 1260 LOG.debug("Bucket cache free space completed; freed space : {} bytes of cold data blocks.", 1261 StringUtils.byteDesc(bytesFreed)); 1262 return; 1263 } 1264 1265 if (LOG.isDebugEnabled()) { 1266 LOG.debug( 1267 "Bucket cache free space completed; freed space : {} " 1268 + "bytes of cold data blocks. {} more bytes required to be freed.", 1269 StringUtils.byteDesc(bytesFreed), bytesToFreeWithExtra); 1270 } 1271 1272 PriorityQueue<BucketEntryGroup> bucketQueue = 1273 new PriorityQueue<>(3, Comparator.comparingLong(BucketEntryGroup::overflow)); 1274 1275 bucketQueue.add(bucketSingle); 1276 bucketQueue.add(bucketMulti); 1277 bucketQueue.add(bucketMemory); 1278 1279 int remainingBuckets = bucketQueue.size(); 1280 BucketEntryGroup bucketGroup; 1281 while ((bucketGroup = bucketQueue.poll()) != null) { 1282 long overflow = bucketGroup.overflow(); 1283 if (overflow > 0) { 1284 long bucketBytesToFree = 1285 Math.min(overflow, (bytesToFreeWithoutExtra - bytesFreed) / remainingBuckets); 1286 bytesFreed += bucketGroup.free(bucketBytesToFree); 1287 } 1288 remainingBuckets--; 1289 } 1290 1291 // Check and free if there are buckets that still need freeing of space 1292 if (bucketSizesAboveThresholdCount(minFactor) > 0) { 1293 bucketQueue.clear(); 1294 remainingBuckets = 3; 1295 bucketQueue.add(bucketSingle); 1296 bucketQueue.add(bucketMulti); 1297 bucketQueue.add(bucketMemory); 1298 while ((bucketGroup = bucketQueue.poll()) != null) { 1299 long bucketBytesToFree = (bytesToFreeWithExtra - bytesFreed) / remainingBuckets; 1300 bytesFreed += bucketGroup.free(bucketBytesToFree); 1301 remainingBuckets--; 1302 } 1303 } 1304 // Even after the above free we might still need freeing because of the 1305 // De-fragmentation of the buckets (also called Slab Calcification problem), i.e 1306 // there might be some buckets where the occupancy is very sparse and thus are not 1307 // yielding the free for the other bucket sizes, the fix for this to evict some 1308 // of the buckets, we do this by evicting the buckets that are least fulled 1309 freeEntireBuckets(DEFAULT_FREE_ENTIRE_BLOCK_FACTOR * bucketSizesAboveThresholdCount(1.0f)); 1310 1311 if (LOG.isDebugEnabled()) { 1312 long single = bucketSingle.totalSize(); 1313 long multi = bucketMulti.totalSize(); 1314 long memory = bucketMemory.totalSize(); 1315 if (LOG.isDebugEnabled()) { 1316 LOG.debug("Bucket cache free space completed; " + "freed=" 1317 + StringUtils.byteDesc(bytesFreed) + ", " + "total=" + StringUtils.byteDesc(totalSize) 1318 + ", " + "single=" + StringUtils.byteDesc(single) + ", " + "multi=" 1319 + StringUtils.byteDesc(multi) + ", " + "memory=" + StringUtils.byteDesc(memory)); 1320 } 1321 } 1322 } catch (Throwable t) { 1323 LOG.warn("Failed freeing space", t); 1324 } finally { 1325 cacheStats.evict(); 1326 freeInProgress = false; 1327 freeSpaceLock.unlock(); 1328 } 1329 } 1330 1331 // This handles flushing the RAM cache to IOEngine. 1332 class WriterThread extends Thread { 1333 private final BlockingQueue<RAMQueueEntry> inputQueue; 1334 private volatile boolean writerEnabled = true; 1335 private final ByteBuffer metaBuff = ByteBuffer.allocate(HFileBlock.BLOCK_METADATA_SPACE); 1336 1337 WriterThread(BlockingQueue<RAMQueueEntry> queue) { 1338 super("BucketCacheWriterThread"); 1339 this.inputQueue = queue; 1340 } 1341 1342 // Used for test 1343 void disableWriter() { 1344 this.writerEnabled = false; 1345 } 1346 1347 @Override 1348 public void run() { 1349 List<RAMQueueEntry> entries = new ArrayList<>(); 1350 try { 1351 while (isCacheEnabled() && writerEnabled) { 1352 try { 1353 try { 1354 // Blocks 1355 entries = getRAMQueueEntries(inputQueue, entries); 1356 } catch (InterruptedException ie) { 1357 if (!isCacheEnabled() || !writerEnabled) { 1358 break; 1359 } 1360 } 1361 doDrain(entries, metaBuff); 1362 } catch (Exception ioe) { 1363 LOG.error("WriterThread encountered error", ioe); 1364 } 1365 } 1366 } catch (Throwable t) { 1367 LOG.warn("Failed doing drain", t); 1368 } 1369 LOG.info(this.getName() + " exiting, cacheEnabled=" + isCacheEnabled()); 1370 } 1371 } 1372 1373 /** 1374 * Put the new bucket entry into backingMap. Notice that we are allowed to replace the existing 1375 * cache with a new block for the same cache key. there's a corner case: one thread cache a block 1376 * in ramCache, copy to io-engine and add a bucket entry to backingMap. Caching another new block 1377 * with the same cache key do the same thing for the same cache key, so if not evict the previous 1378 * bucket entry, then memory leak happen because the previous bucketEntry is gone but the 1379 * bucketAllocator do not free its memory. 1380 * @see BlockCacheUtil#shouldReplaceExistingCacheBlock(BlockCache blockCache,BlockCacheKey 1381 * cacheKey, Cacheable newBlock) 1382 * @param key Block cache key 1383 * @param bucketEntry Bucket entry to put into backingMap. 1384 */ 1385 protected void putIntoBackingMap(BlockCacheKey key, BucketEntry bucketEntry) { 1386 BucketEntry previousEntry = backingMap.put(key, bucketEntry); 1387 updateRegionCachedSize(key, bucketEntry.getLength()); 1388 if (previousEntry != null && previousEntry != bucketEntry) { 1389 previousEntry.withWriteLock(offsetLock, () -> { 1390 blockEvicted(key, previousEntry, false, false); 1391 return null; 1392 }); 1393 } 1394 bucketEntry.withWriteLock(offsetLock, () -> { 1395 if (backingMap.get(key) == bucketEntry) { 1396 blocksByHFile.add(key); 1397 } 1398 return null; 1399 }); 1400 } 1401 1402 /** 1403 * Prepare and return a warning message for Bucket Allocator Exception 1404 * @param fle The exception 1405 * @param re The RAMQueueEntry for which the exception was thrown. 1406 * @return A warning message created from the input RAMQueueEntry object. 1407 */ 1408 private static String getAllocationFailWarningMessage(final BucketAllocatorException fle, 1409 final RAMQueueEntry re) { 1410 final StringBuilder sb = new StringBuilder(); 1411 sb.append("Most recent failed allocation after "); 1412 sb.append(ALLOCATION_FAIL_LOG_TIME_PERIOD); 1413 sb.append(" ms;"); 1414 if (re != null) { 1415 if (re.getData() instanceof HFileBlock) { 1416 final HFileContext fileContext = ((HFileBlock) re.getData()).getHFileContext(); 1417 final String columnFamily = Bytes.toString(fileContext.getColumnFamily()); 1418 final String tableName = Bytes.toString(fileContext.getTableName()); 1419 if (tableName != null) { 1420 sb.append(" Table: "); 1421 sb.append(tableName); 1422 } 1423 if (columnFamily != null) { 1424 sb.append(" CF: "); 1425 sb.append(columnFamily); 1426 } 1427 sb.append(" HFile: "); 1428 if (fileContext.getHFileName() != null) { 1429 sb.append(fileContext.getHFileName()); 1430 } else { 1431 sb.append(re.getKey()); 1432 } 1433 } else { 1434 sb.append(" HFile: "); 1435 sb.append(re.getKey()); 1436 } 1437 } 1438 sb.append(" Message: "); 1439 sb.append(fle.getMessage()); 1440 return sb.toString(); 1441 } 1442 1443 /** 1444 * Flush the entries in ramCache to IOEngine and add bucket entry to backingMap. Process all that 1445 * are passed in even if failure being sure to remove from ramCache else we'll never undo the 1446 * references and we'll OOME. 1447 * @param entries Presumes list passed in here will be processed by this invocation only. No 1448 * interference expected. 1449 */ 1450 void doDrain(final List<RAMQueueEntry> entries, ByteBuffer metaBuff) throws InterruptedException { 1451 if (entries.isEmpty()) { 1452 return; 1453 } 1454 // This method is a little hard to follow. We run through the passed in entries and for each 1455 // successful add, we add a non-null BucketEntry to the below bucketEntries. Later we must 1456 // do cleanup making sure we've cleared ramCache of all entries regardless of whether we 1457 // successfully added the item to the bucketcache; if we don't do the cleanup, we'll OOME by 1458 // filling ramCache. We do the clean up by again running through the passed in entries 1459 // doing extra work when we find a non-null bucketEntries corresponding entry. 1460 final int size = entries.size(); 1461 BucketEntry[] bucketEntries = new BucketEntry[size]; 1462 // Index updated inside loop if success or if we can't succeed. We retry if cache is full 1463 // when we go to add an entry by going around the loop again without upping the index. 1464 int index = 0; 1465 while (isCacheEnabled() && index < size) { 1466 RAMQueueEntry re = null; 1467 try { 1468 re = entries.get(index); 1469 if (re == null) { 1470 LOG.warn("Couldn't get entry or changed on us; who else is messing with it?"); 1471 index++; 1472 continue; 1473 } 1474 // Reset the position for reuse. 1475 // It should be guaranteed that the data in the metaBuff has been transferred to the 1476 // ioEngine safely. Otherwise, this reuse is problematic. Fortunately, the data is already 1477 // transferred with our current IOEngines. Should take care, when we have new kinds of 1478 // IOEngine in the future. 1479 metaBuff.clear(); 1480 BucketEntry bucketEntry = re.writeToCache(ioEngine, bucketAllocator, realCacheSize, 1481 this::createRecycler, metaBuff, acceptableSize()); 1482 // Successfully added. Up index and add bucketEntry. Clear io exceptions. 1483 bucketEntries[index] = bucketEntry; 1484 if (ioErrorStartTime > 0) { 1485 ioErrorStartTime = -1; 1486 } 1487 index++; 1488 } catch (BucketAllocatorException fle) { 1489 long currTs = EnvironmentEdgeManager.currentTime(); 1490 cacheStats.allocationFailed(); // Record the warning. 1491 if ( 1492 allocFailLogPrevTs == 0 || (currTs - allocFailLogPrevTs) > ALLOCATION_FAIL_LOG_TIME_PERIOD 1493 ) { 1494 LOG.warn(getAllocationFailWarningMessage(fle, re)); 1495 allocFailLogPrevTs = currTs; 1496 } 1497 // Presume can't add. Too big? Move index on. Entry will be cleared from ramCache below. 1498 bucketEntries[index] = null; 1499 index++; 1500 } catch (CacheFullException cfe) { 1501 // Cache full when we tried to add. Try freeing space and then retrying (don't up index) 1502 if (!freeInProgress && !re.isPrefetch()) { 1503 freeSpace("Full!"); 1504 } else if (re.isPrefetch()) { 1505 bucketEntries[index] = null; 1506 index++; 1507 } else { 1508 Thread.sleep(50); 1509 } 1510 } catch (IOException ioex) { 1511 // Hopefully transient. Retry. checkIOErrorIsTolerated disables cache if problem. 1512 LOG.error("Failed writing to bucket cache", ioex); 1513 checkIOErrorIsTolerated(); 1514 } 1515 } 1516 1517 // Make sure data pages are written on media before we update maps. 1518 try { 1519 ioEngine.sync(); 1520 } catch (IOException ioex) { 1521 LOG.error("Failed syncing IO engine", ioex); 1522 checkIOErrorIsTolerated(); 1523 // Since we failed sync, free the blocks in bucket allocator 1524 for (int i = 0; i < entries.size(); ++i) { 1525 BucketEntry bucketEntry = bucketEntries[i]; 1526 if (bucketEntry != null) { 1527 bucketAllocator.freeBlock(bucketEntry.offset(), bucketEntry.getLength()); 1528 bucketEntries[i] = null; 1529 } 1530 } 1531 } 1532 1533 // Now add to backingMap if successfully added to bucket cache. Remove from ramCache if 1534 // success or error. 1535 for (int i = 0; i < size; ++i) { 1536 BlockCacheKey key = entries.get(i).getKey(); 1537 // Only add if non-null entry. 1538 if (bucketEntries[i] != null) { 1539 putIntoBackingMap(key, bucketEntries[i]); 1540 if (ioEngine.isPersistent()) { 1541 setCacheInconsistent(true); 1542 } 1543 } 1544 // Always remove from ramCache even if we failed adding it to the block cache above. 1545 boolean existed = ramCache.remove(key, re -> { 1546 if (re != null) { 1547 heapSize.add(-1 * re.getData().heapSize()); 1548 } 1549 }); 1550 if (!existed && bucketEntries[i] != null) { 1551 // Block should have already been evicted. Remove it and free space. 1552 final BucketEntry bucketEntry = bucketEntries[i]; 1553 bucketEntry.withWriteLock(offsetLock, () -> { 1554 if (backingMap.remove(key, bucketEntry)) { 1555 blockEvicted(key, bucketEntry, false, false); 1556 } 1557 return null; 1558 }); 1559 } 1560 long used = bucketAllocator.getUsedSize(); 1561 if (!entries.get(i).isPrefetch() && used > acceptableSize()) { 1562 LOG.debug("Calling freeSpace for block: {}", entries.get(i).getKey()); 1563 freeSpace("Used=" + used + " > acceptable=" + acceptableSize()); 1564 } 1565 } 1566 1567 } 1568 1569 /** 1570 * Blocks until elements available in {@code q} then tries to grab as many as possible before 1571 * returning. 1572 * @param receptacle Where to stash the elements taken from queue. We clear before we use it just 1573 * in case. 1574 * @param q The queue to take from. 1575 * @return {@code receptacle} laden with elements taken from the queue or empty if none found. 1576 */ 1577 static List<RAMQueueEntry> getRAMQueueEntries(BlockingQueue<RAMQueueEntry> q, 1578 List<RAMQueueEntry> receptacle) throws InterruptedException { 1579 // Clear sets all entries to null and sets size to 0. We retain allocations. Presume it 1580 // ok even if list grew to accommodate thousands. 1581 receptacle.clear(); 1582 receptacle.add(q.take()); 1583 q.drainTo(receptacle); 1584 return receptacle; 1585 } 1586 1587 /** 1588 * @see #retrieveFromFile(int[]) 1589 */ 1590 @edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "OBL_UNSATISFIED_OBLIGATION", 1591 justification = "false positive, try-with-resources ensures close is called.") 1592 void persistToFile() throws IOException { 1593 persistToFile(entry -> { 1594 }); 1595 } 1596 1597 private void persistToFile(Consumer<Map.Entry<BlockCacheKey, BucketEntry>> entryCopiedAction) 1598 throws IOException { 1599 LOG.debug("Thread {} started persisting bucket cache to file", 1600 Thread.currentThread().getName()); 1601 if (!isCachePersistent()) { 1602 throw new IOException("Attempt to persist non-persistent cache mappings!"); 1603 } 1604 File tempPersistencePath = new File(persistencePath + EnvironmentEdgeManager.currentTime()); 1605 try (FileOutputStream fos = new FileOutputStream(tempPersistencePath, false)) { 1606 LOG.debug("Persist in new chunked persistence format."); 1607 1608 persistChunkedBackingMap(fos, entryCopiedAction); 1609 1610 LOG.debug( 1611 "PersistToFile: after persisting backing map size: {}, fullycachedFiles size: {}," 1612 + " file name: {}", 1613 backingMap.size(), fullyCachedFiles.size(), tempPersistencePath.getName()); 1614 } catch (IOException e) { 1615 LOG.error("Failed to persist bucket cache to file", e); 1616 throw e; 1617 } catch (Throwable e) { 1618 LOG.error("Failed during persist bucket cache to file: ", e); 1619 throw e; 1620 } 1621 LOG.debug("Thread {} finished persisting bucket cache to file, renaming", 1622 Thread.currentThread().getName()); 1623 if (!tempPersistencePath.renameTo(new File(persistencePath))) { 1624 LOG.warn("Failed to commit cache persistent file. We might lose cached blocks if " 1625 + "RS crashes/restarts before we successfully checkpoint again."); 1626 } 1627 } 1628 1629 public boolean isCachePersistent() { 1630 return ioEngine.isPersistent() && persistencePath != null; 1631 } 1632 1633 @Override 1634 public Optional<Map<String, Long>> getRegionCachedInfo() { 1635 return Optional.of(Collections.unmodifiableMap(regionCachedSize)); 1636 } 1637 1638 /** 1639 * @see #persistToFile() 1640 */ 1641 private void retrieveFromFile(int[] bucketSizes) throws IOException { 1642 LOG.info("Started retrieving bucket cache from file"); 1643 File persistenceFile = new File(persistencePath); 1644 if (!persistenceFile.exists()) { 1645 LOG.warn("Persistence file missing! " 1646 + "It's ok if it's first run after enabling persistent cache."); 1647 bucketAllocator = new BucketAllocator(cacheCapacity, bucketSizes, backingMap, realCacheSize); 1648 blockNumber.add(backingMap.size()); 1649 backingMapValidated.set(true); 1650 return; 1651 } 1652 assert !isCacheEnabled(); 1653 1654 try (FileInputStream in = new FileInputStream(persistenceFile)) { 1655 int pblen = ProtobufMagic.lengthOfPBMagic(); 1656 byte[] pbuf = new byte[pblen]; 1657 IOUtils.readFully(in, pbuf, 0, pblen); 1658 1659 if (ProtobufMagic.isPBMagicPrefix(pbuf)) { 1660 LOG.info("Reading old format of persistence."); 1661 // The old non-chunked version of backing map persistence. 1662 BucketCacheProtos.BucketCacheEntry cacheEntry = 1663 BucketCacheProtos.BucketCacheEntry.parseDelimitedFrom(in); 1664 if (cacheEntry == null) { 1665 throw new IOException( 1666 "Failed to parse cache entry from persistence file: " + persistencePath); 1667 } 1668 parsePB(cacheEntry); 1669 } else if (Arrays.equals(pbuf, BucketProtoUtils.PB_MAGIC_V2)) { 1670 // The new persistence format of chunked persistence. 1671 LOG.info("Reading new chunked format of persistence."); 1672 retrieveChunkedBackingMap(in); 1673 } else { 1674 // In 3.0 we have enough flexibility to dump the old cache data. 1675 // TODO: In 2.x line, this might need to be filled in to support reading the old format 1676 throw new IOException( 1677 "Persistence file does not start with protobuf magic number. " + persistencePath); 1678 } 1679 bucketAllocator = new BucketAllocator(cacheCapacity, bucketSizes, backingMap, realCacheSize); 1680 blockNumber.add(backingMap.size()); 1681 LOG.info("Bucket cache retrieved from file successfully with size: {}", backingMap.size()); 1682 } 1683 } 1684 1685 private void updateRegionSizeMapWhileRetrievingFromFile() { 1686 // Update the regionCachedSize with the region size while restarting the region server 1687 if (LOG.isDebugEnabled()) { 1688 LOG.debug("Updating region size map after retrieving cached file list"); 1689 dumpPrefetchList(); 1690 } 1691 regionCachedSize.clear(); 1692 backingMap.forEach((k, v) -> updateRegionCachedSize(k, v.getLength())); 1693 } 1694 1695 private void dumpPrefetchList() { 1696 for (Map.Entry<String, Pair<String, Long>> outerEntry : fullyCachedFiles.entrySet()) { 1697 LOG.debug("Cached File Entry:<{},<{},{}>>", outerEntry.getKey(), 1698 outerEntry.getValue().getFirst(), outerEntry.getValue().getSecond()); 1699 } 1700 } 1701 1702 private void verifyCapacityAndClasses(long capacitySize, String ioclass, String mapclass) 1703 throws IOException { 1704 if (capacitySize != cacheCapacity) { 1705 throw new IOException("Mismatched cache capacity:" + StringUtils.byteDesc(capacitySize) 1706 + ", expected: " + StringUtils.byteDesc(cacheCapacity)); 1707 } 1708 if (!ioEngine.getClass().getName().equals(ioclass)) { 1709 throw new IOException("Class name for IO engine mismatch: " + ioclass + ", expected:" 1710 + ioEngine.getClass().getName()); 1711 } 1712 if (!backingMap.getClass().getName().equals(mapclass)) { 1713 throw new IOException("Class name for cache map mismatch: " + mapclass + ", expected:" 1714 + backingMap.getClass().getName()); 1715 } 1716 } 1717 1718 private void verifyFileIntegrity(BucketCacheProtos.BucketCacheEntry proto) { 1719 try { 1720 if (proto.hasChecksum()) { 1721 ((PersistentIOEngine) ioEngine).verifyFileIntegrity(proto.getChecksum().toByteArray(), 1722 algorithm); 1723 } 1724 backingMapValidated.set(true); 1725 } catch (IOException e) { 1726 LOG.warn("Checksum for cache file failed. " 1727 + "We need to validate each cache key in the backing map. " 1728 + "This may take some time, so we'll do it in a background thread,"); 1729 1730 Runnable cacheValidator = () -> { 1731 while (bucketAllocator == null) { 1732 try { 1733 Thread.sleep(50); 1734 } catch (InterruptedException ex) { 1735 throw new RuntimeException(ex); 1736 } 1737 } 1738 long startTime = EnvironmentEdgeManager.currentTime(); 1739 int totalKeysOriginally = backingMap.size(); 1740 for (Map.Entry<BlockCacheKey, BucketEntry> keyEntry : backingMap.entrySet()) { 1741 try { 1742 ((FileIOEngine) ioEngine).checkCacheTime(keyEntry.getValue()); 1743 } catch (IOException e1) { 1744 LOG.debug("Check for key {} failed. Evicting.", keyEntry.getKey()); 1745 evictBlock(keyEntry.getKey()); 1746 fileNotFullyCached(keyEntry.getKey(), keyEntry.getValue()); 1747 } 1748 } 1749 backingMapValidated.set(true); 1750 LOG.info("Finished validating {} keys in the backing map. Recovered: {}. This took {}ms.", 1751 totalKeysOriginally, backingMap.size(), 1752 (EnvironmentEdgeManager.currentTime() - startTime)); 1753 }; 1754 Thread t = new Thread(cacheValidator); 1755 t.setDaemon(true); 1756 t.start(); 1757 } 1758 } 1759 1760 private void updateCacheIndex(BucketCacheProtos.BackingMap chunk, 1761 java.util.Map<java.lang.Integer, java.lang.String> deserializer) throws IOException { 1762 Pair<ConcurrentHashMap<BlockCacheKey, BucketEntry>, NavigableSet<BlockCacheKey>> pair2 = 1763 BucketProtoUtils.fromPB(deserializer, chunk, this::createRecycler); 1764 pair2.getFirst().forEach((k, v) -> { 1765 backingMap.put(k, v); 1766 updateRegionCachedSize(k, v.getLength()); 1767 }); 1768 blocksByHFile.addAll(pair2.getSecond()); 1769 } 1770 1771 private void parsePB(BucketCacheProtos.BucketCacheEntry proto) throws IOException { 1772 Pair<ConcurrentHashMap<BlockCacheKey, BucketEntry>, NavigableSet<BlockCacheKey>> pair = 1773 BucketProtoUtils.fromPB(proto.getDeserializersMap(), proto.getBackingMap(), 1774 this::createRecycler); 1775 backingMap = pair.getFirst(); 1776 blocksByHFile = pair.getSecond(); 1777 fullyCachedFiles.clear(); 1778 fullyCachedFiles.putAll(BucketProtoUtils.fromPB(proto.getCachedFilesMap())); 1779 1780 LOG.info("After retrieval Backing map size: {}, fullyCachedFiles size: {}", backingMap.size(), 1781 fullyCachedFiles.size()); 1782 1783 verifyFileIntegrity(proto); 1784 updateRegionSizeMapWhileRetrievingFromFile(); 1785 verifyCapacityAndClasses(proto.getCacheCapacity(), proto.getIoClass(), proto.getMapClass()); 1786 } 1787 1788 private void persistChunkedBackingMap(FileOutputStream fos, 1789 Consumer<Map.Entry<BlockCacheKey, BucketEntry>> entryCopiedAction) throws IOException { 1790 LOG.debug( 1791 "persistToFile: before persisting backing map size: {}, " 1792 + "fullycachedFiles size: {}, chunkSize: {}", 1793 backingMap.size(), fullyCachedFiles.size(), persistenceChunkSize); 1794 1795 BucketProtoUtils.serializeAsPB(this, fos, persistenceChunkSize, entryCopiedAction); 1796 1797 LOG.debug( 1798 "persistToFile: after persisting backing map size: {}, " + "fullycachedFiles size: {}", 1799 backingMap.size(), fullyCachedFiles.size()); 1800 } 1801 1802 private void retrieveChunkedBackingMap(FileInputStream in) throws IOException { 1803 // Read the first chunk that has all the details. 1804 BucketCacheProtos.BucketCacheEntry cacheEntry = 1805 BucketCacheProtos.BucketCacheEntry.parseDelimitedFrom(in); 1806 1807 // HBASE-29857: Handle case where persistence file is empty. 1808 // parseDelimitedFrom() returns null when there's no data to read. 1809 // Note: Corrupted files would throw InvalidProtocolBufferException (subclass of IOException). 1810 if (cacheEntry == null) { 1811 throw new IOException("Failed to read cache entry from persistence file (file is empty)"); 1812 } 1813 1814 fullyCachedFiles.clear(); 1815 fullyCachedFiles.putAll(BucketProtoUtils.fromPB(cacheEntry.getCachedFilesMap())); 1816 1817 backingMap.clear(); 1818 blocksByHFile.clear(); 1819 regionCachedSize.clear(); 1820 1821 // Read the backing map entries in batches. 1822 int numChunks = 0; 1823 while (in.available() > 0) { 1824 updateCacheIndex(BucketCacheProtos.BackingMap.parseDelimitedFrom(in), 1825 cacheEntry.getDeserializersMap()); 1826 numChunks++; 1827 } 1828 1829 LOG.info("Retrieved {} of chunks with blockCount = {}.", numChunks, backingMap.size()); 1830 verifyFileIntegrity(cacheEntry); 1831 verifyCapacityAndClasses(cacheEntry.getCacheCapacity(), cacheEntry.getIoClass(), 1832 cacheEntry.getMapClass()); 1833 } 1834 1835 /** 1836 * Check whether we tolerate IO error this time. If the duration of IOEngine throwing errors 1837 * exceeds ioErrorsDurationTimeTolerated, we will disable the cache 1838 */ 1839 private void checkIOErrorIsTolerated() { 1840 long now = EnvironmentEdgeManager.currentTime(); 1841 // Do a single read to a local variable to avoid timing issue - HBASE-24454 1842 long ioErrorStartTimeTmp = this.ioErrorStartTime; 1843 if (ioErrorStartTimeTmp > 0) { 1844 if (isCacheEnabled() && (now - ioErrorStartTimeTmp) > this.ioErrorsTolerationDuration) { 1845 LOG.error("IO errors duration time has exceeded " + ioErrorsTolerationDuration 1846 + "ms, disabling cache, please check your IOEngine"); 1847 disableCache(); 1848 } 1849 } else { 1850 this.ioErrorStartTime = now; 1851 } 1852 } 1853 1854 /** 1855 * Used to shut down the cache -or- turn it off in the case of something broken. 1856 * @return whether explicit shutdown should wait for cleanup 1857 */ 1858 private synchronized boolean disableCache() { 1859 if (cacheState == CacheState.DISABLED) { 1860 return false; 1861 } 1862 boolean waitForCleanup = cacheState == CacheState.ENABLED && isCachePersistent(); 1863 LOG.info("Disabling cache"); 1864 cacheState = CacheState.DISABLED; 1865 this.scheduleThreadPool.shutdown(); 1866 for (WriterThread writerThread : writerThreads) { 1867 writerThread.interrupt(); 1868 } 1869 // Closing the IO engine helps unblock an in-flight writer before the cleanup thread joins it. 1870 // FileIOEngine can reopen a channel, so cleanup closes the engine again after writers stop. 1871 ioEngine.shutdown(); 1872 if (cacheStats.getMetricsRollerScheduler() != null) { 1873 cacheStats.getMetricsRollerScheduler().shutdownNow(); 1874 } 1875 cacheCleanupThread = Threads.setDaemonThreadRunning(new Thread(this::cleanupCache), 1876 "BucketCacheCleanup-" + System.identityHashCode(this), Threads.LOGGING_EXCEPTION_HANDLER); 1877 return waitForCleanup; 1878 } 1879 1880 private void cleanupCache() { 1881 try { 1882 Threads.shutdown(persistenceRetrieverThread); 1883 for (WriterThread writerThread : writerThreads) { 1884 Threads.shutdown(writerThread); 1885 } 1886 for (BlockingQueue<RAMQueueEntry> writerQueue : writerQueues) { 1887 writerQueue.clear(); 1888 } 1889 ramCache.clear(); 1890 if (cachePersister != null) { 1891 LOG.info("Shutting down cache persister thread."); 1892 cachePersister.shutdown(); 1893 Threads.shutdown(cachePersister); 1894 } 1895 if (isCachePersistent()) { 1896 try { 1897 // The serializer already visits every entry. Release owner references in the same pass. 1898 persistToFile(this::cleanupBackingMapEntry); 1899 } catch (IOException ex) { 1900 LOG.error("Unable to persist data on exit: " + ex.toString(), ex); 1901 } 1902 } 1903 } finally { 1904 try { 1905 cleanupCacheIndex(); 1906 } finally { 1907 ioEngine.shutdown(); 1908 } 1909 } 1910 } 1911 1912 private void cleanupCacheIndex() { 1913 // A successful persistent cleanup emptied the map during serialization. Avoid creating a 1914 // second iterator over a large ConcurrentHashMap in that case. 1915 if (!backingMap.isEmpty()) { 1916 for (Map.Entry<BlockCacheKey, BucketEntry> entry : backingMap.entrySet()) { 1917 cleanupBackingMapEntry(entry); 1918 } 1919 } 1920 blocksByHFile.clear(); 1921 fullyCachedFiles.clear(); 1922 regionCachedSize.clear(); 1923 } 1924 1925 private void cleanupBackingMapEntry(Map.Entry<BlockCacheKey, BucketEntry> entry) { 1926 BlockCacheKey cacheKey = entry.getKey(); 1927 BucketEntry bucketEntry = entry.getValue(); 1928 bucketEntry.withWriteLock(offsetLock, () -> { 1929 if (backingMap.remove(cacheKey, bucketEntry)) { 1930 bucketEntry.markAsEvicted(); 1931 } 1932 return null; 1933 }); 1934 } 1935 1936 private void waitForCacheCleanup() throws InterruptedException { 1937 Thread cleanupThread = cacheCleanupThread; 1938 if (cleanupThread == null || cleanupThread == Thread.currentThread()) { 1939 return; 1940 } 1941 cleanupThread.join(); 1942 } 1943 1944 @Override 1945 public void shutdown() { 1946 if (disableCache()) { 1947 try { 1948 waitForCacheCleanup(); 1949 } catch (InterruptedException e) { 1950 Thread.currentThread().interrupt(); 1951 LOG.warn("Interrupted while waiting for bucket cache cleanup", e); 1952 } 1953 } 1954 LOG.info("Shutdown bucket cache: IO persistent=" + ioEngine.isPersistent() + "; path to write=" 1955 + persistencePath); 1956 } 1957 1958 /** 1959 * Needed mostly for UTs that might run in the same VM and create different BucketCache instances 1960 * on different UT methods. 1961 */ 1962 @Override 1963 protected void finalize() { 1964 if (cachePersister != null && !cachePersister.isInterrupted()) { 1965 cachePersister.interrupt(); 1966 } 1967 } 1968 1969 @Override 1970 public CacheStats getStats() { 1971 return cacheStats; 1972 } 1973 1974 public BucketAllocator getAllocator() { 1975 return this.bucketAllocator; 1976 } 1977 1978 @Override 1979 public long heapSize() { 1980 return this.heapSize.sum(); 1981 } 1982 1983 @Override 1984 public long size() { 1985 return this.realCacheSize.sum(); 1986 } 1987 1988 @Override 1989 public long getCurrentDataSize() { 1990 return size(); 1991 } 1992 1993 @Override 1994 public long getFreeSize() { 1995 if (!isCacheInitialized("BucketCache:getFreeSize")) { 1996 return 0; 1997 } 1998 return this.bucketAllocator.getFreeSize(); 1999 } 2000 2001 @Override 2002 public long getBlockCount() { 2003 return this.blockNumber.sum(); 2004 } 2005 2006 @Override 2007 public long getDataBlockCount() { 2008 return getBlockCount(); 2009 } 2010 2011 @Override 2012 public long getCurrentSize() { 2013 if (!isCacheInitialized("BucketCache::getCurrentSize")) { 2014 return 0; 2015 } 2016 return this.bucketAllocator.getUsedSize(); 2017 } 2018 2019 protected String getAlgorithm() { 2020 return algorithm; 2021 } 2022 2023 /** 2024 * Evicts all blocks for a specific HFile. 2025 * <p> 2026 * This is used for evict-on-close to remove all blocks of a specific HFile. 2027 * @return the number of blocks evicted 2028 */ 2029 @Override 2030 public int evictBlocksByHfileName(String hfileName) { 2031 return evictBlocksRangeByHfileName(hfileName, 0, Long.MAX_VALUE); 2032 } 2033 2034 @Override 2035 public int evictBlocksRangeByHfileName(String hfileName, long initOffset, long endOffset) { 2036 Set<BlockCacheKey> keySet = getAllCacheKeysForFile(hfileName, initOffset, endOffset); 2037 // We need to make sure whether we are evicting all blocks for this given file. In case of 2038 // split references, we might be evicting just half of the blocks 2039 LOG.debug("found {} blocks for file {}, starting offset: {}, end offset: {}", keySet.size(), 2040 hfileName, initOffset, endOffset); 2041 return evictBlockSet(keySet); 2042 } 2043 2044 private int evictBlockSet(Set<BlockCacheKey> keySet) { 2045 int numEvicted = 0; 2046 for (BlockCacheKey key : keySet) { 2047 if (evictBlock(key)) { 2048 ++numEvicted; 2049 } 2050 } 2051 return numEvicted; 2052 } 2053 2054 private Set<BlockCacheKey> getAllCacheKeysForFile(String hfileName, long init, long end) { 2055 Set<BlockCacheKey> cacheKeys = new HashSet<>(); 2056 // At this moment, Some Bucket Entries may be in the WriterThread queue, and not yet put into 2057 // the backingMap. So, when executing this method, we should check both the RAMCache and 2058 // backingMap to ensure all CacheKeys are obtained. 2059 // For more details, please refer to HBASE-29862. 2060 Set<BlockCacheKey> ramCacheKeySet = ramCache.getRamBlockCacheKeysForHFile(hfileName); 2061 for (BlockCacheKey key : ramCacheKeySet) { 2062 if (key.getOffset() >= init && key.getOffset() <= end) { 2063 cacheKeys.add(key); 2064 } 2065 } 2066 2067 // These keys are just for comparison and are short lived, so we need only file name and offset 2068 cacheKeys.addAll(blocksByHFile.subSet(new BlockCacheKey(hfileName, init), true, 2069 new BlockCacheKey(hfileName, end), true)); 2070 return cacheKeys; 2071 } 2072 2073 /** 2074 * Used to group bucket entries into priority buckets. There will be a BucketEntryGroup for each 2075 * priority (single, multi, memory). Once bucketed, the eviction algorithm takes the appropriate 2076 * number of elements out of each according to configuration parameters and their relative sizes. 2077 */ 2078 private class BucketEntryGroup { 2079 2080 private CachedEntryQueue queue; 2081 private long totalSize = 0; 2082 private long bucketSize; 2083 2084 public BucketEntryGroup(long bytesToFree, long blockSize, long bucketSize) { 2085 this.bucketSize = bucketSize; 2086 queue = new CachedEntryQueue(bytesToFree, blockSize); 2087 totalSize = 0; 2088 } 2089 2090 public void add(Map.Entry<BlockCacheKey, BucketEntry> block) { 2091 totalSize += block.getValue().getLength(); 2092 queue.add(block); 2093 } 2094 2095 public long free(long toFree) { 2096 Map.Entry<BlockCacheKey, BucketEntry> entry; 2097 long freedBytes = 0; 2098 // TODO avoid a cycling siutation. We find no block which is not in use and so no way to free 2099 // What to do then? Caching attempt fail? Need some changes in cacheBlock API? 2100 while ((entry = queue.pollLast()) != null) { 2101 BlockCacheKey blockCacheKey = entry.getKey(); 2102 BucketEntry be = entry.getValue(); 2103 if (evictBucketEntryIfNoRpcReferenced(blockCacheKey, be)) { 2104 freedBytes += be.getLength(); 2105 } 2106 if (freedBytes >= toFree) { 2107 return freedBytes; 2108 } 2109 } 2110 return freedBytes; 2111 } 2112 2113 public long overflow() { 2114 return totalSize - bucketSize; 2115 } 2116 2117 public long totalSize() { 2118 return totalSize; 2119 } 2120 } 2121 2122 /** 2123 * Block Entry stored in the memory with key,data and so on 2124 */ 2125 static class RAMQueueEntry { 2126 private final BlockCacheKey key; 2127 private final Cacheable data; 2128 private long accessCounter; 2129 private boolean inMemory; 2130 private boolean isCachePersistent; 2131 2132 private boolean isPrefetch; 2133 2134 RAMQueueEntry(BlockCacheKey bck, Cacheable data, long accessCounter, boolean inMemory, 2135 boolean isCachePersistent, boolean isPrefetch) { 2136 this.key = bck; 2137 this.data = data; 2138 this.accessCounter = accessCounter; 2139 this.inMemory = inMemory; 2140 this.isCachePersistent = isCachePersistent; 2141 this.isPrefetch = isPrefetch; 2142 } 2143 2144 public Cacheable getData() { 2145 return data; 2146 } 2147 2148 public BlockCacheKey getKey() { 2149 return key; 2150 } 2151 2152 public boolean isPrefetch() { 2153 return isPrefetch; 2154 } 2155 2156 public void access(long accessCounter) { 2157 this.accessCounter = accessCounter; 2158 } 2159 2160 private ByteBuffAllocator getByteBuffAllocator() { 2161 if (data instanceof HFileBlock) { 2162 return ((HFileBlock) data).getByteBuffAllocator(); 2163 } 2164 return ByteBuffAllocator.HEAP; 2165 } 2166 2167 public BucketEntry writeToCache(final IOEngine ioEngine, final BucketAllocator alloc, 2168 final LongAdder realCacheSize, Function<BucketEntry, Recycler> createRecycler, 2169 ByteBuffer metaBuff, final Long acceptableSize) throws IOException { 2170 int len = data.getSerializedLength(); 2171 // This cacheable thing can't be serialized 2172 if (len == 0) { 2173 return null; 2174 } 2175 if (isCachePersistent && data instanceof HFileBlock) { 2176 len += Long.BYTES; // we need to record the cache time for consistency check in case of 2177 // recovery 2178 } 2179 long offset = alloc.allocateBlock(len); 2180 // In the case of prefetch, we want to avoid freeSpace runs when the cache is full. 2181 // this makes the cache allocation more predictable, and is particularly important 2182 // when persistent cache is enabled, as it won't trigger evictions of the recovered blocks, 2183 // which are likely the most accessed and relevant blocks in the cache. 2184 if (isPrefetch() && alloc.getUsedSize() > acceptableSize) { 2185 alloc.freeBlock(offset, len); 2186 return null; 2187 } 2188 boolean succ = false; 2189 BucketEntry bucketEntry = null; 2190 try { 2191 int diskSizeWithHeader = (data instanceof HFileBlock) 2192 ? ((HFileBlock) data).getOnDiskSizeWithHeader() 2193 : data.getSerializedLength(); 2194 bucketEntry = new BucketEntry(offset, len, diskSizeWithHeader, accessCounter, inMemory, 2195 createRecycler, getByteBuffAllocator()); 2196 bucketEntry.setDeserializerReference(data.getDeserializer()); 2197 if (data instanceof HFileBlock) { 2198 // If an instance of HFileBlock, save on some allocations. 2199 HFileBlock block = (HFileBlock) data; 2200 ByteBuff sliceBuf = block.getBufferReadOnly(); 2201 block.getMetaData(metaBuff); 2202 // adds the cache time prior to the block and metadata part 2203 if (isCachePersistent) { 2204 ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES); 2205 buffer.putLong(bucketEntry.getCachedTime()); 2206 buffer.rewind(); 2207 ioEngine.write(buffer, offset); 2208 ioEngine.write(sliceBuf, (offset + Long.BYTES)); 2209 } else { 2210 ioEngine.write(sliceBuf, offset); 2211 } 2212 ioEngine.write(metaBuff, offset + len - metaBuff.limit()); 2213 } else { 2214 // Only used for testing. 2215 ByteBuffer bb = ByteBuffer.allocate(len); 2216 data.serialize(bb, true); 2217 ioEngine.write(bb, offset); 2218 } 2219 succ = true; 2220 } finally { 2221 if (!succ) { 2222 alloc.freeBlock(offset, len); 2223 } 2224 } 2225 realCacheSize.add(len); 2226 return bucketEntry; 2227 } 2228 } 2229 2230 /** 2231 * Only used in test 2232 */ 2233 void stopWriterThreads() throws InterruptedException { 2234 for (WriterThread writerThread : writerThreads) { 2235 writerThread.disableWriter(); 2236 writerThread.interrupt(); 2237 writerThread.join(); 2238 } 2239 } 2240 2241 @Override 2242 public Iterator<CachedBlock> iterator() { 2243 // Don't bother with ramcache since stuff is in here only a little while. 2244 final Iterator<Map.Entry<BlockCacheKey, BucketEntry>> i = this.backingMap.entrySet().iterator(); 2245 return new Iterator<CachedBlock>() { 2246 private final long now = System.nanoTime(); 2247 2248 @Override 2249 public boolean hasNext() { 2250 return i.hasNext(); 2251 } 2252 2253 @Override 2254 public CachedBlock next() { 2255 final Map.Entry<BlockCacheKey, BucketEntry> e = i.next(); 2256 return new CachedBlock() { 2257 @Override 2258 public String toString() { 2259 return BlockCacheUtil.toString(this, now); 2260 } 2261 2262 @Override 2263 public BlockPriority getBlockPriority() { 2264 return e.getValue().getPriority(); 2265 } 2266 2267 @Override 2268 public BlockType getBlockType() { 2269 // Not held by BucketEntry. Could add it if wanted on BucketEntry creation. 2270 return null; 2271 } 2272 2273 @Override 2274 public long getOffset() { 2275 return e.getKey().getOffset(); 2276 } 2277 2278 @Override 2279 public long getSize() { 2280 return e.getValue().getLength(); 2281 } 2282 2283 @Override 2284 public long getCachedTime() { 2285 return e.getValue().getCachedTime(); 2286 } 2287 2288 @Override 2289 public String getFilename() { 2290 return e.getKey().getHfileName(); 2291 } 2292 2293 @Override 2294 public int compareTo(CachedBlock other) { 2295 int diff = this.getFilename().compareTo(other.getFilename()); 2296 if (diff != 0) return diff; 2297 2298 diff = Long.compare(this.getOffset(), other.getOffset()); 2299 if (diff != 0) return diff; 2300 if (other.getCachedTime() < 0 || this.getCachedTime() < 0) { 2301 throw new IllegalStateException( 2302 "" + this.getCachedTime() + ", " + other.getCachedTime()); 2303 } 2304 return Long.compare(other.getCachedTime(), this.getCachedTime()); 2305 } 2306 2307 @Override 2308 public int hashCode() { 2309 return e.getKey().hashCode(); 2310 } 2311 2312 @Override 2313 public boolean equals(Object obj) { 2314 if (obj instanceof CachedBlock) { 2315 CachedBlock cb = (CachedBlock) obj; 2316 return compareTo(cb) == 0; 2317 } else { 2318 return false; 2319 } 2320 } 2321 }; 2322 } 2323 2324 @Override 2325 public void remove() { 2326 throw new UnsupportedOperationException(); 2327 } 2328 }; 2329 } 2330 2331 @Override 2332 public BlockCache[] getBlockCaches() { 2333 return null; 2334 } 2335 2336 public int getRpcRefCount(BlockCacheKey cacheKey) { 2337 BucketEntry bucketEntry = backingMap.get(cacheKey); 2338 if (bucketEntry != null) { 2339 return bucketEntry.refCnt() - (bucketEntry.markedAsEvicted.get() ? 0 : 1); 2340 } 2341 return 0; 2342 } 2343 2344 float getAcceptableFactor() { 2345 return acceptableFactor; 2346 } 2347 2348 float getMinFactor() { 2349 return minFactor; 2350 } 2351 2352 float getExtraFreeFactor() { 2353 return extraFreeFactor; 2354 } 2355 2356 float getSingleFactor() { 2357 return singleFactor; 2358 } 2359 2360 float getMultiFactor() { 2361 return multiFactor; 2362 } 2363 2364 float getMemoryFactor() { 2365 return memoryFactor; 2366 } 2367 2368 long getQueueAdditionWaitTime() { 2369 return queueAdditionWaitTime; 2370 } 2371 2372 long getPersistenceChunkSize() { 2373 return persistenceChunkSize; 2374 } 2375 2376 long getBucketcachePersistInterval() { 2377 return bucketcachePersistInterval; 2378 } 2379 2380 public String getPersistencePath() { 2381 return persistencePath; 2382 } 2383 2384 /** 2385 * Wrapped the delegate ConcurrentMap with maintaining its block's reference count. 2386 */ 2387 static class RAMCache { 2388 /** 2389 * Defined the map as {@link ConcurrentHashMap} explicitly here, because in 2390 * {@link RAMCache#get(BlockCacheKey)} and 2391 * {@link RAMCache#putIfAbsent(BlockCacheKey, BucketCache.RAMQueueEntry)} , we need to guarantee 2392 * the atomicity of map#computeIfPresent(key, func) and map#putIfAbsent(key, func). Besides, the 2393 * func method can execute exactly once only when the key is present(or absent) and under the 2394 * lock context. Otherwise, the reference count of block will be messed up. Notice that the 2395 * {@link java.util.concurrent.ConcurrentSkipListMap} can not guarantee that. 2396 */ 2397 final ConcurrentHashMap<BlockCacheKey, RAMQueueEntry> delegate = new ConcurrentHashMap<>(); 2398 2399 public boolean containsKey(BlockCacheKey key) { 2400 return delegate.containsKey(key); 2401 } 2402 2403 public RAMQueueEntry get(BlockCacheKey key) { 2404 return delegate.computeIfPresent(key, (k, re) -> { 2405 // It'll be referenced by RPC, so retain atomically here. if the get and retain is not 2406 // atomic, another thread may remove and release the block, when retaining in this thread we 2407 // may retain a block with refCnt=0 which is disallowed. (see HBASE-22422) 2408 re.getData().retain(); 2409 return re; 2410 }); 2411 } 2412 2413 /** 2414 * Return the previous associated value, or null if absent. It has the same meaning as 2415 * {@link ConcurrentMap#putIfAbsent(Object, Object)} 2416 */ 2417 public RAMQueueEntry putIfAbsent(BlockCacheKey key, RAMQueueEntry entry) { 2418 AtomicBoolean absent = new AtomicBoolean(false); 2419 RAMQueueEntry re = delegate.computeIfAbsent(key, k -> { 2420 // The RAMCache reference to this entry, so reference count should be increment. 2421 entry.getData().retain(); 2422 absent.set(true); 2423 return entry; 2424 }); 2425 return absent.get() ? null : re; 2426 } 2427 2428 public boolean remove(BlockCacheKey key) { 2429 return remove(key, re -> { 2430 }); 2431 } 2432 2433 /** 2434 * Defined an {@link Consumer} here, because once the removed entry release its reference count, 2435 * then it's ByteBuffers may be recycled and accessing it outside this method will be thrown an 2436 * exception. the consumer will access entry to remove before release its reference count. 2437 * Notice, don't change its reference count in the {@link Consumer} 2438 */ 2439 public boolean remove(BlockCacheKey key, Consumer<RAMQueueEntry> action) { 2440 RAMQueueEntry previous = delegate.remove(key); 2441 action.accept(previous); 2442 if (previous != null) { 2443 previous.getData().release(); 2444 } 2445 return previous != null; 2446 } 2447 2448 public boolean isEmpty() { 2449 return delegate.isEmpty(); 2450 } 2451 2452 public void clear() { 2453 Iterator<Map.Entry<BlockCacheKey, RAMQueueEntry>> it = delegate.entrySet().iterator(); 2454 while (it.hasNext()) { 2455 RAMQueueEntry re = it.next().getValue(); 2456 it.remove(); 2457 re.getData().release(); 2458 } 2459 } 2460 2461 public boolean hasBlocksForFile(String fileName) { 2462 return delegate.keySet().stream().filter(key -> key.getHfileName().equals(fileName)) 2463 .findFirst().isPresent(); 2464 } 2465 2466 public Set<BlockCacheKey> getRamBlockCacheKeysForHFile(String fileName) { 2467 Set<BlockCacheKey> ramCacheKeySet = new HashSet<>(); 2468 for (BlockCacheKey blockCacheKey : delegate.keySet()) { 2469 if (blockCacheKey.getHfileName().equals(fileName)) { 2470 ramCacheKeySet.add(blockCacheKey); 2471 } 2472 } 2473 return ramCacheKeySet; 2474 } 2475 } 2476 2477 public Map<BlockCacheKey, BucketEntry> getBackingMap() { 2478 return backingMap; 2479 } 2480 2481 public AtomicBoolean getBackingMapValidated() { 2482 return backingMapValidated; 2483 } 2484 2485 @Override 2486 public Optional<Map<String, Pair<String, Long>>> getFullyCachedFiles() { 2487 return Optional.of(fullyCachedFiles); 2488 } 2489 2490 public static Optional<BucketCache> getBucketCacheFromCacheConfig(CacheConfig cacheConf) { 2491 if (cacheConf.getBlockCache().isPresent()) { 2492 BlockCache bc = cacheConf.getBlockCache().get(); 2493 if (bc instanceof CombinedBlockCache) { 2494 BlockCache l2 = ((CombinedBlockCache) bc).getSecondLevelCache(); 2495 if (l2 instanceof BucketCache) { 2496 return Optional.of((BucketCache) l2); 2497 } 2498 } else if (bc instanceof BucketCache) { 2499 return Optional.of((BucketCache) bc); 2500 } 2501 } 2502 return Optional.empty(); 2503 } 2504 2505 private int countBlocksForFile(Path fileName, List<ReentrantReadWriteLock> locks) { 2506 LOG.debug("iterating over {} entries in the backing map", backingMap.size()); 2507 Set<BlockCacheKey> result = getAllCacheKeysForFile(fileName.getName(), 0, Long.MAX_VALUE); 2508 if (result.isEmpty() && StoreFileInfo.isReference(fileName)) { 2509 result = getAllCacheKeysForFile( 2510 StoreFileInfo.getReferredToRegionAndFile(fileName.getName()).getSecond(), 0, 2511 Long.MAX_VALUE); 2512 } 2513 int count = 0; 2514 for (BlockCacheKey entry : result) { 2515 LOG.debug("found block for file {} in the backing map. Acquiring read lock for offset {}", 2516 fileName.getName(), entry.getOffset()); 2517 ReentrantReadWriteLock lock = offsetLock.getLock(entry.getOffset()); 2518 lock.readLock().lock(); 2519 locks.add(lock); 2520 if (backingMap.containsKey(entry) && entry.getBlockType().isData()) { 2521 count++; 2522 } 2523 } 2524 return count; 2525 } 2526 2527 private void releaseAllLocks(List<ReentrantReadWriteLock> locks) { 2528 for (ReentrantReadWriteLock lock : locks) { 2529 lock.readLock().unlock(); 2530 } 2531 } 2532 2533 @Override 2534 public void notifyFileCachingCompleted(Path fileName, int totalBlockCount, int dataBlockCount, 2535 long size) { 2536 // block eviction may be happening in the background as prefetch runs, 2537 // so we need to count all blocks for this file in the backing map under 2538 // a read lock for the block offset 2539 final List<ReentrantReadWriteLock> locks = new ArrayList<>(); 2540 LOG.debug("Notifying caching completed for file {}, with total blocks {}, and data blocks {}", 2541 fileName, totalBlockCount, dataBlockCount); 2542 try { 2543 boolean lastTry = false; 2544 for (;;) { 2545 int count = countBlocksForFile(fileName, locks); 2546 // BucketCache would only have data blocks 2547 if (dataBlockCount == count) { 2548 LOG.debug("File {} has now been fully cached.", fileName); 2549 fileCacheCompleted(fileName, size); 2550 break; 2551 } 2552 if (lastTry) { 2553 LOG.info( 2554 "The total block count was {}. We found only {} data blocks cached from " 2555 + "a total of {} data blocks for file {}, " 2556 + "but no blocks pending caching. Maybe cache is full or evictions " 2557 + "happened concurrently to cache prefetch.", 2558 totalBlockCount, count, dataBlockCount, fileName); 2559 break; 2560 } 2561 if (ramCache.hasBlocksForFile(fileName.getName())) { 2562 releaseAllLocks(locks); 2563 locks.clear(); 2564 LOG.debug("There are still blocks pending caching for file {}. Will sleep 100ms " 2565 + "and try the verification again.", fileName); 2566 Thread.sleep(100); 2567 } else { 2568 // there are no pending blocks, so count for the last time, if we still can not get enough 2569 // data blocks, quit 2570 LOG.debug("There are no blocks pending cache for file {}. Will try the verification " 2571 + "for the last time."); 2572 lastTry = true; 2573 } 2574 } 2575 } catch (InterruptedException e) { 2576 throw new RuntimeException(e); 2577 } finally { 2578 releaseAllLocks(locks); 2579 } 2580 } 2581 2582 @Override 2583 public Optional<Boolean> blockFitsIntoTheCache(HFileBlock block) { 2584 if (!isCacheInitialized("blockFitsIntoTheCache")) { 2585 return Optional.of(false); 2586 } 2587 2588 long currentUsed = bucketAllocator.getUsedSize(); 2589 boolean result = (currentUsed + block.getOnDiskSizeWithHeader()) < acceptableSize(); 2590 return Optional.of(result); 2591 } 2592 2593 @Override 2594 public Optional<Boolean> shouldCacheFile(HFileInfo hFileInfo, Configuration conf) { 2595 String fileName = hFileInfo.getHFileContext().getHFileName(); 2596 DataTieringManager dataTieringManager = DataTieringManager.getInstance(); 2597 if (dataTieringManager != null && !dataTieringManager.isHotData(hFileInfo, conf)) { 2598 LOG.debug("Custom tiering is enabled for file: '{}' and it is not hot data", fileName); 2599 // If custom tiering has been just enabled for a file that was cached, we now need 2600 // to evict it. 2601 Set<BlockCacheKey> keySet = 2602 getAllCacheKeysForFile(hFileInfo.getHFileContext().getHFileName(), 0, Long.MAX_VALUE); 2603 int evictedBlocks = evictBlockSet(keySet); 2604 if (evictedBlocks > 0) { 2605 LOG.debug( 2606 "Evicted {} blocks for file {} as it is now considered cold by DataTieringManager", 2607 evictedBlocks, fileName); 2608 } 2609 return Optional.of(false); 2610 } 2611 // if we don't have the file in fullyCachedFiles, we should cache it 2612 return Optional.of(!fullyCachedFiles.containsKey(fileName)); 2613 } 2614 2615 @Override 2616 public Optional<Boolean> shouldCacheBlock(BlockCacheKey key, long maxTimestamp, 2617 Configuration conf) { 2618 DataTieringManager dataTieringManager = DataTieringManager.getInstance(); 2619 if (dataTieringManager != null && !dataTieringManager.isHotData(maxTimestamp, conf)) { 2620 LOG.debug("Data tiering is enabled for file: '{}' and it is not hot data", 2621 key.getHfileName()); 2622 return Optional.of(false); 2623 } 2624 return Optional.of(true); 2625 } 2626 2627 @Override 2628 public Optional<Boolean> isAlreadyCached(BlockCacheKey key) { 2629 boolean foundKey = backingMap.containsKey(key); 2630 // if there's no entry for the key itself, we need to check if this key is for a reference, 2631 // and if so, look for a block from the referenced file using this getBlockForReference method. 2632 return Optional.of(foundKey ? true : getBlockForReference(key) != null); 2633 } 2634 2635 @Override 2636 public Optional<Integer> getBlockSize(BlockCacheKey key) { 2637 BucketEntry entry = backingMap.get(key); 2638 if (entry == null) { 2639 // the key might be for a reference tha we had found the block from the referenced file in 2640 // the cache when we first tried to cache it. 2641 entry = getBlockForReference(key); 2642 return entry == null ? Optional.empty() : Optional.of(entry.getOnDiskSizeWithHeader()); 2643 } else { 2644 return Optional.of(entry.getOnDiskSizeWithHeader()); 2645 } 2646 2647 } 2648 2649 boolean isCacheInitialized(String api) { 2650 if (cacheState == CacheState.INITIALIZING) { 2651 LOG.warn("Bucket initialisation pending at {}", api); 2652 return false; 2653 } 2654 return true; 2655 } 2656 2657 @Override 2658 public boolean waitForCacheInitialization(long timeout) { 2659 while (cacheState == CacheState.INITIALIZING) { 2660 if (timeout <= 0) { 2661 break; 2662 } 2663 try { 2664 Thread.sleep(100); 2665 } catch (InterruptedException e) { 2666 LOG.warn("Interrupted while waiting for cache initialization", e); 2667 Thread.currentThread().interrupt(); 2668 break; 2669 } 2670 timeout -= 100; 2671 } 2672 return isCacheEnabled(); 2673 } 2674}