相关
《Postgresql源码(45)SysCache内存结构与搜索流程分析》
0 总结速查
syscache:缓存系统表的行。通用数据结构,可以缓存一切数据(hash + dlist)。可以分别缓存单行和多行查询。
- syscache使用CatCache数组,定义了一些常用查询的结果集缓存,数据放到CatCache里面的dlist中存放。
- syscache查询接口
- SearchSysCache系列接口时,key须按照cacheinfo的定义来查询
- pg_class支持
where relname = ? and relnamespace = ?
的查询:SearchSysCache2(RELNAMENSP,k1,k2)
。 - pg_class支持
where oid = ?
的查询:SearchSysCache1(RELOID,k1)
。
- pg_class支持
- SearchSysCacheList系列接口时,可以使用少于定义的key去查询,例如
SearchSysCacheList1(AMPROCNUM, ObjectIdGetDatum(opfamilyoid));
SearchSysCacheExists4(AMPROCNUM, ObjectIdGetDatum(opfamily), ObjectIdGetDatum(opcintype), ObjectIdGetDatum(opcintype), Int16GetDatum(procno))
- SearchSysCache系列接口时,key须按照cacheinfo的定义来查询
- syscache的查询条件(1个或多个健)组合成key,key经过hash后落到某一个dlist上,在用key按顺序遍历dlist确定哪个是想要的,dlist自带lru机制,访问到的会调整到前面。
relcache:缓存RelationData。
- relcache就是一张hash表保存RelationIdCache结构。
- RelationIdCache结构在进程初始化时分三阶段初始化:创建RelationIdCache hash表、从
pg_filenode.map文件导入
oid→relfilenode、从
pg_internal.init文件导入RelationData(包括
RelationData、
RelationData->rd_rel、
RelationData->rd_attr`)。
失效机制
- 进程本地,维护了数组存放失效消息,在事务提交时决定写共享内存或只失效自己。
- 进程本地,每一层子事务都会维护一个Group结构(InvalidationMsgsGroup),指向消息数组中的几条属于自己的失效消息。
- 进程本地,每一个Group结构中,都会维护一个当前query的group(CurrentCmdInvalidMsgs)、之前消息的group(PriorCmdInvalidMsgs),在事务提交、回滚时,可以分别处理:
- 事务提交:当前的和之前的都需要发送共享内存,被其他进程消费。
- 事务回滚:当前的不管了;之前的需要失效本地缓存,不发送到共享内存。
- 失效时机:子事务/事务提交/回滚AtEOXact_Inval、AtEOSubXact_Inval、CommandCounterIncrement。
1 系统表
系统表记录的元数据用来组织整库的数据结构。
例如:create table t1(a int, b int)
- 在pg_class中记录表名、表文件、行统计信息等等信息:说明表名存在,如何找到表文件等。
- 在pg_attribute中记录列名、列类型等信息:说明表有哪些列、列类型等。
- 在pg_type中增加一条和表名同名的复合类型:声明一个新的复合类型
(a int, b int)
,类型名同表名。
2 系统表缓存
系统表是需要被高频访问的,所以PG为系统表设计了两种进程级缓存:
- syscache:缓存系统表tuple → 缓存行数据。
- relcache:缓存系统表RelationData(表模式信息) → 缓存表结构。
两种缓存保存的都是高频访问数据,可以充分利用cpu的cache,进一步减少访问延迟。
缓存为什么要放到进程本地?因为每个进程执行的业务可能完全不同,缓存的数据也会有差异,并且进程天然隔离,做到本地简单、高效。如果放到共享内存中,并发读写需要有非常精细的控制,肯定要引入锁、atomic等同步机制,得不偿失。
3 syscache(catalog cache)
syscache 以一个数组的形式存放在内存中,每一个数组位置存放一个CatCache,每一个CatCache直观上可以看做一个固定SQL的结果集,具体的数据结构参考这里:
《Postgresql源码(45)SysCache内存结构与搜索流程分析》
cacheinfo数组中保存着上面提到的这些“SQL”例如:
static const struct cachedesc cacheinfo[] = {
...
...
...
[RELNAMENSP] = {
RelationRelationId,
ClassNameNspIndexId,
KEY(Anum_pg_class_relname, Anum_pg_class_relnamespace),
128
},
[RELOID] = {
RelationRelationId,
ClassOidIndexId,
KEY(Anum_pg_class_oid),
128
},
...
...
...
功能上可以看做:
- RELNAMENSP
- 等价为:
select * from pg_class where relname = ? and relnamespace = ?
- 走索引:ClassNameNspIndexId
- 等价为:
- RELOID
- 等价为:
select * from pg_class where oid = ?
- 走索引:ClassOidIndexId
- 等价为:
查询出来的结果(tuple)存放在CatCache的dlist中,CatCache还支持一批数据缓存,具体在上面文章中介绍,不再展开。
初始化流程:
void
InitCatalogCache(void)
{
int cacheId;
SysCacheRelationOidSize = SysCacheSupportingRelOidSize = 0;
for (cacheId = 0; cacheId < SysCacheSize; cacheId++)
{
SysCache[cacheId] = InitCatCache(cacheId,
cacheinfo[cacheId].reloid,
cacheinfo[cacheId].indoid,
cacheinfo[cacheId].nkeys,
cacheinfo[cacheId].key,
cacheinfo[cacheId].nbuckets);
SysCacheRelationOid[SysCacheRelationOidSize++] =
cacheinfo[cacheId].reloid;
SysCacheSupportingRelOid[SysCacheSupportingRelOidSize++] =
cacheinfo[cacheId].reloid;
SysCacheSupportingRelOid[SysCacheSupportingRelOidSize++] =
cacheinfo[cacheId].indoid;
}
qsort(SysCacheRelationOid, SysCacheRelationOidSize,
sizeof(Oid), oid_compare);
SysCacheRelationOidSize =
qunique(SysCacheRelationOid, SysCacheRelationOidSize, sizeof(Oid),
oid_compare);
qsort(SysCacheSupportingRelOid, SysCacheSupportingRelOidSize,
sizeof(Oid), oid_compare);
SysCacheSupportingRelOidSize =
qunique(SysCacheSupportingRelOid, SysCacheSupportingRelOidSize,
sizeof(Oid), oid_compare);
CacheInitialized = true;
}
4 relcache
hash表缓存最常用的数据结构RelationData:
typedef struct RelationData
{
RelFileLocator rd_locator; /* relation physical identifier */
SMgrRelation rd_smgr; /* cached file handle, or NULL */
int rd_refcnt; /* reference count */
ProcNumber rd_backend; /* owning backend's proc number, if temp rel */
bool rd_islocaltemp; /* rel is a temp rel of this session */
bool rd_isnailed; /* rel is nailed in cache */
bool rd_isvalid; /* relcache entry is valid */
bool rd_indexvalid; /* is rd_indexlist valid? (also rd_pkindex and
* rd_replidindex) */
bool rd_statvalid; /* is rd_statlist valid? */
...
...
Form_pg_class rd_rel; /* RELATION tuple */
TupleDesc rd_att; /* tuple descriptor */
Oid rd_id; /* relation's object id */
LockInfoData rd_lockInfo; /* lock mgr's info for locking relation */
...
...
} RelationData;
4.1 重要数据文件
pg_filenode.map
问题:在backend进程启动过程中,需要使用一张系统表,代码中是知道系统表具体oid的,oid对应磁盘上哪个文件,正常需要在pg_class中查询relfilenode,但是pg_class表还没加载。所以现在需要提供一个系统表oid → relfilenode的映射关系,可以找到一些最基础的系统表。
解法:pg_filenode.map提供了表oid到relfilenode的映射关系。
pg_relation_filenode函数可以查询表对应的relfilenode
pg_internal.init
问题:要构造一个RelationData需要访问pg_class、pg_arrtibute、pg_type等等系统表的数据,才能构造出来。但进程启动阶段,一些基础系统表的RelationData 如果每次扫描表再去构造效率会很差。
解法:pg_internal.init提供了预先计算好的系统表的 RelationData 结构。
4.2 初始化一阶段:RelationCacheInitialize
创建hash表RelationIdCache
RelationCacheInitialize
ctl.keysize = sizeof(Oid);
ctl.entrysize = sizeof(RelIdCacheEnt);
RelationIdCache = hash_create("Relcache by OID", INITRELCACHESIZE,
&ctl, HASH_ELEM | HASH_BLOBS);
RelationMapInitialize();
shared_map.magic = 0; /* mark it not loaded */
local_map.magic = 0;
shared_map.num_mappings = 0;
local_map.num_mappings = 0;
active_shared_updates.num_mappings = 0;
active_local_updates.num_mappings = 0;
pending_shared_updates.num_mappings = 0;
pending_local_updates.num_mappings = 0;
4.3 初始化二阶段:RelationCacheInitializePhase2
- 读共享库的pg_filenode.map
- 读共享库的pg_internal.init
void
RelationMapInitializePhase2(void)
{
load_relmap_file(true, false);
...
...
if (!load_relcache_init_file(true))
{
// 失败了要兜底!
formrdesc("pg_database", DatabaseRelation_Rowtype_Id, true,
Natts_pg_database, Desc_pg_database);
formrdesc("pg_authid", AuthIdRelation_Rowtype_Id, true,
Natts_pg_authid, Desc_pg_authid);
formrdesc("pg_auth_members", AuthMemRelation_Rowtype_Id, true,
Natts_pg_auth_members, Desc_pg_auth_members);
formrdesc("pg_shseclabel", SharedSecLabelRelation_Rowtype_Id, true,
Natts_pg_shseclabel, Desc_pg_shseclabel);
formrdesc("pg_subscription", SubscriptionRelation_Rowtype_Id, true,
Natts_pg_subscription, Desc_pg_subscription);
#define NUM_CRITICAL_SHARED_RELS 5 /* fix if you change list above */
}
}
load_relmap_file加载pg_filenode.map
数据
typedef struct RelMapFile
{
int32 magic; /* always RELMAPPER_FILEMAGIC */
int32 num_mappings; /* number of valid RelMapping entries */
RelMapping mappings[MAX_MAPPINGS];
pg_crc32c crc; /* CRC of all above */
} RelMapFile;
(gdb) p shared_map
$1 = {
magic = 5842711,
num_mappings = 50,
mappings = {
{mapoid = 1262, mapfilenumber = 1262},
{mapoid = 2964, mapfilenumber = 2964},
{mapoid = 1213, mapfilenumber = 1213},
...
...
{mapoid = 1260, mapfilenumber = 1260},
{mapoid = 6115, mapfilenumber = 6115},
{mapoid = 0, mapfilenumber = 0}},
crc = 1938758537}
load_relcache_init_file加载pg_internal.init
4.4 初始化三阶段:RelationCacheInitializePhase3
- 读非共享库的pg_filenode.map
- 读非共享库的pg_internal.init
void
RelationMapInitializePhase3(void)
{
load_relmap_file(false, false);
if (IsBootstrapProcessingMode() ||
!load_relcache_init_file(false))
{
// 失败了兜底!
needNewCacheFile = true;
formrdesc("pg_class", RelationRelation_Rowtype_Id, false,
Natts_pg_class, Desc_pg_class);
formrdesc("pg_attribute", AttributeRelation_Rowtype_Id, false,
Natts_pg_attribute, Desc_pg_attribute);
formrdesc("pg_proc", ProcedureRelation_Rowtype_Id, false,
Natts_pg_proc, Desc_pg_proc);
formrdesc("pg_type", TypeRelation_Rowtype_Id, false,
Natts_pg_type, Desc_pg_type);
#define NUM_CRITICAL_LOCAL_RELS 4 /* fix if you change list above */
}
}
数据
typedef struct RelMapFile
{
int32 magic; /* always RELMAPPER_FILEMAGIC */
int32 num_mappings; /* number of valid RelMapping entries */
RelMapping mappings[MAX_MAPPINGS];
pg_crc32c crc; /* CRC of all above */
} RelMapFile;
(gdb) p local_map
{
magic = 5842711,
num_mappings = 17,
mappings = {
{mapoid = 1259, mapfilenumber = 1259},
{mapoid = 1249, mapfilenumber = 1249},
{mapoid = 1255, mapfilenumber = 1255},
...
...
{mapoid = 3455, mapfilenumber = 3455},
{mapoid = 0, mapfilenumber = 0}},
crc = 3752523506}
5 缓存同步
失效消息处理是通过共享内存和轮询的机制实现的。
5.1 进程本地失效消息记录
本地的操作在事务操作之前,不应该通知任何其他进程,所以机制上会先把需要失效的信息记录到进程本地InvalMessageArrays数组中,等事务提交时在做统一处理,这里先看下本地进程如何保存失效消息的。
例如relcache失效入口之一:
- CacheInvalidateRelcache
- PrepareInvalidationState
- 构造TransInvalidationInfo结构,与子事务绑定
- TransInvalidationInfo中记录了当前的InvalidationMsgsGroup和上一个InvalidationMsgsGroup。
- InvalidationMsgsGroup里面记录了数组的起始位置和结束位置。
- RegisterRelcacheInvalidation
- AddRelcacheInvalidationMessage
- 检查InvalMessageArrays数组中没有这一条
- AddInvalidationMessage
- 插入InvalMessageArrays数组中,并更新InvalidationMsgsGroup中记录的位置。
- AddRelcacheInvalidationMessage
- PrepareInvalidationState
注意:InvalidationMsgsGroup的作用就是记录InvalMessageArrays数组中的起始、终止位置。
进程本地保存失效消息数据结构:
(为什么交nestmsg:最后一条失效消息的下一个)
5.2 进程提交、回滚时对失效消息的处理
见注释:
void
AtEOXact_Inval(bool isCommit)
{
...
if (isCommit)
{
if (transInvalInfo->RelcacheInitFileInval)
RelationCacheInitFilePreInvalidate();
// 把当前的失效消息追加到prior中
AppendInvalidationMessages(&transInvalInfo->PriorCmdInvalidMsgs,
&transInvalInfo->CurrentCmdInvalidMsgs);
// 顶层事务提交时:共享内存发送失效消息
ProcessInvalidationMessagesMulti(&transInvalInfo->PriorCmdInvalidMsgs,
SendSharedInvalidMessages);
if (transInvalInfo->RelcacheInitFileInval)
RelationCacheInitFilePostInvalidate();
}
else
{
// 顶层事务回滚时:只需要把自己的失效掉,不需要发送出去
ProcessInvalidationMessages(&transInvalInfo->PriorCmdInvalidMsgs,
LocalExecuteInvalidationMessage);
}
...
}
注意,当进程回滚时,为什么要把自己本地的失效掉?因为事务内的一些写、读操作,可能已经cache了一些会被回滚调的数据,cache没有mvcc机制,这里必须把回滚调(不可见)的数据失效掉,否则后面在读到这些数据就是脏读了。
5.3 CommandCounterIncrement触发本地失效
一个事务中执行了多个命令,但直到事务最终提交之前,这些更改都是暂时的。意味着在事务提交之前,肯定不会将失效消息发送到共享队列。但是,即使事务最终回滚,每个命令执行后的本地缓存仍需要反映这些暂时的更改,保证事物内的后续查询能拿到正确的结果。
CommandCounterIncrement
AtCCI_LocalCache
CommandEndInvalidationMessages
// 先把当前query造成的失效消息做 到 本地
ProcessInvalidationMessages(&transInvalInfo->CurrentCmdInvalidMsgs,
LocalExecuteInvalidationMessage)
// 把当前的失效消息 追加到 历史消息中 PriorCmdInvalidMsgs
AppendInvalidationMessages(&transInvalInfo->PriorCmdInvalidMsgs,
&transInvalInfo->CurrentCmdInvalidMsgs);
5.4 为什么TransInvalidationInfo有两个Group?
InvalidationMsgsGroup记录消息队列中的起止位置,这几个消息是当前Group管理的。
TransInvalidationInfo中记录了两个Group?当前CurrentCmdInvalidMsgs、历史PriorCmdInvalidMsgs。
- 当前的失效消息需要再每个命令执行后,应用到本地,保证事物内的后续SQL能查到正确的缓存数据。
- 当前的失效消息在事务回滚时,不需要处理,只需要把历史PriorCmdInvalidMsgs做到本地即可。
typedef struct TransInvalidationInfo
{
/* Back link to parent transaction's info */
struct TransInvalidationInfo *parent;
/* Subtransaction nesting depth */
int my_level;
/* Events emitted by current command */
InvalidationMsgsGroup CurrentCmdInvalidMsgs;
/* Events emitted by previous commands of this (sub)transaction */
InvalidationMsgsGroup PriorCmdInvalidMsgs;
/* init file must be invalidated? */
bool RelcacheInitFileInval;
} TransInvalidationInfo;