Chương 10 · Scale Out

Replication & Sharding

Khi 1 node Postgres không đủ. Replication cho HA + read scale. Sharding để chia data. Consistent hashing. Failover đúng cách.

1. Scale theo chiều dọc vs ngang

Vertical scaling (scale up)

Mua server mạnh hơn — CPU nhiều core hơn, RAM nhiều hơn, NVMe nhanh hơn.

  • Ưu: đơn giản, không thay đổi app, không trade-off CAP.
  • Nhược: trần (server lớn nhất ~1TB RAM), giá phi tuyến (server 2× mạnh có thể đắt 5×), single point of failure.

Postgres 1 node có thể xử lý hàng terabyte data + 50k QPS — đừng vội scale ngang trước khi vertical đến giới hạn.

Horizontal scaling (scale out)

Thêm nhiều server, chia tải.

  • Ưu: scale gần như vô hạn (Google quản tỉ tỉ row), HA tự nhiên.
  • Nhược: phức tạp (replication, sharding, distributed transaction), CAP trade-off, app phải biết shard.

Thứ tự thực dụng: Vertical → Read Replica → Caching → Sharding. Đa số app dừng ở bước 2-3.

2. Replication — sao chép dữ liệu

Mục đích chính:

  • High Availability (HA) — primary chết, promote replica làm primary mới.
  • Read scaling — phân tán read sang nhiều replica.
  • Backup live — replica có thể dùng làm backup không cản primary.
  • Geo-distribution — replica ở data center xa user → low latency.

2.1. Cách Postgres replicate

Postgres dùng WAL streaming:

  1. Primary ghi mọi thay đổi vào WAL (như đã học Ch6).
  2. Replica connect TCP, đọc WAL stream.
  3. Replica apply WAL vào data file local → state đồng bộ.
# postgresql.conf trên primary:
wal_level = replica           # hoặc 'logical' để hỗ trợ logical replication
max_wal_senders = 10
archive_mode = on             # archive WAL cho PITR
archive_command = 'cp %p /archive/%f'

# pg_hba.conf:
host  replication  replicator  10.0.0.0/8  scram-sha-256

# Trên replica, copy base backup từ primary:
pg_basebackup -h primary-host -D /var/lib/pg/data -U replicator -X stream -P

# postgresql.conf trên replica:
primary_conninfo = 'host=primary-host user=replicator password=...'
hot_standby = on              # cho phép read trên replica

2.2. Logical vs Physical Replication

Physical (streaming)Logical
Cách hoạt độngReplicate WAL bytes y nguyênReplicate "changes" qua publication/subscription
GranularityToàn DBBảng/schema cụ thể
Cross-versionKhông (cần cùng version)Có (Postgres 12 → 16)
Read-only replicaYes (hot standby)Replica có thể write
Use caseHA, full read scaleMigration, multi-region partial, ETL

3. Replication Topologies

3.1. Primary-Replica (Master-Slave)

Writes │ ▼ ┌────────┐ │PRIMARY │ ←── đọc + ghi └───┬────┘ │ WAL ┌────┴────────┐ ▼ ▼ ┌──────┐ ┌──────┐ │REPLICA│ │REPLICA│ ←── chỉ đọc └──────┘ └──────┘

Phổ biến nhất. Đơn giản, đa số use case OK.

3.2. Cascading Replication

PRIMARY │ ▼ REPLICA-A (intermediate) │ ▼ REPLICA-B (replicate from A, không từ primary)

Giảm tải WAL stream trên primary khi có nhiều replica. Postgres 9.2+ hỗ trợ.

3.3. Multi-Region

[Region: APAC] ┌──────────────┐ ←─── │ PRIMARY │ ←── writes └──────┬───────┘ │ async WAL ┌──────┼─────────────────┐ ▼ ▼ ▼ [Region: US] [Region: EU] ┌──────────┐ ┌──────────┐ │ REPLICA │ │ REPLICA │ ←── reads gần user └──────────┘ └──────────┘

4. Sync / Async / Semi-Sync

─── ASYNC ────────────────────────────────────────── Client Primary Replica │ COMMIT │ │ │───────────────►│ │ │ │ ACK COMMIT │ │◄───────────────│ │ │ │ ──── WAL ───────►│ (sau) Pros: low latency Cons: nếu primary crash trước replicate → mất data ─── SYNC ─────────────────────────────────────────── Client Primary Replica │ COMMIT │ │ │───────────────►│ │ │ │ ──── WAL ───────►│ │ │ │ FSYNC │ │ ◄────── ACK ─────│ │ ACK COMMIT │ │ │◄───────────────│ │ Pros: 0 data loss khi primary crash (replica có data) Cons: latency tăng (round-trip + fsync replica) ─── SEMI-SYNC ─────────────────────────────────────── Như SYNC nhưng chỉ chờ ACK đến WAL nhận, không chờ fsync.

4.1. Postgres setup

# postgresql.conf trên primary:

# Async (default) — không cần config gì thêm

# Sync — chờ ít nhất 1 replica ack:
synchronous_standby_names = 'FIRST 1 (replica1, replica2)'
synchronous_commit = on

# Quorum: chờ 2 trong 3 replica:
synchronous_standby_names = 'ANY 2 (replica1, replica2, replica3)'

4.2. Trade-off

AsyncSync
LatencyThấpCao (chờ replica)
ThroughputCaoGiới hạn bởi replica chậm nhất
Data loss khi primary crashCó thể (RPO > 0)Không (RPO = 0)
Khi nào dùngWeb app thườngTài chính, đặt vé

5. Read Replicas — sử dụng đúng

App route read sang replica, write sang primary:

// Pattern thực dụng (PgBouncer, ProxySQL, hoặc app-level):
const writeDb = new Pool({ host: 'primary-host', ... });
const readDb  = new Pool({ host: 'replica-host', ... });

async function getUser(id) {
  return await readDb.query('SELECT * FROM users WHERE id = $1', [id]);
}

async function updateUser(id, data) {
  return await writeDb.query('UPDATE users SET ... WHERE id = $1', [id, ...]);
}

5.1. Replication Lag — vấn đề thực tế

Replica luôn chậm hơn primary vài ms đến vài giây (tùy load). Async lag có thể giây/phút khi primary cao tải.

-- Trên replica, kiểm tra lag:
SELECT now() - pg_last_xact_replay_timestamp() AS lag;
-- → INTERVAL '00:00:00.450'  (450ms)

5.2. Read-your-writes problem

User POST /comment, app ghi primary thành công, redirect → GET /post/X. Read replica chưa kịp apply → user thấy comment chưa có. Bug UX.

Cách giải:

  • Sticky session — sau write, route N giây tiếp theo của session đó về primary.
  • Replication lag aware — query "có lag < 100ms?" trước, nếu cao thì fall back primary.
  • Synchronous replication — đảm bảo không lag → đắt.
  • Cache write trong session — UI optimistic.

5.3. Stale read tolerance

Phân loại read theo "tolerance lag":

  • OK stale 1 phút: dashboard, recommendation, analytics → replica.
  • OK stale < 1s: feed, profile → replica.
  • NEED fresh: balance, inventory check trước checkout, "your order" page → primary.

6. Failover — primary chết, làm gì?

6.1. Manual failover

# Trên replica đã chọn:
pg_ctl promote -D /var/lib/postgresql/data
# Replica trở thành primary mới, chấp nhận writes.

# App đổi connection string sang IP của primary mới
# Replica khác cần re-config để follow primary mới

6.2. Automatic failover — Patroni / Stolon / pg_auto_failover

Tools này tự phát hiện primary down (qua etcd/consul), promote replica, update DNS/HAProxy. Toàn bộ ~15-60 giây.

┌─────────────────────────────────────┐ │ Patroni cluster │ │ │ │ ┌────────────┐ │ │ │ etcd │ ←── consensus │ │ └────────────┘ │ │ ▲ ▲ │ │ │ │ leader election │ │ │ │ │ │ ┌──┴──┐ ┌─┴────┐ ┌──────┐ │ │ │ pri │ │ rep1 │ │ rep2 │ │ │ └─────┘ └──────┘ └──────┘ │ └─────────────────────────────────────┘ │ ▼ ┌──────────┐ │ HAProxy │ ←── route writes to current primary └──────────┘

6.3. Split-brain — bug nguy hiểm

Nếu network partition giữa primary và monitor → 2 node nghĩ mình là primary → cả 2 nhận writes → diverge.

Cách giải: fencing — kill primary cũ trước khi promote. Hoặc dùng quorum-based consensus như Raft (Patroni dùng etcd Raft).

6.4. RTO & RPO

  • RTO (Recovery Time Objective) — bao lâu để hệ trở lại: failover Patroni ~30s, manual ~5-15 phút, restore từ backup ~giờ.
  • RPO (Recovery Point Objective) — bao nhiêu data có thể mất: sync replication = 0, async = vài giây, backup hourly = 1 giờ.

7. Multi-Master / Active-Active

Nhiều node cùng chấp nhận writes. Phức tạp hơn nhiều primary-replica vì cần resolve conflict.

  • BDR (Bi-Directional Replication) trên Postgres — commercial.
  • Cassandra, DynamoDB — multi-master native.
  • CockroachDB, Spanner — distributed, mỗi node nhận write nhưng có consensus protocol.

7.1. Conflict resolution strategies

  • Last-Write-Wins (LWW) — timestamp lớn nhất thắng. Đơn giản, có thể mất data.
  • Vector clock — theo dõi version đa node, app tự merge.
  • CRDT (Conflict-free Replicated Data Type) — kiểu dữ liệu tự merge tự động (Counter, OR-Set).
  • Quorum write — chờ majority ack trước khi commit.

Kinh nghiệm: tránh multi-master khi có thể. 99% app không cần. Multi-region read replicas + 1 primary đủ.

8. Sharding — chia dữ liệu

Sharding = chia bảng thành nhiều phần (shard) lưu trên các DB khác nhau. Khác replication: replica có toàn bộ data, shard chỉ có 1 phần.

Trước (monolithic): ┌─────────────────────┐ │ 1 PostgreSQL │ │ 1 tỉ users │ │ 500 GB │ └─────────────────────┘ Sau (sharded): ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Shard 1 │ │ Shard 2 │ │ Shard 3 │ │ Shard 4 │ │ 250M user│ │ 250M user│ │ 250M user│ │ 250M user│ │ 125 GB │ │ 125 GB │ │ 125 GB │ │ 125 GB │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ ▲ ▲ ▲ ▲ └───────────┴───────────┴───────────┘ │ ┌───────────┐ │ Shard map │ ← user_id → shard └───────────┘

8.1. Khi nào sharding?

  • Bảng > 1TB và không thể vertical scale thêm.
  • Write QPS > khả năng 1 node.
  • RAM cần để cache hot data > server lớn nhất.

Trước đó, hãy thử: index, partition (intra-DB), read replica, cache.

8.2. Trade-off

Khi sharding:

  • JOIN cross-shard cực phức tạp / chậm.
  • Distributed transaction (2PC) chậm và có thể kẹt.
  • Aggregate query phải scatter-gather.
  • App phải biết shard map.
  • Backup/restore phức tạp hơn.

9. Shard Strategies — 3 kiểu chính

9.1. Range Sharding

Chia theo khoảng giá trị key:

Shard 1: user_id 1 → 250M
Shard 2: user_id 250M → 500M
Shard 3: user_id 500M → 750M
Shard 4: user_id 750M → 1B

Ưu: range query nhanh (scan trong shard). Nhược: hot shard nếu data skew (user mới luôn vào shard cuối).

9.2. Hash Sharding

shard_id = hash(user_id) % N

Shard 1: user_id mà hash(id) % 4 == 0
Shard 2: hash(id) % 4 == 1
...

Ưu: phân bố đều. Nhược: range query khó (phải scatter qua mọi shard); resharding khi N đổi.

9.3. Directory-based

Bảng tra cứu (lookup table) ánh xạ key → shard:

shard_map:
| key       | shard_id |
| user:5    | shard-2  |
| user:99   | shard-1  |
| user:200  | shard-3  |

Ưu: linh hoạt nhất, dễ migrate user. Nhược: thêm 1 lookup mỗi query, lookup là single-point-of-failure → cache.

9.4. Geographic / Tenant-based

  • Chia theo region: shard EU, shard US, shard APAC.
  • Chia theo tenant (multi-tenant SaaS): mỗi customer 1 shard.

10. Consistent Hashing — giảm đau khi resharding

Vấn đề với hash(key) % N: đổi N (thêm/bớt shard) → 90% data phải di chuyển.

Consistent hashing giải bằng vòng tròn hash:

┌──── hash space (0 → 2^32) ────┐ │ │ │ ●N1 (10°) │ │ │ │ ●N4 │ │ (340°) ●N2 │ │ (90°) │ │ │ │ K (200°) ─→ tìm node kế │ │ tiếp theo: N3 │ │ │ │ ●N3 (210°) │ │ │ └────────────────────────────────┘ Mỗi key cũng map vào hash space. Key thuộc shard "kế tiếp" (clockwise).

10.1. Lợi ích

  • Thêm node mới → chỉ ~1/N data di chuyển (không phải 90%).
  • Bớt node → chỉ 1/N data tái phân phối.

10.2. Virtual nodes (vnodes)

Vấn đề: với N nhỏ, phân bố không đều. Giải bằng virtual nodes — mỗi node thật map vào nhiều điểm trên vòng. Cassandra mặc định 256 vnode/node.

DynamoDB, Redis Cluster, Cassandra, Riak đều dùng consistent hashing.

11. Resharding — khi N đổi

Resharding là nghiệp vụ phức tạp nhất. Lý do cần:

  • Thêm shard vì data tăng.
  • Bớt shard vì chi phí.
  • Hot shard cần split.
  • Tenant lớn cần shard riêng (split off).

11.1. Approach: split & migrate online

  1. Setup new shard (empty).
  2. Bật dual-write: app ghi cả old shard và new shard cho key thuộc range mới.
  3. Backfill old data: copy old → new (bg job).
  4. Verify data parity (checksum).
  5. Cut over reads: app đọc new shard.
  6. Tắt writes vào old shard cho key đó.
  7. Cleanup old shard data.

Quá trình có thể mất ngày/tuần cho TB data. Mỗi bước có rủi ro.

11.2. Pre-sharding

Mẹo: ngay từ đầu, tạo nhiều shard logical (vd: 4096 shard) trên ít node thật. Khi cần scale, di chuyển shard logical sang node mới — không cần đổi shard map (chỉ đổi shard → node mapping).

Citus (Postgres extension), Vitess (MySQL), MongoDB sharding đều dùng pattern này.

12. Bài tập

  1. Setup Postgres primary + 1 replica với streaming replication trên Docker. Demo write trên primary, read trên replica.
  2. Mô phỏng replication lag bằng SELECT pg_sleep(...). Đo lag qua pg_last_xact_replay_timestamp().
  3. Trình bày plan failover cho hệ Postgres + 2 replica. Chọn tool (Patroni / pg_auto_failover) và lý giải.
  4. Cho 100M user. Đề xuất chiến lược shard + count shard ban đầu. Tại sao chọn hash thay vì range?
  5. Vấn đề "celebrity user" trong Twitter (1 user 100M follower) gây hot shard. Giải pháp?
  6. Khi nào tách 1 monolithic Postgres ra microservice DB (database per service)? Khi nào KHÔNG?

13. Quiz

Quiz cuối Chương 10

Sự khác biệt chính giữa Replication và Sharding:

  • Cả hai giống nhau
  • Replication chỉ cho NoSQL
  • Replication: copy toàn bộ data sang nhiều node. Sharding: chia data, mỗi node giữ 1 phần
  • Sharding tốt hơn replication
Replication giải HA + read scale. Sharding giải dung lượng + write scale. Trong production lớn thường kết hợp: mỗi shard có nhiều replica.

Async replication trade-off:

  • Latency cao
  • Latency thấp nhưng có thể mất vài giây gần nhất nếu primary crash
  • Bắt buộc fsync replica
  • Mạnh hơn sync
Async không chờ replica → primary commit nhanh, replica lag vài ms tới giây. Nếu primary crash + replica chưa nhận transaction cuối → data đó mất. RPO > 0.

"Read your own writes" với async replica đối mặt với:

  • Index corruption
  • Deadlock
  • Sync block
  • User vừa post nhưng read replica chưa apply → không thấy bài của chính mình
Phổ biến với multi-replica setup. Giải bằng sticky session sau write (route về primary N giây) hoặc sync replication cho path nóng.

Hash sharding với hash(id) % N có vấn đề gì khi đổi N?

  • ~90% data phải di chuyển — consistent hashing giải vấn đề này
  • Hash collision tăng
  • Index hỏng
  • Replication chậm
Modulo N với N đổi → key % N_new khác key % N_old → đa số data phải re-shard. Consistent hashing dùng vòng tròn → chỉ ~1/N data di chuyển khi thêm/bớt node.

Range sharding pitfall lớn nhất:

  • Chậm
  • Tốn dung lượng
  • Hot shard khi data skew (vd: user mới luôn vào shard cuối, traffic tập trung)
  • Không hỗ trợ JOIN
Range sharding tốt cho range query nhưng nếu data có "thiên hướng" (mới hơn = hot), shard chứa range đó bị quá tải. Dùng hash sharding hoặc composite (region + id) để tránh.

Split-brain trong replication là gì?

  • DB chia làm 2 schema
  • Hai node cùng nghĩ mình là primary do network partition → cả hai nhận writes → data diverge
  • App có 2 connection pool
  • Index bị partition
Khi network giữa primary và monitor đứt, monitor thấy primary "chết" và promote replica → 2 primary. Phòng tránh bằng quorum consensus (Raft/Paxos qua etcd) và fencing (kill primary cũ trước promote).

Vấn đề lớn nhất sau khi shard:

  • Backup chậm
  • Index thiếu
  • DB tốn nhiều RAM
  • JOIN cross-shard cực phức tạp; distributed transaction chậm; aggregate phải scatter-gather
Sharding hy sinh nhiều tiện lợi của RDBMS. JOIN giữa 2 bảng ở 2 shard khác nhau cực khó (thường app tự assemble hoặc denormalize). 2PC distributed tx chậm. Scatter-gather query mọi shard mới ra kết quả. Đó là lý do delay sharding càng lâu càng tốt.

Pre-sharding (4096 shard logical trên ít node thật) có lợi gì?

  • Khi scale up node, chỉ cần di chuyển shard logical, không re-shard data
  • Giảm dung lượng
  • Nhanh hơn
  • Tự động backup
Citus, Vitess, MongoDB dùng pattern này. Shard map ánh xạ key → shard logical (cố định, hash). Mỗi shard logical → 1 node thật (đổi được). Khi tăng node, chỉ migrate shard logical, không động tới key→shard mapping.

Hoàn thành Chương 10. Tiếp theo: Chương 11 — Caching, Search, OLTP vs OLAP →