<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
前陣子某個客戶反饋他的RDS PostgreSQL無法寫入,報錯資訊如下:
postgres=# select * from test;
id
----
(0 rows)postgres=# insert into test select 1;
ERROR: database is not accepting commands to avoid wraparound data loss in database "xxxx"
HINT: Stop the postmaster and vacuum that database in single-user mode.
You might also need to commit or roll back old prepared transactions.
隨後RDS工程師介入處理以後,該問題立馬得到了解決。
XID(Transaction ID)是 PostgreSQL 內部的事務編號,每個事務都會分配一個XID,依次遞增。PostgreSQL 資料中每個元組頭部都會儲存著 插入 或者 刪除 這條元組的XID(Transaction ID),然後核心通過這個 XID 構造資料庫的一致性讀。在事務隔離級別是 可重複讀 的情況下,假設如有兩個事務,xid1=200,xid2=201,那麼 xid1 中只能看到 t_xmin <= 200 的元組,看不到 t_xmin > 200 的元組。
typedef uint32 TransactionId; /* 事務號定義,32位元無符號整數 */ typedef struct HeapTupleFields { TransactionId t_xmin; /* 插入該元組的事務號 */ TransactionId t_xmax; /* 刪除或鎖定該元組的事務號 */ /*** 其它屬性省略 ***/ } HeapTupleFields; struct HeapTupleHeaderData { union { HeapTupleFields t_heap; DatumTupleFields t_datum; } t_choice; /*** 其它屬性省略 ***/ };
從上面結構中我們可以看到,XID 是一個32位元無符號整數,也就是 XID 的範圍是 0到2^32-1;那麼超過了 2^32-1的事務怎麼辦呢?其實 XID 是一個環,超過了 2^32-1 之後又會從頭開始分配。通過原始碼也證明了上述結論:
// 無效事務號 #define InvalidTransactionId ((TransactionId) 0) // 引導事務號,在資料庫初始化過程(BKI執行)中使用 #define BootstrapTransactionId ((TransactionId) 1) // 凍結事務號用於表示非常陳舊的元組,它們比所有正常事務號都要早(也就是可見) #define FrozenTransactionId ((TransactionId) 2) // 第一個正常事務號 #define FirstNormalTransactionId ((TransactionId) 3) // 把 FullTransactionId 的低32位元作為無符號整數生成 xid #define XidFromFullTransactionId(x) ((uint32) (x).value) static inline void FullTransactionIdAdvance(FullTransactionId *dest) { dest->value++; while (XidFromFullTransactionId(*dest) < FirstNormalTransactionId) dest->value++; } FullTransactionId GetNewTransactionId(bool isSubXact) { /*** 省略 ***/ full_xid = ShmemVariableCache->nextFullXid; xid = XidFromFullTransactionId(full_xid); /*** 省略 ***/ FullTransactionIdAdvance(&ShmemVariableCache->nextFullXid); /*** 省略 *** return full_xid; } static void AssignTransactionId(TransactionState s) { /*** 省略 ***/ s->fullTransactionId = GetNewTransactionId(isSubXact); if (!isSubXact) XactTopFullTransactionId = s->fullTransactionId; /*** 省略 ***/ } TransactionId GetTopTransactionId(void) { if (!FullTransactionIdIsValid(XactTopFullTransactionId)) AssignTransactionId(&TopTransactionStateData); return XidFromFullTransactionId(XactTopFullTransactionId); }
可以看到,新事務號儲存在共用變數快取中:ShmemVariableCache->nextFullXid,每發行一個事務號後,向上調整它的值,並跳過上述三個特殊值。三個特殊仠分別為0、1和2,作用可以看上面程式碼註釋。
前面說到,XID 是一個環,分配到 2^32-1 之後又從 3 開始,那麼核心是怎麼比較兩個事務的大小的呢?比如 xid 經歷了這樣一個過程 3-> 2^32-1 -> 5,那麼核心怎麼樣知道 5 這個事務在 2^32-1 後面呢?我們再看一下程式碼:
/* * TransactionIdPrecedes --- is id1 logically < id2? */ bool TransactionIdPrecedes(TransactionId id1, TransactionId id2) { /* * If either ID is a permanent XID then we can just do unsigned * comparison. If both are normal, do a modulo-2^32 comparison. */ int32 diff; if (!TransactionIdIsNormal(id1) || !TransactionIdIsNormal(id2)) return (id1 < id2); diff = (int32) (id1 - id2); return (diff < 0); }
可以看到,核心使用了一個比較取巧的方法:(int32) (id1 - id2) < 0,32位元有符號整數的取值範圍是 -2^31 到 231-1,5-(232-1) 得到的值比 2^31-1 大,所以轉換成 int32 會變成負數。但是這裡面有一個問題,「最新事務號-最老事務號」 必須小於 2^31,一旦大於就會出現回捲,導致老事務產生的資料對新事務不可見。
前面講到,「最新事務號-最老事務號」 必須小於 2^31,否則會發生回捲導致老事務產生的資料對新事務不可見,那核心是怎麼避免這個問題的呢?核心是這樣處理的:通過定期把老事務產生的元組的 XID 更新為 FrozenTransactionId,即更新為2,來回收 XID,而 XID 為2 的元組對所有的事務可見,這個過程稱為 XID 凍結,通過這個方式可以回收 XID 來保證 |最新事務號-最老事務號| < 2^31。
除了核心自動凍結回收XID,我們也可以通過命令或者 sql 的方式手動進行 xid 凍結回收
# 檢視每個庫的年齡 SELECT datname, age(datfrozenxid) FROM pg_database; # 1個庫每個表的年齡排序 SELECT c.oid::regclass as table_name, 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 desc; # 檢視1個表的年齡 select oid::regclass,age(relfrozenxid) from pg_class where oid='schema名稱.表名稱'::regclass::oid;
手動凍結回收一張表的元組的 xid 的sql:
vacuum freeze 表名;
手動凍結回收一個庫裡面的所有表 xid 的命令:
vacuumdb -d 庫名 --freeze --jobs=30 -h 連線串 -p 埠號 -U 庫Owner
凍結回收過程是一個重 IO 的操作,這個過程核心會描述表的所有頁面,然後把符合要求的元組的 t_xmin 欄位更新為 2,所以這個過程需要在業務低峰進行,避免影響業務。
與凍結回收相關的核心引數有三個:vacuum_freeze_min_age、vacuum_freeze_table_age和autovacuum_freeze_max_age,由於筆者對於這三個引數理解不深,就不在這裡班門弄斧了,感興趣的同學可以自行找資料瞭解一下。
基於上面的原理分析,我們知道,「最新事務號-最老事務號」 = 2^31-1000000,即當前可用的 xid 僅剩下一百萬的時候,核心就會禁止範例寫入並報錯:database is not accepting commands to avoid wraparound data loss in database, 這個時候必須連到提示中的 "xxxx" 對錶進行 freeze 回收更多的 XID。
void SetTransactionIdLimit(TransactionId oldest_datfrozenxid, Oid oldest_datoid) { TransactionId xidVacLimit; TransactionId xidWarnLimit; TransactionId xidStopLimit; TransactionId xidWrapLimit; TransactionId curXid; Assert(TransactionIdIsNormal(oldest_datfrozenxid)); /* * xidWrapLimit = 最老的事務號 + 0x7FFFFFFF,當前事務號一旦到達xidWrapLimit將發生回捲 */ xidWrapLimit = oldest_datfrozenxid + (MaxTransactionId >> 1); if (xidWrapLimit < FirstNormalTransactionId) xidWrapLimit += FirstNormalTransactionId; /* * 一旦當前事務號到達xidStopLimit,範例將不可寫入,保留 1000000 的xid用於vacuum * 每 vacuum 一張表需要佔用一個xid */ xidStopLimit = xidWrapLimit - 1000000; if (xidStopLimit < FirstNormalTransactionId) xidStopLimit -= FirstNormalTransactionId; /* * 一旦當前事務號到達xidWarnLimit,將不停地收到 * WARNING: database "xxxx" must be vacuumed within 2740112 transactions */ xidWarnLimit = xidStopLimit - 10000000; if (xidWarnLimit < FirstNormalTransactionId) xidWarnLimit -= FirstNormalTransactionId; /* * 一旦當前事務號到達xidVacLimit將觸發force autovacuums */ xidVacLimit = oldest_datfrozenxid + autovacuum_freeze_max_age; if (xidVacLimit < FirstNormalTransactionId) xidVacLimit += FirstNormalTransactionId; /* Grab lock for just long enough to set the new limit values */ LWLockAcquire(XidGenLock, LW_EXCLUSIVE); ShmemVariableCache->oldestXid = oldest_datfrozenxid; ShmemVariableCache->xidVacLimit = xidVacLimit; ShmemVariableCache->xidWarnLimit = xidWarnLimit; ShmemVariableCache->xidStopLimit = xidStopLimit; ShmemVariableCache->xidWrapLimit = xidWrapLimit; ShmemVariableCache->oldestXidDB = oldest_datoid; curXid = XidFromFullTransactionId(ShmemVariableCache->nextFullXid); LWLockRelease(XidGenLock); /* Log the info */ ereport(DEBUG1, (errmsg("transaction ID wrap limit is %u, limited by database with OID %u", xidWrapLimit, oldest_datoid))); /* * 如果 當前事務號>=最老事務號+autovacuum_freeze_max_age * 觸發 autovacuum 對年齡最老的資料庫進行清理,如果有多個資料庫達到要求,按年齡最老的順序依次清理 * 通過設定標誌位標記當前 autovacuum 結束之後再來一次 autovacuum */ if (TransactionIdFollowsOrEquals(curXid, xidVacLimit) && IsUnderPostmaster && !InRecovery) SendPostmasterSignal(PMSIGNAL_START_AUTOVAC_LAUNCHER); /* Give an immediate warning if past the wrap warn point */ if (TransactionIdFollowsOrEquals(curXid, xidWarnLimit) && !InRecovery) { char *oldest_datname; if (IsTransactionState()) oldest_datname = get_database_name(oldest_datoid); else oldest_datname = NULL; if (oldest_datname) ereport(WARNING, (errmsg("database "%s" must be vacuumed within %u transactions", oldest_datname, xidWrapLimit - curXid), errhint("To avoid a database shutdown, execute a database-wide VACUUM in that database.n" "You might also need to commit or roll back old prepared transactions, or drop stale replication slots."))); else ereport(WARNING, (errmsg("database with OID %u must be vacuumed within %u transactions", oldest_datoid, xidWrapLimit - curXid), errhint("To avoid a database shutdown, execute a database-wide VACUUM in that database.n" "You might also need to commit or roll back old prepared transactions, or drop stale replication slots."))); } } bool TransactionIdFollowsOrEquals(TransactionId id1, TransactionId id2) { int32 diff; if (!TransactionIdIsNormal(id1) || !TransactionIdIsNormal(id2)) return (id1 >= id2); diff = (int32) (id1 - id2); return (diff >= 0); } FullTransactionId GetNewTransactionId(bool isSubXact) { /*** 省略 ***/ full_xid = ShmemVariableCache->nextFullXid; xid = XidFromFullTransactionId(full_xid); if (TransactionIdFollowsOrEquals(xid, ShmemVariableCache->xidVacLimit)) { TransactionId xidWarnLimit = ShmemVariableCache->xidWarnLimit; TransactionId xidStopLimit = ShmemVariableCache->xidStopLimit; TransactionId xidWrapLimit = ShmemVariableCache->xidWrapLimit; Oid oldest_datoid = ShmemVariableCache->oldestXidDB; /*** 省略 ***/ if (IsUnderPostmaster && TransactionIdFollowsOrEquals(xid, xidStopLimit)) { char *oldest_datname = get_database_name(oldest_datoid); /* complain even if that DB has disappeared */ if (oldest_datname) ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), errmsg("database is not accepting commands to avoid wraparound data loss in database "%s"", oldest_datname), errhint("Stop the postmaster and vacuum that database in single-user mode.n" "You might also need to commit or roll back old prepared transactions, or drop stale replication slots."))); /*** 省略 ***/ } /*** 省略 ***/ } /*** 省略 ***/ }
# 檢視每個庫的年齡 SELECT datname, age(datfrozenxid) FROM pg_database; # 1個庫每個表的年齡排序 SELECT c.oid::regclass as table_name, 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 desc; # 檢視1個表的年齡 select oid::regclass,age(relfrozenxid) from pg_class where oid='schema名稱.表名稱'::regclass::oid;
單程序 Shell 指令碼
# 對指定資料庫中年齡最大的前 50 張表進行 vacuum freeze for cmd in `psql -U使用者名稱 -p埠號 -h連線串 -d資料庫名 -c "SELECT 'vacuum freeze '||c.oid::regclass||';' as vacuum_cmd FROM pg_class c LEFT JOIN pg_class t ON c.reltoastrelid = t.oid WHERE c.relkind IN ('r', 'm') order by greatest(age(c.relfrozenxid),age(t.relfrozenxid)) desc offset 50 limit 50;" | grep -v vacuum_cmd | grep -v row | grep vacuum`; do psql -U使用者名稱 -p埠號 -h連線串 -d資料庫名 -c "$cmd" done
多程序 Python 指令碼
from multiprocessing import Pool import psycopg2 args = dict(host='pgm-bp10xxxx.pg.rds.aliyuncs.com', port=5432, dbname='資料庫名', user='使用者名稱', password='密碼') def vacuum_handler(sql): sql_str = "SELECT c.oid::regclass as table_name, 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 desc limit 10; " try: conn = psycopg2.connect(**args) cur = conn.cursor() cur.execute(sql) conn.commit() cur = conn.cursor() cur.execute(sql_str) print cur.fetchall() conn.close() except Exception as e: print str(e) # 對指定資料庫中年齡最大的前 1000 張表進行 vacuum freeze,32 個程序並行執行 def multi_vacuum(): pool = Pool(processes=32) sql_str = "SELECT 'vacuum freeze '||c.oid::regclass||';' as vacuum_cmd FROM pg_class c LEFT JOIN pg_class t ON c.reltoastrelid = t.oid WHERE c.relkind IN ('r', 'm') order by greatest(age(c.relfrozenxid),age(t.relfrozenxid)) desc limit 1000;"; try: conn = psycopg2.connect(**args) cur = conn.cursor() cur.execute(sql_str) rows = cur.fetchall() for row in rows: cmd = row['vacuum_cmd'] pool.apply_async(vacuum_handler, (cmd, )) conn.close() pool.close() pool.join() except Exception as e: print str(e) multi_vacuum()
vacuum freeze 會掃描表的所有頁面並更新,是一個重 IO 的操作,操作過程中一定要控制好並行數,否則非常容易把範例打掛。
作者資訊
謝桂起(花名:淵渱) 2020年畢業後加入阿里雲,一直從事RDS PostgreSQL相關工作,善於解決線上各類RDS PostgreSQL運維管控相關問題。
到此這篇關於PostgreSQL事務回捲的文章就介紹到這了,更多相關PostgreSQL事務回捲內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45