一、问题背景
使用过pg10.x的小伙伴,一定了解relcache刷新异常这个BUG。通常的表现是vacuum/autovacuum异常,会发生表膨胀;数据库不能进行事务号回收,甚至导致事务回卷。
根据以往的经验,我们见到过以下这些场景:
1.pglog中存在类似vacuum报错:
ERROR: found xmin 10xxxxx from before relfrozenxid 21xxxxxxx
2.不存在长事务的情况下,多个表发生膨胀
3.$PGDATA/global目录下存在大量的pg_internal.init文件
通常我们可以通过删除pg_internal.init文件或者重启数据库来临时规避问题,将数据库版本升级至10.11可以解决此问题。
之前在平安集团时就参与了大量10.x版本实例到10.11版本升级,这篇博文简单分析下问题原理。
二、原理分析
PostgreSQL中存在两种高速缓存:syscache和relcache。syscache主要用于缓存系统表元组;relcache中包含所有访问过的表的模式信息(包含系统表)。这两个缓存在数据库中不是共享的,是每个进程独有的,通过共享消息队列来进行同步。
主要介绍下与问题相关的relcache,初始化在initpostgres时完成;其中存储的内容(RelationData)会记录到本地文件pg_internal.init中;失效和刷新常发生于执行heap_delete和heap_update类似操作后,会有对应的机制进行缓存刷新。
之前提到是relcache刷新存在bug,导致vacuum异常。鉴于篇幅问题,这里只分析这个bug是什么,怎么修改的;不再展开为什么会影响到vacuum。
重点分析两个问题:
- pg10.11版本做了什么改动,解决了这个问题?
- 为什么重启可以临时规避这个问题?
这里选用10.3版本和10.11版本进行比较
首先来看问题1,10.11中做了哪些改动解决了此问题
pg10.3:
pg10.11:
RegisterRelcacheInvalidation这个函数主要用来判断initfile是否已经失效。
If判断条件是比较直观的,当已经记录在initfile中的relId(即表oid)已经为InvalidOid并且OidIsValid(dbId)为true,即databaseOid为valid,这种情况下,那么就断定initfile内容失效,需要进行刷新。
对比两个版本RegisterRelcacheInvalidation函数,可以看到对于initfile是否已经失效的if判断条件,10.11中去掉了OidIsValid(dbId)这个条件。
那顺着反推下,肯定是由于在某些情况下,会导致OidIsValid(dbId)为false,导致整个if条件&&运算结果为假,造成了缓存失效判断错误,不去刷新缓存。
来看什么情况下会出现OidIsValid(dbId)返回false。
OidIsValid(dbId)宏定义如下:
#define OidIsValid(objectId) ((bool) ((objectId) != InvalidOid))
来看CacheInvalidateRelcache函数中的场景,当传入的relation的relisshared属性为true时,就会将databaseId赋值为InvalidOid,传参至RegisterRelcacheInvalidation函数中,OidIsValid(dbId)为false,不会标记initfile已经失效。
那么哪些relation的relisshared属性为true?
rd_rel定义如下:
Form_pg_class rd_rel; /* RELATION tuple */
可以看到这里的rd_rel是从pg_class中查询到的信息,那么可以通过系统表查询哪些表的relisshared属性为true。
postgres=# select relname,relisshared from pg_class where relisshared='t';
relname | relisshared
-----------------------------------------+-------------
pg_authid | t
pg_subscription | t
pg_database | t
pg_db_role_setting | t
pg_tablespace | t
pg_pltemplate | t
pg_auth_members | t
pg_shdepend | t
pg_shdescription | t
pg_replication_origin | t
pg_shseclabel | t
可以看到结果中包含pg_authid,pg_database等。relisshared属性大概含义为,当表的某些数据在所有database中是共享的时,该属性值为ture。
Name | Type | References | Description |
---|---|---|---|
relisshared | bool | True if this table is shared across all databases in the cluster. Only certain system catalogs (such as pg_database) are shared |
最早提到过relcache中会缓存一些系统表,pg_authid和pg_database这些是基础的系统表,肯定是需要放进缓存的。
结合以上的分析,那么OidIsValid(dbId)出现在判断条件中,就不合理,因此在10.11中去除了这个条件。
接下来看下问题2,为什么重启可以临时规避问题呢?
是因为重启时会删除之前的initfile,相当于强制刷新relcache。
来看下删除逻辑:在startup流程中会调用RelationCacheInitFileRemove删除文件。
RelationCacheInitFileRemove函数: