openGauss vacuum analyze代码走读
发表于 2025/11/24
0
触发流程入口
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 ProcArrayLockt_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 spacepin住当前页的vmapbuf = 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更新fsmvac_estimate_reltuples:根据之前的扫描统计清理结果,重新估算reltuplesforeach 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}整理FSMFreeSpaceMapVacuum将索引计数加到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流程中的各个点。


