為什么需要提前寫日志
DBMS處理的數據部分存儲在RAM中,並異步寫入磁盤(或其他非易失性存儲)中。即寫延遲了一段時間。這種情況發生的頻率越低,輸入/輸出越少,系統運行越快。
通常,尤其是在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)。
| => 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 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)
調優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)
在這種情況下,計數的多個空值對應於空緩沖區。對於一個什么都沒有發生的系統來說,這並不奇怪。
=> 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表占用了大部分空間,但是它沒有被長時間訪問,而且也沒有被驅逐,這只是因為空緩沖區仍然可用。
·您需要多次重新運行此類查詢:這些數字將在一定范圍內變化。 ·您不應該連續運行這樣的查詢(作為監視的一部分),因為擴展會暫時阻塞對緩沖區緩存的訪問。
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/