暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PostgreSQL 中的系统表缓存

PolarDB 2025-05-08
255

PostgreSQL 中的系统表缓存

PostgreSQL 中的系统表

系统表是 PostgreSQL 存储元数据的地方,比如表的字段、类型、权限等,一般以 pg_ 开头命名。比如在 PostgreSQL 数据库中创建一张表 test_table,就可以在 pg_class 中查到该表的基本信息。

postgres=# create table test_table(col1 int, col2 text);
CREATE TABLE
postgres=# select * from pg_class where relname = 'test_table';
-[ RECORD 1 ]-------+-----------
oid                 | 16442
relname             | test_table
relnamespace        | 2200
reltype             | 16444
reloftype           | 0
relowner            | 10
relam               | 2
relfilenode         | 16442
reltablespace       | 0
relpages            | 0
reltuples           | -1
relallvisible       | 0
reltoastrelid       | 16445
relhasindex         | f
relisshared         | f
relpersistence      | p
relkind             | r
relnatts            | 2
relchecks           | 0
relhasrules         | f
relhastriggers      | f
relhassubclass      | f
relrowsecurity      | f
relforcerowsecurity | f
relispopulated      | t
relreplident        | d
relispartition      | f
relrewrite          | 0
relfrozenxid        | 1931
relminmxid          | 1
relacl              |
reloptions          |
relpartbound        |

其实仅仅是表,PostgreSQL 中的任何对象都记录在系统表中,包括视图、函数、类型、操作符等等。从接受用户连接到处理 SQL 的各个环节中,PostgreSQL 内部都会访问系统表查询需要的元信息,比如新连接进来之后,首先就需要查系统表中记录的用户和认证信息,某些场景下,系统表中元数据的访问频率甚至远高于用户数据,所以系统表缓存对数据库系统的性能就至关重要。

系统表缓存

PostgreSQL 内部对任何表的读写都会通过 Buffer Pool,这也包括系统表,所以其实系统表已经有一层缓存了,那为什么还需要独立的系统表缓存呢?答案是系统表中的数据组织形式在内存中访问不够高效。

PostgresSQL 中元数据缓存有很多,比如 relation cache, cat/sys cache, type cache, spc cache 等,本文以 relcache 和 cat/sys cache 为例。

Relation Cache

Relation Cache 从名字就可以看出,主要是和表信息相关,这也是 PostgreSQL 内部实现最为复杂的系统表缓存。每一张表在 Relation Cache 中都有一个缓存对象(当然也包括系统表),在内存中使用 `struct RelationData` 这个结构体来表示,里面基本涵盖了一张表相关的所有相关信息, 下面简单列出了几个比较重要的字段,比如表结构信息、索引信息、分区表信息等等,简单一句话概括就是:Relation Cache 把一张表相关的,散落在多个系统表中的元信息都加载到了这个结构体中。

typedef struct RelationData
{
    RelFileLocator rd_locator;    /* relation physical identifier */
    SMgrRelation rd_smgr;    /* cached file handle, or NULL */
    ...
    Form_pg_class rd_rel;    /* RELATION tuple */
    TupleDesc    rd_att;    /* tuple descriptor */
    ...
    PartitionKey rd_partkey;    /* partition key, or NULL */
    PartitionDesc rd_partdesc;    /* partition descriptor, or NULL */
    ...
    Form_pg_index rd_index;    /* pg_index tuple describing this index */
} RelationData;

Cat/Sys Cache

这个 Cache 从实现层面来说,包括两层:Catalog Cache 和 Sys Cache,后者是在前者基础上封装而来。Cat/Sys Cache 中的数据和 Relation Cache 是有部分冗余的,甚至单从名字上来说,Cat/Sys Cache 才叫系统表缓存。那为什么还需要这么个额外的缓存呢?主要还是使用接口的差别。

extern HeapTuple SearchSysCache1(int cacheId, Datum key1);
extern HeapTuple SearchSysCache2(int cacheId, Datum key1, Datum key2);
extern HeapTuple SearchSysCache3(int cacheId, Datum key1, Datum key2, Datum key3);
extern HeapTuple SearchSysCache4(int cacheId, Datum key1, Datum key2, Datum key3, Datum key4);

从 SysCache 接口原型可以看到,这是一个 K-V 的模型,在 PostgreSQL 很多处理逻辑中,只是想简单的查询某张表第 N 列的名字,直接通过简单的 SysCache 接口即可得到,非常方便。可以把 SysCache 当做一个元数据 K-V 内存数据库。

image.png

Cache 的模型 & 失效机制

上面我们提到,PostgreSQL 内部所有访问表数据的操作都是在 Buffer Pool 中完成,这是所有数据库提高性能的基本操作,从 Cache 模型的视角来看,Buffer Pool 是一种 Cache Through 的模型,上层模块只需要和 Buffer Pool 交互,至于 Cache 的换入换出等逻辑,不需要上层关心。

系统表缓存和 Buffer Pool 不太一样,以 Relation Cache 为例,比如优化器在访问 Cache Miss 之后,会直接去访问系统表(当然是通过 Buffer Pool),把系统表中的数据加载到 Relation Cache 中之后再返回 Cache 中的数据。可以看到,系统表缓存是一个 Cache Aside 模型。

不同的 Cache 模型需要不同的失效机制,此外 PostgreSQL 是多进程的架构,系统表缓存在各个进程内部,这有点像 CPU L1 Cache,为 CPU Core 各自私有,所以 PostgreSQL 的系统表缓存失效机制和 CPU Cache 失效机制有一些像。如果一个进程修改了元数据(比如执行 DDL 修改了表结构),在事务提交时,会广播失效消息给所有进程,而其他进程在合适的时机会去尝试接收并处理失效消息,而接收和处理失效消息的时机对正确性起着决定性作用,一个典型的场景就是在上表锁的时候。

image.png

PostgreSQL 系统表缓存的问题

前面提到,系统表缓存在各个进程内部,每个进程在内存中都会缓存一份一样的元数据,这在元数据较少(用户创建的对象,比如表、视图、函数等)和连接数较少时,不是什么太大的问题,浪费不了多少内存,这也是社区为什么这么多年一直没有优化这个问题。然而当连接数达到 10000+ 这个量级,或者数据库中创建了几十万张表,这部分元数据缓存将会消耗大量内存,假如某个数据库创建了 10W 张表,按照 100个链接来计算,仅仅 Relation Cache 就可能消耗 20GB 内存,并且还会随着连接数的增加而线性增长,非常容易导致 OOM,影响数据库服务的稳定性。

PolarDB PG 的解决方案

作为一个云厂商,客户的场景非常多,并且很难控制客户业务的使用方式,只能从根本上彻底解决这个问题才可以保证服务的稳定性。

缓存淘汰

原生 PostgreSQL 的系统表缓存是没有淘汰机制的,而最佳实践又建议业务尽量使用长连接,进程长时间会把所有元数据都加载到内存中,哪怕短期内完全不再使用。此外,为了避免高频访问一个不存在的元数据而导致 Cache 打穿,影响性能,Sys/Cat Cache 还缓存了一种 negative entry,用于标识某个元数据在系统表里是不存在的,告诉上层模块不用再低效的去访问系统表了。如果说系统表中的元数据总量是有限的,那么这类 negative entry 则是“无限”的,最极端的场景下,可以导致 Sys/Cat Cache 中缓存了大量的 negative entry。

为进程私有的系统表增加淘汰机制,可以解决一部分问题,避免缓存内存无节制的增长,但也并不能解决所有问题,比如现在越来越多的业务使用大量分区表,在 parser, optimizer 内部会集中访问大量分区表信息,如果进程内部的系统表缓存容量受限,很容易影响性能。

Global Cache

为了彻底解决系统表缓存带来的内存利用率低、容易导致 OOM 影响服务稳定性问题,同时避免影响到性能,PolarDB 基于共享内存实现了 Global Cache,可以把元数据缓存放到共享内存中,实现了所有进程共享一份缓存。具体使用可以参考文档:https://help.aliyun.com/zh/polardb/polardb-for-oracle/global-cache-1693463956790


文章转载自PolarDB,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论