PostgreSQL checksum
在計算機系統中,checksum 通常用于校驗數據在傳輸或存取過程中是否發生錯誤。PostgreSQL 從 9.3 開始支持 checksum,以發現數據因磁盤、 I/O 損壞等原因造成的數據異常。本文介紹 PostgreSQL 中 checksum 的使用及其實現原理。
概述
PostgreSQL 從 9.3 開始支持數據頁的 checksum,可以在執行 initdb 時指定 -k 或 --data-checksums 參數開啟 checksum,但開啟 checksum 可能會對系統性能有一定影響,官網描述如下:
Use checksums on data pages to help detect corruption by the I/O system that would otherwise be silent. Enabling checksums may incur a noticeable performance penalty. This option can only be set during initialization, and cannot be changed later. If set, checksums are calculated for all objects, in all databases.
啟用 checksum 后,系統會對每個數據頁計算 checksum,從存儲讀取數據時如果檢測 checksum 失敗,則會發生錯誤并終止當前正在執行的事務,該功能使得 PostgreSQL 自身擁有了檢測 I/O 或硬件錯誤的能力。
Checksum 引入一個 GUC 參數 ignore_checksum_failure,該參數若設置為 true,checksum 校驗失敗后不會產生錯誤,而是給客戶端發送一個警告。當然,checksum 失敗意味著磁盤上的數據已經損壞,忽略此類錯誤可能導致數據損壞擴散甚至導致系統奔潰,此時宜盡早修復,因此,若開啟 checksum,該參數建議設置為 false。
實現原理
設置 checksum
數據頁的 checksum 在從 Buffer pool 刷到存儲時才設置,當頁面再此讀取至 Buffer pool 時進行檢測。
PostgreSQL 中 Buffer 刷盤的邏輯集中在 FlushBuffer 中,其中設置 checksum 的邏輯如下:
/** Update page checksum if desired. Since we have only shared lock on the* buffer, other processes might be updating hint bits in it, so we must* copy the page to private storage if we do checksumming.*/ bufToWrite = PageSetChecksumCopy((Page) bufBlock, buf->tag.blockNum);比較有意思的是,其他進程可能會在只加 content-lock 共享鎖的情況下并發修改 page 的 Hint Bits,從而導致 checksum 值發生變化,為確保 page 的內容及其 checksum 保持一致,PostgreSQL 采用了 先復制頁,然后計算 checksum 的方式,如下:
/** We allocate the copy space once and use it over on each subsequent* call. The point of palloc'ing here, rather than having a static char* array, is first to ensure adequate alignment for the checksumming code* and second to avoid wasting space in processes that never call this.*/ if (pageCopy == NULL)pageCopy = MemoryContextAlloc(TopMemoryContext, BLCKSZ);memcpy(pageCopy, (char *) page, BLCKSZ); ((PageHeader) pageCopy)->pd_checksum = pg_checksum_page(pageCopy, blkno);即先將數據頁的內容拷貝一份,拷貝的數據自然不會被其他進程修改,然后基于該拷貝頁計算并設置 checksum 值。
checksum 算法
數據頁的 checksum 算法基于 FNV-1a hash 改造而來,其結果為 32 位無符號整型。由于 PageHeaderData 中 pd_checksum 是 16 位無符號整型,因此將其截取 16 位作為數據頁的 checksum 值,如下:
/** Save pd_checksum and temporarily set it to zero, so that the checksum* calculation isn't affected by the old checksum stored on the page.* Restore it after, because actually updating the checksum is NOT part of* the API of this function.*/ save_checksum = cpage->phdr.pd_checksum; cpage->phdr.pd_checksum = 0; checksum = pg_checksum_block(cpage); cpage->phdr.pd_checksum = save_checksum;/* Mix in the block number to detect transposed pages */ checksum ^= blkno;/** Reduce to a uint16 (to fit in the pd_checksum field) with an offset of* one. That avoids checksums of zero, which seems like a good idea.*/ return (checksum % 65535) + 1;pg_checksum_block 函數計算數據頁的 32 位 checksum 值,具體算法可以參考源碼,在此不詳述。
檢測 checksum
PostgreSQL 會在頁面從存儲讀入內存時檢測其是否可用,調用函數為 PageIsVerified,該函數不僅會檢測正常初始化過的頁(non-zero page),還會檢測 全零頁(all-zero page)。
為什么會出現 全零頁 呢?
在特定場景下表中可能出現 全零頁,比如有進程擴展了一個表,即在該表中添加了一個新頁,但在 WAL 日志寫入存儲之前,進程崩潰了。此時新加的頁可能已經在表文件中,下次重啟時就會讀取到。
對于 non-zero page,檢測其 checksum 是否一致以及 page header 信息是否正確,若 checksum 失敗,但 header 信息正確,此時會根據 ignore_checksum_failure 值判斷驗證是否通過;對于 all-zero page,如果為全零,則驗證通過。
若驗證失敗,兩種處理方式:
- 若讀取數據的模式為 RBM_ZERO_ON_ERROR 且 GUC 參數 zero_damaged_pages 為 true,則將該頁全部置 0
- 報錯,invalid page
checksum 與 Hint bits
數據頁寫至存儲時,如果寫失敗,可能會導致破碎的頁(torn page),PostgreSQL 通過 full_page_writes 特性解決此類寫失敗導致數據不可用的問題。
Hint Bits 是數據頁中用于標識事務狀態的標記位,一般情況下,作為提示位,不是很重要。但如果使用了 checksum,Hint Bits 的變化會導致 checksum 值發生改變。設想如果一個頁面發生部分寫,恰好把某些 Hint Bits 寫錯,此頁面可能并不影響正常使用,但 checksum 會拋出異常,此時應如何恢復呢?
在 checksum 的實現中,checkpoint 后,如果頁面因更新 Hint Bits 第一次被標記為 dirty,需要記錄一個 Full Page Image 至 WAL 日志中,以應對以上提到的因 Hint Bits 更新丟失導致 checksum 失敗的問題,具體實現可參考 MarkBufferDirtyHint。對于已經是 dirty 的頁,更新 Hint Bits 則不需要記錄 WAL 日志,因為在 checkpoint 后,第一次將該頁標記為 dirty 時已經寫入了對應的 Full Page Image。
可見,在啟用 checksum 的情況下,checkpoint 后頁面的第一次修改如果是更新 Hint Bits, 會寫 Full Page Image 至 WAL 日志,這會導致 WAL 日志占用更多的存儲空間。
關于 PostgreSQL checksum 和 Full Page Image 的關系,可以參考 stackoverflow 上這個問題。
查看 checksum
PostgreSQL 10 在 pageinspect 插件中添加了函數 page_checksum() 用來查看 page 的 checksum,當然使用 page_header() 也可以查看 page 的 checksum,如下:
postgres=# SELECT page_checksum(get_raw_page('pg_class', 0), 0);page_checksum ---------------17448 (1 row)postgres=# SELECT * FROM page_header(get_raw_page('pg_class', 0));lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid ------------+----------+-------+-------+-------+---------+----------+---------+-----------0/78A1E918 | 17448 | 0 | 200 | 368 | 8192 | 8192 | 4 | 0 (1 row)總結
Checksum 使 PostgreSQL 具備檢測因硬件故障或傳輸導致數據不一致的能力,一旦發生異常,通常會報錯并終止當前事務,用戶可以盡早察覺數據異常并予以恢復。當然,開啟 checksum 也會引入一些開銷,體現在兩個方面:
- 計算數據頁的 checksum 會引入一些 CPU 開銷,具體開銷取決于 checksum 算法的效率
- checkpoint 后,若因更新 Hint Bits 將頁面第一次置為 dirty 會寫一條記錄 Full Page Image 的 WAL 日志,以用于恢復因更新 Hint Bits 產生的破碎頁。
對于數據可用性要求較高的場景,通常建議將 full_page_writes 和 checksum 都打開,前者用于避免寫失敗導致的數據缺失,后者用于盡早發現因硬件或傳輸導致數據不一致的場景,一旦發現,可以利用 full_page_writes 和 checksum 記錄在 WAL 日志中的 Full Page Image 進行數據恢復。
References
- https://paquier.xyz/postgresql-2/postgres-9-3-feature-highlight-data-checksums/
- https://en.wikipedia.org/wiki/Checksum
- https://www.postgresql.org/docs/11/pgverifychecksums.html
總結
以上是生活随笔為你收集整理的PostgreSQL checksum的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 带标签的PHM2009齿轮箱数据集
- 下一篇: JS内置DATE对象部分函数对日期的支持