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 หนึ่งครั้ง:
docker network create mongodb-labfor 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 128donedocker 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-labPrimary, secondary และ oplog
หัวข้อที่มีชื่อว่า “Primary, secondary และ oplog”มีเฉพาะ 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 นี้จึงรันบนเซตโหนดเดียวของคอร์สได้ด้วย:
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
สมาชิกแลก 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)'Election อัตโนมัติ — จับเวลา
หัวข้อที่มีชื่อว่า “Election อัตโนมัติ — จับเวลา”เมื่อ 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
เชื่อมต่อกับ replica set
หัวข้อที่มีชื่อว่า “เชื่อมต่อกับ replica set”driver เชื่อมต่อกับ เซต: ระบุ seed ไม่กี่ตัวและตั้งชื่อเซตด้วย replicaSet= มันค้นหา topology ที่เหลือ หา primary และตามมันข้าม election ไปได้ Node driver ภายใน lab network (output 3-node — การอ่าน 20 ครั้งด้วย secondaryPreferred ถูกให้บริการโดยสอง secondary):
mongosh "mongodb://rs-1:27017,rs-2:27017,rs-3:27017/?replicaSet=rsb3"// app/rs-connect.mjs @skip-verify needs the 3-node replica set (run inside the lab network)import { MongoClient } from "mongodb";
const uri = "mongodb://rs-1:27017,rs-2:27017,rs-3:27017/?replicaSet=rsb3";const client = new MongoClient(uri);await client.connect();const hello = await client.db("admin").command({ hello: 1 });console.log({ setName: hello.setName, primary: hello.primary, hosts: hello.hosts });
const members = client.db("lib").collection("members", { readPreference: "secondaryPreferred" });const served = new Set();for (let i = 0; i < 20; i++) served.add((await members.find({}).explain()).serverInfo.host);console.log("reads served by:", [...served].sort());await client.close();from pymongo import MongoClient
uri = "mongodb://rs-1:27017,rs-2:27017,rs-3:27017/?replicaSet=rsb3"client = MongoClient(uri)uri := "mongodb://rs-1:27017,rs-2:27017,rs-3:27017/?replicaSet=rsb3"client, err := mongo.Connect(options.Client().ApplyURI(uri))if err != nil { return err}let uri = "mongodb://rs-1:27017,rs-2:27017,rs-3:27017/?replicaSet=rsb3";let client = Client::with_uri_str(uri).await?;{ 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 อาจล้าหลัง
หัวข้อที่มีชื่อว่า “secondary อาจล้าหลัง”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 writeconst 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): nullfix 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 สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”ผู้ช่วย 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 เคียงข้างกัน
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”replica set คือ log ที่เป็นทางการหนึ่งชุด (oplog) กับสำเนาหลายตัวที่เล่นตามมัน โดยมีการโหวตตัดสินว่าใครเป็นเจ้าของ log การเขียนไปที่เจ้าของ log; สำเนาตามหลังเล็กน้อยเสมอ; การโหวตต้องการเสียงข้างมากเด็ดขาดของผู้โหวตทั้งหมด ไม่ว่าจะยังอยู่หรือไม่ failover ไม่ทันทีเพราะที่เหลือต้องตัดสินก่อนว่าเจ้าของหายไปจริง — การตัดสินนั้นคือสิ่งที่ timeout สิบวินาทีซื้อไว้
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | ประโยชน์ | ต้นทุน |
|---|---|---|
Replica set (เทียบกับ mongod ตัวเดียว) | failover อัตโนมัติ; วัดได้หยุดราว 10.8 วินาทีเมื่อล่ม, 0.3 วินาทีเมื่อหยุดอย่างสะอาด | ต้องรันอย่างน้อยสามโหนด; การเขียนยังคอขวดที่ primary ตัวเดียว |
| อ่านจาก secondary | กระจายโหลดอ่านออกจาก primary | พิสูจน์ข้างบน: การเขียนที่ acknowledge แล้วมองไม่เห็นบน secondary ที่ lag |
| อ่านจาก primary เท่านั้น | ได้การเขียนล่าสุดเสมอ | primary รับโหลดทั้งหมด |
| จำนวนผู้โหวตเป็นเลขคู่ | ใช้เครื่องน้อยกว่าเลขคี่ถัดไป | ความล้มเหลวเดียวอาจไม่เหลือเสียงข้างมาก |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “เพิ่ม 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; ไม่ได้ถูกรันโดยตัวตรวจอัตโนมัติของคอร์ส