一、概述
本文基于PostgreSQL14.2版本简单阐述长事务对vacuum操作的影响。在巡检时经常会看到状态处于idle_in_transaction的会话,即事务处于空闲状态(查看时刻啥动作也没发生)且未提交或回滚。在PostgreSQL中,长事务会带来一个令人头疼的问题,它会影响autovacuum后台进程或vacuum操作对死亡元组的及时回收重用,从而导致表膨胀,举一个极端的例子来说:考虑死亡元组都是非页末尾的情况,一个拥有三行记录的表却占用了1GB的磁盘空间,尽管长事务被提交,vacuum操作也正常进行,但不会将表占用的磁盘空间立即释放掉,只是将这些死亡元组占用的空间标记为可重用,该表依旧占用1GB的磁盘空间,如果要释放掉这些空间,需要执行vacuum full对表进行重建,该操作会在表上加Access Exclusive访问排他锁,将阻止其他事务对表进行任何操作,在大表上该操作的代价是极高的,会影响业务系统的正常运转(当然也可以使用扩展pg_squeeze在后台组织表而无须阻塞其他操作)。
当然,并不是所有的长事务都会影响vacuum操作,在实践验证之前,有必要了解一下autovacuum进程和vacuum操作具体会做些什么事。
二、autovacuum守护进程
当启用了autovacuum特性,该守护进程会自动执行vacumm和analyze命令。autovacuum通过统计收集工具检查表是否存在大量元组的插入、更新或删除,也就是说,我们需要开启参数track_counts,否则autovacuum无法正常运转。在默认配置下,autovacuum是启用的且相关参数也被适当设置。
autovacuum由多个进程组成,autovacuum launcher进程负责为所有数据库启动autovacuum工作进程,launcher进程每隔"autovacuum_naptime"秒会尝试在每个数据库中各启动一个工作进程。允许autovacuum_max_workers个进程同时运行,每个工作进程会检查自己数据库中的每一张表,如果需要则执行vacuum或analyze命令。
哪些表会触发autovacuum工作进程执行vacuum操作呢?
1.pg_class.relfrozenxid <= autovacuum_freeze_max_age的表
2.自上一次vacuum操作之后,废弃元组数超过vacuum阈值。阈值计算方式如下:
vaccum threshold=autovacuum_vacuum_threshold+autovacuum_vacuum_scale_factor*pg_class.reltuples
3.自上一次vacuum操作之后,插入的元组数超过定义的插入阈值时。插入阈值计算方式如下:
vacuum insert threshold=autovacuum_vacuum_insert_threshold+ autovacuum_vacuum_insert_scale_factor*pg_class.reltuples
对于analyze,触发analyze的阈值如下:
analyze threshold=autovacuum_analyze_threshold+autovacuum_analyze_scale_factor*number of tuples
其中,number of tuples表示自上一次analyze之后,插入、更新或删除的总元组数。
autovacuum不能访问临时表,autovacuum工作进程不会阻塞其他命令。如果一个进程试图获取一个与autovacuum持有的SHARE UPDATE EXCLUSIVE锁冲突的锁,那么锁获取将中断autovacuum。
三、vacuum作用
vacuum作用如下:
1.回收或重用已更新或已删除元组占用的磁盘空间
2.更新PostgreSQL查询规划器使用的数据统计信息
3.更新可见性映射,这可以加快仅索引扫描(index-only scan)的速度。
4.防止由于事务ID或多事务ID环绕式处理使非常旧的数据丢失。
vacuum会回收重用那些对所有事务不可见的死亡元组。对于第一条,你可能会好奇,vacuum操作居然可以回收死亡元组占用的磁盘空间。是的,在特殊情况下(死亡元组位于页面末尾),vacuum可以回收这些死亡元组占用的空间。例如,
(1)创建一张简单的表然后插入100000行记录
(2)查看表大小
(3)删除id>50000之后的元组
(4)查看表大小
(5)然后执行vacuum,就可以回收释放掉这些死亡元组占用的空间:
postgres=# create table test(id int);
CREATE TABLE
postgres=# insert into test select generate_series(1,100000);
INSERT 0 100000
postgres=# \dt+ test
List of relations
Schema | Name | Type | Owner | Persistence | Access method | Size | Description
--------+------+-------+----------+-------------+---------------+---------+-------------
public | test | table | postgres | permanent | heap | 3568 kB |
(1 row)
postgres=# delete from test where id > 50000;
DELETE 50000
postgres=# \dt+ test
List of relations
Schema | Name | Type | Owner | Persistence | Access method | Size | Description
--------+------+-------+----------+-------------+---------------+---------+-------------
public | test | table | postgres | permanent | heap | 3568 kB |
(1 row)
postgres=# vacuum test;
VACUUM
postgres=# \dt+ test
List of relations
Schema | Name | Type | Owner | Persistence | Access method | Size | Description
--------+------+-------+----------+-------------+---------------+---------+-------------
public | test | table | postgres | permanent | heap | 1808 kB |
(1 row)
四、测试验证
本次测试的思想:构建一种情况,更新某张表数据之后,对该表执行vacuum操作时,数据库中还存在未提交事务,测试验证该事务是否影响vacuum操作。本次测试中,事务隔离级别为默认的读已提交:
postgres=# show transaction_isolation ;
transaction_isolation
-----------------------
read committed
(1 row)
## 默认开启了自动提交的功能
postgres=# \echo :AUTOCOMMIT
on
1.长事务中做变更操作
准备测试表:
postgres=# create table t1 (id int);
CREATE TABLE
postgres=# create table t2 (id int);
CREATE TABLE
postgres=# insert into t1 values(1),(2),(3);
INSERT 0 3
postgres=# insert into t2 values(1),(2),(3);
INSERT 0 3
# 表t1的大小为8k:
postgres=# select pg_size_pretty(pg_table_size('t1'));
pg_size_pretty
----------------
8192 bytes
(1 row)
启动一个会话,开启事务一然后更新表t2:
postgres=# begin;
BEGIN
-- 对表t2进行更新
postgres=*# update t2 set id = 2 where id = 1;
UPDATE 1
另启动一个会话,更新t1中的数据:
# 对下面的语句执行10次
postgres=# update t1 set id = 2;
UPDATE 3
# 查看t1中存在30个死亡元组
postgres=# select * from pg_stat_user_tables where relname = 't1';
-[ RECORD 1 ]-------+-------
relid | 17710
schemaname | public
relname | t1
seq_scan | 10
seq_tup_read | 30
idx_scan |
idx_tup_fetch |
n_tup_ins | 3
n_tup_upd | 30
n_tup_del | 0
n_tup_hot_upd | 30
n_live_tup | 3
n_dead_tup | 30
n_mod_since_analyze | 33
n_ins_since_vacuum | 3
last_vacuum |
last_autovacuum |
last_analyze |
last_autoanalyze |
vacuum_count | 0
autovacuum_count | 0
analyze_count | 0
autoanalyze_count | 0
# 尝试对表执行vacuum
postgres=# vacuum t1;
VACUUM
# 再次查看,发现死亡元组并没有被清理掉
postgres=# select n_dead_tup from pg_stat_user_tables where relname = 't1';
-[ RECORD 1 ]--
n_dead_tup | 30
# 查看t1大小:
postgres=# select pg_size_pretty(pg_table_size('t1'));
-[ RECORD 1 ]--+------
pg_size_pretty | 40 kB
## 表占用的磁盘空间是原来的5倍,这就是所谓的表膨胀。
再次说明,vacuum只会清理回收或重用那些对其他所有事务不可见的死亡元组。本次测试中,为什么vacuum操作不起作用?原因就是要处理的t1表的死亡元组对于事务一可见,事务一出于回滚的目的,需要基于MVCC机制维护一整个数据库的数据版本,此时t1表中未更新的记录对于事务一可见,对t1执行更新操作之后,最老的死亡元组依然对于事务一可见,所以vacuum操作无法回收或重用这些死亡元组。
提交事务一之后,vacuum操作可以起作用:
postgres=*# commit;
COMMIT
## 对表t1执行vacuum操作
postgres=# vacuum t1;
VACUUM
postgres=# select n_dead_tup from pg_stat_user_tables where relname = 't1';
-[ RECORD 1 ]-
n_dead_tup | 0
经测试,在产生死亡元组之前,如果存在更老的事务对数据库进行变更操作(例如:select for update、insert、delete、update、alter table等)且未提交,那么该事务就会影响vacuum操作。所以当观察到处于idle_in_transaction状态的会话时,有必要查看该事务的持续时间,避免不合理或恶意的长事务,导致表膨胀。
针对idle_in_transaction引起的长事务,可以调整配置参数idle_in_transaction_session_timeout来控制事务空闲时间。
2.长事务不做任何操作或做单纯的select查询
在一个会话中开启事务一,不做任何操作:
postgres=# begin;
BEGIN
另起一个会话,更新表t1中的数据,然后执行vacuum:
## 更新t1中所有数据,查看死亡元组有3个
postgres=# update t1 set id = 33;
UPDATE 3
postgres=# select n_dead_tup from pg_stat_user_tables where relname = 't1';
n_dead_tup
------------
3
(1 row)
## 执行vacuum操作,vacuum可以清理死亡元组
postgres=# vacuum t1;
VACUUM
postgres=# select n_dead_tup from pg_stat_user_tables where relname = 't1';
n_dead_tup
------------
0
(1 row)
重复前面的步骤,开启事务一,里面做单纯的select查询操作,然后另起一个会话更新t1表,最后对t1执行vacuum操作,可以正常清理死亡元组,这说明什么?t1的死亡元组对于事务一不可见,该事务在进行更改操作时,数据库会基于MVCC机制为该事务维护一个更改前的数据版本。




