天天看點

Greenplum保證資料隔離的“秘密武器”:快照

作者:阿福ChrisYuan

資料庫系統需要保證ACID特性,其中的I特指隔離性(Isolation),而多版本控制協定(MVCC)和快照(Snapshot)是實作隔離性的重要手段。 多版本控制協定相關内容在之前的相關文章中已經進行了介紹,本文中将會對Greenplum中快照相關的知識進行簡要介紹,歡迎大家留言交流。

本文中涉及到的代碼版本是Greenplum 6X的穩定分支(greenplum-db/gpdb at 6X_STABLE (github.com)),其他分支上(如master,5X等)的代碼邏輯會有所差異,請讀者留意。

01 快照基礎

為了實作多版本控制,Greenplum中的堆資料元組(Heap Tuple)的元組頭部分存儲了這個元組的生命周期(通過事務id辨別),它們分别是:

  • xmin,建立這個元組的事務id(txid)
  • xmax,删除這個元組的事務id

(準确說還有指令号cid和各種标記等等,本文中省略掉了對它們的介紹)

對于增删改操作,對它們的操作如下:

  • insert, set xmin=txid_current,set xmax=0
  • delete, set xmax=txid_current
  • update = delete + insert

而快照如其名字,是在一個特定的時刻對系統目前各個事務的運作狀态的一個拍攝。其格式可以表示為: xmin : xmax : xip_list (,分割),見如下示例:

`demo=# begin;`
`BEGIN`
`demo=# select txid_current();`
` txid_current`
`--------------`
` 3911`
`(1 row)`

`demo=# select txid_current_snapshot();`
` txid_current_snapshot`
`-----------------------`
` 3911:3915:3912,3913`
`(1 row)`

           

一個快照表示了:

  • xmin,最早還在活躍的txid,所有txid < 它的事務都已經結束了(送出或者復原)
  • xmax,下一個要配置設定的txid,所有txid >= 它的事務尚未開始(即它的操作對此快照不可見)
  • xip_list,xmin和xmax之間的活躍txid

是以對于之前例子中的快照 3911:3915:3912,3913,表示了:對于此快照,txid<3911的事務都已經結束,txid>=3915的事務尚未開始,而3912和3913這2個事務尚在運作中。

02 快照相關函數

在Greenplum代碼中使用了SnapshotData結構體來表示:

`typedef struct SnapshotData`
`{`
`SnapshotSatisfiesFunc satisfies;  /* tuple test function */`

`TransactionId xmin;      /* all XID < xmin are visible to me */`
`TransactionId xmax;      /* all XID >= xmax are invisible to me */`

`TransactionId *xip;`
`uint32    xcnt;      /* # of xact ids in xip[] */`

`… // 暫時省略掉其他字段`
`}`

           

除了我們之前介紹的内容之外,還有一個 satisfies 字段,它是用于這個快照的 可見性判斷函數 。 Greenplum中提供了多種不同的可見性判斷函數,以供不同的場景所使用。 它們之間的差別簡單來說在于觀察的視角不同,代碼都位于src/backend/utils/time/tqual.c,常見的可見性函數介紹如下表:

Greenplum保證資料隔離的“秘密武器”:快照

具體每個可見性判斷函數都是由若幹條規則組成的,細節較多,讀者可以參考它們的代碼實作和函數頭上的規則簡化注釋。

可見性判斷函數+快照資料(指xmin/xmax/xip)配合在一起使用,它們像一把”篩子”,對堆元組進行過濾,過濾出來的元組就是對于目前事務可見的元組。例如如下代碼:

`heap_fetch()`
`{`
`/*`
` 宏的實作:調用satisfies指向的可見性函數`
` #define HeapTupleSatisfiesVisibility(rel, tuple, snapshot, buffer) \`
` ((*(snapshot)->satisfies) (rel, tuple, snapshot, buffer))`
`*/`
`valid = HeapTupleSatisfiesVisibility(relation, tuple, snapshot, buffer);`
`…`

`if (valid) return true; // 找到了可見的元組`

`…`
`return false;`
`}`

           

擷取快照資料的函數有很多,它們都位于src/backend/utils/time/snapmgr.c中,例如:GetTransactionSnapshot(),GetLatestSnapshot(),GetCatalogSnapshot()等等。這些函數在一般情況下都會調用到GetSnapshotData函數,這個函數的主要操作就是周遊PGPROC數組,來生成調用時刻的快照資料,它的細節讀者可以閱讀代碼或最後參考書目中的内容。

03 獲得快照的時機

除了以上介紹的内容之外,快照還有一個要點: 快照的擷取時機,即何時生成一個新的快照内容? 或應該複用哪個已經生成了的快照。

以我們熟悉的事務隔離級别為例(這個邏輯位于GetTransactionSnapshot函數中):

  • 讀已送出(RC),對于事務中每條語句,都生成一個新的快照資料
  • 可重複讀(RR),隻有事務的第一條語句才生成快照資料,随後的語句隻複用這個快照資料。

但是如上隻是比較粗略的擷取時機,對于某個特定操作(比如utility函數,vaccum等)的快照擷取細節還需要到代碼中進行檢查(例如和各種鎖的配合)。

Greenplum中處理query的入口是QD進行的exec_simple_query(),相關的快照擷取時機見下:

`exec_simple_query()`
`{`
`…`
`/*`
` * Set up a snapshot if parse analysis/planning will need one.`
` */`
`// 某些類型query在planner階段也需要擷取快照`
`if (analyze_requires_snapshot(parsetree))`
`{`
`PushActiveSnapshot(GetTransactionSnapshot());`
`snapshot_set = true;`
`}`
`…`
`/* Done with the snapshot used for parsing/planning */`
`if (snapshot_set)`
`PopActiveSnapshot();`
`…`

`// 在PortalStart中調用GetTransactionSnapshot(),為executor的執行做準備`
`PortalStart(portal, NULL, 0, InvalidSnapshot, NULL);`
`…`

`// 在PortalRun中通過CdbDispatchXXX dispatch這個snaphost(一般是activesnapshot)到QE上`
`(void) PortalRun(portal, ...);`
`…`

`}`

`QE程序的執行入口是exec_mpp_query(),它直接使用QD dispatch過來的快照(DtxContextInfo結構體)中:`

`PostgresMain()`
`{`
`…`

`case 'M': /* MPP dispatched stmt from QD */`
`…`

`// 擷取QD生成并dispatch過來的(分布式)快照`
`serializedDtxContextInfo = pq_getmsgbytes(&input_message,serializedDtxContextInfolen);`
`…`

`// 已經存儲在QEDtxContextInfo全局變量中,可以在exec_mpp_query中使用`
`exec_mpp_query();`
`…`

`}`

           

通過如上示例中的注釋,讀者可以發現QE上的快照并不是QE程序本地的快照,而是引入了分布式快照的 内容 ,請看下節中的介紹。

04 分布式快照

讓我們回過頭來再看SnapshotData結構體,除了之前介紹的xmin等字段之外,最後還有一個distribSnapshotWithLocalMapping字段:

`typedef struct SnapshotData`
`{`
`… (xmin, xmax, xip ...)`

`/*`
` * GP: Global information about which transactions are visible for a`
` * distributed transaction, with cached local xids`
` */`
`DistributedSnapshotWithLocalMapping  distribSnapshotWithLocalMapping; // 結構見下`

`}`

`typedef struct DistributedSnapshotWithLocalMapping`
`{`
`DistributedSnapshot ds; /* DistributedSnapshot結構簡略如下:`
`{`
`DistributedSnapshotId distribSnapshotId;`
`DistributedTransactionId xmin;`
`DistributedTransactionId xmax;`
`int32    count;`
`/* Array of distributed transactions in progress. */`
`DistributedTransactionId        *inProgressXidArray;`
` } */`
`…`

`int32 currentLocalXidsCount;`
`int32 maxLocalXidsCount;`
`TransactionId *inProgressMappedLocalXids;`
`} DistributedSnapshotWithLocalMapping;`

           

即SnapshotData的大部分字段都是内地快照内容,然後通過最後的distribSnapshotWithLocalMapping 字段來綁定到它對應的分布式快照(在Master上生成)上,而分布式快照的資料結構是DistributedSnapshot結構體。

DistributedSnapshot的結構和本地快照非常相似:

  • distribSnapshotId表示分布式事務ID(Dtxid)
  • xmin,xmax和SnapshotData中的作用一緻,隻不過作用于分布式事務ID上
  • inProgressXidArray對應SnapshotData中的xip,表示運作中的分布式事務ID清單

通過分布式事務資訊就可以保證多個Segment上的判斷可見性是一緻的(即由Master為分布式事務定序),另外還要注意并不是任何時候都需要通過分布式事務資訊來判斷可見性:例如QE上新建立的堆元組尚未送出時就需要通過本地快照資訊來判斷它的可見性。

是以大家在檢視可見性判斷函數(如HeapTupleSatisfiesMVCC)的代碼時,請留意其中涉及到分布式事務資訊的邏輯(如XidInMVCCSnapshot函數)。

05 總結

本文對Greenplum中快照進行簡要的介紹。 網上一般涉及到事務講解時,對MVCC(如元組頭中的xmin/xmax)相關介紹較多,而對快照介紹的相對較少。 讀者可以結合本文,對Greeplum代碼中涉及到各類快照的使用場景(如catalog通路,index,vacuum等等)進行擴充學習和調試,這樣能對快照有更深刻的了解。

06 參考資料

  • Greenplum:基于 PostgreSQL 的分布式資料庫核心揭秘 (下篇)
  • The Internals of PostgreSQL : Chapt er 5 Concurrency Control (interdb.jp):

    https://www.interdb.jp/pg/pgsql05.html

  • MVCC in PostgreSQL-4. Snapshots / Habr: https://habr.com/en/company/postgrespro/blog/479512/
  • 《PostgreSQL技術内幕 事務》作者:張樹傑

繼續閱讀