1. 程式人生 > 實用技巧 >PostgreSQL的WAL(1)--Buffer Cache

PostgreSQL的WAL(1)--Buffer Cache

為什麼需要提前寫日誌

DBMS處理的資料部分儲存在RAM中,並非同步寫入磁碟(或其他非易失性儲存)中。即寫延遲了一段時間。這種情況發生的頻率越低,輸入/輸出越少,系統執行越快。

但是,如果發生故障(例如斷電或DBMS或作業系統的程式碼錯誤),會發生什麼? RAM的所有內容都會丟失,只有寫入磁碟的資料才能倖存(磁碟也無法倖免於某些故障,如果磁碟上的資料受到影響,則只有備份可以提供幫助)。通常,可以以磁碟上的資料始終保持一致的方式來組織輸入/輸出,但這很複雜且效率不高(據我所知,只有Firebird選擇了此選項)。

通常,尤其是在PostgreSQL中,寫入磁碟的資料似乎不一致,並且在故障後恢復時,需要採取特殊措施來恢復資料一致性。預寫日誌記錄(WAL)只是一項使之成為可能的功能。

buffer cache

buffer cache不是儲存在RAM中的唯一結構,而是其中最關鍵和最複雜的結構之一。理解其工作原理本身很重要;此外,我們將以它為例以熟悉RAM和磁碟如何交換資料。

快取在現代計算機系統中無處不在。一個處理器僅具有三級或四級快取。通常,需要快取來減輕兩種記憶體之間的效能差異,其中一種相對較快,但是容量較小,迴圈不足;另一種相對較慢,但是容量足夠。緩衝區快取減輕了訪問RAM的時間(納秒)和磁碟儲存的時間(毫秒)之間的差異。

請注意,作業系統還具有解決相同問題的磁碟快取。因此,資料庫管理系統通常嘗試通過直接訪問磁碟而不是通過OS快取來避免雙重快取。但是PostgreSQL並非如此:所有資料都是使用常規檔案操作讀取和寫入的。

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

但是,讓我們回到DBMS的buffer cache。

每個buffer由資料頁(塊)的空間和header組成。header中包含:

·page在buffer中的位置(檔案和塊號)。

·page上資料更改的指示符,更改遲早需要將其寫入磁碟(這樣的緩衝區稱為髒緩衝區)。

·buffer的使用計數。

·buffer的pin計數。

buffer cache位於伺服器的共享記憶體中,所有程序都可以訪問它。為了處理資料,即讀取或更新資料,這些程序會將頁面讀取到快取中。當頁面在快取中時,我們在RAM中使用它並在磁碟訪問中儲存它。

快取最初包含空緩衝區,並且所有緩衝區都連結到空閒緩衝區列表中。快取的雜湊表用於快速找到您需要的頁面。

在cache中尋找一個page

當程序需要讀取頁面時,它首先嚐試通過雜湊表在buffer cache中找到它。檔案號和檔案中的頁面號用作雜湊鍵。該程序在適當的雜湊桶中找到buffer編號,並檢查它是否確實包含所需的頁面。像任何雜湊表一樣,此處可能會發生衝突,在這種情況下,該過程將不得不檢查多個頁面。

雜湊表的使用長期以來一直是人們抱怨的源頭。像這樣的結構可以快速按頁查詢緩衝區,但是,例如,如果您需要查詢某個表佔用的所有緩衝區,則雜湊表絕對是無用的。但是還沒有人建議好的替代品。

如果在快取記憶體中找到所需的頁,則該程序必須通過增加pin計數來“pin”住緩衝區(多個程序可以同時執行此操作)。被固定的緩衝區(計數值大於零)時,它被認為是已使用並且具有無法“急劇”更改的內容。例如:一個新的元組可以出現在頁面上-由於多版本併發和可見性規則,這對任何人都無害。但是無法將其他頁面讀入固定的緩衝區。

Eviction

可能會出現在快取中找不到所需的頁面的情況。在這種情況下,需要將該頁從磁碟讀入某個緩衝區。

如果快取中的空緩衝區仍然可用,則選擇第一個空緩衝區。但是它們遲早會不夠(資料庫的大小通常大於為快取分配的記憶體),然後我們將不得不選擇一個已佔用的緩衝區,將位於那裡的頁清除出去,並將新的頁讀入已釋放的空間。

清除技術基於這樣一個事實:對於每次訪問緩衝區,程序都會增加緩衝區header中的使用計數。因此,與其他緩衝區相比,使用頻率較低的緩衝區的計數值較小,因此是清除的良好候選物件。

時鐘掃描演算法迴圈地遍歷所有緩衝區(使用指向«next victim»的指標),並將它們的使用量減少1。 為清除選擇的第一個緩衝區要滿足:

·使用計數是0

·pin計數也是0

請注意,如果所有緩衝區都有一個非零的使用計數,那麼演算法將不得不在緩衝區中進行多次迴圈,減少計數的值算,直到其中一些減少到零為止。演算法為了避免«做重疊»的操作,使用計數的最大值被限制為5。然而,對於大型的buffer cache,該演算法可能會造成相當大的開銷。

找到緩衝區後,將對它執行以下操作。

緩衝區被固定以顯示使用它的其他程序。除了固定之外,還使用了其他鎖定技術,但是我們將在後面更詳細地討論。

如果緩衝區看起來是髒的,也就是說,包含已更改的資料,就不能直接刪除頁面——它需要首先儲存到磁碟。 這很難說是一種好情況,因為要讀取頁面的程序必須等待其他程序的資料被寫入,但是檢查點和後臺寫入器程序緩解了這種影響,這將在後面討論。

然後將新頁從磁碟讀入選定的緩衝區。使用計數被設定為1。此外,必須將對已載入頁面的引用寫入雜湊表,以便將來能夠查詢該頁面。

對«next victim»的引用現在指向下一個緩衝區,而剛剛載入的緩衝區有時間增加使用計數,直到指標迴圈地遍歷整個緩衝區快取並再次返回。

自己驗證一下

和往常一樣,PostgreSQL有一個擴充套件,可以讓我們檢視緩衝區快取的內部。

=> CREATE EXTENSION pg_buffercache;

讓我們建立一個表並在那裡插入一行。

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

buffer cache將包含什麼?至少,必須出現只添加了一行的頁面。讓我們用下面的查詢來檢查這個,它只選擇與我們的表相關的緩衝區(通過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建立了可見性map(一頁)和空閒空間map(有三頁,這是這樣一個map的最小尺寸)。

調優buffer cache的大小

我們可以使用shared_buffers引數設定快取大小。預設值是128mb,這是安裝PostgreSQL後應該馬上增加的引數之一。

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

注意,更改此引數需要重新啟動伺服器,因為快取的所有記憶體都是在伺服器啟動時分配的。

即使是最大的資料庫也只有一組有限的“熱”資料,這些資料一直在被集中處理。理想情況下,必須在緩衝區快取中容納這個資料集(加上一些用於一次性資料的空間)。如果快取大小較小,那麼頻繁使用的頁面將不斷地相互清除,這將導致過多的輸入/輸出。但是盲目地增加快取也不好。當快取很大時,維護它的開銷將增加,除此之外,其他用途也需要RAM。

因此,您需要為您的特定系統選擇最佳的緩衝區快取大小:這取決於資料、應用程式和負載。不幸的是,沒有萬能的值。

通常建議使用1/4的記憶體作為第一個近似(低於10的PostgreSQL版本建議Windows使用更小的記憶體)。

然後我們最好進行實驗:增加或減少快取大小,並比較系統特性。為此,您當然需要測試,並且應該能夠重新生成工作負載。在生產環境中進行這樣的實驗是一種可疑的樂趣。

但是,您可以通過相同的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)

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

我們可以看到在我們的資料庫中哪些表被快取了,以及這些資料的使用頻率有多高(在這個查詢中,使用次數大於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通過常規的作業系統呼叫來處理檔案,因此會發生雙重快取:頁面同時進入DBMS和作業系統的快取。因此,沒有命中緩衝區快取並不總是導致需要實際的輸入/輸出。但是作業系統的驅逐策略不同於DBMS:作業系統不知道讀取資料的意義。

Massive eviction

批量讀和寫操作容易產生這樣的風險,即有用的頁面可能會被«一次性»從緩衝區快取中快速驅逐。

為了避免這種情況,使用了所謂的 buffer rings:只是為每個操作分配一小部分緩衝區快取。驅逐僅在環內執行,因此緩衝區快取中的其餘資料不受影響。

對於大型表(其大小大於緩衝區快取的四分之一)的連續掃描,將分配32個頁面。如果在掃描一個表的過程中,另一個程序也需要這些資料,那麼它不會從頭開始讀取表,而是連線到已經可用的緩衝區環。在完成掃描之後,程序繼續讀取表的«missed»開頭部分。

讓我們驗證一下。建立一個表,以便一行佔據整個頁面——這樣計數更方便。緩衝區快取的預設大小為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)

現在我們必須重新啟動伺服器以清除分析中讀取的表資料的快取。

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和create table as select(通常為2048頁,但不超過緩衝區快取的1/8)。

臨時表

臨時表是常見規則中的一個例外。由於臨時資料只對一個程序可見,因此不需要在共享緩衝區快取中使用它們。 此外,臨時資料只存在於一個會話中,因此不需要防止失敗的保護。

臨時資料使用擁有該表的程序的本地記憶體中的快取。由於這些資料只對一個程序可用,因此不需要使用鎖保護它們。本地快取使用正常的驅逐演算法。

與共享緩衝區快取不同,本地快取的記憶體是在需要時分配的,因為在許多會話中都不會使用臨時表。單個會話中臨時表的最大記憶體大小受到temp_buffers引數的限制。

為cache預熱

在伺服器重啟後,快取必須經過一段時間才能“預熱”,也就是說,要填充活躍使用的資料。它可能有時看起來有用,立即讀取某些表的內容到快取中,一個專門的擴充套件是可用的:

=> 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主後臺程序將啟動,每隔pg_prewarm.autoprewarm_interval秒數完成一次重新整理快取中儲存的頁面列表。(在設定max_parallel_processes值時,不要忘記將新程序計算在內)。

=> 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主程序第一次完成,或者我們可以手動啟動重新整理,如下所示:

=> 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/autoprewarm.blocks

現在讓我們重新啟動伺服器。

student$ sudo pg_ctlcluster 11 main restart

在伺服器啟動後,我們的表將再次位於快取中。

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

相同的autoprewarm主程序提供了這一點:它讀取檔案,按資料庫劃分頁面,對它們進行排序(儘可能使從磁碟順序讀取),並將它們傳遞到一個單獨的autoprewarm worker程序進行處理。

原文地址:

https://habr.com/en/company/postgrespro/blog/491730/