背景

KingbaseESV8R6支持snapshot too old 那么实际工作中,经常看到表又膨胀了,那么我们讨论一下导致对象膨胀的常见原因有哪些呢?

  1. 未开启autovacuum,对于未开启autovacuum的用户,同时又没有合理的自定义vacuum调度的话,表的垃圾版本没有及时回收,新的数据又不断进来,表膨胀是必然的。(新的数据包括插入和更新,更新产生新版本的记录)
  2. 开启了autovacuum, 但是种种原因导致回收不及时,并且新的数据又不断产生,从而导致表膨胀。

vacuum回收不及时的原因

2.1. IO差

当数据库非常繁忙时,如果IO比较差,会导致回收垃圾变慢,从而导致膨胀。 这种一般出现在数据库中存在非常巨大的表,并且这些表在执行whole table vacuum或当表的年龄大于vacuum_freeze_table_age时会全表扫,因此产生大量IO,这期间很容易导致自身或其他表膨胀。

2.2. autovacuum触发延迟

什么情况会触发autovacuum?

 * A table needs to be vacuumed if the number of dead tuples exceeds a
* threshold. This threshold is calculated as
*
* threshold = vac_base_thresh + vac_scale_factor * reltuples

如果没有设置表级别的autovacuum thresh和factor,那么默认使用参数文件配置的值。如下:

int             autovacuum_vac_thresh;  // 默认50
double autovacuum_vac_scale; // 默认0.2

也就是说dead tuple达到约为表的20%时,才触发autovacuum。 回收又需要一定的时间,所以最终表的膨胀应该是超过20%。

2.3. 所有worker繁忙

某些表产生的垃圾如果超过阈值,但是在此期间没有可用worker可以为它处理垃圾回收。导致可能发生膨胀。 而可fork的worker进程个数是参数autovacuum_max_workers决定的,初始化autovacuum共享内存时已固定了它的最大进程数。 如果数据库的表很多,而且都比较大,那么当需要vacuum的表超过了配置autovacuum_max_workers的数量,某些表就要等待空闲的worker进程。这个阶段就容易出现表的膨胀。

所以如果你的KingbaseES有很多数据库膨胀率高的表,可能需要更多的worker进程来支撑。

worker进程在工作时,每个worker最多会消耗的内存由以下参数决定:

#maintenance_work_mem = 64MB # min 1MB

#autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem 所以worker进程越多,内存需求量也越大。

2.4. 数据库中存在长SQL或带XID的长事务

通过sys_stat_activity.backend_xid和backend_xmin来观察。 backend_xid表示已申请事务号的事务,例如有增删改,DLL等操作的事务,backend_xid从申请事务号开始持续到事务结束。

backend_xmin表示SQL执行时的snapshot版本,例如查询语句,查询游标。backend_xmin从SQL开始持续到SQL结束,如果是游标的话,持续到游标关闭。

--注意,当数据库中存在未结束的SQL语句或者未结束的持有事务ID的事务,在此事务过程中,或在此SQL执行时间范围内产生垃圾的话,这些垃圾无法回收,导致数据库膨胀。

也即是判断当前数据库中backend_xid和backend_xmin最小的值,凡是超过这个最小值的事务产生的垃圾版本都不能回收。

原因见:

/*

  • HeapTupleSatisfiesVacuum

  • Determine the status of tuples for VACUUM purposes. Here, what

  • we mainly want to know is if a tuple is potentially visible to any

  • running transaction. If so, it can't be removed yet by VACUUM.

  • OldestXmin is a cutoff XID (obtained from GetOldestXmin()). Tuples

  • deleted by XIDs >= OldestXmin are deemed "recently dead"; they might

  • still be visible to some open transaction, so we can't remove them,

  • even if we see that the deleting transaction has committed. */ HTSV_Result HeapTupleSatisfiesVacuum(HeapTuple htup, TransactionId OldestXmin, Buffer buffer) { 后面通过测试来展示。

2.5. 开启了autovacuum_vacuum_cost_delay

在开启了autovacuum_vacuum_cost_delay后,会使用基于成本的垃圾回收,这个可以有利于降低VACUUM带来的IO影响,但是对于IO没有问题的系统,就没有必要开启autovacuum_vacuum_cost_delay,这会使垃圾回收的时间变长。

当autovacuum进程达到autovacuum_vacuum_cost_limit后,会延迟autovacuum_vacuum_cost_delay值后继续。

    /*
* Adjust cost limit of each active worker to balance the total of cost
* limit to autovacuum_vacuum_cost_limit.
*/
cost_avail = (double) vac_cost_limit / vac_cost_delay;
dlist_foreach(iter, &AutoVacuumShmem->av_runningWorkers)
{
WorkerInfo worker = dlist_container(WorkerInfoData, wi_links, iter.cur); if (worker->wi_proc != NULL &&
worker->wi_dobalance &&
worker->wi_cost_limit_base > 0 && worker->wi_cost_delay > 0)
{
int limit = (int)
(cost_avail * worker->wi_cost_limit_base / cost_total); /*
* We put a lower bound of 1 on the cost_limit, to avoid division-
* by-zero in the vacuum code. Also, in case of roundoff trouble
* in these calculations, let's be sure we don't ever set
* cost_limit to more than the base value.
*/
worker->wi_cost_limit = Max(Min(limit,
worker->wi_cost_limit_base),
1);
}

限制计算方法由另外几个参数决定:

包括在SHARED BUFFER中命中的块,未命中的块,非脏块的额外成本。

vacuum_cost_page_hit (integer) The estimated cost for vacuuming a buffer found in the shared buffer cache. It represents the cost to lock the buffer pool, lookup the shared hash table and scan the content of the page. The default value is 1.

vacuum_cost_page_miss (integer) The estimated cost for vacuuming a buffer that has to be read from disk. This represents the effort to lock the buffer pool, lookup the shared hash table, read the desired block in from the disk and scan its content. The default value is 10.

vacuum_cost_page_dirty (integer) The estimated cost charged when vacuum modifies a block that was previously clean. It represents the extra I/O required to flush the dirty block out to disk again. The default value is 20. 对于IO没有问题的系统,不建议设置autovacuum_vacuum_cost_limit参数。

2.6. autovacuum launcher process 唤醒时间太长

唤醒时间由参数autovacuum_naptime决定,autovacuum launcher进程负责告诉kingbase需要fork worker进程来进行垃圾回收,但是如果autovacuum launcher进程一直在睡觉的话,有垃圾版本了它还在睡觉,那就自然导致膨胀了。如果希望autovacuum进程更活跃,可以调小autovacuum_naptime,对于IO正常的系统可以这样做。

2.7 批量删除或批量更新

例如对于一个20GB的表,一条SQL或一个事务中删除或更新19GB的数据,这19GB的数据必须在事务结束后才能进行垃圾回收,无形中增加了膨胀的可能。

2.8 大量的非HOT更新,会导致索引膨胀,对于BTREE索引来说,整个索引页没有任何引用才能被回收利用,因此索引比较容易膨胀

测试过程使用如下参数:

autovacuum = on log_autovacuum_min_duration = 0 autovacuum_max_workers = 10 autovacuum_naptime = 1 autovacuum_vacuum_threshold = 5 autovacuum_analyze_threshold = 5 autovacuum_vacuum_scale_factor = 0.002 autovacuum_analyze_scale_factor = 0.001 autovacuum_vacuum_cost_delay = 0 测试数据:

一个例子:

打开一个游标,backend_xmin会持续到事务结束前,在backend_xmin未释放的过程中,产生的垃圾是无法被回收的。

test=# begin;
BEGIN
test=# declare c1 cursor for select 1 from sys_class;
DECLARE CURSOR
test=# select sys_backend_pid(); sys_backend_pid
---------------- 24992 (1 row)
在另一个会话查询 test=# select backend_xid,backend_xmin from sys_stat_activity where pid=24992;
backend_xid | backend_xmin
-------------+--------------
| 1001
(1 row)
从游标获取数据 test=# fetch all from c1; ?column?
---------- ......
关闭游标前,backend_xmin还有值。 test=# select backend_xid,backend_xmin from sys_stat_activity where pid=24992;
backend_xid | backend_xmin
-------------+--------------
| 1001
(1 row) 所以在事务号大于backend_xmin的事务中产生的垃圾无法回收。 test=# create table t (id int);
test=# insert into t values (3);
INSERT 0 1
test=# delete from t;
DELETE 1
test=# vacuum verbose t;
INFO: vacuuming "public.t"
INFO: "t": found 0 removable, 1 nonremovable row versions in 1 out of 1 pages
DETAIL: 1 dead row versions cannot be removed yet, oldest xmin: 1002
There were 0 unused item identifiers.
Skipped 0 pages due to buffer pins, 0 frozen pages.
0 pages are entirely empty.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
VACUUM 接下来关闭游标 test=# close c1;
CLOSE CURSOR
backend_xmin释放了,也就是没有了OLDEST版本的事务。
test=# select backend_xid,backend_xmin from sys_stat_activity where pid=24992;
backend_xid | backend_xmin
-------------+--------------
|
(1 row) 垃圾版本得以回收 test=# vacuum verbose t;
INFO: vacuuming "public.t"
INFO: "t": removed 1 row versions in 1 pages
INFO: "t": found 1 removable, 0 nonremovable row versions in 1 out of 1 pages
DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 1007
There were 0 unused item identifiers.
Skipped 0 pages due to buffer pins, 0 frozen pages.
0 pages are entirely empty.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
INFO: "t": truncated 1 to 0 pages
DETAIL: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
VACUUM 再举一个例子: 长事务查询导致backend_xmin会持续到SQL结束时,在backend_xmin未释放的过程中,产生的垃圾是无法被回收的。 test=# begin;
BEGIN
test=# select pg_sleep(1000); test=# select backend_xid,backend_xmin from pg_stat_activity where pid=24992;
backend_xid | backend_xmin
-------------+--------------
| 1008
(1 row)
持续到语句执行结束 ^CCancel request sent
ERROR: canceling statement due to user request
事务结束后,backend_xmin释放 test=# select backend_xid,backend_xmin from pg_stat_activity where pid=24992;
backend_xid | backend_xmin
-------------+--------------
|
(1 row)
再来看一个例子: repeatable read或serializable隔离级别的事务,backend_xmin会持续到事务结束前,在backend_xmin未释放的过程中,产生的垃圾是无法被回收的。
test=# begin work isolation level repeatable read;
BEGIN
test=# select 1; ?column?
---------- 1 (1 row)
持续到事务结束 test=# select backend_xid,backend_xmin from pg_stat_activity where pid=24992;
backend_xid | backend_xmin
-------------+--------------
| 1012
(1 row) test=# end;
COMMIT test=# select backend_xid,backend_xmin from pg_stat_activity where pid=24992;
backend_xid | backend_xmin
-------------+--------------
|
(1 row)
所以我们监控长事务应该包含backend_xmin和backend_xid。
用如下语句监控:
select datname,usename,query,xact_start,now()-xact_start,state from pg_stat_activity where state<>'idle' and (backend_xid is not null or backend_xmin is not null) order by 4;
select name,statement,prepare_time,now()-prepare_time,parameter_types,from_sql from sys_prepared_statements;

如果表和索引已经膨胀了,无法常规vacuum收缩,除非使用rewrite table(vacuum full, cluster)

再举一个实际可能存在的例子,例如持续的并发批量更新,也可能导致膨胀:

例如有一个表包含100万条记录,分成10个进程,每个进程批量更新其中的10万条,并且持续不断的更新。

为什么说这样操作会引起膨胀呢,因为autovacuum worker process最小粒度是表级别的,同一张表同一时间只有一个进程在回收垃圾。基于这种场景会产生三个问题:

  1. 瞬时产生垃圾的速度可能超过回收的速度。
  2. 产生新TUPLE版本的需求超过FSM的剩余空间。
  3. 重点: 回收过程中(其他进程可能会启动并发的更新,持有事务排他锁)会遇到不可回收的问题,就是前面这个例子提到的问题。

这几种原因会导致扩展新的数据块可能性变大,即膨胀。

测试例子:

test=# truncate tbl;
TRUNCATE TABLE
test=# insert into tbl select generate_series(1,1000000),md5(random()::text),clock_timestamp();
INSERT 0 1000000
test=# \dt+ tbl
List of relations
-[ RECORD 1 ]---------
Schema | public
Name | tbl
Type | table
Owner | system
Size | 73 MB
Description | test=# \di+ tbl_pkey
List of relations
-[ RECORD 1 ]---------
Schema | public
Name | tbl_pkey
Type | index
Owner | system
Table | tbl
Size | 21 MB
Description | $ vi t1.sql
update tbl set info=info,crt_time=clock_timestamp() where id >=1 and id<100000;
$ vi t2.sql
update tbl set info=info,crt_time=clock_timestamp() where id >=100001 and id<200000;
$ vi t3.sql
update tbl set info=info,crt_time=clock_timestamp() where id >=200001 and id<300000;
$ vi t4.sql
update tbl set info=info,crt_time=clock_timestamp() where id >=300001 and id<400000;
$ vi t5.sql
update tbl set info=info,crt_time=clock_timestamp() where id >=400001 and id<500000;
$ vi t6.sql
update tbl set info=info,crt_time=clock_timestamp() where id >=500001 and id<600000;
$ vi t7.sql
update tbl set info=info,crt_time=clock_timestamp() where id >=600001 and id<700000;
$ vi t8.sql
update tbl set info=info,crt_time=clock_timestamp() where id >=700001 and id<800000;
$ vi t9.sql
update tbl set info=info,crt_time=clock_timestamp() where id >=800001 and id<900000;
$ vi t10.sql
update tbl set info=info,crt_time=clock_timestamp() where id >=900001 and id<=1000000; kbbench -M prepared -n -r -f ./t1.sql -c 1 -j 1 -T 50000 &
kbbench -M prepared -n -r -f ./t2.sql -c 1 -j 1 -T 50000 &
kbbench -M prepared -n -r -f ./t3.sql -c 1 -j 1 -T 50000 &
kbbench -M prepared -n -r -f ./t4.sql -c 1 -j 1 -T 50000 &
kbbench -M prepared -n -r -f ./t5.sql -c 1 -j 1 -T 50000 &
kbbench -M prepared -n -r -f ./t6.sql -c 1 -j 1 -T 50000 &
kbbench -M prepared -n -r -f ./t7.sql -c 1 -j 1 -T 50000 &
kbbench -M prepared -n -r -f ./t8.sql -c 1 -j 1 -T 50000 &
kbbench -M prepared -n -r -f ./t9.sql -c 1 -j 1 -T 50000 &
kbbench -M prepared -n -r -f ./t10.sql -c 1 -j 1 -T 50000 &
观察到出现了不可回收的垃圾 tuples: 0 removed, 2049809 remain, 999991 are dead but not yet removable
tuples: 501373 removed, 2176172 remain, 999991 are dead but not yet removable
tuples: 1603158 removed, 2517367 remain, 899562 are dead but not yet removable
tuples: 405093 removed, 2647780 remain, 899992 are dead but not yet removable
tuples: 1100546 removed, 2724724 remain, 899562 are dead but not yet removable
tuples: 528200 removed, 2864735 remain, 1141307 are dead but not yet removable
tuples: 981628 removed, 2757909 remain, 933307 are dead but not yet removable
表已经膨胀 test=# \dt+ tbl
List of relations
-[ RECORD 1 ]---------
Schema | public
Name | tbl
Type | table
Owner | system
Size | 335 MB
Description | test=# \di+ tbl_pkey
List of relations
-[ RECORD 1 ]---------
Schema | public
Name | tbl_pkey
Type | index
Owner | system
Table | tbl
Size | 48 MB
Description |

如果产生新TUPLE版本的需求超过FSM的剩余空间,还会继续膨胀下去。

这个问题的改进方法,将批量更新的粒度降低,即单个事务时间缩短,可以降低事务排他锁持有时间,减少not yet removable的情况,同时事务变小后,单个事务对剩余空间的需求量也变小了,所以不需要扩展数据块,就不会膨胀。

小结

通过上面的分析,我们应该如何减少或避免KingbaseES数据膨胀呢?

  1. 一定要开启autovacuum。
  2. 提高系统的IO能力,用高性能存储,越高越好。
  3. 调整触发阈值,让触发阈值和记录数匹配。调小autovacuum_vacuum_scale_factor和autovacuum_analyze_scale_factor。比如我想在有1万条垃圾记录后就触发垃圾回收,那么对于一个1000万的表来说,我应该把autovacuum_vacuum_scale_factor调到千分之一即0.001,而autovacuum_analyze_scale_factor应该调到0.0005。(注意也不能太低,否则LONG SQL可能引起无法回收的无用功,而系统资源却在真实消耗)
  4. 增加autovacuum_max_workers,同时增加autovacuum_work_mem,同时增加系统内存。如果autovacuum_work_mem关闭,则需要增加maintenance_work_mem。

例如对于有大量表需要频繁更新的数据库集群,可以将autovacuum_max_workers调整为与CPU核数一致,并将autovacuum_work_mem调整为2GB,同时需要确保操作系统预留的内存大于autovacuum_max_workers*autovacuum_work_mem

  1. 应用程序设计时,尽量避免如下:

​ 5.1 LONG SQL(包括查,增,删,改,DDL所有的SQL)

​ 5.2 打开游标后不关闭

​ 5.3 在不必要的场景使用repeatable read或serializable事务隔离级别

​ 5.4 对大的数据库执行sys_dump进行逻辑备份(隐式repeatable read隔离级别的全库备份)

​ 5.5 长时间不提交或回滚申请了事务号的事务(增,删,改,DDL的SQL)

​ 6.对于IO没有问题的系统,关闭autovacuum_vacuum_cost_delay或保持默认。

​ 7.调整autovacuum_naptime参数到最低(但是也要慎重,比如有长事务导致某些垃圾无法回收时,会不断的唤醒VACUUM WORKER去扫描垃圾页,然后发现无法回收,循环往复,浪费IO和CPU。比如本地有LONG SQL或者STANDBY开启了hot_standby_feedback并有LONG SQL时,都是问题。

​ 8.应用程序设计时,避免使用大批量的更新,删除操作,可以切分为多个事务,短小的事务进行。

​ 9.万一真的膨胀了,可以通过table rewrite来回收(如vacuum full, cluster),但是需要持有排他锁。而且会在此期间消耗大量IO,请勿在业务运行期执行。

或使用sys_squeeze插件,该插件在清理表的过程中,不会全程加排他锁,能保证运行期间尽可能不影响对目标表的访问。该插件的实现依赖于逻辑解码,因此使用该插件之前必须保证数据库wal_level等级设置为'logical'。

KingbaseES 垃圾回收原理以及如何预防膨胀更新的更多相关文章

  1. KingbaseESV8R6 垃圾回收原理以及如何预防膨胀

    背景 KingbaseESV8R6支持snapshot too old 那么实际工作中,经常看到表又膨胀了,那么我们讨论一下导致对象膨胀的常见原因有哪些呢? 未开启autovacuum 对于未开启au ...

  2. jvm垃圾回收原理(转)

    原文链接:jvm垃圾回收原理 在jvm中堆空间划分为三个代:年轻代(Young Generation).年老代(Old Generation)和永久代(Permanent Generation).年轻 ...

  3. go GC垃圾回收原理

    目录 1.前言 2. 垃圾回收算法 3. Golang垃圾回收 3.1 垃圾回收原理 3.2 内存标记(Mark) 3.3 三色标记 3.4 Stop The World 4. 垃圾回收优化 4.1 ...

  4. jvm的垃圾回收原理

    什么是垃圾回收? 垃圾回收是Java中自动内存管理的另一种叫法.垃圾回收的目的是为程序保持尽可能多的可用堆(heap). JVM会删除堆上不再需要从堆引用的对象. 用一个例子解释垃圾回收? 比方说,下 ...

  5. JVM垃圾回收原理

    原文地址:http://chenchendefeng.iteye.com/blog/455883 一.相关概念 基本回收算法 1. 引用计数(Reference Counting) 比较古老的回收算法 ...

  6. .NET垃圾回收 – 原理浅析

    在开发.NET程序过程中,由于CLR中的垃圾回收(garbage collection)机制会管理已分配的对象,所以程序员就可以不用关注对象什么时候释放内存空间了.但是,了解垃圾回收机制还是很有必要的 ...

  7. Java垃圾回收原理

    无意中在网络上找到了这篇介绍垃圾回收机制的文章,好文!转一下: 垃圾回收器是如何工作的?我现在就简单的介绍一下 首先要明确几点: Java是在堆上为对象分配空间的 垃圾回收器只跟内存有关,什么IO啊, ...

  8. .net垃圾回收-原理浅析

    本文引自:http://www.cnblogs.com/wilber2013/p/4357910.html 在开发.NET程序过程中,由于CLR中的垃圾回收(garbage collection)机制 ...

  9. KingbaseESV8R6垃圾回收受到参数old_snapshot_threshold的影响

    垃圾回收影响因素 影响垃圾回收的因素有很多,垃圾回收不及时,最直接导致表膨胀,详情查看文档<KingbaseESV8R6 垃圾回收原理以及如何预防膨胀>. vacuum回收垃圾的tuple ...

  10. PHP5底层原理之垃圾回收机制

    概念 垃圾回收机制 是一种内存动态分配的方案,它会自动释放程序不再使用的已分配的内存块. 垃圾回收机制 可以让程序员不必过分关心程序内存分配,从而将更多的精力投入到业务逻辑. 与之相关的一个概念,内存 ...

随机推荐

  1. mysqlGTID主从同步出现1236错误问题

    从主库xtrabackup备份,配置好gtid复制,从主库的从库复制.一直报错误 Last_IO_Error: Got fatal error 1236 from master when readin ...

  2. Linux证书问题:curl#60 - “The certificate issuer‘s certificate has expired

    问题说明 最近在centos7上打算安装php7版本,需要下载一个外网https的yum源,结果报错如下: 执行命令 rpm -Uvh https://mirror.webtatic.com/yum/ ...

  3. Vue+SpringBoot+ElementUI实战学生管理系统-5.用户管理模块

    1.章节介绍 前一篇介绍了项目的API接口设计,这一篇编写用户管理模块,需要的朋友可以拿去自己定制.:) 2.获取源码 源码是捐赠方式获取,详细请QQ联系我 :)! 3.项目截图 列表操作 动态图 4 ...

  4. spring boot2.0集成mybatis-plus实战

    说明: 本例演示spring boot2.0如何集成mybatis-plus 如何使用代码生成器 项目源码: https://gitee.com/indexman/mybatis-plus-demo ...

  5. OCP试题解析之053-16 MEMORY_TARGET

    16.Setting which of the following initialization parameters enables Automatic Memory Management? A. ...

  6. Vue实现简单计算器功能

    知识点: v-model双向绑定 v-on事件绑定 实现效果 源码 <!DOCTYPE html> <html lang="en"> <head> ...

  7. Maven如何打包可执行jar包

    假设我有一个maven项目叫:hello-world 新建一个HelloWorld类: package com.dylan.mvnbook.helloworld; public class Hello ...

  8. 我的小程序之旅七:微信公众号设置IP白名单

    一.为什么要配置IP白名单 此处IP为服务器对公网IP: 在IP白名单内的IP地址作为来源,获取access_token接口才可调用成功. 而想要调用公众号相关API,就必须获取access_toke ...

  9. 项目实战:C#上位机+arduino下位机+控制点亮LED灯

    前言   当前比较流行的arduino开发,联动做一个Demo.   应用构架   上位机:C#上位机通过串口发送接收控制协议,来控制下位机:  下位机:arduino下位机主控,接受上位机串口协议控 ...

  10. 详细的BoltDB学习记录文档

    最近项目中用到了boltdb这个go开发的key/value 数据库,但是之前并有接触过,所以特意去看了官方,也找了些资料,网上找的资料要不就是官方文档的翻译,要不就是简单的介绍一点,都不是很全,所以 ...