CMU15-445 數據庫系統播客:數據庫系統的內存管理與緩沖池
本次課堂討論的核心是如何在數據庫管理系統(DBMS)中有效地管理內存,特別是如何將磁盤上的數據頁(pages)引入內存以便操作,并使其在用戶看來所有數據都如同在內存中一樣。DBMS 自己管理內存優于操作系統(OS)管理的原因在于,DBMS 擁有關于查詢計劃和數據訪問模式的 語義知識 ,這使得它能夠做出更優化的決策,從而最大限度地減少從磁盤讀取數據導致的停頓。
緩沖池(Buffer Pool)與幀(Frame)
- 幀(Frame)的定義 : 幀 是緩沖池內固定大小的內存塊。當 DBMS 需要一個數據頁時,它會將該頁的精確副本放入緩沖池中的一個幀內。
- DBMS 為何要自己管理幀/內存 : DBMS 會自行分配一大塊內存區域作為 緩沖池 ,并完全由數據庫系統管理這塊內存,而不是依賴于操作系統進行細粒度的內存管理。主要原因是:
超越內存容量的數據庫 :DBMS 能夠支持數據庫大小遠超過物理內存的情況。
最小化磁盤 I/O 影響 :通過精心管理內存,DBMS 可以最大限度地減少因從磁盤讀取數據而導致的查詢停頓或問題。
空間與時間控制 :DBMS 不僅關注數據在磁盤上的物理位置( 空間控制 ),還關注何時將數據頁讀入內存以及何時將其寫回磁盤( 時間控制 )。操作系統無法理解數據頁的上下文和查詢意圖,而 DBMS 可以利用這些信息進行優化。
緩沖池元數據(Meta-data)
緩沖池管理器會維護一些額外的元數據來跟蹤當前在內存中的頁面狀態:
- 頁面表(Page Table) :這是一個 哈希表 ,用于跟蹤當前在內存中的頁面。它將頁面 ID 映射到緩沖池中對應的 幀 ID 或內存地址。頁面表是臨時的內存結構,不需要持久化到磁盤。
- 臟頁標志(Dirty Flag) :這是一個 單比特標志 ,指示自頁面從磁盤讀入緩沖池以來是否已被修改。如果頁面被修改,它必須在被驅逐回磁盤前安全地寫回。
- 引用計數(Pin Count / Reference Counter) :這個計數器跟蹤當前有多少個線程或查詢正在使用(讀取或修改)該頁面。如果一個頁面的引用計數大于零,則緩沖池管理器 不允許 將其從內存中驅逐出去。
并發控制:鎖(Locks)與栓鎖(Latches)
理解鎖和栓鎖的區別對于并發控制至關重要。
- 鎖(Locks)
保護對象 :保護數據庫的 邏輯內容 ,如元組(tuples)、表(tables)、整個數據庫。
持有時間 :通常由事務持有,持續整個事務的執行期間,可能長達數毫秒、數秒,甚至數分鐘或數小時。
回滾需求 :需要支持 回滾 操作,以防事務失敗。
- 栓鎖(Latches)
保護對象 :保護 DBMS 內部數據結構的 關鍵區域 ,例如緩沖池中的頁面表條目或物理數據結構。
持有時間 :持有時間極短,僅限于單個操作的持續時間。一旦操作完成,栓鎖就會立即釋放。
回滾需求 :通常 不需要 回滾,因為它們是用于保護內部物理數據結構,而非邏輯數據。
實現 :在操作系統層面,栓鎖通常通過 互斥量(mutexes) 實現。
緩沖池優化手段
為了提高性能,DBMS 采用多種優化策略:
- 多緩沖池(Multiple Buffer Pools) DBMS 可以擁有多個緩沖池實例,而不是一個單一的全局緩沖池。這樣做有幾個好處:
減少栓鎖競爭 :不同的線程可以訪問不同的緩沖池,減少了對單一頁面表栓鎖的爭用,從而提高并發性能和可伸縮性。
改善局部性 :可以針對不同類型的數據(例如,為索引單獨設置一個緩沖池,為表數據設置另一個)或訪問模式(例如,點查詢與順序掃描)定制不同的 替換策略 。
- 預取(Pre-fetching) : DBMS 可以根據查詢計劃 預先讀取 頁面,將它們從磁盤加載到緩沖池中。這可以減少查詢因等待磁盤 I/O 而導致的停頓。
順序掃描 :對于順序掃描,DBMS 可以輕松預測接下來將訪問的頁面。
索引掃描 :對于索引掃描,DBMS 能夠理解索引結構(例如 B+ 樹)并提前預測需要跳躍讀取哪些非連續的頁面,這是操作系統無法做到的。
- 掃描共享(Scan Sharing) :多個查詢可以 共享 同一個數據掃描游標,從而復用從磁盤讀取的數據頁。
這與 結果緩存 (result caching,緩存查詢結果)不同,掃描共享發生在更低的存儲層級。
查詢不一定需要完全相同,只要它們掃描相同的頁面,就可以共享。
這種技術可以有效避免 顛簸(thrashing) ,即因一個查詢驅逐了另一個查詢很快又需要的頁面而導致反復的磁盤 I/O。
例如,DB2 和 SQL Server 完全支持此功能,而 Oracle 的游標共享(cursor sharing)只對完全相同的查詢有效。
- 緩沖池旁路(Buffer Pool Bypass) :某些查詢操作(特別是順序掃描)可以選擇 不將讀取的頁面放入主緩沖池 ,而是將它們放入一個 查詢本地的臨時內存區域 。
這有助于避免 污染(pollute) 緩沖池中的“熱點”數據,即那些可能在短期內不再需要的頁面。
適用于需要讀取大量連續頁面或處理臨時數據(如排序、連接)的操作。
減少了查詢頁面表和獲取栓鎖的開銷。
頁面替換策略(Replacement Policies)
當緩沖池滿且需要為新頁面騰出空間時,DBMS 必須決定驅逐哪個頁面。目標是正確性、準確性、速度和元數據開銷。
- 最近最少使用(Least Recently Used, LRU)
跟蹤每個頁面最后一次訪問的時間戳,驅逐時間戳最老的頁面。
可以通過維護一個按時間戳排序的隊列來加速查找。
- Clock 算法
LRU 的一種 近似算法 ,不需要為每個頁面精確跟蹤時間戳。
每個頁面有一個 引用位(reference bit) 。當頁面被訪問時,其引用位設為 1。
頁面被組織成一個 環形緩沖區 ,有一個“時鐘指針”掃過這些頁面。
當指針掃到一個頁面時:如果引用位為 1,則將其設為 0 并跳過;如果引用位為 0,則說明該頁面自上次檢查以來未被訪問,可以被驅逐。
- 順序洪泛(Sequential Flooding)問題 : LRU 和 Clock 算法都容易受到 順序洪泛 的影響。當一個查詢執行順序掃描并讀取大量頁面時,這些頁面都會被標記為“最近使用”,從而導致緩沖池中真正有用的(但暫時未被訪問的)熱點頁面被驅逐出去。
- 更優的替換策略
LRU-K :跟蹤頁面過去 K 次 訪問的歷史時間戳,并計算訪問間隔,以更好地預測頁面下次被訪問的時間。這使得驅逐決策更準確。
本地化(Localization) :DBMS 可以根據每個事務或查詢的訪問模式來決定驅逐哪些頁面,從而減少對全局緩沖池的污染。例如,Postgres 維護一個小的 環形緩沖區 作為查詢的私有緩存。
優先級提示(Priority Hints) :DBMS 利用其對查詢執行上下文的了解,向緩沖池提供提示,指出哪些頁面是重要的(例如,索引的根頁、頻繁寫入的頁),應盡量保留在內存中。
臟頁(Dirty Pages)處理與后臺寫入
- 臟頁處理
如果緩沖池中的頁面 未被修改 (干凈頁),DBMS 可以直接“丟棄”它,并將其幀用于新頁面,這是 最快 的。
如果頁面 已修改 (臟頁),則在驅逐之前,必須將其安全地寫回磁盤以確保數據持久性,這會比較 慢 ,因為它涉及到額外的磁盤 I/O。
替換策略需要在“快速驅逐干凈頁”和“支付寫入臟頁的成本以保留未來可能需要的干凈頁”之間做出權衡。
- 后臺寫入(Background Writing)
為了避免在需要空閑空間時才被迫寫入臟頁,DBMS 可以定期運行一個后臺線程,掃描緩沖池并主動將臟頁寫入磁盤。一旦臟頁安全寫入,就可以將其標記為干凈,從而在需要驅逐時有更多干凈的頁面可供選擇。
重要限制 :后臺寫入必須確保在寫入臟頁之前,與該修改相關的 日志記錄 已經安全地寫入了磁盤。這是為了確保崩潰恢復時的正確性。
PostgresQL 與操作系統頁面緩存的利用
- 操作系統頁面緩存(OS Page Cache) :默認情況下,操作系統會維護自己的文件系統緩存(OS Page Cache)。當程序從磁盤讀取文件時,OS 會在自己的緩存中保留一份副本。
- 大多數 DBMS 的做法
大多數主流的數據庫系統(如 Oracle, MySQL, SQL Server, DB2, Sybase)都會使用 O_DIRECT(Direct I/O)等 POSIX 標志, 繞過 操作系統的頁面緩存。它們自己管理所有的內存和緩存。
避免 冗余數據副本 (數據在 OS 緩存和 DBMS 緩沖池中都有),節省內存。
確保 一致的性能 和行為,因為不同操作系統的緩存策略可能不同。
對 寫入操作有完全控制 ,確保數據以正確的順序持久化(如先寫日志再寫數據),OS 的緩存可能無法提供這種保證。
- PostgresQL 的做法
Postgres 是一個顯著的例外,它是少數 依賴于 操作系統頁面緩存的主流數據庫系統。
Postgres 的開發者認為這可以減少 工程開銷 ,因為他們不需要像其他 DBMS 那樣管理一個巨大的緩沖池,可以將部分緩存管理的工作交給 OS。
雖然會帶來一些細微的性能損失,但他們認為是可以接受的。
在演示中,即使重啟 Postgres 清空了其自身的緩沖池,但如果 OS 頁面緩存中仍有數據,查詢執行時間仍然會比完全從磁盤讀取快得多,這證明了 OS 頁面緩存對 Postgres 的性能影響。然而,為了達到最佳性能(如 700 毫秒),仍然需要將整個表(345MB)加載到 Postgres 自身的共享緩沖區(shared buffers,Postgres 的緩沖池)中(從 128MB 增加到 360MB),并執行預熱操作(PG Warm 擴展),才能實現幾乎 100% 的緩存命中。





































