一、介绍
如果大家用过ORACLE的话,可能对于如下这个报错不会陌生
A transaction fails with an ORA-01555 error.
ORA-01555: snapshot too old (rollback segment too small)
Cause: rollback records needed by a reader for consistent read are overwritten by other writers
ORA-01555,作为ORACLE运维比较经典的一个错误,他的最直观的解释是“snapshot too old” ,也就是快照过旧,也就是查询需要的前镜像太久,已经无法找到了。可能你进行了一个很久的查询,最后报了ORA-01555: snapshot too old,获取不到最终的结果。PostgreSQL在使用过程中一定条件下也会出现snapshot too old的问题,但是可能不如ORACLE频繁,因为ORACLE出现ORA-01555出现最多的原因大部分可能是SQL语句执行时间太长,或者UNDO表空间过小,或者事务量过大,或者过于频繁的提交,导致执行SQL过程中进行一致性读时,SQL执行后修改的前镜像(即UNDO数据)在UNDO表空间中已经被覆盖,不能构造一致性读块(CR blocks),但是PostgreSQL没有回滚段,所以UNDO表空间过小 或者SQL执行后修改的前镜像(即UNDO数据)在UNDO表空间中已经被覆盖这些原因不会出现 。
ORACLE、MYSQL的innodb引擎与PostgreSQL现阶段,在MVCC的实现是不同的,前两种,采取的是:写新数据时,把旧数据移到一个单独的地方,如回滚段中,其他人读数据时,从回滚段中把旧的数据读出来,也就是使用undo。而PostgreSQL实现MVCC是在写数据时,旧数据不删除,而是把新数据插入。
PostgreSQL的这种基于多个旧值版本的并发控制有缺点也有优点。
优点是:
事务回滚可以立即完成,无论事务进行了多少操作。数据可以进行很多更新,不必像Oracle和MySQL的Innodb引擎那样需要经常保证回滚段不会被用完。也不会像oracle数据库那样经常遇到“ORA-1555”错误。
缺点是:
旧版本数据需要清理。PostgreSQL清理旧版本的命令成为Vacuum。旧版本的数据会导致查询更慢一些,因为旧版本的数据存在于数据文件中,查询时需要扫描更多的数据块。
PG中是通过vacuum回收脏数据,也就是我们常说的dead tuple。但是有时候长事物会持有一些dead tuple很长时间,这样就不能及时做vacuum回收脏数据。这样就使得数据库中的表很容易膨胀,并占用额外的存储空间。也会导致相关SQL的性能下降,所以从9.6引入了old_snapshot_threshold参数 。这个参数设置在使用快照时,一个快照可以被使用而没有发生“snapshot too old” 错误的风险的最小时间。超过此阈值时间的死数据将允许被清除。 这可以有助于阻止长时间使用的快照造成的快照膨胀。为了阻止由于本来对该快照可见的数据被清理导致的不正确结果,当快照比这个阈值更旧并且该快照被用来读取一个该快照建立以来被修改过的页面时,将会产生一个错误。
提到PG因为MCVCC方式不同产生的垃圾数据 ,不得不说基于原生PG研发的的openGauss/MOGDB国产数据库,其在2.1.0版本引入的Ustore存储引擎,是openGauss/MOGDB 内核新增的一种存储模式 。Ustore存储引擎将最新版本的“有效数据”和历史版本的“垃圾数据”分离存储。将最新版本的“有效数据”存储在数据页面上,并单独开辟一段UNDO空间,用于统一管理历史版本的“垃圾数据”,因此数据空间不会由于频繁更新而膨胀,“垃圾数据”集中回收效率更高。
二、snapshot too old测试
如下,是一个PostgreSQL产生snapshot too old报错的例子,我的版本为PG14.1。
1.修改old_snapshot_threshold参数
首先修改old_snapshot_threshold参数,当old_snapshot_threshold默认值为-1,是关闭的。设置该值为具体时间的时候,vacuum 就不会等待长时间的查询结束,会继续对dead tuple进行处理,如果刚好有事物正好在使用这些dead tuple,那么就会报错snapshot too old。
old_snapshot_threshold这个参数是postmaster 数据库服务端参数,需要重启生效
2.重启数据库
3.测试(RR隔离级别)
加入测试数据
postgres=# create table tb_snapshot_old(id int);
CREATE TABLE
postgres=# insert into tb_snapshot_old select generate_series(1,10000);
INSERT 0 10000
postgres=# create index idx_snapshot_old on tb_snapshot_old(id);
CREATE INDEX
需要两个session
session1:
postgres=# begin transaction isolation level repeatable read;
BEGIN
第一个数据块的数据
postgres=*# select ctid,* from tb_snapshot_old where id=1;
ctid | id
-------+----
(0,1) | 1
(1 row)
另一个数据块的内容
postgres=*# select ctid,* from tb_snapshot_old where id=999;
ctid | id
--------+-----
(4,95) | 999
(1 row)
session2:
更改1号数据块的数据
postgres=# update tb_snapshot_old set id=10000 where id=5 returning ctid,*;
ctid | id
---------+-------
(0,227) | 10000
(1 row)
UPDATE 1
等待一分钟之后
session1:
访问没有变化的数据块是正常的,因为走的索引,不会扫描数据块。
postgres=*# select ctid,* from tb_snapshot_old where id=999;
ctid | id
--------+-----
(4,95) | 999
(1 row)
访问进行更改的数据块,报 snapshot too old错误
postgres=*# select ctid,* from tb_snapshot_old where id=4;
2022-01-05 16:51:27.308 CST [81934] ERROR: snapshot too old
2022-01-05 16:51:27.308 CST [81934] STATEMENT: select ctid,* from tb_snapshot_old where id=4;
ERROR: snapshot too old
如上是一个RR隔离级别的例子,一个已申请xid的RC写事务,在QUERY开始时会重新生成快照,所以通常query持有的快照LSN大于或等于访问到的PAGE的LSN,就不会出现ERROR: snapshot too old。但是如果QUERY本身访问时间长,并且访问到了快照创建以后被修改的页,还是会出现这个问题。
4.old_snapshot插件
PostgreSQL14版本中添加了一个插件,可以在old_snapshot_threshold关闭的时候(设置为-1),查看映射的XID
postgres=# create extension old_snapshot;
CREATE EXTENSION
postgres=# select * from pg_old_snapshot_time_mapping();
array_offset | end_timestamp | newest_xmin
--------------+------------------------+-------------
0 | 2022-01-05 17:42:00+08 | 782
1 | 2022-01-05 17:43:00+08 | 782
2 | 2022-01-05 17:44:00+08 | 782
3 | 2022-01-05 17:45:00+08 | 783
4 | 2022-01-05 17:46:00+08 | 783
5 | 2022-01-05 17:47:00+08 | 783
6 | 2022-01-05 17:48:00+08 | 783
7 | 2022-01-05 17:49:00+08 | 783
8 | 2022-01-05 17:50:00+08 | 783
9 | 2022-01-05 17:51:00+08 | 783
10 | 2022-01-05 17:52:00+08 | 783
(11 rows)
或者如下语句获取更详细内容
postgres=# select *, age(newest_xmin), clock_timestamp() from pg_old_snapshot_time_mapping();
array_offset | end_timestamp | newest_xmin | age | clock_timestamp
--------------+------------------------+-------------+-----+-------------------------------
0 | 2022-01-05 17:42:00+08 | 782 | 1 | 2022-01-05 17:51:54.144469+08
1 | 2022-01-05 17:43:00+08 | 782 | 1 | 2022-01-05 17:51:54.144474+08
2 | 2022-01-05 17:44:00+08 | 782 | 1 | 2022-01-05 17:51:54.144475+08
3 | 2022-01-05 17:45:00+08 | 783 | 0 | 2022-01-05 17:51:54.144475+08
4 | 2022-01-05 17:46:00+08 | 783 | 0 | 2022-01-05 17:51:54.144476+08
5 | 2022-01-05 17:47:00+08 | 783 | 0 | 2022-01-05 17:51:54.144476+08
6 | 2022-01-05 17:48:00+08 | 783 | 0 | 2022-01-05 17:51:54.144477+08
7 | 2022-01-05 17:49:00+08 | 783 | 0 | 2022-01-05 17:51:54.144477+08
8 | 2022-01-05 17:50:00+08 | 783 | 0 | 2022-01-05 17:51:54.144478+08
9 | 2022-01-05 17:51:00+08 | 783 | 0 | 2022-01-05 17:51:54.144478+08
10 | 2022-01-05 17:52:00+08 | 783 | 0 | 2022-01-05 17:51:54.144479+08
(11 rows)
三、可能导致ERROR: snapshot too old的原因:
1、SQL的执行时间超过old_snapshot_threshold阈值,并且该SQL读取到了LSN超过快照存储的LSN的数据块时 。2、执行时间按很长的SQL,同时读取的数据块在不断的变化 。3、pg_dump备份数据库时,因为pg_dump使用的是RR隔离级别,snapshot是在事务启动后的第一条SQL创建的,备份时间长的话,就有可能在备份过程中读取到LSN大于快照LSN的数据块,导致snapshot too old报错 。