MongoDB Replica Set Guide
Kiến trúc chuyên sâu — Replica Set, WiredTiger, Data flow, và Simulation.
MongoDB Replica Set
Kiến Trúc Chuyên Sâu
Từ Golang client SDK → MongoDB driver → Wire Protocol → WiredTiger → disk. Hiểu mọi thứ diễn ra bên trong để vận hành tự tin và debug nhanh trong production.
Một MongoDB Replica Set không đơn giản chỉ là "3 cái database chạy song song". Đây là một distributed consensus system với từng node đảm nhận vai trò cụ thể, giao tiếp liên tục qua heartbeat, đồng bộ data thông qua oplog stream, và có khả năng tự bầu lãnh đạo mới khi Primary gặp sự cố — tất cả hoàn toàn tự động.
Replica Set Topology
Vai Trò Từng Node
Khi fail: Secondaries bắt đầu election, chọn 1 Secondary mới làm Primary trong ~10–30 giây.
readPreference: secondary. Có thể có replication lag.Priority: Quyết định ai được ưu tiên trở thành Primary trong election. Priority=0 không bao giờ thành Primary.
Trade-off: Tiết kiệm chi phí hardware nhưng không giúp read scalability và không failover được. Dùng khi không đủ budget cho 3 full data nodes.
Full Component Map — Bên Trong Mỗi Node
Trước khi một dòng code Golang chạm được vào MongoDB, dữ liệu phải trải qua nhiều lớp xử lý. Hiểu các lớp này giúp bạn debug connection issues, optimize performance, và đặt đúng write concern.
Golang Driver — Connection Setup & Server Discovery
package main import ( "context" "time" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/readpref" ) func connectReplicaSet() *mongo.Client { // Driver tự discover toàn bộ topology từ bất kỳ seed nào uri := "mongodb://mongo1:27017,mongo2:27017,mongo3:27017/?replicaSet=rs0" opts := options.Client().ApplyURI(uri). SetMaxPoolSize(100). // Max connections per node SetMinPoolSize(5). // Warmup connections SetMaxConnIdleTime(10 * time.Minute). SetServerSelectionTimeout(5 * time.Second). SetHeartbeatInterval(10 * time.Second). // Client-side monitoring SetReadPreference(readpref.SecondaryPreferred()). // Read from secondary nếu có SetWriteConcern(/* w:majority, j:true */) client, _ := mongo.Connect(context.Background(), opts) // Driver ngay lập tức bắt đầu Server Monitoring (SDAM): // 1. Connect đến seed nodes // 2. Chạy "isMaster" command → nhận topology info // 3. Discover tất cả replica set members // 4. Monitor liên tục (heartbeat interval) return client }
BSON Encoding — Binary JSON
BSON (Binary JSON) là format wire duy nhất giữa client và MongoDB. Khác JSON ở chỗ: có schema rõ ràng, length-prefixed (O(1) skip), và hỗ trợ types mà JSON không có (ObjectID, Date, Binary, Decimal128...).
Wire Protocol — OP_MSG Format
// ── OP_MSG Frame Structure ───────────────────────────────── // MsgHeader (16 bytes) messageLength: 4 bytes // Total message size requestID: 4 bytes // Client-assigned, dùng để match response responseTo: 4 bytes // 0 cho requests, requestID của request cho responses opCode: 4 bytes // 2013 (0x07DD) = OP_MSG // OP_MSG body flagBits: 4 bytes // checksumPresent(bit0), moreToCome(bit1) // Section type 0 — Command document (BSON) // Ví dụ InsertOne command: { "insert": "users", // collection name "documents": [{ "name":"Alice", "age":30 }], "ordered": true, "writeConcern": { "w": "majority", "j": true, "wtimeout": 5000 }, "lsid": { "id": /* UUID */ }, // Logical session ID "$db": "mydb", "$readPreference": { "mode": "primary" } } // Server response (OP_MSG): // { "n": 1, "ok": 1.0 } — thành công // { "ok": 0, "code": 11000, "errmsg": "duplicate key..." } — lỗi
mongotop, mongostat, hoặc network captures (tcpdump), bạn đang nhìn thẳng vào Wire Protocol frames. Hiểu requestID/responseTo matching giúp trace một request cụ thể qua toàn bộ system. writeConcern trong frame là thứ quyết định khi nào server trả response về client.WiredTiger là storage engine mặc định từ MongoDB 3.2. Nó quyết định mọi thứ về performance: read/write throughput, memory usage, disk I/O pattern, và crash recovery behavior. DevOps cần hiểu WiredTiger để tune đúng chỗ thay vì "mò mẫm" tuning parameters.
MVCC — Snapshot Isolation trong WiredTiger
WiredTiger dùng MVCC (Multi-Version Concurrency Control) — mỗi read transaction thấy một snapshot nhất quán của data tại thời điểm nó bắt đầu, bất kể concurrent writes đang diễn ra. Không có read lock → reads không block writes, writes không block reads.
storage:
engine: wiredTiger
wiredTiger:
engineConfig:
# Cache size — critical tuning parameter
# Rule: (RAM - 1GB) * 0.5, nhưng để lại cho OS file cache
cacheSizeGB: 4 # Với 16GB RAM → 4-6GB WT cache
journalCompressor: snappy # snappy | zlib | none
collectionConfig:
blockCompressor: snappy # Compression cho collection data
# snappy: fast + decent compression (default)
# zlib: slower + better compression (archive data)
# zstd: best ratio (MongoDB 4.2+)
indexConfig:
prefixCompression: true # Compress index keys với common prefix
# Kiểm tra cache usage:
# db.serverStatus().wiredTiger.cache
# "bytes currently in the cache" / "maximum bytes configured" = eviction pressure
# Nếu > 80%: tăng cacheSizeGB hoặc scale RAM
Oplog (Operations Log) là trái tim của replication. Đây là một capped collection đặc biệt (local.oplog.rs) trên mỗi node, ghi lại mọi write operation theo dạng idempotent — nghĩa là apply cùng operation nhiều lần vẫn cho kết quả giống nhau.
Replication Lag — Hiểu và Monitor
// Xem replication status rs.printReplicationInfo() // Output: // configured oplog size: 10240 MB // log length start to end: 86400 secs (24.0 hrs) // oplog first event time: 2024-01-15 00:00:00 GMT // oplog last event time: 2024-01-16 00:00:00 GMT // now: 2024-01-16 00:00:00 GMT rs.printSecondaryReplicationInfo() // source: mongo2:27017 // syncedTo: Mon Jan 16 2024 09:59:58 GMT // 0 secs (0 hrs) behind the primary // Chi tiết lag per member: rs.status().members.forEach(m => { if (m.state === 2) { // 2 = SECONDARY lag = m.optimeDate - rs.status().members.find(x => x.state===1).optimeDate; print(m.name + " lag: " + lag/1000 + "s"); } }); // Alert nếu lag > threshold (ví dụ từ monitoring script) // Lag cao → secondaries không kịp apply → đọc stale data khi dùng readPreference:secondary
MongoDB dùng một biến thể của Raft consensus algorithm để elect Primary. Không phải Raft thuần túy — có nhiều thay đổi để phù hợp với distributed database requirements — nhưng nguyên lý cốt lõi giống nhau: majority votes, term-based leadership.
Điều Kiện Để Được Bầu Làm Primary
- Priority > 0 — priority=0 không bao giờ candidate
- Oplog up-to-date — lastOptime ≥ majority của members
- Không bị blacklisted (đã vote trong 30s qua)
- Không có newer term đang active
- Member ở trạng thái SECONDARY (không ROLLBACK, RECOVERING)
- electionTimeout: mặc định 10s — sau 10s không có heartbeat → election bắt đầu
- Election timeout: 10–30s tổng cộng tùy network
- Golang driver sẽ nhận
ServerSelectionTimeout errortrong thời gian này - Cần retry logic trong app code cho writes
- Sau election: driver tự discover Primary mới qua SDAM
Scenario 1 — Write Path Đầy Đủ: Golang → ACK
db.collection VÀ oplog entry được ghi vào local.oplog.rs. Đây là atomic operation — không thể có một cái mà thiếu cái kia. Journal flush xảy ra nếu j:true.InsertOneResult{InsertedID: ObjectID("...")}. Application nhận được và data đảm bảo đã tồn tại trên majority nodes → durable ngay cả khi Primary crash ngay lập tức sau đó.Scenario 2 — Read từ Secondary (readPreference)
// ── Read Preference Options ──────────────────────────────── // 1. primary (default) — luôn đọc từ Primary // Đảm bảo đọc data mới nhất, không lag opts := options.Find().SetReadPreference(readpref.Primary()) // 2. secondary — chỉ đọc từ Secondary // Có thể lag → stale data. Tăng read throughput. opts := options.Find().SetReadPreference(readpref.Secondary()) // 3. secondaryPreferred — Secondary nếu có, fallback Primary // Best for read-heavy workloads với acceptable staleness opts := options.Find().SetReadPreference(readpref.SecondaryPreferred()) // 4. nearest — node có RTT thấp nhất (Primary hoặc Secondary) // Dùng cho geo-distributed clusters opts := options.Find().SetReadPreference(readpref.Nearest()) // 5. primaryPreferred — Primary nếu available, fallback Secondary // Graceful degradation khi Primary overloaded opts := options.Find().SetReadPreference(readpref.PrimaryPreferred()) // ── Với MaxStaleness (giới hạn stale data) ──────────────── rp, _ := readpref.New( readpref.SecondaryMode, readpref.WithMaxStaleness(90*time.Second), // Tối đa lag 90s ) // Driver sẽ không route đến Secondary có lag > 90s
Scenario 3 — Primary Failure & Automatic Failover
Scenario 4 — Network Partition (Split-Brain Prevention)
Scenario 5 — Initial Sync (Node Mới Join Cluster)
rs.add("mongo4:27017") — node nhận cấu hình replica set và bắt đầu initial sync. Trạng thái: STARTUP2.T_start.T_start đến hiện tại. Đây có thể tốn nhiều thời gian nếu clone kéo dài và oplog đã rotate.mongodump/mongorestore để bootstrap node mới với data trực tiếp từ snapshot.Interactive Simulation
Metrics Quan Trọng — Và Tại Sao (Architectural Mapping)
| Metric | Command/Source | Ngưỡng Alert | Architectural Meaning |
|---|---|---|---|
| Replication Lag | rs.printSecondaryReplicationInfo() |
Alert >30s Critical >5min | Secondary oplog applier không kịp Primary. Nguyên nhân: I/O bottleneck, CPU quá tải, network bandwidth, long-running write operations. |
| WT Cache Usage | db.serverStatus().wiredTiger.cache |
Alert >80% | WiredTiger bắt đầu evict dirty pages → background I/O tăng → latency spike. Cần tăng cacheSizeGB hoặc scale RAM. |
| Oplog Window | rs.printReplicationInfo() |
Alert <24h | Nếu window < expected recovery time → Initial Sync sẽ fail. Tăng oplog size hoặc giảm write rate. |
| Connections | db.serverStatus().connections |
Alert >80% max | Connection pool exhaustion. Mỗi connection dùng ~1MB RAM. Quá nhiều → OOM risk, query queuing. |
| Query Executors | db.serverStatus().metrics.queryExecutor |
collScans > 0 trên writes | COLLSCAN trên write queries → toàn bộ collection phải locked. Cần index. |
| Document Locks | db.serverStatus().locks |
timeAcquiringMicros tăng | Lock contention trên collection hoặc database. WiredTiger dùng document-level locking, nhưng DDL (index creation) vẫn có collection lock. |
Đọc MongoDB Logs Qua Lens Kiến Trúc
// ── Log 1: Slow Query (default threshold: 100ms) ────────── {"t":{"$date":"2024-01-15T14:37:22.123Z"},"s":"I","c":"COMMAND","id":51803, "attr":{"type":"command","ns":"mydb.orders","command":{"find":"orders", "filter":{"status":"pending"}}, "planSummary":"COLLSCAN", ← KHÔNG CÓ INDEX! → add index ngay "durationMillis":2847}} ← 2.8 giây — quá chậm // ── Log 2: Replication lag warning ──────────────────────── {"s":"W","c":"REPL","id":21793, "msg":"Replication lag is over the threshold", "attr":{"lagMillis":35000, ← 35 giây lag → secondary có vấn đề I/O "threshold":10000}} ← Alert threshold: 10s // ── Log 3: Election triggered ───────────────────────────── {"s":"I","c":"ELECTION","id":21335, "msg":"Starting an election", "attr":{"reason":"election timeout", ← Primary không respond 10s "term":4}} {"s":"I","c":"ELECTION","id":21353, "msg":"Election succeeded", "attr":{"electionId":"7fffffff0000000000000004", "term":4}} ← Primary mới được bầu với term=4 // ── Log 4: WiredTiger cache pressure ────────────────────── {"s":"W","c":"WTRECOV","id":22430, "msg":"WiredTiger cache currently full", "attr":{"cacheUsagePercent":95}} ← 95% → eviction pressure cao, tăng RAM // ── Log 5: Rollback triggered ────────────────────────────── {"s":"W","c":"REPL","id":21600, "msg":"Rollback required", "attr":{"commonPoint":"7fff...:123", ← Điểm chung của 2 oplog history "source":"mongo2:27017"}} // → Old Primary có writes chưa replicate → phải rollback // → Các writes này được ghi vào rollback/ directory
Troubleshooting Guide — Từ Symptom đến Root Cause
1. WiredTiger cache >80%? → I/O bound, tăng RAM hoặc giảm cacheSizeGB
2. w:majority đang chờ Secondary? → Check replication lag
3. Slow queries blocking? →
db.currentOp({active:true, secs_running:{$gt:5}})4. Index missing → COLLSCAN trên write path triggers full scan
5. Disk IOPS saturation? →
db.serverStatus().wiredTiger.block-manager
1. Secondary disk I/O? → Oplog applier bị block bởi slow disk
2. Large write operations? → Một write >16MB bloats oplog
3. Network bandwidth giữa nodes? →
ping + iperf34. Nhiều indexes trên Secondary? → Apply oplog cần update nhiều indexes
5. CPU overload trên Secondary? → Oplog applier threads bị starved
1. maxPoolSize quá thấp trong client config?
2. Slow queries giữ connections lâu?
3. Không có connection timeout → leaked connections
4. Microservices tất cả connect đến 1 mongod → mỗi service có pool riêng
5. Giải pháp: MongoDB Connection Pooling + mongos proxy cho sharded clusters
1. Dùng
readPreference: secondary nhưng lag cao2. Giải pháp: Set
maxStalenessSeconds trong read preference3. Dùng
readConcern: majority để đọc data đã commit4. Đọc sau write: dùng
readPreference: primary cho operation đó5. Causal consistency: dùng sessions để đảm bảo read-your-writes
Essential Commands — Vận Hành Hàng Ngày
package db import ( "context" "time" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/readpref" "go.mongodb.org/mongo-driver/mongo/writeconcern" ) // WriteWithRetry — retry logic cho election window func WriteWithRetry[T any]( ctx context.Context, col *mongo.Collection, doc T, maxRetries int, ) (*mongo.InsertOneResult, error) { var lastErr error for attempt := 0; attempt < maxRetries; attempt++ { if attempt > 0 { // Exponential backoff: 1s, 2s, 4s... backoff := time.Duration(1<Sleep(backoff) } result, err := col.InsertOne(ctx, doc) if err == nil { return result, nil } // Retry chỉ với transient errors (election, network) if mongo.IsTimeout(err) || mongo.IsNetworkError(err) { lastErr = err continue } // Non-retriable: duplicate key, validation error... return nil, err } return nil, lastErr } // NewMongoClient — production config func NewMongoClient(uri string) (*mongo.Client, error) { wc := writeconcern.Majority() wc.WTimeout = 5 * time.Second wc.Journal = boolPtr(true) return mongo.Connect(context.Background(), options.Client().ApplyURI(uri). SetMaxPoolSize(100). SetMinPoolSize(5). SetMaxConnIdleTime(10*time.Minute). SetServerSelectionTimeout(30*time.Second). // Cover election window SetReadPreference(readpref.SecondaryPreferred()). SetWriteConcern(wc), ) }
☐ Set priority cao hơn cho nodes mạnh hơn — Primary sẽ thường là server tốt nhất
☐ Write concern: majority cho mọi critical write — đừng dùng w:1
☐ Monitor oplog window > 24h — tăng nếu cần Initial Sync
☐ App code phải có retry logic với backoff để handle election (30s window)
☐ Đặt serverSelectionTimeout > 30s trong driver để survive election
☐ Monitor replication lag — alert khi >30s, critical >5min
☐ Dùng hidden secondary (slaveDelay=1h) như buffer cho human error
☐ WiredTiger cache: test với 50% RAM, tune dựa trên cache pressure metrics
☐ Khi thêm node mới: tăng oplog size trước để tránh sync fail