一、預寫日志(WAL)
預寫式日志(Write Ahead Log,WAL)是保證數據完整性的一種標准方法。簡單來說,WAL的中心概念是數據文件(存儲着表和索引)的修改必須在這些動作被日志記錄之后才被寫入,即在描述這些改變的日志記錄被刷到持久存儲以后。如果我們遵循這種過程,我們不需要在每個事務提交時刷寫數據頁面到磁盤,因為我們知道在發生崩潰時可以使用日志來恢復數據庫:任何還沒有被應用到數據頁面的改變可以根據其日志記錄重做(這是前滾恢復,也被稱為REDO)。
使用WAL可以顯著降低磁盤的寫次數,因為只有日志文件需要被刷出到磁盤以保證事務被提交,而被事務改變的每一個數據文件則不必被刷出。日志文件被按照順序寫入,因此同步日志的代價要遠低於刷寫數據頁面的代價。在處理很多影響數據存儲不同部分的小事務的服務器上這一點尤其明顯。此外,當服務器在處理很多小的並行事務時,日志文件的一個fsync可以提交很多事務。關閉 fsync 對 SELECT 無影響, 而 UPDATE 性能有較大提升,這個場景提升了 111%;當然關閉 fsync 參數的代價是巨大的,當數據庫主機遭受操作系統故障或硬件故障時,數據庫很有可能無法啟動,並丟失數據,建議生產庫不要關閉這參數。
WAL也使得在線備份和時間點恢復能被支持。通過歸檔WAL數據,我們可以支持回轉到被可用WAL數據覆蓋的任何時間:我們簡單地安裝數據庫的一個較早的物理備份,並且重放WAL日志一直到所期望的時間。另外,該物理備份不需要是數據庫狀態的一個一致的快照 — 如果它的制作經過了一段時間,則重放這一段時間的WAL日志將會修復任何內部不一致性。
二、簡介
類似於Oracle的redo,PostgreSQL的redo文件被稱為WAL文件或XLOG文件,存放在 $PGDATA/pg_xlog或 $PGDATA/pg_wal目錄中(PostgreSQL從10版本開始,將所用xlog相關的全部用wal替換了)。任何試圖修改數據庫數據的操作都會寫一份日志到磁盤。wal命名格式文件名稱為16進制的24個字符組成,每8個字符一組,每組的意義如下:
通過select pg_switch_xlog()或select pg_switch_wal();可以切換xlog/wal日志。
wal日志即write ahead log預寫式日志,簡稱wal日志。wal日志可以說是PostgreSQL中十分重要的部分,相當於oracle中的redo日志。 當數據庫中數據發生變更時: change發生時:先要將變更后內容計入wal buffer中,再將變更后的數據寫入data buffer; commit發生時:wal buffer中數據刷新到磁盤; checkpoint發生時:將所有data buffer刷新的磁盤。
如果沒有wal日志,那么數據庫中將會發生什么? 首先,當我們在數據庫中更新數據時,如果沒有wal日志,那么每次更新都會將數據刷到磁盤上,並且這個動作是隨機i/o,性能可想而知。並且沒有wal日志,關系型數據庫中事務的ACID如何保證呢? 因此wal日志重要性可想而知。其中心思想就是:先寫入日志文件,再寫入數據。
說到checkpoint,我們再來看看哪些情況會觸發數據庫的checkpoing: 1.手動執行CHECKPOINT命令; 2.執行需要檢查點的命令(例如pg_start_backup 或pg_ctl stop|restart等等); 3.達到檢查點配置時間(checkpoint_timeout); 4.max_wal_size已滿。
checkpoint_timeout: 自動 WAL 檢查點之間的最長時間,以秒計。合理的范圍在 30 秒到 1 天之間。默認是 5 分鍾(5min)。增加這個參數的值會增加崩潰恢復所需的時間。
max_wal_size: 在自動 WAL檢查點之間允許WAL 增長到的最大尺寸。這是一個軟限制,在特殊的情況 下 WAL 尺寸可能會超過max_wal_size, 例如在重度負荷下、archive_command失敗或者高的 wal_keep_segments設置。默認為 1 GB。增加這個參數可能導致崩潰恢復所需的時間。( wal_keep_segments用於指定pg_wal目錄中保存的過去的wal文件(wal 段)的最小數量,以防備用服務器在進行流復制時需要。)
和max_wal_size相對應的還有個min_wal_size,只要 WAL 磁盤用量保持在這個設置之下,在檢查點時舊的 WAL文件總是被回收以便未來使用,而不是直接被刪除。
wal切換步驟是這樣的:單個wal日志寫滿(默認大小16MB,編譯數據庫時指定)繼續寫下一個wal日志,直到磁盤剩余空間不足min_wal_size時才會將舊的 WAL文件回收以便繼續使用。但是這種模式有一個弊端就是如果在checkpoint之前產生了大量的wal日志就會導致發生checkpoint時對性能的影響巨大,因此pg中還有一個參數checkpoint_completion_target 來進行調整。
checkpoint_completion_target: 指定檢查點完成的目標,作為檢查點之間總時間的一部分。默認是 0.5。假如我的checkpoint_timeout設置是30分鍾,而wal生成了10G,那么設置成0.5就允許我在15分鍾內完成checkpoint,調大這個值就可以降低checkpoint對性能的影響,但是萬一數據庫出現故障,那么這個值設置越大數據就越危險。
總結:大多數檢查點應該是基於時間的,即由checkpoint_timeout觸發。 性能(不頻繁檢查點)與恢復所需時間(頻繁檢查點)之間需要抉擇: 值在15-30分鍾之間是比例合適的,但到1小時不是什么壞事。 在決定checkpoint_timeout后,通過估計WAL的數量選擇max_wal_size。 設置checkpoint_completion_target以便內核將數據刷新到磁盤的時間足夠(但不是太多)
三、切換WAL日志
pg_switch_wal()或pg_switch_xlog()強制服務器切換到一個新的預寫式日志文件,這允許對當前文件進行歸檔(假設你正在使用連續歸檔)。 其結果是在剛剛完成的預寫式日志文件中結束預寫式日志位置加1。 如果自從上次預寫式日志切換以來沒有提前寫日志活動,pg_switch_wal將不做任何操作,並返回當前正在使用的提前寫日志文件的起始位置。默認情況下該函數僅限超級用戶使用,但可以授權其他用戶執行該函數。pg_switch_xlog()用於PG 10之前,從PG 10開始切換歸檔日志使用pg_switch_wal()。PG也提供了相應的函數根據LSN獲取日志文件名:
四、配置WAL歸檔
必須重啟,如果開啟了歸檔,則在歸檔路徑下的archive_status目錄里,會有類似000000010000000000000002.ready和000000010000000000000003.done的文件,.ready表示XLOG文件已寫滿,可以調用歸檔命令了,.done表示已歸檔完成。開啟了歸檔后,只有歸檔成功的pg_xlog文件才會被清除。在每次歸檔命令被執行后,會觸發清除標簽的動作,在執行檢查點時,也會觸發清除歸檔標簽文件的動作。
五、自動清理WAL日志
一般來說,設置自動清理archive_log 可以在配置文件中添加archive_cleanup_command = 'pg_archivecleanup archivelocation %r'
或者:alter system set archive_cleanup_command='pg_archivecleanup /var/lib/postgresql/data/pg_wal %r';
六、手動清理WAL日志
如果配置了archive_mode=on,但是沒有配置archive_command,那么xlog文件會一直堆積(xlog寫完后,會寫.ready,但是由於沒有配置archive_command,也就是說不會觸發歸檔命令,所以一直都不會寫 .done),所以xlog會一直不清理。
1、讀取控制文件,找到能清理的xlog范圍
其中,“Latest checkpoint's REDO WAL file: 000000010000000000000005”之前的文件都可以清理。
2、通過pg_archivecleanup清理
pg_archivecleanup -d $PGDATA/pg_wal 000000010000000000000005
pg的wal日志保存在數據庫目錄下的pg_wal/子目錄,這個日志存在的目的是為了保證崩潰后的安全,如果系統崩潰,可以“重放”從最后一次檢查點以來的日志項來恢復數據庫的一致性。但是也存在日志膨脹的問題,
七、WAL歸檔
在生產環境,為了保證數據高可用性,通常需要設置歸檔,所謂的歸檔,其實就是把pg_wal里面的日志備份出來,當系統故障后可以通過歸檔的日志文件對數據進行恢復:
配置歸檔需要開啟如下參數:
1、wal_level = replica (pg13默認已經開啟replica) 該參數的可選的值有minimal,replica和logical,wal的級別依次增高,在wal的信息也越多。由於minimal這一級別的wal不包含從基礎的備份和wal日志重建數據的足夠信息,在該模式下,無法開啟wal日志歸檔。
2、archive_mode = on 上述參數為on,表示打開歸檔備份,可選的參數為on,off,always 默認值為off,所以要手動打開。
3、archive_command = 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f' 該參數的默認值是一個空字符串,他的值可以是一條shell命令或者一個復雜的shell腳本。在shell腳本或命令中可以用 “%p” 表示將要歸檔的wal文件包含完整路徑的信息的文件名,用“%f” 代表不包含路徑信息的wal文件的文件名。
注意:wal_level和archive_mode參數修改都需要重新啟動數據庫才可以生效。而修改archive_command則不需要。所以一般配置新系統時,無論當時是否需要歸檔,這要建議將這兩個參數開啟。
八、如何計算WAL文件名
我們知道由三部分組成,那么又是如何計算呢?公式如下:
WAL segment file name = timelineId +(uint32)LSN−1 / (16M ∗ 256) + (uint32)(LSN − 1 / 16M) % 256
示例:
1、查看當前LSN位置
查看當前wal: select pg_walfile_name(pg_current_wal_lsn());
2、這里的LSN是’ 1/C469AA30’ 我們轉換為十進制數:
3、利用公式計算:
算出來的值與通過函數查詢的一致:
九、計算WAL日志的大小
10、查看wal生成頻率和大小
–wal 文件生成數量
–linux ls --full-time stat filename
–pg_stat_file返回一個記錄,其中包含
– 1 size 文件尺寸
– 2 access 最后訪問時間戳(linux:最近訪問) 、
– 3 modification 最后修改時間戳(linux:最近更改–) 、
– 4 change 最后文件狀態改變時間戳(只支持 Unix 平台)(linux:最近改動) 、
– 5 creation 文件創建時間戳(只支持 Windows)
– 6 isdir 一個boolean指示它是否為目錄 isdir
with tmp_file as ( select t1.file, t1.file_ls, (pg_stat_file(t1.file)).size as size, (pg_stat_file(t1.file)).access as access, (pg_stat_file(t1.file)).modification as last_update_time, (pg_stat_file(t1.file)).change as change, (pg_stat_file(t1.file)).creation as creation, (pg_stat_file(t1.file)).isdir as isdir from (select dir||'/'||pg_ls_dir(t0.dir) as file, pg_ls_dir(t0.dir) as file_ls from ( select '/pg13/pgdata/pg_wal'::text as dir ) t0 ) t1 where 1=1 order by (pg_stat_file(file)).modification desc ) select to_char(date_trunc('day',tf0.last_update_time),'yyyymmdd') as day_id, sum(case when date_part('hour',tf0.last_update_time) >=0 and date_part('hour',tf0.last_update_time) <24 then 1 else 0 end) as wal_num_all, sum(case when date_part('hour',tf0.last_update_time) >=0 and date_part('hour',tf0.last_update_time) <1 then 1 else 0 end) as wal_num_00_01, sum(case when date_part('hour',tf0.last_update_time) >=1 and date_part('hour',tf0.last_update_time) <2 then 1 else 0 end) as wal_num_01_02, sum(case when date_part('hour',tf0.last_update_time) >=2 and date_part('hour',tf0.last_update_time) <3 then 1 else 0 end) as wal_num_02_03, sum(case when date_part('hour',tf0.last_update_time) >=3 and date_part('hour',tf0.last_update_time) <4 then 1 else 0 end) as wal_num_03_04, sum(case when date_part('hour',tf0.last_update_time) >=4 and date_part('hour',tf0.last_update_time) <5 then 1 else 0 end) as wal_num_04_05, sum(case when date_part('hour',tf0.last_update_time) >=5 and date_part('hour',tf0.last_update_time) <6 then 1 else 0 end) as wal_num_05_06, sum(case when date_part('hour',tf0.last_update_time) >=6 and date_part('hour',tf0.last_update_time) <7 then 1 else 0 end) as wal_num_06_07, sum(case when date_part('hour',tf0.last_update_time) >=7 and date_part('hour',tf0.last_update_time) <8 then 1 else 0 end) as wal_num_07_08, sum(case when date_part('hour',tf0.last_update_time) >=8 and date_part('hour',tf0.last_update_time) <9 then 1 else 0 end) as wal_num_08_09, sum(case when date_part('hour',tf0.last_update_time) >=9 and date_part('hour',tf0.last_update_time) <10 then 1 else 0 end) as wal_num_09_10, sum(case when date_part('hour',tf0.last_update_time) >=10 and date_part('hour',tf0.last_update_time) <11 then 1 else 0 end) as wal_num_10_11, sum(case when date_part('hour',tf0.last_update_time) >=11 and date_part('hour',tf0.last_update_time) <12 then 1 else 0 end) as wal_num_11_12, sum(case when date_part('hour',tf0.last_update_time) >=12 and date_part('hour',tf0.last_update_time) <13 then 1 else 0 end) as wal_num_12_13, sum(case when date_part('hour',tf0.last_update_time) >=13 and date_part('hour',tf0.last_update_time) <14 then 1 else 0 end) as wal_num_13_14, sum(case when date_part('hour',tf0.last_update_time) >=14 and date_part('hour',tf0.last_update_time) <15 then 1 else 0 end) as wal_num_14_15, sum(case when date_part('hour',tf0.last_update_time) >=15 and date_part('hour',tf0.last_update_time) <16 then 1 else 0 end) as wal_num_15_16, sum(case when date_part('hour',tf0.last_update_time) >=16 and date_part('hour',tf0.last_update_time) <17 then 1 else 0 end) as wal_num_16_17, sum(case when date_part('hour',tf0.last_update_time) >=17 and date_part('hour',tf0.last_update_time) <18 then 1 else 0 end) as wal_num_17_18, sum(case when date_part('hour',tf0.last_update_time) >=18 and date_part('hour',tf0.last_update_time) <19 then 1 else 0 end) as wal_num_18_19, sum(case when date_part('hour',tf0.last_update_time) >=19 and date_part('hour',tf0.last_update_time) <20 then 1 else 0 end) as wal_num_19_20, sum(case when date_part('hour',tf0.last_update_time) >=20 and date_part('hour',tf0.last_update_time) <21 then 1 else 0 end) as wal_num_20_21, sum(case when date_part('hour',tf0.last_update_time) >=21 and date_part('hour',tf0.last_update_time) <22 then 1 else 0 end) as wal_num_21_22, sum(case when date_part('hour',tf0.last_update_time) >=22 and date_part('hour',tf0.last_update_time) <23 then 1 else 0 end) as wal_num_22_23, sum(case when date_part('hour',tf0.last_update_time) >=23 and date_part('hour',tf0.last_update_time) <24 then 1 else 0 end) as wal_num_23_24 from tmp_file tf0 where 1=1 and tf0.file_ls not in ('archive_status') group by to_char(date_trunc('day',tf0.last_update_time),'yyyymmdd') order by to_char(date_trunc('day',tf0.last_update_time),'yyyymmdd') desc ;