0

PostgreSQL Bài 33: Cơ chế WAL (Write-Ahead Logging), Checkpoint và kiến trúc khôi phục thảm họa (Crash Recovery)

Trong bộ 4 tính chất kinh điển ACID của cơ sở dữ liệu, chữ D (Durability - Tính bền vững) là lời cam kết đanh thép nhất của hệ thống: "Một khi tôi đã báo cho bạn biết giao dịch đã COMMIT thành công, thì dữ liệu đó sẽ không bao giờ mất, kể cả khi máy chủ bị rút điện cái rụp ngay giây tiếp theo".

Nhưng để thực hiện lời hứa này, PostgreSQL phải đối mặt với một nghịch lý vật lý vật lý:

  • Ổ đĩa cứng (Disk/SSD) thì chậm, đọc ghi ngẫu nhiên (Random I/O) trên hàng triệu Data Pages (8KB) sẽ làm hệ thống chậm như rùa bò.

  • Bộ nhớ đệm (RAM / shared_buffers) thì cực kỳ nhanh, nhưng chỉ cần cúp điện là bốc hơi toàn bộ dữ liệu.

Làm thế nào để vừa đạt được tốc độ của RAM, vừa có được sự an toàn của Đĩa cứng? Chìa khóa chính là cơ chế WAL (Write-Ahead Logging) kết hợp với tiến trình Checkpoint.

1. Dòng chảy dữ liệu: Shared Buffers và Dirty Pages

Khi bạn thực hiện một lệnh UPDATE, PostgreSQL không bao giờ ghi trực tiếp sự thay đổi đó xuống file dữ liệu (.heap) trên ổ cứng ngay lập tức.

Quy trình thực tế diễn ra như sau:

  1. Engine kiểm tra xem trang dữ liệu (Data Page) chứa dòng cần sửa đã nằm trên bộ nhớ RAM (shared_buffers) chưa. Nếu chưa, nó sẽ đọc từ đĩa lên RAM.

  2. Việc sửa đổi (UPDATE, INSERT, DELETE) được thực hiện trực tiếp trên bộ nhớ RAM.

  3. Trang dữ liệu trên RAM vừa bị sửa đổi này giờ đây khác biệt so với bản sao của nó dưới ổ cứng. Nó được dán nhãn là một Dirty Page (Trang bẩn).

  4. Cùng lúc đó, thông tin về sự thay đổi (chỉ phần thay đổi, không phải cả trang 8KB) được ghi vào một bộ đệm khác trên RAM gọi là wal_buffers.

2. Nguyên lý Write-Ahead Logging (WAL)

Chuyện gì xảy ra nếu tại thời điểm này máy chủ bị mất điện? Toàn bộ Dirty Pages trên RAM sẽ bị xóa sạch, và dữ liệu sẽ mất vĩnh viễn!

Để ngăn chặn thảm họa, PostgreSQL áp dụng quy tắc sống còn của thuật toán Write-Ahead Logging (Ghi nhật ký trước):

Quy tắc WAL: "Bất kỳ sự thay đổi nào trên dữ liệu cũng bắt buộc phải được ghi nhật ký (Log) và xả (flush / fsync) an toàn xuống ổ đĩa cứng TRƯỚC KHI hệ thống trả về thông báo COMMIT thành công cho client".

MÔ HÌNH HOẠT ĐỘNG KHI GỌI LỆNH COMMIT:

Client ──(COMMIT)──► [ PostgreSQL ]
                         │
                         ├── 1. Ghi thay đổi vào `wal_buffers` (RAM).
                         ├── 2. Gọi lệnh hệ điều hành `fsync()` ép xả `wal_buffers` xuống file WAL trên đĩa.
                         │      (Thao tác này là GHI TUẦN TỰ - Sequential I/O ──► Rất nhanh!)
                         │
                         └── 3. Sau khi file WAL đã nằm an toàn trên đĩa ──► Trả về SUCCESS cho Client.
                                (Lúc này, Data Page thực tế VẪN LÀ DIRTY PAGE TRÊN RAM, chưa hề được ghi xuống đĩa).

Tại sao ghi WAL lại nhanh hơn ghi Data Page?

  • Ghi vào Data Page là Ghi ngẫu nhiên (Random I/O): Đầu từ của ổ đĩa phải nhảy lung tung khắp nơi để tìm đúng vị trí của bảng, của index.

  • Ghi vào file WAL (thường có dung lượng 16MB/file trong thư mục pg_wal) là Ghi tuần tự (Sequential I/O): Dữ liệu cứ thế được ghi nối tiếp nhau vào cuối file (Append-only). Ổ đĩa, đặc biệt là HDD truyền thống, xử lý ghi tuần tự nhanh hơn ghi ngẫu nhiên hàng chục đến hàng trăm lần.

(Mỗi bản ghi trong WAL được đánh dấu bằng một số định danh tăng dần duy nhất gọi là LSN - Log Sequence Number, ví dụ: 0/1AE5420).

3. Tiến trình Checkpoint: Người dọn rác cần mẫn

Nhờ WAL, dữ liệu của bạn đã an toàn. Nhưng nếu cứ để mặc cho Dirty Pages tích tụ trên RAM, thì khi RAM đầy hệ thống sẽ sụp đổ. Hơn nữa, nếu máy chủ chạy liên tục 1 năm rồi mới sập, khi khởi động lại, PostgreSQL sẽ phải đọc và phát lại (Replay) file WAL của trọn vẹn 1 năm đó để khôi phục các Dirty Pages bị mất ──► Có thể tốn hàng tuần để khởi động xong database!

Vì vậy, chúng ta cần một tiến trình định kỳ xả các Dirty Pages từ RAM xuống ổ đĩa thực sự. Đó chính là Checkpoint Process (Tiến trình điểm chốt).

3.1. Nhiệm vụ của Checkpoint

  1. Đẩy lệnh xả (fsync) toàn bộ các Dirty Pages hiện có trên bộ nhớ shared_buffers xuống các file dữ liệu tương ứng trên ổ cứng.

  2. Đánh dấu mốc thời gian hoàn tất (Checkpoint Record) vào một file cấu hình hệ thống siêu nhỏ là pg_control.

  3. Các file WAL chứa lịch sử cũ trước mốc Checkpoint này giờ đây không còn cần thiết nữa (vì dữ liệu đã nằm an toàn trên đĩa). Tiến trình sẽ tự động xóa hoặc tái sử dụng (recycle) các file WAL cũ để giải phóng không gian ổ cứng.

3.2. Kiểm soát I/O của Checkpoint

Quá trình xả hàng gigabytes Dirty Pages xuống đĩa cùng một lúc (Checkpoint Spike) sẽ gây nghẽn I/O (I/O Hang), làm hệ thống đột ngột phản hồi rất chậm trong vài phút.

PostgreSQL giải quyết bằng cơ chế dàn trải I/O:

  • checkpoint_timeout: Khoảng thời gian tối đa giữa 2 lần checkpoint (Mặc định 5 phút, trên Production thường set 15 - 30 phút).

  • checkpoint_completion_target: (Mặc định 0.9). Thông số này yêu cầu tiến trình Checkpoint phải từ từ ghi dữ liệu sao cho việc xả đĩa kéo dài suốt 90% khoảng thời gian chờ đến lần checkpoint tiếp theo (ví dụ kéo dài việc ghi đĩa đều đặn trong 27 phút thay vì dội bom I/O trong 1 phút).

4. Thảm họa Torn Page (Trang bị rách) và Full Page Writes (FPW)

Có một nguy cơ thảm họa cực độ tinh vi: Sự lệch pha về kích thước Block.

  • Một Data Page của PostgreSQL luôn là 8 KB.

  • Nhưng Block size ghi đĩa của Hệ điều hành (Linux) / Ổ cứng thường chỉ là 4 KB.

Điều gì xảy ra nếu cúp điện trong lúc hệ thống đang thực hiện Checkpoint? Tiến trình đang xả một trang 8 KB xuống đĩa. Nó mới ghi được 4 KB đầu tiên (thành công), đang định ghi 4 KB tiếp theo thì CÚP ĐIỆN. Kết quả: Trang 8 KB trên ổ đĩa giờ đây là một sản phẩm lai tạo - nửa đầu là dữ liệu mới, nửa sau là dữ liệu cũ. Khối 8 KB này bị hỏng vĩnh viễn (gọi là Torn Page). Thảm họa ở chỗ, WAL cũng không thể sửa được lỗi này vì WAL chỉ lưu "sự thay đổi" chứ không lưu trọn vẹn dữ liệu gốc để chắp vá.

Giải pháp: Full Page Writes (full_page_writes = on)

Mặc định tính năng này luôn bật. Cơ chế hoạt động:

"Sau mỗi lần Checkpoint hoàn tất, trong lần CHỈNH SỬA ĐẦU TIÊN lên một Data Page bất kỳ, PostgreSQL sẽ ghi toàn bộ hình ảnh nguyên bản 8 KB (Full Page Image) của trang đó vào file WAL, thay vì chỉ ghi phần thay đổi."

Nhờ có bản sao lưu toàn bộ hình ảnh gốc của trang 8 KB trong WAL, nếu hiện tượng Torn Page xảy ra khi sập nguồn, PostgreSQL có thể dễ dàng chép đè lại trang 8 KB toàn vẹn từ WAL lên ổ cứng để làm lại từ đầu.

5. Kiến trúc Crash Recovery (Khôi phục thảm họa)

Khi máy chủ bị cúp điện đột ngột và bạn khởi động lại PostgreSQL (Postmaster process start), nó sẽ tự động nhận diện hệ thống chưa được shut down hợp lệ và tiến hành chu trình Crash Recovery.

QUY TRÌNH HỒI SINH TỪ THẢM HỌA (REDO PHASE):

Bước 1: Đọc file `pg_control`
  └── Tìm vị trí (LSN) của điểm Checkpoint hoàn tất gần nhất.
      (Mọi dữ liệu trước LSN này chắc chắn đã nằm an toàn trên đĩa).

Bước 2: Bắt đầu đọc file WAL
  └── Bắt đầu từ mốc LSN tìm được ở Bước 1, engine tiến về phía trước (Roll-forward).

Bước 3: Phát lại (Redo) các thao tác
  ├── Nếu gặp bản ghi sửa đổi bình thường ──► Áp dụng (Replay) sự thay đổi đó lên Data Page trên RAM.
  └── Nếu gặp một trang bị hỏng (Torn Page) ──► Trích xuất "Full Page Image" 8KB từ WAL để đè lên, chữa lành dữ liệu, sau đó áp dụng tiếp các thay đổi.

Bước 4: Hoàn thành Redo
  └── Khi đọc đến cuối file WAL hiện tại, dữ liệu trên RAM (Dirty Pages) đã khôi phục lại ĐÚNG TRẠNG THÁI ngay trước phần nghìn giây bị sập nguồn.
      Database mở kết nối, sẵn sàng phục vụ!

6. Tóm tắt & Bài tiếp theo

  • Ghi đĩa ngẫu nhiên quá chậm, nên Postgres cập nhật dữ liệu trên RAM (Dirty Pages) và đảm bảo an toàn bằng cách ghi tuần tự vào nhật ký WAL (Write-Ahead Log).

  • Lệnh COMMIT chỉ thành công khi WAL đã được gọi hàm hệ thống fsync xả an toàn xuống ổ đĩa.

  • Tiến trình Checkpoint định kỳ xả các Dirty Pages từ RAM xuống đĩa thật để giải phóng file WAL cũ và rút ngắn thời gian khởi động lại.

  • Torn Page xảy ra do ghi hỏng 1 phần của khối 8KB; được phòng ngừa bằng tính năng Full Page Writes (FPW) lưu bản sao nguyên vẹn của trang vào WAL ở lần sửa đổi đầu tiên.

  • Khi khởi động lại sau crash, Postgres tự động bắt đầu từ mốc LSN Checkpoint gần nhất trong pg_control và REDO (phát lại) các lệnh trong file WAL để tái tạo lại RAM.

Bài 34 xem tiếp: Streaming Replication và Kiến trúc High Availability (HA) với Master-Slave — chúng ta sẽ thấy WAL không chỉ dùng để cứu hộ! Nếu chúng ta liên tục sao chép (stream) file WAL qua mạng TCP từ máy chủ Master sang máy chủ Slave, máy chủ Slave có thể liên tục phát lại (Redo) các WAL đó để tạo ra một bản sao y hệt theo thời gian thực (Hot Standby) phục vụ cân bằng tải đọc và dự phòng sập hệ thống (Failover).


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí