1. 程式人生 > 實用技巧 >PostgreSQL的WAL(2)--Write-Ahead Log

PostgreSQL的WAL(2)--Write-Ahead Log

丟失RAM中的資料的風險是我們需要在故障後恢復資料的技術的主要原因。現在我們來討論這些技術。

日誌

為了避免RAM中資料丟失,必須將所有必需的東西妥善儲存到磁碟(或其他非易失性介質)中。為此,做了以下的操作。在更改資料時,還維護了這些更改的日誌。當我們更改buffer cache中頁面上的某些內容時,我們會在日誌中建立此更改的記錄。 該記錄包含必要時足以重做更改的最少資訊。為此,日誌記錄必須在更改後的頁面寫入磁碟之前必須先寫入磁碟。 這解釋了名稱:預寫日誌(WAL)。

如果發生故障,磁碟上的資料可能會不一致:某些頁面寫得較早,而另一些頁面寫得較晚。但是WAL仍然存在,我們可以讀取並重做在故障之前執行的操作,但是操作的結果會晚一點到磁碟。

為什麼不強行將資料頁面本身寫入磁碟,為什麼要重複工作呢?

首先,WAL是按順序追加順序流資料。甚至HDD磁碟也可以很好地進行順序寫入。但是,由於頁面或多或少地分散在磁碟上,因此資料本身是以隨機方式寫入的。

其次,WAL記錄可能比頁面小很多。

第三,在寫入磁碟時,我們不必在每個時間點都要維護磁碟上資料的一致性。

第四,正如我們稍後將看到的,WAL(一旦可用)不僅可以用於恢復,還可以用於備份和複製。

必須對所有操作進行WAL記錄,以防發生故障時導致磁碟上的資料不一致。具體來說,以下操作是WAL記錄的:

·buffer cache中對頁面的修改(主要是表頁面和索引頁面)—因為頁面更改需要花費一些時間才能到達磁碟。

·事務的提交和中止-因為狀態更改是在XACT緩衝區中完成的,所以更改也需要一些時間才能到達磁碟。

·檔案操作(建立和刪除檔案和目錄,例如在建立表期間建立檔案)-因為這些操作必須與資料更改同步。

以下內容未進行WAL記錄:

·對unlogged屬性的表的操作。

·對臨時表的操作-日誌記錄沒有意義,因為此類表的生存期不超過建立它們的會話的生存期。

在PostgreSQL 10之前,沒有對雜湊索引寫WAL,但是此問題已得到糾正。

邏輯結構

我們可以從邏輯上將WAL想象成一系列不同長度的記錄。每個記錄都包含有關特定操作的資料,並以標準header作為字首。在header中,其餘部分指定以下內容:

·與記錄相關的事務ID。

·資源管理器-負責記錄的系統元件。

·校驗和(CRC)-用於檢測資料是否損壞。

·記錄的長度,並連結到上一個記錄。

至於資料,它們可以具有不同的格式和含義。例如:它們可以由需要以一定偏移量寫在頁面內容頂部的頁面片段表示。資源管理器指定了“理解”如何解釋其記錄中的資料。有單獨的表管理器,每種型別的索引,事務狀態等等。可以使用以下命令獲取它們的完整列表

pg_waldump -r list
XLOG
Transaction
Storage
CLOG
Database
Tablespace
MultiXact
RelMap
Standby
Heap2
Heap
Btree
Hash
Gin
Gist
Sequence
SPGist
BRIN
CommitTs
ReplicationOrigin
Generic
LogicalMessage

物理結構

WAL作為$PGDATA/pg_wal目錄中的檔案儲存在磁碟上。預設情況下,每個檔案為16 MB。可以增加此大小,以避免在一個目錄中包含多個檔案。在PostgreSQL 11之前,只能在編譯原始碼時執行此操作,但是現在可以在初始化叢集時指定大小(使用--wal-segsize選項)。

WAL記錄寫入當前使用的檔案,一旦結束,將使用下一個檔案。

在伺服器的共享記憶體中,為WAL分配了特殊的緩衝區。wal_buffers引數指定WAL快取的大小(預設值表示自動設定:已分配buffer cache的1/32)。

WAL快取的結構類似於buffer cache,但是以迴圈模式下工作:將記錄新增到“ head”,但從“ tail”開始寫入磁碟。

pg_current_wal_lsn和pg_current_wal_insert_lsn函式分別返回寫(«tail»)和插入(«head»)位置:

=> SELECT pg_current_wal_lsn(), pg_current_wal_insert_lsn();
 pg_current_wal_lsn | pg_current_wal_insert_lsn 
--------------------+---------------------------
 0/331E4E64         | 0/331E4EA0
(1 row)

為了引用某個記錄,使用了pg_lsn資料型別:它是一個64位整數,表示記錄的開始相對於WAL的開始的位元組偏移。LSN以斜槓分隔的兩個32位十六進位制數字輸出。

我們可以知道在哪個檔案中可以找到所需的位置以及與檔案開頭的偏移量:

# select pg_current_wal_lsn(),pg_current_wal_insert_lsn();
 pg_current_wal_lsn | pg_current_wal_insert_lsn 
--------------------+---------------------------
 23C/5AFCAE38       | 23C/5AFCAE38
(1 row)

s=# SELECT file_name, upper(to_hex(file_offset)) file_offset
postgres-# FROM pg_walfile_name_offset('23C/5AFCAE38');
        file_name         | file_offset 
--------------------------+-------------
 000000010000023C0000005A | FCAE38
(1 row)

=# 

檔名由兩部分組成。8位高位十六進位制數字顯示時間線的編號(用於從備份還原),其餘部分對應於LSN的高位(LSN其餘的低位顯示偏移量)。

在檔案系統中,可以在$PGDATA/pg_wal/目錄中看到WAL檔案,但是從PostgreSQL 10開始,還可以使用專門的功能來檢視它們:

=> SELECT * FROM pg_ls_waldir() WHERE name = '000000010000000000000033';
           name           |   size   |      modification      
--------------------------+----------+------------------------
 000000010000000000000033 | 16777216 | 2019-07-08 20:24:13+03
(1 row)

WAL

讓我們看看如何進行WAL以及如何確保提前寫入。讓我們建立一個表:

=> CREATE TABLE wal(id integer);
=> INSERT INTO wal VALUES (1);

我們來研究page的header部分。為此,我們需要一個著名的擴充套件:

=> CREATE EXTENSION pageinspect;

讓我們開始一個事務,並記住插入WAL的位置:

=> BEGIN;
=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn 
---------------------------
 0/331F377C
(1 row)

  

現在我們將執行一些操作,例如,更新一行:

=> UPDATE wal set id = id + 1;

此更改已寫WAL記錄,並且插入位置已更改:

=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn 
---------------------------
 0/331F37C4
(1 row)

為了確保在WAL寫入之前不會將更改後的資料頁重新整理到磁碟,與該頁相關的最後一個WAL記錄的LSN儲存在頁頭中:

=> SELECT lsn FROM page_header(get_raw_page('wal',0));
    lsn     
------------
 0/331F37C4
(1 row)

請注意,WAL是整個叢集的,新記錄始終都在那裡。因此,頁面上的LSN可以小於pg_current_wal_insert_lsn函式剛返回的值。但是由於在我們的系統中什麼也沒有發生,因此數字是相同的。

現在,讓我們提交事務。

=> COMMIT;

提交也被WAL記錄,並且位置再次更改:

=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn 
---------------------------
 0/331F37E8
(1 row)

每次提交都會變更XACT結構中事務狀態(我們已經討論過)。狀態儲存在檔案中,但它們也使用自己的快取,該快取在共享記憶體中佔據128頁。因此,對於XACT頁面,也必須跟蹤最後一個WAL記錄的LSN。但是,此資訊儲存在RAM中,而不是儲存在頁面本身中。

建立的WAL記錄將被一次寫入磁碟。我們將在稍後的某個時間討論確切的情況,但是在上述情況下,情況已經發生:

=> SELECT pg_current_wal_lsn(), pg_current_wal_insert_lsn();
 pg_current_wal_lsn | pg_current_wal_insert_lsn 
--------------------+---------------------------
 0/331F37E8         | 0/331F37E8
(1 row)

此後,資料和XACT頁面可以重新整理到磁碟。但是,如果我們不得不更早地重新整理它們,它將被檢測到,並且WAL記錄將被迫首先進入磁碟。

具有兩個LSN位置,我們可以通過簡單地從另一箇中減去一個來獲得它們之間的WAL記錄數量(以位元組為單位)。我們只需要將ksn位置轉換為pg_lsn型別:

=> SELECT '0/331F37E8'::pg_lsn - '0/331F377C'::pg_lsn;
 ?column? 
----------
      108
(1 row)

在這種情況下,行和提交的更新在WAL中需要108個位元組。

我們可以用相同的方式評估伺服器在一定負載下每單位時間生成的WAL記錄的數量。這是重要的資訊,需要進行調優(我們將在下次討論)。

現在,讓我們使用pg_waldump實用工具檢視建立的WAL記錄。

該工具還可以使用一系列LSN(如本例所示),併為指定的事務選擇記錄。應該以postgres OS使用者身份執行該工具,因為它需要訪問磁碟上的WAL檔案。

postgres$ /usr/lib/postgresql/11/bin/pg_waldump -p /var/lib/postgresql/11/main/pg_wal -s 0/331F377C -e 0/331F37E8 000000010000000000000033
rmgr: Heap        len (rec/tot):     69/    69, tx:     101085, lsn: 0/331F377C, prev 0/331F3014, desc: HOT_UPDATE off 1 xmax 101085 ; new off 2 xmax 0, blkref #0: rel 1663/16386/33081 blk 0
rmgr: Transaction len (rec/tot):     34/    34, tx:     101085, lsn: 0/331F37C4, prev 0/331F377C, desc: COMMIT 2019-07-08 20:24:13.945435 MSK

在這裡,我們看到兩個記錄的header。

第一個是HOT_UPDATE操作,與堆資源管理器有關。檔名和頁號在blkref欄位中指定,與更新後的表頁面相同:

=> SELECT pg_relation_filepath('wal');
 pg_relation_filepath 
----------------------
 base/16386/33081
(1 row)

第二個記錄是COMMIT,與事務資源管理器有關。

這種格式幾乎不容易閱讀,但是可以讓我們在需要時使用。

恢復

當我們啟動伺服器時,首先啟動postmaster程序,然後啟動startup程序,該啟動程序的任務是確保在發生故障時進行恢復。

為了確定是否需要恢復,startup程序會在專用控制檔案$PGDATA/global/pg_control中檢視叢集狀態。但是我們也可以通過pg_controldata實用程式自己檢查狀態:

postgres$ /usr/lib/postgresql/11/bin/pg_controldata -D /var/lib/postgresql/11/main | grep state
Database cluster state:               in production

常規方式關閉的postgresql server將處於“shutdown”狀態。如果postgresql server不工作,但狀態仍處於“in production”,則表示DBMS已關閉,並且恢復將自動完成。

為了進行恢復,startup程序將順序讀取WAL並將記錄應用於頁面(如果需要)。可以通過將磁碟上頁面的LSN與WAL記錄的LSN進行比較來確定需求。如果頁面的LSN看起來更大,則不需要應用該記錄。實際上,它甚至無法應用,因為記錄是按照嚴格的順序應用的。

但是也有例外。某些記錄被建立為FPI(full page image),該記錄將覆蓋頁面內容,因此無論其狀態如何都可以將其應用於頁面。可以將事務狀態的更改應用於XACT頁的任何版本,因此無需在此類頁中儲存LSN。

在恢復過程中,與常規工作一樣,頁面將在buffer cache中更改。為此,postmaster程序啟動所需的後臺程序。

WAL記錄以類似的方式應用於檔案。

And at the very end of the recovery process, respective initialization forks overwrite all unlogged tables to make them empty。

這是該演算法的非常簡化的描述。具體來說,到目前為止,我們還沒有說起從哪裡開始閱讀WAL記錄。

最後要提的是:恢復過程包括兩個階段。在第一(前滾)階段,將應用日誌記錄,並且伺服器將重做由於故障而丟失的所有工作。在第二(回滾)階段,將回滾在故障時刻尚未提交的事務。但是PostgreSQL不需要第二階段。如前所述,由於多版本併發控制的實現功能,無需物理回滾事務-因此無需在XACT中設定commit位。

原文地址:https://habr.com/en/company/postgrespro/blog/494246/