ข้ามไปยังเนื้อหา

Replica sets

replica set คือคำตอบของคำถามตรง ๆ ข้อเดียว: จะเกิดอะไรขึ้นเมื่อ server ที่เก็บข้อมูลของคุณหยุดตอบสนอง? ด้วย mongod ตัวเดียว คำตอบคือแอปของคุณล่มจนกว่าจะมีคนซ่อมเครื่อง ด้วย replica set สำเนาอีกตัวเข้ามาแทนภายในไม่กี่วินาที replica set คือกลุ่มของ mongod ที่ถือข้อมูลเดียวกันทั้งหมด — primary หนึ่งตัวที่รับทุกการเขียน และ secondary หนึ่งตัวหรือมากกว่าที่เก็บสำเนาของตัวเองให้ตามทัน

ทุกอย่างที่ระบุ 3-node ด้านล่างรันบนเครื่อง Mac นี้ บน replica set ชั่วคราวของ container mongo:8.3.11 สามตัว (rs-1, rs-2, rs-3 ชื่อเซต rsb3 mongosh 2.11.1 และ Node driver 7.7.0 ใน container ตัวที่สี่) แต่ละตัวจำกัด 512 MB และลบทิ้งทั้งหมดหลังใช้งาน fence ที่มี path comment รันบน replica set โหนดเดียวของคอร์สด้วย แท็บ Python/Go/Rust สะท้อนการเรียกที่ตรวจแล้วใน shell/Node และไม่ได้ถูกรันโดยตัวตรวจอัตโนมัติของคอร์ส

container สามตัวบน Docker network ส่วนตัว และ rs.initiate หนึ่งครั้ง:

scripts/rs-lab.sh
docker network create mongodb-lab
for i in 1 2 3; do
docker run -d --name rs-$i --hostname rs-$i --network mongodb-lab --memory 512m \
mongo:8 mongod --replSet rsb3 --bind_ip_all --wiredTigerCacheSizeGB 0.25 --oplogSize 128
done
docker exec rs-1 mongosh --quiet --eval 'rs.initiate({ _id: "rsb3", members: [
{ _id: 0, host: "rs-1:27017" }, { _id: 1, host: "rs-2:27017" }, { _id: 2, host: "rs-3:27017" } ] })'
# clean up when done: docker rm -f rs-1 rs-2 rs-3 && docker network rm mongodb-lab

มีเฉพาะ primary ที่รับการเขียน ทุกการเขียนที่มันทำถูกบันทึกลงใน capped collection ชื่อ oplog (local.oplog.rs) ด้วย secondary ทุกตัวอ่าน oplog ของ primary และเล่นการทำงานเดียวกันซ้ำ สำเนาทั้งหมดจึงลู่เข้าหากัน นั่นทำให้ secondary เป็น eventually consistent: มี lag ระหว่างการเขียนบน primary กับการเขียนเดียวกันที่อื่น ๆ oplog entry จริงของ insertOne({ name: "Ada" }):

{
op: 'i',
ns: 'lib.members',
o: { _id: ObjectId('6abccde57c8538514ddd8679'), name: 'Ada' },
ts: Timestamp({ t: 1790758373, i: 2 }),
t: Long('1')
}

op: "i" คือ insert, o คือ document, ts เรียงลำดับ, t คือ election term และหน้าตาของทั้งเซต (3-node):

// mongosh/rs-status.js @skip-verify needs the 3-node replica set (real output below)
const st = rs.status();
console.log(st.members.map((m) => ({ name: m.name, stateStr: m.stateStr, health: m.health })));
const c = rs.conf();
console.log({ heartbeatIntervalMillis: c.settings.heartbeatIntervalMillis, electionTimeoutMillis: c.settings.electionTimeoutMillis, writeConcernMajorityJournalDefault: c.writeConcernMajorityJournalDefault });
const h = db.hello();
console.log({ setName: h.setName, hosts: h.hosts, primary: h.primary, isWritablePrimary: h.isWritablePrimary });
[
{ name: 'rs-1:27017', stateStr: 'PRIMARY', health: 1 },
{ name: 'rs-2:27017', stateStr: 'SECONDARY', health: 1 },
{ name: 'rs-3:27017', stateStr: 'SECONDARY', health: 1 }
]
{
heartbeatIntervalMillis: 2000,
electionTimeoutMillis: 10000,
writeConcernMajorityJournalDefault: true
}
{
setName: 'rsb3',
hosts: [ 'rs-1:27017', 'rs-2:27017', 'rs-3:27017' ],
primary: 'rs-1:27017',
isWritablePrimary: true
}

ค่า heartbeat interval (2 วินาที) และ election timeout (10 วินาที) เป็นค่าเริ่มต้นที่เหมือนกันในทุกเซต fence นี้จึงรันบนเซตโหนดเดียวของคอร์สได้ด้วย:

mongosh/rs-settings.js
const s = rs.conf().settings;
console.log({ heartbeatIntervalMillis: s.heartbeatIntervalMillis, electionTimeoutMillis: s.electionTimeoutMillis });
assert.strictEqual(s.heartbeatIntervalMillis, 2000);
assert.strictEqual(s.electionTimeoutMillis, 10000);
{ heartbeatIntervalMillis: 2000, electionTimeoutMillis: 10000 }
flowchart TB
  Client["Application writes"] --> P["Primary"]
  P -- "records every write" --> OL["Oplog (capped collection)"]
  OL -- "tail and replay" --> S1["Secondary 1"]
  OL -- "tail and replay" --> S2["Secondary 2"]
  P -. "heartbeats every 2s" .- S1
  P -. "heartbeats every 2s" .- S2
primary รับทุกการเขียนและบันทึก; secondary ตาม oplog และเล่นซ้ำเพื่อให้ sync กัน

สมาชิกแลก heartbeat ทุก 2 วินาที (heartbeatIntervalMillis); เอกสาร MongoDB เสริมว่าสมาชิกที่ heartbeat ไม่ตอบกลับภายใน 10 วินาทีจะถูกทำเครื่องหมายว่าเข้าถึงไม่ได้ (Replica set elections, ตรวจเมื่อ 2026-09-30) secondary ปฏิเสธการเขียน (3-node, เชื่อมตรงกับ rs-2):

MongoServerError | 10107 NotWritablePrimary | not primary

ขนาดของ oplog ตัดสินว่า secondary (หรือ consumer ของ change stream) ออฟไลน์ได้นานแค่ไหนแล้วยังตามทัน เซตใน lab มี 128 MB (rs.printReplicationInfo()):

actual oplog size
'128 MB'
---
configured oplog size
'128 MB'
---
log length start to end
'13 secs (0 hrs)'
---
oplog first event time
'Wed Sep 30 2026 08:52:19 GMT+0000 (Coordinated Universal Time)'
---
oplog last event time
'Wed Sep 30 2026 08:52:32 GMT+0000 (Coordinated Universal Time)'
---
now
'Wed Sep 30 2026 08:52:50 GMT+0000 (Coordinated Universal Time)'

เมื่อ primary หายไป สมาชิกที่เหลือโหวต ผู้สมัครที่ได้เสียงข้างมากเป็น primary ใช้เวลานานแค่ไหน? manual บอกว่า: “The median time before a cluster elects a new primary should not typically exceed 12 seconds, assuming default replica configuration settings. This includes time required to mark the primary as unavailable and call and complete an election.” (Replica set elections) ระหว่าง election เซตประมวลผลการเขียนไม่ได้; การอ่านบน secondary ทำต่อได้

วัดสองครั้ง writer ภายในเซต insert ด้วย w: "majority" ทุก 100 ms (retryWrites เปิดโดยค่าเริ่มต้น การเขียนที่ถูกขัดจังหวะจึงถูก retry เมื่อมี primary) และ writer บันทึกว่าแต่ละ insert ใช้เวลาเท่าไร:

// mongosh/rs-writer.js @skip-verify needs the 3-node replica set (run inside a member)
const c = db.getSiblingDB("lib").ticks;
const end = Date.now() + 40000;
let i = 0;
while (Date.now() < end) {
const t0 = Date.now();
let r = "ok";
try { c.insertOne({ i: i++, t: new Date() }, { writeConcern: { w: "majority" } }); } catch (e) { r = "ERR " + e.name + " " + e.code; }
print(JSON.stringify({ at: t0, ms: Date.now() - t0, r }));
sleep(100);
}

รอบที่ 1 — primary ถูก kill ด้วย docker kill (SIGKILL: ล่มโดยไม่บอกลา) รอบที่ 2 — primary ถูกหยุดด้วย docker stop (SIGTERM: ปิดอย่างสะอาด) รายการเดียวที่ช้ากว่า 300 ms:

{"at":1790758407048,"ms":10773,"r":"ok"}
rs-1:27017 SECONDARY | rs-2:27017 PRIMARY | rs-3:27017 SECONDARY
{"at":1790758473930,"ms":324,"r":"ok"}
rs-1:27017 PRIMARY | rs-2:27017 (not reachable/healthy) | rs-3:27017 SECONDARY

การล่มทำให้การเขียนหนึ่งครั้งค้างราว 10.8 วินาที — election timeout 10 วินาทีบวก election เอง สอดคล้องกับ “typically … 12 seconds” ของ manual — แล้วการเขียนนั้น สำเร็จ: driver รอ primary ใหม่แล้ว retry การปิดอย่างสะอาดทำให้ primary ส่งมอบงาน และค้างไม่ถึงครึ่งวินาที ทั้งสองให้บทเรียนเดียวกัน: โค้ดแอปต้องรองรับการหยุดราวสิบวินาทีเมื่อเกิดการล่มจริง ไม่ใช่ fail fast (Atlas มีปุ่มทดสอบ failover และระบุว่า election เฉลี่ยห้าวินาทีบน cluster ของมัน — Test Resilience ตรวจเมื่อ 2026-09-30)

ผู้สมัครต้องได้เสียงจากสมาชิกที่มีสิทธิ์โหวตมากกว่าครึ่ง ด้านล่างสองในสามสมาชิกถูกหยุด ตัวที่รอดชนะ election ไม่ได้และยังเป็น secondary จึงปฏิเสธการเขียน:

rs-1:27017 SECONDARY | rs-2:27017 (not reachable/healthy) | rs-3:27017 (not reachable/healthy)
MongoServerError | 10107 NotWritablePrimary | not primary

หลังสมาชิกสองตัวถูกสตาร์ทใหม่ เซตฟื้นตัวเอง:

rs-1:27017 PRIMARY | rs-2:27017 SECONDARY | rs-3:27017 SECONDARY

ด้วยสามเสียง ความล้มเหลวหนึ่งครั้งยังเหลือเสียงข้างมากสอง ด้วยสองเสียง ความล้มเหลวหนึ่งครั้งเหลือหนึ่งในสอง — ไม่มีเสียงข้างมาก ไม่มี primary

driver เชื่อมต่อกับ เซต: ระบุ seed ไม่กี่ตัวและตั้งชื่อเซตด้วย replicaSet= มันค้นหา topology ที่เหลือ หา primary และตามมันข้าม election ไปได้ Node driver ภายใน lab network (output 3-node — การอ่าน 20 ครั้งด้วย secondaryPreferred ถูกให้บริการโดยสอง secondary):

scripts/rs-connect.sh
mongosh "mongodb://rs-1:27017,rs-2:27017,rs-3:27017/?replicaSet=rsb3"
{
setName: 'rsb3',
primary: 'rs-1:27017',
hosts: [ 'rs-1:27017', 'rs-2:27017', 'rs-3:27017' ]
}
reads served by: [ 'rs-2', 'rs-3' ]

โดยค่าเริ่มต้นการอ่านไปที่ primary ซึ่งทำให้ consistent อย่างเข้ม เพื่อกระจายโหลดอ่านคุณเลือก read preference เช่น secondaryPreferred ได้ — แต่มีราคา ดังที่แสดงต่อไป

secondary หนึ่งตัว (rs-2) ถูกแช่แข็งด้วย db.fsyncLock() — แทน member ที่ lag — ขณะที่ order ถูกเขียนลง primary และอ่านกลับทันทีสามวิธี (Node, 3-node):

// app/rs-stale-read.mjs @skip-verify needs the 3-node replica set and a frozen secondary (real output below)
const orders = client.db("shop").collection("orders");
const direct = new MongoClient("mongodb://rs-2:27017/?directConnection=true", { readPreference: "secondary" });
await direct.connect();
await orders.insertOne({ _id: "o-1", total: 42 });
console.log("bug (read the frozen secondary):", await direct.db("shop").collection("orders").findOne({ _id: "o-1" }));
console.log("fix 1 (read the primary) :", await orders.findOne({ _id: "o-1" }, { readPreference: "primary" }));
// fix 2: causal consistency - the read waits for the secondary to reach the write
const session = client.startSession({ causalConsistency: true });
await orders.insertOne({ _id: "o-2", total: 7 }, { session, writeConcern: { w: "majority" } });
await orders.findOne({ _id: "o-2" }, { session, readPreference: "secondary", readConcern: { level: "majority" }, maxTimeMS: 2000 });
bug (read the frozen secondary): null
fix 1 (read the primary) : { _id: 'o-1', total: 42 }
fix 2 (causal session, secondary) : served by a caught-up secondary in 40 ms

การเขียนถูก acknowledge (w: "majority" โดยค่าเริ่มต้น — secondary อีกตัวและ primary มีมัน) แต่ secondary ที่ถูกแช่แข็งคืน null session แบบ causally consistent ให้การรับประกัน read-your-writes บน secondary: การอ่านถูกให้บริการโดยสมาชิกที่ตามการเขียนทันแล้วเท่านั้น — ที่นี่คือ rs-3 ที่ปกติ ใน 40 ms — แทนที่จะเป็น rs-2 ที่ถูกแช่แข็ง

ใน Atlas: cluster ของ Atlas คือ replica set (ใน sharded cluster แต่ละ shard คือหนึ่งเซต — Manage clusters) และ Test Failover กระตุ้น election ให้คุณ

  • ใช้จำนวนสมาชิกที่โหวตได้เป็น เลขคี่: สามทนล้มได้หนึ่ง ห้าทนได้สอง
  • arbiter โหวตแต่ไม่เก็บข้อมูล มันเป็น primary ไม่ได้และไม่เพิ่มความคงทน และยังเปลี่ยน write concern เริ่มต้นด้วย (ดูบท read/write concerns)
  • secondary lag ถ้าแอปอ่านสิ่งที่เพิ่งเขียนทันที ให้อ่านจาก primary หรือใช้ session แบบ causally consistent (พิสูจน์ข้างบน)
  • Replication คือ availability ไม่ใช่ backup: deleteMany ที่ผิดก็ replicate ไปด้วย
  • การล่มไม่เหมือนการหยุดอย่างสะอาด เผื่อเวลาที่การเขียนหยุดราวสิบวินาทีสำหรับความล้มเหลวจริง

ผู้ช่วย AI ขยายการอ่านโดยส่งไป secondary และเขียนคอมเมนต์อย่างมั่นใจ:

🐛 หา bug ในโค้ดนี้

// AI comment: "the insert is acknowledged, so reading it back from a secondary is safe"
await orders.insertOne({ _id: "o-1", total: 42 });
const saved = await ordersOnSecondary.findOne({ _id: "o-1" }); // readPreference: "secondary"
if (!saved) throw new Error("order lost");
เฉลย

การเขียนที่ถูก acknowledge อยู่บน primary และเสียงข้างมาก — ไม่จำเป็นต้องอยู่บน secondary ตัว นั้น ที่คุณอ่าน ใน lab secondary ที่ถูกแช่แข็งคืน null สำหรับการเขียนที่ acknowledge ไปแล้ว และโค้ดข้างบนจะรายงาน order หายทั้งที่ไม่ได้หาย อ่านสิ่งที่เพิ่งเขียนจาก primary หรือใช้ session แบบ causally consistent (causalConsistency: true) การรันข้างบนแสดงบั๊ก การอ่านจาก primary และการอ่านแบบ causal เคียงข้างกัน

replica set คือ log ที่เป็นทางการหนึ่งชุด (oplog) กับสำเนาหลายตัวที่เล่นตามมัน โดยมีการโหวตตัดสินว่าใครเป็นเจ้าของ log การเขียนไปที่เจ้าของ log; สำเนาตามหลังเล็กน้อยเสมอ; การโหวตต้องการเสียงข้างมากเด็ดขาดของผู้โหวตทั้งหมด ไม่ว่าจะยังอยู่หรือไม่ failover ไม่ทันทีเพราะที่เหลือต้องตัดสินก่อนว่าเจ้าของหายไปจริง — การตัดสินนั้นคือสิ่งที่ timeout สิบวินาทีซื้อไว้

ตัวเลือกประโยชน์ต้นทุน
Replica set (เทียบกับ mongod ตัวเดียว)failover อัตโนมัติ; วัดได้หยุดราว 10.8 วินาทีเมื่อล่ม, 0.3 วินาทีเมื่อหยุดอย่างสะอาดต้องรันอย่างน้อยสามโหนด; การเขียนยังคอขวดที่ primary ตัวเดียว
อ่านจาก secondaryกระจายโหลดอ่านออกจาก primaryพิสูจน์ข้างบน: การเขียนที่ acknowledge แล้วมองไม่เห็นบน secondary ที่ lag
อ่านจาก primary เท่านั้นได้การเขียนล่าสุดเสมอprimary รับโหลดทั้งหมด
จำนวนผู้โหวตเป็นเลขคู่ใช้เครื่องน้อยกว่าเลขคี่ถัดไปความล้มเหลวเดียวอาจไม่เหลือเสียงข้างมาก
  • “เพิ่ม secondary แล้วการเขียน scale” — ทุกการเขียนไปที่ primary ตัวเดียว; secondary scale เฉพาะการอ่าน การ scale การเขียนคือ sharding
  • “failover ใช้เวลาแค่วินาทีสองวินาที” — การล่มทำให้การเขียนหนึ่งครั้งค้างราว 10.8 วินาที (ปิดสะอาด 0.3 วินาที); manual บอกว่าค่ามัธยฐานไม่ควรเกิน 12 วินาที
  • “การเขียนที่ acknowledge แล้วมองเห็นได้ทุกที่” — มองเห็นบน primary และเสียงข้างมาก; secondary ตัวใดตัวหนึ่งอาจล้าหลัง (null ข้างบน)
  • “สองโหนดก็พอสำหรับ HA” — ด้วยสองผู้โหวต ความล้มเหลวหนึ่งครั้งเหลือหนึ่งในสอง และตัวที่รอดยังเป็น secondary
  • “replica คือ backup” — collection ที่ถูก drop ก็ replicate ไปด้วย
  • “ทุกสมาชิกอยู่ใน availability zone เดียวไม่เป็นไร” — replica set ป้องกันโหนดล้ม ไม่ใช่ zone ล้ม กระจายสมาชิกข้าม zone

แท็บ Python/Go/Rust สะท้อนพฤติกรรมที่ตรวจแล้วใน shell/Node; ไม่ได้ถูกรันโดยตัวตรวจอัตโนมัติของคอร์ส

secondary ทำให้ข้อมูลของตัวเองตรงกับ primary ได้อย่างไร?
คุณ docker kill (SIGKILL) primary ของเซต 3 โหนดขณะ writer insert ด้วย w: "majority" การรันจริงแสดงอะไร?
ทำไมแนะนำจำนวนสมาชิกที่โหวตได้เป็นเลขคี่?
insert ถูก acknowledge แล้ว คุณอ่านจาก secondary ได้ null คำอธิบายไหนเข้ากัน?
heartbeat interval และ election timeout เริ่มต้นคืออะไร?