在前面的文章中,我們已經回顧了許多與WAL相關的重要設置。在本文(本系列的最后一篇)中,我們將討論尚未解決的WAL設置問題:WAL的不同級別及其用途、以及WAL的可靠性和性能。
WAL級別
1.minimal
最小可能的級別由wal_level=minimal設置,只能保證在出現故障后可以執行恢復。為了節省空間,不會將與批量數據處理有關的操作(例如CREATE TABLE AS SELECT或CREATE INDEX)寫入WAL。而是將所需的數據立即寫入磁盤,並將新對象添加到系統目錄中,並在事務提交時可見。如果在執行操作時發生故障,則已寫入的數據將保持不可見並且不會違反一致性規則。並且,如果在完成操作后發生故障,則所需的所有內容都已經在磁盤上,不需要進行日志記錄。
=> ALTER SYSTEM SET wal_level = minimal; => ALTER SYSTEM SET max_wal_senders = 0;
student$ sudo pg_ctlcluster 11 main restart
這些修改需要重啟postgresql server。
=> SELECT pg_current_wal_insert_lsn(); pg_current_wal_insert_lsn --------------------------- 0/353927BC (1 row)
現在,讓我們創建一個表(CREATE TABLE AS SELECT),並再次記下WAL位置。在這種情況下,由SELECT運算符檢索的數據量完全無關緊要,因此一行就足夠了。
=> CREATE TABLE wallevel AS SELECT 1 AS n; => SELECT pg_current_wal_insert_lsn(); pg_current_wal_insert_lsn --------------------------- 0/353A7DFC (1 row)
來看看wal記錄:
postgres$ /usr/lib/postgresql/11/bin/pg_waldump -p /var/lib/postgresql/11/main/pg_wal -s 0/353927BC -e 0/353A7DFC
當然,可能有一些細節上的不同,但在本例中,我們將得到以下內容。Heap2 manager的記錄與vacuum有關,這里是系統目錄表的in-page vacuum。
rmgr: Heap2 len (rec/tot): 59/ 7587, tx: 0, lsn: 0/353927BC, prev 0/35392788, desc: CLEAN remxid 101126, blkref #0: rel 1663/16386/1247 blk 8 FPW
為要創建的表獲取下一個OID的記錄如下:
rmgr: XLOG len (rec/tot): 30/ 30, tx: 0, lsn: 0/35394574, prev 0/353927BC, desc: NEXTOID 82295
這里是純粹創建表:
rmgr: Storage len (rec/tot): 42/ 42, tx: 0, lsn: 0/35394594, prev 0/35394574, desc: CREATE base/16386/74103
但是數據插入到表中並沒有寫入wal。在不同的表和索引中插入多個記錄——通過這種方式,PostgreSQL將創建的表上的信息寫入系統目錄:
rmgr: Heap len (rec/tot): 203/ 203, tx: 101127, lsn: 0/353945C0, prev 0/35394594, desc: INSERT off 71, blkref #0: rel 1663/16386/1247 blk 8 rmgr: Btree len (rec/tot): 53/ 685, tx: 101127, lsn: 0/3539468C, prev 0/353945C0, desc: INSERT_LEAF off 37, blkref #0: rel 1663/16386/2703 blk 2 FPW ... rmgr: Btree len (rec/tot): 53/ 2393, tx: 101127, lsn: 0/353A747C, prev 0/353A6788, desc: INSERT_LEAF off 10, blkref #0: rel 1664/0/1233 blk 1 FPW
最后提交事務。
rmgr: Transaction len (rec/tot): 34/ 34, tx: 101127, lsn: 0/353A7DD8, prev 0/353A747C, desc: COMMIT 2019-07-23 18:59:34.923124 MSK
復制也是如此:沒有記錄的所有內容都不會發送給副本,也不會重新播放。如果希望在副本上運行查詢,則會使情況更加復雜。
首先,我們需要主服務器上出現的exclusive advisory locks的信息,因為它們可能會與副本上的查詢發生沖突。這些鎖將被寫入wal,然后啟動過程將它們應用到副本上。
其次,我們需要創建數據快照,要做到這一點,需要關於正在執行的事務的信息。對於副本,不僅意味着本地事務,還意味着主服務器上的事務。提供這些信息的唯一方法是不時地寫wal日志記錄(這種情況每15秒發生一次)。
確保從備份恢復和物理復制可能性的WAL級別由wal_level = replica設置。(在版本9.6之前,有兩個獨立的級別——archive和hot_standby——但后來它們被合並了)
=> ALTER SYSTEM RESET wal_level; => ALTER SYSTEM RESET max_wal_senders; student$ sudo pg_ctlcluster 11 main restart
刪除表並重新執行與上次完全相同的步驟序列:
=> DROP TABLE wallevel; => SELECT pg_current_wal_insert_lsn(); pg_current_wal_insert_lsn --------------------------- 0/353AF21C (1 row)
=> CREATE TABLE wallevel AS SELECT 1 AS n; => SELECT pg_current_wal_insert_lsn(); pg_current_wal_insert_lsn --------------------------- 0/353BE51C (1 row)
再來檢查一下wal記錄:
postgres$ /usr/lib/postgresql/11/bin/pg_waldump -p /var/lib/postgresql/11/main/pg_wal -s 0/353AF21C -e 0/353BE51C
vacuum、獲取OID、創建表並在系統目錄中注冊——與之前相同:
rmgr: Heap2 len (rec/tot): 58/ 58, tx: 0, lsn: 0/353AF21C, prev 0/353AF044, desc: CLEAN remxid 101128, blkref #0: rel 1663/16386/1247 blk 8 rmgr: XLOG len (rec/tot): 30/ 30, tx: 0, lsn: 0/353AF258, prev 0/353AF21C, desc: NEXTOID 82298 rmgr: Storage len (rec/tot): 42/ 42, tx: 0, lsn: 0/353AF278, prev 0/353AF258, desc: CREATE base/16386/74106 rmgr: Heap len (rec/tot): 203/ 203, tx: 101129, lsn: 0/353AF2A4, prev 0/353AF278, desc: INSERT off 73, blkref #0: rel 1663/16386/1247 blk 8 rmgr: Btree len (rec/tot): 53/ 717, tx: 101129, lsn: 0/353AF370, prev 0/353AF2A4, … rmgr: Btree len (rec/tot): 53/ 2413, tx: 101129, lsn: 0/353BD954, prev 0/353BCC44, desc: INSERT_LEAF off 10, blkref #0: rel 1664/0/1233 blk 1 FPW
出現了新的內容。與`Standby` manager相關的排他鎖記錄——這里是事務ID上的鎖:
rmgr: Standby len (rec/tot): 42/ 42, tx: 101129, lsn: 0/353BE2D8, prev 0/353BD954, desc: LOCK xid 101129 db 16386 rel 74106
這是我們表中插入行的記錄(比較文件號rel和創建記錄中的記錄):
rmgr: Heap len (rec/tot): 59/ 59, tx: 101129, lsn: 0/353BE304, prev 0/353BE2D8, desc: INSERT+INIT off 1, blkref #0: rel 1663/16386/74106 blk 0
這里是提交記錄:
rmgr: Transaction len (rec/tot): 421/ 421, tx: 101129, lsn: 0/353BE340, prev 0/353BE304, desc: COMMIT 2019-07-23 18:59:37.870333 MSK; inval msgs: catcache 74 catcache 73 catcache 74 catcache 73 catcache 50 catcache 49 catcache 7 catcache 6 catcache 7 catcache 6 catcache 7 catcache 6 catcache 7 catcache 6 catcache 7 catcache 6 catcache 7 catcache 6 catcache 7 catcache 6 snapshot 2608 relcache 74106 snapshot 1214
rmgr: Standby len (rec/tot): 50/ 50, tx: 0, lsn: 0/353BE4E8, prev 0/353BE340, desc: RUNNING_XACTS nextXid 101130 latestCompletedXid 101129 oldestRunningXid 101130
最后,最后一個級別由wal_level=logical指定,提供了邏輯解碼和邏輯復制。邏輯訂閱時必須打開。
從WAL記錄的角度來看,此級別實際上與replica級別相同:添加了與復制源相關的記錄,以及應用程序可以添加到WAL的任意邏輯記錄。但是邏輯解碼主要取決於有關正在執行的事務的信息,因為需要創建數據快照來跟蹤對系統目錄的更改。
寫的可靠性
日志記錄技術必須是可靠的,並確保在任何情況下都可以恢復。許多因素都會影響可靠性,我們將討論緩存、數據損壞和寫入原子性。
將數據存儲到非易失性存儲的過程存在着多級緩存。
如果某個程序要求操作系統(OS)在磁盤上寫入內容,則OS會將數據傳輸至其RAM緩存。寫入實際上是異步發生的,具體取決於操作系統的I/O調度程序的設置。
當操作系統決定寫入數據時,它們會進入存儲(硬盤)的緩存中。存儲器的電子設備還可以例如通過將更有效地寫入在一起的數據分組來推遲寫入。而且,如果使用RAID控制器,則在操作系統和磁盤之間添加一個更高的緩存級別。
因此,如果不采取特殊措施,就絕對不清楚何時以可靠的方式實際保存數據。但是PostgreSQL在某些關鍵區域必須確保以適當的可靠性寫入數據。這里主要是日志(如果WAL記錄未到達磁盤,則它將與其余RAM內容一起丟失)和檢查點(我們必須確保臟頁確實已寫入磁盤)。但是還有其他情況,例如以最小級別執行未記錄的操作等。
操作系統提供了確保立即將數據寫入非易失性存儲器的功能。有幾個選項,但是它們減少到兩個主要選項:在寫操作之后,執行同步調用(fsync,fdatasync),或者在打開文件(或寫入文件)之后,設置一個特殊標志來指示需要同步,甚至直接寫繞過OS緩存。
對於WAL,pg_test_fsync實用程序允許我們選擇最適合特定操作系統和特定文件系統的方法,並且該方法在wal_sync_method參數中指定。普通文件使用fsync同步。
選擇方法時,我們需要考慮硬件特性。例如:如果使用的控制器使用了電池備份單元(BBU),則沒有理由避免使用控制器的緩存,因為BBU可以在斷電的情況下保存數據。
在任何情況下,同步都是昂貴的。
通常,你可以關閉同步(fsync參數負責此操作),但是在這種情況下,你必須忘記存儲的可靠性。通過關閉fsync,你同意可以隨時丟失數據。 當可以輕松地從其他來源恢復數據時(例如在初始遷移時),此參數唯一合理的使用案例是性能的暫時提高。
2.數據損壞
硬件並不完美,通過接口電纜等傳輸時,存儲中的數據可能會損壞。 其中一些錯誤是在硬件級別處理的,而其他則沒有。
為了快速檢測問題,WAL記錄中提供了校驗和。
數據頁也可以由校驗和保護。 之前只能在集群初始化時執行此操作,但是在PostgreSQL 12中,可以通過pg_checksums實用程序打開和關閉校驗和。
在生產環境中,必須強制啟用校驗和,無論計算和驗證它們的開銷成本如何。 這降低了無法及時檢測到損壞的可能性。
校驗和只是減少了但沒有消除數據損壞的可能性: 首先,僅在訪問頁面時才校驗和。因此,損壞可以逃脫檢測,直到它進入全備份為止。出於這個原因,pg_probackup在數據備份期間驗證所有集群頁面的校驗和。 其次,用零填充的頁面被認為是正確的,因此,如果文件系統錯誤地使文件為«nullifies» ,則可以逃避檢測。 第三,校驗和僅保護數據的主分支。其他派生文件和其余文件(例如,事務狀態XACT)完全不受保護。
讓我們看看它是如何工作的。 首先,我們確保打開校驗和(請注意,在類似Debian的系統上安裝的軟件包中,默認情況下並非如此):
postgres=# show fsync; fsync ------- on (1 row) postgres=# show wal_sync_method; wal_sync_method ----------------- fdatasync (1 row) postgres=#
這是我們表所在的文件:
=> SELECT pg_relation_filepath('wallevel'); pg_relation_filepath ---------------------- base/16386/24890 (1 row)
現在我們關閉postgresql server並在零頁上更改一些字節,例如:從header中刪除最后一個WAL記錄的LSN。
student$ sudo pg_ctlcluster 11 main stop postgres$ dd if=/dev/zero of=/var/lib/postgresql/11/main/base/16386/24890 oflag=dsync conv=notrunc bs=1 count=8 8+0 records in 8+0 records out 8 bytes copied, 0,0083022 s, 1,0 kB/s
通常,不需要關閉服務器。 將頁面刷新到磁盤並從緩存中逐出就足夠了(否則,服務器將繼續使用緩存中的頁面)。 但是,這種情況下重現起來更加復雜。
student$ sudo pg_ctlcluster 11 main start => SELECT * FROM wallevel; WARNING: page verification failed, calculated checksum 23222 but expected 50884 ERROR: invalid page in block 0 of relation base/16386/24890
但是,如果無法從備份還原數據,我們該怎么辦? 當然,ignore_checksum_failure參數使您能夠嘗試讀取表,但有可能損壞數據。
=> SET ignore_checksum_failure = on; => SELECT * FROM wallevel; WARNING: page verification failed, calculated checksum 23222 but expected 50884 n --- 1 (1 row)
還有一點需要注意。啟用校驗和后,提示位(hint bits)將被WAL記錄,因為對任何位(甚至是非必需位)的更改都會導致對校驗和的更改。關閉校驗和時,wal_log_hints參數負責WAL記錄提示位。
提示位的更改始終記錄為FPI(full page image),這會增加了WAL大小。 在這種情況下,使用wal_compression參數打開FPI壓縮是有意義的(此參數已在9.5版中添加)。
最后,看看寫的原子性存在的問題。數據庫頁占用不少於8 KB(可能為16或32KB),在底層,寫操作以塊形式進行,通常較小(通常為512字節或4KB)。因此,在斷電的情況下,可能會將部分寫入數據頁。顯然,在恢復期間,將常規的WAL記錄應用於這樣的頁面是沒有意義的。
為了避免這種情況,PostgreSQL在檢查點周期開始以來的頁面的第一次更改中啟用了WAL記錄整個頁面的鏡像(當提示位更改時,也會記錄同一圖像)。full_page_writes參數對此進行控制,並且默認情況下處於啟用狀態。
如果恢復過程遇到WAL中的FPI,它將無條件地將鏡像寫入磁盤(無需LSN檢查):FPI更受信任,因為它受校驗和保護,就像每個WAL記錄一樣。
盡管在PostgreSQL中,FPI不包括可用空間(我們之前討論了塊結構),但FPI大大增加了WAL記錄的生成量。如前所述,可以通過壓縮FPI(使用wal_compression參數)來改善這種情況。
為了深入了解FPI會如何更改WAL的大小,讓我們使用pgbench實用工具進行簡單的實驗。執行初始化:
student$ pgbench -i test dropping old tables... creating tables... generating data... 100000 of 100000 tuples (100%) done (elapsed 0.15 s, remaining 0.00 s) vacuuming... creating primary keys... done.
full_page_writes是開啟的:
=> SHOW full_page_writes; full_page_writes ------------------ on (1 row)
我們來執行一個檢查點並立即運行測試30秒鍾。
=> CHECKPOINT; => SELECT pg_current_wal_insert_lsn(); pg_current_wal_insert_lsn --------------------------- 0/38E04A08 (1 row) student$ pgbench -T 30 test starting vacuum...end. transaction type: TPC-B (sort of) scaling factor: 1 query mode: simple number of clients: 1 number of threads: 1 duration: 30 s number of transactions actually processed: 26851 latency average = 1.117 ms tps = 895.006720 (including connections establishing) tps = 895.095229 (excluding connections establishing) => SELECT pg_current_wal_insert_lsn(); pg_current_wal_insert_lsn --------------------------- 0/3A69C478 (1 row)
看看wal記錄大小:
=> SELECT pg_size_pretty('0/3A69C478'::pg_lsn - '0/38E04A08'::pg_lsn); pg_size_pretty ---------------- 25 MB (1 row)
現在我們來關閉full_page_writes參數:
=> ALTER SYSTEM SET full_page_writes = off; => SELECT pg_reload_conf();
再重復上面的實驗:
=> CHECKPOINT; => SELECT pg_current_wal_insert_lsn(); pg_current_wal_insert_lsn --------------------------- 0/3A69C530 (1 row) student$ pgbench -T 30 test starting vacuum...end. transaction type: TPC-B (sort of) scaling factor: 1 query mode: simple number of clients: 1 number of threads: 1 duration: 30 s number of transactions actually processed: 27234 latency average = 1.102 ms tps = 907.783080 (including connections establishing) tps = 907.895326 (excluding connections establishing) => SELECT pg_current_wal_insert_lsn(); pg_current_wal_insert_lsn --------------------------- 0/3BE87658 (1 row)
查看生成的wal的大小:
=> SELECT pg_size_pretty('0/3BE87658'::pg_lsn - '0/3A69C530'::pg_lsn); pg_size_pretty ---------------- 24 MB (1 row)
wal大小減小了,但沒有我們期望的那么大。
postgres$ /usr/lib/postgresql/11/bin/pg_waldump --stats -p /var/lib/postgresql/11/main/pg_wal -s 0/3A69C530 -e 0/3BE87658 Type N (%) Record size (%) FPI size (%) ---- - --- ----------- --- -------- --- XLOG 1721 ( 1,03) 84329 ( 0,77) 13916104 (100,00) Transaction 27235 ( 16,32) 926070 ( 8,46) 0 ( 0,00) Storage 1 ( 0,00) 42 ( 0,00) 0 ( 0,00) CLOG 1 ( 0,00) 30 ( 0,00) 0 ( 0,00) Standby 4 ( 0,00) 240 ( 0,00) 0 ( 0,00) Heap2 27522 ( 16,49) 1726352 ( 15,76) 0 ( 0,00) Heap 109691 ( 65,71) 8169121 ( 74,59) 0 ( 0,00) Btree 756 ( 0,45) 45380 ( 0,41) 0 ( 0,00) -------- -------- -------- Total 166931 10951564 [44,04%] 13916104 [55,96%]
其中值為0的被移除了,這樣可以看得更緊湊一點。注意看匯總那一行(total),比較FPI和常規的WAL記錄的大小。
僅當文件系統和硬件本身確保寫入的原子性時,才能關閉full_page_writes參數。 但是,正如我們所看到的,它沒有太多意義(提供的校驗和已打開)。
=> ALTER SYSTEM SET full_page_writes = on; => ALTER SYSTEM SET wal_compression = on; => SELECT pg_reload_conf();
重復上面的實驗:
=> CHECKPOINT; => SELECT pg_current_wal_insert_lsn(); pg_current_wal_insert_lsn --------------------------- 0/3BE87710 (1 row) student$ pgbench -T 30 test starting vacuum...end. transaction type: TPC-B (sort of) scaling factor: 1 query mode: simple number of clients: 1 number of threads: 1 duration: 30 s number of transactions actually processed: 26833 latency average = 1.118 ms tps = 894.405027 (including connections establishing) tps = 894.516845 (excluding connections establishing) => SELECT pg_current_wal_insert_lsn(); pg_current_wal_insert_lsn --------------------------- 0/3CBD3EA8 (1 row)
查看wal記錄的大小:
=> SELECT pg_size_pretty('0/3CBD3EA8'::pg_lsn - '0/3BE87710'::pg_lsn); pg_size_pretty ---------------- 13 MB (1 row)
在正常工作期間,WAL文件按照一個接一個的順序被連續寫入。由於沒有隨機訪問,即使是HDD磁盤也能正常工作。但是,這種負載與訪問數據文件時的負載有很大不同。
因此,將WAL存儲在單獨的物理磁盤(或磁盤陣列)上通常是有益的。必須創建指向相應目錄的符號鏈接,而不是$PGDATA/pg_wal目錄。
在某些情況下,不僅需要寫入WAL文件,還需要讀取WAL文件。第一個是發生故障后恢復的明確案例。第二個不那么瑣碎。如果使用流復制,WAL記錄仍在主服務器的OS緩沖區中時,副本延遲接收WAL記錄。在這種情況下,walsender進程必須從磁盤讀取必要的數據。進行復制時,我們將對此進行更詳細的討論。
WAL以以下兩種方式之一寫入:
·同步-在事務提交時,直到該事務的所有WAL記錄都進入磁盤后,才能繼續工作。 ·異步-事務立即完成,並且WAL在后台寫入。
缺省情況下,synchronous_commit參數處於打開狀態,用於設置同步模式。
由於同步與實際的(即較慢的)輸入/輸出有關,因此盡可能少地進行同步是有好處的。為此,完成事務並寫入WAL的后端進程會稍作暫停,這由commit_delay參數定義。但這僅在系統具有不少於commit_siblings活動事務的情況下才會發生。此行為依賴於這樣的期望:在等待時間內,一些事務將完成,並且可以一次性同步它們。這類似於你摁住電梯門的方式,以便有人有時間進入。
默認情況下,commit_siblings = 5,commit_delay = 0,因此實際上沒有等待。僅對於執行大量OLTP事務的系統,更改commit_delay的值才有意義。
然后,將WAL的一部分刷新到所需的LSN位置(如果在等待時間內添加了新記錄,則刷新更多)。刷新之后,事務被視為完成。
同步寫入可確保持久性(ACID縮寫中的字母D):如果提交了事務,則其所有WAL記錄都已經在磁盤上,並且不會丟失。但是缺點是同步寫入會增加響應時間(COMMIT命令直到同步結束才返回控制權),並且會降低系統性能。
您可以通過設置sync_commit = off(或local)來使寫入變成異步。
當異步寫入時,WAL記錄將由wal writer進程刷新,該進程將交替工作並等待(等待時間由wal_writer_delay參數指定,默認值為200 ms)。
該進程被喚醒后,它將檢查自上次以來是否出現了完全填充的WAL頁面。如果確實出現了,則該進程將忽略當前未填充到最后的頁面,僅寫入完全填充的頁面。(但是,並非一次完成:寫入到達高速緩存末尾時會停止,而下一次從高速緩存起始點開始。)
但是,如果沒有任何頁面被填滿,則該進程將刷新當前的WAL頁面(未填充到末尾)—要不然,喚醒后干嘛呢?
該算法旨在盡可能避免多次同步同一頁面,這對於大量更新至關重要。
異步寫入比同步寫入更有效,因為更改的提交不等待WAL頁面的寫入。但是可靠性降低了:如果在提交和失敗之間經過的時間少於3×wal_writer_delay單位,則提交失敗的數據可能會在失敗的情況下丟失(使用默認設置,該時間稍長於半秒)。
在效率和可靠性之間並非一個簡單的選擇,要取決於系統管理員。
請注意:與關閉同步(fsync = off)不同,異步模式不會使恢復變得不可能。萬一發生故障,系統將恢復一致狀態,但也許某些最后的事務將不存在。
可以為單獨的事務設置synchronous_commit 參數。這樣可以通過僅犧牲某些事務的可靠性來提高性能。例如金融交易必須同步進行,而聊天消息卻可能使用異步。
實際上,兩種模式可以協同工作。即使使用同步提交,長事務的WAL記錄也將被異步寫入,以釋放WAL緩沖區。而且,如果在從緩沖區高速緩存中刷新頁面期間,似乎還沒有在磁盤上存儲相應的WAL記錄,那么它將以同步模式立即被刷新。
為了深入了解異步提交的好處,讓我們嘗試在此模式下重復pgbench測試。
=> ALTER SYSTEM SET synchronous_commit = off; => SELECT pg_reload_conf(); student$ pgbench -T 30 test starting vacuum...end. transaction type: TPC-B (sort of) scaling factor: 1 query mode: simple number of clients: 1 number of threads: 1 duration: 30 s number of transactions actually processed: 45439 latency average = 0.660 ms tps = 1514.561710 (including connections establishing) tps = 1514.710558 (excluding connections establishing)
使用同步提交,我們每秒大約獲得900個事務(tps),使用異步提交則獲得1500 tps。 不用說,在實際系統中的實際負載下,所占的比例會有所不同,但是很明顯,對於短期交易而言,其影響可能是相當大的。
原文地址:https://habr.com/en/company/postgrespro/blog/496150/