PostgreSQL 中的 WAL:1. 緩沖區緩存


本文抄自:

https://mp.weixin.qq.com/s/YdOEHFVxiOkIp6Blcly2lQ

 

本系列將由四部分組成:

  • 緩沖區緩存(本文)。

  • 預寫日志 ——它的結構和用於恢復數據的方式。

  • 檢查點和后台寫進程——我們為什么需要它們以及我們如何設置它們。

  • WAL 的設置和調優 ——問題級別和解決方案、可靠性和性能。

為什么我們需要預寫日志?

數據庫管理系統(DBMS)使得部分數據存儲在緩存中並異步寫入磁盤(或其他非易失性存儲),即,寫入存儲被推遲了一段時間。這種情況發生得越少,輸入/輸出就越少,系統運行得越快。

但是如果出現故障,例如停電或 DBMS 或操作系統的代碼錯誤,會發生什么?緩存中的所有內容都將丟失,只有寫入磁盤的數據會保留下來(磁盤也不能免受某些故障的影響,如果磁盤上的數據受到影響,則只有備份副本可以提供幫助)。一般來說,可以以磁盤上的數據始終一致的方式組織輸入/輸出,但這很復雜且效率不高(據我所知,只有 Firebird 是這樣做的)。

通常,特別是在PostgreSQL中,寫入磁盤的數據會出現不一致的情況,並且在失敗后恢復時,需要采取特殊措施來恢復數據一致性。預寫日志 (WAL) 只是使之成為可能的一項功能。

緩沖區緩存

我們將從討論緩沖區緩存(buffer cache)來開始討論 WAL。緩沖區緩存不是存儲在內存中的唯一結構,而是其中最關鍵和最復雜的結構之一。了解它的工作原理本身很重要;此外,我們將使用它作為示例,以熟悉內存和磁盤如何交換數據。

內存在現代計算機系統中無處不在;一個處理器本身就有三到四級緩存。一般情況下需要內存來緩解兩種內存的性能差異,一種比較快,但是不夠,另一種比較慢,但是足夠。並且緩沖區緩存緩解了訪問內存(納秒)和磁盤存儲(毫秒)之間的時間差異。

請注意,操作系統也有解決相同問題的磁盤緩存。因此,數據庫管理系統通常通過直接訪問磁盤而不是通過操作系統內存來嘗試避免雙重緩存。但 PostgreSQL 並非如此:所有數據都是使用正常的文件操作讀取和寫入的。

此外,磁盤陣列的控制器甚至磁盤本身也有自己的緩存。當我們討論可靠性時,這將很有用。

但是讓我們回到 DBMS 的緩沖區緩存。

之所以這樣稱呼是因為它表示為一個緩沖區數組。每個緩沖區(buffer)由一個數據頁面(塊)加上文件頭的空間組成。文件頭中包括:

  • 頁在緩沖區中的位置(那里的文件和塊號)。

  • 頁上數據更改的指針,這些數據遲早需要寫入磁盤(這種緩沖區稱為臟的)。

  • 緩沖區的使用計數。

  • 緩沖區的引腳計數。

緩沖區緩存位於共享緩存中,可供所有進程訪問。為了處理數據,即讀取或更新它們,進程將頁面讀入緩存。當頁面在緩存中時,我們在內存中處理它並節省磁盤訪問

緩存最初包含空緩沖區(free buffers),並且所有這些緩沖區都鏈接到空閑緩沖區列表中。指向“next victim”的指針的含義稍后會清楚。緩存中的哈希表(hash table)用於快速找到您需要的頁面

在緩存中搜索頁面

當一個進程需要讀取一個頁面時,它首先嘗試通過哈希表在緩沖區緩存中找到它。文件號和文件中的頁面數用作哈希鍵。該進程在適當的hash bucket中找到緩沖區編號,並檢查它是否真的包含所需的頁面。與任何哈希表一樣,這里可能會出現沖突,在這種情況下,進程將不得不檢查多個頁面。

長期以來,哈希表的使用一直被人們所詬病。像這樣的結構可以按頁面快速找到緩沖區,但是如果您需要查找某個表占用的所有緩沖區,那么哈希表絕對沒有用。但是還沒有人提出好的替代品。

如果在緩存中找到所需的頁面,則進程必須通過增加引腳計數來“固定”緩沖區(多個進程可以同時執行此操作)。當一個緩沖區被固定(計數值大於零)時,它被認為是被使用的,且其內容不能“急劇”改變。例如:一個新的元組可以出現在頁面上——由於多版本並發和可見性規則,這對任何人都沒有傷害。但是不能將不同的頁面讀入固定緩沖區。

回收

可能會發生在緩存中找不到所需頁面的情況。在這種情況下,頁面將需要從磁盤讀取到某個緩沖區中。

如果緩存中仍有空緩沖區可用,則選擇第一個空緩沖區。但是這些空緩沖區遲早會被占用(數據庫的大小通常大於為緩存分配的內存),然后我們將不得不選擇一個被占用的緩沖區,回收位於那里的頁面並將新的頁面讀入被釋放的緩沖區。

回收技術基於這樣一個事實,即每次訪問緩沖區時,進程都會增加緩沖區文件頭的使用計數(usage count)。因此,與其他緩沖區相比,使用頻率較低的緩沖區具有較小的計數值,因此是回收的良好候選者。

時鍾掃描算法循環遍歷所有緩沖區(使用指針“next victim”),並將其使用計數減少一。回收會選擇的緩沖區:

  1. 使用計數為零

  2. 引腳數為零(即未固定)

請注意,如果所有緩沖區的使用計數都非零,則該算法將不得不在緩沖區執行不止一個循環,減少計數值,直到其中一些減少為零。為了避免大量的循環,使用計數的最大值限制為5。但是,對於大尺寸的緩沖區緩存,該算法會造成相當大的開銷成本。

一旦找到緩沖區,就會發生以下情況。

緩沖區被固定(pinned buffer)以顯示它被其他進程使用。除了固定之外,還使用了其他鎖定技術,我們將在稍后更詳細地討論這一點。

如果緩沖區看起來很“臟”,也就是說,一旦緩沖區包含更改的數據,則不能直接刪除頁面——它需要先保存到磁盤。這不是一個好的情況,因為要讀取頁面的進程必須等到其他進程的數據被寫入,但這種影響可以通過檢查點和后台寫進程緩解,稍后將討論。

然后將新頁面從磁盤讀取到選定的緩沖區中。使用計數被設置為1。此外,必須將已加載頁面的引用寫入哈希表,以便將來能夠找到該頁面。

對“next victim”的引用現在指向下一個緩沖區,剛剛加載的緩沖區有時間增加使用計數,直到指針循環遍歷整個緩沖區緩存並再次返回。

實驗觀察

PostgreSQL 有一個擴展,使我們能夠查看緩沖區緩存的內部。

=> CREATE EXTENSION pg_buffercache;

讓我們創建一個表並在那里插入一行。

=> CREATE TABLE cacheme(
     id integer
   ) WITH (autovacuum_enabled = off);
=> INSERT INTO cacheme VALUES (1);

緩沖區緩存將包含什么?至少,必須出現上面添加了一行的頁面。讓我們使用以下查詢來檢查,它僅查詢與我們的表相關的緩沖區(按relfilenode)並查詢relforknumber

=> SELECT bufferid,
  CASE relforknumber
    WHEN 0 THEN 'main'
    WHEN 1 THEN 'fsm'
    WHEN 2 THEN 'vm'
  END relfork,
 relblocknumber,
 isdirty,
 usagecount,
 pinning_backends
 FROM pg_buffercache
 WHERE relfilenode = pg_relation_filenode('cacheme'::regclass);
bufferid  | relfork | relblocknumber | isdirty | usagecount | pinning_backends
----------+---------+----------------+---------+------------+------------------
    15735 | main    |              0 | t       |          1 |                0
(1 row)

正如我們所想:緩沖區包含一頁。它是臟的 ( isdirty),使用計數 ( usagecount) 等於 1,並且該頁面未被任何進程 ( pinning_backends)固定。

現在讓我們再添加一行並重新運行查詢。為了保存前面的查詢結果,我們在另一個會話中插入一行並使用\g命令重新運行該長查詢。

=> INSERT INTO cacheme VALUES (2);
=> \g
 bufferid | relfork | relblocknumber | isdirty | usagecount | pinning_backends
----------+---------+----------------+---------+------------+------------------
    15735 | main    |              0 | t       |          2 |                0
(1 row)

沒有添加新的緩沖區:第二行適合同一頁。注意增加的使用次數。

=> SELECT * FROM cacheme;
   id
  ----
    1
    2
  (2 rows)
=> \g 
 bufferid | relfork | relblocknumber | isdirty | usagecount | pinning_backends
----------+---------+----------------+---------+------------+------------------
    15735 | main    |              0 | t       |          3 |                0
(1 row)

讀頁面后計數也會增加。

但是如果我們做vacuum呢?

=> VACUUM cacheme;
=> \g
 bufferid | relfork | relblocknumber | isdirty | usagecount | pinning_backends
----------+---------+----------------+---------+------------+------------------
    15731 |fsm      |              1 | t       |          1 |                0
    15732 |fsm      |              0 | t       |          1 |                0
    15733 |fsm      |              2 | t       |          2 |                0
    15734 |vm       |              0 | t       |          2 |                0
    15735 |main     |              0 | t       |          3 |                0
(5 rows)

VACUUM 創建了visibility map(一頁)和free space map(有三頁,這是此類地圖的最小尺寸)

調整大小

我們可以使用shared_buffers參數設置緩存大小。默認值是 128 MB。這是安裝 PostgreSQL 后立即增加的參數之一。

=> SELECT setting, unit FROM pg_settings WHERE name = 'shared_buffers';
 setting | unit
---------+------
 16384   | 8kB
(1 row)

請注意,更改此參數需要重新啟動數據庫服務,因為緩存的所有內存在服務器啟動時都已分配。

我們需要考慮什么來選擇合適的值?

即使是最大的數據庫也有一組有限的“熱”數據,這些數據一直在進行密集處理。理想情況下,這個數據集必須適合緩沖區緩存(加上一些一次性數據的空間)。如果緩存較小,那么密集使用的頁面將不斷相互驅逐,這將導致過多的輸入/輸出。但是盲目增加緩存也不好。當緩存很大時,其維護的開銷成本會增加,此外,內存也需要用於其他用途。

因此,您需要為特定系統選擇最佳緩沖區緩存大小:這取決於數據、應用程序和負載。不幸的是,沒有什么神奇的、一刀切的值。

通常建議使用 1/4 的系統內存作為粗略估算(PostgreSQL 版本低於 10 建議在 Windows 中使用較小的內存)。

然后我們想得到最好的值,應該進行試驗:增加或減少緩存大小並比較性能。為此,您當然需要一個測試設備,並且您應該能夠重現工作負載。

請務必查看 Nikolay Samokhvalov 在 PostgresConf Silicon Valley 2018 上的演講:數據庫實驗的藝術

但是您可以通過相同的pg_buffercache擴展直接在您的實時系統上獲得一些關於正在發生的事情的信息。最重要的是從正確的角度看。

例如:您可以通過緩沖區的使用來探索緩沖區的分布:

=> SELECT usagecount, count(*)
FROM pg_buffercache
GROUP BY usagecount
ORDER BY usagecount;
usagecount
| count -----------+------- 1 | 221 2 | 869 3 | 29 4 | 12 5 | 564 | 14689 (6 rows)

在這種情況下,count的大多數空值對應於空緩沖區。對於一個什么都沒有發生的系統來說,這並不奇怪。

我們可以看到我們的數據庫中哪些表被緩存的份額以及這些數據的使用強度(“密集使用”緩沖區體現在這個查詢中使用計數大於 3 (usagecount > 3)的緩沖區):

=> SELECT c.relname,
  count(*) blocks,
  round( 100.0 * 8192 * count(*) / pg_table_size(c.oid) ) "% of rel",
  round( 100.0 * 8192 * count(*) FILTER (WHERE b.usagecount > 3) / pg_table_size(c.oid) ) "% hot"
FROM pg_buffercache b
  JOIN pg_class c ON pg_relation_filenode(c.oid) = b.relfilenode
WHERE  b.reldatabase IN (
         0, (SELECT oid FROM pg_database WHERE datname = current_database())
       )
AND    b.usagecount is not null
GROUP BY c.relname, c.oid
ORDER BY 2 DESC
LIMIT 10;
          relname          | blocks | % of rel | % hot
---------------------------+--------+----------+-------
 vac                       |    833 |      100 |     0
 pg_proc                   |     71 |       85 |    37
 pg_depend                 |     57 |       98 |    19
 pg_attribute              |     55 |      100 |    64
 vac_s                     |     32 |        4 |     0
 pg_statistic              |     27 |       71 |    63
 autovac                   |     22 |      100 |    95
 pg_depend_reference_index |     19 |       48 |    35
 pg_rewrite                |     17 |       23 |     8
 pg_class                  |     16 |      100 |   100
(10 rows)

例如:我們在這里可以看到vac表占據了大部分空間,但是它沒有被訪問很長時間了,並且還沒有被回收,因為空緩沖區仍然可用。

你可以考慮其他觀點,這會為你提供思路:

  • 您需要多次重新運行此類查詢:數字將在一定范圍內變化。

  • 您不應連續運行此類查詢(作為監控的一部分),因為擴展會暫時阻止對緩沖區緩存的訪問。

還有一點需要注意。不要忘記 PostgreSQL 通常通過操作系統調用處理文件,因此會發生雙重緩存:頁面同時進入 數據庫管理系統緩沖區緩存和操作系統緩存。因此,不命中緩沖區緩存並不總是需要真正的輸入/輸出。但是操作系統的回收策略與數據庫管理系統不同:操作系統對讀取數據的意義一無所知。

大規模回收

批量讀取和寫入操作容易出現有用頁面被“一次性”的從緩沖區緩存中回收的風險。

為了避免這種情況,使用了所謂的緩沖區環(buffer rings):每個操作只分配緩沖區緩存的一小部分。回收僅在環內進行,因此緩沖區緩存中的其余數據不受影響。

對於大表(其大小大於緩沖區緩存的四分之一)的順序掃描,分配了 32 個頁面。如果在一個表的掃描過程中,另一個進程也需要這些數據,它不會從頭開始讀取表,而是連接到已經可用的緩沖環。完成掃描后,該過程繼續讀取表頭。

讓我們檢查一下。為此,讓我們創建一個表格,讓一行占據一整頁——這樣計數更方便。緩沖區緩存的默認大小為 128 MB = 16384 頁*8 KB)。這意味着我們需要在表中插入超過 4096 行,即頁面。

=> CREATE TABLE big(
  id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
  s char(1000)
) WITH (fillfactor=10);
=> INSERT INTO big(s) SELECT 'FOO' FROM generate_series(1,4096+1);

我們來分析一下表格。

=> ANALYZE big;
=> SELECT relpages FROM pg_class WHERE oid = 'big'::regclass;
relpages
---------- 4097 (1 row)

現在我們將不得不重新啟動數據庫以清除ANALYZE已讀取的表數據的緩存。

student$ sudo pg_ctlcluster 11 main restart

讓我們在重啟后讀取整個表:

=> EXPLAIN (ANALYZE, COSTS OFF) SELECT count(*) FROM big;
QUERY
PLAN --------------------------------------------------------------------- Aggregate (actual time=14.472..14.473 rows=1 loops=1) -> Seq Scan on big (actual time=0.031..13.022 rows=4097 loops=1) Planning Time: 0.528 ms Execution Time: 14.590 ms (4 rows)

讓我們確保表頁僅占用緩沖區緩存中的 32 個緩沖區:

=> SELECT count(*)
FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode('big'::regclass);
count ------- 32 (1 row)

但是如果我們禁止順序掃描,表將使用索引掃描讀取:

=> SET enable_seqscan = off;
=> EXPLAIN (ANALYZE, COSTS OFF) SELECT count(*) FROM big;
                                       QUERY PLAN                                         
-------------------------------------------------------------------------------------------
 Aggregate (actual time=50.300..50.301 rows=1 loops=1)
   ->  Index Only Scan using big_pkey on big (actual time=0.098..48.547 rows=4097 loops=1)
         Heap Fetches: 4097
 Planning Time: 0.067 ms
 Execution Time: 50.340 ms
(5 rows)

在這種情況下,不使用緩沖區環,整個表將進入緩沖區緩存(以及幾乎整個索引):

=> SELECT count(*)
   FROM pg_buffercache
   WHERE relfilenode = pg_relation_filenode('big'::regclass);
count
-------
4097
(1 row)

緩沖環以類似的方式用於vacuum進程 (也是 32 頁)和批量寫入操作 COPY INCREATE TABLE AS SELECT(通常為 2048 頁,但不超過緩沖區緩存的 1/8)。

臨時表

臨時表是通用規則的一個例外。由於臨時數據僅對一個進程可見,因此共享緩沖區緩存中不需要它們。此外,臨時數據僅存在於一個會話中,因此不需要針對故障進行保護。

臨時數據使用擁有該表的進程的本地內存中的緩存。由於此類數據僅對一個進程可用,因此不需要用鎖保護它們。本地緩存使用正常的逐出算法。

與共享緩沖區緩存不同,本地緩存的內存是根據需要分配的,因為臨時表遠未在許多會話中使用。單個會話中臨時表的最大內存大小受temp_buffers參數的限制。

預熱緩存

數據庫重新啟動后,緩存必須經過一段時間才能“預熱”,即填充實時使用的數據。有時將某些表的內容立即讀入緩存似乎很有用,為此可以使用專門的擴展:

=> CREATE EXTENSION pg_prewarm;

早些時候,擴展只能將某些表讀入緩沖區緩存(或僅讀入操作系統緩存)。但是 PostgreSQL 11 使它能夠將緩存的最新狀態保存到磁盤並在服務器重啟后恢復它。要使用它,您需要將庫添加到shared_preload_libraries,並重新啟動數據庫集群。

=> ALTER SYSTEM SET shared_preload_libraries = 'pg_prewarm';
student$ sudo pg_ctlcluster 11 main restart

重啟后,如果pg_prewarm.autoprewarm參數的值沒有改變,就會啟動autoprewarm master后台進程,它會每隔pg_prewarm.autoprewarm_interval秒刷新一次緩存中存儲的頁面列表(設置max_parallel_process(最大並行進程)的值時,不要忘記計算新進程的數量)。

該進程在 PostgreSQL 13 中重命名為autoprewarm leader

 
=> SELECT name, setting, unit FROM pg_settings WHERE name LIKE 'pg_prewarm%';
name
| setting | unit ---------------------------------+---------+------ pg_prewarm.autoprewarm | on | pg_prewarm.autoprewarm_interval | 300 | s (2 rows)
postgres$
ps -o pid,command --ppid `head -n 1 /var/lib/postgresql/11/main/postmaster.pid` | grep prewarm 10436 postgres: 11/main: autoprewarm master
 
        

現在緩存不包含表big

=> SELECT count(*)
FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode('big'::regclass);
count
------- 0 (1 row)

如果我們認為它的所有內容都是關鍵的,我們可以通過調用以下函數將其讀入緩沖區緩存:

=> SELECT pg_prewarm('big');
pg_prewarm
------------ 4097 (1 row)
=> SELECT count(*) FROM pg_buffercache WHERE relfilenode = pg_relation_filenode('big'::regclass);
count
------- 4097 (1 row)

塊列表被刷新到autoprewarm.blocks文件中。要查看列表,我們可以等待 autoprewarm master 進程第一次完成,或者我們可以像這樣手動啟動刷新:

 

SELECT autoprewarm_dump_now();
autoprewarm_dump_now
---------------------- 4340 (1 row)

刷新頁數已經超過4097;數據庫已經讀取的系統目錄的頁面被計算在內。這是文件:

postgres$ ls -l /var/lib/postgresql/11/main/autoprewarm.blocks
-rw------- 1 postgres postgres 102078 jun 29 15:51 /var/lib/postgresql/11/main/

現在讓我們再次重新啟動數據庫。

student$ sudo pg_ctlcluster 11 main restart

數據庫啟動后,我們的表將再次位於緩存中。

=> SELECT count(*)
FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode('big'::regclass);
count ------- 4097 (1 row)

autoprewarm master主進程為此提供了:讀取文件,按數據庫划分頁面,對它們進行排序(盡可能從磁盤順序讀取)並將它們傳遞給單獨的自動autoprewarm worker進程進行處理。

 


免責聲明!

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



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