|
92 | 92 | import org.apache.hadoop.hbase.util.IdReadWriteLockWithObjectPool; |
93 | 93 | import org.apache.hadoop.hbase.util.IdReadWriteLockWithObjectPool.ReferenceType; |
94 | 94 | import org.apache.hadoop.hbase.util.Pair; |
| 95 | +import org.apache.hadoop.hbase.util.Threads; |
95 | 96 | import org.apache.hadoop.util.StringUtils; |
96 | 97 | import org.apache.yetus.audience.InterfaceAudience; |
97 | 98 | import org.slf4j.Logger; |
@@ -160,6 +161,7 @@ public class BucketCache implements BlockCache, HeapSize { |
160 | 161 |
|
161 | 162 | final static int DEFAULT_WRITER_THREADS = 3; |
162 | 163 | final static int DEFAULT_WRITER_QUEUE_ITEMS = 64; |
| 164 | + private static final long WRITER_QUEUE_POLL_INTERVAL_MS = 100; |
163 | 165 |
|
164 | 166 | final static long DEFAULT_BACKING_MAP_PERSISTENCE_CHUNK_SIZE = 10000; |
165 | 167 |
|
@@ -206,6 +208,16 @@ protected enum CacheState { |
206 | 208 | */ |
207 | 209 | private volatile CacheState cacheState; |
208 | 210 |
|
| 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 | + |
209 | 221 | /** |
210 | 222 | * A list of writer queues. We have a queue per {@link WriterThread} we have running. In other |
211 | 223 | * words, the work adding blocks to the BucketCache is divided up amongst the running |
@@ -589,47 +601,76 @@ protected boolean shouldReplaceExistingCacheBlock(BlockCacheKey cacheKey, Cachea |
589 | 601 |
|
590 | 602 | protected void cacheBlockWithWaitInternal(BlockCacheKey cacheKey, Cacheable cachedItem, |
591 | 603 | 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(); |
612 | 631 | } |
| 632 | + |
613 | 633 | int queueNum = (cacheKey.hashCode() & 0x7FFFFFFF) % writerQueues.size(); |
614 | 634 | BlockingQueue<RAMQueueEntry> bq = writerQueues.get(queueNum); |
615 | 635 | 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(); |
625 | 641 | } |
626 | 642 | if (!successfulAddition) { |
627 | 643 | LOG.debug("Failed to insert block {} into the cache writers queue", cacheKey); |
628 | 644 | ramCache.remove(cacheKey); |
629 | 645 | 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))); |
633 | 674 | } |
634 | 675 | } |
635 | 676 |
|
@@ -690,6 +731,7 @@ public Cacheable getBlock(BlockCacheKey key, boolean caching, boolean repeat, |
690 | 731 | if (bucketEntry != null) { |
691 | 732 | long start = System.nanoTime(); |
692 | 733 | ReentrantReadWriteLock lock = offsetLock.getLock(bucketEntry.offset()); |
| 734 | + boolean failedBucketEntryRead = false; |
693 | 735 | try { |
694 | 736 | lock.readLock().lock(); |
695 | 737 | // 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, |
722 | 764 | // When using file io engine persistent cache, |
723 | 765 | // the cache map state might differ from the actual cache. If we reach this block, |
724 | 766 | // we should remove the cache key entry from the backing map |
725 | | - backingMap.remove(key); |
726 | | - fileNotFullyCached(key, bucketEntry); |
| 767 | + failedBucketEntryRead = true; |
727 | 768 | LOG.debug("Failed to fetch block for cache key: {}.", key, hioex); |
728 | 769 | } catch (IOException ioex) { |
729 | 770 | LOG.error("Failed reading block " + key + " from bucket cache", ioex); |
730 | 771 | checkIOErrorIsTolerated(); |
731 | 772 | } finally { |
732 | 773 | lock.readLock().unlock(); |
733 | 774 | } |
| 775 | + if (failedBucketEntryRead) { |
| 776 | + removeBucketEntry(key, bucketEntry, true, false); |
| 777 | + } |
734 | 778 | } |
735 | 779 | if (!repeat && updateCacheMetrics) { |
736 | 780 | cacheStats.miss(caching, key.isPrimary(), key.getBlockType()); |
@@ -849,18 +893,24 @@ private boolean doEvictBlock(BlockCacheKey cacheKey, BucketEntry bucketEntry, |
849 | 893 | } |
850 | 894 | return existedInRamCache; |
851 | 895 | } 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); |
861 | 898 | } |
862 | 899 | } |
863 | 900 |
|
| 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 | + |
864 | 914 | /** |
865 | 915 | * <pre> |
866 | 916 | * Create the {@link Recycler} for {@link BucketEntry#refCnt},which would be used as |
@@ -1317,13 +1367,15 @@ public void run() { |
1317 | 1367 | while (isCacheEnabled() && writerEnabled) { |
1318 | 1368 | try { |
1319 | 1369 | try { |
1320 | | - // Blocks |
1321 | | - entries = getRAMQueueEntries(inputQueue, entries); |
| 1370 | + entries = pollRAMQueueEntries(inputQueue, entries); |
1322 | 1371 | } catch (InterruptedException ie) { |
1323 | 1372 | if (!isCacheEnabled() || !writerEnabled) { |
1324 | 1373 | break; |
1325 | 1374 | } |
1326 | 1375 | } |
| 1376 | + if (entries.isEmpty()) { |
| 1377 | + continue; |
| 1378 | + } |
1327 | 1379 | doDrain(entries, metaBuff); |
1328 | 1380 | } catch (Exception ioe) { |
1329 | 1381 | LOG.error("WriterThread encountered error", ioe); |
@@ -1545,6 +1597,17 @@ static List<RAMQueueEntry> getRAMQueueEntries(BlockingQueue<RAMQueueEntry> q, |
1545 | 1597 | return receptacle; |
1546 | 1598 | } |
1547 | 1599 |
|
| 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 | + |
1548 | 1611 | /** |
1549 | 1612 | * @see #retrieveFromFile(int[]) |
1550 | 1613 | */ |
@@ -1809,59 +1872,107 @@ private void checkIOErrorIsTolerated() { |
1809 | 1872 | * Used to shut down the cache -or- turn it off in the case of something broken. |
1810 | 1873 | */ |
1811 | 1874 | 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(); |
1831 | 1891 | } |
1832 | 1892 | } |
1833 | 1893 |
|
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()) { |
1846 | 1903 | 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 | 1904 | persistToFile(); |
1856 | 1905 | } catch (IOException ex) { |
1857 | 1906 | 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); |
1860 | 1907 | } |
1861 | 1908 | } |
| 1909 | + } finally { |
| 1910 | + try { |
| 1911 | + releaseBackingMapReferences(); |
| 1912 | + } finally { |
| 1913 | + ioEngine.shutdown(); |
| 1914 | + } |
1862 | 1915 | } |
1863 | 1916 | } |
1864 | 1917 |
|
| 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 | + |
1865 | 1976 | /** |
1866 | 1977 | * Needed mostly for UTs that might run in the same VM and create different BucketCache instances |
1867 | 1978 | * on different UT methods. |
|
0 commit comments