GaussDB(DWS)文件级空间统计简介
背景介绍
GaussDB(DWS)在8.1.3版本新增了文件级空间统计能力,提供了记录各relfilenode物理文件空间大小的pg_relfilenode_size系统表,内核的常驻线程会维护该表数据的准确性。基于pg_relfilenode_size系统表,主要能解决两个痛点问题:
1.GaussDB(DWS)在8.1.3版本之前,Schema空间管控一直有一个限制,即:Schema级别的空间统计无法统计到列存表的辅助表(如:CUDesc、Delta、Toast)空间,仅能包含列存表的主数据空间;这个限制的缘由是由于CUDesc、Delta表固定属于CSTORE Schema(对应为pg_namespace系统表中oid为100),Toast表固定属于PG_TOAST Schema(对应为pg_namespace系统表中oid为99),当内核存储底层对单一辅助表进行增删改操作时,无法联想到所属主表的schema,因此,辅助表该部分空间的增删无法反馈到主表schema上;
2.GaussDB(DWS)在8.1.3版本之前,统计一个库底下所有表的空间占用情况非常耗时,也会长时间占用不小内存空间,基于SSD环境的测试结果显示,通过table_distribution()函数查询200万张表的空间数据约耗时4.5小时,这个耗时长度是无法满足业务日常记录空间增长曲线的需求的。
痛点问题的应对方案
针对上文提出的两个痛点问题,GaussDB(DWS)在8.1.3版本中给了相应的解决方案,应对思路如下:
1.针对问题1,存储底层操作单一列存辅助表时,是无法打开关联的主表,寻找主表和打开主表的操作会给存储底层操作带来严重的负担,代价过高,会影响业务性能,打开关联主表的思路是不可行的;因此,GaussDB(DWS)在8.1.3版本中的应对方案是消息队列,通过消息队列将文件级的增删改操作动作记录下来,然后在内核后台维护线程中回放操作,实现了异步处理流程,解耦了辅助表与主表的强依赖关系;但这也会引发另一个问题,当内核后台线程异步回放文件增删改操作时,在关联文件所属schema时,是无法保证对应表是否已经被删除,这一细节将会在后文介绍;
2.针对问题2,table_distribution()函数查全库表大小耗时久主要有2点原因:①在查单表大小时,会通过try_relation_open函数连接该表的所有元数据定义,即必然打开pg_class、pg_attribute系统表,选择性打开pg_partition、pg_index系统表,通过这些系统表中的元数据信息组合成完整的表信息,虽然都是索引点查操作,但200万次索引点查动作仍然会是一个耗时很高的动作;②在组合完表的元数据信息之后,仍然需要通过磁盘寻址找到所关联的多个物理文件,计算这些物理文件大小之和。在分析出耗时久的原因基础之上,相应的解决思路也很清晰:①将200万次索引点查转化为对系统表的顺序扫描会极大的降低耗时,通过缓存机制将元数据缓存到内存中,在内存中批量连接元数据;该操作能降低时耗,但内存占用无法降低,通过进一步分析,可以发现,针对于查询表大小这一操作,是无需知道表的字段信息(pg_attribute系统表信息)和索引信息(pg_index系统表信息);对于普通表,仅需要依赖pg_class系统表,对于分区表,仅需要依赖pg_class和pg_partition系统表;②为规避掉磁盘的寻址操作,那就需要在查询动作触发之前,计算出每个物理文件大小并缓存下来,即前文已经提到的pg_relfilenode_size系统表。
下文将会更加详细的介绍核心思路的设计方案,相关内容有:消息队列、辅助表反查、pg_relfilenode_size系统表。
消息队列
由于空间的增删消息是有一定顺序的,必然都是先增后删,为保证消息的有序性,我们使用一个无锁队列来确保消息的有序性。消息分为:空间增加消息(SpaceIncrease)、空间删除消息(SpaceDecrease)。
ThreadA. EnqueueSpaceIncreaseMsg
==> malloc SpaceUpdateMsg
==> SpaceUpdateMsg.type = SpaceIncrease (空间增加消息)
==> SpaceUpdateMsg.spaceInfo = 其它信息 <db, relfilenode, type, size>
ThreadB. EnqueueSpaceDecreaseMsg
==> malloc SpaceUpdateMsg
==> SpaceUpdateMsg.type = SpaceDecrease (空间删除消息)
==> SpaceUpdateMsg.spaceInfo = 其它信息 <db, relfilenode, type, size>
除此之外,前文还提到过异步处理消息时,该物理文件相关联的用户表可能已经被删除,进而无法找到所属的schema,为此,我们还需要记录短期内被删除的表信息。示意图如下所示:
业务线程 |
后台常驻线程 |
DROP TABLE/INDEX |
|
ALTER TABLE DROP PARTITION |
|
ALTER TABLE TRUNCATE PARTITION |
|
|
收到SpaceDecrease消息进行处理 |
|
查询pg_class、pg_index、pg_partition无相关relation信息 |
我们再考虑一个极端场景,如果用户业务在事务内建表、建索引、导数、删表,在这个流程中产生的空间增加和删除消息,由异步线程处理时,由于事务隔离性,对该用户业务所建立的表信息和索引信息是不可见的,更加无法找到所属的schema,为此,我们还需要记录短期内新创建的表信息和索引信息。示意图如下所示:
业务线程 |
后台常驻线程 |
START TRANSACTION |
|
CREATE TABLE/INDEX |
|
INSERT INTO TABLE |
|
|
收到SpaceIncrease消息进行处理 |
|
查询pg_class、pg_index、pg_partition无相关relation信息 |
COMMIT |
|
|
查询pg_class、pg_index、pg_partition有相关relation信息 |
业务线程 |
后台常驻线程 |
START TRANSACTION |
|
CREATE TABLE/INDEX |
|
INSERT INTO TABLE |
|
|
收到SpaceIncrease消息进行处理 |
|
查询pg_class、pg_index、pg_partition无相关relation信息 |
ABORT |
|
|
收到SpaceDecrease消息进行处理 |
|
查询pg_class、pg_index、pg_partition无相关relation信息 |
相关消息类型:
ThreadA. 删除Relation操作
==> malloc DeleteRelationInfo
==> DeleteRelationInfo.type = DeleteRelation (删除的relation消息)
==> DeleteRelationInfo.relationInfo = 其它信息
ThreadB. 创建Relation操作
==> malloc UncommitRelationInfo
==> UncommitRelationInfo.type = UncommitRelation (新建的relation消息)
==> UncommitRelationInfo.relationInfo = 其它信息
ThreadC. 创建Index操作
==> malloc UncommitIndexInfo
==> UncommitIndexInfo.type = UncommitIndex (新建的index消息)
==> UncommitIndexInfo.relationInfo = 其它信息
总结,消息队列一共有5个消息类型,如下表:
消息类型 |
含义 |
SpaceIncrease |
物理文件空间增加消息 |
SpaceDecrease |
物理文件空间减小消息 |
DeleteRelation |
被删除的Relation表信息 |
UncommitRelation |
事务未提交的建表信息 |
UncommitIndex |
事务未提交的建索引消息 |
辅助表反查
前文已经提到过,Schema空间管控约束是列存表无法计算其辅助表部分的空间,而引入消息队列的目的就是为了解除此约束,上一段落已经介绍过消息队列中的五种消息类型,那么本段落将会介绍消息队列中的消息处理和核心的辅助表反查思路。
在此,我们还需要细化一下辅助表的详细类型,如下表所示:
表类型 |
含义 |
ToastIdx |
Toast表的索引 |
Toast |
列存Cudesc辅助表关联的Toast表 |
CudescIdx |
列存Cudesc辅助表的索引 |
Cudesc |
列存Cudesc辅助表 |
Delta |
列存Delta辅助表 |
针对以上这5类辅助表,不考虑事务隔离性,不考虑表信息的删除,反查这类辅助表所属主表的查询流程如下图所示,依赖对pg_index、pg_depend、pg_partition系统表的点查操作能逐步找到其主表信息。
在考虑事务隔离性、考虑表信息的删除的前提下,上图依靠系统表反查的过程中,随时可能因为对应系统表中无相应数据而中断反查流程,这种情况就需要依靠前面消息队列中记录的DeleteRelation、UncommitRelation、UncommitIndex三类消息,再处理消息队列中这三类消息时,我们使用哈希结构(命名为:relationInfoHash)保留主表的相关信息,而对于创建的辅助表,为减小内存占用,我们使用哈希结构(命名为:relationReverseHash)仅保留辅助表的反查关系<辅助表Oid,关联表Oid>,即:
- <ToastIdx表Oid, Toast表Oid>:表示ToastIdx表属于哪张Toast表;
- <Toast表Oid, CuDesc表Oid>:表示Toast表属于哪张CuDesc表;
- <CuDescIdx表Oid, CuDesc表Oid>:表示CuDescIdx表属于哪张CuDesc表;
- <CuDesc表Oid, 主表Oid>:表示CuDesc表属于哪张主表;
- <Delta表Oid, 主表Oid>:表示Delta表属于哪张主表。
基于表信息缓存哈希<relationInfoHash>和反查哈希<relationReverseHash>,前文中提到的反查辅助表所属主表的查询流程可变更为如图所示:
到此处为止,前文已经介绍了2个核心逻辑:消息队列、辅助表反查,在这两个机制的基础上,其实已经能解决痛点问题1,即:Schema级别的空间统计无法统计到列存表的辅助表;下文中,将会介绍pg_relfilenode_size系统表,解决痛点问题2的必要基石。
pg_relfilenode_size系统表
上文中,对于痛点问题2的应对方案中,其实已经提到,为解决查询全库表大小耗时久的问题,最主要的措施就是规避掉磁盘的寻址操作,那就需要在查询动作触发之前,计算出每个物理文件大小并缓存下来;为此,我们引入了pg_relfilenode_size系统表,其表结构定义如下:
字段含义如下:
Databaseid |
Oid |
数据库oid; |
Tablespaceid |
Oid |
表空间oid; |
Relfilenode |
Oid |
Relfilenode物理文件oid; |
Backendid |
Integer |
后台backendId号; |
Type |
Integer |
物理文件类型,如:FSM、BCM、VM等; |
Filesize |
Bigint |
文件大小; |
在存储底层记录和更新文件大小的操作,也是高危逻辑,导致逻辑混杂和性能问题,相信看到此处的读者已经发现,前文提到的消息队列正是解决此问题的良策,即:在处理消息队列中的IncreaseSpace、DecreaseSpace消息时,借助于pg_relfilenode_size系统表,维护好relfilenode级别的物理文件空间大小数据。总体流程如下图所示:
快速查询全库表大小
在pg_relfilenode_size系统表的基础上,GaussDB(DWS)在8.1.3版本提供了快速查询全库表达小函数gs_table_distribution(),其查询结果示意如下所示,返回内容为某个表在各个数据节点上的表大小。
经过测试对比,200万张表,gs_table_distribution()函数的查询时间约为7分钟,相较table_distribution()函数的4.5小时,时间缩短了30-40倍,而内存占用也下降至原来的10%左右(数据量越大比例越低),关于内存的优化过于技术,此博文将不阐述该部分内容。
结束语
看到此处的用户,感谢支持,还不快来感受一下GaussDB(DWS)在8.1.3版本新提供的gs_table_distribution()函数,这速度,飞的起来。
- 点赞
- 收藏
- 关注作者
评论(0)