暂无图片
暂无图片
5
暂无图片
暂无图片
暂无图片

谈谈 Vacuum freeze 的4种模式

1732

Image.png
大家好,今天和大家聊聊 vacuum freeze的4种模式。

众所周知,由于 txid 是 unsign 32 bit 的长度限制问题, PG 需要定期的冻结一些老的TXID, 从而防止这些老的TXID阻碍 transaction id 的 warp-around.

下面我们看看 PG freeze txid 的 4 种模式:
1.懒惰模式(Lazy mode): vacuum_freeze_min_age
2.饥渴模式(Eager mode): vacuum_freeze_table_age
3.强制模式 (Forced mode): autovacuum_freeze_max_age
4.安全保护模式(Failsafe mode): vacuum_failsafe_age

(上面的中文翻译是我自己起的名字,并不是官方名称, 英文的名字Lazy和Eager 是来自于JP作家的internals那本著作,Forced和Failsafe 来自于俄罗斯的PG14 internals的那本著作)

冻结模式 触发参数 默认数值 表级别参数设置
懒惰模式(Lazy mode) vacuum_freeze_min_age 50000000 autovacuum_freeze_min_age and toast.autovacuum_freeze_min_age
饥渴(侵略)模式(Eager mode) vacuum_freeze_table_age 150000000 autovacuum_freeze_table_age and toast.autovacuum_freeze_table_age
强制模式 (Forced mode) autovacuum_freeze_max_age 200000000 autovacuum_freeze_max_age and toast.autovacuum_freeze_max_age
安全保护模式(Failsafe mode) vacuum_failsafe_age(PG14开始支持) 1600000000 NA

我们可以看到 触发的参数从 5千万->1.5亿->2亿->16亿 逐步递增。
我们先来看看 Lazy mode 的触发条件和限制:

freezeLimit_txid =(OldestXmin−vacuum_freeze_min_age)

这里的OldestXmin 指的是元祖上最老的年龄(当前current_txid- xmin), vacuum_freeze_min_age 是系统参数默认是 50000000

freezeLimit_txid 是表上的元祖的最老的年龄-系统参数vacuum_freeze_min_age 得到的限制冻结的TXID的值。

freezeLimit_txid 这个值作为基准值,对于年龄比这个freezeLimit_txid 大的元祖进行freeze的操作。

这里值得注意的是 Lazy mode并不会对所有的 age > freezeLimit_txid 元祖进行freeze, lazy模式会跳过没有死元祖的page(通过visibility map),
所以 Lazy_mode的freeze 并不彻底,存在 tuple age > freezeLimit_txid 并不会freeze的情况,这可能也是叫懒惰模式的原因吧

我们来借用JP作家的图描述一下:

Image.png

当vacuum或者autovacuum process 触发的时候, 当前表T有3个page, 每个page中有3条记录,假设表的年龄为 50002500, 系统参数vacuum_freeze_min_age 是系统参数默认是 50000000

我们根据公式 50002500 - 50000000 = 2500, 这个2500即为 freeLimit_txid的值.

当进行vacuum的时候,根据VM的位图的标记:
1.会跳过没有死元组的page1,不会进行任何freezed的操作
2.扫描page0的时候,发现元组 tuple1,tuple2,tuple3 的xmin分别是1999,2000,2000, 全部小于 freeLimit_txid, 则3条记录全部标记为 xmin_frozen的状态
3.扫描page2的之后,tuple7和tuple8的xmin为2300,2301 均小于 freeLimit_txid =2500,这2条记录的状态标记为 xmin_frozen的状态。而tuple9的xmin为3000,大于 freeLimit_txid, 保持状态不变,

由于Lazy mode 并不能保证所有达到freezeLimit_txid 的元祖freeze, 才引入了饥渴模式的freeze,这个模式也叫侵略模式aggressive mode

从名字上也可以看出饥渴模式是有侵入性的,会读取扫描目标表相关所有的page, 不再会参考 visibility map. 这里指的是PG9.6之前的版本。
对于9.6之后的版本,会根据VM位图中的 all_frozen 的标记位跳过之前已经完全被freeze的page,从而减小了IO的读取,达到了优化的目的。

我们来看一下触发 Eager mode的条件:

pg_database.datfrozenxid < (OldestXmin − vacuum_freeze_table_age)

这里pg_database.datfrozenxid是数据库最早冻结的xid(也就是这个数据库中年龄最大的表中的xmin)

我们还是借助于JP作家的图来描述一下:

Image.png

当vacuum和autovacuum运行的时候: 当前表T有3个page, 每个page中的tuple分布如上图,假设表T的xmin 为2200 ,当前的txid 是150002500,vacuum_freeze_table_age 系统参数默认为 150000000

那么表T的年龄 150002500 - 2200 = 150000300 > vacuum_freeze_table_age (vacuum_freeze_table_age ) 触发Eager mode freeze:
此时同样需要根据 freezeLimit_txid =(150002500−50000000)=100002500,来作为冻结的阈值 ,即为小于100002500 全部冻结

1.同样根据VM 来判断,page0在VM的位图中标记为 all_frozen为1,直接跳过page0
2.扫描page1, tuple 4, tuple5, tuple6的xmin 全部小于 freezeLimit_txid(100002500),则全部标识为冻结,并且更新VM标记page1的 all frozen为1
3.继续扫描page 2, tuple 9和tuple 10 的 xmin 全部小于 freezeLimit_txid(100002500), 进行freeze操作, 而tuple11的xmin 为100003000 大于freezeLimit_txid(100002500),则保持状态不变。
VM 中page2的 all frozen 则保持为0不变

我们简单动手最一个例子加深对Lazy mode和 Eager mode的理解:

1.创建一张表 t_freeze , fillfactor = 10 保留90%的空间,设置autovacuum_freeze_min_age=200: 下调lazy mode freeze 的门槛到200

postgres=# CREATE TABLE t_freeze( postgres(# id integer, postgres(# name char(300) postgres(# ) postgres-# WITH (fillfactor = 10,autovacuum_freeze_min_age=200); CREATE TABLE

2.手动插入100条数据:

postgres=# INSERT INTO t_freeze(id, name) postgres-# SELECT id, 'JASON'||id FROM generate_series(1,100) id; INSERT 0 100

3.查看表的年龄: 这个时候表的年龄 233 大于我们设置的表级别的lazy mode freeze的参数 autovacuum_freeze_min_age=200

postgres=# SELECT postgres-# c.oid::regclass as table_name, postgres-# pg_size_pretty(pg_total_relation_size(c.oid::regclass)) as table_size, postgres-# least(c.relfrozenxid::text::int,t.relfrozenxid::text::int) as relfrozenxid, postgres-# greatest(age(c.relfrozenxid),age(t.relfrozenxid)) as age postgres-# FROM pg_class c postgres-# LEFT JOIN pg_class t ON c.reltoastrelid = t.oid postgres-# WHERE c.relkind IN ('r', 'm') and c.oid::regclass::text = 't_freeze'; table_name | table_size | relfrozenxid | age ------------+------------+--------------+----- t_freeze | 424 kB | 8554153 | 233 (1 row)

4.这个时候我们手动执行 vacuum table:

postgres=# vacuum t_freeze; VACUUM

理论上这个时候会触发 lazy mode 的free: 但是由于VM 的存在, freeze 跳过了这些 page:

all_visible 都是 T, 即为可见,
all_frozen 都是 F, 全部没有冻结

postgres=# select count(1),all_visible,all_frozen from pg_visibility('t_freeze') group by all_visible,all_frozen ; count | all_visible | all_frozen -------+-------------+------------ 50 | t | f (1 row)

5.我们尝试删除一些记录,造成一些空的page,目的是使这些page在VM中像是为F , 即为不可见,这样LAZY mode才回去冻结这些page

再删除9条记录之后

postgres=# delete from t_freeze where id < 10; DELETE 9 postgres=# select count(1),all_visible,all_frozen from pg_visibility('t_freeze') group by all_visible,all_frozen ; count | all_visible | all_frozen -------+-------------+------------ 5 | f | f 45 | t | f (2 rows)

6.我们再次查询表的年龄,由于上次VM的原因,没有被冻结,表的年龄一直往上涨

postgres=# SELECT postgres-# c.oid::regclass as table_name, postgres-# pg_size_pretty(pg_total_relation_size(c.oid::regclass)) as table_size, postgres-# least(c.relfrozenxid::text::int,t.relfrozenxid::text::int) as relfrozenxid, postgres-# greatest(age(c.relfrozenxid),age(t.relfrozenxid)) as age postgres-# FROM pg_class c postgres-# LEFT JOIN pg_class t ON c.reltoastrelid = t.oid postgres-# WHERE c.relkind IN ('r', 'm') and c.oid::regclass::text = 't_freeze'; table_name | table_size | relfrozenxid | age ------------+------------+--------------+------ t_freeze | 432 kB | 8554326 | 1350 (1 row)

手动触发 vacuum table: 这个时候,我们可以观察到LAZY mode 的冻结方式生效了:

4个page 的 all_frozen 已经标记为 all_frozen =f , 由于vacuum同时也删除了死元祖,所以之前删除涉及到的4个page 的all_visible 也全部修改为t
另外的46个page, 由于VM中标记为可见,所以再次被skip过

postgres=# vacuum t_freeze; VACUUM postgres=# select count(1),all_visible,all_frozen from pg_visibility('t_freeze') group by all_visible,all_frozen ; count | all_visible | all_frozen -------+-------------+------------ 4 | t | t 46 | t | f (2 rows)

接下来,我们再动的模拟一下 Eager模式的冻结: 直接执行命令 vacuum freeze 命令, 目的是把剩余46可page也冻结掉。
这个时候,我们看到50个page全部可见,全部为冻结状态

postgres=# vacuum freeze t_freeze; VACUUM postgres=# select count(1),all_visible,all_frozen from pg_visibility('t_freeze') group by all_visible,all_frozen ; count | all_visible | all_frozen -------+-------------+------------ 50 | t | t (1 row)

我们再次查看表的年龄,已经下降到了86 (之前年龄是1350)

postgres=# SELECT postgres-# c.oid::regclass as table_name, postgres-# pg_size_pretty(pg_total_relation_size(c.oid::regclass)) as table_size, postgres-# least(c.relfrozenxid::text::int,t.relfrozenxid::text::int) as relfrozenxid, postgres-# greatest(age(c.relfrozenxid),age(t.relfrozenxid)) as age postgres-# FROM pg_class c postgres-# LEFT JOIN pg_class t ON c.reltoastrelid = t.oid postgres-# WHERE c.relkind IN ('r', 'm') and c.oid::regclass::text = 't_freeze'; table_name | table_size | relfrozenxid | age ------------+------------+--------------+----- t_freeze | 432 kB | 8556214 | 86 (1 row)

我们接下来看一下,强制模式(Forced mode): 2亿是 2的31次方的1/10的大小.

Forced mode主要发生如下的情况
1.Autovacuum 参数关闭了,或者在表级别进行了关闭(WITH (autovacuum_enabled = off),导致 前面2种模式(Lazy和Eager)没有触发。
2.Template0 模板数据库

如果上述2种freeze模式运行正常的话(这里假设也不存在long time transaction的情况),理论上是不会有database中的表的age 大于 vacuum_freeze_table_age(默认即为150000000)长时间存在的情况,这里有一个例外是 template0这个数据库,

template0是在initdb的时候生产的静态库,并且是不允许登录的,所以这个库并不会触发autovacuum,也就不会触发到2种冻结模式了

在生产的巡检中我发现有一个生产库的template0的age 远远大于 vacuum_freeze_table_age(150000000):

template0中最老的表的年龄19亿多:

postgres=# select * from (SELECT datname, age(datfrozenxid) as age FROM pg_database) t order by age desc; datname | age -------------+----------- template0 | 192862306 postgres | 10543365 template1 | 10543309

Let’s google some from 大佬们;

https://stackoverflow.com/questions/62598131/why-does-the-transaction-age-of-template0-db-increase-in-postgresql

Cybertec的CEO 说 template0不需要定时的做vacuum, 当template0的age超过 autovacuum_freeze_max_age(2亿)的时候,
会强制触发freeze.

Image.png

https://postgrespro.com/list/thread-id/2492704
来自于PG core team 的Tom: 大致的意思是你不需要在 template0上做 freeze的监控。 template0足够小,freeze没有任何开销。

Image.png
这个时候有人会问 template1这个库需要vacuum吗? 我们需要知道 template1和template0的区别是什么?

对于一个全新的数据库来说, template0就是template1的一个静态copy.

唯一区别是 template0是不允许登录的,不允许修改的。template1的目的是为用户提定制化比如字符集,locate排序什么的自定义模板,是可以改变的。

我们可以参考源码中的注释:

Image.png

所以我们也就template1由于是可变的,那么需要vacuum来维护。

From PG core team TOM: https://www.postgresql.org/message-id/20780.1142357270%40sss.pgh.pa.us

Image.png
最后我们来看一下安全保护模式(Failsafe mode: PG14 开始支持):

由于某些原因,造成前3种模式freeze 没有冻结成功,于是数据库开始自我抢救: 这种模式下会忽略参数autovacuum_vacuum_cost_delay 并且会过跳过索引的vacuum.

vacuum_failsafe_age= 16亿已经式接近于 20亿大小了

源码触发的函数:backend/access/heap/vacuumlazy.c, lazy_check_wraparound_failsafe function

忽略参数autovacuum_vacuum_cost_delay:

Image.png

跳过索引:

Image.png

Okay. 我们已经理解了PG的4种冻结模式,那么在我们实际的生产系统中必须要监控一下数据库表的年龄:

SELECT c.oid::regclass as table_name, pg_size_pretty(pg_total_relation_size(c.oid::regclass)) as table_size, least(c.relfrozenxid::text::int,t.relfrozenxid::text::int) as relfrozenxid, greatest(age(c.relfrozenxid),age(t.relfrozenxid)) as age FROM pg_class c LEFT JOIN pg_class t ON c.reltoastrelid = t.oid WHERE c.relkind IN ('r', 'm') order by age ;

我们做一个每日定时监控table age 的report :

Image.png

把将要触发eager mode freeze的表列出来找一个业务低峰的时候手动或者脚本进行

vacuum freeze TABLENAME;

最后我们总结一下4种冻结模式:

1.懒惰模式(Lazy mode): table age > vacuum_freeze_min_age(5千万), 冻结不彻底,不能够冻结VM中all_visibility的page
2.饥渴模式(Eager mode): table age > vacuum_freeze_table_age(1.5亿),作为Lazy mode的补漏方案,会根据VM的all_frozen来优化跳过全部冻结的page
3.强制模式 (Forced mode): table age > autovacuum_freeze_max_age (2亿) , 发生在autovacuum没有开启,也没有手动vacuum的情况下,实例中默认创建的template0就是通过这个模式来冻结的。
二另外2个默认的template1和postgres数据库则需要常规的冻结方式。(Lazy mode和Eager mode)
4.安全保护模式(Failsafe mode): table age > vacuum_failsafe_age (16亿), 这个参数是PG14之后引入的,数据库实例进入自我抢救模式或者说是紧急模式:忽略参数autovacuum_vacuum_cost_delay 并且会过跳过索引的vacuum
16亿这个数字已经非常接近 int 32位的一半:20亿。

5.作为生产环境,DBA有必要做table age的监控和报警,定期去在业务低估去去手动的做一些超级大表的 vacuum freeze的操作,目的是避免大表集中爆发freeze, 造成业务高峰高IO,高WAL log 日志量。

对于一些大量的数据迁移特别是(历史归档数据,或者是不变的静态数据): 建议手动的去 vacuum freeze一下归档数据表,这样会更新VM的all_frozen的page,为未来的冻结操作提供了大大的优化。

如果用Psql 原生的copy命令中可以设置参数 FREEZE,这个参数默认会在数据copy完成后,进行 vacuum freeze table的操作。
如果用Kettle, dataworks 这种基于JAVA JDBC的迁移工具, 那么手动执行 vacuum freeze table的操作。

Have a fun 🙂 !

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

评论