Catcache rehash hits MaxAllocSize (1GB) and errors with "invalid memory alloc request size" on very large numbers of relations

Started by Reshmithaa9 days ago2 messageshackers
Jump to latest
#1Reshmithaa
reshmithaa.b@zohocorp.com

Hi,

While running ANALYZE across a database with 500,000+ tables (~350

columns each) in a single client session, we hit with an error:

  ERROR: invalid memory alloc request size 1073741824

This isn't just "the catcache got big and used a lot of RAM" - it's a

specific, deterministic ceiling in RehashCatCache()/RehashCatCacheLists()

that any sufficiently large catcache will hit, regardless of how much

memory is available on the machine.

What We Observed

-----------------

After ANALYZE had processed ~505,180 tables (each contains 350 cols

approx) in one session, a memory context dump showed:

  === Memory Contexts after vacuuming 505180 tables ===

  CacheMemoryContext                       26 GB      26 GB      1

  Attopt cache                             120 MB     118 MB     1

  PgStat Shared Ref                        35 MB      35 MB      1

  Relcache by OID                          24 MB      19 MB      1

  PgStat Shared Ref Hash                   24 MB      24 MB      1

  ...

Eventually a subsequent allocation fails with:

  ERROR: invalid memory alloc request size 1073741824

Root cause

----------

catcache.c's CatalogCacheCreateEntry() triggers a rehash whenever the

load factor exceeds 2:

    if (cache->cc_ntup > cache->cc_nbuckets * 2)

        RehashCatCache(cache);

RehashCatCache() doubles the bucket count and allocates the new bucket

array with the *regular* allocator:

    newnbuckets = cp->cc_nbuckets * 2;

    newbucket = (dlist_head *) MemoryContextAllocZero(CacheMemoryContext,

                                                       newnbuckets * sizeof(dlist_head));

MemoryContextAllocZero() enforces MaxAllocSize (0x3fffffff =

1,073,741,823 bytes - "1 gigabyte minus 1", see memutils.h). sizeof

(dlist_head) is 16 bytes on 64-bit platforms. Buckets are powers of two,

so the bucket array size doubles:..., 512MB, then exactly 1GB. That

final doubling requests:

    67,108,864 buckets * 16 bytes = 1,073,741,824 bytes

which is exactly 1 byte over MaxAllocSize, so the allocation is refused

and the backend hard-errors with "invalid memory alloc request size

1073741824" - the exact message and exact number we observed.

Given the load factor of 2, this doubling occurs once a catcache

accumulates roughly 67.1 million live tuples. For ATTNUM specifically

(examine_attribute() in analyze.c calls SearchSysCache2(ATTNUM, ...)

once per column of every relation ANALYZE processes), that means any

session whose ANALYZE/autovacuum touches enough distinct relations to

push pg_attribute entries past ~67M will hit this.

Why this matters beyond "reduce your table count"

--------------------------------------------------

- It fails outright rather than degrading gracefully or scaling with

  available RAM. A machine with terabytes of free memory will still hit

  this at exactly the same tuple count, because the ceiling is

  MaxAllocSize, not physical memory.

- Felt patching a single call site as a partial fix. We tested

  replacing SearchSysCache2() with a direct systable_beginscan() in

  examine_attribute(), which will avoids populating the ATTNUM catcache

  from that one code path. That slows down how fast the cache fills, but

  any other code path that still uses the catcache heavily (in this backend

  or others) will eventually hit the same ceiling.

Open Questions / Possible Fixes

--------------------------------

1. Have RehashCatCache()/RehashCatCacheLists() allocate the bucket array

   with MemoryContextAllocHuge() instead of MemoryContextAllocZero().

   Is there a known reason this wasn't already done, or would a patch

   along these lines be welcome?

2. If there's a reason the 1GB ceiling must stay in place for this

   allocation, then is splitting ANALYZE into multiple sessions/backends,

   each covering a limited number of tables (so no single backend's catcache

   grows past the threshold), the only real workaround available today? Or

   is there some other supported way to Invalidate/reset a backend's catcache

   growth (short of restarting the connection) that we're missing?

The reproduction scripts we used are attached below.

Thanks & Regards,

Reshmithaa B

Member Technical Staff

ZOHO Corporation

Attachments:

create.shapplication/octet-stream; name=create.shDownload
test.shapplication/octet-stream; name=test.shDownload
#2Tom Lane
tgl@sss.pgh.pa.us
In reply to: Reshmithaa (#1)
Re: Catcache rehash hits MaxAllocSize (1GB) and errors with "invalid memory alloc request size" on very large numbers of relations

Reshmithaa <reshmithaa.b@zohocorp.com> writes:

While running ANALYZE across a database with 500,000+ tables (~350
columns each) in a single client session, we hit with an error:
  ERROR: invalid memory alloc request size 1073741824

Hmm...

1. Have RehashCatCache()/RehashCatCacheLists() allocate the bucket array
   with MemoryContextAllocHuge() instead of MemoryContextAllocZero().
   Is there a known reason this wasn't already done, or would a patch
   along these lines be welcome?

That by itself wouldn't move the goalposts very far: you're within
a factor of 10 or so of overflowing the int32 fields like cc_ntups,
cc_nbuckets, etc. Widening those would take a lot of work, eg
it bleeds into hash calculations, not to mention that 32-bit
hash values for entries start to seem not wide enough when you
have that many entries. And I don't even want to think about
how much RAM a backend is going to be eating with 500K relcache
entries and corresponding catcache entries for a lot of them.

What would probably be a more productive answer is to look at limiting
the caches' size by discarding not-recently-used entries. We actually
had code to do that for catcaches, long ago, and dropped it because
keeping track of what was least recently used added large amounts of
overhead. But possibly a less-naive implementation could do better on
that score.

In the meantime, I think you've chosen an unworkable database setup
and you need to rethink how many tables you've got.

regards, tom lane