MongoDB replica set 3 node: vì sao không có master-master và cách dựng cho đúng

“Cài MongoDB master-master cho tôi” là một yêu cầu rất hay gặp, và câu trả lời đúng là: MongoDB không có mô hình đó. Mỗi phần dữ liệu tại mỗi thời điểm chỉ có một node nhận ghi. Đây là thiết kế có chủ đích, để không bao giờ phải gỡ xung đột ghi như ở MySQL master-master. Thứ MongoDB cung cấp là replica set: tự bầu primary mới khi primary chết, driver tự chuyển theo, và không mất lệnh ghi đã được xác nhận. Bài này dựng replica set 3 node MongoDB 8.0 trên Ubuntu 24.04, rồi kill -9 primary giữa lúc đang ghi để xem điều đó có đúng không.

Vì sao phải 3 node

Replica set chỉ bầu được primary khi còn đa số phiếu. Với 2 node, mất 1 node thì node còn lại chỉ có 1/2 phiếu, không phải đa số, nên nó tự lùi về secondary và từ chối ghi. Đó là cơ chế chống split-brain: khi đứt mạng giữa hai máy, không bên nào được tự nhận mình là primary.

Bố trí Mất 1 máy Ghi chú
2 node dữ liệu Chỉ đọc, phải ép failover bằng tay Không có HA thật
2 node dữ liệu + arbiter (PSA) Tự failover Khi mất 1 node dữ liệu, ghi w:majority bị treo; ghi w:1 có thể bị rollback
3 node dữ liệu (PSS) Tự failover w:majority vẫn chạy, không rollback, bảo trì cuốn chiếu vẫn còn 2 bản dữ liệu

Nếu có đủ 3 máy, hãy chọn PSS. Arbiter chỉ là giải pháp tình thế khi thiếu máy.

Bước 1: cài đặt từ kho chính thức

MongoDB không có trong kho Ubuntu. MongoDB 5.0 trở lên còn bắt buộc CPU có AVX; trên máy ảo, kiểm tra bằng grep -m1 -o avx /proc/cpuinfo trước khi cài.

curl -fsSL https://pgp.mongodb.com/server-8.0.asc | \
  sudo gpg --dearmor -o /usr/share/keyrings/mongodb-server-8.0.gpg
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu noble/mongodb-org/8.0 multiverse" | \
  sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
sudo apt update && sudo apt install -y mongodb-org

Bước 2: tinh chỉnh kernel, lưu ý MongoDB 8.0 đổi khuyến nghị

Suốt nhiều năm, tài liệu MongoDB bảo tắt Transparent Huge Pages. Từ bản 8.0, MongoDB chuyển sang TCMalloc mới và khuyến nghị điều ngược lại: bật THP ở chế độ always, đồng thời tắt rseq của glibc. Nếu bạn làm theo các hướng dẫn cũ trên mạng, MongoDB 8.0 sẽ chạy chậm hơn mức nó có thể đạt được.

# /etc/systemd/system/mongodb-thp.service
[Unit]
Description=Transparent Huge Pages theo khuyen nghi MongoDB 8.0
DefaultDependencies=no
After=sysinit.target local-fs.target
Before=mongod.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh -c 'echo always > /sys/kernel/mm/transparent_hugepage/enabled; echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag; echo 0 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none'

[Install]
WantedBy=basic.target
# /etc/systemd/system/mongod.service.d/override.conf
[Service]
Environment=GLIBC_TUNABLES=glibc.pthread.rseq=0
LimitNOFILE=64000
LimitNPROC=64000

# /etc/sysctl.d/60-mongodb.conf
vm.max_map_count = 262144
vm.swappiness = 1
vm.overcommit_memory = 1

Kích hoạt bằng systemctl daemon-reload, systemctl enable --now mongodb-thp và sysctl -p /etc/sysctl.d/60-mongodb.conf. Nếu có thể, hãy đặt /var/lib/mongodb trên phân vùng XFS. Với ext4, MongoDB vẫn chạy nhưng in cảnh báo lúc khởi động, và hiệu năng ghi của WiredTiger kém hơn.

Bước 3: keyFile và mongod.conf

Khi đã bật xác thực, các node trong replica set cũng phải xác thực lẫn nhau. Cách đơn giản nhất là dùng một keyFile giống hệt nhau trên cả 3 máy:

openssl rand -base64 756 > keyfile          # tao MOT lan, chep sang ca 3 may
sudo install -d -o root -g mongodb -m 750 /etc/mongodb
sudo install -o mongodb -g mongodb -m 400 keyfile /etc/mongodb/keyfile
# /etc/mongod.conf - chi khac bindIp giua 3 may
storage:
  dbPath: /var/lib/mongodb
  wiredTiger:
    engineConfig:
      cacheSizeGB: 6          # may 16 GB: phan con lai cho page cache cua OS
systemLog:
  destination: file
  logAppend: true
  path: /var/log/mongodb/mongod.log
net:
  port: 27017
  bindIp: 127.0.0.1,192.168.1.41
replication:
  replSetName: rs0
  oplogSizeMB: 20480           # node tat vai ngay van tu bat kip
security:
  authorization: enabled
  keyFile: /etc/mongodb/keyfile

oplogSizeMB là tham số hay bị bỏ qua nhất. Oplog là nhật ký các thay đổi mà secondary đọc để bắt kịp primary. Một node tắt lâu hơn khoảng thời gian oplog chứa được sẽ không bắt kịp được nữa, và phải chép lại toàn bộ dữ liệu từ đầu. Xem oplog đang chứa được bao nhiêu giờ bằng lệnh rs.printReplicationInfo().

Bước 4: khởi tạo replica set

Chạy systemctl enable --now mongod trên cả 3 máy. Trên mongo1, chưa có user nào nên MongoDB cho phép kết nối từ localhost mà không cần đăng nhập, để tạo user đầu tiên (localhost exception):

mongosh --host 127.0.0.1 --eval '
rs.initiate({_id: "rs0", members: [
  {_id: 0, host: "192.168.1.41:27017", priority: 2},
  {_id: 1, host: "192.168.1.42:27017", priority: 1},
  {_id: 2, host: "192.168.1.43:27017", priority: 1}]})'

# cho vai giay de mongo1 len PRIMARY, roi tao user dau tien
mongosh --host 127.0.0.1 --eval '
db.getSiblingDB("admin").createUser({user: "admin", pwd: passwordPrompt(),
  roles: [{role: "root", db: "admin"}]})'

priority: 2 khiến mongo1 là primary ưu tiên. Sau khi hỏng và hồi phục, nó sẽ tự lấy lại vai primary (priority takeover). Nếu bạn không muốn ứng dụng bị gián đoạn thêm lần thứ hai, hãy đặt cả ba node priority 1.

Chuỗi kết nối cho ứng dụng phải liệt kê đủ 3 node và có replicaSet. Chỉ khi đó driver mới tự tìm được primary và tự chuyển theo khi failover:

mongodb://shop:[email protected],192.168.1.42,192.168.1.43/shop?replicaSet=rs0&authSource=shop

Bước 5: script giám sát

Script hỏi mongod ngay trên máy đang chạy nó (directConnection=true), nên vẫn trả lời được cả khi cụm đã mất primary. Đó chính là lúc bạn cần giám sát nhất. Script trả exit code 0 khi cụm khoẻ, và có thể chạy bằng systemd timer mỗi phút:

#!/bin/bash
# Kiem tra replica set rs0.
#   mongo-rs-check.sh        -> in trang thai, exit 0 = khoe, 1 = co van de
#   mongo-rs-check.sh --log  -> (timer goi) chi ghi log khi trang thai thay doi hoac dang loi
LOG=/var/log/mongodb/rs-check.log
STATE=/run/mongo-rs-check.state
LAG_WARN=30
PW=$(cat /etc/mongodb/admin.password)
# Hoi node cuc bo truoc (van tra loi khi khong co primary), roi moi hoi ca cum
OUT=$(timeout 20 mongosh --quiet "mongodb://admin:[email protected]/?authSource=admin&directConnection=true" --eval '
const s = rs.status(); const lag = '"$LAG_WARN"';
const p = s.members.filter(m => m.stateStr === "PRIMARY");
const pt = p.length ? p[0].optimeDate : null;
const probs = [];
if (p.length !== 1) probs.push("so PRIMARY = " + p.length + " (cum CHI DOC)");
s.members.forEach(m => {
  if (m.health !== 1) probs.push(m.name + " khong lien lac duoc");
  else if (!["PRIMARY","SECONDARY"].includes(m.stateStr)) probs.push(m.name + " dang " + m.stateStr);
  else if (m.stateStr === "SECONDARY" && pt) { const d = (pt - m.optimeDate)/1000; if (d > lag) probs.push(m.name + " tre " + d + "s"); }
});
const roles = s.members.map(m => m.name.split(":")[0] + "=" + m.stateStr).join(" ");
print((probs.length ? "LOI: " + probs.join("; ") : "OK: 1 PRIMARY, secondary khong tre") + " | " + roles);
' 2>&1 | tail -1)
[ -z "$OUT" ] && OUT="LOI: mongod tren may nay khong tra loi"
case "$OUT" in OK:*) OK=0;; *) OK=1;; esac
if [ "${1:-}" = --log ]; then
  KEY=$(echo "$OUT" | cut -d'|' -f1)
  if [ "$OK" = 1 ] || [ "$KEY" != "$(cat $STATE 2>/dev/null)" ]; then echo "$(date '+%F %T') $OUT" >> "$LOG"; fi
  echo "$KEY" > "$STATE"
else echo "$(hostname) $OUT"; fi
exit $OK

Kiểm thử: rút phích primary

Một client ghi insertOne với w:"majority" mỗi 0,1 giây, tổng cộng 300 lệnh. Giữa chừng, ta tắt primary mongo1 theo hai cách:

Kịch bản Thời gian có primary mới Lệnh ghi
systemctl stop mongod (tắt có trật tự) 28 ms: primary tự nhường vai trước khi tắt 300/300 thành công, ứng dụng không thấy lỗi
kill -9 (giả lập sập nguồn) khoảng 10,9 giây (electionTimeoutMillis mặc định 10 giây) 299 thành công, 1 báo lỗi; collection có đủ 300 document

Dòng cuối là bài học đáng nhớ nhất: lệnh ghi báo lỗi khi đứt kết nối chưa chắc đã thất bại. Lệnh đó đã được ghi, chỉ có xác nhận là không về kịp. Ứng dụng nên sinh _id hoặc dùng khoá tự nhiên, để việc ghi lại (tự động hay bằng tay) không tạo ra bản trùng. Log cũng cho thấy không có rollback replication. Thứ có trong log là “rollback to stable”, tức là WiredTiger tự phục hồi sau khi tắt không sạch, mất 667 ms.

Kịch bản khác Kết quả
Bật lại mongo1 Bắt kịp dữ liệu và tự lấy lại vai primary nhờ priority 2
Tắt 2/3 node Không có primary; lệnh ghi bị từ chối NotWritablePrimary, vẫn đọc được với secondaryPreferred
Reboot một node THP, sysctl và biến tắt rseq tự áp dụng; node quay lại làm secondary

Vận hành

  1. Bảo trì cuốn chiếu: làm từng secondary một và chờ cụm báo khoẻ rồi mới sang máy tiếp theo. Primary làm sau cùng, chạy rs.stepDown() trước khi dừng.
  2. Mất 2/3 node: cách đúng là bật lại một node đã tắt. Chỉ ép rs.reconfig(cfg, {force: true}) trên node còn sống khi chắc chắn hai node kia không quay lại được.
  3. Node kẹt ở RECOVERING với log “too stale”: xoá /var/lib/mongodb trên node đó rồi khởi động lại; node sẽ tự chép lại toàn bộ dữ liệu.
  4. Replica set không phải là backup. Một lệnh db.dropDatabase() chạy nhầm sẽ có mặt trên cả ba node trong vài mili giây. Hãy chạy mongodump --readPreference=secondary --oplog mỗi đêm.
  5. Cần chia tải ghi? Đó là việc của sharding, mỗi shard là một replica set như trên, chứ không phải master-master.
Lên đầu trang