Skip to content

Commit 195f2a9

Browse files
committed
HBASE-30285 BucketCache shutdown/disable does not release backingMap-owned BucketEntry references
1 parent f9a41c7 commit 195f2a9

4 files changed

Lines changed: 471 additions & 87 deletions

File tree

hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/bucket/BucketCache.java

Lines changed: 197 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
import org.apache.hadoop.hbase.util.IdReadWriteLockWithObjectPool;
9393
import org.apache.hadoop.hbase.util.IdReadWriteLockWithObjectPool.ReferenceType;
9494
import org.apache.hadoop.hbase.util.Pair;
95+
import org.apache.hadoop.hbase.util.Threads;
9596
import org.apache.hadoop.util.StringUtils;
9697
import org.apache.yetus.audience.InterfaceAudience;
9798
import org.slf4j.Logger;
@@ -160,6 +161,7 @@ public class BucketCache implements BlockCache, HeapSize {
160161

161162
final static int DEFAULT_WRITER_THREADS = 3;
162163
final static int DEFAULT_WRITER_QUEUE_ITEMS = 64;
164+
private static final long WRITER_QUEUE_POLL_INTERVAL_MS = 100;
163165

164166
final static long DEFAULT_BACKING_MAP_PERSISTENCE_CHUNK_SIZE = 10000;
165167

@@ -206,6 +208,16 @@ protected enum CacheState {
206208
*/
207209
private volatile CacheState cacheState;
208210

211+
/**
212+
* Prevent publishing new RAM cache entries while the cache is transitioning to disabled. The read
213+
* lock is only held for the RAM cache and writer queue publication path, so it is never nested
214+
* with an offset lock.
215+
*/
216+
private final ReentrantReadWriteLock cacheLifecycleLock = new ReentrantReadWriteLock();
217+
218+
/** The single cleanup thread shared by disable and explicit shutdown calls. */
219+
private volatile Thread cacheCleanupThread;
220+
209221
/**
210222
* A list of writer queues. We have a queue per {@link WriterThread} we have running. In other
211223
* words, the work adding blocks to the BucketCache is divided up amongst the running
@@ -589,47 +601,76 @@ protected boolean shouldReplaceExistingCacheBlock(BlockCacheKey cacheKey, Cachea
589601

590602
protected void cacheBlockWithWaitInternal(BlockCacheKey cacheKey, Cacheable cachedItem,
591603
boolean inMemory, boolean wait) {
592-
if (!isCacheEnabled()) {
593-
return;
594-
}
595-
if (cacheKey.getBlockType() == null && cachedItem.getBlockType() != null) {
596-
cacheKey.setBlockType(cachedItem.getBlockType());
597-
}
598-
LOG.debug("Caching key={}, item={}, key heap size={}", cacheKey, cachedItem,
599-
cacheKey.heapSize());
600-
// Stuff the entry into the RAM cache so it can get drained to the persistent store
601-
RAMQueueEntry re = new RAMQueueEntry(cacheKey, cachedItem, accessCount.incrementAndGet(),
602-
inMemory, isCachePersistent() && ioEngine instanceof FileIOEngine, wait);
603-
/**
604-
* Don't use ramCache.put(cacheKey, re) here. because there may be a existing entry with same
605-
* key in ramCache, the heap size of bucket cache need to update if replacing entry from
606-
* ramCache. But WriterThread will also remove entry from ramCache and update heap size, if
607-
* using ramCache.put(), It's possible that the removed entry in WriterThread is not the correct
608-
* one, then the heap size will mess up (HBASE-20789)
609-
*/
610-
if (ramCache.putIfAbsent(cacheKey, re) != null) {
611-
return;
604+
Lock lifecycleReadLock = cacheLifecycleLock.readLock();
605+
RAMQueueEntry re;
606+
lifecycleReadLock.lock();
607+
try {
608+
if (!isCacheEnabled()) {
609+
return;
610+
}
611+
if (cacheKey.getBlockType() == null && cachedItem.getBlockType() != null) {
612+
cacheKey.setBlockType(cachedItem.getBlockType());
613+
}
614+
LOG.debug("Caching key={}, item={}, key heap size={}", cacheKey, cachedItem,
615+
cacheKey.heapSize());
616+
// Stuff the entry into the RAM cache so it can get drained to the persistent store
617+
re = new RAMQueueEntry(cacheKey, cachedItem, accessCount.incrementAndGet(), inMemory,
618+
isCachePersistent() && ioEngine instanceof FileIOEngine, wait);
619+
/**
620+
* Don't use ramCache.put(cacheKey, re) here. because there may be a existing entry with same
621+
* key in ramCache, the heap size of bucket cache need to update if replacing entry from
622+
* ramCache. But WriterThread will also remove entry from ramCache and update heap size, if
623+
* using ramCache.put(), It's possible that the removed entry in WriterThread is not the
624+
* correct one, then the heap size will mess up (HBASE-20789)
625+
*/
626+
if (ramCache.putIfAbsent(cacheKey, re) != null) {
627+
return;
628+
}
629+
} finally {
630+
lifecycleReadLock.unlock();
612631
}
632+
613633
int queueNum = (cacheKey.hashCode() & 0x7FFFFFFF) % writerQueues.size();
614634
BlockingQueue<RAMQueueEntry> bq = writerQueues.get(queueNum);
615635
boolean successfulAddition = false;
616-
if (wait) {
617-
try {
618-
successfulAddition = bq.offer(re, queueAdditionWaitTime, TimeUnit.MILLISECONDS);
619-
} catch (InterruptedException e) {
620-
LOG.error("Thread interrupted: ", e);
621-
Thread.currentThread().interrupt();
622-
}
623-
} else {
624-
successfulAddition = bq.offer(re);
636+
try {
637+
successfulAddition = offerToWriterQueue(bq, re, cachedItem, wait);
638+
} catch (InterruptedException e) {
639+
LOG.error("Thread interrupted: ", e);
640+
Thread.currentThread().interrupt();
625641
}
626642
if (!successfulAddition) {
627643
LOG.debug("Failed to insert block {} into the cache writers queue", cacheKey);
628644
ramCache.remove(cacheKey);
629645
cacheStats.failInsert();
630-
} else {
631-
this.blockNumber.increment();
632-
this.heapSize.add(cachedItem.heapSize());
646+
}
647+
}
648+
649+
private boolean offerToWriterQueue(BlockingQueue<RAMQueueEntry> queue, RAMQueueEntry entry,
650+
Cacheable cachedItem, boolean wait) throws InterruptedException {
651+
long timeoutNanos = TimeUnit.MILLISECONDS.toNanos(queueAdditionWaitTime);
652+
long startTime = System.nanoTime();
653+
while (true) {
654+
Lock lifecycleReadLock = cacheLifecycleLock.readLock();
655+
lifecycleReadLock.lock();
656+
try {
657+
if (!isCacheEnabled()) {
658+
return false;
659+
}
660+
if (queue.offer(entry)) {
661+
blockNumber.increment();
662+
heapSize.add(cachedItem.heapSize());
663+
return true;
664+
}
665+
} finally {
666+
lifecycleReadLock.unlock();
667+
}
668+
if (!wait || System.nanoTime() - startTime >= timeoutNanos) {
669+
return false;
670+
}
671+
long remainingNanos = timeoutNanos - (System.nanoTime() - startTime);
672+
TimeUnit.NANOSECONDS.sleep(
673+
Math.min(remainingNanos, TimeUnit.MILLISECONDS.toNanos(WRITER_QUEUE_POLL_INTERVAL_MS)));
633674
}
634675
}
635676

@@ -690,6 +731,7 @@ public Cacheable getBlock(BlockCacheKey key, boolean caching, boolean repeat,
690731
if (bucketEntry != null) {
691732
long start = System.nanoTime();
692733
ReentrantReadWriteLock lock = offsetLock.getLock(bucketEntry.offset());
734+
boolean failedBucketEntryRead = false;
693735
try {
694736
lock.readLock().lock();
695737
// We can not read here even if backingMap does contain the given key because its offset
@@ -722,15 +764,17 @@ public Cacheable getBlock(BlockCacheKey key, boolean caching, boolean repeat,
722764
// When using file io engine persistent cache,
723765
// the cache map state might differ from the actual cache. If we reach this block,
724766
// we should remove the cache key entry from the backing map
725-
backingMap.remove(key);
726-
fileNotFullyCached(key, bucketEntry);
767+
failedBucketEntryRead = true;
727768
LOG.debug("Failed to fetch block for cache key: {}.", key, hioex);
728769
} catch (IOException ioex) {
729770
LOG.error("Failed reading block " + key + " from bucket cache", ioex);
730771
checkIOErrorIsTolerated();
731772
} finally {
732773
lock.readLock().unlock();
733774
}
775+
if (failedBucketEntryRead) {
776+
removeBucketEntry(key, bucketEntry, true, false);
777+
}
734778
}
735779
if (!repeat && updateCacheMetrics) {
736780
cacheStats.miss(caching, key.isPrimary(), key.getBlockType());
@@ -849,18 +893,24 @@ private boolean doEvictBlock(BlockCacheKey cacheKey, BucketEntry bucketEntry,
849893
}
850894
return existedInRamCache;
851895
} else {
852-
return bucketEntryToUse.withWriteLock(offsetLock, () -> {
853-
if (backingMap.remove(cacheKey, bucketEntryToUse)) {
854-
LOG.debug("removed key {} from back map with offset lock {} in the evict process",
855-
cacheKey, bucketEntryToUse.offset());
856-
blockEvicted(cacheKey, bucketEntryToUse, !existedInRamCache, evictedByEvictionProcess);
857-
return true;
858-
}
859-
return false;
860-
});
896+
return removeBucketEntry(cacheKey, bucketEntryToUse, !existedInRamCache,
897+
evictedByEvictionProcess);
861898
}
862899
}
863900

901+
private boolean removeBucketEntry(BlockCacheKey cacheKey, BucketEntry bucketEntry,
902+
boolean decrementBlockNumber, boolean evictedByEvictionProcess) {
903+
return bucketEntry.withWriteLock(offsetLock, () -> {
904+
if (backingMap.remove(cacheKey, bucketEntry)) {
905+
LOG.debug("removed key {} from back map with offset lock {} in the evict process", cacheKey,
906+
bucketEntry.offset());
907+
blockEvicted(cacheKey, bucketEntry, decrementBlockNumber, evictedByEvictionProcess);
908+
return true;
909+
}
910+
return false;
911+
});
912+
}
913+
864914
/**
865915
* <pre>
866916
* Create the {@link Recycler} for {@link BucketEntry#refCnt},which would be used as
@@ -1317,13 +1367,15 @@ public void run() {
13171367
while (isCacheEnabled() && writerEnabled) {
13181368
try {
13191369
try {
1320-
// Blocks
1321-
entries = getRAMQueueEntries(inputQueue, entries);
1370+
entries = pollRAMQueueEntries(inputQueue, entries);
13221371
} catch (InterruptedException ie) {
13231372
if (!isCacheEnabled() || !writerEnabled) {
13241373
break;
13251374
}
13261375
}
1376+
if (entries.isEmpty()) {
1377+
continue;
1378+
}
13271379
doDrain(entries, metaBuff);
13281380
} catch (Exception ioe) {
13291381
LOG.error("WriterThread encountered error", ioe);
@@ -1545,6 +1597,17 @@ static List<RAMQueueEntry> getRAMQueueEntries(BlockingQueue<RAMQueueEntry> q,
15451597
return receptacle;
15461598
}
15471599

1600+
private static List<RAMQueueEntry> pollRAMQueueEntries(BlockingQueue<RAMQueueEntry> q,
1601+
List<RAMQueueEntry> receptacle) throws InterruptedException {
1602+
receptacle.clear();
1603+
RAMQueueEntry entry = q.poll(WRITER_QUEUE_POLL_INTERVAL_MS, TimeUnit.MILLISECONDS);
1604+
if (entry != null) {
1605+
receptacle.add(entry);
1606+
q.drainTo(receptacle);
1607+
}
1608+
return receptacle;
1609+
}
1610+
15481611
/**
15491612
* @see #retrieveFromFile(int[])
15501613
*/
@@ -1809,59 +1872,107 @@ private void checkIOErrorIsTolerated() {
18091872
* Used to shut down the cache -or- turn it off in the case of something broken.
18101873
*/
18111874
private void disableCache() {
1812-
if (!isCacheEnabled()) {
1813-
return;
1814-
}
1815-
LOG.info("Disabling cache");
1816-
cacheState = CacheState.DISABLED;
1817-
ioEngine.shutdown();
1818-
this.scheduleThreadPool.shutdown();
1819-
for (int i = 0; i < writerThreads.length; ++i)
1820-
writerThreads[i].interrupt();
1821-
this.ramCache.clear();
1822-
if (!ioEngine.isPersistent() || persistencePath == null) {
1823-
// If persistent ioengine and a path, we will serialize out the backingMap.
1824-
this.backingMap.clear();
1825-
this.blocksByHFile.clear();
1826-
this.fullyCachedFiles.clear();
1827-
this.regionCachedSize.clear();
1828-
}
1829-
if (cacheStats.getMetricsRollerScheduler() != null) {
1830-
cacheStats.getMetricsRollerScheduler().shutdownNow();
1875+
Lock lifecycleWriteLock = cacheLifecycleLock.writeLock();
1876+
lifecycleWriteLock.lock();
1877+
try {
1878+
if (!isCacheEnabled()) {
1879+
return;
1880+
}
1881+
LOG.info("Disabling cache");
1882+
cacheState = CacheState.DISABLED;
1883+
this.scheduleThreadPool.shutdown();
1884+
if (cacheStats.getMetricsRollerScheduler() != null) {
1885+
cacheStats.getMetricsRollerScheduler().shutdownNow();
1886+
}
1887+
cacheCleanupThread = Threads.setDaemonThreadRunning(new Thread(this::cleanupCache),
1888+
"BucketCacheCleanup-" + System.identityHashCode(this), Threads.LOGGING_EXCEPTION_HANDLER);
1889+
} finally {
1890+
lifecycleWriteLock.unlock();
18311891
}
18321892
}
18331893

1834-
private void join() throws InterruptedException {
1835-
for (int i = 0; i < writerThreads.length; ++i)
1836-
writerThreads[i].join();
1837-
}
1838-
1839-
@Override
1840-
public void shutdown() {
1841-
if (isCacheEnabled()) {
1842-
disableCache();
1843-
LOG.info("Shutdown bucket cache: IO persistent=" + ioEngine.isPersistent()
1844-
+ "; path to write=" + persistencePath);
1845-
if (ioEngine.isPersistent() && persistencePath != null) {
1894+
private void cleanupCache() {
1895+
try {
1896+
joinWriterThreads();
1897+
ramCache.clear();
1898+
for (BlockingQueue<RAMQueueEntry> writerQueue : writerQueues) {
1899+
writerQueue.clear();
1900+
}
1901+
stopCachePersister();
1902+
if (isCachePersistent()) {
18461903
try {
1847-
join();
1848-
if (cachePersister != null) {
1849-
LOG.info("Shutting down cache persister thread.");
1850-
cachePersister.shutdown();
1851-
while (cachePersister.isAlive()) {
1852-
Thread.sleep(10);
1853-
}
1854-
}
18551904
persistToFile();
18561905
} catch (IOException ex) {
18571906
LOG.error("Unable to persist data on exit: " + ex.toString(), ex);
1858-
} catch (InterruptedException e) {
1859-
LOG.warn("Failed to persist data on exit", e);
18601907
}
18611908
}
1909+
} finally {
1910+
try {
1911+
releaseBackingMapReferences();
1912+
} finally {
1913+
ioEngine.shutdown();
1914+
}
18621915
}
18631916
}
18641917

1918+
private void joinWriterThreads() {
1919+
for (WriterThread writerThread : writerThreads) {
1920+
Threads.shutdown(writerThread);
1921+
}
1922+
}
1923+
1924+
private void stopCachePersister() {
1925+
if (cachePersister != null) {
1926+
LOG.info("Shutting down cache persister thread.");
1927+
cachePersister.shutdown();
1928+
Threads.shutdown(cachePersister);
1929+
}
1930+
}
1931+
1932+
private void releaseBackingMapReferences() {
1933+
for (Map.Entry<BlockCacheKey, BucketEntry> entry : backingMap.entrySet()) {
1934+
BlockCacheKey cacheKey = entry.getKey();
1935+
BucketEntry bucketEntry = entry.getValue();
1936+
bucketEntry.withWriteLock(offsetLock, () -> {
1937+
if (backingMap.remove(cacheKey, bucketEntry)) {
1938+
bucketEntry.markAsEvicted();
1939+
}
1940+
return null;
1941+
});
1942+
}
1943+
blocksByHFile.clear();
1944+
fullyCachedFiles.clear();
1945+
regionCachedSize.clear();
1946+
blockNumber.reset();
1947+
heapSize.reset();
1948+
}
1949+
1950+
private void waitForCacheCleanup() {
1951+
Thread cleanupThread = cacheCleanupThread;
1952+
if (cleanupThread == null || cleanupThread == Thread.currentThread()) {
1953+
return;
1954+
}
1955+
boolean interrupted = false;
1956+
while (cleanupThread.isAlive()) {
1957+
try {
1958+
cleanupThread.join();
1959+
} catch (InterruptedException e) {
1960+
interrupted = true;
1961+
}
1962+
}
1963+
if (interrupted) {
1964+
Thread.currentThread().interrupt();
1965+
}
1966+
}
1967+
1968+
@Override
1969+
public void shutdown() {
1970+
disableCache();
1971+
waitForCacheCleanup();
1972+
LOG.info("Shutdown bucket cache: IO persistent=" + ioEngine.isPersistent() + "; path to write="
1973+
+ persistencePath);
1974+
}
1975+
18651976
/**
18661977
* Needed mostly for UTs that might run in the same VM and create different BucketCache instances
18671978
* on different UT methods.

0 commit comments

Comments
 (0)