openGauss

开源数据库

openGauss社区官网

开源社区

从运维视角来解析vacuum机制跟相关参数(2)

xuchunyang2024-05-21openGauss

上一篇从运维视角解析了vacuum的机制,但还没讲解完,有几个跟freeze相关的参数还没有涉及到,今天接着往下继续挖,看能否解析到相关参数。

上一篇讲到了vacuum线程如何调起,如何选择db进行vacuum ,以及选择哪些表进行vacuum , 但还没有讲到vacuum是如何做的,做完后影响了(更新了)哪些视图数据。

我们先来看一下test1表的跟vacuum相关的视图的当前数据。

xcytest=# select oid,relname,relfrozenxid,relfrozenxid64 from pg_class where relname='test1';
  oid  | relname | relfrozenxid | relfrozenxid64 
-------+---------+--------------+----------------
 16408 | test1   | 0            |        2270175

当前的fronzenxid为2270175

xcytest=# delete from   test1 limit 10000;
DELETE 10000
xcytest=# delete from test1 limit 30000;
DELETE 30000
xcytest=# select * from pg_stat_all_tables where relname='test1';
-[ RECORD 1 ]-----+------------------------------
relid             | 16408
schemaname        | public
relname           | test1
seq_scan          | 4
seq_tup_read      | 430000
idx_scan          | 
idx_tup_fetch     | 
n_tup_ins         | 0
n_tup_upd         | 0
n_tup_del         | 40000
n_tup_hot_upd     | 0
n_live_tup        | 0
n_dead_tup        | 40000
last_vacuum       | 2024-05-23 11:33:59.193913+08
last_autovacuum   | 2024-05-23 11:33:59.193913+08
last_analyze      | 
last_autoanalyze  | 
vacuum_count      | 2
autovacuum_count  | 2
analyze_count     | 0
autoanalyze_count | 0
last_data_changed | 2024-05-23 11:34:19.086808+08

手工删除了40000条记录了,在这里提一个问题,为什么手工删除了40000条记录了,表还没有被选中自动做vacuum? 表的定义如下:

xcytest=# select pg_get_tabledef(16408);
-[ RECORD 1 ]---+----------------------------------------------------------------------------------------------------------
pg_get_tabledef | SET search_path = public;
                | CREATE TABLE test1 (
                |     id integer,
                |     name character varying(2000)
                | )
                | WITH (orientation=row, compression=no, autovacuum_freeze_max_age=100000, autovacuum_vacuum_threshold=10);

同时,为了更容易触发,还特意表将autovacuum_vacuum_scale_factor的参数修改成了0.001 , 按照前面介绍到的,死亡元组达到设定的阈值之后,就会执行vacuum。已经删除了40000行,但为何迟迟没有执行vacuum ? 一度怀疑之前的解析有差错。再次跟踪do_autovacuum函数,最后发现它确实会做,调试跟跟踪如下:

(gdb) l autovacuum.cpp:3284
3279
3280        reltuples = classForm->reltuples;
3281        vacthresh = (float4)vac_base_thresh + vac_scale_factor * reltuples;
3282        anlthresh = (float4)anl_base_thresh + anl_scale_factor * reltuples;
3283
3284        if ((avwentry == NULL) && (tabentry == NULL)) {
3285            *dovacuum = force_vacuum;
3286            *doanalyze = false;
3287        } else {
3288            if (tabentry && (tabentry->changes_since_analyze || tabentry->n_dead_tuples)) {
(gdb) p reltuples
$33 = 160000   #表的当前行数为160000, 200000-40000(删除了40000)

(gdb) p reltuples
$34 = 160000
(gdb) p vac_scale_factor  数据库参数定义,vac的比例
$35 = 0.00100000005
(gdb) p anl_scale_factor
$36 = 0.100000001  数据库参数定义,anlayze 的比例
(gdb) p vacthresh  根据上述代码的计算方式,计算出死亡元组达到该值后进行vacuum.
$37 = 170
(gdb) p anlthresh   根据上述代码的计算方式,计算出行变化的数量达到该值后进行anlayze.
$38 = 16050
(gdb) p vac_base_thresh
$39 = 10
(gdb) p anl_base_thresh
$40 = 50

至于迟迟没有做vacuum的原因,是因为调度周期还没有到,参数autovacuum_naptime 的默认值为600秒,也就是10分钟一个调度周期。 上面算是回顾了上一篇所学习的内容,加深了对上一篇所得的理解,咱们接着往下解析。上一篇提到,关于freeze的参数还没有解析到,继续往下学习,看是否能够得到答案。 继续往下找,看到了vacuum_set_xid_limits函数,该函数的调用堆栈如下,在下面的堆栈中,我们看到了函数的入参freeze_min_age 以及freeze_table_age的值,刚好是数据库参数vacuum_freeze_min_age与vacuum_freeze_table_age,解析完这部分,大概可以了解到该参数的具体作用。

(gdb) bt
#0  vacuum_set_xid_limits (rel=0x7f460121cea8, freeze_min_age=80, freeze_table_age=100, oldestXmin=0x7f461410ce40, 
    freezeLimit=0x7f461410ce48, freezeTableLimit=0x7f4609cca7b8, multiXactFrzLimit=0x7f461410ce50) at vacuum.cpp:1092
#1  0x00000000016dd4f2 in lazy_vacuum_rel (onerel=0x7f460121cea8, vacstmt=0x7f4609ccae60, bstrategy=0x7f4601356088) at vacuumlazy.cpp:346
#2  0x00000000016d297a in TableRelationVacuum (rel=0x7f460121cea8, vacstmt=0x7f4609ccae60, lockmode=4, vacStrategy=0x7f4601356088)
    at vacuum.cpp:1867
#3  0x00000000016d6610 in vacuum_rel (relid=16408, vacstmt=0x7f4609ccae60, do_toast=false) at vacuum.cpp:2785
#4  0x00000000016cdc62 in vacuum (vacstmt=0x7f4609ccae60, relid=16408, do_toast=false, bstrategy=0x7f4601356088, isTopLevel=true)
    at vacuum.cpp:352
#5  0x000000000173d1d6 in autovacuum_local_vac_analyze (tab=0x7f46013568b0, bstrategy=0x7f4601356088) at autovacuum.cpp:3448
#6  0x0000000001739e71 in do_autovacuum () at autovacuum.cpp:2762
#7  0x0000000001734363 in AutoVacWorkerMain () at autovacuum.cpp:1410
#8  0x0000000001790910 in GaussDbThreadMain<(knl_thread_role)8> (arg=0x7f464c53e448) at postmaster.cpp:12980
#9  0x000000000178a6ba in InternalThreadFunc (args=0x7f464c53e448) at postmaster.cpp:13517
#10 0x000000000232811f in ThreadStarterFunc (arg=0x7f464c53e438) at gs_thread.cpp:382
#11 0x00007f46e848dea5 in start_thread () from /lib64/libpthread.so.0
#12 0x00007f46e81b68dd in clone () from /lib64/libc.so.6

我们来解析这个函数,目的是通过计算,得出freezelimit与freetablelimit的值,然后给调用函数lazy_vacuum_rel使用。

void vacuum_set_xid_limits(Relation rel, int64 freeze_min_age, int64 freeze_table_age, TransactionId* oldestXmin,
    TransactionId* freezeLimit, TransactionId* freezeTableLimit, MultiXactId* multiXactFrzLimit)
{
    int64 freezemin;
    TransactionId limit;
    TransactionId safeLimit;
    TransactionId nextXid;
    。。。。。。。。
    获取当前所有会话中最老的事务id.
    *oldestXmin = GetOldestXmin(rel);
    将入参赋值给临时变量。
    freezemin = freeze_min_age;
   下面是计算freezeLimit的值,可以简单理解为当前活跃的最老的事务id,
   减去参数vacuum_freeze_min_age的值所得。
   如果有从库,从库的当前最老的活跃事务id也在考虑之内. 
   计算出来的值,会给调用函数使用。
   freezemin = Min(freezemin, g_instance.attr.attr_storage.autovacuum_freeze_max_age / 2);
    limit = *oldestXmin;
    if (limit > FirstNormalTransactionId + (uint64)freezemin)
        limit -= (uint64)freezemin;
    else
        limit = FirstNormalTransactionId;
    *freezeLimit = limit;

    接下来计算freezeTableLimit的值,可以简单解析为当前最新的xid ,
    减去参数vacuum_freeze_table_age的值所得。
    计算出来的值,会给调用函数使用。

       freezetable = freeze_table_age;
       freezetable = Min(freezetable, g_instance.attr.attr_storage.autovacuum_freeze_max_age * 0.95);
        limit = ReadNewTransactionId();
        if (limit > FirstNormalTransactionId + (uint64)freezetable)
            limit -= (uint64)freezetable;
        else
            limit = FirstNormalTransactionId;

       
        *freezeTableLimit = limit;

。。。。。。。。。。
}

接下来,我们看lazy_vacuum_rel函数是怎么使用这两个变量的。该函数是通过下面的调用语句来获得这两个变量的值,然后接着就使用。

 通过下面函数计算出u_sess->cmd_cxt.FreezeLimit与freezeTableLimit
   的值。
   
    vacuum_set_xid_limits(onerel,
        vacstmt->freeze_min_age,
        vacstmt->freeze_table_age,
        &u_sess->cmd_cxt.OldestXmin,
        &u_sess->cmd_cxt.FreezeLimit,
        &freezeTableLimit,
        &u_sess->cmd_cxt.MultiXactFrzLimit);  
  然后将表的relfrozenxid(可以通过pg_class查询到)跟freezeTableLimit
  比较,如果小,则scan_all等于true, 做vacuum的时候,
  会对表的所有页面进行scan.  从这里,我们可以得到参数vacuum_freeze_table_age
  的真正意义,同时也应该明白了该参数应该怎样合理设置:设置太小,将
  导致表做vacuum的时候,没有修改的页面也被无意义的重复扫描,白白加重
  数据库的负担。 如果scan_all为false,只会对不是完全可见的页面进行扫描,
  也就是进行过删除数据,或者修改数据的页面进行扫描,页面是否完全可见,
  来自页面可见性位图数据的判断,这里不再展开。
  scan_all = TransactionIdPrecedesOrEquals(relfrozenxid, freezeTableLimit);

上面讲到了freezeTableLimit的意义,还没有讲到u_sess->cmd_cxt.FreezeLimit,该变量在什么时候会被用到? 如下,在判断表里面的行是否需要做freeze的时候被使用到。

我们来看看这个heap_tuple_needs_freeze函数,函数非常简单,如下,最简单的场景,就是xmin小于u_sess->cmd_cxt.FreezeLimit时,该行就会被freeze.

到这里,我们大概知道了vacuum_freeze_min_age参数的意义,该参数决定了表里面的行在什么时候做freeze。如果对行过早做freeze,freeze之后,该行又被更新了,之前的freeze就白做了。另外,freeze 会修改行的infomask ,自然会产生脏页,如果一个页面中不同行,在不同的批次做freeze,自然带来额外的刷脏负担。 所以,设置非常合理的vacuum相关的参数,对数据库的性能的稳定性非常重要,有时候,统一的数据库参数可能无法适用所有的表的各自使用场景,为个别表单独设置特定的参数,我想是非常有必要的。