Durability
⚓ 「寫入成功」到底代表什麼
應用程式送出一筆寫入後,資料庫回覆成功,這句成功背後對資料安全保存的承諾強度,每一種資料庫都不一樣,這篇整理把這件事拆開來看。
成功回覆背後藏著什麼
假設應用程式執行了一筆 INSERT INTO orders (...),接著收到資料庫回覆 success。這個 success 其實可能代表很多不同層級的安全程度,從只是進了應用程式自己的記憶體,到已經跨機房複製到其他節點,中間差距非常大。
資料先進到資料庫程式自己的記憶體結構裡,這一層本身完全不具備斷電保護能力。
作業系統層級的快取,仍然停留在記憶體,尚未真正落到磁碟上。
描述這次修改內容的日誌被寫進持久儲存,這是多數資料庫定義「已經安全」的最低門檻。
資料真正落在磁碟的持久儲存介質上。
資料被複製到其他節點,即使整台機器故障,其他節點仍保有這份資料。
只停在記憶體的風險:如果資料庫只做到「資料進了 RAM 就回覆成功」,一旦發生斷電,RAM 裡的內容會直接消失,等於已經回報成功的資料實際上並不存在。
比較可靠的做法:WAL / Journal
比較可靠的資料庫通常會使用 WAL(Write-Ahead Log)或 Journal。以 PostgreSQL 為例,每次 transaction commit 並不會立刻把真正的 table data page 全部寫進磁碟,而是先確保描述這次修改的 WAL record 被寫進持久儲存,再告訴客戶端 transaction committed。即使真正的資料頁還沒完成寫入,重新開機後也能利用 WAL 把資料重做回來。PostgreSQL 官方文件也明確說明,正常的 synchronous commit 會等待 WAL flush 到永久儲存後才回覆成功,詳見 WAL Reliability。
這就是 ACID 裡面的 D:Durability。只要資料庫正式告訴使用端 transaction committed,就不應該因為普通的 process crash、OS crash 或 power loss 而消失。
| 縮寫 | 名稱 | 保證內容 |
|---|---|---|
| A | Atomicity(原子性) | 一筆 transaction 要嘛全部生效,要嘛完全不生效 |
| C | Consistency(一致性) | transaction 結束後,資料要符合資料庫定義的所有規則 |
| I | Isolation(隔離性) | 多個 transaction 同時執行時,彼此互不干擾 |
| D | Durability(持久性) | 一旦 commit 成功,就不能因為一般性的當機而消失 |
各家資料庫的 durability 機制一覽
| DB | 主要 durability 機制 | 一般預設行為 | 單機斷電保護 | 多機故障保護 |
|---|---|---|---|---|
| PostgreSQL | WAL + fsync | commit 等 WAL 持久化 | 強 | 取決於 replication 設定 |
| MySQL / InnoDB | Redo Log + fsync | commit flush redo log | 強 | 取決於 replication 設定 |
| MongoDB | Journal + Write Concern | 預設多為 majority | 強 | 很強,可要求 majority |
| DynamoDB | Multi-AZ replication | AWS 自動處理 | 很強 | 預設跨 3 個 AZ |
| SQL Server | Transaction Log | commit 等 log 持久化 | 強 | 取決於 Availability Group 等設定 |
| Redis | RDB / AOF | durability 可高度調整 | 預設不像傳統 RDBMS 那麼強 | 取決於 replication / persistence 設定 |
越安全,代價就是效能
這裡碰到一個非常重要的資料庫設計思想:越要求高 durability,一次 write 需要等待的事情就越多,延遲與成本也會跟著上升。
很多 NoSQL 系統真正提供的並不是「不保證資料」,而是把選擇權交出來:由使用端決定這次資料值得付出多少 consistency、durability、latency 成本。
不同情境需要的 durability 程度不一樣
- 銀行轉帳:非常在乎 durability,不能有任何遺失。
- Like 數計數器:偶爾掉一個可能可以接受。
- Session cache:掉了之後重新登入也許就足夠。
- Analytics event:可能可以接受批次或非同步處理。
- 訂單付款:絕對不能隨便遺失。
真正該記住的是,不要只看到 API 回傳 success,就假設已經知道這個 success 背後的 durability semantics。現代資料庫的 durability 並不是單純「SQL 有、NoSQL 沒有」,真正的差異是它預設把「成功」定義在哪個層級,以及允許怎麼調整 durability 與 performance 之間的交換。
接下來幾個分頁,依序拆解 PostgreSQL、MySQL、SQL Server、MongoDB、DynamoDB、Redis 各自的 durability 機制與可調整項目。
PostgreSQL:WAL 是 durability 的核心
PostgreSQL 正常的 synchronous commit 流程如下,重點在於它並不是等真正的 table data page 寫入 SSD,只需要 WAL 已經 durable 就足夠。
修改記憶體裡的 page。
把這次修改描述成一筆 WAL record。
確保這筆 WAL record 已經落到永久儲存。
此時才回覆客戶端 commit 成功。
PostgreSQL 官方文件正是這樣描述 WAL 的作用:先確保描述修改的 WAL record 進入永久儲存,data page 不需要在每次 commit 時同步落盤。
可調整項:synchronous_commit
PostgreSQL 允許把 synchronous_commit 設成 off,流程會變成先回覆成功,稍後才由 WAL writer 補寫。
這時如果發生 crash,可能出現「資料庫已經回覆這筆 transaction 成功,但實際上這筆 transaction 還沒真正 durable」的落差,也就是最近幾筆 transaction 可能遺失。PostgreSQL 的設計仍然保證,即使遺失最近的 transaction,也不會因此讓資料庫變成不一致的狀態。
PostgreSQL 官方特別區分 synchronous_commit=off 與更危險的 fsync=off:後者在 OS 或硬體發生 crash 時,甚至可能導致無法恢復的資料損毀,兩者的風險等級並不相同。
| 設定 | commit 回覆時機 | crash 後果 |
|---|---|---|
| 預設(synchronous commit) | WAL durable 之後才回覆 success | 已 commit 的 transaction 不會遺失 |
synchronous_commit = off | 先回覆 success,稍後才補寫 WAL | 可能遺失最近幾筆 transaction,但資料庫仍保持一致 |
fsync = off | 連 WAL 本身都可能沒有真正落盤 | OS / 硬體 crash 時可能造成不可恢復的損毀 |
MySQL:InnoDB 的概念跟 PostgreSQL 非常像
這裡特別要說 MySQL 加上 InnoDB,因為 MySQL 本身是 DBMS,實際行為會受 storage engine 影響。InnoDB 的核心機制是 Redo Log。
修改 buffer pool。
把修改描述寫進 redo log。
把 redo log flush 到磁碟。
此時才回覆成功。
可調整項:innodb_flush_log_at_trx_commit
| 設定值 | 行為 | power loss 後果 |
|---|---|---|
1(預設) | 每次 transaction commit 都 write log 並 flush disk 才回覆成功,這是 MySQL 官方要求的完整 ACID durability 設定 | 不會遺失已 commit 的 transaction |
2 | commit 先 write 並回覆成功,大約每秒才真正 flush 一次 | 可能損失最近尚未 flush 的 transaction |
0 | write 與 flush 都主要變成週期性處理 | 可能損失更大範圍尚未寫入的 transaction |
對照兩套系統,可以看到相同的哲學:PostgreSQL 用 WAL 搭配 synchronous_commit 調整 durability 與效能,MySQL InnoDB 用 Redo Log 搭配 innodb_flush_log_at_trx_commit 調整同一件事,兩者的取捨方式非常接近。
另外,如果 MySQL 涉及 binary log 與 replication,官方對高 durability 的建議還包括同時設定 innodb_flush_log_at_trx_commit = 1 與 sync_binlog = 1,詳見 MySQL 官方文件。
SQL Server:Transaction Log,本質上是同一派
先在記憶體裡修改資料頁。
把修改描述寫進 transaction log。
確保 log 已經落到永久儲存。
Microsoft 把這個預設行為稱為 Fully Durable Transaction。
到這裡可以看出同一個 pattern:PostgreSQL 的 WAL、MySQL 的 Redo Log、SQL Server 的 Transaction Log,三個名稱不同,但共同的基本思想是不急著把整個 data page 寫完,而是先把「如何恢復這次 transaction」的日誌安全保存下來,再回覆成功。
可調整項:Delayed Durability
SQL Server 也提供 Delayed Durability,開啟後流程會變成先回覆成功,log 之後才批次 flush。
在這種模式下,crash 發生時最近的 transaction 可能會消失。Microsoft 官方也明確表示,delayed durable transaction 要等到 transaction log 真正 flush 到 disk 之後,才算真正變成 durable。
MongoDB:Journal 之外,多了 Write Concern
MongoDB 有 Journal 機制,概念跟 WAL 很像,write 之後先進 journal 再進 disk。MongoDB 官方也把 journaling 描述為用 write-ahead logging 提供 crash durability。除此之外,MongoDB 多了一個需要理解的概念:Write Concern,用來決定要等多少節點確認,才算真正完成這次寫入。
| Write Concern | 意義 |
|---|---|
{ w: 1 } | Primary 收到寫入就先回覆 ACK |
{ w: "majority" } | 等多數節點都達到要求,才回覆 ACK |
{ j: true } | 要求相關 write 進入 on-disk journal 之後,才回覆 ACK |
MongoDB 官方目前說明,大多數部署情境下 implicit 的預設 write concern 是 { w: "majority" }。在預設 writeConcernMajorityJournalDefault=true 的一般 replica-set 設定中,majority acknowledgment 同時也要求 on-disk journaling,這跟早期 MongoDB 給人的刻板印象已經差很多。
majority acknowledgment 涵蓋的失效範圍不同
單機 PostgreSQL 的 WAL 落盤,跟 MongoDB replica set 要求 majority durable,兩者涵蓋的是不同層級的 failure model。
如果只有單機的 PostgreSQL,一旦整台伺服器故障,只能靠 replication 或 backup 補救。而 MongoDB 搭配 majority write concern,即使 Primary 整台故障,只要多數節點已經確認寫入,資料仍然安全。這代表沒有「SQL durability 一定大於 NoSQL durability」這種結論,真正要問的是這次寫入要求什麼樣的 acknowledgment semantics。
DynamoDB:由 AWS 處理 infrastructure durability
DynamoDB 是 managed distributed database,幾乎不需要思考 fsync、WAL file、SSD controller 或 server crash 這些細節,因為 AWS 已經把底層基礎設施抽象掉了。DynamoDB 在一個 Region 裡會自動把資料複製到三個 Availability Zone,AWS 把這件事作為其內建高 durability 設計的一部分。
AZ-A
其中一份資料複本。
AZ-B
另一份資料複本。
AZ-C
第三份資料複本。
因此這裡的 failure model 已經不只是單一硬碟壞掉,而是同時考慮 server failure、storage failure,甚至整個 AZ failure,這正是 managed distributed DB 吸引人的地方。
durability 不等於 consistency
使用 DynamoDB 時要特別小心一件事:durability 不等於 consistency。資料已經安全保存,跟某次 read 是否立刻看到最新資料,是完全不同的兩件事。
Durability
資料會不會消失。
Consistency
目前讀到的是不是最新資料。
後續延伸
ACID 與 CAP 的討論會延伸這個區分。
Redis:以 RAM 為中心,Persistence 是額外機制
Redis 的設計中心首先是 RAM,所以一筆 SET user:1 ... 只要進到記憶體就會非常快。Persistence 屬於額外機制,主要有兩種:RDB 與 AOF。
RDB:定期拍照式快照
RDB 比較像定期幫整個資料庫拍一次照片。
完成一次完整快照。
完成下一次快照。
距離上一次快照已經過了 4 分鐘。
這次快照還沒執行,crash 已經發生。
如果 crash 發生在 12:09,介於 12:05 快照與下一次快照之間的資料就可能遺失,因為 RDB 只保證「最近一次快照當下」的狀態,不保證快照之間的每一筆寫入。
相較於 RDB 的定期快照,AOF 會持續記錄每一筆寫入指令,可以把資料遺失的視窗縮到更小,但需要付出額外的效能與磁碟成本,這也回到最前面提到的原則:durability 越強,代價越高,最終要依情境決定合理的取捨。


