[譯] 8. PG緩沖區管理器(Buffer Manager)


[譯] 8. 緩沖區管理器(Buffer Manager)

原文地址:https://www.interdb.jp/pg/pgsql02.html

原文作者:Hironobu SUZUKI

緩沖管理器管理共享內存和持久存儲之間的數據傳輸,並且可以對 DBMS 的性能產生重大影響。 PostgreSQL 緩沖區管理器的工作效率很高。

在本章中,將描述 PostgreSQL 緩沖區管理器。第一部分提供其概述,后續部分描述以下主題:

  • 緩沖管理器結構
  • 緩沖管理器鎖
  • 緩沖管理器工作原理
  • 環形緩沖區(Ring Buffer)
  • 刷新臟頁

Fig. 8.1. Relations between buffer manager, storage, and backend processes.

Fig. 8.1. Relations between buffer manager, storage, and backend processes.

8.1 概述

本節介紹了便於后續章節所需的關鍵概念。

8.1.1 緩沖管理器結構

PostgreSQL 緩沖管理器包含在下一節中介紹的緩沖表,緩沖區描述符和緩沖池。緩沖池存儲了數據文件頁面,如表和索引,以及可用空間映射和可見性映射。它是一個數組,即每個槽存儲一個數據文件的一頁。緩沖池數組的索引(下標)稱為buffer_id

8.2 和 8.3 節描述了緩沖區管理器內部的細節。

8.1.2 緩沖區標簽(Buffer Tag)

在PostgreSQL中,所有數據文件的每一頁都可以分配一個唯一的標簽,即緩沖區標簽。當緩沖區管理器收到請求時,PostgreSQL 使用所需頁面的 buffer_tag。

buffer_tag由三個值組成:RelFileNode及其頁面所屬關系(表)的fork編號,以及其頁面的塊編號。表、可用空間映射和可見性映射的fork編號分別定義為 0、1 和 2。

例如,緩沖區標簽'{(16821, 16384, 37721), 0, 7}' 標識了表的第7塊block的頁面,其relation OID 為 37721,fork 編號是0。該 relation 屬於 OID 為16821的數據庫中,位於 OID 為 16384 的表空間下。類似的,緩沖區標簽 '{(16821, 16384, 37721), 1, 3}' 標識可用空間映射的第3塊的頁面的OID 是 37721,fork 編號為1。

緩沖區標簽格式:

8.1.3 后端進程如何讀取頁面

本小節描述了后端進程如何從緩沖區管理器中讀取頁面(圖 8.2)。

Fig. 8.2. How a backend reads a page from the buffer manager.

Fig. 8.2. How a backend reads a page from the buffer manager.

(1) 讀取表或索引頁時,后端進程向緩沖區管理器發送包含該頁的 buffer_tag 的請求。

(2) 緩沖區管理器返回存儲請求頁的槽的buffer_ID。如果請求的頁面未存儲在緩沖池中,則緩沖管理器將頁面從持久存儲加載到緩沖池其中的一個槽上,然后返回 該槽的buffer_ID。

(3) 后端進程訪問buffer_ID的槽(slot)(讀取想要的頁面)。

當后端進程修改緩沖池中的頁面時(例如,通過插入元組),尚未刷新到存儲的修改頁面稱為臟頁面。

8.4 節描述了緩沖區管理器是如何工作的。

8.1.4 頁面替換算法

當所有緩沖池槽都被占用但請求的頁面沒有被存儲時,緩沖管理器必須在緩沖池中選擇一頁將被請求的頁面替換。通常,在計算機科學領域,頁面選擇算法被稱為頁面替換算法,被選中的頁面被稱為"受害者頁面(victim page)"。

自計算機科學出現以來,頁面替換算法的研究一直在前進;從而,之前已經提出了許多替換算法。從 PostgreSQL 8.1 版本開始使用時鍾掃描(clock sweep)算法,因為它比以前版本中使用的 LRU 算法更簡單、更高效。

8.4.4 節描述了時鍾掃描的細節。

8.1.5 刷新臟頁

臟頁最終應該被刷新到存儲中;但是,緩沖區管理器需要幫助才能執行此任務。在 PostgreSQL 中,有兩個后台進程( checkpointer 和 background writer )幫助完成這個任務。

8.6 節描述了檢查點進程(checkpointer )和 后台寫進程(background writer)。

Direct I/O

PostgreSQL 不支持直接 I/O,盡管有時已經討論過了。如果您想了解更多詳細信息,請參閱有關 pgsql-ML 的討論論文

8.2 緩沖區管理器結構

PostgreSQL 緩沖區管理器包括三層,即緩沖區表、緩沖區描述符和緩沖池(圖 8.3):

Fig. 8.3. Buffer manager's three-layer structure.

Fig. 8.3. Buffer manager's three-layer structure.

  • buffer pool:緩沖池是一個數組。每個槽存儲一個數據文件頁。數組槽的索引稱為 buffer_ids。

  • buffer descriptors:緩沖區描述符層是一個緩沖區描述符數組。每個描述符與緩沖池槽一一對應,並在對應槽中保存存儲頁面的元數據。

    請注意,為方便起見,采用了術語“緩沖區描述符層”,並且僅在本文檔中使用。

  • buffer table:緩沖區表是一個哈希表,它存儲了存放頁面的buffer_tag和保存了存放各自元數據描述符的buffer_id之間的關系。

下面小節中將會介紹這些層的詳細內容。

8.2.1 緩沖區表(buffer table)

緩沖區表在邏輯上可以分為三部分:哈希函數(hash function)、哈希桶槽(hash bucket slots)和數據條目(data entries)(圖 8.4)。

Fig. 8.4. Buffer table.

內建的哈希函數將buffer_tag 映射到哈希桶槽。即使哈希桶槽的數量大於緩沖池槽的數量,可能發生(哈希)碰撞。哈希區表使用單獨的鏈接列表的方法解決沖突問題。當數據條目映射到同一個桶槽時,該方法將條目存儲在同一個鏈表中,如圖8.4所示。

數據條目包含兩個值:頁面的 buffer_tag 和保存頁面元數據的描述符的 buffer_id。例如,數據條目‘Tag_A, id=1’意味着buffer_id為1的緩沖區描述符存儲了帶有Tag_A標簽的頁面的元數據。

hash function(哈希函數)

哈希函數是 calc_bucket() 和 hash() 的復合函數。以下是其偽代碼的表示。

uint32 bucket_slot = calc_bucket(unsigned hash(BufferTag buffer_tag), uint32 bucket_size)

請注意,這里未介紹基本操作(數據條目的查找、插入和刪除)。這些是非常常見的操作,將在以下部分中進行說明。

8.2.2 緩沖區描述符(Buffer Desciptor)

本小節介紹緩沖區描述符的結構,下一小節介紹緩沖區描述符層。

緩沖區描述符保存了對應緩沖區池槽存儲的頁面的元數據。它由 [BufferDesc](javascript:void(0))結構定義。該結構由多個字段,下面顯示一些主要的列:

  • tag: 保存了存儲緩沖池槽對應頁的 buffer_tag 信息(緩沖區標簽在第8.1.2節定義)

  • buffer_id:標識描述符(相當於對應緩沖池槽的buffer_id)

  • refcount:保存當前正在訪問相關存儲頁的PostgreSQL進程數量,也稱為pin count。當PostgreSQL訪問存儲頁時,refcount必須增加1(refcount++),訪問過后,refcount 必須減1(refcout--)

    當 refcount 為0時,即當前未訪問相關的存儲頁時,該頁面為unpinned,否則是pinned

  • usage_count:保存相關頁面自加載到相應緩沖池槽以來被訪問次數。請注意,usage_count用於頁面替換算法(第8.4.4節)

  • context_lock 和 io_in_progress_lock:用於控制訪問相關存儲頁面的輕量級鎖。它在第8.3.2節介紹說明

  • flags:保存存儲頁面的幾種狀態。下面顯示了主要的狀態值

    • dirty bit:標識存儲的頁面是否為臟頁
    • valid bit:標識存儲頁面能否讀或寫(valid)。例如:如果該標識位的值為'valid',則對應的緩沖池槽存儲了一個頁面並且描述符(valid bit)保存了該頁的元數據;因此,可以讀取或寫入存儲的頁面。如果值為'invalid',則該描述符未保存任何元數據;這意味着存儲的頁面無法讀取或寫入,或者緩沖區管理器正在替換存儲的頁面
    • io_in_progress bit:表示緩沖區管理器正在讀取/寫入相關存儲頁面。換句話說,它表示單個進程是否持有該描述符的io_in_progress_lock
  • freeNext:指向下一個描述符的指針以生成一個 freelist。將在下一小節中介紹

為了簡化以下描述,定義了三個描述符狀態:

  • Empty:當對應的緩沖池槽未存儲頁面時(即 refcount 和 usage_count都為0),描述符的狀態為empty
  • Pinned:當對應的緩沖池槽存儲了一個頁面且PostgreSQL進程可訪問該頁面(即refcount 和 usage_count 的值都大於或等於1),該緩沖區描述符的狀態為 pinned
  • Unpinned:當對應的緩沖池槽存儲了一個頁面但PostgreSQL進程不可訪問該頁面(即 usage_count 的值都大於或等於1,但refcount值為0),該緩沖區描述符的狀態為 unpinned

每個描述符將具有上述狀態之一。描述符狀態相對於特定條件發生變化而改變,這將在下一小節中描述。

在下圖中,緩沖區描述符的狀態由彩色框表示。

image-20220323213717912

此外,臟頁表示為“X”。例如,未固定的臟描述符由image-20220323214030148表示。

8.2.3 緩沖區描述符層(Buffer Descriptors Layer)

緩沖區描述符的集合形成一個數組。在本文檔中,該數組被稱為緩沖區描述符層。

PostgreSQL server啟動時,所有緩沖區描述符的狀態為empty。在PostgreSQL 中,這些描述符構成一個鏈接列表稱為freelist(圖8.5)。

Fig. 8.5. Buffer manager initial state.

Fig. 8.5. Buffer manager initial state.

請注意,PostgreSQL中的 freelist 和 Oracle 中的 freelist 完全不同的概念。PostgreSQL 中的 freelist 只是empty 緩沖區描述的鏈表。在 5.3.4 節中描述的PostgreSQL 可用空間映射(VM) 與 Oracle中的 freelist 作用相同。

圖 8.6 顯示了第一個頁面是如何加載的。

(1) 從 freelist 的頂部回收一個empty緩沖區描述符並將其固定(pin)。(即將refcount 和 usage_count 增加 1)

(2) 在緩沖區表(哈希表)中插入新條目,它保存了第一頁的標簽和上一步回收的描述符的buffer_id之間的關系

(3) 將新頁面從存儲加載到對應的緩沖池槽

(4) 將新頁面的元數據保存到之前回收的描述符中。

Fig. 8.6. Loading the first page.

Fig. 8.6. Loading the first page.

從freelist中回收的描述符總是包含頁面的元數據。換句話說,non-empty 描述符可以繼續被使用而不會返回到freelist中。但是,當發生以下情形之一時,描述符的狀態變為empty並且重新添加到freelist:

  1. 表或索引已被刪除
  2. 數據庫已被刪除
  3. 表或索引已使用vacuum full命令清理

為什么empty描述符組成freelist?

為了立刻獲取第一個描述符。這是動態內存資源分配的常用手段。請參閱文檔

緩沖區描述符層包含一個無符號的 32 位整數變量,即 nextVictimBuffer。該變量用於第 8.4.4 節中描述的頁面替換算法。

8.2.4 緩沖池(Buffer Pool)

緩沖池是一個簡單數組,用於存儲數據文件頁面,如表和索引。緩沖池數組的下標稱為 buffer_id

緩沖池槽的大小為8KB,和頁面的大小相等。因此,每一個槽能夠存儲整個頁面。

8.3 緩沖區管理器鎖

緩沖區管理器為不同的目的使用不同的鎖。本節描述了后續章節中所需鎖的說明。

請注意,本節中描述的鎖是緩沖區管理器同步機制的一部分;它們與任何 SQL 語句和 SQL 選項無關。

8.3.1 緩沖區表鎖(Buffer Table Locks)

BufMappingLock 保護整個緩沖區表的數據完整性。它是一種可用於共享(shared )模式和獨占(exclusive)模式的輕量級鎖。在緩沖區表中檢索條目時,該后端進程便持有了一個共享的BufMappingLock。在插入或更新條目時,后端進程持有一個獨占鎖。

將 BufMappingLock 拆分為分區以減少緩沖表中的爭用(默認128個分區)。每個BufMappingLock 分區保護部分對應的哈希桶槽(hash bucket slot)。

圖8.7 展示了一個典型的拆分 BufMappingLock 效果的例子。兩個后端進程可以同時以獨占模式持有各自的 BufMappingLock 分區,以便插入新的數據條目。如果 BufMappingLock 是單個系統范圍(system-wide)的鎖,則兩個進程都應等待另一個進程的處理,具體取決於哪個進程先開始處理。

Fig. 8.7. Two processes simultaneously acquire the respective partitions of BufMappingLock in exclusive mode to insert new data entries.

Fig. 8.7. Two processes simultaneously acquire the respective partitions of BufMappingLock in exclusive mode to insert new data entries.

緩沖表需要許多其他鎖。例如,緩沖區表在內部使用自旋鎖(spin lock)來刪除條目。但是,本文省略了對這些鎖的描述,因為后續未涉及到它們。

在 9.4 版本之前,BufMappingLock 默認被拆分為 16 個單獨的鎖。

8.3.2 每個緩沖區描述符的鎖

每個緩沖區描述符使用兩個輕量級鎖 content_lock 和 io_in_progress_lock 來控制對相應緩沖池槽中存儲頁面的訪問。當檢查或更改自己字段的值時,使用自旋鎖。

8.3.2.1 content_lock

content_lock 是一個典型的強制性訪問限制的鎖,它可用於共享和獨占模式。

在讀取一個頁面時,后端進程請求獲取存儲該頁面緩沖區描述符的shared conntent_lock。

但是,在執行以下其中一個操作時,需要獲取獨占模式的content_lock:

  1. 往存儲頁面插入行(元組)或更改存儲頁面內元組的t_xmin/t_xmax 列(t_xmin/t_xmax 已在第5.2節介紹過;簡單地說,當刪除或更新行時,元組的這些字段就會改變)
  2. 物理刪除元組或壓縮存儲頁面的可用空間(分別通過執行第6章描述的vacuum處理和第7章描述的HOT)
  3. 凍結存儲頁面中的元組(凍結在第 5.10.1 節和第 6.3 節中描述)

官方README 文件顯示了更多詳細內容。

8.3.2.2 io_in_progress_lock

io_in_progress 鎖用於等待緩沖區上的I/O完成。當 PostgreSQL 進程從/向磁盤加載/寫入頁面數據時,該進程在訪問磁盤時持有相應描述符的獨占 io_in_progress 鎖。

8.3.3.3 spinlock(自旋鎖)

當檢查或更改標志(flag)或其他字段(例如 refcount 和 usage_count)時,將使用自旋鎖。下面給出了使用自旋鎖的兩個具體示例:

(1) 下面顯示了如何固定(pin)緩沖區描述符:

  1. 獲取緩沖區描述符的自旋鎖
  2. 將refcount 和 usage_count 的值加1
  3. 釋放自旋鎖
LockBufHdr(bufferdesc);    /* Acquire a spinlock */
bufferdesc->refcont++;
bufferdesc->usage_count++;
UnlockBufHdr(bufferdesc); /* Release the spinlock */

(2) 下面顯示了如何將臟位(dirty bit)設置為'1':

  1. 獲取緩沖區描述符的自旋鎖
  2. 使用按位運算將臟位設置為'1'
  3. 釋放自旋鎖
#define BM_DIRTY             (1 << 0)    /* data needs writing */
#define BM_VALID             (1 << 1)    /* data is valid */
#define BM_TAG_VALID         (1 << 2)    /* tag is assigned */
#define BM_IO_IN_PROGRESS    (1 << 3)    /* read or write in progress */
#define BM_JUST_DIRTIED      (1 << 5)    /* dirtied since write started */

LockBufHdr(bufferdesc);
bufferdesc->flags |= BM_DIRTY;
UnlockBufHdr(bufferdesc);

以相同的方式更改其他位。

用原子操作替換緩沖區管理器自旋鎖

在 9.6 版本中,緩沖區管理器的自旋鎖將被替換為原子操作。查看 commitfest 的結果。如果您想了解詳細信息,請參閱此討論

8.4 緩沖區管理器工作原理

本節描述緩沖區管理器的工作原理。后端進程想要訪問所需的頁面時,調用ReadBufferExtended 函數。

ReadBufferExtended 函數的行為取決於三種邏輯情況。以下每一小節描述一種情況,此外,PostgreSQL 的時鍾掃描頁面替換算法在最后一小節描述。

8.4.1 訪問存儲在緩沖池中的頁面

首先,描述最簡單的情況,即所需頁面已經存儲在緩沖池中。在這種情況下,緩沖區管理器執行以下步驟:

(1) 創建所需頁面的buffer_tag(在本例中,buffer_tag 為'Tag_C')並使用哈希函數計算包含所創建的buffer_tag 的相關條目的哈希桶槽。

(2) 以共享模式獲取覆蓋所獲得的哈希桶槽的BufMappingLock分區(該鎖將在步驟(5)中釋放)。

(3) 查找標簽為'Tag_C'的條目,並從該條目中獲取buffer_id。在本例中,buffer_id 為 2。

(4) 為 buffer_id 2 (pin)固定緩沖區描述符,即為描述符的 refcount 和 usage_count 增加 1(第 8.3.2 節描述了 pinning)。

(5) 釋放BufMappingLock

(6) 訪問 buffer_id 為 2 的緩沖池槽。

Fig. 8.8. Accessing a page stored in the buffer pool.

Fig. 8.8. Accessing a page stored in the buffer pool.

然后,當從緩沖池槽中的頁面讀取行時,PostgreSQL 進程獲取相應緩沖區描述符的共享 content_lock。因此,緩沖池槽可以被多個進程同時讀取。

當往頁面插入(和更新或刪除)行時,Postgres 進程獲取相應緩沖區描述符的獨占 content_lock(注意頁面的臟位必須設置為 '1')。

訪問頁面后,相應緩沖區描述符的 refcount 值減 1。

8.4.2 將頁面從磁盤(存儲)儲加載到空槽

在第二個案例中,假設所需的頁面不在緩沖池中,並且freelist中有空元素(空描述符)。在這種情況下,緩沖區管理執行以下步驟:

(1) 查找緩沖表(我們假設它沒有找到)。

  1. 為所需頁面創建buffer_tag(在本例中,buffer_tag 為 'Tag_E')並計算哈希桶槽
  2. 以共享模式獲取 BufMappingLock 分區
  3. 查找緩沖表(根據假設沒有找到)
  4. 釋放BufMappingLock

(2) 從freelist中獲取空緩沖描述符並固定(pin)它,在本例中,獲取到的描述符的buffer_id為4

(3) 以獨占模式獲取BufMappingLock 分區(該鎖將在步驟(6)釋放)

(4) 創建一個包含buffer_tag ‘Tag_E’和buffer_id 4的新數據條目;將創建的條目插入緩沖表。

(5) 將所需的頁面數據從磁盤加載到 buffer_id 為 4 的緩沖池槽中,如下所示:

  1. 獲取對應描述符的獨占io_in_progress_lock
  2. 設置對應描述符的io_in_progress位為'1',防止其他進程訪問
  3. 將所需的頁面數據從磁盤加載到緩沖池槽
  4. 改變對應緩沖區描述符的狀態;io_in_progress 設置為'0',valid 設置為'1'
  5. 釋放io_in_progress_lock

(6) 釋放BufMappingLock

(7) 訪問 buffer_id 為 4 的緩沖池槽

Fig. 8.9. Loading a page from storage to an empty slot.

Fig. 8.9. Loading a page from storage to an empty slot.

8.4.3 將頁面從磁盤加載到已占滿的緩沖池槽

在這個案例中,假設所有緩沖池槽已被頁面占用,但沒有存儲所需的頁面。緩沖區管理器執行以下步驟以獲取所需的頁面:

(1) 為所需頁面創建buffer_tag並查找緩沖表。在此例中,我們假設 buffer_tag 為 'Tag_M'(未找到所需頁面)

(2) 使用時鍾掃描(clock-sweep)算法選擇一個“犧牲”緩沖池槽,從緩沖區表中獲取包含“犧牲”緩沖池槽的buffer_id的舊條目,並將“犧牲”緩沖池槽pin(固定)到緩沖區描述符層中。在此例中,“犧牲”緩沖池槽的buffer_id 是5,舊條目是'Tag_F, id=5'。時鍾掃描將在下一節中描述。

(3) 如果“犧牲”頁面數據是臟的,則刷新(寫入和fsync)到磁盤。否則,執行步驟(4)

​ 臟頁必須在新數據覆蓋前寫入到存儲(磁盤)。刷新臟頁的操作如下:

  1. 獲取buffer_id為5的描述符的共享content_lock和獨占io_in_progress鎖(在步驟6中釋放)
  2. 更改對應描述符的狀態;io_in_progress 設置為'1',just_dirtied 設置為'0'
  3. 根據情況,調用XLogFlush()函數將WAL緩沖區上的WAL數據寫入當前WAL文件(具體省略,WAL和XLogFlush函數在第9章介紹)
  4. 將“犧牲”頁數據刷新到存儲
  5. 更改對應描述符的狀態;io_in_progress 設置為'0',valid 設置為'1'
  6. 釋放the io_in_progress 和 content_lock 鎖

(4) 以獨占模式獲取覆蓋包含舊條目的槽的舊BufMappingLock分區

(5) 獲取新的 BufMappingLock 分區並將新條目插入到緩沖表中:

  1. 創建由新的 buffer_tag 'Tag_M' 和“犧牲” buffer_id 組成的新條目
  2. 以獨占模式獲取覆蓋包含新條目的槽的新 BufMappingLock 分區
  3. 將新條目插入緩沖表

Fig. 8.10. Loading a page from storage to a victim buffer pool slot.

Fig. 8.10. Loading a page from storage to a victim buffer pool slot.

(6) 從緩沖區表刪除舊條目並釋放舊BufMappingLock 分區

(7) 從存儲中加載所需頁面數據到“犧牲”緩沖區槽。然后,更新buffer_id 5的描述符標簽;dirty bit設置為0並初始化其它標識位

(8) 釋放新BufMappingLock 分區

(9) 訪問buffer_id 5的緩沖池槽

Fig. 8.11. Loading a page from storage to a victim buffer pool slot (continued from Fig. 8.10).

Fig. 8.11. Loading a page from storage to a victim buffer pool slot (continued from Fig. 8.10).

8.4.4 頁面替換算法:時鍾掃描(clock-sweep)

本節的其余部分描述時鍾掃描算法。該算法是 NFU (Not Frequently Used) 的一種變體,開銷較低。它有效地選擇不常用的頁面。

將緩沖區描述符想象成一個循環列表(圖 8.12)。nextVictimBuffer 是一個無符號的 32 位整數,始終指向其中一個緩沖區描述符並順時針旋轉。該算法的偽代碼和描述如下:

Pseudocode: clock-sweep

  WHILE true
(1)     Obtain the candidate buffer descriptor pointed by the nextVictimBuffer
(2)     IF the candidate descriptor is unpinned THEN
(3)	       IF the candidate descriptor's usage_count == 0 THEN
	            BREAK WHILE LOOP  /* the corresponding slot of this descriptor is victim slot. */
	       ELSE
		    Decrease the candidate descriptpor's usage_count by 1
            END IF
      END IF
(4)     Advance nextVictimBuffer to the next one
   END WHILE 
(5) RETURN buffer_id of the victim

(1) 獲取nextVictimBuffer指向的候選緩沖區描述符

(2) 如果候選緩沖區描述符是unpinned,則進行步驟(3);否則,進行步驟(4)

(3) 如果候選緩沖區描述符的 usage_count 為0,則選擇該描述符對應的槽作為"犧牲者"並執行步驟(5);否則,將該描述符的usage_count 減1,然后執行步驟(4)

(4) 將 nextVictimBuffer 推進到下一個描述符(如果在最后,則回繞)並返回步驟(1)。重復直到找到"犧牲者"

(5) 返回"犧牲者"的buffer_id

具體示例如圖 8.12 所示。緩沖區描述符顯示為藍色或青色框,框中的數字顯示每個描述符的 usage_count。

Fig. 8.12. Clock Sweep.

Fig. 8.12. Clock Sweep.

  1. nextVictimBuffer 指向第一個描述符(buffer_id 1);但是,此描述符被跳過,因為它是pinned狀態
  2. nextVictimBuffer 指向第二個描述符(buffer_id 2)。此描述符是unpinned ,但其 usage_count 為2;因此,usage_count 減 1,nextVictimBuffer 前進到第三個候選者
  3. extVictimBuffer 指向第三個描述符(buffer_id 3)。這個描述符是unpinned並且它的usage_count是0;因此,這是本輪的"犧牲者"

每當 nextVictimBuffer 掃過一個 unpinned 描述符時,它的 usage_count 就會減 1。因此,如果緩沖池中存在 unpinned 描述符,該算法總是可以通過輪換 nextVictimBuffer 找到一個usage_count 為0 的"犧牲者"。

8.5 環形緩沖區(Ring Buffer)

當讀取或寫入一個巨大的表時,PostgreSQL 使用環形緩沖區而不是緩沖池。環形緩沖區是一個小且臨時的緩沖區區域。當滿足下面列出的任何條件時,將向共享內存分配一個環形緩沖區:

  1. 批量讀(Bulk-reading)

    當掃描大小超過緩沖池大小 (shared_buffers/4) 四分之一的關系(表)時。在這種情況下,環形緩沖區大小為 256 KB。

  2. 批量寫(Bulk-writing)

    當執行下面列出的 SQL 命令時。在這種情況下,環形緩沖區大小為 16 MB。

  3. Vacuum處理

​ 當 autovacuum 執行vacuum處理時。在這種情況下,環形緩沖區大小為 256 KB。

分配的環形緩沖區在使用后立即釋放。

環形緩沖區的好處是顯而易見的。如果后端進程在不使用環形緩沖區的情況下讀取一個巨大的表,則緩沖池中所有存儲的頁面都將被移除(踢出);因此,緩存命中率降低。環形緩沖區避免了這個問題。

為什么批量讀取和vacuum處理的默認環形緩沖區大小為 256 KB?

為什么是 256 KB?答案在位於緩沖區管理器源目錄下的 README 中進行了解釋。

對於順序掃描,使用 256 KB 的環。這已足以適合 L2 緩存,使得將頁面從 OS 緩存傳輸到共享緩沖區緩存有效。通常甚至更少就足夠了,但是環必須足夠大以容納同時掃描所有頁面的pinned。

8.6 刷新臟頁

除了替換"犧牲者"頁面之外,檢查點和后台寫進程將臟頁刷新到存儲中。兩個進程具有相同的功能(刷新臟頁);但是,他們有不同的角色和行為。

checkpointer 進程將檢查點記錄寫入 WAL 文件,並在檢查點開始時刷新臟頁。9.7 節描述了檢查點以及它何時開始。

background writer 進程的作用是減少檢查點密集寫入的影響。它持續一點一點地刷新臟頁,對數據庫的性能你影響最小。默認情況下,bgwriter 每 200 毫秒(由 bgwriter_delay 定義)喚醒一次,並且最多刷新 bgwriter_lru_maxpages(默認為 100 頁)。

為什么檢查點與 bgwriter 分開?

在 9.1 或更早的版本中,bgwriter 會定期進行檢查點處理。在 9.2 版本中,checkpointer 進程已從bgwriter 進程中分離處理。在標題為“Separating bgwriter and checkpointer”的提案中描述了原因,因此其原文如下所示。

Currently(in 2011) the bgwriter process performs both background writing, checkpointing and some other duties. This means that we can't perform the final checkpoint fsync without stopping background writing, so there is a negative performance effect from doing both things in one process.
Additionally, our aim in 9.2 is to replace polling loops with latches for power reduction. The complexity of the bgwriter loops is high and it seems unlikely to come up with a clean approach using latches.

(snip)

翻譯

目前(2011 年)bgwriter 進程同時執行后台寫入、檢查點和其他一些職責。這意味着我們無法在不停止后台寫入的情況下執行最終檢查點 fsync,因此在一個進程中執行這兩項操作會對性能產生負面影響。

此外,我們在 9.2 中的目標是用閂鎖(latch)替換輪詢循環以降低功耗。 bgwriter 循環的復雜性很高,似乎不太可能提出使用閂鎖的完整方法。

(裁剪)

附錄

BufferDesc 結構

BufferDesc 結構在 src/include/storage/buf_internals.h 文件中定義。

/*
 * Flags for buffer descriptors
 *
 * Note: TAG_VALID essentially means that there is a buffer hashtable
 * entry associated with the buffer's tag.
 */
#define BM_DIRTY                (1 << 0)    /* data needs writing */
#define BM_VALID                (1 << 1)    /* data is valid */
#define BM_TAG_VALID            (1 << 2)    /* tag is assigned */
#define BM_IO_IN_PROGRESS       (1 << 3)    /* read or write in progress */
#define BM_IO_ERROR             (1 << 4)    /* previous I/O failed */
#define BM_JUST_DIRTIED         (1 << 5)    /* dirtied since write started */
#define BM_PIN_COUNT_WAITER     (1 << 6)    /* have waiter for sole pin */
#define BM_CHECKPOINT_NEEDED    (1 << 7)    /* must write for checkpoint */
#define BM_PERMANENT            (1 << 8)    /* permanent relation (not unlogged) */

src/include/storage/buf_internals.h
typedef struct sbufdesc
{
   BufferTag    tag;                 /* ID of page contained in buffer */
   BufFlags     flags;               /* see bit definitions above */
   uint16       usage_count;         /* usage counter for clock sweep code */
   unsigned     refcount;            /* # of backends holding pins on buffer */
   int          wait_backend_pid;    /* backend PID of pin-count waiter */
   slock_t      buf_hdr_lock;        /* protects the above fields */
   int          buf_id;              /* buffer's index number (from 0) */
   int          freeNext;            /* link in freelist chain */

   LWLockId     io_in_progress_lock; /* to wait for I/O to complete */
   LWLockId     content_lock;        /* to lock access to buffer contents */
} BufferDesc;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM