openGauss vacuum analyze代码走读


74efb063-088b-4408-8882-2851bc3c68e3.gif

一些概念


vacuum、analyze在很多流程上是公用的,主要是analyze用了vacuum的很多流程,包括语法树主要结构体 VacuumStmt ,主要流程操作函数 vacuum() 等。


vacuum、analyze可以手动以SQL的形式触发,也可以通过autovacuum、autoanalyze进行自动触发。


触发流程入口

SQL语句触发主要流程

   语法分析中创建出结构体VacuumStmt。      语义分析直接按照功能性语句挂到Query树中。      功能性语句走ProcessUtility执行器,进入主函数:   DoVacuumMppTable() {       解析SQL语句, 校验一些权限啥的,之后调了do_vacuum_mpp_table_other_node:            delta merge:begin_delta_merge()             verify: DoVerifyTableOtherNode                    主要操作:vacuum()      }

autovacuum触发流程

AVCWorker线程执行,直接调用 vacuum() 函数,对某一张特定的表进行分析或整理。

可参考: openGauss autovacuum autoanalyze代码走读

主要操作函数 vacuum()

可以被语句调用,也会被avc调用。
入参:stmt, 表oid, 要不要搞toast,buffer使用策略

函数走读伪代码:
   一些安全性检查,上下文、计数统计等乱七八糟的初始化。        获取要vacuum的目标关系的oid列表。如果给了则直接用,不然就是解析SQL,获取可能是一个表,也可能是一整个数据库的表等。    relations = get_rel_oids(relid, vacstmt);          一些事务相关的操作       try:        置空vacuum_cxt,某个级别的vacuum信息,看上去和页面buffer命中率、速度控制等有关。                遍历每个需要的关系。        foreach (cur, relations) {            设置一些状态、上下文变量等。                        如果指定了做vacuum则调用:vacuum_rel()            如果指定了做analyze则调用:analyze_rel()        catch:        ....            一些事务的处理    如果vacuum了,并且不是avc的话,则vac_update_datfrozenxid更新datfrozenxid。(更新成pg_class的frozenxid最小值)。        

关键函数:vacuum_rel(), 对一个关系做vacuum操作

VACOPT_NOWAIT:取锁时不等待,只有avc才是true.

    一些事务的操作,一些检查等。        如果不是vacuum full则设置一个全局flag,表示正在做lazy vacuum。    X ProcArrayLock    t_thrd.pgxact->vacuumFlags |= PROC_IN_VACUUM;    D ProcArrayLock

   判断锁力度、分区的锁粒度。full需要加几级,lazy多少级。系统表又是多少级。   if vacuumRelation(vacstmt->flags) && !(vacstmt->options & VACOPT_NOWAIT):主动触发的vacuum relation        判断一些安全性与权限:            一些安全性的,vacuum系统表的权限校验等            啥xc_maintenance_mode            啥升级        onerel = try_relation_open(relid, lmode); 按照算好的锁力度打开表   elif vacuumPartition(vacstmt->flags) && !(vacstmt->options & VACOPT_NOWAIT): 主动触发vacuum 分区        区分区分主表、分区、二级分区等,按照算好的力度打开表        onepartrel  onepart     onerel  onesubpartrel等   elif vacuumMainPartition(vacstmt->flags) && !(vacstmt->options & VACOPT_NOWAIT):主动触发 vacuum 分区表        onerel = try_relation_open(relid, lmode);  按照算好的力度打开表   elif vacuumRelation(vacstmt->flags) && ConditionalLockRelationOid(relid, lmode):autovacuum时,仅尝试打开表        已经在ConditionalLockRelationOid拿到锁了,再nolock随便打开一下。   elif vacuumPartition(vacstmt->flags) && ConditionalLockRelationOid(relationid, lmodePartTable):autovacuum时,仅尝试打开分区        已经在ConditionalLockRelationOid拿到锁了,再nolock随便打开一下。        还得处理一些分区的情况   elif vacuumMainPartition(vacstmt->flags) && ConditionalLockRelationOid(relationid, lmodePartTable):  autovacuum时,仅尝试打开分区表        已经在ConditionalLockRelationOid拿到锁了,再nolock随便打开一下。            最后如果没有拿到或拿全锁,有些分区没拿到等。改打日志打日志,该放锁的放锁,        退出。            如果是列存表则打开cu、delta等    又是一大坨校验,权限、安全、表类型、支不支持等,    又是对啥ustore、分区、二级分区、物化视图、增量物化视图之类的,一大坨校验、加锁操作。           switch:根据不同的表类型、不同的操作粒度,调用不同的接口来处理。    - vacuum full ustore: 跳过    - vacuum full 普通表:cluster_rel()    - vacuum full 分区:vacuumFullPart()    - vacuum full main分区表:GpiVacuumFullMainPartiton(), CBIVacuumFullMainPartiton()    - vacuum ustore 分区表:UstoreVacuumMainPartitionGPIs()    - vacuum 分区表:GPIVacuumMainPartition、CBIVacuumMainPartition    - vacuum 其他(普通表、分区、普通ustore):TableRelationVacuum        提交事务等。        如果有toast则触发一下toast的vacuum    如果是物化视图的啥玩意则干点物化视图的啥玩意。        释放一堆锁。    

子函数 cluster_rel():对普通表做vacuum full

cluster 是顺序聚簇操作,将元组按照某个索引的顺序聚簇,并重建整个表以及索引。因此 vacuum full cluster 的一个简化版,不需要排序,仅是重建。重建原理过程比较简单。新建一个heap,然后遍历老heap,将元组堆积就好了。


但是需要注意,元组之间存在hot、ctid链,这些链不能破坏,因此在重建期间会有几个hash表,用于存放扫描到的映射关系,来建立这些元组间指针。


由于加的是八级锁,也不需要担心并发访问问题,先重建表,然后重建索引即可。

子函数 TableRelationVacuum()->lazy_vacuum_rel():大部份表的vacuum

适用范围:普通表、分区
一些概念:
lazy vacuum 会启动一个事物,但不会在任何页面上写自己的事务号。包括更新pg_class内相关数据的时候,都不会写自己的事务号,这可能与系统表的扫描snapshot相关。

        处理点二级分区的特殊情况。        处理列存表:        PASS        结束                处理下avc打日志的情况。        vacuum_set_xid_limits():获取vacuum所需要的各种值:        oldestxmin:当前全局需要用到的最小的事务号。这里需要注意,如果目标并是系统表,则这里使用catalogxmin,普通表则使用全局oldestxmin,这和逻辑复制有关,逻辑复制需要使用xlog当时的元数据,所以将系统表的底线xmin与普通表的分开,防止旧元数据被清理,也防止普通表长期得不到清理。        freezelimit:        freezeTableLimit:            获取relfrozenxid。在pg_class或pg_partition里获取    判断是否需要全表扫描:relfrozenxid < freezeTableLimit  ?        LVRelStats* vacrelstats;  一个看上去是用来记录vacuum进展状态以及一些计数的上下文。初始化。        打开对应的索引。vac_open_part_indexes()、vac_open_indexes()            【清理操作】    lazy_scan_rel(){                初始化一些东西。            例如给每个索引都申请一些块内存结构,看上去是用来计数的。            nblocks:获取文件一共有多少个block            申请一段内存,用来在vacuum的时候用。内存大小主要是根据一个页面最多有多少个元组,来申请头部的一些容量。也和是否有索引有关,受maintenance_work_mem控制            InitVacPrintStat(&printStats) 初始化计数结构。                            vacuum动作,主要有两遍。        1、lazy_scan_heap()       一扫清理。执行heap page prune进行单页面内的清理。死元组位置列表、空闲空间等记录到vacrelstats。计算统计信息、标记页面头全可见等。清理索引。        2、lazy_vacuum_heap()     二扫清理。因为在索引被清理之前,死元组是无法删除的,上一步已经清理索引了,所以需要二扫来清理死元组,且如果有记录死元组,则在上一步已经记录到vacrelstats了。        一扫伪代码逻辑如下:        lazy_scan_heap {            又初始化了一些结构,像是计数的。                        根据vmap,找到第一个非全可见页面。next_not_all_visible_block。这里有一个逻辑,如果连续32个页面都全可见,那么除非入参指定了scan_all,否则会跳过这32个页面,不进行扫描。                        for 每个页面:                判断检查是否满足32页面跳过的逻辑。是否进行跳过。                                异步IO预读?enable_adio_function                                ???有点没看懂的  close to overrunning the available space                                pin住当前页的vmap                                buf = ReadBufferExtended()加载页面。                需要加X锁独占页面,尝试加X锁失败则尝试加S锁判断一些freeze的情况,如果不要紧的话,就跳过这个页面。                                获取页面                                if 这是个新页面或空页面,就稍微规整一下页面头,置置脏,记录一下fsm或vmap等,然后下一个。                                页面剪枝清理。heap page prune只会去删除页面元组的data部分,清理hot链,不会删除指针部分。                heap_page_prune() {                     创建一个PruneState,记录剪枝状态。                                        for 每个元组:                        已经被前边链式访问时清理过,或者死元组,或者是个unused指针,则跳过。                        记录需要修建的链,需要修剪的元组等,到PruneState                                        根据PruneState进行修剪。(根据元组页面结构,显然这里是要先遍历记录,然后统一修剪的,应该无法做到边遍历边修剪)                                        维护pageheader的一些flag,如pagefull之类的。                                        返回删除了的元组数。                }                                    for 每个元组: (重新遍历页面,搞一些计数之类的东西。判断allvisible之类的。)                    看看是不是重定向了或已经知道的死元组,做一些啥处理。                    检测vacuum mvcc,根据元组状态,做一些啥东西。例如更新元组状态、记录计数、等。                                    根据刚才遍历统计记录的结果,按需执行freeze、invalid、等,做一些操作:                    log_heap_freeze:冻结页面元组                    log_heap_invalid:给页面置成invalid?                    lazy_vacuum_page:如果没有索引的话,直接清理死元组。                                如果页面全可见的话,更新vm                                更新fsm                                    vac_estimate_reltuples:根据之前的扫描统计清理结果,重新估算reltuples                        foreach indedx do lazy_vacuum_index():如果有死元组,则清理每个索引                调用ambulkdelete的函数,btree是 btbulkdelete                调用btvacuumscan,清理。                并更新和返回                        foreach index do lazy_cleanup_index():                调用amvacuumcleanup函数,btree是 btvacuumcleanup                看上去如果btbulkdelete做了,这步就只做一个fsm的更新,不然还会btvacuumscan一遍。                                        打印点日志,更新一下printStats                        返回indstats,索引数量        }                二扫伪代码逻辑如下:        lazy_vacuum_heap(){            在上一步的lazy_scan_heap中,已经给hot链、索引清理完了,现在二刷,就可以清理那些没有指针指到的死元组了。            但是头部指针仍然不能动,因为如果头也清理了,必然影响其他的头的顺序,所以只会给那个指针的flag制成unusable。后续可以直接复用。                        还需要记录fsm        }                        整理FSM        FreeSpaceMapVacuum                将索引计数加到vacuum计数上。            }        看看能不能截断最后面的空block(存在日志复制的时候不会,可能redo到时候会有问题,不懂)        visibilitymap_count()更新vmap        更新数据特征统计信息:pg_class、pg_partition内的relfrozenxid、行数、全可见页面数量等等。        关闭对应的索引        pstat上报vacuum计数统计信息        avc Log_autovacuum_min_duration打印日志                


关键函数 analyze_rel():对一个关系做analyze操作

analyze相对于vacuum来说比较简单。


相关数据结构:

struct VacAttrStats :统计采样分析算法的计算上下文。用在对某一列进行统计算法计算的时候,存储这列的数据类型、各种统计信息计算算法的中间变量数据、结果等。


主要代码

    Relation onerel = analyze_get_relation(relid, vacstmt);  打开需要analyze的表        一堆校验        【开始的analyze】    analyze_rel_internal() :        又是一堆校验,啥系统表、临时表、pg_statistic啥玩意的。                如果是外表,则调用fdw相关hook,然后基本就结束了                relpages:获取表的页面数量。                do_analyze_rel():            啥玩意检查和日志                       任务进度上下文,记录analyze任务的进度、状态            caller_context = do_analyze_preprocess()            算法变量上下文。为需要analyze的每一列创建算法变量上下文,获取这些列上是不是有索引等信息            VacAttrStats** vacattrstats = get_vacattrstats_by_vacstmt(onerel, vacstmt, $attr_cnd, &nindexes, &indexdata, &hasindex, inh, &Irel):
    targrows: 采样需要的行数。与表、索引都有关。           rows = get_total_rows<false>(...)  采样。         acquire_sample_rows:  采样,使用BlockSampler模块进行采样,可见性判断用HeapTupleStatisfiesVacuum(Oldestxmin)。                      因此可以认为是按页面进行采样。                   bool ret = do_analyze_samplerows(): 对采样行进行统计。            更新pg_class,汇报pgstat            如果不是vacuum analyze语句触发的话,需要清理索引?            关闭索引     autoanalyze的的话打印相关日志。     do_analyze_finalize():关闭事务,清理内存等。


其他函数 vacuum_delay_point():控制vacuum速度

控制vacuum速度的东西,还和几个vacuum cost、delay等参数有关。函数存在于vacuum流程中的各个点。

往期推荐

openGauss autovacuum

autoanalyze代码走读

a3901e3e-3f7a-4657-8107-e490fd17e3d1.png

openGauss数据库段页式与代码走读

a3901e3e-3f7a-4657-8107-e490fd17e3d1.png



本文分享自微信公众号 - openGauss(openGauss)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“ OSC源创计划 ”,欢迎正在阅读的你也加入,一起分享。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

openGauss社区

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值