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/

 

 

 

 

 

  

 

 

 

 


免責聲明!

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



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