暂无图片
暂无图片
3
暂无图片
暂无图片
暂无图片

PG 之 Free Space Map & Visibility Map

原创 大表哥 2022-07-27
1800

image.png

大家好,今天大表哥和大家分享一下 PG的 Free Space Map 和 Visibility Map.

大家从名字上就可以看出这2个概念的含义:

FSM: 每个 heap 表和索引 都会有一个相对应的 FSM 文件来追踪记录一下对象的可用空间。
VM: 每个 heap 表的对象 都会有一个对应的VM文件追踪记录:

VISIBILITYMAP_ALL_VISIBLE: 标记哪些page里面包含的所有元祖对 active transaction 的可见的。
VISIBILITYMAP_ALL_FROZEN: 标记哪些page里面包含的所有元祖都是frozen的状态

我们先简单的创建一个表:我们可以看到 FSM和VM 2个文件会自动生成,

命名规则如下: oid_fsm, oid_vm

dbtest@[local:/tmp]:1992=#32679 create table t_vm_fsm (id int, name varchar(200)); CREATE TABLE dbtest@[local:/tmp]:1992=#32679 select oid,relname from pg_class where relname = 't_vm_fsm'; oid | relname -------+---------- 33240 | t_vm_fsm (1 row) dbtest@[local:/tmp]:1992=#32679 insert into t_vm_fsm values (generate_series(1,10000),'hello world!'); INSERT 0 10000 INFRA [postgres@wqdcsrv3352 32963]# ls -lhtr | grep 33240 -rw------- 1 postgres postgres 24K Jul 26 14:54 33240_fsm -rw------- 1 postgres postgres 512K Jul 26 14:54 33240 -rw------- 1 postgres postgres 8.0K Jul 26 14:54 33240_vm

oid_fsm 和 oid_vm 是2个 二进制的文件, 并不能直接查看里面的内容。 不过官方提供了 2个 extension 来查看 二进制文件的内容

pg_freespacemap 是用来查看 FSM文件中记录的 blkno 块号和 avi free space 的大小关系。

我们来安装一下 pg_freespacemap 这个 extension:

dbtest@[local:/tmp]:1992=#32679 create extension pg_freespacemap; CREATE EXTENSION

里面包含了一个函数 的2个 overload 的方法

dbtest@[local:/tmp]:1992=#32679 \df pg_freespace List of functions Schema | Name | Result data type | Argument data types | Type --------+--------------+------------------+----------------------------------------------------+------ public | pg_freespace | smallint | regclass, bigint | func public | pg_freespace | SETOF record | rel regclass, OUT blkno bigint, OUT avail smallint | func (2 rows)

我们查询一下表 t_vm_fsm的大小: 512KB, 默认的page的 大小是 8K, 所以 会有 512/8 = 64 个page (或者是 block)

dbtest@[local:/tmp]:1992=#32679 select pg_size_pretty(pg_relation_size('t_vm_fsm')); pg_size_pretty ---------------- 512 kB (1 row)

我们可以利用函数 pg_freespace 查看一下 64个 block 的使用情况,我们可以看到 0-62 号 block 中 (注意下标是从0开始的) avail space 是0 , 也就是说没有了可用的空间。 63号 block 是有 2496 byte 的空间的.

dbtest@[local:/tmp]:1992=#32679 SELECT * FROM pg_freespace('t_vm_fsm'); blkno | avail -------+------- 0 | 0 1 | 0 2 | 0 ... ... ... 62 | 0 63 | 2496 (64 rows)

如果要单独查 63号 block 的 AVI 的空间大小,可以 传入2个参数

dbtest@[local:/tmp]:1992=#32679 SELECT * FROM pg_freespace('t_vm_fsm',63); pg_freespace -------------- 2496 (1 row)

如果我们现在要对表,进行一些 delete 操作, 再次查看空间大小:

dbtest@[local:/tmp]:1992=#32679 delete from t_vm_fsm where id < 1000; DELETE 999

由于我们表的基数是 10000 , autovacuum_vacuum_threshold 是 50 , autovacuum_vacuum_scale_factor 是 0.2

所以触发 auto_vacuumn 的阀值是 50 + 10000 * 0.2 = 2050,

999 条数据的删除,达不到阀值,所以不会触发 auto vacuum

我们再次查看 pg_freespace : 并没有什么变化

dbtest@[local:/tmp]:1992=#32679 SELECT * FROM pg_freespace('t_vm_fsm'); blkno | avail -------+------- 0 | 0 1 | 0 2 | 0 ... ... ... 62 | 0 63 | 2496 (64 rows)

我们手动执行一下 vacuum table 的操作:

dbtest@[local:/tmp]:1992=#32679 vacuum t_vm_fsm; VACUUM

再次查看 pg_freespace : 我们可以发现 block 0-6 号 已经释放了可重用的空间

dbtest@[local:/tmp]:1992=#32679 select * from (SELECT * FROM pg_freespace('t_vm_fsm')) fsm where fsm.avail > 0; blkno | avail -------+------- 0 | 8160 1 | 8160 2 | 8160 3 | 8160 4 | 8160 5 | 8160 6 | 2720 63 | 2496 (8 rows)

我们再一次运行一下 vacuum full , 回收一下物理的空间 ,注意这个是锁表的,生产上需要禁用:

dbtest@[local:/tmp]:1992=#48460 vacuum full t_vm_fsm; VACUUM

再次观察: page 已经进行物理上的重组, 之前的 64个 block 变成了 58个 block ,

物理存储从 512 kB 下降到 464kB

dbtest@[local:/tmp]:1992=#48460 select * from (SELECT * FROM pg_freespace('t_vm_fsm')) fsm where fsm.avail > 0; blkno | avail -------+------- (0 rows) dbtest@[local:/tmp]:1992=#48460 SELECT count(1) FROM pg_freespace('t_vm_fsm'); count ------- 58 (1 row) dbtest@[local:/tmp]:1992=#48460 select pg_size_pretty(pg_relation_size('t_vm_fsm')); pg_size_pretty ---------------- 464 kB (1 row)

从代码的角度上来说 FSM 是用数据结构 二叉树来维护的, 对算法感兴趣的同学们可以看看:
src/backend/storage/freespace/README

FSM page structure ------------------ Within each FSM page, we use a binary tree structure where leaf nodes store the amount of free space on heap pages (or lower level FSM pages, see "Higher-level structure" below), with one leaf node per heap page. A non-leaf node stores the max amount of free space on any of its children. For example: 4 4 2 3 4 0 2 <- This level represents heap pages

下面我们接着来看 Visibility Map :

官方文档上的定义是 The visibility map stores two bits per heap page。 为每一个 page 或者 block 存储一个2 bit 的标记

a)第一个 bit 是标记 VISIBILITYMAP_ALL_VISIBLE,如果一个page 中所有的 tuple 都是 all visible 的, 那么进行标记
b)第二个bit 是标记 VISIBILITYMAP_ALL_FROZEN,如果一个page 中所有的 tuple 都是 forzen 的, 那么进行标记

Image.png

Visibility Map 的2大用途是:

1.触发Index only scan 的访问,加快SQL 执行效率
2.加速 vacuum 的执行效率

我们可以通过 extension : pg_visibility 来观测一下:

dbtest@[local:/tmp]:1992=#80334 create extension pg_visibility; CREATE EXTENSION dbtest@[local:/tmp]:1992=#80334 create table t_vm (id int , name varchar(200)); CREATE TABLE dbtest@[local:/tmp]:1992=#80334 insert into t_vm values(generate_series(1,10000),'hello VM !'); INSERT 0 10000 dbtest@[local:/tmp]:1992=#80334 create index concurrently idx_name_vm on t_vm(name); CREATE INDEX

我们可以看到,目前的表没有任何修改,删除等操作,所以在VM中的元祖都是可见的。

dbtest@[local:/tmp]:1992=#80334 select count(0) from pg_visibility('t_vm') ; count ------- 55 (1 row) dbtest@[local:/tmp]:1992=#80334 select count(0) from pg_visibility('t_vm') where all_visible = 'f' ; count ------- 0 (1 row) dbtest@[local:/tmp]:1992=#80334 select all_visible, count(1) from pg_visibility('t_vm') group by all_visible ; all_visible | count -------------+------- t | 55 (1 row)

这个时候我们运行一条SQL 可以出发 index only scan 的执行计划:

几个重要的信息指标:
Index Only Scan using idx_name_vm on t_vm
Heap Fetches: 0

dbtest@[local:/tmp]:1992=#80334 explain(analyze) select name from t_vm where name = 'test'; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------ Index Only Scan using idx_name_vm on t_vm (cost=0.29..4.30 rows=1 width=11) (actual time=0.016..0.017 rows=0 loops=1) Index Cond: (name = 'test'::text) Heap Fetches: 0 Planning Time: 0.470 ms Execution Time: 0.045 ms (5 rows)

下面我们要的更新一些数据, 注意更新的数据不要触发 auto vacuum 的进程, 因为vacuum 的进程结束后, 会更新VM

由于我们表的基数是 10000 , autovacuum_vacuum_threshold 是 50 , autovacuum_vacuum_scale_factor 是 0.2

所以触发 auto_vacuumn 的阀值是 50 + 10000 * 0.2 = 2050,

1000条数据的更新,不会触发 auto vacuum

dbtest@[local:/tmp]:1992=#80334 update t_vm set name = 'test' where id <= 1000; UPDATE 1000

我们可以看到 有 12个 page (block ) 执行完更新的操作后, 标记更新成了 f

dbtest@[local:/tmp]:1992=#80334 select all_visible, count(1) from pg_visibility('t_vm') group by all_visible ; all_visible | count -------------+------- f | 12 t | 48 (2 rows)

我们再次执行SQL: 可以看到 Heap Fetches: 1000 变成了 1000

dbtest@[local:/tmp]:1992=#80334 explain(analyze) select name from t_vm where name = 'test'; QUERY PLAN --------------------------------------------------------------------------------------------------------------------------- Index Only Scan using idx_name_vm on t_vm (cost=0.29..4.30 rows=1 width=11) (actual time=0.030..0.179 rows=1000 loops=1) Index Cond: (name = 'test'::text) Heap Fetches: 1000 Planning Time: 0.065 ms Execution Time: 0.225 ms (5 rows)

我们手动 vacuum 一下表 t_vm :

dbtest@[local:/tmp]:1992=#80334 vacuum t_vm; VACUUM

再次观察 VM 的标志位: 全部标记为 visibility

dbtest@[local:/tmp]:1992=#80334 select all_visible, count(1) from pg_visibility('t_vm') group by all_visible ; all_visible | count -------------+------- t | 60 (1 row)

再次查看SQL的执行计划: Heap Fetches: 0 变成了 0

dbtest@[local:/tmp]:1992=#80334 explain(analyze) select name from t_vm where name = 'test'; QUERY PLAN --------------------------------------------------------------------------------------------------------------------------- Index Only Scan using idx_name_vm on t_vm (cost=0.29..4.30 rows=1 width=11) (actual time=0.020..0.093 rows=1000 loops=1) Index Cond: (name = 'test'::text) Heap Fetches: 0 Planning Time: 0.120 ms Execution Time: 0.147 ms (5 rows)

我们继续看一下 VISIBILITYMAP_ALL_FROZEN 这个标记位:

我们来删除500条记录:

dbtest@[local:/tmp]:1992=#80334 delete from t_vm where id <= 500; DELETE 500

我们可以观察 frozen block 的信息: blkno [0-4] 一共5个块 被标记为了 all_frozen = t

dbtest@[local:/tmp]:1992=#80334 select blkno, all_frozen from pg_visibility('t_vm') where all_frozen = 't'; blkno | all_frozen -------+------------ 0 | t 1 | t 2 | t 3 | t 4 | t (5 rows) dbtest@[local:/tmp]:1992=#80334 select * from pg_visibility_map_summary('t_vm'); all_visible | all_frozen -------------+------------ 57 | 5 (1 row)

我们可以通过 extension pageinspect 查看一下 标记为 forzen 的 blkno [0-4] 里面的内容都是空的

dbtest@[local:/tmp]:1992=#80334 create extension pageinspect; CREATE EXTENSION dbtest@[local:/tmp]:1992=#80334 select * from heap_page_items(get_raw_page('t_vm',0)); lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+-------- 1 | 0 | 0 | 0 | | | | | | | | | | (1 row) dbtest@[local:/tmp]:1992=#80334 select * from heap_page_items(get_raw_page('t_vm',4)); lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+-------- 1 | 0 | 0 | 0 | | | | | | | | | | (1 row)

正常的 block 页面的信息如下 blkno 为5号 的页面信息

dbtest@[local:/tmp]:1992=#80334 select * from heap_page_items(get_raw_page('t_vm',5)) where t_data is not null limit 10; lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data ----+--------+----------+--------+---------+--------+----------+--------+-------------+------------+--------+--------+-------+---------------------------------- 76 | 8152 | 1 | 39 | 1523617 | 0 | 0 | (5,76) | 2 | 2306 | 24 | | | \xe90300001768656c6c6f20564d2021 77 | 8112 | 1 | 39 | 1523617 | 0 | 0 | (5,77) | 2 | 2306 | 24 | | | \xea0300001768656c6c6f20564d2021 78 | 8072 | 1 | 39 | 1523617 | 0 | 0 | (5,78) | 2 | 2306 | 24 | | | \xeb0300001768656c6c6f20564d2021 79 | 8032 | 1 | 39 | 1523617 | 0 | 0 | (5,79) | 2 | 2306 | 24 | | | \xec0300001768656c6c6f20564d2021 80 | 7992 | 1 | 39 | 1523617 | 0 | 0 | (5,80) | 2 | 2306 | 24 | | | \xed0300001768656c6c6f20564d2021 81 | 7952 | 1 | 39 | 1523617 | 0 | 0 | (5,81) | 2 | 2306 | 24 | | | \xee0300001768656c6c6f20564d2021 82 | 7912 | 1 | 39 | 1523617 | 0 | 0 | (5,82) | 2 | 2306 | 24 | | | \xef0300001768656c6c6f20564d2021 83 | 7872 | 1 | 39 | 1523617 | 0 | 0 | (5,83) | 2 | 2306 | 24 | | | \xf00300001768656c6c6f20564d2021 84 | 7832 | 1 | 39 | 1523617 | 0 | 0 | (5,84) | 2 | 2306 | 24 | | | \xf10300001768656c6c6f20564d2021 85 | 7792 | 1 | 39 | 1523617 | 0 | 0 | (5,85) | 2 | 2306 | 24 | | | \xf20300001768656c6c6f20564d2021 (10 rows)

最后我们总结一下:

Free Space Map: 记录追踪每个(blk 或 page)的空闲空间的信息,文件的命名形式 xxx_fsm, 可以通过 extension pg_freespacemap 查看具体的详细信息

Visibility Map: 记录追踪每个(blk 或 page)的 VISIBILITYMAP_ALL_VISIBLE 和 VISIBILITYMAP_ALL_FROZEN 信息。 文件的命名形式 xxx_vm, 可以通过 extension pg_visibility 查看具体的详细信息

Have a fun ! 🙂

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

评论