From: Kyotaro Horiguchi Date: Tue, 26 Dec 2017 17:43:09 +0900 Subject: [PATCH 1/4] Remove entries that haven't been used for a certain time 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 | 3 + src/backend/utils/cache/catcache.c | 153 +++++++++++++++++++++++- src/backend/utils/cache/plancache.c | 163 ++++++++++++++++++++++++++ src/backend/utils/misc/guc.c | 33 ++++++ src/backend/utils/misc/postgresql.conf.sample | 2 + src/include/utils/catcache.h | 19 +++ src/include/utils/plancache.h | 7 +- 8 files changed, 413 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 5b913f00c1..76745047af 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1617,6 +1617,44 @@ include_dir 'conf.d' + + syscache_memory_target (integer) + + syscache_memory_target configuration parameter + + + + + 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 + . If you need to keep + certain amount of syscache entries with intermittent usage, try + increase this setting. + + + + + + syscache_prune_min_age (integer) + + syscache_prune_min_age configuration parameter + + + + + 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 + (10 minutes). 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 + . + + + + max_stack_depth (integer) diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 8e6aef332c..e4a4a5874c 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -732,6 +732,9 @@ void SetCurrentStatementStartTimestamp(void) { stmtStartTimestamp = GetCurrentTimestamp(); + + /* Set this timestamp as aproximated current time */ + SetCatCacheClock(stmtStartTimestamp); } /* diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index 5ddbf6eab1..9f421cd242 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, @@ -866,9 +881,130 @@ 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. + * Since the area for bucket array is dominant, consider only it. + */ + hash_size = cp->cc_nbuckets * sizeof(dlist_head); + if (hash_size < (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. */ @@ -1282,6 +1418,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. @@ -1813,7 +1954,6 @@ ReleaseCatCacheList(CatCList *list) CatCacheRemoveCList(list->my_cache, list); } - /* * CatalogCacheCreateEntry * Create a new CatCTup entry, copying the given HeapTuple and other @@ -1906,6 +2046,8 @@ 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); @@ -1913,10 +2055,13 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, CacheHdr->ch_ntup++; /* - * 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/cache/plancache.c b/src/backend/utils/cache/plancache.c index 0ad3e3c736..701ead152c 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -63,12 +63,14 @@ #include "storage/lmgr.h" #include "tcop/pquery.h" #include "tcop/utility.h" +#include "utils/catcache.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/resowner_private.h" #include "utils/rls.h" #include "utils/snapmgr.h" #include "utils/syscache.h" +#include "utils/timestamp.h" /* @@ -86,6 +88,12 @@ * guarantee to save a CachedPlanSource without error. */ static CachedPlanSource *first_saved_plan = NULL; +static CachedPlanSource *last_saved_plan = NULL; +static int num_saved_plans = 0; +static TimestampTz oldest_saved_plan = 0; + +/* GUC variables */ +int min_cached_plans = 1000; static void ReleaseGenericPlan(CachedPlanSource *plansource); static List *RevalidateCachedQuery(CachedPlanSource *plansource, @@ -105,6 +113,7 @@ static TupleDesc PlanCacheComputeResultDesc(List *stmt_list); static void PlanCacheRelCallback(Datum arg, Oid relid); static void PlanCacheFuncCallback(Datum arg, int cacheid, uint32 hashvalue); static void PlanCacheSysCallback(Datum arg, int cacheid, uint32 hashvalue); +static void PruneCachedPlan(void); /* @@ -208,6 +217,8 @@ CreateCachedPlan(RawStmt *raw_parse_tree, plansource->generic_cost = -1; plansource->total_custom_cost = 0; plansource->num_custom_plans = 0; + plansource->last_access = GetCatCacheClock(); + MemoryContextSwitchTo(oldcxt); @@ -423,6 +434,28 @@ CompleteCachedPlan(CachedPlanSource *plansource, plansource->is_valid = true; } +/* moves the plansource to the first in the list */ +static inline void +MovePlansourceToFirst(CachedPlanSource *plansource) +{ + if (first_saved_plan != plansource) + { + /* delink this element */ + if (plansource->next_saved) + plansource->next_saved->prev_saved = plansource->prev_saved; + if (plansource->prev_saved) + plansource->prev_saved->next_saved = plansource->next_saved; + if (last_saved_plan == plansource) + last_saved_plan = plansource->prev_saved; + + /* insert at the beginning */ + first_saved_plan->prev_saved = plansource; + plansource->next_saved = first_saved_plan; + plansource->prev_saved = NULL; + first_saved_plan = plansource; + } +} + /* * SaveCachedPlan: save a cached plan permanently * @@ -470,6 +503,11 @@ SaveCachedPlan(CachedPlanSource *plansource) * Add the entry to the global list of cached plans. */ plansource->next_saved = first_saved_plan; + if (first_saved_plan) + first_saved_plan->prev_saved = plansource; + else + last_saved_plan = plansource; + plansource->prev_saved = NULL; first_saved_plan = plansource; plansource->is_saved = true; @@ -492,7 +530,11 @@ DropCachedPlan(CachedPlanSource *plansource) if (plansource->is_saved) { if (first_saved_plan == plansource) + { first_saved_plan = plansource->next_saved; + if (first_saved_plan) + first_saved_plan->prev_saved = NULL; + } else { CachedPlanSource *psrc; @@ -502,10 +544,19 @@ DropCachedPlan(CachedPlanSource *plansource) if (psrc->next_saved == plansource) { psrc->next_saved = plansource->next_saved; + if (psrc->next_saved) + psrc->next_saved->prev_saved = psrc; break; } } } + + if (last_saved_plan == plansource) + { + last_saved_plan = plansource->prev_saved; + if (last_saved_plan) + last_saved_plan->next_saved = NULL; + } plansource->is_saved = false; } @@ -537,6 +588,13 @@ ReleaseGenericPlan(CachedPlanSource *plansource) Assert(plan->magic == CACHEDPLAN_MAGIC); plansource->gplan = NULL; ReleaseCachedPlan(plan, false); + + /* decrement "saved plans" counter */ + if (plansource->is_saved) + { + Assert (num_saved_plans > 0); + num_saved_plans--; + } } } @@ -1148,6 +1206,17 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams, if (useResOwner && !plansource->is_saved) elog(ERROR, "cannot apply ResourceOwner to non-saved cached plan"); + /* + * set last-accessed timestamp and move this plan to the first of the list + */ + if (plansource->is_saved) + { + plansource->last_access = GetCatCacheClock(); + + /* move this plan to the first of the list */ + MovePlansourceToFirst(plansource); + } + /* Make sure the querytree list is valid and we have parse-time locks */ qlist = RevalidateCachedQuery(plansource, queryEnv); @@ -1156,6 +1225,11 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams, if (!customplan) { + /* Prune cached plans if needed */ + if (plansource->is_saved && + min_cached_plans >= 0 && num_saved_plans > min_cached_plans) + PruneCachedPlan(); + if (CheckCachedPlan(plansource)) { /* We want a generic plan, and we already have a valid one */ @@ -1168,6 +1242,11 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams, plan = BuildCachedPlan(plansource, qlist, NULL, queryEnv); /* Just make real sure plansource->gplan is clear */ ReleaseGenericPlan(plansource); + + /* count this new saved plan */ + if (plansource->is_saved) + num_saved_plans++; + /* Link the new generic plan into the plansource */ plansource->gplan = plan; plan->refcount++; @@ -1856,6 +1935,90 @@ PlanCacheSysCallback(Datum arg, int cacheid, uint32 hashvalue) ResetPlanCache(); } +/* + * PrunePlanCache: removes generic plan of "old" saved plans. + */ +static void +PruneCachedPlan(void) +{ + CachedPlanSource *plansource; + TimestampTz currclock = GetCatCacheClock(); + long age; + int us; + int nremoved = 0; + + /* do nothing if not wanted */ + if (cache_prune_min_age < 0 || num_saved_plans <= min_cached_plans) + return; + + /* Fast check for oldest cache */ + if (oldest_saved_plan > 0) + { + TimestampDifference(oldest_saved_plan, currclock, &age, &us); + if (age < cache_prune_min_age) + return; + } + + /* last plan is the oldest. */ + for (plansource = last_saved_plan; plansource; plansource = plansource->prev_saved) + { + long plan_age; + int us; + + Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC); + + /* we want to prune no more plans */ + if (num_saved_plans <= min_cached_plans) + break; + + /* + * No work if it already doesn't have gplan and move it to the + * beginning so that we don't see it at the next time + */ + if (!plansource->gplan) + continue; + + /* + * Check age for pruning. Can exit immediately when finding a + * not-older element. + */ + TimestampDifference(plansource->last_access, currclock, &plan_age, &us); + if (plan_age <= cache_prune_min_age) + { + /* this entry is the next oldest */ + oldest_saved_plan = plansource->last_access; + break; + } + + /* + * Here, remove generic plans of this plansrouceif it is not actually + * used and move it to the beginning of the list. Just update + * last_access and move it to the beginning if the plan is used. + */ + if (plansource->gplan->refcount <= 1) + { + ReleaseGenericPlan(plansource); + nremoved++; + } + + plansource->last_access = currclock; + } + + /* move the "removed" plansrouces altogehter to the beginning of the list */ + if (plansource != last_saved_plan && plansource) + { + plansource->next_saved->prev_saved = NULL; + first_saved_plan->prev_saved = last_saved_plan; + last_saved_plan->next_saved = first_saved_plan; + first_saved_plan = plansource->next_saved; + plansource->next_saved = NULL; + last_saved_plan = plansource; + } + + if (nremoved > 0) + elog(DEBUG1, "plancache removed %d/%d", nremoved, num_saved_plans); +} + /* * ResetPlanCache: invalidate all cached plans. */ diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index b05fb209bb..e49346707d 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -79,6 +79,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/memutils.h" #include "utils/pg_locale.h" @@ -2105,6 +2106,38 @@ 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 + }, + + { + {"min_cached_plans", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the minimum number of cached plans kept on memory."), + gettext_noop("Timeout invalidation of plancache is not activated until the number of plancaches reaches this value. -1 means timeout invalidation is always active.") + }, + &min_cached_plans, + 1000, -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 9e39baf466..3f2760ef9d 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -126,6 +126,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 #dynamic_shared_memory_type = posix # 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 7b22f9c7bc..599303be56 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; /* approx. timestamp of the last usage */ /* * The tuple may also be a member of at most one CatCList. (If a single @@ -189,6 +192,22 @@ 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; +} + extern void CreateCacheMemoryContext(void); extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid, diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h index ab20aa04b0..f3c5b2010d 100644 --- a/src/include/utils/plancache.h +++ b/src/include/utils/plancache.h @@ -110,11 +110,13 @@ typedef struct CachedPlanSource bool is_valid; /* is the query_list currently valid? */ int generation; /* increments each time we create a plan */ /* If CachedPlanSource has been saved, it is a member of a global list */ - struct CachedPlanSource *next_saved; /* list link, if so */ + struct CachedPlanSource *prev_saved; /* list prev link, if so */ + struct CachedPlanSource *next_saved; /* list next link, if so */ /* State kept to help decide whether to use custom or generic plans: */ double generic_cost; /* cost of generic plan, or -1 if not known */ double total_custom_cost; /* total cost of custom plans so far */ int num_custom_plans; /* number of plans included in total */ + TimestampTz last_access; /* timestamp of the last usage */ } CachedPlanSource; /* @@ -143,6 +145,9 @@ typedef struct CachedPlan MemoryContext context; /* context containing this CachedPlan */ } CachedPlan; +/* GUC variables */ +extern int min_cached_plans; +extern int plancache_prune_min_age; extern void InitPlanCache(void); extern void ResetPlanCache(void); -- 2.16.3 ----Next_Part(Tue_Jul_03_09_29_14_2018_474)-- Content-Type: Text/X-Patch; charset=us-ascii Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="v5-0002-introduce-dynhash-pruning.patch"