You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by al...@apache.org on 2020/01/24 13:17:43 UTC

[ignite] branch ignite-2.8 updated: IGNITE-12530 Pages list cache limit added to prevent IgniteOOME on checkpoint - Fixes #7245.

This is an automated email from the ASF dual-hosted git repository.

alexpl pushed a commit to branch ignite-2.8
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/ignite-2.8 by this push:
     new 3d38b55  IGNITE-12530 Pages list cache limit added to prevent IgniteOOME on checkpoint - Fixes #7245.
3d38b55 is described below

commit 3d38b550705e8b3409d72a4faf1e1a18ccc3972f
Author: Aleksey Plekhanov <pl...@gmail.com>
AuthorDate: Mon Jan 20 18:21:12 2020 +0300

    IGNITE-12530 Pages list cache limit added to prevent IgniteOOME on checkpoint - Fixes #7245.
    
    (cherry picked from commit 2a9239d20025d0f69003a33878f788952191f513)
---
 .../processors/cache/mvcc/txlog/TxLog.java         |  3 +-
 .../GridCacheDatabaseSharedManager.java            | 28 ++++++++
 .../cache/persistence/GridCacheOffheapManager.java | 15 +++-
 .../IgniteCacheDatabaseSharedManager.java          |  3 +-
 .../persistence/freelist/AbstractFreeList.java     | 12 +++-
 .../cache/persistence/freelist/CacheFreeList.java  |  7 +-
 .../cache/persistence/freelist/PagesList.java      | 29 +++++---
 .../cache/persistence/metastorage/MetaStorage.java |  3 +-
 .../cache/persistence/pagemem/PageMemoryEx.java    |  5 ++
 .../cache/persistence/pagemem/PageMemoryImpl.java  |  2 +-
 .../partstorage/PartitionMetaStorageImpl.java      |  6 +-
 .../persistence/tree/reuse/ReuseListImpl.java      |  8 ++-
 .../db/checkpoint/CheckpointFreeListTest.java      | 35 ++++++++++
 .../persistence/freelist/FreeListCachingTest.java  | 79 ++++++++++++++++++++--
 .../database/BPlusTreeReuseSelfTest.java           |  2 +-
 .../processors/database/CacheFreeListSelfTest.java |  3 +-
 16 files changed, 211 insertions(+), 29 deletions(-)

diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/mvcc/txlog/TxLog.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/mvcc/txlog/TxLog.java
index 3d2a8fd..85d8fa5 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/mvcc/txlog/TxLog.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/mvcc/txlog/TxLog.java
@@ -179,7 +179,8 @@ public class TxLog implements DbCheckpointListener {
                     reuseListRoot,
                     isNew,
                     txLogReuseListLockLsnr,
-                    ctx
+                    ctx,
+                    null
                 );
 
                 tree = new TxLogTree(
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/GridCacheDatabaseSharedManager.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/GridCacheDatabaseSharedManager.java
index 2eeb330..4dccd95 100755
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/GridCacheDatabaseSharedManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/GridCacheDatabaseSharedManager.java
@@ -229,6 +229,19 @@ public class GridCacheDatabaseSharedManager extends IgniteCacheDatabaseSharedMan
     /** MemoryPolicyConfiguration name reserved for meta store. */
     public static final String METASTORE_DATA_REGION_NAME = "metastoreMemPlc";
 
+    /**
+     * Threshold to calculate limit for pages list on-heap caches.
+     * <p>
+     * Note: When a checkpoint is triggered, we need some amount of page memory to store pages list on-heap cache.
+     * If a checkpoint is triggered by "too many dirty pages" reason and pages list cache is rather big, we can get
+     * {@code IgniteOutOfMemoryException}. To prevent this, we can limit the total amount of cached page list buckets,
+     * assuming that checkpoint will be triggered if no more then 3/4 of pages will be marked as dirty (there will be
+     * at least 1/4 of clean pages) and each cached page list bucket can be stored to up to 2 pages (this value is not
+     * static, but depends on PagesCache.MAX_SIZE, so if PagesCache.MAX_SIZE > PagesListNodeIO#getCapacity it can take
+     * more than 2 pages). Also some amount of page memory needed to store page list metadata.
+     */
+    private static final double PAGE_LIST_CACHE_LIMIT_THRESHOLD = 0.1;
+
     /** Skip sync. */
     private final boolean skipSync = getBoolean(IGNITE_PDS_CHECKPOINT_TEST_SKIP_SYNC);
 
@@ -405,6 +418,9 @@ public class GridCacheDatabaseSharedManager extends IgniteCacheDatabaseSharedMan
     /** Pointer to a memory recovery record that should be included into the next checkpoint record. */
     private volatile WALPointer memoryRecoveryRecordPtr;
 
+    /** Page list cache limits per data region. */
+    private final Map<String, AtomicLong> pageListCacheLimits = new ConcurrentHashMap<>();
+
     /**
      * @param ctx Kernal context.
      */
@@ -3366,6 +3382,18 @@ public class GridCacheDatabaseSharedManager extends IgniteCacheDatabaseSharedMan
     }
 
     /**
+     * @return Holder for page list cache limit for given data region.
+     */
+    public AtomicLong pageListCacheLimitHolder(DataRegion dataRegion) {
+        if (dataRegion.config().isPersistenceEnabled()) {
+            return pageListCacheLimits.computeIfAbsent(dataRegion.config().getName(), name -> new AtomicLong(
+                (long)(((PageMemoryEx)dataRegion.pageMemory()).totalPages() * PAGE_LIST_CACHE_LIMIT_THRESHOLD)));
+        }
+
+        return null;
+    }
+
+    /**
      * Partition destroy queue.
      */
     private static class PartitionDestroyQueue {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/GridCacheOffheapManager.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/GridCacheOffheapManager.java
index b321321..af7998e 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/GridCacheOffheapManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/GridCacheOffheapManager.java
@@ -30,6 +30,7 @@ import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
 import javax.cache.processor.EntryProcessor;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteException;
@@ -131,6 +132,9 @@ public class GridCacheOffheapManager extends IgniteCacheOffheapManagerImpl imple
     /** */
     private ReuseListImpl reuseList;
 
+    /** Page list cache limit for data region of this cache group. */
+    private AtomicLong pageListCacheLimit;
+
     /** Flag indicates that all group partitions have restored their state from page memory / disk. */
     private volatile boolean partitionStatesRestored;
 
@@ -152,6 +156,8 @@ public class GridCacheOffheapManager extends IgniteCacheOffheapManagerImpl imple
 
         RootPage reuseListRoot = metas.reuseListRoot;
 
+        pageListCacheLimit = ((GridCacheDatabaseSharedManager)ctx.database()).pageListCacheLimitHolder(grp.dataRegion());
+
         reuseList = new ReuseListImpl(
             grp.groupId(),
             grp.cacheOrGroupName(),
@@ -160,7 +166,8 @@ public class GridCacheOffheapManager extends IgniteCacheOffheapManagerImpl imple
             reuseListRoot.pageId().pageId(),
             reuseListRoot.isAllocated(),
             diagnosticMgr.pageLockTracker().createPageLockTracker(reuseListName),
-            ctx.kernalContext()
+            ctx.kernalContext(),
+            pageListCacheLimit
         );
 
         RootPage metastoreRoot = metas.treeRoot;
@@ -1694,7 +1701,8 @@ public class GridCacheOffheapManager extends IgniteCacheOffheapManagerImpl imple
                         reuseRoot.pageId().pageId(),
                         reuseRoot.isAllocated(),
                         ctx.diagnostic().pageLockTracker().createPageLockTracker(freeListName),
-                        ctx.kernalContext()
+                        ctx.kernalContext(),
+                        pageListCacheLimit
                     ) {
                         /** {@inheritDoc} */
                         @Override protected long allocatePageNoReuse() throws IgniteCheckedException {
@@ -1718,7 +1726,8 @@ public class GridCacheOffheapManager extends IgniteCacheOffheapManagerImpl imple
                         partMetastoreReuseListRoot.pageId().pageId(),
                         partMetastoreReuseListRoot.isAllocated(),
                         ctx.diagnostic().pageLockTracker().createPageLockTracker(partitionMetaStoreName),
-                        ctx.kernalContext()
+                        ctx.kernalContext(),
+                        pageListCacheLimit
                     ) {
                         /** {@inheritDoc} */
                         @Override protected long allocatePageNoReuse() throws IgniteCheckedException {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java
index a1a7913..52c14e6 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java
@@ -268,7 +268,8 @@ public class IgniteCacheDatabaseSharedManager extends GridCacheSharedManagerAdap
                 0L,
                 true,
                 lsnr,
-                cctx.kernalContext()
+                cctx.kernalContext(),
+                null
             );
 
             freeListMap.put(memPlcCfg.getName(), freeList);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/AbstractFreeList.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/AbstractFreeList.java
index 10fa687..b46417b 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/AbstractFreeList.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/AbstractFreeList.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.processors.cache.persistence.freelist;
 
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReferenceArray;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteLogger;
@@ -94,6 +95,9 @@ public abstract class AbstractFreeList<T extends Storable> extends PagesList imp
     /** */
     private final PageEvictionTracker evictionTracker;
 
+    /** Page list cache limit. */
+    private final AtomicLong pageListCacheLimit;
+
     /**
      *
      */
@@ -433,7 +437,8 @@ public abstract class AbstractFreeList<T extends Storable> extends PagesList imp
         long metaPageId,
         boolean initNew,
         PageLockListener lockLsnr,
-        GridKernalContext ctx
+        GridKernalContext ctx,
+        AtomicLong pageListCacheLimit
     ) throws IgniteCheckedException {
         super(cacheId, name, memPlc.pageMemory(), BUCKETS, wal, metaPageId, lockLsnr, ctx);
 
@@ -462,6 +467,8 @@ public abstract class AbstractFreeList<T extends Storable> extends PagesList imp
 
         this.memMetrics = memMetrics;
 
+        this.pageListCacheLimit = pageListCacheLimit;
+
         init(metaPageId, initNew);
     }
 
@@ -860,7 +867,8 @@ public abstract class AbstractFreeList<T extends Storable> extends PagesList imp
     @Override protected PagesCache getBucketCache(int bucket, boolean create) {
         PagesCache pagesCache = bucketCaches.get(bucket);
 
-        if (pagesCache == null && create && !bucketCaches.compareAndSet(bucket, null, pagesCache = new PagesCache()))
+        if (pagesCache == null && create &&
+            !bucketCaches.compareAndSet(bucket, null, pagesCache = new PagesCache(pageListCacheLimit)))
             pagesCache = bucketCaches.get(bucket);
 
         return pagesCache;
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/CacheFreeList.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/CacheFreeList.java
index 3f7051f..a4a4363 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/CacheFreeList.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/CacheFreeList.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.processors.cache.persistence.freelist;
 
+import java.util.concurrent.atomic.AtomicLong;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.internal.GridKernalContext;
 import org.apache.ignite.internal.metric.IoStatisticsHolder;
@@ -50,7 +51,8 @@ public class CacheFreeList extends AbstractFreeList<CacheDataRow> {
         long metaPageId,
         boolean initNew,
         PageLockListener lockLsnr,
-        GridKernalContext ctx
+        GridKernalContext ctx,
+        AtomicLong pageListCacheLimit
     ) throws IgniteCheckedException {
         super(
             cacheId,
@@ -62,7 +64,8 @@ public class CacheFreeList extends AbstractFreeList<CacheDataRow> {
             metaPageId,
             initNew,
             lockLsnr,
-            ctx
+            ctx,
+            pageListCacheLimit
         );
     }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/PagesList.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/PagesList.java
index fc93fef..70e19d7 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/PagesList.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/PagesList.java
@@ -22,6 +22,7 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicLongArray;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteLogger;
@@ -1911,7 +1912,6 @@ public abstract class PagesList extends DataStructure {
     }
 
     /** Class to store page-list cache onheap. */
-    @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
     public static class PagesCache {
         /** Pages cache max size. */
         private static final int MAX_SIZE =
@@ -1948,14 +1948,19 @@ public abstract class PagesList extends DataStructure {
         /** Count of flush calls with empty cache. */
         private int emptyFlushCnt;
 
+        /** Global (per data region) limit of caches for page lists. */
+        private final AtomicLong pagesCacheLimit;
+
         /**
          * Default constructor.
          */
-        public PagesCache() {
+        public PagesCache(@Nullable AtomicLong pagesCacheLimit) {
             assert U.isPow2(STRIPES_COUNT) : STRIPES_COUNT;
 
             for (int i = 0; i < STRIPES_COUNT; i++)
                 stripeLocks[i] = new Object();
+
+            this.pagesCacheLimit = pagesCacheLimit;
         }
 
         /**
@@ -1972,8 +1977,10 @@ public abstract class PagesList extends DataStructure {
 
                 boolean rmvd = stripe != null && stripe.removeValue(0, pageId) >= 0;
 
-                if (rmvd)
-                    sizeUpdater.decrementAndGet(this);
+                if (rmvd) {
+                    if (sizeUpdater.decrementAndGet(this) == 0 && pagesCacheLimit != null)
+                        pagesCacheLimit.incrementAndGet();
+                }
 
                 return rmvd;
             }
@@ -1995,7 +2002,8 @@ public abstract class PagesList extends DataStructure {
                     GridLongList stripe = stripes[stripeIdx];
 
                     if (stripe != null && !stripe.isEmpty()) {
-                        sizeUpdater.decrementAndGet(this);
+                        if (sizeUpdater.decrementAndGet(this) == 0 && pagesCacheLimit != null)
+                            pagesCacheLimit.incrementAndGet();
 
                         return stripe.remove();
                     }
@@ -2008,7 +2016,6 @@ public abstract class PagesList extends DataStructure {
         /**
          * Flush all stripes to one list and clear stripes.
          */
-        @SuppressWarnings("NonAtomicOperationOnVolatileField")
         public GridLongList flush() {
             GridLongList res = null;
 
@@ -2048,7 +2055,8 @@ public abstract class PagesList extends DataStructure {
                         if (res == null)
                             res = new GridLongList(size);
 
-                        sizeUpdater.addAndGet(this, -stripe.size());
+                        if (sizeUpdater.addAndGet(this, -stripe.size()) == 0 && pagesCacheLimit != null)
+                            pagesCacheLimit.incrementAndGet();
 
                         res.addAll(stripe);
 
@@ -2070,6 +2078,10 @@ public abstract class PagesList extends DataStructure {
             assert pageId != 0L;
 
             // Ok with race here.
+            if (size == 0 && pagesCacheLimit != null && pagesCacheLimit.get() <= 0)
+                return false; // Pages cache limit exceeded.
+
+            // Ok with race here.
             if (size >= MAX_SIZE)
                 return false;
 
@@ -2086,7 +2098,8 @@ public abstract class PagesList extends DataStructure {
                 else {
                     stripe.add(pageId);
 
-                    sizeUpdater.incrementAndGet(this);
+                    if (sizeUpdater.getAndIncrement(this) == 0 && pagesCacheLimit != null)
+                        pagesCacheLimit.decrementAndGet();
 
                     return true;
                 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/metastorage/MetaStorage.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/metastorage/MetaStorage.java
index f437126..670992c 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/metastorage/MetaStorage.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/metastorage/MetaStorage.java
@@ -269,7 +269,8 @@ public class MetaStorage implements DbCheckpointListener, ReadWriteMetastorage {
                 reuseListRoot.pageId().pageId(),
                 reuseListRoot.isAllocated(),
                 diagnosticMgr.pageLockTracker().createPageLockTracker(freeListName),
-                cctx.kernalContext()
+                cctx.kernalContext(),
+                null
             ) {
                 @Override protected long allocatePageNoReuse() throws IgniteCheckedException {
                     return pageMem.allocatePage(grpId, partId, FLAG_DATA);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/pagemem/PageMemoryEx.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/pagemem/PageMemoryEx.java
index f9fdb0d..5d104ae 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/pagemem/PageMemoryEx.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/pagemem/PageMemoryEx.java
@@ -165,4 +165,9 @@ public interface PageMemoryEx extends PageMemory {
      * @return Future that will be completed when all pages are cleared.
      */
     public IgniteInternalFuture<Void> clearAsync(LoadedPagesMap.KeyPredicate pred, boolean cleanDirty);
+
+    /**
+     * Total pages can be placed to memory.
+     */
+    public long totalPages();
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/pagemem/PageMemoryImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/pagemem/PageMemoryImpl.java
index 7adf1c5..bef3316 100755
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/pagemem/PageMemoryImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/pagemem/PageMemoryImpl.java
@@ -1113,7 +1113,7 @@ public class PageMemoryImpl implements PageMemoryEx {
     /**
      * @return Total pages can be placed in all segments.
      */
-    public long totalPages() {
+    @Override public long totalPages() {
         if (segments == null)
             return 0;
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/partstorage/PartitionMetaStorageImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/partstorage/PartitionMetaStorageImpl.java
index db1c796..0e9062a 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/partstorage/PartitionMetaStorageImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/partstorage/PartitionMetaStorageImpl.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.processors.cache.persistence.partstorage;
 
 import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicLong;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.internal.GridKernalContext;
 import org.apache.ignite.internal.pagemem.PageUtils;
@@ -58,9 +59,10 @@ public class PartitionMetaStorageImpl<T extends Storable> extends AbstractFreeLi
         long metaPageId,
         boolean initNew,
         PageLockListener lsnr,
-        GridKernalContext ctx
+        GridKernalContext ctx,
+        AtomicLong pageListCacheLimit
     ) throws IgniteCheckedException {
-        super(cacheId, name, memMetrics, memPlc, reuseList, wal, metaPageId, initNew, lsnr, ctx);
+        super(cacheId, name, memMetrics, memPlc, reuseList, wal, metaPageId, initNew, lsnr, ctx, pageListCacheLimit);
     }
 
     /**
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/reuse/ReuseListImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/reuse/ReuseListImpl.java
index 088a6ee..a59b308 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/reuse/ReuseListImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/reuse/ReuseListImpl.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.processors.cache.persistence.tree.reuse;
 
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.internal.GridKernalContext;
@@ -38,7 +39,7 @@ public class ReuseListImpl extends PagesList implements ReuseList {
     private volatile Stripe[] bucket;
 
     /** Onheap pages cache. */
-    private final PagesCache bucketCache = new PagesCache();
+    private final PagesCache bucketCache;
 
     /**
      * @param cacheId   Cache ID.
@@ -57,7 +58,8 @@ public class ReuseListImpl extends PagesList implements ReuseList {
         long metaPageId,
         boolean initNew,
         PageLockListener lockLsnr,
-        GridKernalContext ctx
+        GridKernalContext ctx,
+        AtomicLong pageListCacheLimit
     ) throws IgniteCheckedException {
         super(
             cacheId,
@@ -70,6 +72,8 @@ public class ReuseListImpl extends PagesList implements ReuseList {
             ctx
         );
 
+        bucketCache = new PagesCache(pageListCacheLimit);
+
         reuseList = this;
 
         init(metaPageId, initNew);
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/checkpoint/CheckpointFreeListTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/checkpoint/CheckpointFreeListTest.java
index 100a959..f3d3842 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/checkpoint/CheckpointFreeListTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/db/checkpoint/CheckpointFreeListTest.java
@@ -33,12 +33,14 @@ import java.util.concurrent.BrokenBarrierException;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.CyclicBarrier;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.atomic.AtomicReferenceArray;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
+import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteSystemProperties;
 import org.apache.ignite.cache.CacheAtomicityMode;
 import org.apache.ignite.cache.CacheMode;
@@ -51,6 +53,8 @@ import org.apache.ignite.internal.IgniteEx;
 import org.apache.ignite.internal.IgniteInternalFuture;
 import org.apache.ignite.internal.IgniteKernal;
 import org.apache.ignite.internal.processors.cache.GridCacheAdapter;
+import org.apache.ignite.internal.processors.cache.persistence.DbCheckpointListener;
+import org.apache.ignite.internal.processors.cache.persistence.GridCacheDatabaseSharedManager;
 import org.apache.ignite.internal.processors.cache.persistence.GridCacheOffheapManager;
 import org.apache.ignite.internal.processors.cache.persistence.freelist.PagesList;
 import org.apache.ignite.internal.util.typedef.T2;
@@ -334,6 +338,37 @@ public class CheckpointFreeListTest extends GridCommonAbstractTest {
         AtomicBoolean done = new AtomicBoolean();
         AtomicReference<Throwable> error = new AtomicReference<>();
 
+        GridCacheDatabaseSharedManager db = (GridCacheDatabaseSharedManager)ignite.context().cache().context().database();
+
+        AtomicLong pageListCacheLimitHolder = db.pageListCacheLimitHolder(ignite.context().cache()
+            .cache(DEFAULT_CACHE_NAME).context().dataRegion());
+
+        long initPageListCacheLimit = pageListCacheLimitHolder.get();
+
+        // Add listener after cache is started, so this listener will be triggered after listener for cache.
+        db.addCheckpointListener(new DbCheckpointListener() {
+            @Override public void onMarkCheckpointBegin(Context ctx) throws IgniteCheckedException {
+                // Check under checkpoint write lock that page list cache limit is correctly restored.
+                // Need to wait for condition here, since checkpointer can store free-list metadata asynchronously.
+                if (!waitForCondition(() -> initPageListCacheLimit == pageListCacheLimitHolder.get(), 1_000L)) {
+                    IgniteCheckedException e = new IgniteCheckedException("Page list cache limit doesn't restored " +
+                        "correctly [init=" + initPageListCacheLimit + ", cur=" + pageListCacheLimitHolder.get() + ']');
+
+                    error.set(e);
+
+                    throw e;
+                }
+            }
+
+            @Override public void onCheckpointBegin(Context ctx) {
+                // No-op.
+            }
+
+            @Override public void beforeCheckpointBegin(Context ctx) {
+                // No-op.
+            }
+        });
+
         IgniteInternalFuture<Long> fut = GridTestUtils.runMultiThreadedAsync(() -> {
             Random rnd = new Random();
 
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/freelist/FreeListCachingTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/freelist/FreeListCachingTest.java
index 0ca2b11..47d777e 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/freelist/FreeListCachingTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/freelist/FreeListCachingTest.java
@@ -21,8 +21,10 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLongArray;
 import org.apache.ignite.IgniteCache;
+import org.apache.ignite.IgniteDataStreamer;
 import org.apache.ignite.cache.CacheAtomicityMode;
 import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
 import org.apache.ignite.configuration.CacheConfiguration;
@@ -30,9 +32,11 @@ import org.apache.ignite.configuration.DataRegionConfiguration;
 import org.apache.ignite.configuration.DataStorageConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.processors.cache.GridCacheContext;
 import org.apache.ignite.internal.processors.cache.GridCacheProcessor;
 import org.apache.ignite.internal.processors.cache.persistence.GridCacheDatabaseSharedManager;
 import org.apache.ignite.internal.processors.cache.persistence.GridCacheOffheapManager;
+import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Test;
 
@@ -64,11 +68,15 @@ public class FreeListCachingTest extends GridCommonAbstractTest {
 
         cfg.setConsistentId(igniteInstanceName);
 
-        cfg.setDataStorageConfiguration(new DataStorageConfiguration()
-            .setDefaultDataRegionConfiguration(new DataRegionConfiguration()
+        DataStorageConfiguration dsCfg = new DataStorageConfiguration();
+
+        int pageSize = dsCfg.getPageSize() == 0 ? DataStorageConfiguration.DFLT_PAGE_SIZE : dsCfg.getPageSize();
+
+        dsCfg.setDefaultDataRegionConfiguration(new DataRegionConfiguration()
                 .setPersistenceEnabled(true)
-                .setMaxSize(300L * 1024 * 1024)
-            ));
+                .setMaxSize(pageSize * 40_000L));
+
+        cfg.setDataStorageConfiguration(dsCfg);
 
         return cfg;
     }
@@ -215,4 +223,67 @@ public class FreeListCachingTest extends GridCommonAbstractTest {
             assertTrue("Some buckets should be cached [partId=" + cacheData.partId() + ']', totalCacheSize > 0);
         });
     }
+
+    /**
+     * @throws Exception If test failed.
+     */
+    @Test
+    public void testPageListCacheLimit() throws Exception {
+        IgniteEx ignite = startGrid(0);
+
+        ignite.cluster().active(true);
+
+        ignite.getOrCreateCache("cache1");
+        ignite.getOrCreateCache("cache2");
+
+        GridCacheContext<?, ?> cctx1 = ignite.context().cache().cache("cache1").context();
+        GridCacheContext<?, ?> cctx2 = ignite.context().cache().cache("cache2").context();
+
+        GridCacheOffheapManager offheap1 = (GridCacheOffheapManager)cctx1.offheap();
+        GridCacheOffheapManager offheap2 = (GridCacheOffheapManager)cctx2.offheap();
+
+        GridCacheDatabaseSharedManager db = (GridCacheDatabaseSharedManager)ignite.context().cache().context().database();
+
+        assertEquals(db.pageListCacheLimitHolder(cctx1.dataRegion()), db.pageListCacheLimitHolder(cctx2.dataRegion()));
+
+        long limit = db.pageListCacheLimitHolder(cctx1.dataRegion()).get();
+
+        try (IgniteDataStreamer<Object, Object> streamer1 = ignite.dataStreamer("cache1");
+            IgniteDataStreamer<Object, Object> streamer2 = ignite.dataStreamer("cache2")) {
+            // Fill caches to trigger "too many dirty pages" checkpoint.
+            for (int i = 0; i < 50_000; i++) {
+                streamer1.addData(i, new byte[i % 2048]);
+                streamer2.addData(i, new byte[i % 2048]);
+
+                // Calculates page list caches count and validate this value periodically.
+                if (i % 5_000 == 0) {
+                    streamer1.flush();
+                    streamer2.flush();
+
+                    AtomicInteger pageCachesCnt = new AtomicInteger();
+
+                    for (GridCacheOffheapManager offheap : F.asList(offheap1, offheap2)) {
+                        offheap.cacheDataStores().forEach(cacheData -> {
+                            if (cacheData.rowStore() == null)
+                                return;
+
+                            PagesList list = (PagesList)cacheData.rowStore().freeList();
+
+                            for (int b = 0; b < list.bucketsSize.length(); b++) {
+                                PagesList.PagesCache pagesCache = list.getBucketCache(b, false);
+
+                                if (pagesCache != null && pagesCache.size() > 0)
+                                    pageCachesCnt.incrementAndGet();
+                            }
+                        });
+                    }
+
+                    // There can be a race and actual page list caches count can exceed the limit in very rare cases.
+                    assertTrue("Page list caches count is more than expected [count: " + pageCachesCnt.get() +
+                        ", limit=" + limit + ']', pageCachesCnt.get() <= limit + ignite.configuration()
+                        .getDataStreamerThreadPoolSize() - 1);
+                }
+            }
+        }
+    }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/database/BPlusTreeReuseSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/database/BPlusTreeReuseSelfTest.java
index 2c7d514..8d50fad 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/database/BPlusTreeReuseSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/database/BPlusTreeReuseSelfTest.java
@@ -83,7 +83,7 @@ public class BPlusTreeReuseSelfTest extends BPlusTreeSelfTest {
             boolean initNew,
             GridKernalContext ctx
         ) throws IgniteCheckedException {
-            super(cacheId, name, pageMem, wal, metaPageId, initNew, new TestPageLockListener(), ctx);
+            super(cacheId, name, pageMem, wal, metaPageId, initNew, new TestPageLockListener(), ctx, null);
         }
 
         /**
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/database/CacheFreeListSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/database/CacheFreeListSelfTest.java
index e4a8c9b..68f1668 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/database/CacheFreeListSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/database/CacheFreeListSelfTest.java
@@ -529,7 +529,8 @@ public class CacheFreeListSelfTest extends GridCommonAbstractTest {
             metaPageId,
             true,
             null,
-            new GridTestKernalContext(log)
+            new GridTestKernalContext(log),
+            null
         );
     }