Chuỗi bài (Phần 2 của 8): Bài này giả định bạn đã quen với Double-Entry Ledger ở Phần 1. Chúng ta sẽ phân tích tại sao PostgreSQL monolith gặp giới hạn ở quy mô lớn và các Distributed SQL options giải quyết bài toán đó như thế nào.
⚠️ Lưu ý: Bài viết này tổng hợp từ documentation chính thức, engineering blogs, và benchmark papers đã công bố. Các con số latency và schema design phản ánh tài liệu nguồn tại thời điểm viết. Hãy verify với kiến trúc sư hoặc lead engineer của team trước khi áp dụng vào hệ thống production.
Distributed SQL Transaction Latency Là Gì?
Các distributed SQL databases như TiDB, Spanner, và CockroachDB sinh ra độ trễ mạng (network latency overheads) cho ACID transactions do quá trình distributed consensus và đồng bộ thời gian. Two-phase commit (2PC) và timestamp oracles thường cộng thêm 1-3ms độ trễ cho mỗi transaction — con số nhỏ nhưng có tác động đáng kể khi nhân với hàng triệu giao dịch/giây.
Tại Sao PostgreSQL Gặp Giới Hạn Ở Quy Mô Lớn?
PostgreSQL là lựa chọn tốt cho hầu hết Fintech startups. Nhưng ở quy mô 10,000+ TPS với dataset hàng trăm triệu records, các giới hạn bắt đầu xuất hiện:
- Vertical scaling ceiling: Một máy chủ vật lý chỉ có thể nâng cấp CPU/RAM đến một mức nhất định.
- Write bottleneck: Mọi writes đều đi qua một Primary node — không thể horizontal scale writes.
- Sharding complexity: Manual sharding PostgreSQL đòi hỏi application-layer logic phức tạp, dễ gây cross-shard transaction anomalies.
- Migration pain: WeBank và Shopee Pay đều đã migrate từ sharded MySQL sang TiDB để giải quyết đúng vấn đề này.
Khi nào nên migrate?
| Dấu hiệu | Hành động |
|---|---|
| Write TPS > 10,000 một single node | Xem xét TiDB hoặc CockroachDB |
| P99 latency > 100ms do table scan | Thêm read replicas hoặc CQRS |
| Shard count > 16 với manual routing | Migrate sang distributed SQL |
| Cross-shard transactions > 20% workload | Cần distributed ACID (2PC) |
Google Spanner TrueTime: Math Chính Xác
Google Spanner sử dụng GPS receivers và atomic clocks để cung cấp external consistency (linearizability). Đây là cơ chế toán học đằng sau commit wait:
TrueTime API
Mỗi datacenter Spanner có GPS clock + atomic clock. TrueTime API trả về khoảng thời gian bất định:
$$\text{TT.now()} = [\text{earliest}, \text{latest}]$$
Trong đó $\epsilon$ là uncertainty interval (thường 1–7ms tùy datacenter):
$$\text{latest} - \text{earliest} = 2\epsilon$$
Commit Wait Protocol
Khi transaction $T_1$ muốn commit với timestamp $s$:
- Spanner gán $s = \text{TT.now().latest}$
- Chờ cho đến khi: $\text{TT.now().earliest} > s$
- Chỉ sau đó mới expose kết quả cho clients
Thời gian commit wait: $$\text{Wait} = s - \text{TT.now().earliest} \approx 2\epsilon \approx 2\text{–}14\text{ms}$$
Ý nghĩa: Đây là lý do tại sao Spanner writes có baseline latency ~4ms ngay cả với queries đơn giản nhất — đây là cost của external consistency với hardware clocks.
Paxos Lock Table Recovery
Khác với 2PC truyền thống (coordinator crash → transaction lockout mãi mãi), Spanner lưu active lock tables bên trong mỗi Paxos replica group:
- Khi lock được acquire trên một partition, nó được ghi vào memory lock table của Paxos Leader cho partition đó.
- Lock state được replicate qua Paxos consensus log.
- Nếu leader crash, new leader được bầu bởi Paxos và tự động restore lock table từ log.
- Distributed commit tiếp tục an toàn — không bị stuck vĩnh viễn.
CockroachDB Hybrid Logical Clocks (HLC)
CockroachDB chạy trên commodity hardware — không có GPS hay atomic clocks. Nó dùng HLC kết hợp physical wall clock và logical counter để đảm bảo causal ordering:
HLC Update Rules
Khi event $e$ xảy ra locally trên Node $i$:
- Nếu $P_i > \text{HLC.physical}$: set $\text{HLC.physical} = P_i$, reset $\text{HLC.logical} = 0$
- Nếu $P_i = \text{HLC.physical}$: increment $\text{HLC.logical}$
Khi Node $i$ nhận message từ Node $j$ chứa timestamp $T_j$: $$\text{HLC.physical} = \max(P_i, \text{HLC.physical}, T_j\text{.physical})$$
Logical counters được tăng nếu physical times bằng nhau.
Uncertainty Interval
CockroachDB sử dụng max clock offset parameter (mặc định 500ms). Nếu một transaction đọc value có timestamp nằm trong uncertainty interval, nó trigger retry với pushed timestamp.
Hệ quả thực tế: Với NTP-synchronized clocks (±50ms accuracy), CockroachDB có thể trigger retry storms trong môi trường high-contention. Vì vậy khuyến nghị dùng PTP (Precision Time Protocol) để giảm clock offset xuống <1ms.
TiDB Percolator: Distributed Transaction Với TSO
TiDB implement distributed transactions bằng mô hình Percolator (từ Google) trên nền TiKV key-value store:
Ba Column Logic
TiDB/TiKV map transaction state vào ba columns trong key-value store:
| Column | Format | Ý nghĩa |
|---|---|---|
data | key + start_ts → value | Dữ liệu thực tế |
lock | key → start_ts + primary_key | Active locks |
write | key + commit_ts → start_ts | Committed transaction metadata |
TSO Overhead: 1-3ms Per Transaction
Mỗi distributed transaction cần liên hệ Placement Driver (PD) Timestamp Oracle (TSO) để lấy start_ts và commit_ts. Network RTT đến PD là 1-3ms, cộng thêm:
- Async Commit (TiDB 5.0+): Cho phép ghi secondary keys song song, giảm latency còn 2-5ms total.
- 1PC optimization: Single-region transactions có thể commit trong một network round trip thay vì hai.
Percolator 2PC Timeline:
Client PD (TSO) TiKV (Primary) TiKV (Secondary)
│────get_ts────▶│ │ │
│◀──start_ts────│ │ │
│ │ │
│────prewrite(primary)───────────────▶│ │
│────prewrite(secondary)────────────────────────────────▶│
│◀──prewrite_ok──────────────────────│ │
│◀──prewrite_ok──────────────────────────────────────────│
│ │ │
│────get_ts────▶│ │ │
│◀──commit_ts───│ │ │
│ │ │
│────commit(primary, commit_ts)──────▶│ │
│◀──commit_ok────────────────────────│ │
│ │ │
│ (async cleanup secondary locks in background)
Total latency: start_ts RTT (1-3ms) + prewrite RTTs + commit_ts RTT = ~5-15ms
Percolator Lock Recovery Algorithm
Khi transaction $T_2$ gặp stale lock của $T_1$:
- $T_2$ đọc primary lock metadata của $T_1$ (được reference từ secondary lock).
- Nếu primary key của $T_1$ có record trong
writecolumn → $T_1$ đã commit: $T_2$ roll forward bằng cách ghiwriterecord tạicommit_tscủa $T_1$ và xóa secondary lock. - Nếu primary key không có lock VÀ không có
writerecord → $T_1$ đã abort: $T_2$ xóa secondary lock. - Nếu primary lock của $T_1$ vẫn active → $T_2$ kiểm tra TTL. Nếu expired: $T_2$ xóa primary lock (abort $T_1$) và dọn secondary locks.
Redlock Là Không An Toàn Cho Fintech
Martin Kleppmann đã chứng minh Redlock không an toàn cho correctness-critical systems vì:
- GC Pause: JVM Garbage Collection pause có thể kéo dài vài trăm milliseconds, khiến lock TTL expire trong khi worker vẫn đang thực thi DB writes.
- Clock Skew: NTP clock synchronization có thể drift đủ để làm Redis nodes bất đồng về việc lock có còn valid không.
- Network Partition: Minority Redis node có thể grant lock cho client thứ hai sau split-brain.
Kết quả: Hai workers có thể simultaneously hold cùng một lock → double-processing → double-spend hoặc ledger imbalance.
Thay thế an toàn cho Fintech:
| Solution | Latency | Guarantee | Use Case |
|---|---|---|---|
| etcd (Raft-based) | 1-5ms | Strong consistency | Production distributed locks |
| ZooKeeper | 1-5ms | Strong consistency | Legacy systems |
| PostgreSQL SELECT FOR UPDATE | <1ms | Serializable | Single-node ledger |
| TiKV (Percolator) | 1-3ms | ACID | TiDB transactions |
Migration Case Studies
WeBank: MySQL Sharding → TiDB
WeBank (tháng 2021) migrate từ sharded MySQL sang TiDB để xử lý transaction history scale:
- Before: 16 MySQL shards với application-layer routing logic phức tạp
- After: TiDB cluster, horizontal scaling tự động
- Kết quả: Loại bỏ cross-shard JOIN problems và giảm ops complexity
Groww (Ấn Độ): MySQL → CockroachDB
Groww (fintech Ấn Độ) migrate MySQL sang CockroachDB sử dụng MOLT (Migrate Off Legacy Technologies):
- Động cơ: Cần multi-region deployment với strong consistency
- Kết quả: Distributed ACID transactions across 3 AWS regions
Latency Comparison Matrix
| Database | Write Latency (single op) | Cross-region | Consistency Model |
|---|---|---|---|
| PostgreSQL (single node) | <1ms | N/A | Serializable |
| MySQL + Sharding | <1ms + routing | N/A | Per-shard Serializable |
| TiDB | 3-8ms (Percolator + TSO) | Optional | External Consistency |
| CockroachDB | 2-10ms (HLC uncertainty) | Yes (multi-region) | Serializable |
| Spanner | 4-14ms (TrueTime commit wait) | Yes (global) | External Consistency |
QA & SDET Testing Strategy
Test 1: Network Split-Brain Simulation
# Sử dụng tc (traffic control) để simulate network partition
# Chia 5-node cluster thành 3 + 2 partition
# Trên minority nodes (2 nodes):
sudo tc qdisc add dev eth0 root netem loss 100%
# Kỳ vọng:
# - Writes trên majority (3 nodes) → THÀNH CÔNG
# - Writes trên minority (2 nodes) → FAIL với "leader not available"
# - Sau khi heal partition: consistency tự phục hồi
Test 2: Clock Skew Injection (libfaketime)
# Inject clock drift vượt max_clock_offset của CockroachDB (500ms)
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1 \
FAKETIME="+0.6s" \
go test ./ledger/... -run TestClockSkewResilience
# Kỳ vọng:
# - CockroachDB detect clock skew > 500ms
# - Database tự trigger transaction retry hoặc abort
# - KHÔNG return stale/out-of-order data
Test 3: TSO Latency Measurement
// Đo overhead của TSO round trip trong TiDB
func BenchmarkTiDBTransactionLatency(b *testing.B) {
db := openTiDBConnection()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
start := time.Now()
tx, _ := db.Begin()
// Simple single-row update
tx.Exec("UPDATE accounts SET balance = balance - 1 WHERE id = $1", "acc-001")
tx.Commit()
latency := time.Since(start)
// P99 phải < 15ms dưới điều kiện bình thường
if latency > 15*time.Millisecond {
b.Logf("High latency detected: %v", latency)
}
}
})
}
📚 Xem thêm: PayPay Architecture — TiDB at Scale — kiến trúc TiDB trong thực tế
FAQ
TiDB hay CockroachDB phù hợp hơn cho Fintech Việt Nam?
TiDB có tài liệu tiếng Hoa phong phú hơn và được nhiều fintech châu Á adopt (WeBank, Shopee Pay, ZaloPay). CockroachDB mạnh hơn về multi-region deployment nếu bạn cần active-active cross-datacenter.
Có nên bắt đầu với Spanner không?
Chỉ nên chọn Spanner nếu bạn đang chạy trên GCP và cần global scale ngay từ đầu. Chi phí Spanner cao hơn đáng kể so với self-managed TiDB/CockroachDB.
Làm sao giảm TSO overhead trong TiDB?
- Enable Async Commit (mặc định trong TiDB 5.0+).
- Đặt PD (Placement Driver) nodes gần TiKV nodes về mặt network.
- Dùng 1PC (one-phase commit) cho single-region transactions khi có thể.
Tiếp theo: Phần 3 — Event Sourcing & CQRS — Thiết kế ledger bất biến với event store schema, CQRS read models, và Transactional Outbox pattern để tránh dual-write.