前言

PostgreSQL 是我目前工作與專案上最常用到的 RDBMs,為了能夠更了解 PostgreSQL 的運行以彌補底層的知識,就找到了 這系列 的文章,覺得內容非常的詳細與完整,很適合做為 PostgreSQL 進階的學習教材,就花了些時間整理了一下

底下的內容都是整理自 Distributed Systems Authority 的文章

PostgreSQL 底層架構

開始以前先放上 Distributed Systems Authority 繪製的架構圖,底下會依據各元件一一說明

postgresql_architecture

整體來說,PostgreSQL 是採用近幾年蔚為流行的 Client-Server 架構

Client (frontend)

泛指一系列 client 連線方式,包含:psql, createdb, dropdb, createuser… 等 command line 指令

Server (backend)

可以概括稱之為 server process,負責管理 database files,接受 client 的連線,並執行 client 一系列相關的操作

當 server 收到每一筆 client 的連線,main process 都會 fork 出新的 process 來處理,所以 client 和 server 的溝通是與 main process 無關的

整體而言,backend 可分成底下四個元件:

Process Manager

  • 當執行 /usr/local/pgsql/bin/postgres 後就會建立第一個 daemon process,監聽每一個 client 對 server 發起的連線請求,在 max_connections 的數量限制下,daemon process 會為每一個連線 spawn 出 backend process 來讓 client query

    postgresProcessPerConnection

  • 一旦連線成功之後,client 將會讀 (SELECT) 寫 (INSERT, UPDATE, DELETE…) 資料,如果這時候有上千萬個 client 要做這些事情,將會讓系統的複雜度遽增,所以 Postgresql 採用 shared buffers(RAM) 來讓 backend process 來讀寫,大幅提升效能

Query Processor

主要負責 query 語句的解析與執行,主要有底下這些元件

The_Path_of_Query

  • Connection Manager
    • 負責確認 application program 和 PostgreSQL server 的連線已經建立
  • Parser
    • 負責檢查 application program 送來的 query 語法是否正確,並解析成 query tree
  • Rewriter
    • Rewrite system 會從 parse 階段取得 query tree,依據系統定義的規則 (system catalogs) 來 rewrite 他
    • 舉例來說,當 create 或 query view(virtual table) 時,rewrite system 會把這些 query 給 rewrite 成給 base table 的語句
  • Planner/Optimizer
    • 從 rewrite 階段取出 rewritten 的 query tree,並組建成 query plan
    • 此階段的任務是盡可能的組建出各種可能的 query paths
    • 舉例來說,一個對 index 欄位做的 query,將會產出兩種可能的 query paths,一個是 sequential scan,另一個則是 use index,最後會選擇成本最低的一條路作為 query path,並送給 executor 來執行
  • Executor
    • 負責遞迴的走訪 plan 階段組建的 plan tree,並執行各個操作行為 (ex: scanning relations, perform sorts and joins…),返回查詢到的資料

Utilities Processor

提供一系列維護資料庫的方法,預設的配置會有以下的元件:

  • Background writer process

    • 依據演算法來將 dirty buffers 寫入 disk,會考慮該 dirty buffers 中哪些 block 被 used/modified (LRU)
    • 反觀下一個要提到的 checkpoint process,則是一股腦兒把所有 dirty buffers 都寫入 disk

    postgresWritesToFiles-1

  • Checkpoint process

    • 與 Background writer 的目的一樣,都是負責把 user 更新過的資料 (dirty data) 寫入到 disk 做永久性儲存
    • 主要做三個步驟,第一個是把所有 dirty data page 寫入到 table 和 index,第二步把 buffers 標記為清空的狀態,第三步確認 WAL 的最新 log 有寫入該歷程

    postgres_checkpoint

    • PostgreSQL 中的 checkpoint_segmentscheckpoint_timeout 這兩個設定會影響每次 buffer 寫入 disk 的大小和頻率,在 write-heavy 的情境下,適當的設定才不會造成頻繁的去執行昂貴的 disk 寫入
  • WAL writer process

    • WAL(Write-Ahead log) 意思是每當 user 修改資料,PostgreSQL 會將 buffer 內的 block 標記為 dirty,同時會將該筆變更寫入到 WAL buffer,爾後當變更被 committed 後,該變更就會同時被 flushed 到 WAL segments
    • WAL 會受到 PostgreSQL 內的 fsyncmax_wal_size 這兩項設定影響
    • fsync 會強制每個 transaction 在被 commit 之後都被寫進 disk,關閉這項設定會提升 bulk-upload 的效能,但遇到停電或是 crash 時會有 data inconsistency 的風險
    • max_wal_size 指的是每次 WAL 要寫入的大小,在 write-heavy 的情境下不能設定太小,否則會有 performance penalty; 反之,過大的 max_wal_size 又會拉高每次 flushed 的 recovery time。
  • Autovacuum launcher process

    • vacuum process 會把之前被 DELETE 或 UPDATE 的 record 占用的空間給釋放,以利後續 TABLE 的使用
    • 一般來說執行 VACUUM 是不會 lock 住 table,但使用 VACUUM FULL 時就會 lock 住 table,因為 VACUUM FULL 除了清空 DELETE 和 UPDATE 占用的空間以外,還會 reorder 整個 table 的資料
    • AUTOVACUUM 指的就是自動執行 VACUUM,會在 off-peak 的離峰時間執行
    • PostgreSQL 建議就算沒有很多 DELETE/UPDATE 的操作,仍然要定期 VACUUM,因為 planner 使用的 data statistic 也會在執行 VACUUM 時被更新,所以 PostgreSQL 預設開啟 AUTOVACUUM,若要手動強制執行,也可以使用 VACUUM 指令
  • Stats collector process

    • 負責監控與追蹤所有對 database 的 queries,這些數據可用來識別出哪一個欄位該加上 index、哪些 table 最常被 accessed、shared buffers 的使用狀況.. 等等
  • Archiever

    • 負責複製 pg_xlog(或稱pg_wal) 的檔案到要被 archive 的位址
  • Logging collector

    • Optional 的 process,預設是關閉的
    • 負責將 database messages 寫入到 database log files,以利後續 report 的生成

Storage Manager

負責 memory cache、disk buffers 和 storage allocation

PostgreSQL 系統記憶體介紹

postgresql_memory

PostgreSQL 的記憶體機制是基於 OS Cache 之上,可粗略分為左側的 Shared memory 和右側的 isolated memory per backend process。

  • Shared Memory
    • Shared Buffers: 底下會說明
    • CLOG Buffers: CLOG 指的是 Commit Log,用於 MVCC 時的各種 isolation 情境判別
    • WAL Buffers: Write-ahead log 的內存,當內存滿了之後才會一次寫入到 WAL 中
  • Isolated Memory
    • work_mem: 每個連線的 session 最多能夠使用 (allocate) 的 memory,用於 memory-intensive 的行為,例如:sorting, joins…

Data Block Page

PostgreSQL 從 disk 讀取的資料將會存成 data block page,是最基本的最小單元,預設大小是 8KB

MySQL 的 data block page 是 16KB,目的是一次 cache 多一點資料,降低 Disk I/O 的成本。PostgreSQL 的 data block page 是 8KB,cache 的資料較少,但當頻繁寫入或更動時可以避免不斷處理 B+ Tree leaf node 的 splitting/merging 問題 (Hot Spot issue)。

所以兩個 RDBMs 關注的議題不同,當然隨著科技日新月異,Storage 的成本愈來愈低,這時候 Hot Spot 問題帶來的負面影響漸漸比 Disk I/O 的問題嚴重,所以 PostgreSQL 選用更小的 Data Block Page 大小來設計,這也是 PostgreSQL 和 MySQL 最常被提到的差別之一

Shared Buffer

常用的 data block page 會放進 shared buffer 內,做 cache 使用,避免每次都訪問一次 disk,降低 disk I/O 的成本。 所以 Shared Buffer 是一個用於 PostgreSQL 讀寫時的對象,讀的時候可以把 frequently accessed 的 data 給 cache 起來,寫的時候則可以用作 Staging area

Shared Buffer 可以被所有連線的 process 給 access 得到,意味著 Shared Buffer 是共享的。 當然,每個連線當下的 session 也都各自會有私有的 memory area,稱為 local buffer,用於紀錄當前連線有哪些被修改的 data block pages

當 PostgreSQL 要讀取資料的時候,首先會去問 Shared Buffer 是否有要找的資料,找不到再去問 OS Cache,再找不到才去問 Disk

shared_buffers_flow

類似的機制也可以在 WAL buffer 與 WAL file 看的到

shared_buffers

Why Shared Buffer over OS Cache?

為什麼要在 OS Cache 之上引入 Shared Buffer 呢?為什麼不直接使用 OS Cache 傳統的 LRU 機制呢?

這就要提到 Shared Buffer 和 OS Cache 的差異了,那就是優化後的演算法:clock-sweep algorithm。實作上引入 buffer 被使用幾次的計數器 (counter),將 buffer 的計次轉換成 0-5 分的"熱門程度",熱門程度愈高的 buffer 愈不可能在未來被 evict,所以透過這個機制可以更好的管理 Shared Buffer 的生命週期。

Dirty page 處理 - Checkpoint

當 user 修改資料後,該筆變動會反映在 Shared Buffer 上,此時該 Buffer 已經被標記為 dirty,意味著它內部的資料狀態與內容與 persist 的 disk 資料內容有差異,需要在寫入 disk 時做額外處理,而這項任務就是交由 checkpoint process 來負責。

當 checkpoint 發生時,所有 dirty pages 會被寫入到 table 和 index files,並標註該 data page 為清空的狀態,並確保 Write-ahead log 有紀錄最新的這筆 checkpoint 時間點。

在一系列 Transaction 之中,一旦發生 checkpoint,所有發生在 checkpoint 之前寫入的資料都可保證已經被更新。此時,PostgreSQL 會將所有 dirty pages flush 到 disk 中,接著 PostgreSQL 會寫一筆 checkpoint 的記錄到 WAL,假設此時發生 crash,recovery procedure 會去察看最新的 checkpoint record 來決定是否要執行 REDO

發生 checkpoint 的時間點是受到三個設定決定:

  • checkpoint_segments: 當 WAL 的 segments 達到設定的數量時,就會觸發,預設是 3 個。
  • checkpoint_timeout: 當時間達到設定的 timeout 時會觸發 checkpoint
  • checkpoint_completion_target: 由於上述兩個參數都會影響 checkpoint 的頻率,頻率過高會讓 disk I/O 成本遽增,頻率過低會累積太多資料要寫入 disk,造成 checkpoint 時會有顯著的 disk spikes。所以有了這項參數來制衡,預設是 0.5,PostgreSQL 會在下一個 checkpoint 開始之前大約一半的時間內完成每個 checkpoint,當設定成更高時,disk spikes 將會被更多的時間給攤平

結語

透過這次學習的筆記,比較了解 PostgreSQL 的底層架構與設計,最後再回頭去看 ddia 的內容,整體會更有感覺,後續還會繼續這系列文章的整理。