KingbaseESV8R6 snapshot too old的配置和测试

背景

书接上文,我们很好的理解了xmin和xid的区别。我们继续上文《KingbaseESV8R6不同隔离级下xmin的区别》来讨论 snapshot too old 的功能。

当kingbaseES中有事务长时间持有backend_xmin,就会通过参数old_snapshot_threshold把快照中的lsn和当前事务数据块中的lsn做对比判断快照是否过旧。

它并不关心是否存在backend_xid。

[复制代码](javascript:void(0)😉

test=# show old_snapshot_threshold ;
 old_snapshot_threshold
------------------------
 -1
(1 row)

test=# alter system set old_snapshot_threshold= 1;
ALTER SYSTEM

[复制代码](javascript:void(0)😉

修改后重启数据库生效

第一种情况: 包含xmin,没有申请xid的只读事务

当持有xmin的query执行时间超过old_snapshot_threshold设置的阈值,并且读取到数据块的LSN大于快照存储的LSN时,报snapshot too old错误。

[复制代码](javascript:void(0)😉

test=# create table e1(id int);
CREATE TABLE
test=# insert into e1 select generate_series(1,10000);
INSERT 0 10000
test=# create index idx_e1 on e1(id);
CREATE INDEX


session A:
test=# begin transaction isolation level repeatable read;
BEGIN
1号数据块的数据
test=# select ctid,* from e1 where id=2;
 ctid  | id 
-------+----
 (0,1) |  2
(1 row)
另一个数据块的数据
test=# select ctid,* from e1 where id=800;
  ctid  |  id  
--------+------
 (3,122) | 800
(1 row)


session B:
更新1号数据块的某条记录
test=# update e1 set id=0 where id=2 returning ctid,*;
  ctid   | id 
---------+----
 (44,57) |  0
(1 row)
UPDATE 1


1分钟后
session A:
访问未发生变化的数据块正常(因为id=1000走索引,所以不会扫描变更的数据块,这也是前面测试要建立索引的原因)
test=# select ctid,* from e1 where id=800;
  ctid  |  id  
--------+------
 (3,122) | 800
(1 row)
访问发生变化的数据块, 报错snapshot too old  
test=# select ctid,* from e1 where id=6;
ERROR:  snapshot too old
test=# end;
ROLLBACK

[复制代码](javascript:void(0)😉

第二种情况:已申请xid的写,在隔离级别repeatable read/serializable事务,由于持有了xmin,并且repeatable read隔离级别,事务只要不结束,xmin不会变化。这种情况一样可能出现snapshot too old。

[复制代码](javascript:void(0)😉

session A:
test=# begin transaction isolation level repeatable read;
BEGIN
test=# insert into e1 values (1) returning ctid,*;
  ctid   | id 
---------+----
 (44,60) |  1
(1 row)


session B:
修改10号数据块的记录,导致10号数据块LSN变大
test=# update e1 set id=1 where ctid::text ~ '^\(10,' returning ctid,*;
  ctid   | id
----------+----
 (44,60)  |  1
 (44,61)  |  1
 (44,62)  |  1
 (44,63)  |  1
......
UPDATE 226


1分钟后  
session A:
访问变更的数据块,报错  
test=# select * from e1 where ctid::text ~ '^\(10,';
ERROR:  snapshot too old
test=# end;
ROLLBACK

[复制代码](javascript:void(0)😉

第三种情况:已申请xid的,在隔离级别read committed写事务,由于query开始时会重新生成快照,所以通常query持有的快照lsn大于或等于访问到的PAGE的LSN,则不会出现snapshot too old。因为这种情况不用去读取快照

[复制代码](javascript:void(0)😉

session A:
test=# begin transaction isolation level read committed;
BEGIN
test=# insert into e1 values (1) returning ctid,*;
   ctid   | id 
----------+----
 (46,1) |  1
(1 row)


session B:
修改44号数据块的记录,导致44号数据块LSN变大
test=# update e1 set id=2 where ctid::text ~ '^\(44,' returning ctid,*;
   ctid   | id
----------+----
 (45,60)  |  2
 (45,61)  |  2
 (45,62)  |  2
 (45,63)  |  2
......
UPDATE 225


1分钟后  
session A:
访问变更的数据块,不会报错  
test=# select * from e1 where ctid::text ~ '^\(44,';
 id 
----
  0
  0
  0
  0
  0
......

但是如果QUERY本身访问时间长,并且访问到了快照创建以后被修改的页,还是会报错的。也就是访问到被修改的块的时候发现快照号lsn大于数据块lsn。这时候再去找以前的快照lsn已经发现超过1min过于旧。  
模拟长SQL
session A:
with t as (select pg_sleep(100) ) select * from e1,t;

立即执行如下
session B:
test=# update e1 set id=7 where ctid::text ~ '^\(4,' returning ctid,*;
   ctid   | id
----------+----
 (47,59)  |  7
 (47,60)  |  7
 (47,61)  |  7
........
UPDATE 226
长SQL报错
session A:
ERROR:  snapshot too old

[复制代码](javascript:void(0)😉

总结

哪些情况可能导致snapshot too old

包含了backend_xmin的事务,SQL的执行时间超过old_snapshot_threshold阈值,并且该SQL读取到了LSN超过快照存储的LSN的数据块时。

\1. snapshot too old报错通常出现在非常耗时的SQL,同时读取的数据块在不断的变化。当读取时间在10点,但10:03分另外的事务更改了查询中的某个数据块,这时候,查询进行到10:04分时候发现这个数据块中的lsn大于快照中的lsn,就要去快照中读取过去版本,这是为了保证一致性读,如果查询开始到这个时刻超过snapshot too old就会报snapshot too old报错。

\2. snapshot too old也可能出现在sys_dump备份数据库时,因为sys_dump使用的是repeatable read隔离级别,快照是在事务启动后的第一条SQL创建的,备份时间长的话,极有可能在备份过程中读取到LSN大于快照LSN的数据块,导致snapshot too old报错。

快照与隔离级别

  • 已提交读:在该事务的每条SQL执行之前都会重新获取一次快照
  • 可重复读和可串行化:该事务只在第一条SQL执行之前获取一次快照

img

最后我们比对理解oracle中undo机制和kingbaseES中的snapshot too old。

undo表空间的其中一个功能就是实现一致性读,当我在15:00开始查询,15:00的SCN号记录假设为100。那么在15:00这个时刻100就是最大的SCN,

这里需要引入一个ITL的概念,ITL全称为 Interested Transaction List,是Oracle中数据块的组成部分,用来记录在这个数据块上发生的所有事务,一个ITL可以记录一个事务不论这个事务是否已经提交,一个数据块可以有多个ITL。如果这个事务已经提交了那么这个ITL的位置就可以被反复使用了,因为ITL类似记录,所以,有的时候也叫ITL槽位。ITL槽中会记录对应undo块的地址。可以说上面记录的100SCN号在15:00的时候大于所有的数据块上ITL记录的SCN(多个ITL取最大SCN)

执行查询时,服务器进程扫描这个表中的数据块时,会把每个数据块ITL槽中最大的SCN与100进行比较,如果比100小则说明这个数据块没有被修改服务器进程直接进行数据读取即可。如果数据块ITL槽中的SCN大于100那么说明这个数据块在发起查询后被修改了,需要借助undo去获取15:00那个时刻数据块的数据。

根据上面的例子,我是在15:00开始的查询,而数据是在15:03的时候被修改(这里不用考虑有没有提交,因为ITL只要数据块被修改就会有记录,那么这个查询就会去读undo数据块)。我们假设这个被修改的数据块是n号数据块,修改后n号数据块的ITL中记录的SCN是120,当服务器进程扫描到这个数据块进行SCN比较时发现这个数据块的SCN要大于100,服务器进程就知道了这个数据块在发起查询后被修改了,于是服务器进程到n号数据块的头部找到120对应的ITL槽,然后找到对应undo块的位置。将undo块中所存放的n号块修改前的数据取出再结合n号块里的当前数据行进而构建15:00这个时间点未被修改的数据块,这个被新构建的数据块被称为CR块(Consistant Read)。然后服务器进程扫描这个块,得到15:00一致性的数据,返回正确的数据。这就是一致性读

因为我们的修改操作是delete,那么undo中对应的信息就是insert,insert将被删除的数据插入到CR块中,实现一致性读。undo记录的是buffer_cache中对应修改的前镜像,所以undo记录buffer修改的反向操作。

好了,我们把undo中记录的事务槽中scn号比作snapshot中的记录lsn号。结合案例3,第三种情况最符合oracle中的场景应用,因为我们数据库默认的隔离级别也是read committed。如果QUERY时间很长,也就是访问到被修改的块的时候发现快照号lsn大于数据块lsn。也就是相当于oracle中buffer_cache中数据块的scn大于undo中对应这条记录的scn,就需要读取前镜像,在我们数据库就需要读取snapshot。而snapshot保存多久靠old_snapshot_threshold这个参数设置,在oracle中undo表空间保留策略靠undo_retention参数,默认15分钟。

下文我们讨论垃圾回收受到参数old_snapshot_threshold参数的影响。

posted @ 2022-07-30 11:26  KINGBASE研究院  阅读(74)  评论(0编辑  收藏  举报