暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

PostgreSQL:简单探讨长事务对vacuum的影响

1535

一、概述

  本文基于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机制为该事务维护一个更改前的数据版本。

最后修改时间:2023-08-11 16:02:39
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论