一些概念
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数据库段页式与代码走读