diff --git "a/app/zh/blogs/chunyangxu/2024-05-20-\344\273\216\350\277\220\347\273\264\350\247\206\350\247\222\346\235\245\350\247\243\346\236\220vacuum\346\234\272\345\210\266\344\270\216\347\233\270\345\205\263\345\217\202\346\225\260\357\274\2101\357\274\211.md" "b/app/zh/blogs/chunyangxu/2024-05-20-\344\273\216\350\277\220\347\273\264\350\247\206\350\247\222\346\235\245\350\247\243\346\236\220vacuum\346\234\272\345\210\266\344\270\216\347\233\270\345\205\263\345\217\202\346\225\260\357\274\2101\357\274\211.md" new file mode 100644 index 0000000000000000000000000000000000000000..ae398e6e3e8538f20ef6d6de21ed056279aa1340 --- /dev/null +++ "b/app/zh/blogs/chunyangxu/2024-05-20-\344\273\216\350\277\220\347\273\264\350\247\206\350\247\222\346\235\245\350\247\243\346\236\220vacuum\346\234\272\345\210\266\344\270\216\347\233\270\345\205\263\345\217\202\346\225\260\357\274\2101\357\274\211.md" @@ -0,0 +1,297 @@ +--- +title: "从运维视角来解析vacuum机制跟相关参数(1)" +date: '2024-05-20' +category: 'blog' +tags: ['openGauss'] +archives: '2024-05' +author:'xuchunyang' +summary: "从运维视角来解析vacuum机制跟相关参数" +--- + +​ 这两天想仔细了解一下vacuum机制,因为该机制会影响数据库的性能以及表的占用空间。通过网上了解一些资料,有些是从纯代码角度来解析的,有些是用纯文字来描述的,看了之后,似懂非懂,心中还是没有完全理清楚vacuum的机制。于是,准备按照自己的思路,来撸撸opengauss的代码,以便解答自己的疑惑。下面将描述个人的学习思路以及学习所得。 + +​ 想了解vacuum的机制,打算从参数入手,vacuum相关的参数如下: + +``` +xcytest=# select name,setting from pg_settings where name like '%vacuum%'; + name | setting +---------------------------------+------------ + autovacuum | on + autovacuum_analyze_scale_factor | 0.1 + autovacuum_analyze_threshold | 50 + autovacuum_freeze_max_age | 4000000000 + autovacuum_io_limits | -1 + autovacuum_max_workers | 3 + autovacuum_mode | mix + autovacuum_naptime | 600 + autovacuum_vacuum_cost_delay | 20 + autovacuum_vacuum_cost_limit | 20 + autovacuum_vacuum_scale_factor | 0.001 + autovacuum_vacuum_threshold | 50 + enable_debug_vacuum | on + log_autovacuum_min_duration | 1 + vacuum_cost_delay | 0 + vacuum_cost_limit | 200 + vacuum_cost_page_dirty | 20 + vacuum_cost_page_hit | 1 + vacuum_cost_page_miss | 10 + vacuum_defer_cleanup_age | 0 + vacuum_freeze_min_age | 80 + vacuum_freeze_table_age | 100 + vacuum_gtt_defer_check_age | 10000 +(23 rows) + +``` + +通过参数的名字,在网上搜索一通,对参数有大概的了解。但是还是以下疑惑: + + 1.vacuum_freeze_min_age,vacuum_free_table_age,autovacuum_freeze_max_age 这三个参数,都跟freeze有关,但一个表到底什么时候做freeze/vacuum,最终是由哪个参数决定的?为什么需要这么多类似的参数?(这个问题提出来了,但写完这篇文章后,但还是没有解析到freeze这一步,后续继续解析)。 + + 2. autovacuum_vacuum_threshold ,autovacuum_vacuum_scale_factor这两跟vacuum有关,第一问提到的参数也大概跟vacuum有关,这两个参数的准确含义又是什么? + + 3. .autovacuum_vacuum_cost_limit 跟vacuum_cost_limit这两个参数,都跟vacuum cost有关,具体各自限制什么?其他cost相关的参数有类似疑惑。 + + 4. autovacuum_max_workers 这个参数应该跟vacuum 线程有关系,vacuum线程之间是如何协同工作的? + +作者本来的解析路线是,先通过跟踪vacuum 函数,找到哪个表在做vacuum ,然后进一步去找为什么这个表触发了vacuum ,而其他表没有触发vacuum的原因,找到表触发vacuum的原因之后,进一步挖掘vacuum线程是怎么调度这个表来做vacuum的,vacuum线程是如何调度起来的,何时调度起来的。这个解析路径回头看,特别合理跟清晰,但当时处于完全未知的状态时,一顿瞎摸索。 + +但本次总结vacuum ,不打算按照我摸索的路线来,准备从源头开始,从vacuum线程是如何调度的开始: + +opengauss 后台线程中,有个叫AVClauncher常驻线程,该线程负责调度vacuum 线程,在不做autovacuum的时候,是没有vacuum线程在执行的。我们来看看autovacuum线程是如何被调度起来的。线程vacuum调度命令跟堆栈如下: +``` +(gdb) bt +#0 do_start_worker () at autovacuum.cpp:880 +#1 0x000000000173382e in launch_worker (now=769162991223266) at autovacuum.cpp:1096 +#2 0x000000000173273c in AutoVacLauncherMain () at autovacuum.cpp:577 +#3 0x0000000001790494 in GaussDbThreadMain<(knl_thread_role)7> (arg=0x7feb2253e170) at postmaster.cpp:12974 +#4 0x000000000178a6ba in InternalThreadFunc (args=0x7feb2253e170) at postmaster.cpp:13517 +#5 0x000000000232811f in ThreadStarterFunc (arg=0x7feb2253e160) at gs_thread.cpp:382 +#6 0x00007febbe357ea5 in start_thread () from /lib64/libpthread.so.0 +#7 0x00007febbe0808dd in clone () from /lib64/libc.so.6 +``` +我们来解析do_start_worker函数,其中有下面一段: +``` + foreach (cell, dblist) { + avw_dbase* tmp = (avw_dbase*)lfirst(cell); + Dlelem* elem = NULL; + + /* Check to see if this one is need freeze */ + if (TransactionIdPrecedes(tmp->adw_frozenxid, xidForceLimit)) { + if (avdb == NULL || TransactionIdPrecedes(tmp->adw_frozenxid, avdb->adw_frozenxid)) + avdb = tmp; + for_xid_wrap = true; + continue; +``` +上面这段代码的作用为遍历所有的数据库,在frozenxid小于xidForceLimit的情况下,找到frozenxid最小的数据库优先调度,同时,将参数for_xid_wrap设置为true. 单纯从变量名称的字面意思来理解,在这种情况下,做vacuum是为了防止xid回卷。但大部分情况下,这个条件不容易满足,做vacuum还有另外一个目的是为了删除死亡元组,而不仅仅是为了防止xid回卷。 这个xidForceLimit变量的值跟参数autovacuum_freeze_max_age有关系(如下)。当recentXid大于autovacuum_freeze_max_age的时候,取值为t_thrd.autovacuum_cxt.recentXid 减去autovacuum_freeze_max_age;否则就为3. +``` + t_thrd.autovacuum_cxt.recentXid = ReadNewTransactionId(); + if (t_thrd.autovacuum_cxt.recentXid > + FirstNormalTransactionId + (uint64)g_instance.attr.attr_storage.autovacuum_freeze_max_age) + xidForceLimit = t_thrd.autovacuum_cxt.recentXid - + (uint64)g_instance.attr.attr_storage.autovacuum_freeze_max_age; + else + xidForceLimit = FirstNormalTransactionId; +``` + +在没有数据库满足frozenxid小于xidForceLimit的情况下,依然会选择数据库进行调度,原因如下:avdb变量最终总会赋值。 +``` + avw_dbase* tmp = (avw_dbase*)lfirst(cell); + ................... + if (avdb == NULL || tmp->adw_entry->last_autovac_time < avdb->adw_entry->last_autovac_time) + avdb = tmp; +``` + +给avdb赋值后,下面就是具体的调度vaccuum worker线程的代码,通过共享内存AutoVacuumShmem控制运行中的work线程的数量,该数量的大小由参数autovacuum_max_workers控制。如果没有空闲的worker线程,则停止调度。用变量worker->wi_dboid指定此次被调度执行vacuum的数据库的oid. 然后给postmaster线程发送PMSIGNAL_START_AUTOVAC_WORKER信号,postmaster线程收到信号后,就会调起vacuum worker线程。 +``` + worker = t_thrd.autovacuum_cxt.AutoVacuumShmem->av_freeWorkers; + if (worker == NULL) + ereport(FATAL, (errmsg("no free worker found"))); + + t_thrd.autovacuum_cxt.AutoVacuumShmem->av_freeWorkers = (WorkerInfo)worker->wi_links.next; + + worker->wi_dboid = avdb->adw_datid; + worker->wi_proc = NULL; + worker->wi_launchtime = GetCurrentTimestamp(); + + t_thrd.autovacuum_cxt.AutoVacuumShmem->av_startingWorker = worker; + + LWLockRelease(AutovacuumLock); + + SendPostmasterSignal(PMSIGNAL_START_AUTOVAC_WORKER); + + retval = avdb->adw_datid +``` + + 后面就进入autovacuum worker的线程的处理流程。autovacuum worker线程对指定的数据库(由上面的处理流程指定)进行vacuum. 堆栈如下: + + +下面我们来看看do_autovacuum函数是如何选择需要vacuum的表来进行vacuum的。 + +``` + t_thrd.autovacuum_cxt.default_freeze_min_age = u_sess->attr.attr_storage.vacuum_freeze_min_age; + t_thrd.autovacuum_cxt.default_freeze_table_age = u_sess->attr.attr_storage.vacuum_freeze_table_age; +``` + +首先对上面两个变量赋值,看起来跟参数free_min_age,free_table_age有关。对比上面查询pg_settings表出来的结果,赋值还真的是来源参数vacuum_freeze_min_age与vacuum_freeze_table_age + +``` +(gdb) p u_sess->attr.attr_storage.vacuum_freeze_min_age +$1 = 80 +(gdb) p u_sess->attr.attr_storage.vacuum_freeze_table_age +$2 = 100 +``` + +然后开始扫描表pg_class,代码如下,1259为pg_class表的oid. 然后找出需要vacuum的表。 + +``` +#define RelationRelationId 1259 + classRel = heap_open(RelationRelationId, AccessShareLock); + relScan = tableam_scan_begin(classRel, SnapshotNow, 0, NULL); + while ((tuple = (HeapTuple) tableam_scan_getnexttuple(relScan, ForwardScanDirection)) != NULL) { + /*如果表的类型不是RELKIND_RELATION或者RELKIND_MATVIEW,则 + 直接跳过*/ + if (classForm->relkind != RELKIND_RELATION && + classForm->relkind != RELKIND_MATVIEW) + continue; + Oid relid = HeapTupleGetOid(tuple); + /* Fetch reloptions for this table */ + relopts = extract_autovac_opts(tuple, pg_class_desc) + /*检查表类型是否支持vacuum以及analyze*/ + relation_support_autoavac(tuple, &enable_analyze, &enable_vacuum, &is_internal_relation); + /*检查表是否需要vacuum以及analyze*,这个函数决定表是否需要做vacuum*/ + relation_needs_vacanalyze(relid, relopts, rawRelopts, classForm, tuple, tabentry, enable_analyze, enable_vacuum, + false, &dovacuum, &doanalyze, &need_freeze) + + /* relations that need work are added to table_oids */ + if (dovacuum || doanalyze || isUstorePartitionTable) { + vacObj = (vacuum_object*)palloc(sizeof(vacuum_object)); + vacObj->tab_oid = relid; + vacObj->parent_oid = InvalidOid; + vacObj->dovacuum = isUstorePartitionTable ? true : dovacuum; + vacObj->dovacuum_toast = false; + vacObj->doanalyze = doanalyze; + vacObj->need_freeze = isUstorePartitionTable ? false : need_freeze; + vacObj->is_internal_relation = isUstorePartitionTable ? false : is_internal_relation; + vacObj->gpi_vacuumed = false; + vacObj->flags = (isPartitionedRelation(classForm) ? VACFLG_MAIN_PARTITION : VACFLG_SIMPLE_HEAP); + table_oids = lappend(table_oids, vacObj); + } + + + } +``` + +下面我们来重点看一下relation_needs_vacanalyze 这个函数,这个函数决定了表是否需要vacuum 或者analyze . + +``` + /*获取跟vacuum 相关的参数,这些值来自于数据库参数,具体后面再详细解析*/ + determine_vacuum_params(vac_scale_factor, vac_base_thresh, anl_scale_factor, anl_base_thresh, freeze_max_age, + av_enabled, xidForceLimit, multiForceLimit, relopts); + /*获取该表的relforzenxid,用于后面的判断*/ + Datum xid64datum = heap_getattr(tuple, Anum_pg_class_relfrozenxid64, RelationGetDescr(rel), &isNull); + + if (isNull) { + relfrozenxid = classForm->relfrozenxid; + + if (TransactionIdPrecedes(t_thrd.xact_cxt.ShmemVariableCache->nextXid, relfrozenxid) || + !TransactionIdIsNormal(relfrozenxid)) { + relfrozenxid = FirstNormalTransactionId; + } + } else { + relfrozenxid = DatumGetTransactionId(xid64datum); + } + /* 根据relfrozenxid的值,判断是否需要force_vacuum*/ + force_vacuum = (TransactionIdIsNormal(relfrozenxid) && TransactionIdPrecedes(relfrozenxid, xidForceLimit)); +#ifndef ENABLE_MULTIPLE_NODES + if (!force_vacuum) { + Datum mxidDatum = heap_getattr(tuple, Anum_pg_class_relminmxid, RelationGetDescr(rel), &isNull); + MultiXactId relminmxid = isNull ? FirstMultiXactId : DatumGetTransactionId(mxidDatum); + force_vacuum = (MultiXactIdIsValid(relminmxid) && MultiXactIdPrecedes(relminmxid, multiForceLimit)); + } + #endif + *need_freeze = force_vacuum; + /*判断用户是否对该表禁用了vacuum,貌似可以通过参数设置, + 除了force_vacuum场景需要外,禁止对表进行vacuum ?*/ + + /* User disabled it in pg_class.reloptions? (But ignore if at risk) */ + if (!force_vacuum && (!av_enabled || !u_sess->attr.attr_storage.autovacuum_start_daemon)) { + userEnabled = false; + } + + /*下面是根据死亡元组的数量来判断是否需要做vacuum, + 根据变化的元组的数量来决定是否需要做analyze + ,有数据库的四个参数在此段代码被使用 */ + reltuples = classForm->reltuples; + vacthresh = (float4)vac_base_thresh + vac_scale_factor * reltuples; + anlthresh = (float4)anl_base_thresh + anl_scale_factor * reltuples; + + if (tabentry && (tabentry->changes_since_analyze || tabentry->n_dead_tuples)) { + anltuples = tabentry->changes_since_analyze; + vactuples = tabentry->n_dead_tuples; + AUTOVAC_LOG(DEBUG2, "fetch local stat info: vac \"%s\" changes_since_analyze = %ld n_dead_tuples = %ld ", + NameStr(classForm->relname), tabentry->changes_since_analyze, tabentry->n_dead_tuples); + } + + if (avwentry && (avwentry->changes_since_analyze || avwentry->n_dead_tuples)) { + anltuples = avwentry->changes_since_analyze; + vactuples = avwentry->n_dead_tuples; + AUTOVAC_LOG(DEBUG2, "fetch global stat info: vac \"%s\" changes_since_analyze = %ld n_dead_tuples = %ld ", + NameStr(classForm->relname), avwentry->changes_since_analyze, avwentry->n_dead_tuples); + } + + /* Determine if this table needs vacuum. */ + *dovacuum = force_vacuum || delta_vacuum; + *doanalyze = false; + + if (false == *dovacuum && allowVacuum) + *dovacuum = ((float4)vactuples > vacthresh); + /*上面决定是否因为死亡元组数量达到数据库参数设置的阈值 + 而需要做vacuum*/ + + /* Determine if this table needs analyze. */ + if (allowAnalyze) + *doanalyze = ((float4)anltuples > anlthresh); + /*上面决定是否因为变化的元组的数量达到阈值而进行analyze*/ +``` + + 以上逻辑决定了表是否需要做vacuum或者analyze,最后,将需要vacuum的表,放入列表table_oids.然后逐个进行vacuum. 在解析过程中,看到代码中涉及到了相关数据库参数。 + + 通过上面的解析,我们了解到, auto vacuum 是按照库为单位进行调度的,函数do_autovacuum 是对一个库下面的表逐个进行处理,如果该表不需要vacuum 或者analyze的,则跳过。对表进行vacuum 的原因有两种:1.为了避免事务回卷,而进行force_vacuum ,这个触发条件跟参数autovacuum_freeze_max_age 有关系,具体条件见上面的解析。2.另外,就是表的死亡元组达到一定数量,具体的触发条件跟参数autovacuum_vacuum_scale_factor与autovacuum_vacuum_threshold 参数有关系。 同时,需要提一下,就是参数vacuum_freeze_min_age,vacuum_freeze_table_age虽然在do_autovacuum函数中有利用,但上面解析到的代码中,目前还没发现该参数的作用,这应该跟freeze 有关系,目前还没有解析到该步骤。 + + 接着往下看,当对表执行vacuum的时候,当表太大或者需要删除的死元组比较多的时候,可能会消耗的大量数据库资源,为了不影响数据库的稳定性,数据库做了对vacuum 线程消耗资源限制的功能,当vacuum线程消耗资源到一定程度的时候,会进行sleep 操作,sleep的时间由参数autovacuum_vacuum_cost_delay决定,单位是ms. 具体实现资源限制的代码如下: + +``` +/* + * vacuum_delay_point --- check for interrupts and cost-based delay. + * + * This should be called in each major loop of VACUUM processing, + * typically once per page processed. + */ +void vacuum_delay_point(void) +{ + /* Always check for interrupts */ + CHECK_FOR_INTERRUPTS(); + + /* Nap if appropriate */ + if (t_thrd.vacuum_cxt.VacuumCostActive && !InterruptPending && + t_thrd.vacuum_cxt.VacuumCostBalance >= u_sess->attr.attr_storage.VacuumCostLimit) { + int msec; + + msec = u_sess->attr.attr_storage.VacuumCostDelay * t_thrd.vacuum_cxt.VacuumCostBalance / + u_sess->attr.attr_storage.VacuumCostLimit; + if (msec > u_sess->attr.attr_storage.VacuumCostDelay * 4) + msec = u_sess->attr.attr_storage.VacuumCostDelay * 4; + + pg_usleep(msec * 1000L); + + t_thrd.vacuum_cxt.VacuumCostBalance = 0; + + /* update balance values for workers */ + AutoVacuumUpdateDelay(); + + /* Might have gotten an interrupt while sleeping */ + CHECK_FOR_INTERRUPTS(); + } +``` + +当t_thrd.vacuum_cxt.VacuumCostBalance 大于u_sess->attr.attr_storage.VacuumCostLimit时,会进行sleep 操作。VacuumCostLimit 计算方式跟参数autovacuum_vacuum_cost_limit 有关系,但好像不完全等同于该值,大概是因为可以同时运行多个autovacuum 线程的原因,但参数autovacuum_vacuum_cost_limit 应该是限制了总的消耗量,具体代码尚未解析,后面有时间再分享。 \ No newline at end of file diff --git "a/app/zh/blogs/chunyangxu/2024-05-20-\344\273\216\350\277\220\347\273\264\350\247\206\350\247\222\346\235\245\350\247\243\346\236\220vacuum\346\234\272\345\210\266\350\267\237\345\217\202\346\225\260-00.png" "b/app/zh/blogs/chunyangxu/2024-05-20-\344\273\216\350\277\220\347\273\264\350\247\206\350\247\222\346\235\245\350\247\243\346\236\220vacuum\346\234\272\345\210\266\350\267\237\345\217\202\346\225\260-00.png" new file mode 100644 index 0000000000000000000000000000000000000000..6fac0d84ad0c2c73f5d575903c4467b2bdfbf376 Binary files /dev/null and "b/app/zh/blogs/chunyangxu/2024-05-20-\344\273\216\350\277\220\347\273\264\350\247\206\350\247\222\346\235\245\350\247\243\346\236\220vacuum\346\234\272\345\210\266\350\267\237\345\217\202\346\225\260-00.png" differ diff --git "a/app/zh/blogs/chunyangxu/2024-05-23-\344\273\216\350\277\220\347\273\264\350\247\206\350\247\222\346\235\245\350\247\243\346\236\220vacuum\346\234\272\345\210\266\350\267\237\345\217\202\346\225\260(2)-00.png" "b/app/zh/blogs/chunyangxu/2024-05-23-\344\273\216\350\277\220\347\273\264\350\247\206\350\247\222\346\235\245\350\247\243\346\236\220vacuum\346\234\272\345\210\266\350\267\237\345\217\202\346\225\260(2)-00.png" new file mode 100644 index 0000000000000000000000000000000000000000..b34d37e746d5130dcf31217ff80a5d37fd0e5c9a Binary files /dev/null and "b/app/zh/blogs/chunyangxu/2024-05-23-\344\273\216\350\277\220\347\273\264\350\247\206\350\247\222\346\235\245\350\247\243\346\236\220vacuum\346\234\272\345\210\266\350\267\237\345\217\202\346\225\260(2)-00.png" differ diff --git "a/app/zh/blogs/chunyangxu/2024-05-23-\344\273\216\350\277\220\347\273\264\350\247\206\350\247\222\346\235\245\350\247\243\346\236\220vacuum\346\234\272\345\210\266\350\267\237\345\217\202\346\225\260(2)-01.png" "b/app/zh/blogs/chunyangxu/2024-05-23-\344\273\216\350\277\220\347\273\264\350\247\206\350\247\222\346\235\245\350\247\243\346\236\220vacuum\346\234\272\345\210\266\350\267\237\345\217\202\346\225\260(2)-01.png" new file mode 100644 index 0000000000000000000000000000000000000000..d0daf826a4fd7c383713cddfdb61cb555679e28b Binary files /dev/null and "b/app/zh/blogs/chunyangxu/2024-05-23-\344\273\216\350\277\220\347\273\264\350\247\206\350\247\222\346\235\245\350\247\243\346\236\220vacuum\346\234\272\345\210\266\350\267\237\345\217\202\346\225\260(2)-01.png" differ diff --git "a/app/zh/blogs/chunyangxu/2024-05-23-\344\273\216\350\277\220\347\273\264\350\247\206\350\247\222\346\235\245\350\247\243\346\236\220vacuum\346\234\272\345\210\266\350\267\237\347\233\270\345\205\263\345\217\202\346\225\260(2).md" "b/app/zh/blogs/chunyangxu/2024-05-23-\344\273\216\350\277\220\347\273\264\350\247\206\350\247\222\346\235\245\350\247\243\346\236\220vacuum\346\234\272\345\210\266\350\267\237\347\233\270\345\205\263\345\217\202\346\225\260(2).md" new file mode 100644 index 0000000000000000000000000000000000000000..5241e28cf8720a0cc3e4e30846a9590063f09a1b --- /dev/null +++ "b/app/zh/blogs/chunyangxu/2024-05-23-\344\273\216\350\277\220\347\273\264\350\247\206\350\247\222\346\235\245\350\247\243\346\236\220vacuum\346\234\272\345\210\266\350\267\237\347\233\270\345\205\263\345\217\202\346\225\260(2).md" @@ -0,0 +1,202 @@ +--- +title: "从运维视角来解析vacuum机制跟相关参数(2)" +date: '2024-05-21' +category: 'blog' +tags: ['openGauss'] +archives: '2024-05' +author:'xuchunyang' +summary: "从运维视角来解析vacuum机制跟相关参数" +--- + + 上一篇从运维视角解析了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相关的参数,对数据库的性能的稳定性非常重要,有时候,统一的数据库参数可能无法适用所有的表的各自使用场景,为个别表单独设置特定的参数,我想是非常有必要的。 \ No newline at end of file diff --git "a/app/zh/blogs/chunyangxu/2024-06-19-opengauss\345\206\205\345\255\230\345\210\206\351\205\215\350\267\237\350\270\252-01.png" "b/app/zh/blogs/chunyangxu/2024-06-19-opengauss\345\206\205\345\255\230\345\210\206\351\205\215\350\267\237\350\270\252-01.png" new file mode 100644 index 0000000000000000000000000000000000000000..17b6aed518a4960926b32b5c058106aa0c8615de Binary files /dev/null and "b/app/zh/blogs/chunyangxu/2024-06-19-opengauss\345\206\205\345\255\230\345\210\206\351\205\215\350\267\237\350\270\252-01.png" differ diff --git "a/app/zh/blogs/chunyangxu/2024-06-19-opengauss\345\206\205\345\255\230\345\210\206\351\205\215\350\267\237\350\270\252-02.png" "b/app/zh/blogs/chunyangxu/2024-06-19-opengauss\345\206\205\345\255\230\345\210\206\351\205\215\350\267\237\350\270\252-02.png" new file mode 100644 index 0000000000000000000000000000000000000000..e0cab3e8c2ace2d431aa0e6a2c5af08e6bb9b11a Binary files /dev/null and "b/app/zh/blogs/chunyangxu/2024-06-19-opengauss\345\206\205\345\255\230\345\210\206\351\205\215\350\267\237\350\270\252-02.png" differ diff --git "a/app/zh/blogs/chunyangxu/2024-06-19-opengauss\345\206\205\345\255\230\345\210\206\351\205\215\350\267\237\350\270\252.md" "b/app/zh/blogs/chunyangxu/2024-06-19-opengauss\345\206\205\345\255\230\345\210\206\351\205\215\350\267\237\350\270\252.md" new file mode 100644 index 0000000000000000000000000000000000000000..3101b0d7b5fa23f326e1262f13011bd749fe300a --- /dev/null +++ "b/app/zh/blogs/chunyangxu/2024-06-19-opengauss\345\206\205\345\255\230\345\210\206\351\205\215\350\267\237\350\270\252.md" @@ -0,0 +1,81 @@ +--- +title: "opengauss内存分配跟踪" +date: '2024-06-19' +category: 'blog' +tags: ['openGauss'] +archives: '2024-06' +author:'xuchunyang' +summary: "如何使用dbe_perf.track_memory_context以及pv_session_memctx_detail追踪内存分配" +--- + +​ 近日,我们线上系统遇到动态内存高的报警(通过查询视图gs_total_memory_detail 获取的监控数值),经过定位,发现是绑定变量在不应该使用的场景使用了,导致会话线程在缓存执行计划上消耗了大量的内存,也就是CachedPlan 内存上下文占用内存多(通过查询gs_session_memory_detail可以获得某个会话线程各个上下文占用的内存)。虽然该问题已经定位,但还是想对opengauss的内存知识以及问题定位有更多的了解,然后查找一些资料以及学习了一小段代码,在这里做一下笔记。 + + 通过查询下面的视图,获取一个会话的内存消耗情况 +``` +xcytest=# select * from gs_session_memory_detail where sessid like '%140445449905920%' and contextname like 'CachedPlan'; + sessid | sesstype | contextname | level | parent | totalsize | freesize | usedsize +----------------------------+----------+-------------+-------+---------------------------+-----------+----------+---------- + 1716794872.140445449905920 | postgres | CachedPlan | 2 | SessionCacheMemoryContext | 15360 | 5760 | 9600 + 1716794872.140445449905920 | postgres | CachedPlan | 2 | SessionCacheMemoryContext | 15360 | 5760 | 9600 + 1716794872.140445449905920 | postgres | CachedPlan | 2 | SessionCacheMemoryContext | 7168 | 888 | 6280 +``` +​ 除上述说到的两个视图外,还可以查询到更详细的内存分配信息,就是利用opengauss提供的context track 函数。 例如,想知道CachedPlan内存上下文在哪里被消耗的,可以使用select * from dbe_perf.track_memory_context('CachedPlan'); 命令进行查看, 执行完命令后,再查询dbe_perf.track_memory_context_detail 视图,就能看到详细信息,样式如下: +``` +xcytest=# select * from dbe_perf.track_memory_context_detail(); + context_name | file | line | size +--------------+---------------+------+------ + CachedPlan | copyfuncs.cpp | 2351 | 168 + CachedPlan | copyfuncs.cpp | 2262 | 6 + CachedPlan | copyfuncs.cpp | 6382 | 8 + CachedPlan | list.cpp | 104 | 16 + CachedPlan | plancache.cpp | 1301 | 88 + CachedPlan | bitmapset.cpp | 94 | 8 + CachedPlan | copyfuncs.cpp | 6371 | 32 +``` +当不需要track详细信息时,可以使用下面的语句进行关闭,关闭后再查询上述视图,视图将没有数据。 +``` +select * from dbe_perf.track_memory_context(''); +``` +除上述方法可以track内存上下文外,还有pv_session_memctx_detail 函数,它的用法如下: + +打印一个会话的内存使用情况 +``` +xcytest=# select * from pv_session_memctx_detail(140445646583552,''); +-[ RECORD 1 ]------------+-- +pv_session_memctx_detail | t +``` +打印某个会话的某个内存上下文的使用情况 +select pv_session_memctx_detail(140444974577408,'OptimizerTopMemoryContext'); + +执行上述命令后,会在数据库的pg_log目录下生成一个memdump目录,生成如下文件 +``` +[omm@nd1 memdump]$ ls -lrt +total 36 +-rw-------. 1 omm omm 6625 May 27 16:06 140445449905920_1716797214.log +-rw-------. 1 omm omm 7254 May 27 16:07 140445159192320_1716797266.log +-rw-------. 1 omm omm 6256 May 27 16:18 140445646583552_1716797900.log +-rw-------. 1 omm omm 27 May 27 16:36 SessionSelfMemoryContext_140444974577408_1716798975.log +-rw-------. 1 omm omm 225 May 27 16:37 OptimizerTopMemoryContext_140444974577408_1716799037.log +``` +文件内容的样式如下: +``` +[omm@nd1 memdump]$ cat OptimizerTopMemoryContext_140444974577408_1716799191.log +variable.cpp:801, 32, 20 +variable.cpp:459, 32, 24 +variable.cpp:371, 32, 32 +variable.cpp:961, 32, 24 +variable.cpp:667, 32, 20 +variable.cpp:600, 32, 20 +variable.cpp:205, 32, 24 +variable.cpp:865, 32, 24 +variable.cpp:161, 64, 48 +``` +对文件里面的内容的含义非常模糊,没有找到对其解析的相关资料,于是自行解析。通过从函数pv_session_memctx_detail入手,找到函数DumpMemoryCtxOnBackend, 该函数会给master线程发送PROCSIG_MEMORYCONTEXT_DUMP 信号,master线程收到信号后,调用DumpMemoryContext函数,然后调用recursiveMemoryContextForDump 函数,最后调用dumpAllocBlock函数,该函数的代码如下: + + + +上述代码,就是通过函数dumpAllocChunk逐个处理该内存上下文的所有的block,如果这个block没有被使用,则跳过。通过上述代码,我们可以更直观地了解到内存上下文的基本结构:它里面可以包含很多block, block里面又包含chunk.我们来看看dumpAllocChunk这个函数。 + + + +查看上面代码,可以知道memdump文件中的内容就是上面的代码输出的,可以知道文件中第一列表示chunk是由哪行代码申请的,第二列就是chunk的大小,第三列就是chunk的实际使用大小。通过阅读上述代码,可以更进一步直观地知道,一个block里面,可以包含很多chunk. \ No newline at end of file