openGauss vacuum analyze代码走读

2024年 3月 19日 29.2k 0

一些概念

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(...)  采样。
                      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代码走读

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

              相关文章

              Oracle如何使用授予和撤销权限的语法和示例
              Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
              下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
              社区版oceanbase安装
              Oracle 导出CSV工具-sqluldr2
              ETL数据集成丨快速将MySQL数据迁移至Doris数据库

              发布评论