PgSQL · 答疑解惑 · 表膨胀

背景

最近处理了几起线上实例表膨胀的问题。表膨胀是指表的数据和索引所占文件系统的空间,在有效数据量并未发生大的变化的情况下,不断增大。PG使用过程中需要特别关注这方面,我们来给大家解析一下表膨胀的原因。

表膨胀的直接触发因素是表上的大量更新,如全表的update操作、大量的insert+delete操作等。而我们知道,PG在更新数据时,是不直接删除老数据的。一个update操作执行后,被更改的数据的旧版本也被保留下来,直到对表做vacuum操作的时候,才考虑回收旧版本。做数据更新时,这些旧版本不及时回收就会造成表膨胀。

线上实例都配置了autovacuum,有了autovacuum,PG会定期自动启动autovacuum worker进程,执行vacuum回收旧版本,防止表膨胀。但在我们看到的几起表膨胀问题里面,autovacuum几乎每分钟运行一次,仍然没有避免表膨胀,这是为什么呢?

表膨胀问题的重现

从问题实例的pg_stat_activity视图里面,可以发现它们有一个特点,就是有长时间未提交或终止的事务。

我们用下面的例子简单模拟一下。先创建一张表,插入一条数据。再建立两个连接,其中一个开启一个事务,执行插入和查询,但不提交;另一个不断执行update操作。

控制台A:

postgres=# begin;
BEGIN
postgres=# insert into test_bloat values(1);
INSERT 0 1
postgres=# select * from test_bloat;
 a
---
 1
 1
(2 rows)

postgres=#

控制台B:

postgres=# update test_bloat set a = 1;
UPDATE 1
postgres=# \watch 1
Watch every 1s	Wed Nov 11 17:04:31 2015
....

过不多久,查看表的大小就会发现它不断增大:

postgres=# \dt+ test_bloat
                           List of relations
 Schema |    Name    | Type  |     Owner     |    Size    | Description
--------+------------+-------+---------------+------------+-------------
 public | test_bloat | table | guangzhou.zgz | 8192 bytes |
(1 row)

....

postgres=# \dt+ test_bloat
                         List of relations
 Schema |    Name    | Type  |     Owner     | Size  | Description
--------+------------+-------+---------------+-------+-------------
 public | test_bloat | table | guangzhou.zgz | 40 kB |

查看第一个连接中的事务状态如下:

postgres=# select * from pg_stat_activity;
-[ RECORD 1 ]----+--------------------------------
datid            | 13003
datname          | postgres
pid              | 113306
usesysid         | 10
usename          | guangzhou.zgz
application_name | psql
client_addr      |
client_hostname  |
client_port      | -1
backend_start    | 2015-11-11 16:45:38.452728+08
xact_start       | 2015-11-11 16:47:00.998929+08
query_start      | 2015-11-11 16:47:43.657035+08
state_change     | 2015-11-11 16:47:43.658001+08
waiting          | f
state            | idle in transaction
backend_xid      | 2431157
backend_xmin     |
query            | select * from test_bloat;

可以发现,它有两个特点:

  1. 它的状态为“idle in transaction”,这是因为它未提交,又没有正在进行的查询;
  2. 它的backend_xid不为空,即它有事务号,这是因为它执行了更新操作,插入了一条数据。注意,PG只在发生更新的时候才分配事务ID,没有执行更新操作的事务(即只读事务)是没有backend_xid的。

测试发现,如果及时将此种事务提交,并不会造成表膨胀。可见正是这些事务导致了旧版本无法回收!

为什么旧版本没有被回收

我们从代码里面看看,为什么长事务阻止了版本回收?先看看vacuum回收旧版本的代码。vacuum操作由autovacuum worker执行,其调用顺序如下:

vacuum->vacuum_rel->lazy_vacuum_rel->lazy_scan_heap->heap_page_prune -> heap_prune_chain

vacuum每次处理一个表;每个表同时只有一个进程进行vacuuum。其中,lazy_scan_heap扫描一个表的所有页面,对每个页面调用heap_page_prune进行处理。heap_page_prune调用heap_prune_chain函数来判断,一个旧版本是否可以被回收(即删除)。这里要考虑的一个重要因素是,旧版本是否可能被当前系统里正在进行的事务(活跃事务,即已开始但未提交或终止的事务)需要。heap_prune_chain调用了HeapTupleSatisfiesVacuum来对一个数据记录的旧版本(tuple)做这个判断。HeapTupleSatisfiesVacuum里面最重要的一个判断如下:

if (!TransactionIdPrecedes(HeapTupleHeaderGetRawXmax(tuple), OldestXmin))
	return HEAPTUPLE_RECENTLY_DEAD;

这个判断实际上是计算当前tuple的xmax是否大于或等于OldestXmin。我们知道,xmax是删除这个tuple的事务ID,而OldestXmin由GetOldestXmin函数计算,是所有活跃事务的ID,以及所有事务的xmin 组成的集合中最小的事务ID。所有ID大于这个OldestXmin的事务,都是“新近”开启的事务,其他事务可能需要读取这个旧版本用于查询,所以不能物理删除,则返回HEAPTUPLE_RECENTLY_DEAD,保留此tuple(即不回收)。但如果系统中含有很久之前开启而未提交的事务,并且这个事务由于执行过更新,创建了事务ID(成为“长事务”),那么OldestXmin会非常小,vacuum做上述这个判断时,结果通常为true,即返回HEAPTUPLE_RECENTLY_DEAD,这样heap_prune_chain将会保留此tuple(旧版本),导致回收无法完成,表膨胀由此发生。

需要注意的是,并不是只有更新过数据的事务,长时间不提交会造成表膨胀,只读的事务也是同样的!看下面的case:

控制台A:

postgres=# begin;
BEGIN
postgres=# declare c1 cursor for select * from test_bloat for read only;
DECLARE CURSOR
postgres=#

控制台B:

postgres=# select * from pg_stat_activity;
-[ RECORD 1 ]----+--------------------------------------------------------------
datid            | 13003
datname          | postgres
pid              | 110811
usesysid         | 10
usename          | guangzhou.zgz
application_name | psql
client_addr      |
client_hostname  |
client_port      | -1
backend_start    | 2015-11-12 11:01:00.880549+08
xact_start       | 2015-11-12 11:01:32.843927x+08
query_start      | 2015-11-12 11:01:33.87293+08
state_change     | 2015-11-12 11:01:33.873547+08
waiting          | f
state            | idle in transaction
backend_xid      |
backend_xmin     | 2436509
query            | declare c1 cursor for select * from test_bloat for read only;

我们看到,这个只读事务的xmin是非空的,会被用来做OldestXmin,如果它长时间不完成操作(即cursore不close),就会造成整个数据库的表膨胀!因此,即便是只读事务,也要及时提交;另外,避免在存在大量更新操作的实例上,跑运行时间很长的查询语句。

回收膨胀的空间

如何回收膨胀的空间?长事务结束后,vacuum会回收一部分旧版本。但它回收数据页内的旧版本后,一般情况下并不能把空间还给操作系统。就是说,表所占的空间没有变化。只有一种情况下,即回收的页处于存储数据的文件(一张表对应一个或多个文件)尾部,并且页内没有事务可见的tuple(即整个页都可以删除)时,会做truncate操作,把尾部的这些页统一从文件中删除,文件大小和表所占空间随之减少。

另一种回收膨胀空间的方法是,执行vacuum full <table name> 操作。vacuum full命令实际上重建了整张表和上面的索引。它的缺点是,需要长时间锁住整张表,并耗费大量的IO,对应用影响很大。要减少vacuum full锁住表的时间,可以使用社区提供的pg_repack工具。它的原理是基于原表创建一张新表,同时利用触发器,将原表上当前的增量更新不断记录下来。新表建好后,将所记录的增量更新应用到新表,直到新旧表数据完全一致。最后将新旧表表名互换,删除旧表,即完成了表的空间整理操作,回收了空间。

避免表膨胀的方法

上面看到,表一旦膨胀,空间很难回收回来,所以要尽可能的避免表膨胀。要避免表膨胀,需要注意:

  1. 尽早的、及时的提交事务;
  2. 设计应用时,要使事务尽量短小;
  3. 注意配置与应用规模相适应的硬件资源(IO能力、CPU、内存等),并调教好数据库,使其性能最优,避免有些事务因为资源或性能问题长时间无法完成;
  4. 提交autovacuum,使其能按合理的周期运行。这方面的内容,我们今后专门介绍;
  5. 定期监控系统中是否有长事务,可以使用下面的SQL监控持续时间超过一定时间的事务:
     select * from pg_stat_activity where state<>'idle' and pg_backend_pid() != pid and (backend_xid is not null or backend_xmin is not null ) and extract(epoch from (now() - xact_start))  > <时间阈值,单位秒> ;
    

只要运用好上述方法,表膨胀是可以有效控制的。正常的表膨胀不会超过原来数据量的20%。

时间: 2024-12-21 19:51:44

PgSQL · 答疑解惑 · 表膨胀的相关文章

PgSQL · 答疑解惑 · PostgreSQL 9.6 并行查询实现分析

背景 随着PG9.5 项目的release,属于PG9.6的代码也陆续进入代码主干,其中最让人激动的特性并行查询终于进入了核心代码.pger们对这个新特性期待了太久的时间,代码刚提交我们就迫不及待的拿到,从设计到性能进行一番探究,并通过本文介绍给大家. 并行技术的过去和未来 这是个很困难的工作,要说清楚它需要讲清楚并行技术相关的一些背景. PG 目前的架构是基于多进程的,必要的信息通过共享内存这样的机制来传递. 该架构的好处是: 代码相对简单: 在多CPU环境下多会话任务可以由操作系统来调度:

c++-有没有能为编程新手答疑解惑的平台?

问题描述 有没有能为编程新手答疑解惑的平台? 本人自学C++,有时会碰到一些问题,所以想问有没有这样的平台存在? 解决方案 对的,一般很基础的就去问百度,比如一些语法.函数的用法等.如果是比较复杂的问题,或者直接是一个编程题目的话,就在CSDN论坛发帖或者来问答频道问 解决方案二: win32平台中的程序转换为wince中的一些错误 . 未能为"VCCLCompilerTool"工具生成命令行 解决方案三: 能举例吗?比如什么样的问题呢? 解决方案四: 特别新手的问题百度就可以了,百度

《沟通的技术——让交流、会议与演讲更有效》一第2章 答疑解惑

第2章 答疑解惑 沟通的技术--让交流.会议与演讲更有效爸爸,为什么天空是蓝色的? 当我们还是孩童时,总是会问一些看似天真简单的问题.怀揣着对外部世界的好奇心,我们通过自己年轻懵懂的双眼追寻对事物内涵的理解.为什么天空是蓝色的?为什么蝴蝶有这么美丽的翅膀?为什么饭前要洗手? 这种对事物理解的需求根植于我们的DNA.从20世纪后半期开始,我们将500个人类送出地球,并帮助科学家开启了新纪元-创造出的细菌能像电脑一样被编码.而这一切都源于询问为什么.

中国文化遗产日,非遗传承人展示传统技艺为观众答疑解惑

新京报讯 2005年12月,国务院下发通知将2006年起每年6月的第二个星期六定为"文化遗产日". 今年6月14日是我国的第九个文化遗产日,本次文化遗产日的主题是"非遗保护与城镇化同行",本次活动由"城镇化进程中的非物质文化遗产保护"论坛.文化遗产日主场活动.中国非物质文化遗产出版成果展.非物质文化遗产讲座月.古琴艺术进社区进学校等部分组成. 活动向社会和民众展示非物质文化遗产保护成果,提供非物质文化遗产相关文化产品和服务,弘扬优秀传统文化,传递

院长齐聚,答疑解惑 | 清华-青岛数据科学研究院“院长接待日”成功举办

首先,工业工程系的王明哲同学代表学生大数据研究协会(以下简称:协会),就一年来数据院学生社团工作向院长们做了详细的汇报.协会结合学生大数据知识技能参差不齐.课堂学习效果需要补充.资源交互共享少等现状,制定了以学生为主要服务对象,由学生作为组织者和推进者的"大数据菜鸟进阶之路".希望,依托清华高水平的人才供应,团结一大批热爱数据科学的同学,以逐步完善的制度保证活动质量,以老带新的机制激发长效动力,配合数据院共同完成"顶天.立地.育人"的终极目标. 学生代表汇报工作情况

LILY英语人大校区公开课为家长答疑解惑

2014年12月28日星期日下午2-3点,LILY英语人大校区举行公开课,9位5-7岁的孩子和他们的家长参加了试听,一半以上的孩子在课程结束后报读了明年春季的课程. (图:家长报名参加公开课)LILY英语目前在北京有17个校区,一半以上的校区都会在周末开办公开课,迎接来自北京各个区的家长的试听,解答家长各样的疑惑. (图:公开课现场老师讲课生动活泼)公开课现场,老师会给到场的孩子上15分钟课,父母在教室后排旁听.课程结束后,教学主任根据上课内容给家长解释LILY英语的教学原理,告诉家长按照习得母

数据库内核月报 - 2015 / 11-PgSQL · 答疑解惑 · PostgreSQL 用户组权限管理

背景 RDS上的PG没有开放超级用户,这给很多云上的客户使用PG带来了困难.因此有必要给大家讲讲PG的用户权限管理的一些小知识,它可以很好的帮助用户顺利的从之前的 DB 管理方式过度到云上. PG 的 superuser 拥有几乎全部的数据库权限,甚至可以直接修改系统表,潜在风险相当大: RDS PG 使用 superuser 运维 DB,例如管理用.管理流复制.备份等,这些操作用户是不需要关心的,换句话说它应该完全的交给云服务来处理: 对于用户而言,PG 的普通用户权限是完全够用的.使用普通用

携手共进——《Netty IN ACTION》中文版《Netty实战》答疑解惑

引子 所谓一千个人的眼中,就会有一千个哈姆雷特,每位读者对于一本书中的意境的理解都是不一样的,当然这也包括技术类书籍.可是对于技术书籍来说,最重要的就是准确性,这种准确性是要求100%的,不能说"千人千面".作为书籍的作者和译者,自然在表达的时候可能有所纰漏,不能做到面面俱到,所以也就有了这篇小文,用来收集以及解答译者收到的读者的一些反馈和疑问,希望能够帮助到大家. 共进 书中说的每一个Channel都具有唯一的id是什么意思呢,如果返回相同的hashCode就会报错? 解惑:书中对于

MySQL · 答疑解惑 · MySQL Sort 分页

背景 6.5号,小编在 Aliyun 的论坛中发现一位开发者提的一个问题,说 RDS 发现了一个超级大BUG,吓的小编一身冷汗 = =!! 赶紧来看看,背景是一个RDS用户创建了一张表,在一个都是NULL值的非索引字段上进行了排序并分页,用户发现第二页和第一页的数据有重复,然后以为是NULL值的问题,把这个字段都更新成相同的值,发现问题照旧.详细的信息可以登录阿里云的官方论坛查看. 小编进行了尝试,确实如此,并且5.5的版本和5.6的版本行为不一致,所以,必须要查明原因. 原因调查 在MySQL