public inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH 1/4] Remove entries that haven't been used for a certain time
19+ messages / 4 participants
[nested] [flat]

* [PATCH 1/4] Remove entries that haven't been used for a certain time
@ 2018-10-16 04:04  Kyotaro Horiguchi <[email protected]>
  0 siblings, 0 replies; 19+ messages in thread

From: Kyotaro Horiguchi @ 2018-10-16 04:04 UTC (permalink / raw)

Catcache entries can be left alone for several reasons. It is not
desirable that they eat up memory. With this patch, This adds
consideration of removal of entries that haven't been used for a
certain time before enlarging the hash array.
---
 doc/src/sgml/config.sgml                      |  38 ++++++
 src/backend/access/transam/xact.c             |   5 +
 src/backend/utils/cache/catcache.c            | 164 ++++++++++++++++++++++++--
 src/backend/utils/misc/guc.c                  |  23 ++++
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/utils/catcache.h                  |  28 ++++-
 6 files changed, 252 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 9b7a7388d5..d0d2374944 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1662,6 +1662,44 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-syscache-memory-target" xreflabel="syscache_memory_target">
+      <term><varname>syscache_memory_target</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>syscache_memory_target</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the maximum amount of memory to which syscache is expanded
+        without pruning. The value defaults to 0, indicating that pruning is
+        always considered. After exceeding this size, syscache pruning is
+        considered according to
+        <xref linkend="guc-syscache-prune-min-age"/>. If you need to keep
+        certain amount of syscache entries with intermittent usage, try
+        increase this setting.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-syscache-prune-min-age" xreflabel="syscache_prune_min_age">
+      <term><varname>syscache_prune_min_age</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>syscache_prune_min_age</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the minimum amount of unused time in seconds at which a
+        syscache entry is considered to be removed. -1 indicates that syscache
+        pruning is disabled at all. The value defaults to 600 seconds
+        (<literal>10 minutes</literal>). The syscache entries that are not
+        used for the duration can be removed to prevent syscache bloat. This
+        behavior is suppressed until the size of syscache exceeds
+        <xref linkend="guc-syscache-memory-target"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth">
       <term><varname>max_stack_depth</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 92bda87804..ddc433c59e 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -734,7 +734,12 @@ void
 SetCurrentStatementStartTimestamp(void)
 {
 	if (!IsParallelWorker())
+	{
 		stmtStartTimestamp = GetCurrentTimestamp();
+
+		/* Set this timestamp as aproximated current time */
+		SetCatCacheClock(stmtStartTimestamp);
+	}
 	else
 		Assert(stmtStartTimestamp != 0);
 }
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 258a1d64cc..769e173844 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -71,9 +71,24 @@
 #define CACHE6_elog(a,b,c,d,e,f,g)
 #endif
 
+/*
+ * GUC variable to define the minimum size of hash to cosider entry eviction.
+ * This variable is shared among various cache mechanisms.
+ */
+int cache_memory_target = 0;
+
+/* GUC variable to define the minimum age of entries that will be cosidered to
+ * be evicted in seconds. This variable is shared among various cache
+ * mechanisms.
+ */
+int cache_prune_min_age = 600;
+
 /* Cache management header --- pointer is NULL until created */
 static CatCacheHeader *CacheHdr = NULL;
 
+/* Timestamp used for any operation on caches. */
+TimestampTz	catcacheclock = 0;
+
 static inline HeapTuple SearchCatCacheInternal(CatCache *cache,
 					   int nkeys,
 					   Datum v1, Datum v2,
@@ -490,6 +505,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct)
 		CatCacheFreeKeys(cache->cc_tupdesc, cache->cc_nkeys,
 						 cache->cc_keyno, ct->keys);
 
+	cache->cc_tupsize -= ct->size;
 	pfree(ct);
 
 	--cache->cc_ntup;
@@ -841,6 +857,7 @@ InitCatCache(int id,
 	cp->cc_nkeys = nkeys;
 	for (i = 0; i < nkeys; ++i)
 		cp->cc_keyno[i] = key[i];
+	cp->cc_tupsize = 0;
 
 	/*
 	 * new cache is initialized as far as we can go for now. print some
@@ -858,9 +875,127 @@ InitCatCache(int id,
 	 */
 	MemoryContextSwitchTo(oldcxt);
 
+	/* initilize catcache reference clock if haven't done yet */
+	if (catcacheclock == 0)
+		catcacheclock = GetCurrentTimestamp();
+
 	return cp;
 }
 
+/*
+ * CatCacheCleanupOldEntries - Remove infrequently-used entries
+ *
+ * Catcache entries can be left alone for several reasons. We remove them if
+ * they are not accessed for a certain time to prevent catcache from
+ * bloating. The eviction is performed with the similar algorithm with buffer
+ * eviction using access counter. Entries that are accessed several times can
+ * live longer than those that have had no access in the same duration.
+ */
+static bool
+CatCacheCleanupOldEntries(CatCache *cp)
+{
+	int			i;
+	int			nremoved = 0;
+	size_t		hash_size;
+#ifdef CATCACHE_STATS
+	/* These variables are only for debugging purpose */
+	int			ntotal = 0;
+	/*
+	 * nth element in nentries stores the number of cache entries that have
+	 * lived unaccessed for corresponding multiple in ageclass of
+	 * cache_prune_min_age. The index of nremoved_entry is the value of the
+	 * clock-sweep counter, which takes from 0 up to 2.
+	 */
+	double		ageclass[] = {0.05, 0.1, 1.0, 2.0, 3.0, 0.0};
+	int			nentries[] = {0, 0, 0, 0, 0, 0};
+	int			nremoved_entry[3] = {0, 0, 0};
+	int			j;
+#endif
+
+	/* Return immediately if no pruning is wanted */
+	if (cache_prune_min_age < 0)
+		return false;
+
+	/*
+	 * Return without pruning if the size of the hash is below the target.
+	 */
+	hash_size = cp->cc_nbuckets * sizeof(dlist_head);
+	if (hash_size + cp->cc_tupsize < (Size) cache_memory_target * 1024L)
+		return false;
+	
+	/* Search the whole hash for entries to remove */
+	for (i = 0; i < cp->cc_nbuckets; i++)
+	{
+		dlist_mutable_iter iter;
+
+		dlist_foreach_modify(iter, &cp->cc_bucket[i])
+		{
+			CatCTup    *ct = dlist_container(CatCTup, cache_elem, iter.cur);
+			long entry_age;
+			int us;
+
+
+			/*
+			 * Calculate the duration from the time of the last access to the
+			 * "current" time. Since catcacheclock is not advanced within a
+			 * transaction, the entries that are accessed within the current
+			 * transaction won't be pruned.
+			 */
+			TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us);
+
+#ifdef CATCACHE_STATS
+			/* count catcache entries for each age class */
+			ntotal++;
+			for (j = 0 ;
+				 ageclass[j] != 0.0 &&
+					 entry_age > cache_prune_min_age * ageclass[j] ;
+				 j++);
+			if (ageclass[j] == 0.0) j--;
+			nentries[j]++;
+#endif
+
+			/*
+			 * Try to remove entries older than cache_prune_min_age seconds.
+			 * Entries that are not accessed after last pruning are removed in
+			 * that seconds, and that has been accessed several times are
+			 * removed after leaving alone for up to three times of the
+			 * duration. We don't try shrink buckets since pruning effectively
+			 * caps catcache expansion in the long term.
+			 */
+			if (entry_age > cache_prune_min_age)
+			{
+#ifdef CATCACHE_STATS
+				Assert (ct->naccess >= 0 && ct->naccess <= 2);
+				nremoved_entry[ct->naccess]++;
+#endif
+				if (ct->naccess > 0)
+					ct->naccess--;
+				else if (ct->refcount == 0 &&
+						 (!ct->c_list || ct->c_list->refcount == 0))
+				{
+					CatCacheRemoveCTup(cp, ct);
+					nremoved++;
+				}
+			}
+		}
+	}
+
+#ifdef CATCACHE_STATS
+	ereport(DEBUG1,
+			(errmsg ("removed %d/%d, age(-%.0fs:%d, -%.0fs:%d, *-%.0fs:%d, -%.0fs:%d, -%.0fs:%d) naccessed(0:%d, 1:%d, 2:%d)",
+					 nremoved, ntotal,
+					 ageclass[0] * cache_prune_min_age, nentries[0],
+					 ageclass[1] * cache_prune_min_age, nentries[1],
+					 ageclass[2] * cache_prune_min_age, nentries[2],
+					 ageclass[3] * cache_prune_min_age, nentries[3],
+					 ageclass[4] * cache_prune_min_age, nentries[4],
+					 nremoved_entry[0], nremoved_entry[1], nremoved_entry[2]),
+			 errhidestmt(true)));
+#endif
+
+	return nremoved > 0;
+}
+
 /*
  * Enlarge a catcache, doubling the number of buckets.
  */
@@ -1274,6 +1409,11 @@ SearchCatCacheInternal(CatCache *cache,
 		 */
 		dlist_move_head(bucket, &ct->cache_elem);
 
+		/* Update access information for pruning */
+		if (ct->naccess < 2)
+			ct->naccess++;
+		ct->lastaccess = catcacheclock;
+
 		/*
 		 * If it's a positive entry, bump its refcount and return it. If it's
 		 * negative, we can report failure to the caller.
@@ -1819,11 +1959,13 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	CatCTup    *ct;
 	HeapTuple	dtp;
 	MemoryContext oldcxt;
+	int			tupsize = 0;
 
 	/* negative entries have no tuple associated */
 	if (ntp)
 	{
 		int			i;
+		int			tupsize;
 
 		Assert(!negative);
 
@@ -1842,13 +1984,14 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 		/* Allocate memory for CatCTup and the cached tuple in one go */
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
 
-		ct = (CatCTup *) palloc(sizeof(CatCTup) +
-								MAXIMUM_ALIGNOF + dtp->t_len);
+		tupsize = sizeof(CatCTup) +	MAXIMUM_ALIGNOF + dtp->t_len;
+		ct = (CatCTup *) palloc(tupsize);
 		ct->tuple.t_len = dtp->t_len;
 		ct->tuple.t_self = dtp->t_self;
 		ct->tuple.t_tableOid = dtp->t_tableOid;
 		ct->tuple.t_data = (HeapTupleHeader)
 			MAXALIGN(((char *) ct) + sizeof(CatCTup));
+		ct->size = tupsize;
 		/* copy tuple contents */
 		memcpy((char *) ct->tuple.t_data,
 			   (const char *) dtp->t_data,
@@ -1876,8 +2019,8 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	{
 		Assert(negative);
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
-		ct = (CatCTup *) palloc(sizeof(CatCTup));
-
+		tupsize = sizeof(CatCTup);
+		ct = (CatCTup *) palloc(tupsize);
 		/*
 		 * Store keys - they'll point into separately allocated memory if not
 		 * by-value.
@@ -1898,17 +2041,24 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	ct->dead = false;
 	ct->negative = negative;
 	ct->hash_value = hashValue;
+	ct->naccess = 0;
+	ct->lastaccess = catcacheclock;
+	ct->size = tupsize;
 
 	dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem);
 
 	cache->cc_ntup++;
 	CacheHdr->ch_ntup++;
+	cache->cc_tupsize += tupsize;
 
 	/*
-	 * If the hash table has become too full, enlarge the buckets array. Quite
-	 * arbitrarily, we enlarge when fill factor > 2.
+	 * If the hash table has become too full, try cleanup by removing
+	 * infrequently used entries to make a room for the new entry. If it
+	 * failed, enlarge the bucket array instead.  Quite arbitrarily, we try
+	 * this when fill factor > 2.
 	 */
-	if (cache->cc_ntup > cache->cc_nbuckets * 2)
+	if (cache->cc_ntup > cache->cc_nbuckets * 2 &&
+		!CatCacheCleanupOldEntries(cache))
 		RehashCatCache(cache);
 
 	return ct;
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 8681ada33a..06c589f725 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -81,6 +81,7 @@
 #include "tsearch/ts_cache.h"
 #include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/catcache.h"
 #include "utils/guc_tables.h"
 #include "utils/float.h"
 #include "utils/memutils.h"
@@ -2204,6 +2205,28 @@ static struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"cache_memory_target", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the minimum syscache size to keep."),
+			gettext_noop("Cache is not pruned before exceeding this size."),
+			GUC_UNIT_KB
+		},
+		&cache_memory_target,
+		0, 0, MAX_KILOBYTES,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"cache_prune_min_age", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the minimum unused duration of cache entries before removal."),
+			gettext_noop("Cache entries that live unused for longer than this seconds are considered to be removed."),
+			GUC_UNIT_S
+		},
+		&cache_prune_min_age,
+		600, -1, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	/*
 	 * We use the hopefully-safely-small value of 100kB as the compiled-in
 	 * default for max_stack_depth.  InitializeGUCOptions will increase it if
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index c7f53470df..108d332f2c 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -128,6 +128,8 @@
 #work_mem = 4MB				# min 64kB
 #maintenance_work_mem = 64MB		# min 1MB
 #autovacuum_work_mem = -1		# min 1MB, or -1 to use maintenance_work_mem
+#cache_memory_target = 0kB	# in kB
+#cache_prune_min_age = 600s	# -1 disables pruning
 #max_stack_depth = 2MB			# min 100kB
 #shared_memory_type = mmap		# the default is the first option
 					# supported by the operating system:
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h
index 65d816a583..5d24809900 100644
--- a/src/include/utils/catcache.h
+++ b/src/include/utils/catcache.h
@@ -22,6 +22,7 @@
 
 #include "access/htup.h"
 #include "access/skey.h"
+#include "datatype/timestamp.h"
 #include "lib/ilist.h"
 #include "utils/relcache.h"
 
@@ -61,6 +62,7 @@ typedef struct catcache
 	slist_node	cc_next;		/* list link */
 	ScanKeyData cc_skey[CATCACHE_MAXKEYS];	/* precomputed key info for heap
 											 * scans */
+	int			cc_tupsize;		/* total amount of catcache tuples */
 
 	/*
 	 * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS
@@ -119,7 +121,9 @@ typedef struct catctup
 	bool		dead;			/* dead but not yet removed? */
 	bool		negative;		/* negative cache entry? */
 	HeapTupleData tuple;		/* tuple management header */
-
+	int			naccess;		/* # of access to this entry, up to 2  */
+	TimestampTz	lastaccess;		/* approx. timestamp of the last usage */
+	int			size;			/* palloc'ed size off this tuple */
 	/*
 	 * The tuple may also be a member of at most one CatCList.  (If a single
 	 * catcache is list-searched with varying numbers of keys, we may have to
@@ -189,6 +193,28 @@ typedef struct catcacheheader
 /* this extern duplicates utils/memutils.h... */
 extern PGDLLIMPORT MemoryContext CacheMemoryContext;
 
+/* for guc.c, not PGDLLPMPORT'ed */
+extern int cache_prune_min_age;
+extern int cache_memory_target;
+
+/* to use as access timestamp of catcache entries */
+extern TimestampTz catcacheclock;
+
+/*
+ * SetCatCacheClock - set timestamp for catcache access record
+ */
+static inline void
+SetCatCacheClock(TimestampTz ts)
+{
+	catcacheclock = ts;
+}
+
+static inline TimestampTz
+GetCatCacheClock(void)
+{
+	return catcacheclock;
+}
+
 extern void CreateCacheMemoryContext(void);
 
 extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid,
-- 
2.16.3


----Next_Part(Wed_Feb_06_15_17_59_2019_321)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="v10-0002-Syscache-usage-tracking-feature.patch"



^ permalink  raw  reply  [nested|flat] 19+ messages in thread

* [PATCH 1/4] Remove entries that haven't been used for a certain time
@ 2018-10-16 04:04  Kyotaro Horiguchi <[email protected]>
  0 siblings, 0 replies; 19+ messages in thread

From: Kyotaro Horiguchi @ 2018-10-16 04:04 UTC (permalink / raw)

Catcache entries can be left alone for several reasons. It is not
desirable that they eat up memory. With this patch, This adds
consideration of removal of entries that haven't been used for a
certain time before enlarging the hash array.
---
 doc/src/sgml/config.sgml                      |  38 ++++++
 src/backend/access/transam/xact.c             |   5 +
 src/backend/utils/cache/catcache.c            | 168 ++++++++++++++++++++++++--
 src/backend/utils/misc/guc.c                  |  23 ++++
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/utils/catcache.h                  |  28 ++++-
 6 files changed, 256 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 9b7a7388d5..d0d2374944 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1662,6 +1662,44 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-syscache-memory-target" xreflabel="syscache_memory_target">
+      <term><varname>syscache_memory_target</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>syscache_memory_target</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the maximum amount of memory to which syscache is expanded
+        without pruning. The value defaults to 0, indicating that pruning is
+        always considered. After exceeding this size, syscache pruning is
+        considered according to
+        <xref linkend="guc-syscache-prune-min-age"/>. If you need to keep
+        certain amount of syscache entries with intermittent usage, try
+        increase this setting.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-syscache-prune-min-age" xreflabel="syscache_prune_min_age">
+      <term><varname>syscache_prune_min_age</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>syscache_prune_min_age</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the minimum amount of unused time in seconds at which a
+        syscache entry is considered to be removed. -1 indicates that syscache
+        pruning is disabled at all. The value defaults to 600 seconds
+        (<literal>10 minutes</literal>). The syscache entries that are not
+        used for the duration can be removed to prevent syscache bloat. This
+        behavior is suppressed until the size of syscache exceeds
+        <xref linkend="guc-syscache-memory-target"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth">
       <term><varname>max_stack_depth</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 92bda87804..ddc433c59e 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -734,7 +734,12 @@ void
 SetCurrentStatementStartTimestamp(void)
 {
 	if (!IsParallelWorker())
+	{
 		stmtStartTimestamp = GetCurrentTimestamp();
+
+		/* Set this timestamp as aproximated current time */
+		SetCatCacheClock(stmtStartTimestamp);
+	}
 	else
 		Assert(stmtStartTimestamp != 0);
 }
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 258a1d64cc..5106ed896a 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -71,9 +71,24 @@
 #define CACHE6_elog(a,b,c,d,e,f,g)
 #endif
 
+/*
+ * GUC variable to define the minimum size of hash to cosider entry eviction.
+ * This variable is shared among various cache mechanisms.
+ */
+int cache_memory_target = 0;
+
+/* GUC variable to define the minimum age of entries that will be cosidered to
+ * be evicted in seconds. This variable is shared among various cache
+ * mechanisms.
+ */
+int cache_prune_min_age = 600;
+
 /* Cache management header --- pointer is NULL until created */
 static CatCacheHeader *CacheHdr = NULL;
 
+/* Timestamp used for any operation on caches. */
+TimestampTz	catcacheclock = 0;
+
 static inline HeapTuple SearchCatCacheInternal(CatCache *cache,
 					   int nkeys,
 					   Datum v1, Datum v2,
@@ -490,6 +505,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct)
 		CatCacheFreeKeys(cache->cc_tupdesc, cache->cc_nkeys,
 						 cache->cc_keyno, ct->keys);
 
+	cache->cc_tupsize -= ct->size;
 	pfree(ct);
 
 	--cache->cc_ntup;
@@ -841,6 +857,7 @@ InitCatCache(int id,
 	cp->cc_nkeys = nkeys;
 	for (i = 0; i < nkeys; ++i)
 		cp->cc_keyno[i] = key[i];
+	cp->cc_tupsize = 0;
 
 	/*
 	 * new cache is initialized as far as we can go for now. print some
@@ -858,9 +875,127 @@ InitCatCache(int id,
 	 */
 	MemoryContextSwitchTo(oldcxt);
 
+	/* initilize catcache reference clock if haven't done yet */
+	if (catcacheclock == 0)
+		catcacheclock = GetCurrentTimestamp();
+
 	return cp;
 }
 
+/*
+ * CatCacheCleanupOldEntries - Remove infrequently-used entries
+ *
+ * Catcache entries can be left alone for several reasons. We remove them if
+ * they are not accessed for a certain time to prevent catcache from
+ * bloating. The eviction is performed with the similar algorithm with buffer
+ * eviction using access counter. Entries that are accessed several times can
+ * live longer than those that have had no access in the same duration.
+ */
+static bool
+CatCacheCleanupOldEntries(CatCache *cp)
+{
+	int			i;
+	int			nremoved = 0;
+	size_t		hash_size;
+#ifdef CATCACHE_STATS
+	/* These variables are only for debugging purpose */
+	int			ntotal = 0;
+	/*
+	 * nth element in nentries stores the number of cache entries that have
+	 * lived unaccessed for corresponding multiple in ageclass of
+	 * cache_prune_min_age. The index of nremoved_entry is the value of the
+	 * clock-sweep counter, which takes from 0 up to 2.
+	 */
+	double		ageclass[] = {0.05, 0.1, 1.0, 2.0, 3.0, 0.0};
+	int			nentries[] = {0, 0, 0, 0, 0, 0};
+	int			nremoved_entry[3] = {0, 0, 0};
+	int			j;
+#endif
+
+	/* Return immediately if no pruning is wanted */
+	if (cache_prune_min_age < 0)
+		return false;
+
+	/*
+	 * Return without pruning if the size of the hash is below the target.
+	 */
+	hash_size = cp->cc_nbuckets * sizeof(dlist_head);
+	if (hash_size + cp->cc_tupsize < (Size) cache_memory_target * 1024L)
+		return false;
+	
+	/* Search the whole hash for entries to remove */
+	for (i = 0; i < cp->cc_nbuckets; i++)
+	{
+		dlist_mutable_iter iter;
+
+		dlist_foreach_modify(iter, &cp->cc_bucket[i])
+		{
+			CatCTup    *ct = dlist_container(CatCTup, cache_elem, iter.cur);
+			long entry_age;
+			int us;
+
+
+			/*
+			 * Calculate the duration from the time of the last access to the
+			 * "current" time. Since catcacheclock is not advanced within a
+			 * transaction, the entries that are accessed within the current
+			 * transaction won't be pruned.
+			 */
+			TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us);
+
+#ifdef CATCACHE_STATS
+			/* count catcache entries for each age class */
+			ntotal++;
+			for (j = 0 ;
+				 ageclass[j] != 0.0 &&
+					 entry_age > cache_prune_min_age * ageclass[j] ;
+				 j++);
+			if (ageclass[j] == 0.0) j--;
+			nentries[j]++;
+#endif
+
+			/*
+			 * Try to remove entries older than cache_prune_min_age seconds.
+			 * Entries that are not accessed after last pruning are removed in
+			 * that seconds, and that has been accessed several times are
+			 * removed after leaving alone for up to three times of the
+			 * duration. We don't try shrink buckets since pruning effectively
+			 * caps catcache expansion in the long term.
+			 */
+			if (entry_age > cache_prune_min_age)
+			{
+#ifdef CATCACHE_STATS
+				Assert (ct->naccess >= 0 && ct->naccess <= 2);
+				nremoved_entry[ct->naccess]++;
+#endif
+				if (ct->naccess > 0)
+					ct->naccess--;
+				else if (ct->refcount == 0 &&
+						 (!ct->c_list || ct->c_list->refcount == 0))
+				{
+					CatCacheRemoveCTup(cp, ct);
+					nremoved++;
+				}
+			}
+		}
+	}
+
+#ifdef CATCACHE_STATS
+	ereport(DEBUG1,
+			(errmsg ("removed %d/%d, age(-%.0fs:%d, -%.0fs:%d, *-%.0fs:%d, -%.0fs:%d, -%.0fs:%d) naccessed(0:%d, 1:%d, 2:%d)",
+					 nremoved, ntotal,
+					 ageclass[0] * cache_prune_min_age, nentries[0],
+					 ageclass[1] * cache_prune_min_age, nentries[1],
+					 ageclass[2] * cache_prune_min_age, nentries[2],
+					 ageclass[3] * cache_prune_min_age, nentries[3],
+					 ageclass[4] * cache_prune_min_age, nentries[4],
+					 nremoved_entry[0], nremoved_entry[1], nremoved_entry[2]),
+			 errhidestmt(true)));
+#endif
+
+	return nremoved > 0;
+}
+
 /*
  * Enlarge a catcache, doubling the number of buckets.
  */
@@ -1274,6 +1409,11 @@ SearchCatCacheInternal(CatCache *cache,
 		 */
 		dlist_move_head(bucket, &ct->cache_elem);
 
+		/* Update access information for pruning */
+		if (ct->naccess < 2)
+			ct->naccess++;
+		ct->lastaccess = catcacheclock;
+
 		/*
 		 * If it's a positive entry, bump its refcount and return it. If it's
 		 * negative, we can report failure to the caller.
@@ -1819,11 +1959,13 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	CatCTup    *ct;
 	HeapTuple	dtp;
 	MemoryContext oldcxt;
+	int			tupsize = 0;
 
 	/* negative entries have no tuple associated */
 	if (ntp)
 	{
 		int			i;
+		int			tupsize;
 
 		Assert(!negative);
 
@@ -1842,13 +1984,14 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 		/* Allocate memory for CatCTup and the cached tuple in one go */
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
 
-		ct = (CatCTup *) palloc(sizeof(CatCTup) +
-								MAXIMUM_ALIGNOF + dtp->t_len);
+		tupsize = sizeof(CatCTup) +	MAXIMUM_ALIGNOF + dtp->t_len;
+		ct = (CatCTup *) palloc(tupsize);
 		ct->tuple.t_len = dtp->t_len;
 		ct->tuple.t_self = dtp->t_self;
 		ct->tuple.t_tableOid = dtp->t_tableOid;
 		ct->tuple.t_data = (HeapTupleHeader)
 			MAXALIGN(((char *) ct) + sizeof(CatCTup));
+		ct->size = tupsize;
 		/* copy tuple contents */
 		memcpy((char *) ct->tuple.t_data,
 			   (const char *) dtp->t_data,
@@ -1876,8 +2019,8 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	{
 		Assert(negative);
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
-		ct = (CatCTup *) palloc(sizeof(CatCTup));
-
+		tupsize = sizeof(CatCTup);
+		ct = (CatCTup *) palloc(tupsize);
 		/*
 		 * Store keys - they'll point into separately allocated memory if not
 		 * by-value.
@@ -1898,19 +2041,30 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	ct->dead = false;
 	ct->negative = negative;
 	ct->hash_value = hashValue;
+	ct->naccess = 0;
+	ct->lastaccess = catcacheclock;
+	ct->size = tupsize;
 
 	dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem);
 
 	cache->cc_ntup++;
 	CacheHdr->ch_ntup++;
+	cache->cc_tupsize += tupsize;
 
+	/* increase refcount so that this survives pruning */
+	ct->refcount++;
 	/*
-	 * If the hash table has become too full, enlarge the buckets array. Quite
-	 * arbitrarily, we enlarge when fill factor > 2.
+	 * If the hash table has become too full, try cleanup by removing
+	 * infrequently used entries to make a room for the new entry. If it
+	 * failed, enlarge the bucket array instead.  Quite arbitrarily, we try
+	 * this when fill factor > 2.
 	 */
-	if (cache->cc_ntup > cache->cc_nbuckets * 2)
+	if (cache->cc_ntup > cache->cc_nbuckets * 2 &&
+		!CatCacheCleanupOldEntries(cache))
 		RehashCatCache(cache);
 
+	ct->refcount--;
+
 	return ct;
 }
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 8681ada33a..06c589f725 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -81,6 +81,7 @@
 #include "tsearch/ts_cache.h"
 #include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/catcache.h"
 #include "utils/guc_tables.h"
 #include "utils/float.h"
 #include "utils/memutils.h"
@@ -2204,6 +2205,28 @@ static struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"cache_memory_target", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the minimum syscache size to keep."),
+			gettext_noop("Cache is not pruned before exceeding this size."),
+			GUC_UNIT_KB
+		},
+		&cache_memory_target,
+		0, 0, MAX_KILOBYTES,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"cache_prune_min_age", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the minimum unused duration of cache entries before removal."),
+			gettext_noop("Cache entries that live unused for longer than this seconds are considered to be removed."),
+			GUC_UNIT_S
+		},
+		&cache_prune_min_age,
+		600, -1, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	/*
 	 * We use the hopefully-safely-small value of 100kB as the compiled-in
 	 * default for max_stack_depth.  InitializeGUCOptions will increase it if
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index c7f53470df..108d332f2c 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -128,6 +128,8 @@
 #work_mem = 4MB				# min 64kB
 #maintenance_work_mem = 64MB		# min 1MB
 #autovacuum_work_mem = -1		# min 1MB, or -1 to use maintenance_work_mem
+#cache_memory_target = 0kB	# in kB
+#cache_prune_min_age = 600s	# -1 disables pruning
 #max_stack_depth = 2MB			# min 100kB
 #shared_memory_type = mmap		# the default is the first option
 					# supported by the operating system:
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h
index 65d816a583..5d24809900 100644
--- a/src/include/utils/catcache.h
+++ b/src/include/utils/catcache.h
@@ -22,6 +22,7 @@
 
 #include "access/htup.h"
 #include "access/skey.h"
+#include "datatype/timestamp.h"
 #include "lib/ilist.h"
 #include "utils/relcache.h"
 
@@ -61,6 +62,7 @@ typedef struct catcache
 	slist_node	cc_next;		/* list link */
 	ScanKeyData cc_skey[CATCACHE_MAXKEYS];	/* precomputed key info for heap
 											 * scans */
+	int			cc_tupsize;		/* total amount of catcache tuples */
 
 	/*
 	 * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS
@@ -119,7 +121,9 @@ typedef struct catctup
 	bool		dead;			/* dead but not yet removed? */
 	bool		negative;		/* negative cache entry? */
 	HeapTupleData tuple;		/* tuple management header */
-
+	int			naccess;		/* # of access to this entry, up to 2  */
+	TimestampTz	lastaccess;		/* approx. timestamp of the last usage */
+	int			size;			/* palloc'ed size off this tuple */
 	/*
 	 * The tuple may also be a member of at most one CatCList.  (If a single
 	 * catcache is list-searched with varying numbers of keys, we may have to
@@ -189,6 +193,28 @@ typedef struct catcacheheader
 /* this extern duplicates utils/memutils.h... */
 extern PGDLLIMPORT MemoryContext CacheMemoryContext;
 
+/* for guc.c, not PGDLLPMPORT'ed */
+extern int cache_prune_min_age;
+extern int cache_memory_target;
+
+/* to use as access timestamp of catcache entries */
+extern TimestampTz catcacheclock;
+
+/*
+ * SetCatCacheClock - set timestamp for catcache access record
+ */
+static inline void
+SetCatCacheClock(TimestampTz ts)
+{
+	catcacheclock = ts;
+}
+
+static inline TimestampTz
+GetCatCacheClock(void)
+{
+	return catcacheclock;
+}
+
 extern void CreateCacheMemoryContext(void);
 
 extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid,
-- 
2.16.3


----Next_Part(Wed_Feb_06_17_37_04_2019_764)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="v11-0002-Syscache-usage-tracking-feature.patch"



^ permalink  raw  reply  [nested|flat] 19+ messages in thread

* [PATCH 2/4] Remove entries that haven't been used for a certain time
@ 2018-10-16 04:04  Kyotaro Horiguchi <[email protected]>
  0 siblings, 0 replies; 19+ messages in thread

From: Kyotaro Horiguchi @ 2018-10-16 04:04 UTC (permalink / raw)

Catcache entries can be left alone for several reasons. It is not
desirable that they eat up memory. With this patch, This adds
consideration of removal of entries that haven't been used for a
certain time before enlarging the hash array.

This also can put a hard limit on the number of catcache entries.
---
 doc/src/sgml/config.sgml                      |  38 ++++++
 src/backend/access/transam/xact.c             |   5 +
 src/backend/utils/cache/catcache.c            | 190 +++++++++++++++++++++++++-
 src/backend/utils/misc/guc.c                  |  63 +++++++++
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/utils/catcache.h                  |  32 ++++-
 6 files changed, 322 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 9b7a7388d5..d0d2374944 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1662,6 +1662,44 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-syscache-memory-target" xreflabel="syscache_memory_target">
+      <term><varname>syscache_memory_target</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>syscache_memory_target</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the maximum amount of memory to which syscache is expanded
+        without pruning. The value defaults to 0, indicating that pruning is
+        always considered. After exceeding this size, syscache pruning is
+        considered according to
+        <xref linkend="guc-syscache-prune-min-age"/>. If you need to keep
+        certain amount of syscache entries with intermittent usage, try
+        increase this setting.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-syscache-prune-min-age" xreflabel="syscache_prune_min_age">
+      <term><varname>syscache_prune_min_age</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>syscache_prune_min_age</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the minimum amount of unused time in seconds at which a
+        syscache entry is considered to be removed. -1 indicates that syscache
+        pruning is disabled at all. The value defaults to 600 seconds
+        (<literal>10 minutes</literal>). The syscache entries that are not
+        used for the duration can be removed to prevent syscache bloat. This
+        behavior is suppressed until the size of syscache exceeds
+        <xref linkend="guc-syscache-memory-target"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth">
       <term><varname>max_stack_depth</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 92bda87804..ddc433c59e 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -734,7 +734,12 @@ void
 SetCurrentStatementStartTimestamp(void)
 {
 	if (!IsParallelWorker())
+	{
 		stmtStartTimestamp = GetCurrentTimestamp();
+
+		/* Set this timestamp as aproximated current time */
+		SetCatCacheClock(stmtStartTimestamp);
+	}
 	else
 		Assert(stmtStartTimestamp != 0);
 }
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 258a1d64cc..0a56390352 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -71,9 +71,32 @@
 #define CACHE6_elog(a,b,c,d,e,f,g)
 #endif
 
+/*
+ * GUC variable to define the minimum size of hash to cosider entry eviction.
+ * This variable is shared among various cache mechanisms.
+ */
+int cache_memory_target = 0;
+
+
+/*
+ * GUC for entry limit. Entries are removed when the number of them goes above
+ * cache_entry_limit by the ratio specified by cache_entry_limit_prune_ratio
+ */
+int cache_entry_limit = 0;
+double cache_entry_limit_prune_ratio = 0.8;
+
+/* GUC variable to define the minimum age of entries that will be cosidered to
+ * be evicted in seconds. This variable is shared among various cache
+ * mechanisms.
+ */
+int cache_prune_min_age = 600;
+
 /* Cache management header --- pointer is NULL until created */
 static CatCacheHeader *CacheHdr = NULL;
 
+/* Timestamp used for any operation on caches. */
+TimestampTz	catcacheclock = 0;
+
 static inline HeapTuple SearchCatCacheInternal(CatCache *cache,
 					   int nkeys,
 					   Datum v1, Datum v2,
@@ -481,6 +504,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct)
 
 	/* delink from linked list */
 	dlist_delete(&ct->cache_elem);
+	dlist_delete(&ct->lru_node);
 
 	/*
 	 * Free keys when we're dealing with a negative entry, normal entries just
@@ -490,6 +514,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct)
 		CatCacheFreeKeys(cache->cc_tupdesc, cache->cc_nkeys,
 						 cache->cc_keyno, ct->keys);
 
+	cache->cc_tupsize -= ct->size;
 	pfree(ct);
 
 	--cache->cc_ntup;
@@ -841,7 +866,9 @@ InitCatCache(int id,
 	cp->cc_nkeys = nkeys;
 	for (i = 0; i < nkeys; ++i)
 		cp->cc_keyno[i] = key[i];
+	cp->cc_tupsize = 0;
 
+	dlist_init(&cp->cc_lru_list);
 	/*
 	 * new cache is initialized as far as we can go for now. print some
 	 * debugging information, if appropriate.
@@ -858,9 +885,133 @@ InitCatCache(int id,
 	 */
 	MemoryContextSwitchTo(oldcxt);
 
+	/* initilize catcache reference clock if haven't done yet */
+	if (catcacheclock == 0)
+		catcacheclock = GetCurrentTimestamp();
+
 	return cp;
 }
 
+/*
+ * CatCacheCleanupOldEntries - Remove infrequently-used entries
+ *
+ * Catcache entries can be left alone for several reasons. We remove them if
+ * they are not accessed for a certain time to prevent catcache from
+ * bloating. The eviction is performed with the similar algorithm with buffer
+ * eviction using access counter. Entries that are accessed several times can
+ * live longer than those that have had no access in the same duration.
+ */
+#define PRUNE_BY_AGE	0x01
+#define PRUNE_BY_NUMBER	0x02
+
+static bool
+CatCacheCleanupOldEntries(CatCache *cp)
+{
+	int			nremoved = 0;
+	size_t		hash_size;
+	int			nelems_before = cp->cc_ntup;
+	int			ndelelems = 0;
+	int			action = 0;
+	dlist_mutable_iter	iter;
+
+	if (cache_prune_min_age >= 0)
+	{
+		/* prune only if the size of the hash is above the target */
+
+		hash_size = cp->cc_nbuckets * sizeof(dlist_head);
+		if (hash_size + cp->cc_tupsize > (Size) cache_memory_target * 1024L)
+			action |= PRUNE_BY_AGE;
+	}
+
+	if (cache_entry_limit > 0 && nelems_before >= cache_entry_limit)
+	{
+		ndelelems = nelems_before -
+			(int) (cache_entry_limit * cache_entry_limit_prune_ratio);
+
+		if (ndelelems < 256)
+			ndelelems = 256;
+		if (ndelelems > nelems_before)
+			ndelelems = nelems_before;
+
+		action |= PRUNE_BY_NUMBER;
+	}
+
+	/* Return immediately if no pruning is wanted */
+	if (action == 0)
+		return false;
+
+	/* Scan over LRU to find entries to remove */
+	dlist_foreach_modify(iter, &cp->cc_lru_list)
+	{
+		CatCTup    *ct = dlist_container(CatCTup, lru_node, iter.cur);
+		bool		remove_this = false;
+
+		/* We don't remove referenced entry */
+		if (ct->refcount != 0 ||
+			(ct->c_list && ct->c_list->refcount != 0))
+			continue;
+
+		/* check against age */
+		if (action & PRUNE_BY_AGE)
+		{
+			long	entry_age;
+			int		us;
+
+			/*
+			 * Calculate the duration from the time of the last access to the
+			 * "current" time. Since catcacheclock is not advanced within a
+			 * transaction, the entries that are accessed within the current
+			 * transaction won't be pruned.
+			 */
+			TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us);
+
+			if (entry_age < cache_prune_min_age)
+			{
+				/* no longer have a business with further entries, exit  */
+				action &= ~PRUNE_BY_AGE;
+					break;
+			}
+
+			/*
+			 * Entries that are not accessed after last pruning are removed in
+			 * that seconds, and that has been accessed several times are
+			 * removed after leaving alone for up to three times of the
+			 * duration. We don't try shrink buckets since pruning effectively
+			 * caps catcache expansion in the long term.
+			 */
+			if (ct->naccess > 0)
+				ct->naccess--;
+			else 
+				remove_this = true;
+		}
+
+		/* check against entry number */
+		if (action & PRUNE_BY_NUMBER)
+		{
+			if (nremoved < ndelelems)
+				remove_this = true;
+			else
+				action &= ~PRUNE_BY_NUMBER; /* satisfied */
+		}
+
+		/* exit if finished */
+		if (action == 0)
+			break;
+
+		/* do the work */
+		if (remove_this)
+		{
+			CatCacheRemoveCTup(cp, ct);
+			nremoved++;
+		}
+	}
+
+	elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d",
+		 cp->id, cp->cc_relname, nremoved, nelems_before);
+
+	return nremoved > 0;
+}
+
 /*
  * Enlarge a catcache, doubling the number of buckets.
  */
@@ -1274,6 +1425,12 @@ SearchCatCacheInternal(CatCache *cache,
 		 */
 		dlist_move_head(bucket, &ct->cache_elem);
 
+		/* Update access information for pruning */
+		if (ct->naccess < 2)
+			ct->naccess++;
+		ct->lastaccess = catcacheclock;
+		dlist_move_tail(&cache->cc_lru_list, &ct->lru_node);
+
 		/*
 		 * If it's a positive entry, bump its refcount and return it. If it's
 		 * negative, we can report failure to the caller.
@@ -1819,11 +1976,13 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	CatCTup    *ct;
 	HeapTuple	dtp;
 	MemoryContext oldcxt;
+	int			tupsize = 0;
 
 	/* negative entries have no tuple associated */
 	if (ntp)
 	{
 		int			i;
+		int			tupsize;
 
 		Assert(!negative);
 
@@ -1842,13 +2001,14 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 		/* Allocate memory for CatCTup and the cached tuple in one go */
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
 
-		ct = (CatCTup *) palloc(sizeof(CatCTup) +
-								MAXIMUM_ALIGNOF + dtp->t_len);
+		tupsize = sizeof(CatCTup) +	MAXIMUM_ALIGNOF + dtp->t_len;
+		ct = (CatCTup *) palloc(tupsize);
 		ct->tuple.t_len = dtp->t_len;
 		ct->tuple.t_self = dtp->t_self;
 		ct->tuple.t_tableOid = dtp->t_tableOid;
 		ct->tuple.t_data = (HeapTupleHeader)
 			MAXALIGN(((char *) ct) + sizeof(CatCTup));
+		ct->size = tupsize;
 		/* copy tuple contents */
 		memcpy((char *) ct->tuple.t_data,
 			   (const char *) dtp->t_data,
@@ -1876,8 +2036,8 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	{
 		Assert(negative);
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
-		ct = (CatCTup *) palloc(sizeof(CatCTup));
-
+		tupsize = sizeof(CatCTup);
+		ct = (CatCTup *) palloc(tupsize);
 		/*
 		 * Store keys - they'll point into separately allocated memory if not
 		 * by-value.
@@ -1898,18 +2058,34 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	ct->dead = false;
 	ct->negative = negative;
 	ct->hash_value = hashValue;
+	ct->naccess = 0;
+	ct->lastaccess = catcacheclock;
+	dlist_push_tail(&cache->cc_lru_list, &ct->lru_node);
+	ct->size = tupsize;
 
 	dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem);
 
 	cache->cc_ntup++;
 	CacheHdr->ch_ntup++;
+	cache->cc_tupsize += tupsize;
+
+	/* increase refcount so that this survives pruning */
+	ct->refcount++;
 
 	/*
-	 * If the hash table has become too full, enlarge the buckets array. Quite
-	 * arbitrarily, we enlarge when fill factor > 2.
+	 * If the hash table has become too full, try cleanup by removing
+	 * infrequently used entries to make a room for the new entry. If it
+	 * failed, enlarge the bucket array instead.  Quite arbitrarily, we try
+	 * this when fill factor > 2.
 	 */
-	if (cache->cc_ntup > cache->cc_nbuckets * 2)
+	if (cache->cc_ntup > cache->cc_nbuckets * 2 &&
+		!CatCacheCleanupOldEntries(cache))
 		RehashCatCache(cache);
+	/* we may still want to prune by entry number, check it */
+	else if (cache_entry_limit > 0 && cache->cc_ntup > cache_entry_limit)
+		CatCacheCleanupOldEntries(cache);
+
+	ct->refcount--;
 
 	return ct;
 }
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 8681ada33a..d4df841982 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -81,6 +81,7 @@
 #include "tsearch/ts_cache.h"
 #include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/catcache.h"
 #include "utils/guc_tables.h"
 #include "utils/float.h"
 #include "utils/memutils.h"
@@ -2204,6 +2205,58 @@ static struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"cache_memory_target", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the minimum syscache size to keep."),
+			gettext_noop("Cache is not pruned before exceeding this size."),
+			GUC_UNIT_KB
+		},
+		&cache_memory_target,
+		0, 0, MAX_KILOBYTES,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"cache_prune_min_age", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the minimum unused duration of cache entries before removal."),
+			gettext_noop("Cache entries that live unused for longer than this seconds are considered to be removed."),
+			GUC_UNIT_S
+		},
+		&cache_prune_min_age,
+		600, -1, INT_MAX,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"cache_entry_limit", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the maximum entries of catcache."),
+		 	NULL
+		},
+		&cache_entry_limit,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"cache_entry_limit", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the maximum entries of catcache."),
+		 	NULL
+		},
+		&cache_entry_limit,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"cache_entry_limit", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the maximum entries of catcache."),
+		 	NULL
+		},
+		&cache_entry_limit,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	/*
 	 * We use the hopefully-safely-small value of 100kB as the compiled-in
 	 * default for max_stack_depth.  InitializeGUCOptions will increase it if
@@ -3368,6 +3421,16 @@ static struct config_real ConfigureNamesReal[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"cache_entry_limit_prune_ratio", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the maximum entries of catcache."),
+		 	NULL
+		},
+		&cache_entry_limit_prune_ratio,
+		0.8, 0.0, 1.0,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, 0.0, 0.0, 0.0, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index c7f53470df..108d332f2c 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -128,6 +128,8 @@
 #work_mem = 4MB				# min 64kB
 #maintenance_work_mem = 64MB		# min 1MB
 #autovacuum_work_mem = -1		# min 1MB, or -1 to use maintenance_work_mem
+#cache_memory_target = 0kB	# in kB
+#cache_prune_min_age = 600s	# -1 disables pruning
 #max_stack_depth = 2MB			# min 100kB
 #shared_memory_type = mmap		# the default is the first option
 					# supported by the operating system:
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h
index 65d816a583..973a87c2cf 100644
--- a/src/include/utils/catcache.h
+++ b/src/include/utils/catcache.h
@@ -22,6 +22,7 @@
 
 #include "access/htup.h"
 #include "access/skey.h"
+#include "datatype/timestamp.h"
 #include "lib/ilist.h"
 #include "utils/relcache.h"
 
@@ -61,6 +62,8 @@ typedef struct catcache
 	slist_node	cc_next;		/* list link */
 	ScanKeyData cc_skey[CATCACHE_MAXKEYS];	/* precomputed key info for heap
 											 * scans */
+	dlist_head	cc_lru_list;
+	int			cc_tupsize;		/* total amount of catcache tuples */
 
 	/*
 	 * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS
@@ -119,7 +122,10 @@ typedef struct catctup
 	bool		dead;			/* dead but not yet removed? */
 	bool		negative;		/* negative cache entry? */
 	HeapTupleData tuple;		/* tuple management header */
-
+	int			naccess;		/* # of access to this entry, up to 2  */
+	TimestampTz	lastaccess;		/* approx. timestamp of the last usage */
+	dlist_node	lru_node;		/* LRU node */
+	int			size;			/* palloc'ed size off this tuple */
 	/*
 	 * The tuple may also be a member of at most one CatCList.  (If a single
 	 * catcache is list-searched with varying numbers of keys, we may have to
@@ -189,6 +195,30 @@ typedef struct catcacheheader
 /* this extern duplicates utils/memutils.h... */
 extern PGDLLIMPORT MemoryContext CacheMemoryContext;
 
+/* for guc.c, not PGDLLPMPORT'ed */
+extern int cache_prune_min_age;
+extern int cache_memory_target;
+extern int cache_entry_limit;
+extern double cache_entry_limit_prune_ratio;
+
+/* to use as access timestamp of catcache entries */
+extern TimestampTz catcacheclock;
+
+/*
+ * SetCatCacheClock - set timestamp for catcache access record
+ */
+static inline void
+SetCatCacheClock(TimestampTz ts)
+{
+	catcacheclock = ts;
+}
+
+static inline TimestampTz
+GetCatCacheClock(void)
+{
+	return catcacheclock;
+}
+
 extern void CreateCacheMemoryContext(void);
 
 extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid,
-- 
2.16.3


----Next_Part(Thu_Feb_07_15_24_18_2019_171)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="v12-0003-Syscache-usage-tracking-feature.patch"



^ permalink  raw  reply  [nested|flat] 19+ messages in thread

* [PATCH 2/3] Remove entries that haven't been used for a certain time
@ 2018-10-16 04:04  Kyotaro Horiguchi <[email protected]>
  0 siblings, 0 replies; 19+ messages in thread

From: Kyotaro Horiguchi @ 2018-10-16 04:04 UTC (permalink / raw)

Catcache entries can be left alone for several reasons. It is not
desirable that they eat up memory. With this patch, This adds
consideration of removal of entries that haven't been used for a
certain time before enlarging the hash array.

This also can put a hard limit on the number of catcache entries.
---
 doc/src/sgml/config.sgml                      |  41 ++++
 src/backend/tcop/postgres.c                   |  13 ++
 src/backend/utils/cache/catcache.c            | 285 +++++++++++++++++++++++++-
 src/backend/utils/init/globals.c              |   1 +
 src/backend/utils/init/postinit.c             |  11 +
 src/backend/utils/misc/guc.c                  |  43 ++++
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/miscadmin.h                       |   1 +
 src/include/utils/catcache.h                  |  49 ++++-
 src/include/utils/timeout.h                   |   1 +
 10 files changed, 440 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 07b847a8e9..71d784b6fe 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1661,6 +1661,47 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-catalog-cache-prune-min-age" xreflabel="catalog_cache_prune_min_age">
+      <term><varname>catalog_cache_prune_min_age</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>catalog_cache_prune_min_age</varname> configuration
+       parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+
+        Specifies the minimum amount of unused time in seconds at which a
+        catalog cache entry is considered to be removed. -1 indicates that
+        this feature is disabled at all. The value defaults to 300 seconds
+        (<literal>5 minutes</literal>). The catalog cache entries that are
+        not used for the duration can be removed to prevent bloat. This
+        behavior is suppressed until the size of a catalog cache exceeds
+        <xref linkend="guc-catalog-cache-memory-target"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-catalog-cache-memory-target" xreflabel="catalog_cache_memory_target">
+      <term><varname>catalog_cache_memory_target</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>syscache_memory_target</varname> configuration
+       parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the maximum amount of memory to which syscache is expanded
+        without pruning in kilobytes. The value defaults to 0, indicating that
+        pruning is always considered. After exceeding this size, catalog cache
+        pruning is considered according to
+        <xref linkend="guc-catalog-cache-prune-min-age"/>. If you need to keep
+        certain amount of catalog cache entries with intermittent usage, try
+        increase this setting.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth">
       <term><varname>max_stack_depth</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 36cfd507b2..f192ee2ca6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -71,6 +71,7 @@
 #include "tcop/pquery.h"
 #include "tcop/tcopprot.h"
 #include "tcop/utility.h"
+#include "utils/catcache.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
@@ -2584,6 +2585,7 @@ start_xact_command(void)
 	 * not desired, the timeout has to be disabled explicitly.
 	 */
 	enable_statement_timeout();
+	SetCatCacheClock(GetCurrentStatementStartTimestamp());
 }
 
 static void
@@ -3159,6 +3161,14 @@ ProcessInterrupts(void)
 
 	if (ParallelMessagePending)
 		HandleParallelMessages();
+
+	if (CatcacheClockTimeoutPending)
+	{
+		CatcacheClockTimeoutPending = 0;
+
+		/* Update timetamp then set up the next timeout */
+		UpdateCatCacheClock();
+	}
 }
 
 
@@ -4021,6 +4031,9 @@ PostgresMain(int argc, char *argv[],
 		QueryCancelPending = false; /* second to avoid race condition */
 		stmt_timeout_active = false;
 
+		/* get sync with the timer state */
+		catcache_clock_timeout_active = false;
+
 		/* Not reading from the client anymore. */
 		DoingCommandRead = false;
 
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 258a1d64cc..0195e19976 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -39,6 +39,7 @@
 #include "utils/rel.h"
 #include "utils/resowner_private.h"
 #include "utils/syscache.h"
+#include "utils/timeout.h"
 
 
  /* #define CACHEDEBUG */	/* turns DEBUG elogs on */
@@ -71,9 +72,43 @@
 #define CACHE6_elog(a,b,c,d,e,f,g)
 #endif
 
+/* GUC variable to define the minimum age of entries that will be considered to
+ * be evicted in seconds. This variable is shared among various cache
+ * mechanisms.
+ */
+int catalog_cache_prune_min_age = 300;
+
+/*
+ * GUC variable to define the minimum size of hash to cosider entry eviction.
+ * This variable is shared among various cache mechanisms.
+ */
+int catalog_cache_memory_target = 0;
+
+/*
+ * GUC for limit by the number of entries. Entries are removed when the number
+ * of them goes above catalog_cache_entry_limit and leaving newer entries by
+ * the ratio specified by catalog_cache_prune_ratio.
+ */
+int catalog_cache_entry_limit = 0;
+double catalog_cache_prune_ratio = 0.8;
+
+/*
+ * Flag to keep track of whether catcache clock timer is active.
+ */
+bool catcache_clock_timeout_active = false;
+
+/*
+ * Minimum interval between two success move of a cache entry in LRU list,
+ * in microseconds.
+ */
+#define MIN_LRU_UPDATE_INTERVAL 100000	/* 100ms */
+
 /* Cache management header --- pointer is NULL until created */
 static CatCacheHeader *CacheHdr = NULL;
 
+/* Clock used to record the last accessed time of a catcache record. */
+TimestampTz	catcacheclock = 0;
+
 static inline HeapTuple SearchCatCacheInternal(CatCache *cache,
 					   int nkeys,
 					   Datum v1, Datum v2,
@@ -481,6 +516,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct)
 
 	/* delink from linked list */
 	dlist_delete(&ct->cache_elem);
+	dlist_delete(&ct->lru_node);
 
 	/*
 	 * Free keys when we're dealing with a negative entry, normal entries just
@@ -490,6 +526,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct)
 		CatCacheFreeKeys(cache->cc_tupdesc, cache->cc_nkeys,
 						 cache->cc_keyno, ct->keys);
 
+	cache->cc_memusage -= ct->size;
 	pfree(ct);
 
 	--cache->cc_ntup;
@@ -841,7 +878,13 @@ InitCatCache(int id,
 	cp->cc_nkeys = nkeys;
 	for (i = 0; i < nkeys; ++i)
 		cp->cc_keyno[i] = key[i];
+	cp->cc_memusage =
+		CacheMemoryContext->methods->get_chunk_space(CacheMemoryContext,
+													 cp) +
+		CacheMemoryContext->methods->get_chunk_space(CacheMemoryContext,
+													 cp->cc_bucket);
 
+	dlist_init(&cp->cc_lru_list);
 	/*
 	 * new cache is initialized as far as we can go for now. print some
 	 * debugging information, if appropriate.
@@ -858,9 +901,191 @@ InitCatCache(int id,
 	 */
 	MemoryContextSwitchTo(oldcxt);
 
+	/* initialize catcache reference clock if haven't done yet */
+	if (catcacheclock == 0)
+		catcacheclock = GetCurrentTimestamp();
+
 	return cp;
 }
 
+/*
+ * helper routine for SetCatCacheClock and UpdateCatCacheClockTimer.
+ *
+ * We need to maintain the catcache clock during a long query.
+ */
+void
+SetupCatCacheClockTimer(void)
+{
+	long delay;
+
+	/* stop timer if not needed */
+	if (catalog_cache_prune_min_age == 0)
+	{
+		catcache_clock_timeout_active = false;
+		return;
+	}
+
+	/* One 10th of the variable, in milliseconds */
+	delay  = catalog_cache_prune_min_age * 1000/10;
+
+	/* Lower limit is 1 second */
+	if (delay < 1000)
+		delay = 1000;
+
+	enable_timeout_after(CATCACHE_CLOCK_TIMEOUT, delay);
+
+	catcache_clock_timeout_active = true;
+}
+
+/*
+ * Update catcacheclock: this is intended to be called from
+ * CATCACHE_CLOCK_TIMEOUT. The interval is expected more than 1 second (see
+ * above), so GetCurrentTime() doesn't harm.
+ */
+void
+UpdateCatCacheClock(void)
+{
+	catcacheclock = GetCurrentTimestamp();
+	SetupCatCacheClockTimer();
+}
+
+/*
+ * It may take an unexpectedly long time before the next clock update when
+ * catalog_cache_prune_min_age gets shorter. Disabling the current timer let
+ * the next update happen at the expected interval. We don't necessariry
+ * require this for increase the age but we don't need to avoid to disable
+ * either.
+ */
+void
+assign_catalog_cache_prune_min_age(int newval, void *extra)
+{
+	if (catcache_clock_timeout_active)
+		disable_timeout(CATCACHE_CLOCK_TIMEOUT, false);
+
+	catcache_clock_timeout_active = false;
+}
+
+/*
+ * CatCacheCleanupOldEntries - Remove infrequently-used entries
+ *
+ * Catcache entries can be left alone for several reasons. We remove them if
+ * they are not accessed for a certain time to prevent catcache from
+ * bloating. The eviction is performed with the similar algorithm with buffer
+ * eviction using access counter. Entries that are accessed several times can
+ * live longer than those that have had less access in the same duration.
+ */
+static bool
+CatCacheCleanupOldEntries(CatCache *cp)
+{
+	int			nremoved = 0;
+	size_t		hash_size;
+	int			nelems_before = cp->cc_ntup;
+	int			ndelelems = 0;
+	bool		prune_by_age = false;
+	bool		prune_by_number = false;
+	dlist_mutable_iter	iter;
+
+	if (catalog_cache_prune_min_age >= 0)
+	{
+		/* prune only if the size of the hash is above the target */
+
+		hash_size = cp->cc_nbuckets * sizeof(dlist_head);
+		if (hash_size + cp->cc_memusage >
+			(Size) catalog_cache_memory_target * 1024L)
+			prune_by_age = true;
+	}
+
+	if (catalog_cache_entry_limit > 0 &&
+		nelems_before >= catalog_cache_entry_limit)
+	{
+		ndelelems = nelems_before -
+			(int) (catalog_cache_entry_limit * catalog_cache_prune_ratio);
+
+		/* an arbitrary lower limit.. */
+		if (ndelelems < 256)
+			ndelelems = 256;
+		if (ndelelems > nelems_before)
+			ndelelems = nelems_before;
+
+		prune_by_number = true;
+	}
+
+	/* Return immediately if no pruning is wanted */
+	if (!prune_by_age && !prune_by_number)
+		return false;
+
+	/* Scan over LRU to find entries to remove */
+	dlist_foreach_modify(iter, &cp->cc_lru_list)
+	{
+		CatCTup    *ct = dlist_container(CatCTup, lru_node, iter.cur);
+		bool		remove_this = false;
+
+		/* We don't remove referenced entry */
+		if (ct->refcount != 0 ||
+			(ct->c_list && ct->c_list->refcount != 0))
+			continue;
+
+		/* check against age */
+		if (prune_by_age)
+		{
+			long	entry_age;
+			int		us;
+
+			/*
+			 * Calculate the duration from the time of the last access to the
+			 * "current" time. Since catcacheclock is not advanced within a
+			 * transaction, the entries that are accessed within the current
+			 * transaction won't be pruned.
+			 */
+			TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us);
+
+			if (entry_age < catalog_cache_prune_min_age)
+			{
+				/* no longer have a business with further entries, exit  */
+				prune_by_age = false;
+				break;
+			}
+			/*
+			 * Entries that are not accessed after last pruning are removed in
+			 * that seconds, and that has been accessed several times are
+			 * removed after leaving alone for up to three times of the
+			 * duration. We don't try shrink buckets since pruning effectively
+			 * caps catcache expansion in the long term.
+			 */
+			if (ct->naccess > 0)
+				ct->naccess--;
+			else 
+				remove_this = true;
+		}
+
+		/* check against entry number */
+		if (prune_by_number)
+		{
+			if (nremoved < ndelelems)
+				remove_this = true;
+			else
+				prune_by_number = false; /* we're satisfied */
+		}
+
+		/* exit immediately if all finished */
+		if (!prune_by_age && !prune_by_number)
+			break;
+
+		/* do the work */
+		if (remove_this)
+		{
+			CatCacheRemoveCTup(cp, ct);
+			nremoved++;
+		}
+	}
+
+	if (nremoved > 0)
+		elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d",
+			 cp->id, cp->cc_relname, nremoved, nelems_before);
+
+	return nremoved > 0;
+}
+
 /*
  * Enlarge a catcache, doubling the number of buckets.
  */
@@ -878,6 +1103,13 @@ RehashCatCache(CatCache *cp)
 	newnbuckets = cp->cc_nbuckets * 2;
 	newbucket = (dlist_head *) MemoryContextAllocZero(CacheMemoryContext, newnbuckets * sizeof(dlist_head));
 
+	/* recalculate memory usage from the first */
+	cp->cc_memusage =
+		CacheMemoryContext->methods->get_chunk_space(CacheMemoryContext,
+													 cp) +
+		CacheMemoryContext->methods->get_chunk_space(CacheMemoryContext,
+													 newbucket);
+
 	/* Move all entries from old hash table to new. */
 	for (i = 0; i < cp->cc_nbuckets; i++)
 	{
@@ -890,6 +1122,7 @@ RehashCatCache(CatCache *cp)
 
 			dlist_delete(iter.cur);
 			dlist_push_head(&newbucket[hashIndex], &ct->cache_elem);
+			cp->cc_memusage += ct->size;
 		}
 	}
 
@@ -1274,6 +1507,21 @@ SearchCatCacheInternal(CatCache *cache,
 		 */
 		dlist_move_head(bucket, &ct->cache_elem);
 
+		/* Update access information for pruning */
+		if (ct->naccess < 2)
+			ct->naccess++;
+
+		/*
+		 * We don't want too frequent update of
+		 * LRU. catalog_cache_prune_min_age can be changed on-session so we
+		 * need to maintain the LRU regardless of catalog_cache_prune_min_age.
+		 */
+		if (catcacheclock - ct->lastaccess > MIN_LRU_UPDATE_INTERVAL)
+		{
+			ct->lastaccess = catcacheclock;
+			dlist_move_tail(&cache->cc_lru_list, &ct->lru_node);
+		}
+
 		/*
 		 * If it's a positive entry, bump its refcount and return it. If it's
 		 * negative, we can report failure to the caller.
@@ -1709,6 +1957,11 @@ SearchCatCacheList(CatCache *cache,
 		/* Now we can build the CatCList entry. */
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
 		nmembers = list_length(ctlist);
+
+		/*
+		 * Don't waste a time by counting the list in catcache memory usage,
+		 * since it doesn't live a long life.
+		 */
 		cl = (CatCList *)
 			palloc(offsetof(CatCList, members) + nmembers * sizeof(CatCTup *));
 
@@ -1824,6 +2077,7 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	if (ntp)
 	{
 		int			i;
+		int			tupsize;
 
 		Assert(!negative);
 
@@ -1842,8 +2096,8 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 		/* Allocate memory for CatCTup and the cached tuple in one go */
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
 
-		ct = (CatCTup *) palloc(sizeof(CatCTup) +
-								MAXIMUM_ALIGNOF + dtp->t_len);
+		tupsize = sizeof(CatCTup) +	MAXIMUM_ALIGNOF + dtp->t_len;
+		ct = (CatCTup *) palloc(tupsize);
 		ct->tuple.t_len = dtp->t_len;
 		ct->tuple.t_self = dtp->t_self;
 		ct->tuple.t_tableOid = dtp->t_tableOid;
@@ -1877,7 +2131,6 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 		Assert(negative);
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
 		ct = (CatCTup *) palloc(sizeof(CatCTup));
-
 		/*
 		 * Store keys - they'll point into separately allocated memory if not
 		 * by-value.
@@ -1898,18 +2151,38 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	ct->dead = false;
 	ct->negative = negative;
 	ct->hash_value = hashValue;
+	ct->naccess = 0;
+	ct->lastaccess = catcacheclock;
+	dlist_push_tail(&cache->cc_lru_list, &ct->lru_node);
 
 	dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem);
 
 	cache->cc_ntup++;
 	CacheHdr->ch_ntup++;
 
+	ct->size = 
+		CacheMemoryContext->methods->get_chunk_space(CacheMemoryContext,
+														 ct);
+	cache->cc_memusage += ct->size;
+
+	/* increase refcount so that this survives pruning */
+	ct->refcount++;
+
 	/*
-	 * If the hash table has become too full, enlarge the buckets array. Quite
-	 * arbitrarily, we enlarge when fill factor > 2.
+	 * If the hash table has become too full, try cleanup by removing
+	 * infrequently used entries to make a room for the new entry. If it
+	 * failed, enlarge the bucket array instead.  Quite arbitrarily, we try
+	 * this when fill factor > 2.
 	 */
-	if (cache->cc_ntup > cache->cc_nbuckets * 2)
+	if (cache->cc_ntup > cache->cc_nbuckets * 2 &&
+		!CatCacheCleanupOldEntries(cache))
 		RehashCatCache(cache);
+	/* we may still want to prune by entry number, check it */
+	else if (catalog_cache_entry_limit > 0 &&
+			 cache->cc_ntup > catalog_cache_entry_limit)
+		CatCacheCleanupOldEntries(cache);
+
+	ct->refcount--;
 
 	return ct;
 }
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index fd51934aaf..0e8b972a29 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -32,6 +32,7 @@ volatile sig_atomic_t QueryCancelPending = false;
 volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t CatcacheClockTimeoutPending = false;
 volatile sig_atomic_t ConfigReloadPending = false;
 volatile uint32 InterruptHoldoffCount = 0;
 volatile uint32 QueryCancelHoldoffCount = 0;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index a5ee209f91..9eb50e9676 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -72,6 +72,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void CatcacheClockTimeoutHandler(void);
 static bool ThereIsAtLeastOneRole(void);
 static void process_startup_options(Port *port, bool am_superuser);
 static void process_settings(Oid databaseid, Oid roleid);
@@ -628,6 +629,8 @@ InitPostgres(const char *in_dbname, Oid dboid, const char *username,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(CATCACHE_CLOCK_TIMEOUT,
+						CatcacheClockTimeoutHandler);
 	}
 
 	/*
@@ -1238,6 +1241,14 @@ IdleInTransactionSessionTimeoutHandler(void)
 	SetLatch(MyLatch);
 }
 
+static void
+CatcacheClockTimeoutHandler(void)
+{
+	CatcacheClockTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 /*
  * Returns true if at least one role is defined in this database cluster.
  */
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 41d477165c..c62d5ad8b8 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -81,6 +81,7 @@
 #include "tsearch/ts_cache.h"
 #include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/catcache.h"
 #include "utils/guc_tables.h"
 #include "utils/float.h"
 #include "utils/memutils.h"
@@ -2205,6 +2206,38 @@ static struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"catalog_cache_prune_min_age", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the minimum unused duration of cache entries before removal."),
+			gettext_noop("Catalog cache entries that live unused for longer than this seconds are considered to be removed."),
+			GUC_UNIT_S
+		},
+		&catalog_cache_prune_min_age,
+		300, -1, INT_MAX,
+		NULL, assign_catalog_cache_prune_min_age, NULL
+	},
+
+	{
+		{"catalog_cache_memory_target", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the minimum syscache size to keep."),
+			gettext_noop("Time-based cache pruning starts working after exceeding this size."),
+			GUC_UNIT_KB
+		},
+		&catalog_cache_memory_target,
+		0, 0, MAX_KILOBYTES,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"catalog_cache_entry_limit", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the maximum entries of catcache."),
+		 	NULL
+		},
+		&catalog_cache_entry_limit,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	/*
 	 * We use the hopefully-safely-small value of 100kB as the compiled-in
 	 * default for max_stack_depth.  InitializeGUCOptions will increase it if
@@ -3368,6 +3401,16 @@ static struct config_real ConfigureNamesReal[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"catalog_cache_prune_ratio", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Reduce ratio of pruning caused by catalog_cache_entry_limit."),
+		 	NULL
+		},
+		&catalog_cache_prune_ratio,
+		0.8, 0.0, 1.0,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, 0.0, 0.0, 0.0, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index ad6c436f93..aeb5968e75 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -128,6 +128,8 @@
 #work_mem = 4MB				# min 64kB
 #maintenance_work_mem = 64MB		# min 1MB
 #autovacuum_work_mem = -1		# min 1MB, or -1 to use maintenance_work_mem
+#catalog_cache_memory_target = 0kB	# in kB
+#catalog_cache_prune_min_age = 300s	# -1 disables pruning
 #max_stack_depth = 2MB			# min 100kB
 #shared_memory_type = mmap		# the default is the first option
 					# supported by the operating system:
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index c9e35003a5..33b800e80f 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -82,6 +82,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t CatcacheClockTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ConfigReloadPending;
 
 extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost;
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h
index 65d816a583..0425fc0786 100644
--- a/src/include/utils/catcache.h
+++ b/src/include/utils/catcache.h
@@ -22,6 +22,7 @@
 
 #include "access/htup.h"
 #include "access/skey.h"
+#include "datatype/timestamp.h"
 #include "lib/ilist.h"
 #include "utils/relcache.h"
 
@@ -61,6 +62,10 @@ typedef struct catcache
 	slist_node	cc_next;		/* list link */
 	ScanKeyData cc_skey[CATCACHE_MAXKEYS];	/* precomputed key info for heap
 											 * scans */
+	dlist_head	cc_lru_list;
+	int			cc_memusage;	/* memory usage of this catcache (excluding
+								 * header part) */
+	int			cc_nfreeent;	/* # of entries currently not referenced */
 
 	/*
 	 * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS
@@ -119,7 +124,10 @@ typedef struct catctup
 	bool		dead;			/* dead but not yet removed? */
 	bool		negative;		/* negative cache entry? */
 	HeapTupleData tuple;		/* tuple management header */
-
+	int			naccess;		/* # of access to this entry, up to 2  */
+	TimestampTz	lastaccess;		/* approx. timestamp of the last usage */
+	dlist_node	lru_node;		/* LRU node */
+	int			size;			/* palloc'ed size off this tuple */
 	/*
 	 * The tuple may also be a member of at most one CatCList.  (If a single
 	 * catcache is list-searched with varying numbers of keys, we may have to
@@ -189,6 +197,45 @@ typedef struct catcacheheader
 /* this extern duplicates utils/memutils.h... */
 extern PGDLLIMPORT MemoryContext CacheMemoryContext;
 
+/* for guc.c, not PGDLLPMPORT'ed */
+extern int catalog_cache_prune_min_age;
+extern int catalog_cache_memory_target;
+extern int catalog_cache_entry_limit;
+extern double catalog_cache_prune_ratio;
+
+/* to use as access timestamp of catcache entries */
+extern TimestampTz catcacheclock;
+
+/*
+ * Flag to keep track of whether catcache timestamp timer is active.
+ */
+extern bool catcache_clock_timeout_active;
+
+/* catcache prune time helper functions  */
+extern void SetupCatCacheClockTimer(void);
+extern void UpdateCatCacheClock(void);
+
+/*
+ * SetCatCacheClock - set timestamp for catcache access record and start
+ * maintenance timer if needed. We keep to update the clock even while pruning
+ * is disable so that we are not confused by bogus clock value.
+ */
+static inline void
+SetCatCacheClock(TimestampTz ts)
+{
+	catcacheclock = ts;
+
+	if (!catcache_clock_timeout_active && catalog_cache_prune_min_age > 0)
+		SetupCatCacheClockTimer();
+}
+
+static inline TimestampTz
+GetCatCacheClock(void)
+{
+	return catcacheclock;
+}
+
+extern void assign_catalog_cache_prune_min_age(int newval, void *extra);
 extern void CreateCacheMemoryContext(void);
 
 extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid,
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 9244a2a7b7..b2d97b4f7b 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	CATCACHE_CLOCK_TIMEOUT,
 	/* First user-definable timeout reason */
 	USER_TIMEOUT,
 	/* Maximum number of timeout reasons */
-- 
2.16.3


----Next_Part(Tue_Feb_12_20_36_28_2019_578)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="v14-0003-Syscache-usage-tracking-feature.patch"



^ permalink  raw  reply  [nested|flat] 19+ messages in thread

* [PATCH 2/4] Remove entries that haven't been used for a certain time
@ 2018-10-16 04:04  Kyotaro Horiguchi <[email protected]>
  0 siblings, 0 replies; 19+ messages in thread

From: Kyotaro Horiguchi @ 2018-10-16 04:04 UTC (permalink / raw)

Catcache entries can be left alone for several reasons. It is not
desirable that they eat up memory. With this patch, This adds
consideration of removal of entries that haven't been used for a
certain time before enlarging the hash array.

This also can put a hard limit on the number of catcache entries.
---
 doc/src/sgml/config.sgml                      |  38 +++++
 src/backend/access/transam/xact.c             |   5 +
 src/backend/utils/cache/catcache.c            | 205 +++++++++++++++++++++++++-
 src/backend/utils/misc/guc.c                  |  63 ++++++++
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/utils/catcache.h                  |  33 ++++-
 6 files changed, 338 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 9b7a7388d5..d0d2374944 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1662,6 +1662,44 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-syscache-memory-target" xreflabel="syscache_memory_target">
+      <term><varname>syscache_memory_target</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>syscache_memory_target</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the maximum amount of memory to which syscache is expanded
+        without pruning. The value defaults to 0, indicating that pruning is
+        always considered. After exceeding this size, syscache pruning is
+        considered according to
+        <xref linkend="guc-syscache-prune-min-age"/>. If you need to keep
+        certain amount of syscache entries with intermittent usage, try
+        increase this setting.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-syscache-prune-min-age" xreflabel="syscache_prune_min_age">
+      <term><varname>syscache_prune_min_age</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>syscache_prune_min_age</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the minimum amount of unused time in seconds at which a
+        syscache entry is considered to be removed. -1 indicates that syscache
+        pruning is disabled at all. The value defaults to 600 seconds
+        (<literal>10 minutes</literal>). The syscache entries that are not
+        used for the duration can be removed to prevent syscache bloat. This
+        behavior is suppressed until the size of syscache exceeds
+        <xref linkend="guc-syscache-memory-target"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth">
       <term><varname>max_stack_depth</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 92bda87804..ddc433c59e 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -734,7 +734,12 @@ void
 SetCurrentStatementStartTimestamp(void)
 {
 	if (!IsParallelWorker())
+	{
 		stmtStartTimestamp = GetCurrentTimestamp();
+
+		/* Set this timestamp as aproximated current time */
+		SetCatCacheClock(stmtStartTimestamp);
+	}
 	else
 		Assert(stmtStartTimestamp != 0);
 }
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 258a1d64cc..c70ce3b745 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -71,9 +71,38 @@
 #define CACHE6_elog(a,b,c,d,e,f,g)
 #endif
 
+/*
+ * GUC variable to define the minimum size of hash to cosider entry eviction.
+ * This variable is shared among various cache mechanisms.
+ */
+int cache_memory_target = 0;
+
+
+/*
+ * GUC for entry limit. Entries are removed when the number of them goes above
+ * cache_entry_limit by the ratio specified by cache_entry_limit_prune_ratio
+ */
+int cache_entry_limit = 0;
+double cache_entry_limit_prune_ratio = 0.8;
+
+/* GUC variable to define the minimum age of entries that will be cosidered to
+ * be evicted in seconds. This variable is shared among various cache
+ * mechanisms.
+ */
+int cache_prune_min_age = 600;
+
+/*
+ * Ignorance interval between two success move of a cache entry in LRU list,
+ * in microseconds.
+ */
+#define LRU_IGNORANCE_INTERVAL 100000	/* 100ms */
+
 /* Cache management header --- pointer is NULL until created */
 static CatCacheHeader *CacheHdr = NULL;
 
+/* Timestamp used for any operation on caches. */
+TimestampTz	catcacheclock = 0;
+
 static inline HeapTuple SearchCatCacheInternal(CatCache *cache,
 					   int nkeys,
 					   Datum v1, Datum v2,
@@ -481,6 +510,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct)
 
 	/* delink from linked list */
 	dlist_delete(&ct->cache_elem);
+	dlist_delete(&ct->lru_node);
 
 	/*
 	 * Free keys when we're dealing with a negative entry, normal entries just
@@ -490,6 +520,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct)
 		CatCacheFreeKeys(cache->cc_tupdesc, cache->cc_nkeys,
 						 cache->cc_keyno, ct->keys);
 
+	cache->cc_tupsize -= ct->size;
 	pfree(ct);
 
 	--cache->cc_ntup;
@@ -841,7 +872,9 @@ InitCatCache(int id,
 	cp->cc_nkeys = nkeys;
 	for (i = 0; i < nkeys; ++i)
 		cp->cc_keyno[i] = key[i];
+	cp->cc_tupsize = 0;
 
+	dlist_init(&cp->cc_lru_list);
 	/*
 	 * new cache is initialized as far as we can go for now. print some
 	 * debugging information, if appropriate.
@@ -858,9 +891,133 @@ InitCatCache(int id,
 	 */
 	MemoryContextSwitchTo(oldcxt);
 
+	/* initilize catcache reference clock if haven't done yet */
+	if (catcacheclock == 0)
+		catcacheclock = GetCurrentTimestamp();
+
 	return cp;
 }
 
+/*
+ * CatCacheCleanupOldEntries - Remove infrequently-used entries
+ *
+ * Catcache entries can be left alone for several reasons. We remove them if
+ * they are not accessed for a certain time to prevent catcache from
+ * bloating. The eviction is performed with the similar algorithm with buffer
+ * eviction using access counter. Entries that are accessed several times can
+ * live longer than those that have had no access in the same duration.
+ */
+#define PRUNE_BY_AGE	0x01
+#define PRUNE_BY_NUMBER	0x02
+
+static bool
+CatCacheCleanupOldEntries(CatCache *cp)
+{
+	int			nremoved = 0;
+	size_t		hash_size;
+	int			nelems_before = cp->cc_ntup;
+	int			ndelelems = 0;
+	int			action = 0;
+	dlist_mutable_iter	iter;
+
+	if (cache_prune_min_age >= 0)
+	{
+		/* prune only if the size of the hash is above the target */
+
+		hash_size = cp->cc_nbuckets * sizeof(dlist_head);
+		if (hash_size + cp->cc_tupsize > (Size) cache_memory_target * 1024L)
+			action |= PRUNE_BY_AGE;
+	}
+
+	if (cache_entry_limit > 0 && nelems_before >= cache_entry_limit)
+	{
+		ndelelems = nelems_before -
+			(int) (cache_entry_limit * cache_entry_limit_prune_ratio);
+
+		if (ndelelems < 256)
+			ndelelems = 256;
+		if (ndelelems > nelems_before)
+			ndelelems = nelems_before;
+
+		action |= PRUNE_BY_NUMBER;
+	}
+
+	/* Return immediately if no pruning is wanted */
+	if (action == 0)
+		return false;
+
+	/* Scan over LRU to find entries to remove */
+	dlist_foreach_modify(iter, &cp->cc_lru_list)
+	{
+		CatCTup    *ct = dlist_container(CatCTup, lru_node, iter.cur);
+		bool		remove_this = false;
+
+		/* We don't remove referenced entry */
+		if (ct->refcount != 0 ||
+			(ct->c_list && ct->c_list->refcount != 0))
+			continue;
+
+		/* check against age */
+		if (action & PRUNE_BY_AGE)
+		{
+			long	entry_age;
+			int		us;
+
+			/*
+			 * Calculate the duration from the time of the last access to the
+			 * "current" time. Since catcacheclock is not advanced within a
+			 * transaction, the entries that are accessed within the current
+			 * transaction won't be pruned.
+			 */
+			TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us);
+
+			if (entry_age < cache_prune_min_age)
+			{
+				/* no longer have a business with further entries, exit  */
+				action &= ~PRUNE_BY_AGE;
+					break;
+			}
+
+			/*
+			 * Entries that are not accessed after last pruning are removed in
+			 * that seconds, and that has been accessed several times are
+			 * removed after leaving alone for up to three times of the
+			 * duration. We don't try shrink buckets since pruning effectively
+			 * caps catcache expansion in the long term.
+			 */
+			if (ct->naccess > 0)
+				ct->naccess--;
+			else 
+				remove_this = true;
+		}
+
+		/* check against entry number */
+		if (action & PRUNE_BY_NUMBER)
+		{
+			if (nremoved < ndelelems)
+				remove_this = true;
+			else
+				action &= ~PRUNE_BY_NUMBER; /* satisfied */
+		}
+
+		/* exit if finished */
+		if (action == 0)
+			break;
+
+		/* do the work */
+		if (remove_this)
+		{
+			CatCacheRemoveCTup(cp, ct);
+			nremoved++;
+		}
+	}
+
+	elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d",
+		 cp->id, cp->cc_relname, nremoved, nelems_before);
+
+	return nremoved > 0;
+}
+
 /*
  * Enlarge a catcache, doubling the number of buckets.
  */
@@ -1274,6 +1431,21 @@ SearchCatCacheInternal(CatCache *cache,
 		 */
 		dlist_move_head(bucket, &ct->cache_elem);
 
+		/* Update access information for pruning */
+		if (ct->naccess < 2)
+			ct->naccess++;
+
+		/*
+		 * We don't want too frequent update of LRU. cache_prune_min_age can
+		 * be changed on-session so we need to maintan the LRU regardless of
+		 * cache_prune_min_age.
+		 */
+		if (catcacheclock - ct->lastaccess > LRU_IGNORANCE_INTERVAL)
+		{
+			ct->lastaccess = catcacheclock;
+			dlist_move_tail(&cache->cc_lru_list, &ct->lru_node);
+		}
+
 		/*
 		 * If it's a positive entry, bump its refcount and return it. If it's
 		 * negative, we can report failure to the caller.
@@ -1819,11 +1991,13 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	CatCTup    *ct;
 	HeapTuple	dtp;
 	MemoryContext oldcxt;
+	int			tupsize = 0;
 
 	/* negative entries have no tuple associated */
 	if (ntp)
 	{
 		int			i;
+		int			tupsize;
 
 		Assert(!negative);
 
@@ -1842,13 +2016,14 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 		/* Allocate memory for CatCTup and the cached tuple in one go */
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
 
-		ct = (CatCTup *) palloc(sizeof(CatCTup) +
-								MAXIMUM_ALIGNOF + dtp->t_len);
+		tupsize = sizeof(CatCTup) +	MAXIMUM_ALIGNOF + dtp->t_len;
+		ct = (CatCTup *) palloc(tupsize);
 		ct->tuple.t_len = dtp->t_len;
 		ct->tuple.t_self = dtp->t_self;
 		ct->tuple.t_tableOid = dtp->t_tableOid;
 		ct->tuple.t_data = (HeapTupleHeader)
 			MAXALIGN(((char *) ct) + sizeof(CatCTup));
+		ct->size = tupsize;
 		/* copy tuple contents */
 		memcpy((char *) ct->tuple.t_data,
 			   (const char *) dtp->t_data,
@@ -1876,8 +2051,8 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	{
 		Assert(negative);
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
-		ct = (CatCTup *) palloc(sizeof(CatCTup));
-
+		tupsize = sizeof(CatCTup);
+		ct = (CatCTup *) palloc(tupsize);
 		/*
 		 * Store keys - they'll point into separately allocated memory if not
 		 * by-value.
@@ -1898,18 +2073,34 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	ct->dead = false;
 	ct->negative = negative;
 	ct->hash_value = hashValue;
+	ct->naccess = 0;
+	ct->lastaccess = catcacheclock;
+	dlist_push_tail(&cache->cc_lru_list, &ct->lru_node);
+	ct->size = tupsize;
 
 	dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem);
 
 	cache->cc_ntup++;
 	CacheHdr->ch_ntup++;
+	cache->cc_tupsize += tupsize;
+
+	/* increase refcount so that this survives pruning */
+	ct->refcount++;
 
 	/*
-	 * If the hash table has become too full, enlarge the buckets array. Quite
-	 * arbitrarily, we enlarge when fill factor > 2.
+	 * If the hash table has become too full, try cleanup by removing
+	 * infrequently used entries to make a room for the new entry. If it
+	 * failed, enlarge the bucket array instead.  Quite arbitrarily, we try
+	 * this when fill factor > 2.
 	 */
-	if (cache->cc_ntup > cache->cc_nbuckets * 2)
+	if (cache->cc_ntup > cache->cc_nbuckets * 2 &&
+		!CatCacheCleanupOldEntries(cache))
 		RehashCatCache(cache);
+	/* we may still want to prune by entry number, check it */
+	else if (cache_entry_limit > 0 && cache->cc_ntup > cache_entry_limit)
+		CatCacheCleanupOldEntries(cache);
+
+	ct->refcount--;
 
 	return ct;
 }
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 8681ada33a..d4df841982 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -81,6 +81,7 @@
 #include "tsearch/ts_cache.h"
 #include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/catcache.h"
 #include "utils/guc_tables.h"
 #include "utils/float.h"
 #include "utils/memutils.h"
@@ -2204,6 +2205,58 @@ static struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"cache_memory_target", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the minimum syscache size to keep."),
+			gettext_noop("Cache is not pruned before exceeding this size."),
+			GUC_UNIT_KB
+		},
+		&cache_memory_target,
+		0, 0, MAX_KILOBYTES,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"cache_prune_min_age", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the minimum unused duration of cache entries before removal."),
+			gettext_noop("Cache entries that live unused for longer than this seconds are considered to be removed."),
+			GUC_UNIT_S
+		},
+		&cache_prune_min_age,
+		600, -1, INT_MAX,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"cache_entry_limit", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the maximum entries of catcache."),
+		 	NULL
+		},
+		&cache_entry_limit,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"cache_entry_limit", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the maximum entries of catcache."),
+		 	NULL
+		},
+		&cache_entry_limit,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"cache_entry_limit", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the maximum entries of catcache."),
+		 	NULL
+		},
+		&cache_entry_limit,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	/*
 	 * We use the hopefully-safely-small value of 100kB as the compiled-in
 	 * default for max_stack_depth.  InitializeGUCOptions will increase it if
@@ -3368,6 +3421,16 @@ static struct config_real ConfigureNamesReal[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"cache_entry_limit_prune_ratio", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the maximum entries of catcache."),
+		 	NULL
+		},
+		&cache_entry_limit_prune_ratio,
+		0.8, 0.0, 1.0,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, 0.0, 0.0, 0.0, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index c7f53470df..108d332f2c 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -128,6 +128,8 @@
 #work_mem = 4MB				# min 64kB
 #maintenance_work_mem = 64MB		# min 1MB
 #autovacuum_work_mem = -1		# min 1MB, or -1 to use maintenance_work_mem
+#cache_memory_target = 0kB	# in kB
+#cache_prune_min_age = 600s	# -1 disables pruning
 #max_stack_depth = 2MB			# min 100kB
 #shared_memory_type = mmap		# the default is the first option
 					# supported by the operating system:
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h
index 65d816a583..3c6842e272 100644
--- a/src/include/utils/catcache.h
+++ b/src/include/utils/catcache.h
@@ -22,6 +22,7 @@
 
 #include "access/htup.h"
 #include "access/skey.h"
+#include "datatype/timestamp.h"
 #include "lib/ilist.h"
 #include "utils/relcache.h"
 
@@ -61,6 +62,9 @@ typedef struct catcache
 	slist_node	cc_next;		/* list link */
 	ScanKeyData cc_skey[CATCACHE_MAXKEYS];	/* precomputed key info for heap
 											 * scans */
+	dlist_head	cc_lru_list;
+	int			cc_tupsize;		/* total amount of catcache tuples */
+	int			cc_nfreeent;	/* # of entries currently not referenced */
 
 	/*
 	 * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS
@@ -119,7 +123,10 @@ typedef struct catctup
 	bool		dead;			/* dead but not yet removed? */
 	bool		negative;		/* negative cache entry? */
 	HeapTupleData tuple;		/* tuple management header */
-
+	int			naccess;		/* # of access to this entry, up to 2  */
+	TimestampTz	lastaccess;		/* approx. timestamp of the last usage */
+	dlist_node	lru_node;		/* LRU node */
+	int			size;			/* palloc'ed size off this tuple */
 	/*
 	 * The tuple may also be a member of at most one CatCList.  (If a single
 	 * catcache is list-searched with varying numbers of keys, we may have to
@@ -189,6 +196,30 @@ typedef struct catcacheheader
 /* this extern duplicates utils/memutils.h... */
 extern PGDLLIMPORT MemoryContext CacheMemoryContext;
 
+/* for guc.c, not PGDLLPMPORT'ed */
+extern int cache_prune_min_age;
+extern int cache_memory_target;
+extern int cache_entry_limit;
+extern double cache_entry_limit_prune_ratio;
+
+/* to use as access timestamp of catcache entries */
+extern TimestampTz catcacheclock;
+
+/*
+ * SetCatCacheClock - set timestamp for catcache access record
+ */
+static inline void
+SetCatCacheClock(TimestampTz ts)
+{
+	catcacheclock = ts;
+}
+
+static inline TimestampTz
+GetCatCacheClock(void)
+{
+	return catcacheclock;
+}
+
 extern void CreateCacheMemoryContext(void);
 
 extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid,
-- 
2.16.3


----Next_Part(Thu_Feb_07_21_18_45_2019_504)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="v13-0003-Syscache-usage-tracking-feature.patch"



^ permalink  raw  reply  [nested|flat] 19+ messages in thread

* [PATCH 2/2] Remove entries that haven't been used for a certain time
@ 2018-10-16 04:04  Kyotaro Horiguchi <[email protected]>
  0 siblings, 0 replies; 19+ messages in thread

From: Kyotaro Horiguchi @ 2018-10-16 04:04 UTC (permalink / raw)

Catcache entries can be left alone for several reasons. It is not
desirable that they eat up memory. With this patch, This adds
consideration of removal of entries that haven't been used for a
certain time before enlarging the hash array.

This also can put a hard limit on the number of catcache entries.
---
 doc/src/sgml/config.sgml                      |  40 +++++
 src/backend/tcop/postgres.c                   |  13 ++
 src/backend/utils/cache/catcache.c            | 243 ++++++++++++++++++++++++--
 src/backend/utils/init/globals.c              |   1 +
 src/backend/utils/init/postinit.c             |  11 ++
 src/backend/utils/misc/guc.c                  |  23 +++
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/miscadmin.h                       |   1 +
 src/include/utils/catcache.h                  |  43 ++++-
 src/include/utils/timeout.h                   |   1 +
 10 files changed, 364 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 8bd57f376b..7a93aef659 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1661,6 +1661,46 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-catalog-cache-prune-min-age" xreflabel="catalog_cache_prune_min_age">
+      <term><varname>catalog_cache_prune_min_age</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>catalog_cache_prune_min_age</varname> configuration
+       parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the minimum amount of unused time in seconds at which a
+        system catalog cache entry is removed. -1 indicates that this feature
+        is disabled at all. The value defaults to 300 seconds (<literal>5
+        minutes</literal>). The catalog cache entries that are not used for
+        the duration can be removed to prevent it from being filled up with
+        useless entries. This behaviour is muted until the size of a catalog
+        cache exceeds <xref linkend="guc-catalog-cache-memory-target"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-catalog-cache-memory-target" xreflabel="catalog_cache_memory_target">
+      <term><varname>catalog_cache_memory_target</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>catalog_cache_memory_target</varname> configuration
+       parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the maximum amount of memory to which a system catalog cache
+        can expand without pruning in kilobytes. The value defaults to 0,
+        indicating that age-based pruning is always considered. After
+        exceeding this size, catalog cache starts pruning according to
+        <xref linkend="guc-catalog-cache-prune-min-age"/>. If you need to keep
+        certain amount of catalog cache entries with intermittent usage, try
+        increase this setting.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth">
       <term><varname>max_stack_depth</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 8b4d94c9a1..d9a54ed37f 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -71,6 +71,7 @@
 #include "tcop/pquery.h"
 #include "tcop/tcopprot.h"
 #include "tcop/utility.h"
+#include "utils/catcache.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
@@ -2584,6 +2585,7 @@ start_xact_command(void)
 	 * not desired, the timeout has to be disabled explicitly.
 	 */
 	enable_statement_timeout();
+	SetCatCacheClock(GetCurrentStatementStartTimestamp());
 }
 
 static void
@@ -3159,6 +3161,14 @@ ProcessInterrupts(void)
 
 	if (ParallelMessagePending)
 		HandleParallelMessages();
+
+	if (CatcacheClockTimeoutPending)
+	{
+		CatcacheClockTimeoutPending = false;
+
+		/* Update timestamp then set up the next timeout */
+		UpdateCatCacheClock();
+	}
 }
 
 
@@ -4021,6 +4031,9 @@ PostgresMain(int argc, char *argv[],
 		QueryCancelPending = false; /* second to avoid race condition */
 		stmt_timeout_active = false;
 
+		/* get sync with the timer state */
+		catcache_clock_timeout_active = false;
+
 		/* Not reading from the client anymore. */
 		DoingCommandRead = false;
 
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 78dd5714fa..30ab710aaa 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -39,6 +39,7 @@
 #include "utils/rel.h"
 #include "utils/resowner_private.h"
 #include "utils/syscache.h"
+#include "utils/timeout.h"
 
 
  /* #define CACHEDEBUG */	/* turns DEBUG elogs on */
@@ -61,9 +62,35 @@
 #define CACHE_elog(...)
 #endif
 
+/* GUC variable to define the minimum age of entries that will be considered to
+ * be evicted in seconds. This variable is shared among various cache
+ * mechanisms.
+ */
+int catalog_cache_prune_min_age = 300;
+
+/*
+ * GUC variable to define the minimum size of hash to cosider entry eviction.
+ * This variable is shared among various cache mechanisms.
+ */
+int catalog_cache_memory_target = 0;
+
+/*
+ * Flag to keep track of whether catcache clock timer is active.
+ */
+bool catcache_clock_timeout_active = false;
+
+/*
+ * Minimum interval between two success move of a cache entry in LRU list,
+ * in microseconds.
+ */
+#define MIN_LRU_UPDATE_INTERVAL 100000	/* 100ms */
+
 /* Cache management header --- pointer is NULL until created */
 static CatCacheHeader *CacheHdr = NULL;
 
+/* Clock used to record the last accessed time of a catcache record. */
+TimestampTz	catcacheclock = 0;
+
 static inline HeapTuple SearchCatCacheInternal(CatCache *cache,
 					   int nkeys,
 					   Datum v1, Datum v2,
@@ -97,7 +124,7 @@ static CatCTup *CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp,
 
 static void CatCacheFreeKeys(TupleDesc tupdesc, int nkeys, int *attnos,
 				 Datum *keys);
-static void CatCacheCopyKeys(TupleDesc tupdesc, int nkeys, int *attnos,
+static size_t CatCacheCopyKeys(TupleDesc tupdesc, int nkeys, int *attnos,
 				 Datum *srckeys, Datum *dstkeys);
 
 
@@ -469,6 +496,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct)
 
 	/* delink from linked list */
 	dlist_delete(&ct->cache_elem);
+	dlist_delete(&ct->lru_node);
 
 	/*
 	 * Free keys when we're dealing with a negative entry, normal entries just
@@ -478,6 +506,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct)
 		CatCacheFreeKeys(cache->cc_tupdesc, cache->cc_nkeys,
 						 cache->cc_keyno, ct->keys);
 
+	cache->cc_memusage -= ct->size;
 	pfree(ct);
 
 	--cache->cc_ntup;
@@ -811,7 +840,9 @@ InitCatCache(int id,
 	 */
 	sz = sizeof(CatCache) + PG_CACHE_LINE_SIZE;
 	cp = (CatCache *) CACHELINEALIGN(palloc0(sz));
-	cp->cc_bucket = palloc0(nbuckets * sizeof(dlist_head));
+	cp->cc_head_size = sz;
+	sz = nbuckets * sizeof(dlist_head);
+	cp->cc_bucket = palloc0(sz);
 
 	/*
 	 * initialize the cache's relation information for the relation
@@ -830,6 +861,9 @@ InitCatCache(int id,
 	for (i = 0; i < nkeys; ++i)
 		cp->cc_keyno[i] = key[i];
 
+	cp->cc_memusage = cp->cc_head_size + sz;
+
+	dlist_init(&cp->cc_lru_list);
 	/*
 	 * new cache is initialized as far as we can go for now. print some
 	 * debugging information, if appropriate.
@@ -846,9 +880,143 @@ InitCatCache(int id,
 	 */
 	MemoryContextSwitchTo(oldcxt);
 
+	/* initialize catcache reference clock if haven't done yet */
+	if (catcacheclock == 0)
+		catcacheclock = GetCurrentTimestamp();
+
 	return cp;
 }
 
+/*
+ * helper routine for SetCatCacheClock and UpdateCatCacheClockTimer.
+ *
+ * We need to maintain the catcache clock during a long query.
+ */
+void
+SetupCatCacheClockTimer(void)
+{
+	long delay;
+
+	/* stop timer if not needed */
+	if (catalog_cache_prune_min_age == 0)
+	{
+		catcache_clock_timeout_active = false;
+		return;
+	}
+
+	/* One 10th of the variable, in milliseconds */
+	delay  = catalog_cache_prune_min_age * 1000/10;
+
+	/* Lower limit is 1 second */
+	if (delay < 1000)
+		delay = 1000;
+
+	enable_timeout_after(CATCACHE_CLOCK_TIMEOUT, delay);
+
+	catcache_clock_timeout_active = true;
+}
+
+/*
+ * Update catcacheclock: this is intended to be called from
+ * CATCACHE_CLOCK_TIMEOUT. The interval is expected more than 1 second (see
+ * above), so GetCurrentTime() doesn't harm.
+ */
+void
+UpdateCatCacheClock(void)
+{
+	catcacheclock = GetCurrentTimestamp();
+	SetupCatCacheClockTimer();
+}
+
+/*
+ * It may take an unexpectedly long time before the next clock update when
+ * catalog_cache_prune_min_age gets shorter. Disabling the current timer let
+ * the next update happen at the expected interval. We don't necessariry
+ * require this for increase the age but we don't need to avoid to disable
+ * either.
+ */
+void
+assign_catalog_cache_prune_min_age(int newval, void *extra)
+{
+	if (catcache_clock_timeout_active)
+		disable_timeout(CATCACHE_CLOCK_TIMEOUT, false);
+
+	catcache_clock_timeout_active = false;
+}
+
+/*
+ * CatCacheCleanupOldEntries - Remove infrequently-used entries
+ *
+ * Catcache entries can be left alone for several reasons. We remove them if
+ * they are not accessed for a certain time to prevent catcache from
+ * bloating. The eviction is performed with the similar algorithm with buffer
+ * eviction using access counter. Entries that are accessed several times can
+ * live longer than those that have had less access in the same duration.
+ */
+static bool
+CatCacheCleanupOldEntries(CatCache *cp)
+{
+	int			nremoved = 0;
+	dlist_mutable_iter	iter;
+
+	/* Return immediately if no pruning is wanted */
+	if (catalog_cache_prune_min_age == 0 ||
+		cp->cc_memusage <= (Size) catalog_cache_memory_target * 1024L)
+		return false;
+
+	/* Scan over LRU to find entries to remove */
+	dlist_foreach_modify(iter, &cp->cc_lru_list)
+	{
+		CatCTup    *ct = dlist_container(CatCTup, lru_node, iter.cur);
+		long		entry_age;
+		int			us;
+
+		/* We don't remove referenced entry */
+		if (ct->refcount != 0 ||
+			(ct->c_list && ct->c_list->refcount != 0))
+			continue;
+
+		/*
+		 * Calculate the duration from the time from the last access to
+		 * the "current" time. catcacheclock is updated per-statement
+		 * basis and additionaly udpated periodically during a long
+		 * running query.
+		 */
+		TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us);
+
+		if (entry_age < catalog_cache_prune_min_age)
+		{
+			/*
+			 * no longer have a business with further entries, exit.  At least
+			 * one removal is enough to prevent rehashing this time.
+			 */
+			return nremoved > 0;
+		}
+
+		/*
+		 * Entries that are not accessed after last pruning are removed in
+		 * that seconds, and that has been accessed several times are
+		 * removed after leaving alone for up to three times of the
+		 * duration. We don't try shrink buckets since pruning effectively
+		 * caps catcache expansion in the long term.
+		 */
+		if (ct->naccess > 0)
+			ct->naccess--;
+		else
+		{
+			/* remove this entry */
+			CatCacheRemoveCTup(cp, ct);
+			nremoved++;
+		}
+	}
+
+	if (nremoved > 0)
+		elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d",
+			 cp->id, cp->cc_relname, nremoved, cp->cc_ntup + nremoved);
+
+	return nremoved > 0;
+}
+
 /*
  * Enlarge a catcache, doubling the number of buckets.
  */
@@ -858,13 +1026,18 @@ RehashCatCache(CatCache *cp)
 	dlist_head *newbucket;
 	int			newnbuckets;
 	int			i;
+	size_t		sz;
 
 	elog(DEBUG1, "rehashing catalog cache id %d for %s; %d tups, %d buckets",
 		 cp->id, cp->cc_relname, cp->cc_ntup, cp->cc_nbuckets);
 
 	/* Allocate a new, larger, hash table. */
 	newnbuckets = cp->cc_nbuckets * 2;
-	newbucket = (dlist_head *) MemoryContextAllocZero(CacheMemoryContext, newnbuckets * sizeof(dlist_head));
+	sz = newnbuckets * sizeof(dlist_head);
+	newbucket = (dlist_head *) MemoryContextAllocZero(CacheMemoryContext, sz);
+
+	/* reset memory usage */
+	cp->cc_memusage = cp->cc_head_size + sz;
 
 	/* Move all entries from old hash table to new. */
 	for (i = 0; i < cp->cc_nbuckets; i++)
@@ -878,6 +1051,7 @@ RehashCatCache(CatCache *cp)
 
 			dlist_delete(iter.cur);
 			dlist_push_head(&newbucket[hashIndex], &ct->cache_elem);
+			cp->cc_memusage += ct->size;
 		}
 	}
 
@@ -1260,6 +1434,21 @@ SearchCatCacheInternal(CatCache *cache,
 		 */
 		dlist_move_head(bucket, &ct->cache_elem);
 
+		/* Update access information for pruning */
+		if (ct->naccess < 2)
+			ct->naccess++;
+
+		/*
+		 * We don't want too frequent update of
+		 * LRU. catalog_cache_prune_min_age can be changed on-session so we
+		 * need to maintain the LRU regardless of catalog_cache_prune_min_age.
+		 */
+		if (catcacheclock - ct->lastaccess > MIN_LRU_UPDATE_INTERVAL)
+		{
+			ct->lastaccess = catcacheclock;
+			dlist_move_tail(&cache->cc_lru_list, &ct->lru_node);
+		}
+
 		/*
 		 * If it's a positive entry, bump its refcount and return it. If it's
 		 * negative, we can report failure to the caller.
@@ -1695,6 +1884,11 @@ SearchCatCacheList(CatCache *cache,
 		/* Now we can build the CatCList entry. */
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
 		nmembers = list_length(ctlist);
+
+		/*
+		 * Don't waste a time by counting the list in catcache memory usage,
+		 * since it doesn't live a long life.
+		 */
 		cl = (CatCList *)
 			palloc(offsetof(CatCList, members) + nmembers * sizeof(CatCTup *));
 
@@ -1805,6 +1999,7 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	CatCTup    *ct;
 	HeapTuple	dtp;
 	MemoryContext oldcxt;
+	int			tupsize;
 
 	/* negative entries have no tuple associated */
 	if (ntp)
@@ -1828,8 +2023,8 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 		/* Allocate memory for CatCTup and the cached tuple in one go */
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
 
-		ct = (CatCTup *) palloc(sizeof(CatCTup) +
-								MAXIMUM_ALIGNOF + dtp->t_len);
+		tupsize = sizeof(CatCTup) +	MAXIMUM_ALIGNOF + dtp->t_len;
+		ct = (CatCTup *) palloc(tupsize);
 		ct->tuple.t_len = dtp->t_len;
 		ct->tuple.t_self = dtp->t_self;
 		ct->tuple.t_tableOid = dtp->t_tableOid;
@@ -1862,14 +2057,16 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	{
 		Assert(negative);
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
-		ct = (CatCTup *) palloc(sizeof(CatCTup));
+		tupsize = sizeof(CatCTup);
+		ct = (CatCTup *) palloc(tupsize);
 
 		/*
 		 * Store keys - they'll point into separately allocated memory if not
 		 * by-value.
 		 */
-		CatCacheCopyKeys(cache->cc_tupdesc, cache->cc_nkeys, cache->cc_keyno,
-						 arguments, ct->keys);
+		tupsize +=
+			CatCacheCopyKeys(cache->cc_tupdesc, cache->cc_nkeys,
+							 cache->cc_keyno, arguments, ct->keys);
 		MemoryContextSwitchTo(oldcxt);
 	}
 
@@ -1884,19 +2081,33 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	ct->dead = false;
 	ct->negative = negative;
 	ct->hash_value = hashValue;
+	ct->naccess = 0;
+	ct->lastaccess = catcacheclock;
+	dlist_push_tail(&cache->cc_lru_list, &ct->lru_node);
 
 	dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem);
 
 	cache->cc_ntup++;
 	CacheHdr->ch_ntup++;
 
+	ct->size = tupsize;
+	cache->cc_memusage += ct->size;
+
+	/* increase refcount so that this survives pruning */
+	ct->refcount++;
+
 	/*
-	 * If the hash table has become too full, enlarge the buckets array. Quite
-	 * arbitrarily, we enlarge when fill factor > 2.
+	 * If the hash table has become too full, try cleanup by removing
+	 * infrequently used entries to make a room for the new entry. If it
+	 * failed, enlarge the bucket array instead.  Quite arbitrarily, we try
+	 * this when fill factor > 2.
 	 */
-	if (cache->cc_ntup > cache->cc_nbuckets * 2)
+	if (cache->cc_ntup > cache->cc_nbuckets * 2 &&
+		!CatCacheCleanupOldEntries(cache))
 		RehashCatCache(cache);
 
+	ct->refcount--;
+
 	return ct;
 }
 
@@ -1926,13 +2137,14 @@ CatCacheFreeKeys(TupleDesc tupdesc, int nkeys, int *attnos, Datum *keys)
 /*
  * Helper routine that copies the keys in the srckeys array into the dstkeys
  * one, guaranteeing that the datums are fully allocated in the current memory
- * context.
+ * context. Returns allocated memory size.
  */
-static void
+static size_t
 CatCacheCopyKeys(TupleDesc tupdesc, int nkeys, int *attnos,
 				 Datum *srckeys, Datum *dstkeys)
 {
 	int			i;
+	size_t		sz = 0;
 
 	/*
 	 * XXX: memory and lookup performance could possibly be improved by
@@ -1961,8 +2173,13 @@ CatCacheCopyKeys(TupleDesc tupdesc, int nkeys, int *attnos,
 		dstkeys[i] = datumCopy(src,
 							   att->attbyval,
 							   att->attlen);
+
+		/* approximate size */
+		if (!att->attbyval)
+			sz += VARHDRSZ + att->attlen;
 	}
 
+	return sz;
 }
 
 /*
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index fd51934aaf..0e8b972a29 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -32,6 +32,7 @@ volatile sig_atomic_t QueryCancelPending = false;
 volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t CatcacheClockTimeoutPending = false;
 volatile sig_atomic_t ConfigReloadPending = false;
 volatile uint32 InterruptHoldoffCount = 0;
 volatile uint32 QueryCancelHoldoffCount = 0;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index a5ee209f91..9eb50e9676 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -72,6 +72,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void CatcacheClockTimeoutHandler(void);
 static bool ThereIsAtLeastOneRole(void);
 static void process_startup_options(Port *port, bool am_superuser);
 static void process_settings(Oid databaseid, Oid roleid);
@@ -628,6 +629,8 @@ InitPostgres(const char *in_dbname, Oid dboid, const char *username,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(CATCACHE_CLOCK_TIMEOUT,
+						CatcacheClockTimeoutHandler);
 	}
 
 	/*
@@ -1238,6 +1241,14 @@ IdleInTransactionSessionTimeoutHandler(void)
 	SetLatch(MyLatch);
 }
 
+static void
+CatcacheClockTimeoutHandler(void)
+{
+	CatcacheClockTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 /*
  * Returns true if at least one role is defined in this database cluster.
  */
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 156d147c85..d863c8dec8 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -81,6 +81,7 @@
 #include "tsearch/ts_cache.h"
 #include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/catcache.h"
 #include "utils/guc_tables.h"
 #include "utils/float.h"
 #include "utils/memutils.h"
@@ -2205,6 +2206,28 @@ static struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"catalog_cache_prune_min_age", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the minimum unused duration of cache entries before removal."),
+			gettext_noop("Catalog cache entries that live unused for longer than this seconds are considered to be removed."),
+			GUC_UNIT_S
+		},
+		&catalog_cache_prune_min_age,
+		300, -1, INT_MAX,
+		NULL, assign_catalog_cache_prune_min_age, NULL
+	},
+
+	{
+		{"catalog_cache_memory_target", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the minimum syscache size to keep."),
+			gettext_noop("Time-based cache pruning starts working after exceeding this size."),
+			GUC_UNIT_KB
+		},
+		&catalog_cache_memory_target,
+		0, 0, MAX_KILOBYTES,
+		NULL, NULL, NULL
+	},
+
 	/*
 	 * We use the hopefully-safely-small value of 100kB as the compiled-in
 	 * default for max_stack_depth.  InitializeGUCOptions will increase it if
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 194f312096..7c82b0eca7 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -128,6 +128,8 @@
 #work_mem = 4MB				# min 64kB
 #maintenance_work_mem = 64MB		# min 1MB
 #autovacuum_work_mem = -1		# min 1MB, or -1 to use maintenance_work_mem
+#catalog_cache_memory_target = 0kB	# in kB
+#catalog_cache_prune_min_age = 300s	# -1 disables pruning
 #max_stack_depth = 2MB			# min 100kB
 #shared_memory_type = mmap		# the default is the first option
 					# supported by the operating system:
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index c9e35003a5..33b800e80f 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -82,6 +82,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t CatcacheClockTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ConfigReloadPending;
 
 extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost;
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h
index 65d816a583..1ae49b4819 100644
--- a/src/include/utils/catcache.h
+++ b/src/include/utils/catcache.h
@@ -22,6 +22,7 @@
 
 #include "access/htup.h"
 #include "access/skey.h"
+#include "datatype/timestamp.h"
 #include "lib/ilist.h"
 #include "utils/relcache.h"
 
@@ -61,6 +62,10 @@ typedef struct catcache
 	slist_node	cc_next;		/* list link */
 	ScanKeyData cc_skey[CATCACHE_MAXKEYS];	/* precomputed key info for heap
 											 * scans */
+	dlist_head	cc_lru_list;
+	int			cc_head_size;	/* memory usage of catcache header */
+	int			cc_memusage;	/* total memory usage of this catcache  */
+	int			cc_nfreeent;	/* # of entries currently not referenced */
 
 	/*
 	 * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS
@@ -119,7 +124,10 @@ typedef struct catctup
 	bool		dead;			/* dead but not yet removed? */
 	bool		negative;		/* negative cache entry? */
 	HeapTupleData tuple;		/* tuple management header */
-
+	int			naccess;		/* # of access to this entry, up to 2  */
+	TimestampTz	lastaccess;		/* approx. timestamp of the last usage */
+	dlist_node	lru_node;		/* LRU node */
+	int			size;			/* palloc'ed size off this tuple */
 	/*
 	 * The tuple may also be a member of at most one CatCList.  (If a single
 	 * catcache is list-searched with varying numbers of keys, we may have to
@@ -189,6 +197,39 @@ typedef struct catcacheheader
 /* this extern duplicates utils/memutils.h... */
 extern PGDLLIMPORT MemoryContext CacheMemoryContext;
 
+/* for guc.c, not PGDLLPMPORT'ed */
+extern int catalog_cache_prune_min_age;
+extern int catalog_cache_memory_target;
+extern int catalog_cache_entry_limit;
+extern double catalog_cache_prune_ratio;
+
+/* to use as access timestamp of catcache entries */
+extern TimestampTz catcacheclock;
+
+/*
+ * Flag to keep track of whether catcache timestamp timer is active.
+ */
+extern bool catcache_clock_timeout_active;
+
+/* catcache prune time helper functions  */
+extern void SetupCatCacheClockTimer(void);
+extern void UpdateCatCacheClock(void);
+
+/*
+ * SetCatCacheClock - set timestamp for catcache access record and start
+ * maintenance timer if needed. We keep to update the clock even while pruning
+ * is disable so that we are not confused by bogus clock value.
+ */
+static inline void
+SetCatCacheClock(TimestampTz ts)
+{
+	catcacheclock = ts;
+
+	if (!catcache_clock_timeout_active && catalog_cache_prune_min_age > 0)
+		SetupCatCacheClockTimer();
+}
+
+extern void assign_catalog_cache_prune_min_age(int newval, void *extra);
 extern void CreateCacheMemoryContext(void);
 
 extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid,
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 9244a2a7b7..b2d97b4f7b 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	CATCACHE_CLOCK_TIMEOUT,
 	/* First user-definable timeout reason */
 	USER_TIMEOUT,
 	/* Maximum number of timeout reasons */
-- 
2.16.3


----Next_Part(Wed_Feb_20_13_14_35_2019_032)----





^ permalink  raw  reply  [nested|flat] 19+ messages in thread

* [PATCH 3/5] Remove entries that haven't been used for a certain time
@ 2018-10-16 04:04  Kyotaro Horiguchi <[email protected]>
  0 siblings, 0 replies; 19+ messages in thread

From: Kyotaro Horiguchi @ 2018-10-16 04:04 UTC (permalink / raw)

Catcache entries can be left alone for several reasons. It is not
desirable that they eat up memory. With this patch, This adds
consideration of removal of entries that haven't been used for a
certain time before enlarging the hash array.

This also can put a hard limit on the number of catcache entries.
---
 doc/src/sgml/config.sgml                      |  40 ++++
 src/backend/tcop/postgres.c                   |  13 ++
 src/backend/utils/cache/catcache.c            | 283 +++++++++++++++++++++++++-
 src/backend/utils/init/globals.c              |   1 +
 src/backend/utils/init/postinit.c             |  11 +
 src/backend/utils/misc/guc.c                  |  43 ++++
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/miscadmin.h                       |   1 +
 src/include/utils/catcache.h                  |  50 ++++-
 src/include/utils/timeout.h                   |   1 +
 10 files changed, 436 insertions(+), 9 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 07b847a8e9..4749ad61a9 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1661,6 +1661,46 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-catalog-cache-prune-min-age" xreflabel="catalog_cache_prune_min_age">
+      <term><varname>catalog_cache_prune_min_age</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>catalog_cache_prune_min_age</varname> configuration
+       parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the minimum amount of unused time in seconds at which a
+        system catalog cache entry is removed. -1 indicates that this feature
+        is disabled at all. The value defaults to 300 seconds (<literal>5
+        minutes</literal>). The catalog cache entries that are not used for
+        the duration can be removed to prevent it from being filled up with
+        useless entries. This behaviour is muted until the size of a catalog
+        cache exceeds <xref linkend="guc-catalog-cache-memory-target"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-catalog-cache-memory-target" xreflabel="catalog_cache_memory_target">
+      <term><varname>catalog_cache_memory_target</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>catalog_cache_memory_target</varname> configuration
+       parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the maximum amount of memory to which a system catalog cache
+        can expand without pruning in kilobytes. The value defaults to 0,
+        indicating that age-based pruning is always considered. After
+        exceeding this size, catalog cache starts pruning according to
+        <xref linkend="guc-catalog-cache-prune-min-age"/>. If you need to keep
+        certain amount of catalog cache entries with intermittent usage, try
+        increase this setting.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth">
       <term><varname>max_stack_depth</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 36cfd507b2..f192ee2ca6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -71,6 +71,7 @@
 #include "tcop/pquery.h"
 #include "tcop/tcopprot.h"
 #include "tcop/utility.h"
+#include "utils/catcache.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
@@ -2584,6 +2585,7 @@ start_xact_command(void)
 	 * not desired, the timeout has to be disabled explicitly.
 	 */
 	enable_statement_timeout();
+	SetCatCacheClock(GetCurrentStatementStartTimestamp());
 }
 
 static void
@@ -3159,6 +3161,14 @@ ProcessInterrupts(void)
 
 	if (ParallelMessagePending)
 		HandleParallelMessages();
+
+	if (CatcacheClockTimeoutPending)
+	{
+		CatcacheClockTimeoutPending = 0;
+
+		/* Update timetamp then set up the next timeout */
+		UpdateCatCacheClock();
+	}
 }
 
 
@@ -4021,6 +4031,9 @@ PostgresMain(int argc, char *argv[],
 		QueryCancelPending = false; /* second to avoid race condition */
 		stmt_timeout_active = false;
 
+		/* get sync with the timer state */
+		catcache_clock_timeout_active = false;
+
 		/* Not reading from the client anymore. */
 		DoingCommandRead = false;
 
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 258a1d64cc..04a60a490a 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -39,6 +39,7 @@
 #include "utils/rel.h"
 #include "utils/resowner_private.h"
 #include "utils/syscache.h"
+#include "utils/timeout.h"
 
 
  /* #define CACHEDEBUG */	/* turns DEBUG elogs on */
@@ -71,9 +72,43 @@
 #define CACHE6_elog(a,b,c,d,e,f,g)
 #endif
 
+/* GUC variable to define the minimum age of entries that will be considered to
+ * be evicted in seconds. This variable is shared among various cache
+ * mechanisms.
+ */
+int catalog_cache_prune_min_age = 300;
+
+/*
+ * GUC variable to define the minimum size of hash to cosider entry eviction.
+ * This variable is shared among various cache mechanisms.
+ */
+int catalog_cache_memory_target = 0;
+
+/*
+ * GUC for limit by the number of entries. Entries are removed when the number
+ * of them goes above catalog_cache_entry_limit and leaving newer entries by
+ * the ratio specified by catalog_cache_prune_ratio.
+ */
+int catalog_cache_entry_limit = 0;
+double catalog_cache_prune_ratio = 0.8;
+
+/*
+ * Flag to keep track of whether catcache clock timer is active.
+ */
+bool catcache_clock_timeout_active = false;
+
+/*
+ * Minimum interval between two success move of a cache entry in LRU list,
+ * in microseconds.
+ */
+#define MIN_LRU_UPDATE_INTERVAL 100000	/* 100ms */
+
 /* Cache management header --- pointer is NULL until created */
 static CatCacheHeader *CacheHdr = NULL;
 
+/* Clock used to record the last accessed time of a catcache record. */
+TimestampTz	catcacheclock = 0;
+
 static inline HeapTuple SearchCatCacheInternal(CatCache *cache,
 					   int nkeys,
 					   Datum v1, Datum v2,
@@ -481,6 +516,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct)
 
 	/* delink from linked list */
 	dlist_delete(&ct->cache_elem);
+	dlist_delete(&ct->lru_node);
 
 	/*
 	 * Free keys when we're dealing with a negative entry, normal entries just
@@ -490,6 +526,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct)
 		CatCacheFreeKeys(cache->cc_tupdesc, cache->cc_nkeys,
 						 cache->cc_keyno, ct->keys);
 
+	cache->cc_memusage -= ct->size;
 	pfree(ct);
 
 	--cache->cc_ntup;
@@ -779,6 +816,7 @@ InitCatCache(int id,
 	MemoryContext oldcxt;
 	size_t		sz;
 	int			i;
+	uint64		base_size;
 
 	/*
 	 * nbuckets is the initial number of hash buckets to use in this catcache.
@@ -821,8 +859,12 @@ InitCatCache(int id,
 	 *
 	 * Note: we rely on zeroing to initialize all the dlist headers correctly
 	 */
+	base_size = MemoryContextGetConsumption(CacheMemoryContext);
 	sz = sizeof(CatCache) + PG_CACHE_LINE_SIZE;
 	cp = (CatCache *) CACHELINEALIGN(palloc0(sz));
+	cp->cc_head_alloc_size =
+		MemoryContextGetConsumption(CacheMemoryContext) - base_size;
+
 	cp->cc_bucket = palloc0(nbuckets * sizeof(dlist_head));
 
 	/*
@@ -842,6 +884,11 @@ InitCatCache(int id,
 	for (i = 0; i < nkeys; ++i)
 		cp->cc_keyno[i] = key[i];
 
+	/* cc_head_alloc_size + consumed size for cc_bucket */
+	cp->cc_memusage =
+		MemoryContextGetConsumption(CacheMemoryContext) - base_size;
+
+	dlist_init(&cp->cc_lru_list);
 	/*
 	 * new cache is initialized as far as we can go for now. print some
 	 * debugging information, if appropriate.
@@ -858,9 +905,185 @@ InitCatCache(int id,
 	 */
 	MemoryContextSwitchTo(oldcxt);
 
+	/* initialize catcache reference clock if haven't done yet */
+	if (catcacheclock == 0)
+		catcacheclock = GetCurrentTimestamp();
+
 	return cp;
 }
 
+/*
+ * helper routine for SetCatCacheClock and UpdateCatCacheClockTimer.
+ *
+ * We need to maintain the catcache clock during a long query.
+ */
+void
+SetupCatCacheClockTimer(void)
+{
+	long delay;
+
+	/* stop timer if not needed */
+	if (catalog_cache_prune_min_age == 0)
+	{
+		catcache_clock_timeout_active = false;
+		return;
+	}
+
+	/* One 10th of the variable, in milliseconds */
+	delay  = catalog_cache_prune_min_age * 1000/10;
+
+	/* Lower limit is 1 second */
+	if (delay < 1000)
+		delay = 1000;
+
+	enable_timeout_after(CATCACHE_CLOCK_TIMEOUT, delay);
+
+	catcache_clock_timeout_active = true;
+}
+
+/*
+ * Update catcacheclock: this is intended to be called from
+ * CATCACHE_CLOCK_TIMEOUT. The interval is expected more than 1 second (see
+ * above), so GetCurrentTime() doesn't harm.
+ */
+void
+UpdateCatCacheClock(void)
+{
+	catcacheclock = GetCurrentTimestamp();
+	SetupCatCacheClockTimer();
+}
+
+/*
+ * It may take an unexpectedly long time before the next clock update when
+ * catalog_cache_prune_min_age gets shorter. Disabling the current timer let
+ * the next update happen at the expected interval. We don't necessariry
+ * require this for increase the age but we don't need to avoid to disable
+ * either.
+ */
+void
+assign_catalog_cache_prune_min_age(int newval, void *extra)
+{
+	if (catcache_clock_timeout_active)
+		disable_timeout(CATCACHE_CLOCK_TIMEOUT, false);
+
+	catcache_clock_timeout_active = false;
+}
+
+/*
+ * CatCacheCleanupOldEntries - Remove infrequently-used entries
+ *
+ * Catcache entries can be left alone for several reasons. We remove them if
+ * they are not accessed for a certain time to prevent catcache from
+ * bloating. The eviction is performed with the similar algorithm with buffer
+ * eviction using access counter. Entries that are accessed several times can
+ * live longer than those that have had less access in the same duration.
+ */
+static bool
+CatCacheCleanupOldEntries(CatCache *cp)
+{
+	int			nremoved = 0;
+	int			nelems_before = cp->cc_ntup;
+	int			ndelelems = 0;
+	bool		prune_by_age = false;
+	bool		prune_by_number = false;
+	dlist_mutable_iter	iter;
+
+	/* prune only if the size of the hash is above the target */
+	if (catalog_cache_prune_min_age >= 0 &&
+		cp->cc_memusage > (Size) catalog_cache_memory_target * 1024L)
+		prune_by_age = true;
+
+	if (catalog_cache_entry_limit > 0 &&
+		nelems_before >= catalog_cache_entry_limit)
+	{
+		ndelelems = nelems_before -
+			(int) (catalog_cache_entry_limit * catalog_cache_prune_ratio);
+
+		/* an arbitrary lower limit.. */
+		if (ndelelems < 256)
+			ndelelems = 256;
+		if (ndelelems > nelems_before)
+			ndelelems = nelems_before;
+
+		prune_by_number = true;
+	}
+
+	/* Return immediately if no pruning is wanted */
+	if (!prune_by_age && !prune_by_number)
+		return false;
+
+	/* Scan over LRU to find entries to remove */
+	dlist_foreach_modify(iter, &cp->cc_lru_list)
+	{
+		CatCTup    *ct = dlist_container(CatCTup, lru_node, iter.cur);
+		bool		remove_this = false;
+
+		/* We don't remove referenced entry */
+		if (ct->refcount != 0 ||
+			(ct->c_list && ct->c_list->refcount != 0))
+			continue;
+
+		/* check against age */
+		if (prune_by_age)
+		{
+			long	entry_age;
+			int		us;
+
+			/*
+			 * Calculate the duration from the time of the last access to the
+			 * "current" time. Since catcacheclock is not advanced within a
+			 * transaction, the entries that are accessed within the current
+			 * transaction won't be pruned.
+			 */
+			TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us);
+
+			if (entry_age < catalog_cache_prune_min_age)
+			{
+				/* no longer have a business with further entries, exit  */
+				prune_by_age = false;
+				break;
+			}
+			/*
+			 * Entries that are not accessed after last pruning are removed in
+			 * that seconds, and that has been accessed several times are
+			 * removed after leaving alone for up to three times of the
+			 * duration. We don't try shrink buckets since pruning effectively
+			 * caps catcache expansion in the long term.
+			 */
+			if (ct->naccess > 0)
+				ct->naccess--;
+			else 
+				remove_this = true;
+		}
+
+		/* check against entry number */
+		if (prune_by_number)
+		{
+			if (nremoved < ndelelems)
+				remove_this = true;
+			else
+				prune_by_number = false; /* we're satisfied */
+		}
+
+		/* exit immediately if all finished */
+		if (!prune_by_age && !prune_by_number)
+			break;
+
+		/* do the work */
+		if (remove_this)
+		{
+			CatCacheRemoveCTup(cp, ct);
+			nremoved++;
+		}
+	}
+
+	if (nremoved > 0)
+		elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d",
+			 cp->id, cp->cc_relname, nremoved, nelems_before);
+
+	return nremoved > 0;
+}
+
 /*
  * Enlarge a catcache, doubling the number of buckets.
  */
@@ -870,6 +1093,7 @@ RehashCatCache(CatCache *cp)
 	dlist_head *newbucket;
 	int			newnbuckets;
 	int			i;
+	uint64		base_size = MemoryContextGetConsumption(CacheMemoryContext);
 
 	elog(DEBUG1, "rehashing catalog cache id %d for %s; %d tups, %d buckets",
 		 cp->id, cp->cc_relname, cp->cc_ntup, cp->cc_nbuckets);
@@ -878,6 +1102,10 @@ RehashCatCache(CatCache *cp)
 	newnbuckets = cp->cc_nbuckets * 2;
 	newbucket = (dlist_head *) MemoryContextAllocZero(CacheMemoryContext, newnbuckets * sizeof(dlist_head));
 
+	/* recalculate memory usage from the first */
+	cp->cc_memusage = cp->cc_head_alloc_size +
+		MemoryContextGetConsumption(CacheMemoryContext) - base_size;
+
 	/* Move all entries from old hash table to new. */
 	for (i = 0; i < cp->cc_nbuckets; i++)
 	{
@@ -890,6 +1118,7 @@ RehashCatCache(CatCache *cp)
 
 			dlist_delete(iter.cur);
 			dlist_push_head(&newbucket[hashIndex], &ct->cache_elem);
+			cp->cc_memusage += ct->size;
 		}
 	}
 
@@ -1274,6 +1503,21 @@ SearchCatCacheInternal(CatCache *cache,
 		 */
 		dlist_move_head(bucket, &ct->cache_elem);
 
+		/* Update access information for pruning */
+		if (ct->naccess < 2)
+			ct->naccess++;
+
+		/*
+		 * We don't want too frequent update of
+		 * LRU. catalog_cache_prune_min_age can be changed on-session so we
+		 * need to maintain the LRU regardless of catalog_cache_prune_min_age.
+		 */
+		if (catcacheclock - ct->lastaccess > MIN_LRU_UPDATE_INTERVAL)
+		{
+			ct->lastaccess = catcacheclock;
+			dlist_move_tail(&cache->cc_lru_list, &ct->lru_node);
+		}
+
 		/*
 		 * If it's a positive entry, bump its refcount and return it. If it's
 		 * negative, we can report failure to the caller.
@@ -1709,6 +1953,11 @@ SearchCatCacheList(CatCache *cache,
 		/* Now we can build the CatCList entry. */
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
 		nmembers = list_length(ctlist);
+
+		/*
+		 * Don't waste a time by counting the list in catcache memory usage,
+		 * since it doesn't live a long life.
+		 */
 		cl = (CatCList *)
 			palloc(offsetof(CatCList, members) + nmembers * sizeof(CatCTup *));
 
@@ -1819,11 +2068,13 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	CatCTup    *ct;
 	HeapTuple	dtp;
 	MemoryContext oldcxt;
+	uint64		base_size = MemoryContextGetConsumption(CacheMemoryContext);
 
 	/* negative entries have no tuple associated */
 	if (ntp)
 	{
 		int			i;
+		int			tupsize;
 
 		Assert(!negative);
 
@@ -1842,8 +2093,8 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 		/* Allocate memory for CatCTup and the cached tuple in one go */
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
 
-		ct = (CatCTup *) palloc(sizeof(CatCTup) +
-								MAXIMUM_ALIGNOF + dtp->t_len);
+		tupsize = sizeof(CatCTup) +	MAXIMUM_ALIGNOF + dtp->t_len;
+		ct = (CatCTup *) palloc(tupsize);
 		ct->tuple.t_len = dtp->t_len;
 		ct->tuple.t_self = dtp->t_self;
 		ct->tuple.t_tableOid = dtp->t_tableOid;
@@ -1877,7 +2128,6 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 		Assert(negative);
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
 		ct = (CatCTup *) palloc(sizeof(CatCTup));
-
 		/*
 		 * Store keys - they'll point into separately allocated memory if not
 		 * by-value.
@@ -1898,18 +2148,36 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	ct->dead = false;
 	ct->negative = negative;
 	ct->hash_value = hashValue;
+	ct->naccess = 0;
+	ct->lastaccess = catcacheclock;
+	dlist_push_tail(&cache->cc_lru_list, &ct->lru_node);
 
 	dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem);
 
 	cache->cc_ntup++;
 	CacheHdr->ch_ntup++;
 
+	ct->size = MemoryContextGetConsumption(CacheMemoryContext) - base_size;
+	cache->cc_memusage += ct->size;
+
+	/* increase refcount so that this survives pruning */
+	ct->refcount++;
+
 	/*
-	 * If the hash table has become too full, enlarge the buckets array. Quite
-	 * arbitrarily, we enlarge when fill factor > 2.
+	 * If the hash table has become too full, try cleanup by removing
+	 * infrequently used entries to make a room for the new entry. If it
+	 * failed, enlarge the bucket array instead.  Quite arbitrarily, we try
+	 * this when fill factor > 2.
 	 */
-	if (cache->cc_ntup > cache->cc_nbuckets * 2)
+	if (cache->cc_ntup > cache->cc_nbuckets * 2 &&
+		!CatCacheCleanupOldEntries(cache))
 		RehashCatCache(cache);
+	/* we may still want to prune by entry number, check it */
+	else if (catalog_cache_entry_limit > 0 &&
+			 cache->cc_ntup > catalog_cache_entry_limit)
+		CatCacheCleanupOldEntries(cache);
+
+	ct->refcount--;
 
 	return ct;
 }
@@ -1940,7 +2208,7 @@ CatCacheFreeKeys(TupleDesc tupdesc, int nkeys, int *attnos, Datum *keys)
 /*
  * Helper routine that copies the keys in the srckeys array into the dstkeys
  * one, guaranteeing that the datums are fully allocated in the current memory
- * context.
+ * context. Returns allocated memory size.
  */
 static void
 CatCacheCopyKeys(TupleDesc tupdesc, int nkeys, int *attnos,
@@ -1976,7 +2244,6 @@ CatCacheCopyKeys(TupleDesc tupdesc, int nkeys, int *attnos,
 							   att->attbyval,
 							   att->attlen);
 	}
-
 }
 
 /*
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index fd51934aaf..0e8b972a29 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -32,6 +32,7 @@ volatile sig_atomic_t QueryCancelPending = false;
 volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t CatcacheClockTimeoutPending = false;
 volatile sig_atomic_t ConfigReloadPending = false;
 volatile uint32 InterruptHoldoffCount = 0;
 volatile uint32 QueryCancelHoldoffCount = 0;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index a5ee209f91..9eb50e9676 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -72,6 +72,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void CatcacheClockTimeoutHandler(void);
 static bool ThereIsAtLeastOneRole(void);
 static void process_startup_options(Port *port, bool am_superuser);
 static void process_settings(Oid databaseid, Oid roleid);
@@ -628,6 +629,8 @@ InitPostgres(const char *in_dbname, Oid dboid, const char *username,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(CATCACHE_CLOCK_TIMEOUT,
+						CatcacheClockTimeoutHandler);
 	}
 
 	/*
@@ -1238,6 +1241,14 @@ IdleInTransactionSessionTimeoutHandler(void)
 	SetLatch(MyLatch);
 }
 
+static void
+CatcacheClockTimeoutHandler(void)
+{
+	CatcacheClockTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 /*
  * Returns true if at least one role is defined in this database cluster.
  */
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 41d477165c..c62d5ad8b8 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -81,6 +81,7 @@
 #include "tsearch/ts_cache.h"
 #include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/catcache.h"
 #include "utils/guc_tables.h"
 #include "utils/float.h"
 #include "utils/memutils.h"
@@ -2205,6 +2206,38 @@ static struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"catalog_cache_prune_min_age", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the minimum unused duration of cache entries before removal."),
+			gettext_noop("Catalog cache entries that live unused for longer than this seconds are considered to be removed."),
+			GUC_UNIT_S
+		},
+		&catalog_cache_prune_min_age,
+		300, -1, INT_MAX,
+		NULL, assign_catalog_cache_prune_min_age, NULL
+	},
+
+	{
+		{"catalog_cache_memory_target", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the minimum syscache size to keep."),
+			gettext_noop("Time-based cache pruning starts working after exceeding this size."),
+			GUC_UNIT_KB
+		},
+		&catalog_cache_memory_target,
+		0, 0, MAX_KILOBYTES,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"catalog_cache_entry_limit", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the maximum entries of catcache."),
+		 	NULL
+		},
+		&catalog_cache_entry_limit,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	/*
 	 * We use the hopefully-safely-small value of 100kB as the compiled-in
 	 * default for max_stack_depth.  InitializeGUCOptions will increase it if
@@ -3368,6 +3401,16 @@ static struct config_real ConfigureNamesReal[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"catalog_cache_prune_ratio", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Reduce ratio of pruning caused by catalog_cache_entry_limit."),
+		 	NULL
+		},
+		&catalog_cache_prune_ratio,
+		0.8, 0.0, 1.0,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, 0.0, 0.0, 0.0, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index ad6c436f93..aeb5968e75 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -128,6 +128,8 @@
 #work_mem = 4MB				# min 64kB
 #maintenance_work_mem = 64MB		# min 1MB
 #autovacuum_work_mem = -1		# min 1MB, or -1 to use maintenance_work_mem
+#catalog_cache_memory_target = 0kB	# in kB
+#catalog_cache_prune_min_age = 300s	# -1 disables pruning
 #max_stack_depth = 2MB			# min 100kB
 #shared_memory_type = mmap		# the default is the first option
 					# supported by the operating system:
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index c9e35003a5..33b800e80f 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -82,6 +82,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t CatcacheClockTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ConfigReloadPending;
 
 extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost;
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h
index 65d816a583..0a714bf514 100644
--- a/src/include/utils/catcache.h
+++ b/src/include/utils/catcache.h
@@ -22,6 +22,7 @@
 
 #include "access/htup.h"
 #include "access/skey.h"
+#include "datatype/timestamp.h"
 #include "lib/ilist.h"
 #include "utils/relcache.h"
 
@@ -61,6 +62,11 @@ typedef struct catcache
 	slist_node	cc_next;		/* list link */
 	ScanKeyData cc_skey[CATCACHE_MAXKEYS];	/* precomputed key info for heap
 											 * scans */
+	dlist_head	cc_lru_list;
+	int			cc_head_alloc_size;/* consumed memory to allocate this struct */
+	int			cc_memusage;	/* memory usage of this catcache (excluding
+								 * header part) */
+	int			cc_nfreeent;	/* # of entries currently not referenced */
 
 	/*
 	 * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS
@@ -119,7 +125,10 @@ typedef struct catctup
 	bool		dead;			/* dead but not yet removed? */
 	bool		negative;		/* negative cache entry? */
 	HeapTupleData tuple;		/* tuple management header */
-
+	int			naccess;		/* # of access to this entry, up to 2  */
+	TimestampTz	lastaccess;		/* approx. timestamp of the last usage */
+	dlist_node	lru_node;		/* LRU node */
+	int			size;			/* palloc'ed size off this tuple */
 	/*
 	 * The tuple may also be a member of at most one CatCList.  (If a single
 	 * catcache is list-searched with varying numbers of keys, we may have to
@@ -189,6 +198,45 @@ typedef struct catcacheheader
 /* this extern duplicates utils/memutils.h... */
 extern PGDLLIMPORT MemoryContext CacheMemoryContext;
 
+/* for guc.c, not PGDLLPMPORT'ed */
+extern int catalog_cache_prune_min_age;
+extern int catalog_cache_memory_target;
+extern int catalog_cache_entry_limit;
+extern double catalog_cache_prune_ratio;
+
+/* to use as access timestamp of catcache entries */
+extern TimestampTz catcacheclock;
+
+/*
+ * Flag to keep track of whether catcache timestamp timer is active.
+ */
+extern bool catcache_clock_timeout_active;
+
+/* catcache prune time helper functions  */
+extern void SetupCatCacheClockTimer(void);
+extern void UpdateCatCacheClock(void);
+
+/*
+ * SetCatCacheClock - set timestamp for catcache access record and start
+ * maintenance timer if needed. We keep to update the clock even while pruning
+ * is disable so that we are not confused by bogus clock value.
+ */
+static inline void
+SetCatCacheClock(TimestampTz ts)
+{
+	catcacheclock = ts;
+
+	if (!catcache_clock_timeout_active && catalog_cache_prune_min_age > 0)
+		SetupCatCacheClockTimer();
+}
+
+static inline TimestampTz
+GetCatCacheClock(void)
+{
+	return catcacheclock;
+}
+
+extern void assign_catalog_cache_prune_min_age(int newval, void *extra);
 extern void CreateCacheMemoryContext(void);
 
 extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid,
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 9244a2a7b7..b2d97b4f7b 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	CATCACHE_CLOCK_TIMEOUT,
 	/* First user-definable timeout reason */
 	USER_TIMEOUT,
 	/* Maximum number of timeout reasons */
-- 
2.16.3


----Next_Part(Wed_Feb_13_15_31_14_2019_467)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="v15-0004-Syscache-usage-tracking-feature.patch"



^ permalink  raw  reply  [nested|flat] 19+ messages in thread

* [PATCH 1/4] Remove entries that haven't been used for a certain time
@ 2018-10-16 04:04  Kyotaro Horiguchi <[email protected]>
  0 siblings, 0 replies; 19+ messages in thread

From: Kyotaro Horiguchi @ 2018-10-16 04:04 UTC (permalink / raw)

Catcache entries can be left alone for several reasons. It is not
desirable that they eat up memory. With this patch, This adds
consideration of removal of entries that haven't been used for a
certain time before enlarging the hash array.
---
 doc/src/sgml/config.sgml                      |  38 ++++++
 src/backend/access/transam/xact.c             |   5 +
 src/backend/utils/cache/catcache.c            | 166 ++++++++++++++++++++++++--
 src/backend/utils/misc/guc.c                  |  23 ++++
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/utils/catcache.h                  |  28 ++++-
 6 files changed, 254 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 9b7a7388d5..d0d2374944 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1662,6 +1662,44 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-syscache-memory-target" xreflabel="syscache_memory_target">
+      <term><varname>syscache_memory_target</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>syscache_memory_target</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the maximum amount of memory to which syscache is expanded
+        without pruning. The value defaults to 0, indicating that pruning is
+        always considered. After exceeding this size, syscache pruning is
+        considered according to
+        <xref linkend="guc-syscache-prune-min-age"/>. If you need to keep
+        certain amount of syscache entries with intermittent usage, try
+        increase this setting.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-syscache-prune-min-age" xreflabel="syscache_prune_min_age">
+      <term><varname>syscache_prune_min_age</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>syscache_prune_min_age</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the minimum amount of unused time in seconds at which a
+        syscache entry is considered to be removed. -1 indicates that syscache
+        pruning is disabled at all. The value defaults to 600 seconds
+        (<literal>10 minutes</literal>). The syscache entries that are not
+        used for the duration can be removed to prevent syscache bloat. This
+        behavior is suppressed until the size of syscache exceeds
+        <xref linkend="guc-syscache-memory-target"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth">
       <term><varname>max_stack_depth</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 92bda87804..ddc433c59e 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -734,7 +734,12 @@ void
 SetCurrentStatementStartTimestamp(void)
 {
 	if (!IsParallelWorker())
+	{
 		stmtStartTimestamp = GetCurrentTimestamp();
+
+		/* Set this timestamp as aproximated current time */
+		SetCatCacheClock(stmtStartTimestamp);
+	}
 	else
 		Assert(stmtStartTimestamp != 0);
 }
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 258a1d64cc..2a996d740a 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -71,9 +71,24 @@
 #define CACHE6_elog(a,b,c,d,e,f,g)
 #endif
 
+/*
+ * GUC variable to define the minimum size of hash to cosider entry eviction.
+ * This variable is shared among various cache mechanisms.
+ */
+int cache_memory_target = 0;
+
+/* GUC variable to define the minimum age of entries that will be cosidered to
+ * be evicted in seconds. This variable is shared among various cache
+ * mechanisms.
+ */
+int cache_prune_min_age = 600;
+
 /* Cache management header --- pointer is NULL until created */
 static CatCacheHeader *CacheHdr = NULL;
 
+/* Timestamp used for any operation on caches. */
+TimestampTz	catcacheclock = 0;
+
 static inline HeapTuple SearchCatCacheInternal(CatCache *cache,
 					   int nkeys,
 					   Datum v1, Datum v2,
@@ -490,6 +505,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct)
 		CatCacheFreeKeys(cache->cc_tupdesc, cache->cc_nkeys,
 						 cache->cc_keyno, ct->keys);
 
+	cache->cc_tupsize -= ct->size;
 	pfree(ct);
 
 	--cache->cc_ntup;
@@ -841,6 +857,7 @@ InitCatCache(int id,
 	cp->cc_nkeys = nkeys;
 	for (i = 0; i < nkeys; ++i)
 		cp->cc_keyno[i] = key[i];
+	cp->cc_tupsize = 0;
 
 	/*
 	 * new cache is initialized as far as we can go for now. print some
@@ -858,9 +875,129 @@ InitCatCache(int id,
 	 */
 	MemoryContextSwitchTo(oldcxt);
 
+	/* initilize catcache reference clock if haven't done yet */
+	if (catcacheclock == 0)
+		catcacheclock = GetCurrentTimestamp();
+
 	return cp;
 }
 
+/*
+ * CatCacheCleanupOldEntries - Remove infrequently-used entries
+ *
+ * Catcache entries can be left alone for several reasons. We remove them if
+ * they are not accessed for a certain time to prevent catcache from
+ * bloating. The eviction is performed with the similar algorithm with buffer
+ * eviction using access counter. Entries that are accessed several times can
+ * live longer than those that have had no access in the same duration.
+ */
+static bool
+CatCacheCleanupOldEntries(CatCache *cp)
+{
+	int			i;
+	int			nremoved = 0;
+	size_t		hash_size;
+#ifdef CATCACHE_STATS
+	/* These variables are only for debugging purpose */
+	int			ntotal = 0;
+	/*
+	 * nth element in nentries stores the number of cache entries that have
+	 * lived unaccessed for corresponding multiple in ageclass of
+	 * cache_prune_min_age. The index of nremoved_entry is the value of the
+	 * clock-sweep counter, which takes from 0 up to 2.
+	 */
+	double		ageclass[] = {0.05, 0.1, 1.0, 2.0, 3.0, 0.0};
+	int			nentries[] = {0, 0, 0, 0, 0, 0};
+	int			nremoved_entry[3] = {0, 0, 0};
+	int			j;
+#endif
+
+	/* Return immediately if no pruning is wanted */
+	if (cache_prune_min_age < 0)
+		return false;
+
+	/*
+	 * Return without pruning if the size of the hash is below the target.
+	 */
+	hash_size = cp->cc_nbuckets * sizeof(dlist_head);
+	if (hash_size + cp->cc_tupsize < (Size) cache_memory_target * 1024L)
+		return false;
+	
+	/* Search the whole hash for entries to remove */
+	for (i = 0; i < cp->cc_nbuckets; i++)
+	{
+		dlist_mutable_iter iter;
+
+		dlist_foreach_modify(iter, &cp->cc_bucket[i])
+		{
+			CatCTup    *ct = dlist_container(CatCTup, cache_elem, iter.cur);
+			long entry_age;
+			int us;
+
+
+			/*
+			 * Calculate the duration from the time of the last access to the
+			 * "current" time. Since catcacheclock is not advanced within a
+			 * transaction, the entries that are accessed within the current
+			 * transaction won't be pruned.
+			 */
+			TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us);
+
+#ifdef CATCACHE_STATS
+			/* count catcache entries for each age class */
+			ntotal++;
+			for (j = 0 ;
+				 ageclass[j] != 0.0 &&
+					 entry_age > cache_prune_min_age * ageclass[j] ;
+				 j++);
+			if (ageclass[j] == 0.0) j--;
+			nentries[j]++;
+#endif
+
+			/*
+			 * Try to remove entries older than cache_prune_min_age seconds.
+			 * Entries that are not accessed after last pruning are removed in
+			 * that seconds, and that has been accessed several times are
+			 * removed after leaving alone for up to three times of the
+			 * duration. We don't try shrink buckets since pruning effectively
+			 * caps catcache expansion in the long term.
+			 */
+			if (entry_age > cache_prune_min_age)
+			{
+#ifdef CATCACHE_STATS
+				Assert (ct->naccess >= 0 && ct->naccess <= 2);
+				nremoved_entry[ct->naccess]++;
+#endif
+				if (ct->naccess > 0)
+					ct->naccess--;
+				else
+				{
+					if (!ct->c_list || ct->c_list->refcount == 0)
+					{
+						CatCacheRemoveCTup(cp, ct);
+						nremoved++;
+					}
+				}
+			}
+		}
+	}
+
+#ifdef CATCACHE_STATS
+	ereport(DEBUG1,
+			(errmsg ("removed %d/%d, age(-%.0fs:%d, -%.0fs:%d, *-%.0fs:%d, -%.0fs:%d, -%.0fs:%d) naccessed(0:%d, 1:%d, 2:%d)",
+					 nremoved, ntotal,
+					 ageclass[0] * cache_prune_min_age, nentries[0],
+					 ageclass[1] * cache_prune_min_age, nentries[1],
+					 ageclass[2] * cache_prune_min_age, nentries[2],
+					 ageclass[3] * cache_prune_min_age, nentries[3],
+					 ageclass[4] * cache_prune_min_age, nentries[4],
+					 nremoved_entry[0], nremoved_entry[1], nremoved_entry[2]),
+			 errhidestmt(true)));
+#endif
+
+	return nremoved > 0;
+}
+
 /*
  * Enlarge a catcache, doubling the number of buckets.
  */
@@ -1274,6 +1411,11 @@ SearchCatCacheInternal(CatCache *cache,
 		 */
 		dlist_move_head(bucket, &ct->cache_elem);
 
+		/* Update access information for pruning */
+		if (ct->naccess < 2)
+			ct->naccess++;
+		ct->lastaccess = catcacheclock;
+
 		/*
 		 * If it's a positive entry, bump its refcount and return it. If it's
 		 * negative, we can report failure to the caller.
@@ -1819,11 +1961,13 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	CatCTup    *ct;
 	HeapTuple	dtp;
 	MemoryContext oldcxt;
+	int			tupsize = 0;
 
 	/* negative entries have no tuple associated */
 	if (ntp)
 	{
 		int			i;
+		int			tupsize;
 
 		Assert(!negative);
 
@@ -1842,13 +1986,14 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 		/* Allocate memory for CatCTup and the cached tuple in one go */
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
 
-		ct = (CatCTup *) palloc(sizeof(CatCTup) +
-								MAXIMUM_ALIGNOF + dtp->t_len);
+		tupsize = sizeof(CatCTup) +	MAXIMUM_ALIGNOF + dtp->t_len;
+		ct = (CatCTup *) palloc(tupsize);
 		ct->tuple.t_len = dtp->t_len;
 		ct->tuple.t_self = dtp->t_self;
 		ct->tuple.t_tableOid = dtp->t_tableOid;
 		ct->tuple.t_data = (HeapTupleHeader)
 			MAXALIGN(((char *) ct) + sizeof(CatCTup));
+		ct->size = tupsize;
 		/* copy tuple contents */
 		memcpy((char *) ct->tuple.t_data,
 			   (const char *) dtp->t_data,
@@ -1876,8 +2021,8 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	{
 		Assert(negative);
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
-		ct = (CatCTup *) palloc(sizeof(CatCTup));
-
+		tupsize = sizeof(CatCTup);
+		ct = (CatCTup *) palloc(tupsize);
 		/*
 		 * Store keys - they'll point into separately allocated memory if not
 		 * by-value.
@@ -1898,17 +2043,24 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	ct->dead = false;
 	ct->negative = negative;
 	ct->hash_value = hashValue;
+	ct->naccess = 0;
+	ct->lastaccess = catcacheclock;
+	ct->size = tupsize;
 
 	dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem);
 
 	cache->cc_ntup++;
 	CacheHdr->ch_ntup++;
+	cache->cc_tupsize += tupsize;
 
 	/*
-	 * If the hash table has become too full, enlarge the buckets array. Quite
-	 * arbitrarily, we enlarge when fill factor > 2.
+	 * If the hash table has become too full, try cleanup by removing
+	 * infrequently used entries to make a room for the new entry. If it
+	 * failed, enlarge the bucket array instead.  Quite arbitrarily, we try
+	 * this when fill factor > 2.
 	 */
-	if (cache->cc_ntup > cache->cc_nbuckets * 2)
+	if (cache->cc_ntup > cache->cc_nbuckets * 2 &&
+		!CatCacheCleanupOldEntries(cache))
 		RehashCatCache(cache);
 
 	return ct;
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 8681ada33a..06c589f725 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -81,6 +81,7 @@
 #include "tsearch/ts_cache.h"
 #include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/catcache.h"
 #include "utils/guc_tables.h"
 #include "utils/float.h"
 #include "utils/memutils.h"
@@ -2204,6 +2205,28 @@ static struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"cache_memory_target", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the minimum syscache size to keep."),
+			gettext_noop("Cache is not pruned before exceeding this size."),
+			GUC_UNIT_KB
+		},
+		&cache_memory_target,
+		0, 0, MAX_KILOBYTES,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"cache_prune_min_age", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the minimum unused duration of cache entries before removal."),
+			gettext_noop("Cache entries that live unused for longer than this seconds are considered to be removed."),
+			GUC_UNIT_S
+		},
+		&cache_prune_min_age,
+		600, -1, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	/*
 	 * We use the hopefully-safely-small value of 100kB as the compiled-in
 	 * default for max_stack_depth.  InitializeGUCOptions will increase it if
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index c7f53470df..108d332f2c 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -128,6 +128,8 @@
 #work_mem = 4MB				# min 64kB
 #maintenance_work_mem = 64MB		# min 1MB
 #autovacuum_work_mem = -1		# min 1MB, or -1 to use maintenance_work_mem
+#cache_memory_target = 0kB	# in kB
+#cache_prune_min_age = 600s	# -1 disables pruning
 #max_stack_depth = 2MB			# min 100kB
 #shared_memory_type = mmap		# the default is the first option
 					# supported by the operating system:
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h
index 65d816a583..5d24809900 100644
--- a/src/include/utils/catcache.h
+++ b/src/include/utils/catcache.h
@@ -22,6 +22,7 @@
 
 #include "access/htup.h"
 #include "access/skey.h"
+#include "datatype/timestamp.h"
 #include "lib/ilist.h"
 #include "utils/relcache.h"
 
@@ -61,6 +62,7 @@ typedef struct catcache
 	slist_node	cc_next;		/* list link */
 	ScanKeyData cc_skey[CATCACHE_MAXKEYS];	/* precomputed key info for heap
 											 * scans */
+	int			cc_tupsize;		/* total amount of catcache tuples */
 
 	/*
 	 * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS
@@ -119,7 +121,9 @@ typedef struct catctup
 	bool		dead;			/* dead but not yet removed? */
 	bool		negative;		/* negative cache entry? */
 	HeapTupleData tuple;		/* tuple management header */
-
+	int			naccess;		/* # of access to this entry, up to 2  */
+	TimestampTz	lastaccess;		/* approx. timestamp of the last usage */
+	int			size;			/* palloc'ed size off this tuple */
 	/*
 	 * The tuple may also be a member of at most one CatCList.  (If a single
 	 * catcache is list-searched with varying numbers of keys, we may have to
@@ -189,6 +193,28 @@ typedef struct catcacheheader
 /* this extern duplicates utils/memutils.h... */
 extern PGDLLIMPORT MemoryContext CacheMemoryContext;
 
+/* for guc.c, not PGDLLPMPORT'ed */
+extern int cache_prune_min_age;
+extern int cache_memory_target;
+
+/* to use as access timestamp of catcache entries */
+extern TimestampTz catcacheclock;
+
+/*
+ * SetCatCacheClock - set timestamp for catcache access record
+ */
+static inline void
+SetCatCacheClock(TimestampTz ts)
+{
+	catcacheclock = ts;
+}
+
+static inline TimestampTz
+GetCatCacheClock(void)
+{
+	return catcacheclock;
+}
+
 extern void CreateCacheMemoryContext(void);
 
 extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid,
-- 
2.16.3


----Next_Part(Wed_Feb_06_14_43_34_2019_896)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="v9-0002-Syscache-usage-tracking-feature.patch"



^ permalink  raw  reply  [nested|flat] 19+ messages in thread

* [PATCH] Remove entries that haven't been used for a certain time
@ 2019-03-01 04:32  Kyotaro Horiguchi <[email protected]>
  0 siblings, 0 replies; 19+ messages in thread

From: Kyotaro Horiguchi @ 2019-03-01 04:32 UTC (permalink / raw)

Catcache entries happen to be left alone for several reasons. It is
not desirable that such useless entries eat up memory. Catcache
pruning feature removes entries that haven't been accessed for a
certain time before enlarging hash array.
---
 doc/src/sgml/config.sgml                      |  19 ++++
 src/backend/tcop/postgres.c                   |   2 +
 src/backend/utils/cache/catcache.c            | 121 +++++++++++++++++++++++++-
 src/backend/utils/misc/guc.c                  |  12 +++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/utils/catcache.h                  |  17 ++++
 6 files changed, 169 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index d383de2512..4231235447 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1677,6 +1677,25 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-catalog-cache-prune-min-age" xreflabel="catalog_cache_prune_min_age">
+      <term><varname>catalog_cache_prune_min_age</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>catalog_cache_prune_min_age</varname> configuration
+       parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+		 Specifies the minimum amount of unused time in seconds at which a
+		 system catalog cache entry is removed. -1 indicates that this feature
+		 is disabled at all. The value defaults to 300 seconds (<literal>5
+		 minutes</literal>). The entries that are not used for the duration
+		 can be removed to prevent catalog cache from bloating with useless
+		 entries.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth">
       <term><varname>max_stack_depth</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index f9ce3d8f22..acab473d34 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -71,6 +71,7 @@
 #include "tcop/pquery.h"
 #include "tcop/tcopprot.h"
 #include "tcop/utility.h"
+#include "utils/catcache.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
@@ -2575,6 +2576,7 @@ start_xact_command(void)
 	 * not desired, the timeout has to be disabled explicitly.
 	 */
 	enable_statement_timeout();
+	SetCatCacheClock(GetCurrentStatementStartTimestamp());
 }
 
 static void
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index d05930bc4c..c4582fe5a3 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -38,6 +38,7 @@
 #include "utils/rel.h"
 #include "utils/resowner_private.h"
 #include "utils/syscache.h"
+#include "utils/timeout.h"
 
 
  /* #define CACHEDEBUG */	/* turns DEBUG elogs on */
@@ -60,9 +61,18 @@
 #define CACHE_elog(...)
 #endif
 
+/*
+ * GUC variable to define the minimum age of entries that will be considered
+ * to be evicted in seconds. -1 to disable the feature.
+ */
+int catalog_cache_prune_min_age = 300;
+
 /* Cache management header --- pointer is NULL until created */
 static CatCacheHeader *CacheHdr = NULL;
 
+/* Clock for the last accessed time of a catcache entry. */
+TimestampTz	catcacheclock = 0;
+
 static inline HeapTuple SearchCatCacheInternal(CatCache *cache,
 					   int nkeys,
 					   Datum v1, Datum v2,
@@ -850,9 +860,99 @@ InitCatCache(int id,
 	 */
 	MemoryContextSwitchTo(oldcxt);
 
+	/* initialize catcache reference clock if haven't done yet */
+	if (catcacheclock == 0)
+		catcacheclock = GetCurrentTimestamp();
+
 	return cp;
 }
 
+/*
+ * CatCacheCleanupOldEntries - Remove infrequently-used entries
+ *
+ * Catcache entries happen to be left unused for a long time for several
+ * reasons. Remove such entries to prevent catcache from bloating. It is based
+ * on the similar algorithm with buffer eviction. Entries that are accessed
+ * several times in a certain period live longer than those that have had less
+ * access in the same duration.
+ */
+static bool
+CatCacheCleanupOldEntries(CatCache *cp)
+{
+	int		nremoved = 0;
+	int		i;
+	long	oldest_ts = catcacheclock;
+	long	age;
+	int		us;
+
+	/* Return immediately if disabled */
+	if (catalog_cache_prune_min_age == 0)
+		return false;
+
+	/* Don't scan the hash when we know we don't have prunable entries */
+	TimestampDifference(cp->cc_oldest_ts, catcacheclock, &age, &us);
+	if (age < catalog_cache_prune_min_age)
+		return false;
+
+	/* Scan over the whole hash to find entries to remove */
+	for (i = 0 ; i < cp->cc_nbuckets ; i++)
+	{
+		dlist_mutable_iter	iter;
+
+		dlist_foreach_modify(iter, &cp->cc_bucket[i])
+		{
+			CatCTup    *ct = dlist_container(CatCTup, cache_elem, iter.cur);
+
+			/* Don't remove referenced entries */
+			if (ct->refcount == 0 &&
+				(ct->c_list == NULL || ct->c_list->refcount == 0))
+			{
+				/*
+				 * Calculate the duration from the time from the last access
+				 * to the "current" time. catcacheclock is updated
+				 * per-statement basis and additionaly udpated periodically
+				 * during a long running query.
+				 */
+				TimestampDifference(ct->lastaccess, catcacheclock, &age, &us);
+
+				if (age >= catalog_cache_prune_min_age)
+				{
+					/*
+					 * Entries that are not accessed after the last pruning
+					 * are removed in that seconds, and their lives are
+					 * prolonged according to how many times they are accessed
+					 * up to three times of the duration. We don't try shrink
+					 * buckets since pruning effectively caps catcache
+					 * expansion in the long term.
+					 */
+					if (ct->naccess > 0)
+						ct->naccess--;
+					else
+					{
+						CatCacheRemoveCTup(cp, ct);
+						nremoved++;
+
+						/* don't update oldest_ts by removed entry */
+						continue;
+					}
+				}
+			}
+
+			/* update oldest timestamp if the entry remains alive */
+			if (ct->lastaccess < oldest_ts)
+				oldest_ts = ct->lastaccess;
+		}
+	}
+
+	cp->cc_oldest_ts = oldest_ts;
+
+	if (nremoved > 0)
+		elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d",
+			 cp->id, cp->cc_relname, nremoved, cp->cc_ntup + nremoved);
+
+	return nremoved > 0;
+}
+
 /*
  * Enlarge a catcache, doubling the number of buckets.
  */
@@ -1264,6 +1364,12 @@ SearchCatCacheInternal(CatCache *cache,
 		 */
 		dlist_move_head(bucket, &ct->cache_elem);
 
+		/* prolong life of this entry */
+		if (ct->naccess < 2)
+			ct->naccess++;
+
+		ct->lastaccess = catcacheclock;
+
 		/*
 		 * If it's a positive entry, bump its refcount and return it. If it's
 		 * negative, we can report failure to the caller.
@@ -1888,19 +1994,28 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	ct->dead = false;
 	ct->negative = negative;
 	ct->hash_value = hashValue;
+	ct->naccess = 0;
+	ct->lastaccess = catcacheclock;
 
 	dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem);
 
 	cache->cc_ntup++;
 	CacheHdr->ch_ntup++;
 
+	/* increase refcount so that the new entry survives pruning */
+	ct->refcount++;
+
 	/*
-	 * If the hash table has become too full, enlarge the buckets array. Quite
-	 * arbitrarily, we enlarge when fill factor > 2.
+	 * If the hash table has become too full, try removing infrequently used
+	 * entries to make a room for the new entry. If failed, enlarge the bucket
+	 * array instead.  Quite arbitrarily, we try this when fill factor > 2.
 	 */
-	if (cache->cc_ntup > cache->cc_nbuckets * 2)
+	if (cache->cc_ntup > cache->cc_nbuckets * 2 &&
+		!CatCacheCleanupOldEntries(cache))
 		RehashCatCache(cache);
 
+	ct->refcount--;
+
 	return ct;
 }
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index aa564d153a..e624c74bf9 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -82,6 +82,7 @@
 #include "tsearch/ts_cache.h"
 #include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/catcache.h"
 #include "utils/guc_tables.h"
 #include "utils/float.h"
 #include "utils/memutils.h"
@@ -2202,6 +2203,17 @@ static struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"catalog_cache_prune_min_age", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("System catalog cache entries that live unused for longer than this seconds are considered for removal."),
+			gettext_noop("The value of -1 turns off pruning."),
+			GUC_UNIT_S
+		},
+		&catalog_cache_prune_min_age,
+		300, -1, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	/*
 	 * We use the hopefully-safely-small value of 100kB as the compiled-in
 	 * default for max_stack_depth.  InitializeGUCOptions will increase it if
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index cccb5f145a..fa117f0573 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -128,6 +128,7 @@
 #work_mem = 4MB				# min 64kB
 #maintenance_work_mem = 64MB		# min 1MB
 #autovacuum_work_mem = -1		# min 1MB, or -1 to use maintenance_work_mem
+#catalog_cache_prune_min_age = 300s	# -1 disables pruning
 #max_stack_depth = 2MB			# min 100kB
 #shared_memory_type = mmap		# the default is the first option
 					# supported by the operating system:
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h
index 65d816a583..2f697d5ca4 100644
--- a/src/include/utils/catcache.h
+++ b/src/include/utils/catcache.h
@@ -22,6 +22,7 @@
 
 #include "access/htup.h"
 #include "access/skey.h"
+#include "datatype/timestamp.h"
 #include "lib/ilist.h"
 #include "utils/relcache.h"
 
@@ -61,6 +62,7 @@ typedef struct catcache
 	slist_node	cc_next;		/* list link */
 	ScanKeyData cc_skey[CATCACHE_MAXKEYS];	/* precomputed key info for heap
 											 * scans */
+	long		cc_oldest_ts;
 
 	/*
 	 * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS
@@ -119,6 +121,8 @@ typedef struct catctup
 	bool		dead;			/* dead but not yet removed? */
 	bool		negative;		/* negative cache entry? */
 	HeapTupleData tuple;		/* tuple management header */
+	int			naccess;		/* # of access to this entry, up to 2  */
+	TimestampTz	lastaccess;		/* timestamp of the last usage */
 
 	/*
 	 * The tuple may also be a member of at most one CatCList.  (If a single
@@ -189,6 +193,19 @@ typedef struct catcacheheader
 /* this extern duplicates utils/memutils.h... */
 extern PGDLLIMPORT MemoryContext CacheMemoryContext;
 
+/* for guc.c, not PGDLLPMPORT'ed */
+extern int catalog_cache_prune_min_age;
+
+/* source clock for access timestamp of catcache entries */
+extern TimestampTz catcacheclock;
+
+/* SetCatCacheClock - set catcache timestamp source clodk */
+static inline void
+SetCatCacheClock(TimestampTz ts)
+{
+	catcacheclock = ts;
+}
+
 extern void CreateCacheMemoryContext(void);
 
 extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid,
-- 
2.16.3


----Next_Part(Fri_Mar_29_17_24_40_2019_805)--
Content-Type: Application/Octet-Stream
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="test_script.tar.gz"

H4sIAFPSnVwAA+1b63Paxhb3V+uvOAb5WtQG9AIaO7h1J2mvZ1y74yb3fkhTRkgLqBGSol01ztjc
v/2e3ZVAflCblIhJo+MZHrtnz/t3dpElRigbUDfxY9be+kykI/V6Hf5u9Dp68T2nLcOy7Z7VMQzd
2NINq9vTt6DzuQwqUkqZkwBsTaLEH6fMX8b32PwXSqyQ/zEJB2wYtOJgvTp4grtde2n+bduQ+dc7
lmXhuNHp4hDo6zXjYfrK81/fgXZKk/bQD9sxSQJFdaMg8CmDPtRqR8ooSojjTkD1QdNbLQPT1YBr
ZXvO1uoDjRM/ZCOtdgDurt7xAL/hZ9VvHCmzokCaDilLtHzkAEzkUORqqHlJFAN1J2TqgD8CcoUs
FHiBgutQ1/HI0W8hmpTzu2gZI/kKzifn75hsPnv2TJh8Zx1zhgERy1psV7c9mNvV4HK4/dz8TSfo
M1MR/0katuhk/ToewX9XtzL8W6ah9wT+dcus8F8G1XcE9IcOnShnFxe/nJ3++qqvapS8BwNUo6H8
cHp+eXHxqt+eRFPSzqPAFykvTl6dvDi9vDvlOcwRL4MPUfJuEJlcxs8nv756edlXM3HteEzfB0WG
s8vXd2ddhOlV/O3A4PM/vj47W8Zg5gw/X7xYxmMpZyc/vDz798uTF33x6VBR3vlB4AQBxBFl44RQ
hQaExGAp6txi2RjHA5cF0GzGY+5XX808B147TCHuJAL1ei5/NnUoI0nTGOi8GYGPDRHUPLpH4EWg
MX9KgDdccFyXUMo3XgN3XriBGM2emwTH0PbIn+0wDYIGmMf/Mvj6kCxVaq6o1FyHUmtFpci/itqn
pyOKFUWwYzl9UupwXcl5kxqN1TQO/r7OcgtFalzNS3MNXq5cmWvQuJqX1mpePrG4ORDUrCt+EhB+
RMUlIyFTWS4UhNJysZCpLBcMQmm5aMhUfnY4PKXGi3jAU8KnQ2IaeZuAhVS7AWhwxRuAh1S7AYhw
xRuAiVRbClSeUP6Ilk3/MKmoFCr+/r/bOdal47Hrf1bPlL//Datrmhb+/kduvfr9Xwbdu/4nL69B
jRJ+2Y05QYRtwnEnZBAnaUgGUz8cOGMCLAJdXm/bwCXDxy7xURIQl8E3MEqiaeEa31d0Ye+JtAT/
a/0fwKPX//P//ximoRv8+l/X6tkV/sug+/ivP70B7Fm6TveqLvAF08P4Nze3/1u23P+tCv9l0D9i
/+88Bfm80irk36Ul+N/M/m/2Oj27w/Fvm90K/2XQP2j/r7rAJ9DD+Jf/n1uXjkf3f3u+/2MLyPb/
ToX/Mug+/svH8ULHH1JHQ0EFy073Kxzvt2cVwh+hZfgv8/xvm/bi/G8b1fW/EunLPP9XHWNdtAT/
5Z7/rfn1P9voCPzbRnX/Xyn0d/C/Z2zy9F/1gHVQEf9xEvEGUPb9/7bVzZ//4I+A9Pj9vz2r2v9L
ofrObfinlMAZguzw8DXzA3j/oU3TaRvh5vkEwTOJ0sBD5BLXH30E7A0w8gMCoTMlNX7TvqbWTy5/
+g/s9EFHkKp+GKdsIHj6oPKpN/pbHB/5JPACEuLot/g1dP4kCe8qfejwZhJSZxoHhOL3poHzmCPG
J/Uj5Xs38sSEhvKjGEWcnh/A3vM9Dt6FtpsbEBaPHPzm8W4leNHiFrti2KE+TDib9vz0/Fg0CHcS
TWMuczskV0z40v5dbfNOsy0cy2zgjomGIjnkrcRa65sG8orxbVVgKnCGJOBOo/04KIyRY7GTYJCl
YYdQ4K5xRqmsIGLnf9D+nStoam/05rO3DW2QfWh8J+zjKnlQ+pkqsbivmosvA+ygPGIeGfkhRkO1
4Tv+cgg1vSYlfC9uMJFR5cvyiAuRM+yJQAJ6Ow5GMQ7YdYPf6L4+Fba13u436DwgcUonmlBwwG8p
FxJzcTvoG9ZcckM/0ka+oA4JcaMEC02UAYTpdEgSiEbAJhg5P8Ftgful5OGa18vzPDvoQqGI1Lpw
b1+6k2UjjHL5oygNvax+C6KkqfdUoLi8XnNd29mm+d+Ty/PT858OcSuj6Wjkuz7B4Xylxst9Lqgh
Nk6+eKYo4hVf8iBLe3cK7NDMA56Zr7ZuFQ/msjVfuM95W9K02ly/cBOGKStIJVeIZUa8Vm7NrFgN
NEoYXKsOOn0M6nAGYuJIkUlizjsiMpKjN08QvykcU8Q1UfmYTSfLIb2fsuP74VTlrj+VRfMG9/FF
h8AwvG1Ae7FIWs3riWTL0bIIczvlt/ZLLQfwR8prhhucLXtAERdbjOAiHmpCaBqw65p6zT2bDeQ7
x9WsNrsW8Jvx5DIZG+4hdZ3ASbRxQmK4BnUA5D0IRsAwiibWKHSTHCVi4kAyFgzYvovRWyDVBWNC
vAiKXUk2h5k80LhBhAE6PceldaUOsmClW/JRKBzGcTxjpdMQmM9wJD8z4d8VprAFtRt5tssc0XI/
+MmOxrKf4xJN0xYdvgn4OmYTTfrEg2w2uGWqyw+LQna2uCXltiAb1rLxLBliwfwweAA6xilXw+cz
c1U3M3SWn2XlEbUOlGDvdViUzD1rPsGzW7wLv3DNfu2+Dlnvw8j7KMUJJGgCSO/IRwq7Mua3JO/a
FG746VM8xCYOsQ/YIXEjhkPEv3z0opZvOlmPxBrMalUIy2tz3nNJ3mHlcjzih2HE+J2FqHQK/Izv
h2N5LgYvjQOfP0ADYRQ28zmpaQ7kh9QhqAt9eG5ZgDozvX9h520wP7h8DoxZ4WQPu92WNZKBzG2c
KdvF/Hw95/qKKqqooooqqqiiiiqqqKKKKqqooooq4vR/BXY1+wBQAAA=

----Next_Part(Fri_Mar_29_17_24_40_2019_805)----






^ permalink  raw  reply  [nested|flat] 19+ messages in thread

* [PATCH 1/2] Remove entries that haven't been used for a certain time
@ 2019-03-01 04:32  Kyotaro Horiguchi <[email protected]>
  0 siblings, 0 replies; 19+ messages in thread

From: Kyotaro Horiguchi @ 2019-03-01 04:32 UTC (permalink / raw)

Catcache entries happen to be left alone for several reasons. It is
not desirable that such useless entries eat up memory. Catcache
pruning feature removes entries that haven't been accessed for a
certain time before enlarging hash array.
---
 doc/src/sgml/config.sgml                      |  19 ++++
 src/backend/tcop/postgres.c                   |   2 +
 src/backend/utils/cache/catcache.c            | 124 +++++++++++++++++++++++++-
 src/backend/utils/misc/guc.c                  |  12 +++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/utils/catcache.h                  |  18 ++++
 6 files changed, 172 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bc1d0f7bfa..819b252029 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1677,6 +1677,25 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-catalog-cache-prune-min-age" xreflabel="catalog_cache_prune_min_age">
+      <term><varname>catalog_cache_prune_min_age</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>catalog_cache_prune_min_age</varname> configuration
+       parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+		 Specifies the minimum amount of unused time in seconds at which a
+		 system catalog cache entry is removed. -1 indicates that this feature
+		 is disabled at all. The value defaults to 300 seconds (<literal>5
+		 minutes</literal>). The entries that are not used for the duration
+		 can be removed to prevent catalog cache from bloating with useless
+		 entries.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth">
       <term><varname>max_stack_depth</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 44a59e1d4f..a0efac86bc 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -71,6 +71,7 @@
 #include "tcop/pquery.h"
 #include "tcop/tcopprot.h"
 #include "tcop/utility.h"
+#include "utils/catcache.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
@@ -2577,6 +2578,7 @@ start_xact_command(void)
 	 * not desired, the timeout has to be disabled explicitly.
 	 */
 	enable_statement_timeout();
+	SetCatCacheClock(GetCurrentStatementStartTimestamp());
 }
 
 static void
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index d05930bc4c..e85f2b038c 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -38,6 +38,7 @@
 #include "utils/rel.h"
 #include "utils/resowner_private.h"
 #include "utils/syscache.h"
+#include "utils/timeout.h"
 
 
  /* #define CACHEDEBUG */	/* turns DEBUG elogs on */
@@ -60,9 +61,24 @@
 #define CACHE_elog(...)
 #endif
 
+/*
+ * GUC variable to define the minimum age of entries that will be considered
+ * to be evicted in seconds. -1 to disable the feature.
+ */
+int catalog_cache_prune_min_age = 300;
+
+/*
+ * Minimum interval between two successive moves of a cache entry in LRU list,
+ * in microseconds.
+ */
+#define MIN_LRU_UPDATE_INTERVAL 100000	/* 100ms */
+
 /* Cache management header --- pointer is NULL until created */
 static CatCacheHeader *CacheHdr = NULL;
 
+/* Clock for the last accessed time of a catcache entry. */
+TimestampTz	catcacheclock = 0;
+
 static inline HeapTuple SearchCatCacheInternal(CatCache *cache,
 					   int nkeys,
 					   Datum v1, Datum v2,
@@ -473,6 +489,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct)
 
 	/* delink from linked list */
 	dlist_delete(&ct->cache_elem);
+	dlist_delete(&ct->lru_node);
 
 	/*
 	 * Free keys when we're dealing with a negative entry, normal entries just
@@ -833,6 +850,7 @@ InitCatCache(int id,
 	cp->cc_nkeys = nkeys;
 	for (i = 0; i < nkeys; ++i)
 		cp->cc_keyno[i] = key[i];
+	dlist_init(&cp->cc_lru_list);
 
 	/*
 	 * new cache is initialized as far as we can go for now. print some
@@ -850,9 +868,83 @@ InitCatCache(int id,
 	 */
 	MemoryContextSwitchTo(oldcxt);
 
+	/* initialize catcache reference clock if haven't done yet */
+	if (catcacheclock == 0)
+		catcacheclock = GetCurrentTimestamp();
+
 	return cp;
 }
 
+/*
+ * CatCacheCleanupOldEntries - Remove infrequently-used entries
+ *
+ * Catcache entries happen to be left unused for a long time for several
+ * reasons. Remove such entries to prevent catcache from bloating. It is based
+ * on the similar algorithm with buffer eviction. Entries that are accessed
+ * several times in a certain period live longer than those that have had less
+ * access in the same duration.
+ */
+static bool
+CatCacheCleanupOldEntries(CatCache *cp)
+{
+	int			nremoved = 0;
+	dlist_mutable_iter	iter;
+
+	/* Return immediately if disabled */
+	if (catalog_cache_prune_min_age == 0)
+		return false;
+
+	/* Scan over LRU to find entries to remove */
+	dlist_foreach_modify(iter, &cp->cc_lru_list)
+	{
+		CatCTup    *ct = dlist_container(CatCTup, lru_node, iter.cur);
+		long		entry_age;
+		int			us;
+
+		/* Don't remove referenced entries */
+		if (ct->refcount != 0 ||
+			(ct->c_list && ct->c_list->refcount != 0))
+			continue;
+
+		/*
+		 * Calculate the duration from the time from the last access to
+		 * the "current" time. catcacheclock is updated per-statement
+		 * basis.
+		 */
+		TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us);
+
+		if (entry_age < catalog_cache_prune_min_age)
+		{
+			/*
+			 * We don't have older entries, exit.  At least one removal
+			 * prevents rehashing this time.
+			 */
+			break;
+		}
+
+		/*
+		 * Entries that are not accessed after the last pruning are removed in
+		 * that seconds, and their lives are prolonged according to how many
+		 * times they are accessed up to three times of the duration. We don't
+		 * try shrink buckets since pruning effectively caps catcache
+		 * expansion in the long term.
+		 */
+		if (ct->naccess > 0)
+			ct->naccess--;
+		else
+		{
+			CatCacheRemoveCTup(cp, ct);
+			nremoved++;
+		}
+	}
+
+	if (nremoved > 0)
+		elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d",
+			 cp->id, cp->cc_relname, nremoved, cp->cc_ntup + nremoved);
+
+	return nremoved > 0;
+}
+
 /*
  * Enlarge a catcache, doubling the number of buckets.
  */
@@ -1262,7 +1354,21 @@ SearchCatCacheInternal(CatCache *cache,
 		 * most frequently accessed elements in any hashbucket will tend to be
 		 * near the front of the hashbucket's list.)
 		 */
-		dlist_move_head(bucket, &ct->cache_elem);
+		/* dlist_move_head(bucket, &ct->cache_elem);*/
+
+		/* prolong life of this entry */
+		if (ct->naccess < 2)
+			ct->naccess++;
+
+		/*
+		 * Don't update LRU too frequently. We need to maintain the LRU even
+		 * if pruning is inactive since it can be turned on on-session.
+		 */
+		if (catcacheclock - ct->lastaccess > MIN_LRU_UPDATE_INTERVAL)
+		{
+			ct->lastaccess = catcacheclock;
+			dlist_move_tail(&cache->cc_lru_list, &ct->lru_node);
+		}
 
 		/*
 		 * If it's a positive entry, bump its refcount and return it. If it's
@@ -1888,19 +1994,29 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	ct->dead = false;
 	ct->negative = negative;
 	ct->hash_value = hashValue;
+	ct->naccess = 0;
+	ct->lastaccess = catcacheclock;
+	dlist_push_tail(&cache->cc_lru_list, &ct->lru_node);
 
 	dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem);
 
 	cache->cc_ntup++;
 	CacheHdr->ch_ntup++;
 
+	/* increase refcount so that the new entry survives pruning */
+	ct->refcount++;
+
 	/*
-	 * If the hash table has become too full, enlarge the buckets array. Quite
-	 * arbitrarily, we enlarge when fill factor > 2.
+	 * If the hash table has become too full, try removing infrequently used
+	 * entries to make a room for the new entry. If failed, enlarge the bucket
+	 * array instead.  Quite arbitrarily, we try this when fill factor > 2.
 	 */
-	if (cache->cc_ntup > cache->cc_nbuckets * 2)
+	if (cache->cc_ntup > cache->cc_nbuckets * 2 &&
+		!CatCacheCleanupOldEntries(cache))
 		RehashCatCache(cache);
 
+	ct->refcount--;
+
 	return ct;
 }
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 1766e46037..e671d4428e 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -82,6 +82,7 @@
 #include "tsearch/ts_cache.h"
 #include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/catcache.h"
 #include "utils/guc_tables.h"
 #include "utils/float.h"
 #include "utils/memutils.h"
@@ -2249,6 +2250,17 @@ static struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"catalog_cache_prune_min_age", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("System catalog cache entries that live unused for longer than this seconds are considered for removal."),
+			gettext_noop("The value of -1 turns off pruning."),
+			GUC_UNIT_S
+		},
+		&catalog_cache_prune_min_age,
+		300, -1, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	/*
 	 * We use the hopefully-safely-small value of 100kB as the compiled-in
 	 * default for max_stack_depth.  InitializeGUCOptions will increase it if
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index bbbeb4bb15..d88ec57382 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -128,6 +128,7 @@
 #work_mem = 4MB				# min 64kB
 #maintenance_work_mem = 64MB		# min 1MB
 #autovacuum_work_mem = -1		# min 1MB, or -1 to use maintenance_work_mem
+#catalog_cache_prune_min_age = 300s	# -1 disables pruning
 #max_stack_depth = 2MB			# min 100kB
 #shared_memory_type = mmap		# the default is the first option
 					# supported by the operating system:
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h
index 65d816a583..a21c53644a 100644
--- a/src/include/utils/catcache.h
+++ b/src/include/utils/catcache.h
@@ -22,6 +22,7 @@
 
 #include "access/htup.h"
 #include "access/skey.h"
+#include "datatype/timestamp.h"
 #include "lib/ilist.h"
 #include "utils/relcache.h"
 
@@ -61,6 +62,7 @@ typedef struct catcache
 	slist_node	cc_next;		/* list link */
 	ScanKeyData cc_skey[CATCACHE_MAXKEYS];	/* precomputed key info for heap
 											 * scans */
+	dlist_head	cc_lru_list;
 
 	/*
 	 * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS
@@ -119,6 +121,9 @@ typedef struct catctup
 	bool		dead;			/* dead but not yet removed? */
 	bool		negative;		/* negative cache entry? */
 	HeapTupleData tuple;		/* tuple management header */
+	int			naccess;		/* # of access to this entry, up to 2  */
+	TimestampTz	lastaccess;		/* timestamp of the last usage */
+	dlist_node	lru_node;		/* LRU node */
 
 	/*
 	 * The tuple may also be a member of at most one CatCList.  (If a single
@@ -189,6 +194,19 @@ typedef struct catcacheheader
 /* this extern duplicates utils/memutils.h... */
 extern PGDLLIMPORT MemoryContext CacheMemoryContext;
 
+/* for guc.c, not PGDLLPMPORT'ed */
+extern int catalog_cache_prune_min_age;
+
+/* source clock for access timestamp of catcache entries */
+extern TimestampTz catcacheclock;
+
+/* SetCatCacheClock - set catcache timestamp source clodk */
+static inline void
+SetCatCacheClock(TimestampTz ts)
+{
+	catcacheclock = ts;
+}
+
 extern void CreateCacheMemoryContext(void);
 
 extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid,
-- 
2.16.3


----Next_Part(Thu_Apr_04_21_52_55_2019_099)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="0002-Benchmark-extension-for-catcache-pruning-feature.patch"



^ permalink  raw  reply  [nested|flat] 19+ messages in thread

* [PATCH] Remove entries that haven't been used for a certain time
@ 2019-03-01 04:32  Kyotaro Horiguchi <[email protected]>
  0 siblings, 0 replies; 19+ messages in thread

From: Kyotaro Horiguchi @ 2019-03-01 04:32 UTC (permalink / raw)

Catcache entries happen to be left alone for several reasons. It is
not desirable that such useless entries eat up memory. Catcache
pruning feature removes entries that haven't been accessed for a
certain time before enlarging hash array.
---
 doc/src/sgml/config.sgml                      |  19 +++++
 src/backend/tcop/postgres.c                   |   2 +
 src/backend/utils/cache/catcache.c            | 103 +++++++++++++++++++++++++-
 src/backend/utils/misc/guc.c                  |  12 +++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/utils/catcache.h                  |  16 ++++
 6 files changed, 150 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bc1d0f7bfa..819b252029 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1677,6 +1677,25 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-catalog-cache-prune-min-age" xreflabel="catalog_cache_prune_min_age">
+      <term><varname>catalog_cache_prune_min_age</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>catalog_cache_prune_min_age</varname> configuration
+       parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+		 Specifies the minimum amount of unused time in seconds at which a
+		 system catalog cache entry is removed. -1 indicates that this feature
+		 is disabled at all. The value defaults to 300 seconds (<literal>5
+		 minutes</literal>). The entries that are not used for the duration
+		 can be removed to prevent catalog cache from bloating with useless
+		 entries.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth">
       <term><varname>max_stack_depth</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 44a59e1d4f..a0efac86bc 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -71,6 +71,7 @@
 #include "tcop/pquery.h"
 #include "tcop/tcopprot.h"
 #include "tcop/utility.h"
+#include "utils/catcache.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
@@ -2577,6 +2578,7 @@ start_xact_command(void)
 	 * not desired, the timeout has to be disabled explicitly.
 	 */
 	enable_statement_timeout();
+	SetCatCacheClock(GetCurrentStatementStartTimestamp());
 }
 
 static void
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index d05930bc4c..03c2d8524c 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -38,6 +38,7 @@
 #include "utils/rel.h"
 #include "utils/resowner_private.h"
 #include "utils/syscache.h"
+#include "utils/timeout.h"
 
 
  /* #define CACHEDEBUG */	/* turns DEBUG elogs on */
@@ -60,9 +61,18 @@
 #define CACHE_elog(...)
 #endif
 
+/*
+ * GUC variable to define the minimum age of entries that will be considered
+ * to be evicted in seconds. -1 to disable the feature.
+ */
+int catalog_cache_prune_min_age = 300;
+
 /* Cache management header --- pointer is NULL until created */
 static CatCacheHeader *CacheHdr = NULL;
 
+/* Clock for the last accessed time of a catcache entry. */
+TimestampTz	catcacheclock = 0;
+
 static inline HeapTuple SearchCatCacheInternal(CatCache *cache,
 					   int nkeys,
 					   Datum v1, Datum v2,
@@ -850,9 +860,83 @@ InitCatCache(int id,
 	 */
 	MemoryContextSwitchTo(oldcxt);
 
+	/* initialize catcache reference clock if haven't done yet */
+	if (catcacheclock == 0)
+		catcacheclock = GetCurrentTimestamp();
+
 	return cp;
 }
 
+/*
+ * CatCacheCleanupOldEntries - Remove infrequently-used entries
+ *
+ * Catcache entries happen to be left unused for a long time for several
+ * reasons. Remove such entries to prevent catcache from bloating. It is based
+ * on the similar algorithm with buffer eviction. Entries that are accessed
+ * several times in a certain period live longer than those that have had less
+ * access in the same duration.
+ */
+static bool
+CatCacheCleanupOldEntries(CatCache *cp)
+{
+	int	nremoved = 0;
+	int i;
+
+	/* Return immediately if disabled */
+	if (catalog_cache_prune_min_age == 0)
+		return false;
+
+	/* Scan over the whole hash to find entries to remove */
+	for (i = 0 ; i < cp->cc_nbuckets ; i++)
+	{
+		dlist_mutable_iter	iter;
+
+		dlist_foreach_modify(iter, &cp->cc_bucket[i])
+		{
+			CatCTup    *ct = dlist_container(CatCTup, cache_elem, iter.cur);
+			long		entry_age;
+			int			us;
+
+			/* Don't remove referenced entries */
+			if (ct->refcount != 0 ||
+				(ct->c_list && ct->c_list->refcount != 0))
+				continue;
+
+			/*
+			 * Calculate the duration from the time from the last access to
+			 * the "current" time. catcacheclock is updated per-statement
+			 * basis and additionaly udpated periodically during a long
+			 * running query.
+			 */
+			TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us);
+
+			if (entry_age < catalog_cache_prune_min_age)
+				continue;
+
+			/*
+			 * Entries that are not accessed after the last pruning are
+			 * removed in that seconds, and their lives are prolonged
+			 * according to how many times they are accessed up to three times
+			 * of the duration. We don't try shrink buckets since pruning
+			 * effectively caps catcache expansion in the long term.
+			 */
+			if (ct->naccess > 0)
+				ct->naccess--;
+			else
+			{
+				CatCacheRemoveCTup(cp, ct);
+				nremoved++;
+			}
+		}
+	}
+
+	if (nremoved > 0)
+		elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d",
+			 cp->id, cp->cc_relname, nremoved, cp->cc_ntup + nremoved);
+
+	return nremoved > 0;
+}
+
 /*
  * Enlarge a catcache, doubling the number of buckets.
  */
@@ -1263,6 +1347,10 @@ SearchCatCacheInternal(CatCache *cache,
 		 * near the front of the hashbucket's list.)
 		 */
 		dlist_move_head(bucket, &ct->cache_elem);
+		if (ct->naccess < 2)
+			ct->naccess++;
+
+		ct->lastaccess = catcacheclock;
 
 		/*
 		 * If it's a positive entry, bump its refcount and return it. If it's
@@ -1888,19 +1976,28 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	ct->dead = false;
 	ct->negative = negative;
 	ct->hash_value = hashValue;
+	ct->naccess = 0;
+	ct->lastaccess = catcacheclock;
 
 	dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem);
 
 	cache->cc_ntup++;
 	CacheHdr->ch_ntup++;
 
+	/* increase refcount so that the new entry survives pruning */
+	ct->refcount++;
+
 	/*
-	 * If the hash table has become too full, enlarge the buckets array. Quite
-	 * arbitrarily, we enlarge when fill factor > 2.
+	 * If the hash table has become too full, try removing infrequently used
+	 * entries to make a room for the new entry. If failed, enlarge the bucket
+	 * array instead.  Quite arbitrarily, we try this when fill factor > 2.
 	 */
-	if (cache->cc_ntup > cache->cc_nbuckets * 2)
+	if (cache->cc_ntup > cache->cc_nbuckets * 2 &&
+		!CatCacheCleanupOldEntries(cache))
 		RehashCatCache(cache);
 
+	ct->refcount--;
+
 	return ct;
 }
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 1766e46037..e671d4428e 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -82,6 +82,7 @@
 #include "tsearch/ts_cache.h"
 #include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/catcache.h"
 #include "utils/guc_tables.h"
 #include "utils/float.h"
 #include "utils/memutils.h"
@@ -2249,6 +2250,17 @@ static struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"catalog_cache_prune_min_age", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("System catalog cache entries that live unused for longer than this seconds are considered for removal."),
+			gettext_noop("The value of -1 turns off pruning."),
+			GUC_UNIT_S
+		},
+		&catalog_cache_prune_min_age,
+		300, -1, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	/*
 	 * We use the hopefully-safely-small value of 100kB as the compiled-in
 	 * default for max_stack_depth.  InitializeGUCOptions will increase it if
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index bbbeb4bb15..d88ec57382 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -128,6 +128,7 @@
 #work_mem = 4MB				# min 64kB
 #maintenance_work_mem = 64MB		# min 1MB
 #autovacuum_work_mem = -1		# min 1MB, or -1 to use maintenance_work_mem
+#catalog_cache_prune_min_age = 300s	# -1 disables pruning
 #max_stack_depth = 2MB			# min 100kB
 #shared_memory_type = mmap		# the default is the first option
 					# supported by the operating system:
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h
index 65d816a583..2134839ecf 100644
--- a/src/include/utils/catcache.h
+++ b/src/include/utils/catcache.h
@@ -22,6 +22,7 @@
 
 #include "access/htup.h"
 #include "access/skey.h"
+#include "datatype/timestamp.h"
 #include "lib/ilist.h"
 #include "utils/relcache.h"
 
@@ -119,6 +120,8 @@ typedef struct catctup
 	bool		dead;			/* dead but not yet removed? */
 	bool		negative;		/* negative cache entry? */
 	HeapTupleData tuple;		/* tuple management header */
+	int			naccess;		/* # of access to this entry, up to 2  */
+	TimestampTz	lastaccess;		/* timestamp of the last usage */
 
 	/*
 	 * The tuple may also be a member of at most one CatCList.  (If a single
@@ -189,6 +192,19 @@ typedef struct catcacheheader
 /* this extern duplicates utils/memutils.h... */
 extern PGDLLIMPORT MemoryContext CacheMemoryContext;
 
+/* for guc.c, not PGDLLPMPORT'ed */
+extern int catalog_cache_prune_min_age;
+
+/* source clock for access timestamp of catcache entries */
+extern TimestampTz catcacheclock;
+
+/* SetCatCacheClock - set catcache timestamp source clodk */
+static inline void
+SetCatCacheClock(TimestampTz ts)
+{
+	catcacheclock = ts;
+}
+
 extern void CreateCacheMemoryContext(void);
 
 extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid,
-- 
2.16.3


----Next_Part(Fri_Apr_05_09_44_07_2019_159)----






^ permalink  raw  reply  [nested|flat] 19+ messages in thread

* [PATCH 2/6] Remove entries that haven't been used for a certain time
@ 2019-03-01 04:32  Kyotaro Horiguchi <[email protected]>
  0 siblings, 0 replies; 19+ messages in thread

From: Kyotaro Horiguchi @ 2019-03-01 04:32 UTC (permalink / raw)

Catcache entries happen to be left alone for several reasons. It is
not desirable that such useless entries eat up memory. Catcache
pruning feature removes entries that haven't been accessed for a
certain time before enlarging hash array.
---
 doc/src/sgml/config.sgml                      |  19 ++++
 src/backend/tcop/postgres.c                   |   2 +
 src/backend/utils/cache/catcache.c            | 122 +++++++++++++++++++++++++-
 src/backend/utils/misc/guc.c                  |  12 +++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/utils/catcache.h                  |  18 ++++
 6 files changed, 171 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 6d42b7afe7..737a156bb4 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1661,6 +1661,25 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-catalog-cache-prune-min-age" xreflabel="catalog_cache_prune_min_age">
+      <term><varname>catalog_cache_prune_min_age</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>catalog_cache_prune_min_age</varname> configuration
+       parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+		 Specifies the minimum amount of unused time in seconds at which a
+		 system catalog cache entry is removed. -1 indicates that this feature
+		 is disabled at all. The value defaults to 300 seconds (<literal>5
+		 minutes</literal>). The entries that are not used for the duration
+		 can be removed to prevent catalog cache from bloating with useless
+		 entries.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth">
       <term><varname>max_stack_depth</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 8b4d94c9a1..02b9ef98aa 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -71,6 +71,7 @@
 #include "tcop/pquery.h"
 #include "tcop/tcopprot.h"
 #include "tcop/utility.h"
+#include "utils/catcache.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
@@ -2584,6 +2585,7 @@ start_xact_command(void)
 	 * not desired, the timeout has to be disabled explicitly.
 	 */
 	enable_statement_timeout();
+	SetCatCacheClock(GetCurrentStatementStartTimestamp());
 }
 
 static void
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 78dd5714fa..4386957497 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -39,6 +39,7 @@
 #include "utils/rel.h"
 #include "utils/resowner_private.h"
 #include "utils/syscache.h"
+#include "utils/timeout.h"
 
 
  /* #define CACHEDEBUG */	/* turns DEBUG elogs on */
@@ -61,9 +62,24 @@
 #define CACHE_elog(...)
 #endif
 
+/*
+ * GUC variable to define the minimum age of entries that will be considered
+ * to be evicted in seconds. -1 to disable the feature.
+ */
+int catalog_cache_prune_min_age = 300;
+
+/*
+ * Minimum interval between two successive moves of a cache entry in LRU list,
+ * in microseconds.
+ */
+#define MIN_LRU_UPDATE_INTERVAL 100000	/* 100ms */
+
 /* Cache management header --- pointer is NULL until created */
 static CatCacheHeader *CacheHdr = NULL;
 
+/* Clock for the last accessed time of a catcache entry. */
+TimestampTz	catcacheclock = 0;
+
 static inline HeapTuple SearchCatCacheInternal(CatCache *cache,
 					   int nkeys,
 					   Datum v1, Datum v2,
@@ -469,6 +485,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct)
 
 	/* delink from linked list */
 	dlist_delete(&ct->cache_elem);
+	dlist_delete(&ct->lru_node);
 
 	/*
 	 * Free keys when we're dealing with a negative entry, normal entries just
@@ -829,6 +846,7 @@ InitCatCache(int id,
 	cp->cc_nkeys = nkeys;
 	for (i = 0; i < nkeys; ++i)
 		cp->cc_keyno[i] = key[i];
+	dlist_init(&cp->cc_lru_list);
 
 	/*
 	 * new cache is initialized as far as we can go for now. print some
@@ -846,9 +864,83 @@ InitCatCache(int id,
 	 */
 	MemoryContextSwitchTo(oldcxt);
 
+	/* initialize catcache reference clock if haven't done yet */
+	if (catcacheclock == 0)
+		catcacheclock = GetCurrentTimestamp();
+
 	return cp;
 }
 
+/*
+ * CatCacheCleanupOldEntries - Remove infrequently-used entries
+ *
+ * Catcache entries happen to be left unused for a long time for several
+ * reasons. Remove such entries to prevent catcache from bloating. It is based
+ * on the similar algorithm with buffer eviction. Entries that are accessed
+ * several times in a certain period live longer than those that have had less
+ * access in the same duration.
+ */
+static bool
+CatCacheCleanupOldEntries(CatCache *cp)
+{
+	int			nremoved = 0;
+	dlist_mutable_iter	iter;
+
+	/* Return immediately if disabled */
+	if (catalog_cache_prune_min_age == 0)
+		return false;
+
+	/* Scan over LRU to find entries to remove */
+	dlist_foreach_modify(iter, &cp->cc_lru_list)
+	{
+		CatCTup    *ct = dlist_container(CatCTup, lru_node, iter.cur);
+		long		entry_age;
+		int			us;
+
+		/* Don't remove referenced entries */
+		if (ct->refcount != 0 ||
+			(ct->c_list && ct->c_list->refcount != 0))
+			continue;
+
+		/*
+		 * Calculate the duration from the time from the last access to
+		 * the "current" time. catcacheclock is updated per-statement
+		 * basis.
+		 */
+		TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us);
+
+		if (entry_age < catalog_cache_prune_min_age)
+		{
+			/*
+			 * We don't have older entries, exit.  At least one removal
+			 * prevents rehashing this time.
+			 */
+			break;
+		}
+
+		/*
+		 * Entries that are not accessed after the last pruning are removed in
+		 * that seconds, and their lives are prolonged according to how many
+		 * times they are accessed up to three times of the duration. We don't
+		 * try shrink buckets since pruning effectively caps catcache
+		 * expansion in the long term.
+		 */
+		if (ct->naccess > 0)
+			ct->naccess--;
+		else
+		{
+			CatCacheRemoveCTup(cp, ct);
+			nremoved++;
+		}
+	}
+
+	if (nremoved > 0)
+		elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d",
+			 cp->id, cp->cc_relname, nremoved, cp->cc_ntup + nremoved);
+
+	return nremoved > 0;
+}
+
 /*
  * Enlarge a catcache, doubling the number of buckets.
  */
@@ -1260,6 +1352,20 @@ SearchCatCacheInternal(CatCache *cache,
 		 */
 		dlist_move_head(bucket, &ct->cache_elem);
 
+		/* prolong life of this entry */
+		if (ct->naccess < 2)
+			ct->naccess++;
+
+		/*
+		 * Don't update LRU too frequently. We need to maintain the LRU even
+		 * if pruning is inactive since it can be turned on on-session.
+		 */
+		if (catcacheclock - ct->lastaccess > MIN_LRU_UPDATE_INTERVAL)
+		{
+			ct->lastaccess = catcacheclock;
+			dlist_move_tail(&cache->cc_lru_list, &ct->lru_node);
+		}
+
 		/*
 		 * If it's a positive entry, bump its refcount and return it. If it's
 		 * negative, we can report failure to the caller.
@@ -1884,19 +1990,29 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	ct->dead = false;
 	ct->negative = negative;
 	ct->hash_value = hashValue;
+	ct->naccess = 0;
+	ct->lastaccess = catcacheclock;
+	dlist_push_tail(&cache->cc_lru_list, &ct->lru_node);
 
 	dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem);
 
 	cache->cc_ntup++;
 	CacheHdr->ch_ntup++;
 
+	/* increase refcount so that the new entry survives pruning */
+	ct->refcount++;
+
 	/*
-	 * If the hash table has become too full, enlarge the buckets array. Quite
-	 * arbitrarily, we enlarge when fill factor > 2.
+	 * If the hash table has become too full, try removing infrequently used
+	 * entries to make a room for the new entry. If failed, enlarge the bucket
+	 * array instead.  Quite arbitrarily, we try this when fill factor > 2.
 	 */
-	if (cache->cc_ntup > cache->cc_nbuckets * 2)
+	if (cache->cc_ntup > cache->cc_nbuckets * 2 &&
+		!CatCacheCleanupOldEntries(cache))
 		RehashCatCache(cache);
 
+	ct->refcount--;
+
 	return ct;
 }
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 156d147c85..3acc86cd07 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -81,6 +81,7 @@
 #include "tsearch/ts_cache.h"
 #include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/catcache.h"
 #include "utils/guc_tables.h"
 #include "utils/float.h"
 #include "utils/memutils.h"
@@ -2205,6 +2206,17 @@ static struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"catalog_cache_prune_min_age", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("System catalog cache entries that live unused for longer than this seconds are considered for removal."),
+			gettext_noop("The value of -1 turns off pruning."),
+			GUC_UNIT_S
+		},
+		&catalog_cache_prune_min_age,
+		300, -1, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	/*
 	 * We use the hopefully-safely-small value of 100kB as the compiled-in
 	 * default for max_stack_depth.  InitializeGUCOptions will increase it if
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index bd6ea65d0c..e9e3acc903 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -128,6 +128,7 @@
 #work_mem = 4MB				# min 64kB
 #maintenance_work_mem = 64MB		# min 1MB
 #autovacuum_work_mem = -1		# min 1MB, or -1 to use maintenance_work_mem
+#catalog_cache_prune_min_age = 300s	# -1 disables pruning
 #max_stack_depth = 2MB			# min 100kB
 #shared_memory_type = mmap		# the default is the first option
 					# supported by the operating system:
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h
index 65d816a583..a21c53644a 100644
--- a/src/include/utils/catcache.h
+++ b/src/include/utils/catcache.h
@@ -22,6 +22,7 @@
 
 #include "access/htup.h"
 #include "access/skey.h"
+#include "datatype/timestamp.h"
 #include "lib/ilist.h"
 #include "utils/relcache.h"
 
@@ -61,6 +62,7 @@ typedef struct catcache
 	slist_node	cc_next;		/* list link */
 	ScanKeyData cc_skey[CATCACHE_MAXKEYS];	/* precomputed key info for heap
 											 * scans */
+	dlist_head	cc_lru_list;
 
 	/*
 	 * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS
@@ -119,6 +121,9 @@ typedef struct catctup
 	bool		dead;			/* dead but not yet removed? */
 	bool		negative;		/* negative cache entry? */
 	HeapTupleData tuple;		/* tuple management header */
+	int			naccess;		/* # of access to this entry, up to 2  */
+	TimestampTz	lastaccess;		/* timestamp of the last usage */
+	dlist_node	lru_node;		/* LRU node */
 
 	/*
 	 * The tuple may also be a member of at most one CatCList.  (If a single
@@ -189,6 +194,19 @@ typedef struct catcacheheader
 /* this extern duplicates utils/memutils.h... */
 extern PGDLLIMPORT MemoryContext CacheMemoryContext;
 
+/* for guc.c, not PGDLLPMPORT'ed */
+extern int catalog_cache_prune_min_age;
+
+/* source clock for access timestamp of catcache entries */
+extern TimestampTz catcacheclock;
+
+/* SetCatCacheClock - set catcache timestamp source clodk */
+static inline void
+SetCatCacheClock(TimestampTz ts)
+{
+	catcacheclock = ts;
+}
+
 extern void CreateCacheMemoryContext(void);
 
 extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid,
-- 
2.16.3


----Next_Part(Fri_Mar_01_17_32_45_2019_112)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="v18-0003-Asynchronous-update-of-catcache-clock.patch"



^ permalink  raw  reply  [nested|flat] 19+ messages in thread

* [PATCH 2/2] Remove entries that haven't been used for a certain time
@ 2019-03-01 04:32  Kyotaro Horiguchi <[email protected]>
  0 siblings, 0 replies; 19+ messages in thread

From: Kyotaro Horiguchi @ 2019-03-01 04:32 UTC (permalink / raw)

Catcache entries happen to be left alone for several reasons. It is
not desirable that such useless entries eat up memory. Catcache
pruning feature removes entries that haven't been accessed for a
certain time before enlarging hash array.
---
 doc/src/sgml/config.sgml                      |  19 ++++
 src/backend/tcop/postgres.c                   |   2 +
 src/backend/utils/cache/catcache.c            | 122 +++++++++++++++++++++++++-
 src/backend/utils/misc/guc.c                  |  12 +++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/utils/catcache.h                  |  18 ++++
 6 files changed, 171 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index d383de2512..4231235447 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1677,6 +1677,25 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-catalog-cache-prune-min-age" xreflabel="catalog_cache_prune_min_age">
+      <term><varname>catalog_cache_prune_min_age</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>catalog_cache_prune_min_age</varname> configuration
+       parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+		 Specifies the minimum amount of unused time in seconds at which a
+		 system catalog cache entry is removed. -1 indicates that this feature
+		 is disabled at all. The value defaults to 300 seconds (<literal>5
+		 minutes</literal>). The entries that are not used for the duration
+		 can be removed to prevent catalog cache from bloating with useless
+		 entries.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth">
       <term><varname>max_stack_depth</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index f9ce3d8f22..acab473d34 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -71,6 +71,7 @@
 #include "tcop/pquery.h"
 #include "tcop/tcopprot.h"
 #include "tcop/utility.h"
+#include "utils/catcache.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
@@ -2575,6 +2576,7 @@ start_xact_command(void)
 	 * not desired, the timeout has to be disabled explicitly.
 	 */
 	enable_statement_timeout();
+	SetCatCacheClock(GetCurrentStatementStartTimestamp());
 }
 
 static void
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index d05930bc4c..c8ee0c98fb 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -38,6 +38,7 @@
 #include "utils/rel.h"
 #include "utils/resowner_private.h"
 #include "utils/syscache.h"
+#include "utils/timeout.h"
 
 
  /* #define CACHEDEBUG */	/* turns DEBUG elogs on */
@@ -60,9 +61,24 @@
 #define CACHE_elog(...)
 #endif
 
+/*
+ * GUC variable to define the minimum age of entries that will be considered
+ * to be evicted in seconds. -1 to disable the feature.
+ */
+int catalog_cache_prune_min_age = 300;
+
+/*
+ * Minimum interval between two successive moves of a cache entry in LRU list,
+ * in microseconds.
+ */
+#define MIN_LRU_UPDATE_INTERVAL 100000	/* 100ms */
+
 /* Cache management header --- pointer is NULL until created */
 static CatCacheHeader *CacheHdr = NULL;
 
+/* Clock for the last accessed time of a catcache entry. */
+TimestampTz	catcacheclock = 0;
+
 static inline HeapTuple SearchCatCacheInternal(CatCache *cache,
 					   int nkeys,
 					   Datum v1, Datum v2,
@@ -473,6 +489,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct)
 
 	/* delink from linked list */
 	dlist_delete(&ct->cache_elem);
+	dlist_delete(&ct->lru_node);
 
 	/*
 	 * Free keys when we're dealing with a negative entry, normal entries just
@@ -833,6 +850,7 @@ InitCatCache(int id,
 	cp->cc_nkeys = nkeys;
 	for (i = 0; i < nkeys; ++i)
 		cp->cc_keyno[i] = key[i];
+	dlist_init(&cp->cc_lru_list);
 
 	/*
 	 * new cache is initialized as far as we can go for now. print some
@@ -850,9 +868,83 @@ InitCatCache(int id,
 	 */
 	MemoryContextSwitchTo(oldcxt);
 
+	/* initialize catcache reference clock if haven't done yet */
+	if (catcacheclock == 0)
+		catcacheclock = GetCurrentTimestamp();
+
 	return cp;
 }
 
+/*
+ * CatCacheCleanupOldEntries - Remove infrequently-used entries
+ *
+ * Catcache entries happen to be left unused for a long time for several
+ * reasons. Remove such entries to prevent catcache from bloating. It is based
+ * on the similar algorithm with buffer eviction. Entries that are accessed
+ * several times in a certain period live longer than those that have had less
+ * access in the same duration.
+ */
+static bool
+CatCacheCleanupOldEntries(CatCache *cp)
+{
+	int			nremoved = 0;
+	dlist_mutable_iter	iter;
+
+	/* Return immediately if disabled */
+	if (catalog_cache_prune_min_age == 0)
+		return false;
+
+	/* Scan over LRU to find entries to remove */
+	dlist_foreach_modify(iter, &cp->cc_lru_list)
+	{
+		CatCTup    *ct = dlist_container(CatCTup, lru_node, iter.cur);
+		long		entry_age;
+		int			us;
+
+		/* Don't remove referenced entries */
+		if (ct->refcount != 0 ||
+			(ct->c_list && ct->c_list->refcount != 0))
+			continue;
+
+		/*
+		 * Calculate the duration from the time from the last access to
+		 * the "current" time. catcacheclock is updated per-statement
+		 * basis.
+		 */
+		TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us);
+
+		if (entry_age < catalog_cache_prune_min_age)
+		{
+			/*
+			 * We don't have older entries, exit.  At least one removal
+			 * prevents rehashing this time.
+			 */
+			break;
+		}
+
+		/*
+		 * Entries that are not accessed after the last pruning are removed in
+		 * that seconds, and their lives are prolonged according to how many
+		 * times they are accessed up to three times of the duration. We don't
+		 * try shrink buckets since pruning effectively caps catcache
+		 * expansion in the long term.
+		 */
+		if (ct->naccess > 0)
+			ct->naccess--;
+		else
+		{
+			CatCacheRemoveCTup(cp, ct);
+			nremoved++;
+		}
+	}
+
+	if (nremoved > 0)
+		elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d",
+			 cp->id, cp->cc_relname, nremoved, cp->cc_ntup + nremoved);
+
+	return nremoved > 0;
+}
+
 /*
  * Enlarge a catcache, doubling the number of buckets.
  */
@@ -1264,6 +1356,20 @@ SearchCatCacheInternal(CatCache *cache,
 		 */
 		dlist_move_head(bucket, &ct->cache_elem);
 
+		/* prolong life of this entry */
+		if (ct->naccess < 2)
+			ct->naccess++;
+
+		/*
+		 * Don't update LRU too frequently. We need to maintain the LRU even
+		 * if pruning is inactive since it can be turned on on-session.
+		 */
+		if (catcacheclock - ct->lastaccess > MIN_LRU_UPDATE_INTERVAL)
+		{
+			ct->lastaccess = catcacheclock;
+			dlist_move_tail(&cache->cc_lru_list, &ct->lru_node);
+		}
+
 		/*
 		 * If it's a positive entry, bump its refcount and return it. If it's
 		 * negative, we can report failure to the caller.
@@ -1888,19 +1994,29 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	ct->dead = false;
 	ct->negative = negative;
 	ct->hash_value = hashValue;
+	ct->naccess = 0;
+	ct->lastaccess = catcacheclock;
+	dlist_push_tail(&cache->cc_lru_list, &ct->lru_node);
 
 	dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem);
 
 	cache->cc_ntup++;
 	CacheHdr->ch_ntup++;
 
+	/* increase refcount so that the new entry survives pruning */
+	ct->refcount++;
+
 	/*
-	 * If the hash table has become too full, enlarge the buckets array. Quite
-	 * arbitrarily, we enlarge when fill factor > 2.
+	 * If the hash table has become too full, try removing infrequently used
+	 * entries to make a room for the new entry. If failed, enlarge the bucket
+	 * array instead.  Quite arbitrarily, we try this when fill factor > 2.
 	 */
-	if (cache->cc_ntup > cache->cc_nbuckets * 2)
+	if (cache->cc_ntup > cache->cc_nbuckets * 2 &&
+		!CatCacheCleanupOldEntries(cache))
 		RehashCatCache(cache);
 
+	ct->refcount--;
+
 	return ct;
 }
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index aa564d153a..e624c74bf9 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -82,6 +82,7 @@
 #include "tsearch/ts_cache.h"
 #include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/catcache.h"
 #include "utils/guc_tables.h"
 #include "utils/float.h"
 #include "utils/memutils.h"
@@ -2202,6 +2203,17 @@ static struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"catalog_cache_prune_min_age", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("System catalog cache entries that live unused for longer than this seconds are considered for removal."),
+			gettext_noop("The value of -1 turns off pruning."),
+			GUC_UNIT_S
+		},
+		&catalog_cache_prune_min_age,
+		300, -1, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	/*
 	 * We use the hopefully-safely-small value of 100kB as the compiled-in
 	 * default for max_stack_depth.  InitializeGUCOptions will increase it if
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index cccb5f145a..fa117f0573 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -128,6 +128,7 @@
 #work_mem = 4MB				# min 64kB
 #maintenance_work_mem = 64MB		# min 1MB
 #autovacuum_work_mem = -1		# min 1MB, or -1 to use maintenance_work_mem
+#catalog_cache_prune_min_age = 300s	# -1 disables pruning
 #max_stack_depth = 2MB			# min 100kB
 #shared_memory_type = mmap		# the default is the first option
 					# supported by the operating system:
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h
index 65d816a583..a21c53644a 100644
--- a/src/include/utils/catcache.h
+++ b/src/include/utils/catcache.h
@@ -22,6 +22,7 @@
 
 #include "access/htup.h"
 #include "access/skey.h"
+#include "datatype/timestamp.h"
 #include "lib/ilist.h"
 #include "utils/relcache.h"
 
@@ -61,6 +62,7 @@ typedef struct catcache
 	slist_node	cc_next;		/* list link */
 	ScanKeyData cc_skey[CATCACHE_MAXKEYS];	/* precomputed key info for heap
 											 * scans */
+	dlist_head	cc_lru_list;
 
 	/*
 	 * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS
@@ -119,6 +121,9 @@ typedef struct catctup
 	bool		dead;			/* dead but not yet removed? */
 	bool		negative;		/* negative cache entry? */
 	HeapTupleData tuple;		/* tuple management header */
+	int			naccess;		/* # of access to this entry, up to 2  */
+	TimestampTz	lastaccess;		/* timestamp of the last usage */
+	dlist_node	lru_node;		/* LRU node */
 
 	/*
 	 * The tuple may also be a member of at most one CatCList.  (If a single
@@ -189,6 +194,19 @@ typedef struct catcacheheader
 /* this extern duplicates utils/memutils.h... */
 extern PGDLLIMPORT MemoryContext CacheMemoryContext;
 
+/* for guc.c, not PGDLLPMPORT'ed */
+extern int catalog_cache_prune_min_age;
+
+/* source clock for access timestamp of catcache entries */
+extern TimestampTz catcacheclock;
+
+/* SetCatCacheClock - set catcache timestamp source clodk */
+static inline void
+SetCatCacheClock(TimestampTz ts)
+{
+	catcacheclock = ts;
+}
+
 extern void CreateCacheMemoryContext(void);
 
 extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid,
-- 
2.16.3


----Next_Part(Fri_Mar_29_17_24_40_2019_805)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="FullScan-0001-Remove-entries-that-haven-t-been-used-for-a-certain-.patch"



^ permalink  raw  reply  [nested|flat] 19+ messages in thread

* [PATCH] Remove entries that haven't been used for a certain time
@ 2019-03-01 04:32  Kyotaro Horiguchi <[email protected]>
  0 siblings, 0 replies; 19+ messages in thread

From: Kyotaro Horiguchi @ 2019-03-01 04:32 UTC (permalink / raw)

Catcache entries happen to be left alone for several reasons. It is
not desirable that such useless entries eat up memory. Catcache
pruning feature removes entries that haven't been accessed for a
certain time before enlarging hash array.
---
 doc/src/sgml/config.sgml                      |  19 +++++
 src/backend/tcop/postgres.c                   |   2 +
 src/backend/utils/cache/catcache.c            | 105 +++++++++++++++++++++++++-
 src/backend/utils/misc/guc.c                  |  12 +++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/utils/catcache.h                  |  16 ++++
 6 files changed, 152 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index d383de2512..4231235447 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1677,6 +1677,25 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-catalog-cache-prune-min-age" xreflabel="catalog_cache_prune_min_age">
+      <term><varname>catalog_cache_prune_min_age</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>catalog_cache_prune_min_age</varname> configuration
+       parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+		 Specifies the minimum amount of unused time in seconds at which a
+		 system catalog cache entry is removed. -1 indicates that this feature
+		 is disabled at all. The value defaults to 300 seconds (<literal>5
+		 minutes</literal>). The entries that are not used for the duration
+		 can be removed to prevent catalog cache from bloating with useless
+		 entries.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth">
       <term><varname>max_stack_depth</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index f9ce3d8f22..acab473d34 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -71,6 +71,7 @@
 #include "tcop/pquery.h"
 #include "tcop/tcopprot.h"
 #include "tcop/utility.h"
+#include "utils/catcache.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
@@ -2575,6 +2576,7 @@ start_xact_command(void)
 	 * not desired, the timeout has to be disabled explicitly.
 	 */
 	enable_statement_timeout();
+	SetCatCacheClock(GetCurrentStatementStartTimestamp());
 }
 
 static void
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index d05930bc4c..52586bd415 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -38,6 +38,7 @@
 #include "utils/rel.h"
 #include "utils/resowner_private.h"
 #include "utils/syscache.h"
+#include "utils/timeout.h"
 
 
  /* #define CACHEDEBUG */	/* turns DEBUG elogs on */
@@ -60,9 +61,18 @@
 #define CACHE_elog(...)
 #endif
 
+/*
+ * GUC variable to define the minimum age of entries that will be considered
+ * to be evicted in seconds. -1 to disable the feature.
+ */
+int catalog_cache_prune_min_age = 300;
+
 /* Cache management header --- pointer is NULL until created */
 static CatCacheHeader *CacheHdr = NULL;
 
+/* Clock for the last accessed time of a catcache entry. */
+TimestampTz	catcacheclock = 0;
+
 static inline HeapTuple SearchCatCacheInternal(CatCache *cache,
 					   int nkeys,
 					   Datum v1, Datum v2,
@@ -850,9 +860,83 @@ InitCatCache(int id,
 	 */
 	MemoryContextSwitchTo(oldcxt);
 
+	/* initialize catcache reference clock if haven't done yet */
+	if (catcacheclock == 0)
+		catcacheclock = GetCurrentTimestamp();
+
 	return cp;
 }
 
+/*
+ * CatCacheCleanupOldEntries - Remove infrequently-used entries
+ *
+ * Catcache entries happen to be left unused for a long time for several
+ * reasons. Remove such entries to prevent catcache from bloating. It is based
+ * on the similar algorithm with buffer eviction. Entries that are accessed
+ * several times in a certain period live longer than those that have had less
+ * access in the same duration.
+ */
+static bool
+CatCacheCleanupOldEntries(CatCache *cp)
+{
+	int	nremoved = 0;
+	int i;
+
+	/* Return immediately if disabled */
+	if (catalog_cache_prune_min_age == 0)
+		return false;
+
+	/* Scan over the whole hash to find entries to remove */
+	for (i = 0 ; i < cp->cc_nbuckets ; i++)
+	{
+		dlist_mutable_iter	iter;
+
+		dlist_foreach_modify(iter, &cp->cc_bucket[i])
+		{
+			CatCTup    *ct = dlist_container(CatCTup, cache_elem, iter.cur);
+			long		entry_age;
+			int			us;
+
+			/* Don't remove referenced entries */
+			if (ct->refcount != 0 ||
+				(ct->c_list && ct->c_list->refcount != 0))
+				continue;
+
+			/*
+			 * Calculate the duration from the time from the last access to
+			 * the "current" time. catcacheclock is updated per-statement
+			 * basis and additionaly udpated periodically during a long
+			 * running query.
+			 */
+			TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us);
+
+			if (entry_age < catalog_cache_prune_min_age)
+				continue;
+
+			/*
+			 * Entries that are not accessed after the last pruning are
+			 * removed in that seconds, and their lives are prolonged
+			 * according to how many times they are accessed up to three times
+			 * of the duration. We don't try shrink buckets since pruning
+			 * effectively caps catcache expansion in the long term.
+			 */
+			if (ct->naccess > 0)
+				ct->naccess--;
+			else
+			{
+				CatCacheRemoveCTup(cp, ct);
+				nremoved++;
+			}
+		}
+	}
+
+	if (nremoved > 0)
+		elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d",
+			 cp->id, cp->cc_relname, nremoved, cp->cc_ntup + nremoved);
+
+	return nremoved > 0;
+}
+
 /*
  * Enlarge a catcache, doubling the number of buckets.
  */
@@ -1264,6 +1348,12 @@ SearchCatCacheInternal(CatCache *cache,
 		 */
 		dlist_move_head(bucket, &ct->cache_elem);
 
+		/* prolong life of this entry */
+		if (ct->naccess < 2)
+			ct->naccess++;
+
+		ct->lastaccess = catcacheclock;
+
 		/*
 		 * If it's a positive entry, bump its refcount and return it. If it's
 		 * negative, we can report failure to the caller.
@@ -1888,19 +1978,28 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	ct->dead = false;
 	ct->negative = negative;
 	ct->hash_value = hashValue;
+	ct->naccess = 0;
+	ct->lastaccess = catcacheclock;
 
 	dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem);
 
 	cache->cc_ntup++;
 	CacheHdr->ch_ntup++;
 
+	/* increase refcount so that the new entry survives pruning */
+	ct->refcount++;
+
 	/*
-	 * If the hash table has become too full, enlarge the buckets array. Quite
-	 * arbitrarily, we enlarge when fill factor > 2.
+	 * If the hash table has become too full, try removing infrequently used
+	 * entries to make a room for the new entry. If failed, enlarge the bucket
+	 * array instead.  Quite arbitrarily, we try this when fill factor > 2.
 	 */
-	if (cache->cc_ntup > cache->cc_nbuckets * 2)
+	if (cache->cc_ntup > cache->cc_nbuckets * 2 &&
+		!CatCacheCleanupOldEntries(cache))
 		RehashCatCache(cache);
 
+	ct->refcount--;
+
 	return ct;
 }
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index aa564d153a..e624c74bf9 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -82,6 +82,7 @@
 #include "tsearch/ts_cache.h"
 #include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/catcache.h"
 #include "utils/guc_tables.h"
 #include "utils/float.h"
 #include "utils/memutils.h"
@@ -2202,6 +2203,17 @@ static struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"catalog_cache_prune_min_age", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("System catalog cache entries that live unused for longer than this seconds are considered for removal."),
+			gettext_noop("The value of -1 turns off pruning."),
+			GUC_UNIT_S
+		},
+		&catalog_cache_prune_min_age,
+		300, -1, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	/*
 	 * We use the hopefully-safely-small value of 100kB as the compiled-in
 	 * default for max_stack_depth.  InitializeGUCOptions will increase it if
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index cccb5f145a..fa117f0573 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -128,6 +128,7 @@
 #work_mem = 4MB				# min 64kB
 #maintenance_work_mem = 64MB		# min 1MB
 #autovacuum_work_mem = -1		# min 1MB, or -1 to use maintenance_work_mem
+#catalog_cache_prune_min_age = 300s	# -1 disables pruning
 #max_stack_depth = 2MB			# min 100kB
 #shared_memory_type = mmap		# the default is the first option
 					# supported by the operating system:
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h
index 65d816a583..2134839ecf 100644
--- a/src/include/utils/catcache.h
+++ b/src/include/utils/catcache.h
@@ -22,6 +22,7 @@
 
 #include "access/htup.h"
 #include "access/skey.h"
+#include "datatype/timestamp.h"
 #include "lib/ilist.h"
 #include "utils/relcache.h"
 
@@ -119,6 +120,8 @@ typedef struct catctup
 	bool		dead;			/* dead but not yet removed? */
 	bool		negative;		/* negative cache entry? */
 	HeapTupleData tuple;		/* tuple management header */
+	int			naccess;		/* # of access to this entry, up to 2  */
+	TimestampTz	lastaccess;		/* timestamp of the last usage */
 
 	/*
 	 * The tuple may also be a member of at most one CatCList.  (If a single
@@ -189,6 +192,19 @@ typedef struct catcacheheader
 /* this extern duplicates utils/memutils.h... */
 extern PGDLLIMPORT MemoryContext CacheMemoryContext;
 
+/* for guc.c, not PGDLLPMPORT'ed */
+extern int catalog_cache_prune_min_age;
+
+/* source clock for access timestamp of catcache entries */
+extern TimestampTz catcacheclock;
+
+/* SetCatCacheClock - set catcache timestamp source clodk */
+static inline void
+SetCatCacheClock(TimestampTz ts)
+{
+	catcacheclock = ts;
+}
+
 extern void CreateCacheMemoryContext(void);
 
 extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid,
-- 
2.16.3


----Next_Part(Fri_Mar_29_17_24_40_2019_805)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="FullScan-mod-0001-Remove-entries-that-haven-t-been-used-for-a-certain-.patch"



^ permalink  raw  reply  [nested|flat] 19+ messages in thread

* Re: on placeholder entries in view rule action query's range table
@ 2023-04-05 02:32  Amit Langote <[email protected]>
  0 siblings, 1 reply; 19+ messages in thread

From: Amit Langote @ 2023-04-05 02:32 UTC (permalink / raw)
  To: Tom Lane <[email protected]>; +Cc: vignesh C <[email protected]>; Alvaro Herrera <[email protected]>; PostgreSQL Hackers <[email protected]>

On Thu, Jan 12, 2023 at 10:06 AM Tom Lane <[email protected]> wrote:
> Amit Langote <[email protected]> writes:
> > On Mon, Jan 9, 2023 at 5:58 AM Tom Lane <[email protected]> wrote:
> >> Conceivably we could make it work by allowing RTE_SUBQUERY RTEs to
> >> carry a relation OID and associated RTEPermissionInfo, so that when a
> >> view's RTE_RELATION RTE is transmuted to an RTE_SUBQUERY RTE it still
> >> carries the info needed to let us lock and permission-check the view.
> >> That might be a bridge too far from the ugliness perspective ...
> >> although certainly the business with OLD and/or NEW RTEs isn't very
> >> pretty either.
>
> > I had thought about that idea but was a bit scared of trying it,
> > because it does sound like something that might become a maintenance
> > burden in the future.  Though I gave that a try today given that it
> > sounds like I may have your permission. ;-)
>
> Given the small number of places that need to be touched, I don't
> think it's a maintenance problem.  I agree with your fear that you
> might have missed some, but I also searched and found no more.

While thinking about query view locking in context of [1], I realized
that we have missed also fixing AcquirePlannerLocks() /
ScanQueryForLocks() to consider that an RTE_SUBQUERY rte may belong to
a view, which must be locked the same as RTE_RELATION entries.  Note
we did fix AcquireExecutorLocks() in 47bb9db75 as follows:

@@ -1769,7 +1769,8 @@ AcquireExecutorLocks(List *stmt_list, bool acquire)
        {
            RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc2);

-           if (rte->rtekind != RTE_RELATION)
+           if (!(rte->rtekind == RTE_RELATION ||
+                 (rte->rtekind == RTE_SUBQUERY && OidIsValid(rte->relid))))

Attached a patch to fix.

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

[1] https://commitfest.postgresql.org/42/3478/


Attachments:

  [application/octet-stream] ScanQueryForLocks-lock-views.patch (1.4K, ../../CA+HiwqH0xZOQ+GQAdKeckY1R4NOeHdzhtfxkAMJLSchpapNk5w@mail.gmail.com/2-ScanQueryForLocks-lock-views.patch)
  download | inline diff:
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 5f3ffd98af..d71810b65f 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -527,7 +527,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->partPruneInfos = glob->partPruneInfos;
 	result->rtable = glob->finalrtable;
 	result->permInfos = glob->finalrteperminfos;
-	result->viewRelations = glob->viewRelations;
+	result->viewRelations = NIL;
 	result->resultRelations = glob->resultRelations;
 	result->appendRelations = glob->appendRelations;
 	result->subplans = glob->subplans;
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 4e455d815f..20b0f026e0 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1784,6 +1784,18 @@ ScanQueryForLocks(Query *parsetree, bool acquire)
 				break;
 
 			case RTE_SUBQUERY:
+				/*
+				 * If subquery belongs to a view, we must acquire or release
+				 * the appropriate type of lock on the view.
+				 */
+				if (OidIsValid(rte->relid))
+				{
+					Assert(rte->rellockmode != NoLock);
+					if (acquire)
+						LockRelationOid(rte->relid, rte->rellockmode);
+					else
+						UnlockRelationOid(rte->relid, rte->rellockmode);
+				}
 				/* Recurse into subquery-in-FROM */
 				ScanQueryForLocks(rte->subquery, acquire);
 				break;


^ permalink  raw  reply  [nested|flat] 19+ messages in thread

* Re: on placeholder entries in view rule action query's range table
@ 2023-04-05 18:33  Tom Lane <[email protected]>
  parent: Amit Langote <[email protected]>
  0 siblings, 2 replies; 19+ messages in thread

From: Tom Lane @ 2023-04-05 18:33 UTC (permalink / raw)
  To: Amit Langote <[email protected]>; +Cc: vignesh C <[email protected]>; Alvaro Herrera <[email protected]>; PostgreSQL Hackers <[email protected]>

Amit Langote <[email protected]> writes:
> While thinking about query view locking in context of [1], I realized
> that we have missed also fixing AcquirePlannerLocks() /
> ScanQueryForLocks() to consider that an RTE_SUBQUERY rte may belong to
> a view, which must be locked the same as RTE_RELATION entries.

I think you're right about that, because AcquirePlannerLocks is supposed
to reacquire whatever locks parsing+rewriting would have gotten.
However, what's with this hunk?

@@ -527,7 +527,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->partPruneInfos = glob->partPruneInfos;
 	result->rtable = glob->finalrtable;
 	result->permInfos = glob->finalrteperminfos;
-	result->viewRelations = glob->viewRelations;
+	result->viewRelations = NIL;
 	result->resultRelations = glob->resultRelations;
 	result->appendRelations = glob->appendRelations;
 	result->subplans = glob->subplans;

			regards, tom lane






^ permalink  raw  reply  [nested|flat] 19+ messages in thread

* Re: on placeholder entries in view rule action query's range table
@ 2023-04-05 20:05  Tom Lane <[email protected]>
  parent: Tom Lane <[email protected]>
  1 sibling, 0 replies; 19+ messages in thread

From: Tom Lane @ 2023-04-05 20:05 UTC (permalink / raw)
  To: Amit Langote <[email protected]>; +Cc: vignesh C <[email protected]>; Alvaro Herrera <[email protected]>; PostgreSQL Hackers <[email protected]>

I wrote:
> Amit Langote <[email protected]> writes:
>> While thinking about query view locking in context of [1], I realized
>> that we have missed also fixing AcquirePlannerLocks() /
>> ScanQueryForLocks() to consider that an RTE_SUBQUERY rte may belong to
>> a view, which must be locked the same as RTE_RELATION entries.

> I think you're right about that, because AcquirePlannerLocks is supposed
> to reacquire whatever locks parsing+rewriting would have gotten.

After poking at this a bit more, I'm not sure there is any observable bug,
because we still notice the view change in AcquireExecutorLocks and
loop back to re-plan after that.  It still seems like a good idea to
notice such changes sooner not later to reduce wasted work, so I went
ahead and pushed the patch.

The only way it'd be a live bug is if the planner actually fails because
it's working with a stale view definition.  I tried to make it fail by
adjusting the view to no longer use an underlying table and then
dropping that table ... but AcquirePlannerLocks still detected that,
because of course it recurses and locks the table reference it finds
in the view subquery.  Maybe you could make a failure case involving
dropping a user-defined function instead, but I thought that was getting
pretty far afield, so I didn't pursue it.

			regards, tom lane






^ permalink  raw  reply  [nested|flat] 19+ messages in thread

* Re: on placeholder entries in view rule action query's range table
@ 2023-04-05 23:27  Amit Langote <[email protected]>
  parent: Tom Lane <[email protected]>
  1 sibling, 0 replies; 19+ messages in thread

From: Amit Langote @ 2023-04-05 23:27 UTC (permalink / raw)
  To: Tom Lane <[email protected]>; +Cc: Alvaro Herrera <[email protected]>; PostgreSQL Hackers <[email protected]>; vignesh C <[email protected]>

On Thu, Apr 6, 2023 at 3:33 Tom Lane <[email protected]> wrote:

> Amit Langote <[email protected]> writes:
> > While thinking about query view locking in context of [1], I realized
> > that we have missed also fixing AcquirePlannerLocks() /
> > ScanQueryForLocks() to consider that an RTE_SUBQUERY rte may belong to
> > a view, which must be locked the same as RTE_RELATION entries.
>
> I think you're right about that, because AcquirePlannerLocks is supposed
> to reacquire whatever locks parsing+rewriting would have gotten.
> However, what's with this hunk?
>
> @@ -527,7 +527,7 @@ standard_planner(Query *parse, const char
> *query_string, int cursorOptions,
>         result->partPruneInfos = glob->partPruneInfos;
>         result->rtable = glob->finalrtable;
>         result->permInfos = glob->finalrteperminfos;
> -       result->viewRelations = glob->viewRelations;
> +       result->viewRelations = NIL;
>         result->resultRelations = glob->resultRelations;
>         result->appendRelations = glob->appendRelations;
>         result->subplans = glob->subplans;


Oops, I was working in the wrong local branch.

Thanks for pushing.  I agree that there’s no live bug as such right now,
but still good to be consistent.

> --
Thanks, Amit Langote
EDB: http://www.enterprisedb.com


^ permalink  raw  reply  [nested|flat] 19+ messages in thread

* [PATCH v4 01/12] Change section heading to better reflect saving a result in variable(s)
@ 2023-09-24 20:49  Karl O. Pinc <[email protected]>
  0 siblings, 0 replies; 19+ messages in thread

From: Karl O. Pinc @ 2023-09-24 20:49 UTC (permalink / raw)

The current section title of "Executing a Command with a Single-Row
Result" does not reflect what the section is really about.  Other
sections make clear how to _execute_ commands, single-row result or not.
What this section is about is how to _save_ a single row of results into
variable(s).

It would be nice to talk about saving results into variables in the
section heading but I couldn't come up with anything pithy.  "Saving a
Single-Row of a Command's Result" seems good enough, especially since
there's few other places to save results other than in variables.
---
 doc/src/sgml/plpgsql.sgml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index f55e901c7e..8747e84245 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1126,7 +1126,7 @@ PERFORM create_mv('cs_session_page_requests_mv', my_query);
    </sect2>
 
    <sect2 id="plpgsql-statements-sql-onerow">
-    <title>Executing a Command with a Single-Row Result</title>
+    <title>Saving a Single-Row of a Command's Result</title>
 
     <indexterm zone="plpgsql-statements-sql-onerow">
      <primary>SELECT INTO</primary>
-- 
2.30.2


--MP_/OOXZvOwbpccKfGOtE9/SwX6
Content-Type: text/x-patch
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename=v4-0002-Change-section-heading-to-better-describe-referen.patch



^ permalink  raw  reply  [nested|flat] 19+ messages in thread


end of thread, other threads:[~2023-09-24 20:49 UTC | newest]

Thread overview: 19+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2018-10-16 04:04 [PATCH 2/2] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]>
2018-10-16 04:04 [PATCH 1/4] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]>
2018-10-16 04:04 [PATCH 2/4] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]>
2018-10-16 04:04 [PATCH 2/3] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]>
2018-10-16 04:04 [PATCH 2/4] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]>
2018-10-16 04:04 [PATCH 1/4] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]>
2018-10-16 04:04 [PATCH 3/5] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]>
2018-10-16 04:04 [PATCH 1/4] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]>
2019-03-01 04:32 [PATCH] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]>
2019-03-01 04:32 [PATCH 2/2] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]>
2019-03-01 04:32 [PATCH] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]>
2019-03-01 04:32 [PATCH 1/2] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]>
2019-03-01 04:32 [PATCH 2/6] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]>
2019-03-01 04:32 [PATCH] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]>
2023-04-05 02:32 Re: on placeholder entries in view rule action query's range table Amit Langote <[email protected]>
2023-04-05 18:33 ` Re: on placeholder entries in view rule action query's range table Tom Lane <[email protected]>
2023-04-05 20:05   ` Re: on placeholder entries in view rule action query's range table Tom Lane <[email protected]>
2023-04-05 23:27   ` Re: on placeholder entries in view rule action query's range table Amit Langote <[email protected]>
2023-09-24 20:49 [PATCH v4 01/12] Change section heading to better reflect saving a result in variable(s) Karl O. Pinc <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox