Skip to content

Commit f07a9fe

Browse files
authored
HBASE-30285 BucketCache shutdown/disable does not release backingMap-owned BucketEntry references (#8469)
Signed-off-by: Wellington Chevreuil <wchevreuil@apache.org>
1 parent 28808cc commit f07a9fe

5 files changed

Lines changed: 560 additions & 85 deletions

File tree

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

Lines changed: 157 additions & 64 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;
@@ -206,6 +207,12 @@ protected enum CacheState {
206207
*/
207208
private volatile CacheState cacheState;
208209

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+
209216
/**
210217
* A list of writer queues. We have a queue per {@link WriterThread} we have running. In other
211218
* words, the work adding blocks to the BucketCache is divided up amongst the running
@@ -416,12 +423,17 @@ private void startPersistenceRetriever(int[] bucketSizes, long capacity) {
416423
LOG.error("Exception during Bucket Allocation", allocatorException);
417424
}
418425
} finally {
419-
this.cacheState = CacheState.ENABLED;
420-
startWriterThreads();
426+
synchronized (BucketCache.this) {
427+
if (cacheState == CacheState.INITIALIZING) {
428+
cacheState = CacheState.ENABLED;
429+
startWriterThreads();
430+
}
431+
}
421432
}
422433
};
423-
Thread t = new Thread(persistentCacheRetriever);
424-
t.start();
434+
persistenceRetrieverThread = new Thread(persistentCacheRetriever,
435+
"BucketCachePersistenceRetriever-" + System.identityHashCode(this));
436+
persistenceRetrieverThread.start();
425437
}
426438

427439
private void sanityCheckConfigs() {
@@ -643,21 +655,24 @@ protected void cacheBlockWithWaitInternal(BlockCacheKey cacheKey, Cacheable cach
643655
* the passed key doesn't relate to a reference.
644656
*/
645657
public BucketEntry getBlockForReference(BlockCacheKey key) {
646-
BucketEntry foundEntry = null;
647-
String referredFileName = null;
648-
if (StoreFileInfo.isReference(key.getHfileName())) {
649-
referredFileName = StoreFileInfo.getReferredToRegionAndFile(key.getHfileName()).getSecond();
650-
}
651-
if (referredFileName != null) {
652-
// Since we just need this key for a lookup, it's enough to use only name and offset
653-
BlockCacheKey convertedCacheKey = new BlockCacheKey(referredFileName, key.getOffset());
654-
foundEntry = backingMap.get(convertedCacheKey);
658+
BlockCacheKey referredKey = getBlockKeyForReference(key);
659+
BucketEntry foundEntry = referredKey != null ? backingMap.get(referredKey) : null;
660+
if (referredKey != null) {
655661
LOG.debug("Got a link/ref: {}. Related cacheKey: {}. Found entry: {}", key.getHfileName(),
656-
convertedCacheKey, foundEntry);
662+
referredKey, foundEntry);
657663
}
658664
return foundEntry;
659665
}
660666

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+
661676
/**
662677
* Get the buffer of the block with the specified key.
663678
* @param key block's cache key
@@ -681,23 +696,28 @@ public Cacheable getBlock(BlockCacheKey key, boolean caching, boolean repeat,
681696
re.access(accessCount.incrementAndGet());
682697
return re.getData();
683698
}
684-
BucketEntry bucketEntry = backingMap.get(key);
699+
BlockCacheKey backingMapLookupKey = key;
700+
BucketEntry bucketEntry = backingMap.get(backingMapLookupKey);
685701
LOG.debug("bucket entry for key {}: {}", key,
686702
bucketEntry == null ? null : bucketEntry.offset());
687703
if (bucketEntry == null) {
688-
bucketEntry = getBlockForReference(key);
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+
}
689710
}
690711
if (bucketEntry != null) {
691712
long start = System.nanoTime();
692713
ReentrantReadWriteLock lock = offsetLock.getLock(bucketEntry.offset());
714+
boolean inconsistentEntry = false;
693715
try {
694716
lock.readLock().lock();
695717
// We can not read here even if backingMap does contain the given key because its offset
696718
// maybe changed. If we lock BlockCacheKey instead of offset, then we can only check
697719
// existence here.
698-
if (
699-
bucketEntry.equals(backingMap.get(key)) || bucketEntry.equals(getBlockForReference(key))
700-
) {
720+
if (bucketEntry.equals(backingMap.get(backingMapLookupKey))) {
701721
// Read the block from IOEngine based on the bucketEntry's offset and length, NOTICE: the
702722
// block will use the refCnt of bucketEntry, which means if two HFileBlock mapping to
703723
// the same BucketEntry, then all of the three will share the same refCnt.
@@ -719,25 +739,39 @@ public Cacheable getBlock(BlockCacheKey key, boolean caching, boolean repeat,
719739
return cachedBlock;
720740
}
721741
} catch (HBaseIOException hioex) {
722-
// When using file io engine persistent cache,
723-
// the cache map state might differ from the actual cache. If we reach this block,
724-
// we should remove the cache key entry from the backing map
725-
backingMap.remove(key);
726-
fileNotFullyCached(key, bucketEntry);
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;
727745
LOG.debug("Failed to fetch block for cache key: {}.", key, hioex);
728746
} catch (IOException ioex) {
729747
LOG.error("Failed reading block " + key + " from bucket cache", ioex);
730748
checkIOErrorIsTolerated();
731749
} finally {
732750
lock.readLock().unlock();
733751
}
752+
if (inconsistentEntry) {
753+
evictInconsistentEntry(backingMapLookupKey, bucketEntry);
754+
}
734755
}
735756
if (!repeat && updateCacheMetrics) {
736757
cacheStats.miss(caching, key.isPrimary(), key.getBlockType());
737758
}
738759
return null;
739760
}
740761

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+
741775
/**
742776
* This method is invoked after the bucketEntry is removed from {@link BucketCache#backingMap}
743777
*/
@@ -1350,14 +1384,19 @@ public void run() {
13501384
*/
13511385
protected void putIntoBackingMap(BlockCacheKey key, BucketEntry bucketEntry) {
13521386
BucketEntry previousEntry = backingMap.put(key, bucketEntry);
1353-
blocksByHFile.add(key);
13541387
updateRegionCachedSize(key, bucketEntry.getLength());
13551388
if (previousEntry != null && previousEntry != bucketEntry) {
13561389
previousEntry.withWriteLock(offsetLock, () -> {
13571390
blockEvicted(key, previousEntry, false, false);
13581391
return null;
13591392
});
13601393
}
1394+
bucketEntry.withWriteLock(offsetLock, () -> {
1395+
if (backingMap.get(key) == bucketEntry) {
1396+
blocksByHFile.add(key);
1397+
}
1398+
return null;
1399+
});
13611400
}
13621401

13631402
/**
@@ -1551,6 +1590,12 @@ static List<RAMQueueEntry> getRAMQueueEntries(BlockingQueue<RAMQueueEntry> q,
15511590
@edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "OBL_UNSATISFIED_OBLIGATION",
15521591
justification = "false positive, try-with-resources ensures close is called.")
15531592
void persistToFile() throws IOException {
1593+
persistToFile(entry -> {
1594+
});
1595+
}
1596+
1597+
private void persistToFile(Consumer<Map.Entry<BlockCacheKey, BucketEntry>> entryCopiedAction)
1598+
throws IOException {
15541599
LOG.debug("Thread {} started persisting bucket cache to file",
15551600
Thread.currentThread().getName());
15561601
if (!isCachePersistent()) {
@@ -1560,7 +1605,7 @@ void persistToFile() throws IOException {
15601605
try (FileOutputStream fos = new FileOutputStream(tempPersistencePath, false)) {
15611606
LOG.debug("Persist in new chunked persistence format.");
15621607

1563-
persistChunkedBackingMap(fos);
1608+
persistChunkedBackingMap(fos, entryCopiedAction);
15641609

15651610
LOG.debug(
15661611
"PersistToFile: after persisting backing map size: {}, fullycachedFiles size: {},"
@@ -1740,13 +1785,14 @@ private void parsePB(BucketCacheProtos.BucketCacheEntry proto) throws IOExceptio
17401785
verifyCapacityAndClasses(proto.getCacheCapacity(), proto.getIoClass(), proto.getMapClass());
17411786
}
17421787

1743-
private void persistChunkedBackingMap(FileOutputStream fos) throws IOException {
1788+
private void persistChunkedBackingMap(FileOutputStream fos,
1789+
Consumer<Map.Entry<BlockCacheKey, BucketEntry>> entryCopiedAction) throws IOException {
17441790
LOG.debug(
17451791
"persistToFile: before persisting backing map size: {}, "
17461792
+ "fullycachedFiles size: {}, chunkSize: {}",
17471793
backingMap.size(), fullyCachedFiles.size(), persistenceChunkSize);
17481794

1749-
BucketProtoUtils.serializeAsPB(this, fos, persistenceChunkSize);
1795+
BucketProtoUtils.serializeAsPB(this, fos, persistenceChunkSize, entryCopiedAction);
17501796

17511797
LOG.debug(
17521798
"persistToFile: after persisting backing map size: {}, " + "fullycachedFiles size: {}",
@@ -1807,59 +1853,106 @@ private void checkIOErrorIsTolerated() {
18071853

18081854
/**
18091855
* 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
18101857
*/
1811-
private void disableCache() {
1812-
if (!isCacheEnabled()) {
1813-
return;
1858+
private synchronized boolean disableCache() {
1859+
if (cacheState == CacheState.DISABLED) {
1860+
return false;
18141861
}
1862+
boolean waitForCleanup = cacheState == CacheState.ENABLED && isCachePersistent();
18151863
LOG.info("Disabling cache");
18161864
cacheState = CacheState.DISABLED;
1817-
ioEngine.shutdown();
18181865
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();
1866+
for (WriterThread writerThread : writerThreads) {
1867+
writerThread.interrupt();
18281868
}
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();
18291872
if (cacheStats.getMetricsRollerScheduler() != null) {
18301873
cacheStats.getMetricsRollerScheduler().shutdownNow();
18311874
}
1875+
cacheCleanupThread = Threads.setDaemonThreadRunning(new Thread(this::cleanupCache),
1876+
"BucketCacheCleanup-" + System.identityHashCode(this), Threads.LOGGING_EXCEPTION_HANDLER);
1877+
return waitForCleanup;
18321878
}
18331879

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) {
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()) {
18461896
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-
}
1855-
persistToFile();
1897+
// The serializer already visits every entry. Release owner references in the same pass.
1898+
persistToFile(this::cleanupBackingMapEntry);
18561899
} catch (IOException ex) {
18571900
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);
18601901
}
18611902
}
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+
}
18621953
}
1954+
LOG.info("Shutdown bucket cache: IO persistent=" + ioEngine.isPersistent() + "; path to write="
1955+
+ persistencePath);
18631956
}
18641957

18651958
/**

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.NavigableSet;
2626
import java.util.concurrent.ConcurrentHashMap;
2727
import java.util.concurrent.ConcurrentSkipListSet;
28+
import java.util.function.Consumer;
2829
import java.util.function.Function;
2930
import org.apache.hadoop.hbase.io.ByteBuffAllocator;
3031
import org.apache.hadoop.hbase.io.ByteBuffAllocator.Recycler;
@@ -64,6 +65,12 @@ static BucketCacheProtos.BucketCacheEntry toPB(BucketCache cache,
6465

6566
public static void serializeAsPB(BucketCache cache, FileOutputStream fos, long chunkSize)
6667
throws IOException {
68+
serializeAsPB(cache, fos, chunkSize, entry -> {
69+
});
70+
}
71+
72+
static void serializeAsPB(BucketCache cache, FileOutputStream fos, long chunkSize,
73+
Consumer<Map.Entry<BlockCacheKey, BucketEntry>> entryCopiedAction) throws IOException {
6774
// Write the new version of magic number.
6875
fos.write(PB_MAGIC_V2);
6976

@@ -79,6 +86,7 @@ public static void serializeAsPB(BucketCache cache, FileOutputStream fos, long c
7986
for (Map.Entry<BlockCacheKey, BucketEntry> entry : cache.backingMap.entrySet()) {
8087
blockCount++;
8188
addEntryToBuilder(entry, entryBuilder, builder);
89+
entryCopiedAction.accept(entry);
8290
if (blockCount % chunkSize == 0) {
8391
builder.build().writeDelimitedTo(fos);
8492
builder.clear();

0 commit comments

Comments
 (0)