在當今相互依賴且高度專業化的時代,開源專案之間存在著多方面的相互依賴,這並不令人意外。較高層級的專案依賴於較低層級專案提供的功能,同時也推動著其發展並解決特定專案的問題。然而,這些相互依賴的專案之間若存在隔閡,將難以克服,延遲新功能的採用,固守過時的決策,並普遍降低使用體驗,這已不是什麼新鮮事。
在理想世界中,這不成問題——不同社群的人們會密切關注彼此的動態,正確理解其動機並如何應用這些知識。現實情況則有些混亂:我們不總是有足夠的精力去關注所有有趣且重要的專案,有時不同社群的人們幾乎像是說著不同的語言。
HeptapoDB 的人正試圖與 BernderOS 的開發者溝通。
這正是我寫這篇部落格文章的目的。我將試圖檢視近期的 Linux 核心變更,找出那些對 PostgreSQL 這類資料庫聽起來很有趣的部分。這是一個非常初步的概述,經過更仔細的檢查後,我可能會發現自己錯了,但我仍然希望這能為未來的補丁和討論提供一些啟發。
到目前為止,我一直在談論「對 PostgreSQL 有趣的近期變更」。這需要一些解釋:什麼是「近期」和「有趣」?在本部落格文章的目的下:
「近期」指的是過去幾年。例如,我故意沒有包含關於共享記憶體或 memfd_create 的理解方面的變更,我曾在線上共享記憶體重設大小補丁 [1] 中實驗過這些——那些相對較舊了,只是我們還沒收到通知。
「有趣」意味著該變更明顯開闢了新的可能性,提高了效能,或者該變更是明確針對資料庫的,並且人們認為它應該有所幫助。
我特意忽略了另一個重要的討論面向是可移植性。PostgreSQL 社群以其在不同作業系統和發行版之間盡可能保持可移植性的努力而聞名。這可能導致一種情況,即一個閃亮的新功能如果不夠可移植,並且以特殊情況處理它會帶來問題,那麼它就無法真正被採用。
話雖如此,讓我們直接開始吧。
為了解釋什麼是非快取緩衝 I/O,我們不必深入研究,只需閱讀這個提交訊息 [2]:
主要思想是,正常的緩衝 I/O 有時會不可預測,並在回收記憶體時消耗大量資源。這並非總是如此,但同時很容易陷入這種情況。為了解決這個問題,該補丁引入了一個名為 RWF_DONTCACHE 的新旗標:
指定該旗標後,核心會執行以下操作:
讀寫使用頁面快取,讀取甚至像正常的緩衝讀取一樣工作,沒有什麼新鮮的。
寫操作在操作結束時觸發回寫,並且在此意義上不是同步的。
IO 之外的 Folios 不會被觸及。
非快取 I/O 利用頁面快取來獲取最新資料,但不會在快取中留下任何東西。
很難相信這會是一個合理的做法,所以我準備了一個小測試案例。下圖顯示了在記憶體非常有限的 VM 上進行的兩次 FIO 大於記憶體的載入的 IOPS,這會導致嚴重的回收壓力。FIO 配置的唯一區別在於 I/O 是快取的還是非快取的。
在記憶體很少的 VM 上進行兩次 FIO 隨機讀寫大於記憶體的載入。
正如你所見,一旦快取填滿,快取載入的效能就會下降到非快取載入之下,並且通常保持更高的變動性。
這個我最早是從一位同事那裡聽說的。這項變更似乎是由核心開發者特別為資料庫用例開發的,並引起了從「還好」到「資料庫問題已解決」的各種反應。它實際上不是單一的提交,而是一系列跨不同子系統的變更,但為了有一個起點,現在讓我們看看這個 [3]:
總體思想很簡單——許多現代 NVMe 和 SCSI 裝置實際上支援原子寫入 [4],但有一個條件:寫入必須滿足某些條件:它們必須與區塊大小對齊,並且不超過硬體廣告的最大原子寫入單元大小。為了完成閉環,引入了一個新旗標,它告訴核心強制執行這些要求,並且不要分割標記為「原子」的 I/O 操作:
最初只支援單區塊寫入,後續擴展到多區塊情況,例如通過 XFS 的軟體方法 [5] 或 EXT4 的 bigalloc 功能 [6]。
原子寫入作為一項功能,首先針對的是資料庫,它們在寫出資料時必須進行某種雙重寫入以實現原子性。如果我們在 PostgreSQL 中搜尋「torn pages」(損壞頁面),我們會發現 19 個出現的警告,以一種或另一種方式關於損壞寫入的危害。特別突出的功能當然是全頁映像 (FPI) [7],它將頁面的全部內容放入 WAL,防止部分寫入。不幸的是,有一個問題——原子寫入支援目前需要直接 I/O,並且尚不清楚它是否會為緩衝 I/O 實現。
我花了一些時間試圖想出如何演示這項功能,或者更確切地說,如何故意重現損壞寫入。幸運的是,QEMU 支援帶有原子操作的 NVMe 模擬 [8],但我設法讓 FIO 的驗證模式工作。所以相反,我只是通過在 gdb 中操作 I/O 狀態來強制進行 bio 分割:
另一個變更是稍微舊一點的 [9]:
這個新的系統呼叫本質上是比 mincore 更具擴展性的查詢頁面快取狀態的方法。從使用角度來看,它看起來像這樣:
聲稱的用例之一再次是資料庫,因為了解頁面快取的狀態對於預測某個操作將產生多少 I/O 是顯然很重要的。有趣的是,PostgreSQL CommitFest 中已經有一個關於此系統呼叫的項目 [10]:
關閉了關於 cachestat 系統呼叫的 CF 項目。
正如你所見,最終它被退回並附帶了回饋,主要是因為建議的用法是當快取已滿時停止預取資料。這種方法是否會帶來效能提升並不顯而易見,因此討論就此不了了之。但不要氣餒,這不是 cachestat 的唯一用例!
在 PostgreSQL 的 index_pages_fetched 函數中,我們發現了以下註解:
注意 b ,它代表可用緩衝頁的數量。在引用的文章中,這個變數也包括了頁面快取,而這目前 PostgreSQL 並不知道。因此,它基於 effective_cache_size 計算,後者被稱為影響索引掃描選擇順序掃描的頻率的旋鈕:
但顯然,一個配置旋鈕是一個次優的解決方案,為更準確地估計這個值留下了空間,更重要的是,能夠從 PostgreSQL 已知的資訊和頁面快取的狀態中即時估計這個值。
你可能已經聽說過 BPF——它被用於許多領域,包括網路、監控和安全。所有這些都與資料庫有些無關,但一個新的應用領域正變得越來越受歡迎,即通過 bpf struct ops 自訂 Linux 核心行為。最近的一個例子是自訂排程器類 sched_ext [11],它基本上允許創建自訂的 Linux 排程器:
這對資料庫來說已經更有趣了:人們可以想像一個排程器,它不僅基於系統狀態做出決策,還基於資料庫特定的優先級。例如,可以引入一個優先級分派佇列來偏好某些類型的活動,這些活動是資料庫目前認為重要的(處理某些關鍵查詢),並降低某些背景工作的優先級。或者反過來,如果我們重視穩定性和健壯性,我們就給背景維護工作更高的優先級,同時讓其他所有工作稍微等待更長時間。
sched_ext 最終進入了核心。但還有更多令人興奮的補丁目前正在開發中。其中一個是 cache_ext [12],這是一種用於自訂頁面快取逐出策略的 struct ops 方法。
與 sched_ext 相反,cache_ext 明確針對資料庫,以改善典型的案例,即資料庫主要服務 OLTP 工作負載,但由於幾個大型分析查詢導致頁面快取被沖刷。
但等等,還有更多!這個補丁 [13] 試圖通過 BPF 使 io_uring 可自訂,承諾更好的等待和輪詢模式:
最後但同樣重要的是,提案 [14] 是關於使 OOM Killer 也可自訂,這對記憶體密集型的資料庫可能非常有益:
讓我們等待並希望至少其中一些能進入核心。
正如我們在整篇部落格文章中所見,有許多令人興奮的事情可以在 PostgreSQL 中嘗試或期待並提供回饋。概述有點膚淺,但我希望它足以引發一些討論並觸發一兩個實驗。