PostgreSQL的 "OAR-01555"?

T1YSL發表於2022-01-05

一、介紹

如果大家用過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。
1641369095778.png

1.修改old_snapshot_threshold引數

首先修改old_snapshot_threshold引數,當old_snapshot_threshold預設值為-1,是關閉的。設定該值為具體時間的時候,vacuum 就不會等待長時間的查詢結束,會繼續對dead tuple進行處理,如果剛好有事物正好在使用這些dead tuple,那麼就會報錯snapshot too old。
1641369342163.png

old_snapshot_threshold這個引數是postmaster 資料庫服務端引數,需要重啟生效

1641369121721.png

2.重啟資料庫

1641369545260.png

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

1641374985719.png

如上是一個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報錯 。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69990629/viewspace-2850855/,如需轉載,請註明出處,否則將追究法律責任。

相關文章