Tối ưu 6 — Replication & High Availability

13 thg 7, 2026 3 lượt xem
#postgresql
#sql
#replication
#high-availability
#wal
#failover

Tối ưu 6 — Replication & High Availability

Cho đến giờ chúng ta đã xoay quanh một máy chủ CSDL: đánh index, viết truy vấn tốt, bảo trì VACUUM, giám sát. Nhưng một máy chủ dù mạnh đến đâu cũng có hai giới hạn khó vượt: nó có thể chết (đĩa hỏng, mất điện, kernel panic), và nó chỉ có bấy nhiêu CPU/IO để phục vụ mọi truy vấn đọc lẫn ghi. Replication (nhân bản) là câu trả lời cho cả hai: giữ một hoặc nhiều bản sao dữ liệu trên các máy chủ khác, luôn cập nhật gần thời gian thực.

Bài này tập trung vào PostgreSQL — hệ có mô hình replication rõ ràng và phổ biến bậc nhất — và thỉnh thoảng đối chiếu với MySQL (binlog) để bạn thấy bức tranh chung. Chúng ta sẽ đi từ vì sao cần replication, đến nền tảng WAL, rồi hai họ replication (physical/streaminglogical), chuyện đồng bộ vs bất đồng bộ, replication lag, và cuối cùng là failover & HA với Patroni.

Lưu ý đọc bài: Toàn bộ cấu hình postgresql.conf, pg_hba.conf, lệnh shell và pg_basebackup ở đây là (minh hoạ) — dùng để hiểu cơ chế, không phải để chạy trong sandbox. Chỉ vài truy vấn SELECT giám sát là chạy được trên hệ thật.

Vì sao cần replication?

Có bốn động cơ, thường đi cùng nhau:

  • Chịu lỗi (HA/DR). Nếu primary chết, một standby có thể được promote (thăng cấp) thành primary mới. Đây là nền tảng của High Availability (sẵn sàng cao) và Disaster Recovery (khắc phục thảm hoạ — đặt bản sao ở data center khác).
  • Chịu tải đọc. Ứng dụng đọc nhiều hơn ghi rất nhiều lần. Ta có thể trỏ các truy vấn SELECT sang replica để giảm tải cho primary — chủ đề của bài Tách đọc/ghi.
  • Backup gần thời gian thực. Standby là một bản sao luôn cập nhật. Kết hợp với WAL archive, nó cho phép Point-In-Time Recovery (xem Bảo trì & WAL).
  • Báo cáo tách khỏi primary. Các truy vấn phân tích nặng (report, BI) chạy trên replica sẽ không "bóp nghẹt" hệ thống giao dịch chính.

Hai chỉ số luôn xuất hiện khi bàn về HA: RPO (Recovery Point Objective — mất tối đa bao nhiêu dữ liệu, đo bằng thời gian) và RTO (Recovery Time Objective — mất bao lâu để phục vụ trở lại). Replication đồng bộ nhắm tới RPO = 0; kiểu bố trí và công cụ failover quyết định RTO.

WAL — nền tảng của mọi thứ

Trước khi hiểu replication, phải hiểu WAL (Write-Ahead Log). Nguyên tắc: PostgreSQL không ghi thẳng thay đổi vào file dữ liệu trước. Nó ghi mô tả thay đổi vào một nhật ký tuần tự (WAL) trước, rồi mới lần lượt áp vào file dữ liệu. Nếu máy chủ sập, khi khởi động lại nó phát lại (replay) WAL để đưa dữ liệu về trạng thái nhất quán.

Ý tưởng thiên tài của replication vật lý là: nếu standby cũng phát lại đúng dòng WAL đó, nó sẽ có dữ liệu y hệt primary. WAL vốn được sinh ra cho mục đích crash recovery, nay được tái sử dụng làm "dòng chảy" nhân bản. Ở MySQL, vai trò tương tự do binary log (binlog) đảm nhiệm, nhưng binlog ghi ở tầng logic (câu lệnh/hàng thay đổi) chứ không phải mức khối vật lý — một khác biệt sẽ trở lại ở phần logical replication.

Physical / Streaming Replication

Đây là kiểu replication mặc định người ta nghĩ tới với PostgreSQL. Primary stream (truyền liên tục) các WAL record qua mạng tới standby; standby phát lại chúng.

Đặc điểm cốt lõi:

  • Byte-to-byte giống primary. Standby là bản sao vật lý ở mức khối: cùng toàn bộ database, cùng cấu trúc file, cùng các block. Bạn không chọn được "chỉ nhân bảng X". Cả cụm đi cùng nhau.
  • Hot standby. Mặc định standby ở chế độ read-only nhưng vẫn cho phép đọc trong khi tiếp tục phát lại WAL. Đây chính là cơ sở để dùng replica cho truy vấn đọc.
  • Cùng phiên bản major. Vì nhân ở mức vật lý, primary và standby phải cùng major version (ví dụ cùng PG 16), thường cùng kiến trúc.

Dựng một standby (minh hoạ)

Trên standby, ta chụp một base backup từ primary rồi để nó bắt đầu stream:

# (minh hoạ) — chạy trên máy standby
pg_basebackup \
  --host=primary.internal --port=5432 --username=replicator \
  --pgdata=/var/lib/postgresql/16/main \
  --wal-method=stream \
  --write-recovery-conf \          # tự ghi primary_conninfo + standby.signal
  --slot=standby1 --create-slot \  # tạo & dùng replication slot
  --checkpoint=fast --progress

--write-recovery-conf tạo file standby.signal (đánh dấu đây là standby) và ghi thông tin kết nối vào cấu hình:

# (minh hoạ) — postgresql.auto.conf trên standby
primary_conninfo = 'host=primary.internal port=5432 user=replicator application_name=standby1'
primary_slot_name = 'standby1'

Trên primary, cần bật quyền cho user nhân bản và cho phép kết nối replication:

# (minh hoạ) — postgresql.conf trên primary
wal_level = replica            # đủ cho physical replication
max_wal_senders = 10           # số tiến trình gửi WAL đồng thời
# (minh hoạ) — pg_hba.conf trên primary
host  replication  replicator  10.0.0.0/24  scram-sha-256

Replication slot — giữ WAL cho standby

Bình thường primary tự do xoá/recycle WAL cũ sau khi đã checkpoint. Vấn đề: nếu một standby tạm mất kết nối vài phút rồi quay lại nhưng đoạn WAL nó cần đã bị xoá, standby sẽ hỏng và phải dựng lại từ đầu.

Replication slot giải quyết đúng chuyện này: primary theo dõi standby đã nhận WAL tới đâu và cam kết giữ lại mọi WAL mà standby chưa xác nhận. Standby ngắt rồi nối lại vẫn tiếp tục được.

Nhưng con dao hai lưỡi rất quan trọng phải nhớ: nếu standby chết hẳn mà slot vẫn còn, primary sẽ giữ WAL mãi mãi, WAL dồn lại và có thể lấp đầy đĩa của primary → primary sập. Vì thế phải giám sát các slot và xoá slot của standby đã bỏ (PG 13+ có max_slot_wal_keep_size để đặt trần an toàn).

-- ▶ giám sát slot (chạy được trên hệ thật)
SELECT slot_name, active,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS wal_giu_lai
FROM pg_replication_slots;

Cascading replication

Standby cũng có thể truyền tiếp WAL cho standby khác — gọi là cascading (nối tầng). Thay vì mọi standby đều "hút" WAL từ primary (tốn băng thông và tiến trình wal_sender của primary), ta cho standby cấp 1 nuôi các standby cấp 2. Rất hữu ích khi có nhiều replica hoặc replica đặt ở vùng địa lý xa.

Đồng bộ vs Bất đồng bộ

Đây là quyết định thiết kế quan trọng nhất trong replication, vì nó đánh đổi trực tiếp giữa an toàn dữ liệuđộ trễ ghi.

  • Asynchronous (bất đồng bộ) — mặc định. Primary commit giao dịch ngay khi ghi WAL cục bộ xong, không chờ standby xác nhận. Nhanh, độ trễ thấp. Nhưng nếu primary sập trước khi standby kịp nhận các WAL record cuối, những giao dịch đó mất khi standby được promote → RPO > 0 (mất một khoảng dữ liệu, thường nhỏ nhưng khác 0).

  • Synchronous (đồng bộ). Primary chờ ít nhất một standby xác nhận đã nhận (và tuỳ mức, đã ghi/đã áp) WAL trước khi báo commit thành công cho client. Đánh đổi: mỗi lần ghi tốn thêm một vòng round-trip mạng tới standby → latency ghi tăng. Đổi lại: giao dịch đã báo commit chắc chắn tồn tại ở nhiều nơi → RPO = 0.

Cấu hình đồng bộ dựa trên hai tham số:

# (minh hoạ) — postgresql.conf trên primary
synchronous_standby_names = 'ANY 1 (standby1, standby2)'  # quorum
synchronous_commit = on

synchronous_commit có nhiều mức thể hiện primary chờ tới đâu:

  • off / local: không chờ standby (thực chất là async về mặt nhân bản).
  • remote_write: chờ standby nhận và ghi WAL vào OS của nó (chưa chắc đã flush xuống đĩa).
  • on: chờ standby flush WAL an toàn xuống đĩa — mức đồng bộ điển hình, chống mất dữ liệu.
  • remote_apply: chờ standby đã áp (replay) WAL, nghĩa là đọc trên standby ngay sau đó chắc chắn thấy dữ liệu mới. An toàn nhất cho read-your-writes, nhưng độ trễ cao nhất.

synchronous_standby_names quyết định aibao nhiêu standby phải xác nhận. Dạng ANY 1 (s1, s2)quorum-based: chỉ cần bất kỳ 1 trong hai standby xác nhận là đủ. Điều này quan trọng cho HA: nếu bạn ép "phải là standby1 cụ thể", mà standby1 chết, thì mọi commit trên primary sẽ treo chờ nó. Quorum "ANY 1 trong nhiều" cho bạn độ an toàn RPO=0 mà vẫn không phụ thuộc vào một standby duy nhất.

Một cấu hình HA cân bằng phổ biến: 1 primary + 2 async replica + đặt ANY 1 (r1, r2) đồng bộ. Commit chỉ cần 1 trong 2 replica xác nhận → không mất dữ liệu đã commit, nhưng vẫn chịu được việc 1 replica chết.

Sơ đồ tổng quan

Sơ đồ minh hoạ: primary stream WAL tới nhiều standby; standby1 đồng bộ (RPO=0), standby2 bất đồng bộ, standby3 nhận WAL kiểu cascading từ standby2. Ứng dụng đọc từ các standby; khi primary chết, một standby được promote lên làm primary mới.

Replication lag — độ trễ nhân bản

Vì standby luôn đi sau primary một khoảng (dù rất nhỏ với async), khoảng chênh đó gọi là replication lag. Đọc từ replica nghĩa là bạn có thể thấy dữ liệu cũ một chút — đây là hệ quả trực tiếp cần cân nhắc khi tách đọc/ghi.

Nguyên nhân lag thường gặp:

  • Standby yếu hơn primary (CPU/IO chậm), replay không kịp.
  • Băng thông mạng giữa primary và standby hạn chế hoặc xa về địa lý.
  • Trên standby có truy vấn đọc dài đang chạy, làm chậm việc áp WAL (xung đột giữa replay và query — liên quan max_standby_streaming_delay).
  • Primary đang ghi cực nhiều (bulk load), tạo WAL nhanh hơn standby áp được.

Đo lag — quan trọng để giám sát (xem thêm Giám sát):

-- ▶ trên PRIMARY: xem trạng thái các standby đang kết nối
SELECT application_name, state, sync_state,
       write_lag, flush_lag, replay_lag
FROM pg_stat_replication;
-- ▶ trên STANDBY: độ trễ replay tính theo thời gian
SELECT now() - pg_last_xact_replay_timestamp() AS replay_lag;

pg_stat_replication cho ba loại lag phân theo giai đoạn: write_lag (nhận & ghi), flush_lag (flush xuống đĩa), replay_lag (đã áp và hiển thị cho truy vấn đọc). Với read/write splitting, replay_lag là con số bạn quan tâm nhất: nó nói khách đọc từ replica trễ bao lâu so với thực tế.

Logical Replication

Physical replication nhân toàn bộ cụm ở mức khối. Logical replication thì khác hẳn: nó nhân theo bảng, ở mức logic (các thao tác INSERT/UPDATE/DELETE của từng hàng), qua mô hình publication / subscription (nhà xuất bản / người đăng ký).

-- (minh hoạ) — trên nguồn: cần wal_level = logical
CREATE PUBLICATION pub_orders FOR TABLE orders, order_items;
-- (minh hoạ) — trên đích
CREATE SUBSCRIPTION sub_orders
  CONNECTION 'host=source.internal dbname=shop user=repl'
  PUBLICATION pub_orders;

Khác biệt cốt lõi so với physical:

  • Chọn lọc theo bảng. Bạn nhân một tập bảng, không phải cả cụm. Có thể nhiều publication tới nhiều đích khác nhau.
  • Khác major version. Vì nhân ở mức logic, đích có thể là PG version khác — cực kỳ hữu ích cho nâng cấp major với downtime tối thiểu (dựng cụm mới, đồng bộ logic, cắt sang).
  • Đích ghi được. Bảng đích là bảng thật, có thể có index/trigger riêng, thậm chí nhận thêm dữ liệu từ nguồn khác. Không phải bản sao read-only cứng như physical.
  • Không nhân toàn cụm. Không tự nhân DDL (thay đổi schema), sequence, hay các bảng ngoài publication — cần xử lý thủ công.

Vì thế logical replication là công cụ cho migration, tích hợp dữ liệu giữa các hệ, và gộp/tách dữ liệu. Nó cũng chính là nền tảng kỹ thuật của CDC (Change Data Capture): đọc dòng thay đổi logic từ WAL và đẩy sang hệ khác (ví dụ qua Debezium/Kafka). Chủ đề này được đào sâu trong series CDC/Kafka của knowledge base — logical decoding của PostgreSQL và binlog của MySQL đều là "nguồn" cho các đường ống CDC đó.

Failover & High Availability

Có replica là điều kiện cần, nhưng chưa đủ cho HA. Khi primary chết, ai đó phải: (1) phát hiện primary đã chết, (2) chọn standby phù hợp (thường là standby có LSN mới nhất), (3) promote nó thành primary, (4) trỏ ứng dụng sang primary mới. Làm thủ công thì RTO tính bằng chục phút và dễ sai. HA thực thụ cần tự động hoá.

Promote thủ công (minh hoạ)

# (minh hoạ) — thăng cấp một standby thành primary
pg_ctl promote -D /var/lib/postgresql/16/main
# hoặc: SELECT pg_promote();

Split-brain & fencing — hiểm hoạ lớn nhất

Split-brain (não phân đôi) là ác mộng của HA: do sự cố mạng, standby tưởng primary đã chết nên tự promote, trong khi primary cũ vẫn sống và vẫn nhận ghi. Giờ có hai primary cùng nhận dữ liệu khác nhau → khi mạng hồi phục, dữ liệu xung đột không thể hoà giải, gần như chắc chắn mất/hỏng dữ liệu.

Phòng chống bằng fencing (rào chặn): đảm bảo primary cũ bị vô hiệu hoá chắc chắn trước khi promote cái mới (ví dụ STONITH — "shoot the other node in the head", tắt máy cũ), và dùng một consensus store (etcd/consul) để cả cụm thống nhất ai mới là leader — không cho phép hai node cùng tự nhận là primary.

Patroni + etcd/consul

Patroni là công cụ HA phổ biến nhất cho PostgreSQL hiện nay. Cơ chế:

  • Mỗi node PostgreSQL chạy một tiến trình Patroni.
  • Patroni dùng một DCS (Distributed Configuration Store — etcd, consul, hoặc ZooKeeper) để bầu leader qua cơ chế lease/TTL. Node nào giữ được "leader key" (gia hạn liên tục) thì là primary.
  • Nếu primary treo và không gia hạn được key trong thời hạn (TTL), key hết hạn → Patroni trên các standby tổ chức bầu lại, chọn standby tốt nhất và promote tự động. Vì quyết định leader nằm ở DCS có quorum, split-brain được ngăn chặn ở gốc.
  • Ứng dụng kết nối qua một điểm trỏ động (HAProxy hỏi Patroni REST API "ai là leader", hoặc VIP — địa chỉ IP ảo được gán cho primary hiện tại). Sau failover, luồng ghi tự động đi tới primary mới.
# (minh hoạ) — trích patroni.yml
scope: pg-cluster
etcd3:
  hosts: 10.0.0.10:2379,10.0.0.11:2379,10.0.0.12:2379
bootstrap:
  dcs:
    ttl: 30                 # leader key hết hạn sau 30s không gia hạn
    loop_wait: 10
    synchronous_mode: true  # bật để không mất giao dịch đã commit

Các công cụ khác: repmgr (đơn giản hơn, thiên về quản lý/monitor cụm và hỗ trợ failover), Pgpool-II (thêm connection pooling và load-balancing đọc, nhưng cấu hình HA phức tạp hơn). Với MySQL, hệ sinh thái tương ứng gồm Orchestrator, Group Replication + InnoDB Cluster/MySQL Router.

Managed cloud (nhắc ngắn)

Các dịch vụ quản lý giấu phần lớn phức tạp này đi. Amazon RDS Multi-AZ duy trì một standby ở AZ khác và tự failover khi primary hỏng (thường là đồng bộ, nhắm RPO thấp). Amazon Aurora tách storage khỏi compute: một lớp lưu trữ phân tán nhân 6 bản qua 3 AZ, các replica dùng chung storage nên lag rất thấp và failover nhanh. Bù lại, bạn mất một phần quyền kiểm soát và phụ thuộc nhà cung cấp — nhưng với nhiều đội, đây là đánh đổi hợp lý.

Use case thực tế: cụm 1 primary + 2 replica, RPO = 0

Bối cảnh: hệ thống bán hàng, không được phép mất giao dịch đã commit, nhưng cũng không muốn treo khi một replica bảo trì.

Bố trí:

  • primary (AZ-a): read-write, đích ghi.
  • replica-1 (AZ-b), replica-2 (AZ-c): hot standby, phục vụ đọc và làm ứng viên failover.

Cấu hình đồng bộ quorum trên primary:

# (minh hoạ) — postgresql.conf trên primary
wal_level = replica
max_wal_senders = 10
synchronous_commit = on
synchronous_standby_names = 'ANY 1 (replica1, replica2)'

Ý nghĩa: mỗi commit chỉ trả về thành công khi ít nhất 1 trong 2 replica đã flush WAL. Nếu replica1 xuống bảo trì, replica2 vẫn gánh → primary không treo. Giao dịch đã báo commit chắc chắn tồn tại ở ≥ 2 node → RPO = 0.

Vận hành thường ngày:

  • Ứng dụng ghi vào primary, đọc phân tán sang 2 replica (bài Tách đọc/ghi).
  • Giám sát pg_stat_replication.replay_lag và slot; cảnh báo khi lag vượt ngưỡng hoặc WAL slot giữ lại quá lớn.

Kịch bản primary chết (failover với Patroni):

  1. Patroni trên primary không gia hạn được leader key trong etcd (primary treo).
  2. Sau TTL (ví dụ 30s), key hết hạn; các Patroni còn lại bầu lại.
  3. Patroni chọn replica có LSN mới nhất (giả sử replica1), fence primary cũ, promote replica1.
  4. VIP/HAProxy trỏ luồng ghi sang primary mới; replica2 được cấu hình lại để stream từ primary mới.
  5. Khi máy cũ hồi phục, nó được dựng lại làm standby (pg_rewind hoặc base backup mới).

RTO ở đây thường là vài chục giây (chủ yếu là TTL + thời gian promote), thay vì chục phút nếu làm tay.

Ghi nhớ

  • WAL là nền tảng. Physical replication = stream lại WAL; logical replication = giải mã WAL thành thao tác hàng theo bảng.
  • Physical/streaming: nhân cả cụm, byte-to-byte, cùng major version, hot standby đọc được. Logical: nhân theo bảng, khác version được, đích ghi được — hợp cho migration & CDC.
  • Async (mặc định): nhanh nhưng RPO > 0. Sync (synchronous_commit/synchronous_standby_names): RPO = 0 nhưng tăng latency ghi. Dùng quorum ANY 1 (…) để vừa an toàn vừa không treo khi một replica chết.
  • Replication slot giữ WAL cho standby chậm/ngắt kết nối — nhưng standby chết mà slot còn sẽ làm WAL dồn đầy đĩa primary. Luôn giám sát và đặt max_slot_wal_keep_size.
  • Replication lag khiến đọc từ replica thấy dữ liệu cũ. Đo bằng pg_stat_replication (replay_lag) và pg_last_xact_replay_timestamp().
  • HA cần tự động hoá + chống split-brain. Dùng Patroni + etcd/consul (bầu leader qua quorum, fencing) để failover an toàn; VIP/HAProxy trỏ ứng dụng sang primary mới.

Bài tiếp theo tận dụng chính các replica ta vừa dựng: Tách đọc/ghi (read/write splitting) — cách trỏ đọc sang replica mà vẫn xử lý đúng vấn đề replication lag. Xem thêm nền tảng ở Bảo trì & WALGiám sát; và để scale xa hơn một cụm master–replica, đọc Phân vùng & scaling.

Bài viết liên quan

Cách index hoạt động (B-Tree), đọc EXPLAIN, seq scan vs index scan và mẫu tối ưu truy vấn.

13 thg 7, 2026 4

Khoá, ràng buộc, quan hệ và chuẩn hoá 1NF/2NF/3NF — thiết kế lược đồ đúng từ đầu.

13 thg 7, 2026 4

Kết nhiều bảng đúng cách: các loại JOIN, bẫy thường gặp và phép hợp tập.

13 thg 7, 2026 3

Lọc, sắp xếp, gộp nhóm và hàm tổng hợp — nền tảng mọi truy vấn phân tích.

13 thg 7, 2026 3