PostgreSQL码农集散地

抓狂!啊,表和索引不断膨胀确找不到原因!

文中参考文档在github需点击阅读原文打开, 同时推荐2个学习环境: 

1、懒人Docker镜像, 已打包200+插件:《最好的PostgreSQL学习镜像》

2、有web浏览器就能用的云起实验室: 《免费体验PolarDB开源数据库》
3、PolarDB开源数据库内核、最佳实践等学习图谱:  https://www.aliyun.com/database/openpolardb/activity 
关注公众号, 持续发布PostgreSQL、PolarDB、DuckDB等相关文章. 

抓狂! 表和索引不断膨胀确找不到原因!

一位兄弟疯了一样的来找我, 已经到了语无伦次的地步:  
PG的日志中反复打印这条日志:
automatic vacuum of table "spot_wallet.pg_catalog.pg_statistic": index scans: 0
pages: 0 removed, 834 remain, 0 skipped due to pins, 624 skipped frozen
tuples: 0 removed, 5399 remain, 1663 are dead but not yet removable, oldest xmin: 2848389483
15分钟了,一直打印这条, 0.0几秒打印一次. 很多很多.  
之前设置的这个参数autovacuum_vacuum_scale_factor 0.02 能让 事务ID使用率稳定在20%左右, 现在8个小时 过去了,事务 id 使用率已经到了37%,不确定什么时候能下降. 
初步判断: 

大概率会和这几个有关: long query、未结束事务、2pc、inactive slot

如果存在只读实例, 并且开启了hot_standby_feedback, 也可能和只读实例上的 long query 有关.

结果就是“autovacuum launcher”触发频率越高, 垃圾无法被回收, 但是判断有足够多的垃圾存在, 所以autovacuum worker会被不断唤醒, 所以这个信息打印越频繁.  

接下来分析一下根本原因

普通对象膨胀点

用户创建的表、物化视图、索引等。

哪些垃圾不能被回收?

1、当前数据库中最老事务快照之后产生的垃圾记录

2、年龄小于vacuum_defer_cleanup_age设置的垃圾记录

3、备库开启了feedback后,备库返回的最老事务快照(仅指 global xmin)之后产生的垃圾记录。(catalog xmin无影响)

什么时候可能膨胀?

1、standby 开启了 feedback (且standby有慢事务, LONG SQL),

2、vacuum_defer_cleanup_age 设置太大

3、当前数据库中的 : 长事务, 慢SQL, 未结束2pc,

例子

1、创建slot

postgres=# select pg_create_logical_replication_slot('a','test_decoding');  
pg_create_logical_replication_slot
------------------------------------
(a,0/92C9C038)
(1 row)

2、查看slot的位点信息

postgres=# select * from pg_get_replication_slots();  
slot_name | plugin | slot_type | datoid | temporary | active | active_pid | xmin | catalog_xmin | restart_lsn | confirmed_flush_lsn
-----------+---------------+-----------+--------+-----------+--------+------------+------+--------------+-------------+---------------------
a | test_decoding | logical | 13585 | f | f | | | 1982645 | 0/92C9BFE8 | 0/92C9C038
(1 row)

3、查看catalog_xmin对应XID的事务提交时间,需要开启事务时间跟踪track_commit_timestamp

postgres=# select pg_xact_commit_timestamp(xmin),pg_xact_commit_timestamp(catalog_xmin) from pg_get_replication_slots();  
psql: ERROR: could not get commit timestamp data
HINT: Make sure the configuration parameter "track_commit_timestamp" is set.

4、从RESTART_LSN找到对应WAL文件,从文件中也可以查到大概的时间。

postgres=# select pg_walfile_name(restart_lsn) from pg_get_replication_slots();  
pg_walfile_name
--------------------------
000000010000000000000092
(1 row)

postgres=# select * from pg_stat_file('pg_wal/000000010000000000000092');
size | access | modification | change | creation | isdir
----------+------------------------+------------------------+------------------------+----------+-------
16777216 | 2019-06-29 22:56:16+08 | 2019-07-01 09:50:16+08 | 2019-07-01 09:50:16+08 | | f
(1 row)

postgres=# select * from pg_ls_waldir() where name='000000010000000000000092';
name | size | modification
--------------------------+----------+------------------------
000000010000000000000092 | 16777216 | 2019-07-01 09:50:16+08
(1 row)

5、建表

postgres=# create table b(id int);  
CREATE TABLE
postgres=# insert into b values (1);
INSERT 0 1

6、消费SLOT WAL

postgres=# select * from pg_logical_slot_get_changes('a',pg_current_wal_lsn(),1);  
lsn | xid | data
------------+---------+----------------
0/92C9C0C0 | 1982645 | BEGIN 1982645
0/92CA4A40 | 1982645 | COMMIT 1982645
(2 rows)

postgres=# select * from pg_logical_slot_get_changes('a',pg_current_wal_lsn(),1);
lsn | xid | data
------------+---------+---------------------------------------
0/92CA4A78 | 1982646 | BEGIN 1982646
0/92CA4A78 | 1982646 | table public.b: INSERT: id[integer]:1
0/92CA4AE8 | 1982646 | COMMIT 1982646
(3 rows)

7、删除记录

postgres=# delete from b;  
DELETE 1

8、垃圾回收,正常。本地表垃圾不受slot catalog_xmin影响

postgres=# vacuum verbose b;  
psql: INFO: vacuuming "public.b"
psql: INFO: "b": removed 1 row versions in 1 pages
psql: INFO: "b": found 1 removable, 0 nonremovable row versions in 1 out of 1 pages
DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 1982648
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.
psql: INFO: "b": truncated 1 to 0 pages
DETAIL: CPU: user: 0.09 s, system: 0.00 s, elapsed: 0.09 s
VACUUM

9、建表,删表,使得CATALOG发生变化,产生CATALOG垃圾

postgres=# create table c (id int);  
CREATE TABLE
postgres=# drop table c;
DROP TABLE
postgres=# create table c (id int);
CREATE TABLE
postgres=# drop table c;
DROP TABLE

10、垃圾回收catalog,无法回收SLOT后产生的CATALOG垃圾,因为还需要这个CATALOG版本去解析对应WAL的LOGICAL 日志

postgres=# vacuum verbose pg_class;  
psql: INFO: vacuuming "pg_catalog.pg_class"
psql: INFO: "pg_class": found 0 removable, 465 nonremovable row versions in 13 out of 13 pages
DETAIL: 2 dead row versions cannot be removed yet, oldest xmin: 1982646
There were 111 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

catalog 受影响

postgres=# vacuum verbose pg_attribute ;  
psql: INFO: vacuuming "pg_catalog.pg_attribute"
psql: INFO: "pg_attribute": found 0 removable, 293 nonremovable row versions in 6 out of 62 pages
DETAIL: 14 dead row versions cannot be removed yet, oldest xmin: 1982646
There were 55 unused item identifiers.
Skipped 0 pages due to buffer pins, 55 frozen pages.
0 pages are entirely empty.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
VACUUM

11、长事务不影响其他库的垃圾回收

postgres

postgres=# begin;  
BEGIN
postgres=# delete from a;
DELETE 1

db1

db1=# create table b(id int);  
CREATE TABLE
db1=# insert into b values (1);
INSERT 0 1
db1=# delete from b;
DELETE 1
db1=# vacuum verbose b;
psql: INFO: vacuuming "public.b"
psql: INFO: "b": removed 1 row versions in 1 pages
psql: INFO: "b": found 1 removable, 0 nonremovable row versions in 1 out of 1 pages
DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 1982671
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.
psql: INFO: "b": truncated 1 to 0 pages
DETAIL: CPU: user: 0.09 s, system: 0.00 s, elapsed: 0.09 s
VACUUM

小结

1 全局catalog 膨胀点

哪些垃圾不能被回收?

1、年龄小于vacuum_defer_cleanup_age设置的垃圾记录

2、当前实例中最老事务快照之后产生的垃圾记录

3、SLOT catalog_xmin后产生的垃圾记录

4、备库开启了feedback后,备库中最老事务快照(包括catalog_xmin, global xmin)之后产生的垃圾记录

什么时候可能膨胀?

1、vacuum_defer_cleanup_age 设置太大

2、整个实例中的 : 长事务, 慢SQL, 慢2pc,

3、慢/dead slot(catalog_xmin, 影响catalog垃圾回收),

4、standby 开启了 feedback (且standby有慢事务, LONG SQL, 慢/dead slot),

2 库级catalog 膨胀点

哪些垃圾不能被回收?

1、年龄小于vacuum_defer_cleanup_age设置的垃圾记录

2、当前数据库中最老事务快照之后产生的垃圾记录

3、备库开启了feedback后,备库返回的最老事务快照(包括catalog_xmin, global xmin)之后产生的垃圾记录

4、SLOT catalog_xmin后产生的垃圾记录(create table, drop table, pg_class, pg_att等)。影响全局(所有DB)

什么时候可能膨胀?

1、vacuum_defer_cleanup_age 设置太大

2、当前数据库中的 : 长事务, 慢SQL, 慢2pc,

3、standby 开启了 feedback (且standby有慢事务, LONG SQL, 慢/dead slot),

4、慢/dead slot(catalog_xmin, 影响catalog垃圾回收),

普通对象膨胀点

用户创建的表、物化视图、索引等。

哪些垃圾不能被回收?

1、年龄小于vacuum_defer_cleanup_age设置的垃圾记录

2、当前数据库中最老事务快照之后产生的垃圾记录

3、备库开启了feedback后,备库返回的最老事务快照(仅指 global xmin)之后产生的垃圾记录。(catalog xmin无影响)

什么时候可能膨胀?

1、vacuum_defer_cleanup_age 设置太大

2、当前数据库中的 : 长事务, 慢SQL, 慢2pc,

3、standby 开启了 feedback (且standby有慢事务, LONG SQL),

WAL文件 膨胀点

wal是指PG的REDO文件。

哪些WAL不能被回收 或 不能被重复利用?

1、从最后一次已正常结束的检查点(检查点开始时刻, 不是结束时刻)开始,所有的REDO文件都不能被回收

2、归档开启后,所有未归档的REDO。(.ready对应的redo文件)

3、启用SLOT后,还没有被SLOT消费的REDO文件

4、设置wal_keep_segments时,当REDO文件数还没有达到wal_keep_segments个时。

什么时候可能膨胀?

1、archive failed ,归档失败

2、user defined archive BUG,用户开启了归档,但是没有正常的将.ready改成.done,使得WAL堆积

3、wal_keep_segments 设置太大,WAL保留过多

4、max_wal_size设置太大,并且checkpoint_completion_target设置太大,导致检查点跨度很大,保留WAL文件很多

5、slot slow(dead) ,包括(physical | logical replication) , restart_lsn 开始的所有WAL文件都要被保留

欢迎关注我的github (https://github.com/digoal/blog) , 学习数据库不迷路.  

近期正在写公开课材料, 未来将通过视频号推出, 欢迎关注视频号:

Image

文章中的参考文档请点击阅读原文获得.