Sharding
replica set ทำให้ข้อมูลของคุณคงอยู่ แต่สมาชิกทุกตัวยังถือข้อมูล ทั้งหมด ซึ่งใช้ได้จนกว่าเครื่องเดียวจะไม่พอเก็บข้อมูลทั้งหมด หรือ primary ตัวเดียวรับการเขียนทั้งหมดไม่ไหว การเพิ่มหน่วยความจำและดิสก์ที่เร็วขึ้น — scale up — มีเพดาน Sharding scale out: แบ่ง collection เชิงตรรกะหนึ่งอันข้ามหลายเครื่อง ข้อมูลและโหลดการเขียนจึงถูกแบ่งกัน
ทุกอย่างด้านล่างรันบนเครื่อง Mac นี้ บนคลัสเตอร์ชั่วคราวของ container mongo:8.3.11 สี่ตัวบน Docker network แต่ละตัวจำกัด 512 MB และลบทิ้งหลังใช้งาน: config server หนึ่งตัว (replica set โหนดเดียว), สอง shard (shardA, shardB โหนดเดียวต่อตัว) และ mongos หนึ่งตัว ด้วย mongosh 2.11.1 มันเป็นคลัสเตอร์สำหรับสอน — shard และ config server ใน production เป็น replica set สามโหนด — และตัวตรวจอัตโนมัติของคอร์สรัน fence เหล่านี้ไม่ได้ (ต้องมีคลัสเตอร์) output จึงบันทึกจากการรันจริง แท็บ Python/Go/Rust สะท้อนการเรียกใน shell และไม่ได้ถูกรันโดยตัวตรวจ
docker network create mongodb-shard-labrun() { docker run -d --name "$1" --hostname "$1" --network mongodb-shard-lab --memory 512m mongo:8 "${@:2}"; }run cfg mongod --configsvr --replSet cfg --bind_ip_all --wiredTigerCacheSizeGB 0.25run shard-a mongod --shardsvr --replSet shardA --bind_ip_all --wiredTigerCacheSizeGB 0.25run shard-b mongod --shardsvr --replSet shardB --bind_ip_all --wiredTigerCacheSizeGB 0.25docker exec cfg mongosh --quiet --port 27019 --eval 'rs.initiate({ _id: "cfg", configsvr: true, members: [{ _id: 0, host: "cfg:27019" }] })'docker exec shard-a mongosh --quiet --port 27018 --eval 'rs.initiate({ _id: "shardA", members: [{ _id: 0, host: "shard-a:27018" }] })'docker exec shard-b mongosh --quiet --port 27018 --eval 'rs.initiate({ _id: "shardB", members: [{ _id: 0, host: "shard-b:27018" }] })'run mongos mongos --configdb cfg/cfg:27019 --bind_ip_alldocker exec mongos mongosh --quiet --eval 'sh.addShard("shardA/shard-a:27018"); sh.addShard("shardB/shard-b:27018")'# clean up: docker rm -f cfg shard-a shard-b mongos && docker network rm mongodb-shard-labส่วนประกอบของ sharded cluster
หัวข้อที่มีชื่อว่า “ส่วนประกอบของ sharded cluster”shard คือ replica set ที่ถือข้อมูลส่วนหนึ่ง config server คือ replica set ของมันเองที่เก็บ metadata ของคลัสเตอร์ — chunk ไหนอยู่ shard ไหน mongos คือ router ที่แอปเชื่อมต่อแทน shard ตัวใดตัวหนึ่ง มันอ่าน metadata นั้นและส่งต่อแต่ละคำขอไปยัง shard ที่ถูกต้อง
flowchart TB App["Application"] --> M["mongos router"] M --> CFG["Config servers — store the chunk map"] M --> Sh1["Shard 1 (replica set)"] M --> Sh2["Shard 2 (replica set)"] M --> Sh3["Shard 3 (replica set)"] CFG -. "tells mongos where each chunk lives" .- M
คลัสเตอร์ในสายตาของ mongos:
// mongosh/sh-topology.js @skip-verify needs the sharded lab cluster (real output below)console.log(db.getSiblingDB("config").shards.find({}, { host: 1 }).toArray());console.log("balancer enabled:", sh.getBalancerState(), "| mode:", db.adminCommand({ balancerStatus: 1 }).mode);[ { _id: 'shardA', host: 'shardA/shard-a:27018' }, { _id: 'shardB', host: 'shardB/shard-b:27018' }]balancer enabled: true | mode: fullShard collection — enableSharding เป็นทางเลือก
หัวข้อที่มีชื่อว่า “Shard collection — enableSharding เป็นทางเลือก”บทช่วยสอนเก่า (และบทเรียนเวอร์ชันเก่านี้) เริ่มด้วย sh.enableSharding("library") manual บอกว่า: “Starting in MongoDB 6.0, this command is not required to shard a collection. The enableSharding command explicitly creates a database” (enableSharding, ตรวจเมื่อ 2026-09-30) หลักฐานบน 8.3.11 — คำสั่งแรกสุดบนคลัสเตอร์ใหม่:
// mongosh/sh-shard-collection.js @skip-verify needs the sharded lab cluster (real output below)const r = sh.shardCollection("library.loans", { memberId: 1 }); // no enableSharding firstconsole.log("shardCollection ok:", r.ok);console.log(db.getSiblingDB("config").databases.find({ _id: "library" }, { primary: 1 }).toArray());// app/sh-shard-collection.mjs @skip-verify needs the sharded lab clusterconst admin = client.db("admin");await admin.command({ shardCollection: "library.loans", key: { memberId: 1 } });admin = client.adminadmin.command( "shardCollection", "library.loans", key={"memberId": 1})admin := client.Database("admin")if err := admin.RunCommand(ctx, bson.D{ {Key: "shardCollection", Value: "library.loans"}, {Key: "key", Value: bson.D{{Key: "memberId", Value: 1}}},}).Err(); err != nil { return err}let admin = client.database("admin");admin .run_command(doc! { "shardCollection": "library.loans", "key": { "memberId": 1 }, }) .await?;shardCollection ok: 1[ { _id: 'library', primary: 'shardB' } ]database ถูกสร้างตามต้องการและได้ primary shard (shardB ในการรันนี้; คลัสเตอร์เลือกให้) — บ้านของ collection ที่ไม่ได้ shard นั่นคือทั้งหมดที่ enableSharding ยังทำอยู่
shard key, chunk และ balancer
หัวข้อที่มีชื่อว่า “shard key, chunk และ balancer”shard key คือฟิลด์ (หรือหลายฟิลด์) ที่ MongoDB ใช้ตัดสินว่า document เป็นของ shard ไหน มันแบ่งช่วงค่าของ key เป็น chunk ที่ต่อเนื่องกันและกำหนดแต่ละ chunk ให้ shard; balancer เบื้องหลังย้าย chunk เพื่อให้ shard สมดุล Ranged sharding เก็บค่าใกล้กันไว้ด้วยกัน (ดีสำหรับ range query เสี่ยงถ้าการเขียนกระจุกที่ปลายด้านหนึ่ง); hashed sharding hash key ก่อน (การเขียนกระจายเท่ากัน แต่ range ต้องกระจายไปทุก shard)
flowchart LR K["Shard key value"] --> C["Which chunk owns this value"] C --> S["Which shard owns that chunk"] S --> Done["Document lands on that shard"] Bal["Balancer"] -. "migrates chunks to keep shards even" .- S
Targeted เทียบกับ scatter-gather ใน explain
หัวข้อที่มีชื่อว่า “Targeted เทียบกับ scatter-gather ใน explain”query ที่มี shard key เป็น targeted; ที่ไม่มีเป็น scatter-gather ด้านล่าง โหลด 100 000 document ลง collection แบบ hashed (events, key deviceId) แล้ว explain บน mongos:
// mongosh/sh-route.js @skip-verify needs the sharded lab cluster (real output below)const lib = db.getSiblingDB("library");lib.events.drop();sh.shardCollection("library.events", { deviceId: "hashed" });let batch = [];for (let i = 0; i < 100000; i++) { batch.push({ deviceId: "dev-" + (i % 5000), i }); if (batch.length === 10000) { lib.events.insertMany(batch, { ordered: false }); batch = []; }}function route(label, cursor) { const p = cursor.explain().queryPlanner.winningPlan; console.log(label, "->", p.stage, (p.shards ?? []).map((s) => s.shardName));}route("deviceId = dev-42 (shard key, equality)", lib.events.find({ deviceId: "dev-42" }));route("i = 42 (no shard key) ", lib.events.find({ i: 42 }));route("deviceId range (hashed key, range) ", lib.events.find({ deviceId: { $gte: "dev-1", $lt: "dev-2" } }));deviceId = dev-42 (shard key, equality) -> SINGLE_SHARD [ 'shardA' ]i = 42 (no shard key) -> SHARD_MERGE [ 'shardA', 'shardB' ]deviceId range (hashed key, range) -> SHARD_MERGE [ 'shardA', 'shardB' ]equality บน shard key ไปที่ shard เดียวพอดี (SINGLE_SHARD); ไม่มี shard key หรือ range บน hashed key ต้องใช้ทั้งสอง (SHARD_MERGE) นั่นคือราคาของการ hash: ลำดับถูกทำลาย
การเลือก shard key ที่ดี
หัวข้อที่มีชื่อว่า “การเลือก shard key ที่ดี”shard key ที่ไม่ดีแก้แบบเงียบ ๆ ทีหลังไม่ได้ (ดู resharding ด้านล่าง) สามคุณสมบัติที่สำคัญ: cardinality สูง (ค่าแตกต่างกันมาก จึงมี chunk มาก), การกระจายสม่ำเสมอ (ไม่มีค่าใดครอบงำ) และ ไม่มี hotspot ตอนเขียน key ที่เพิ่มขึ้นเรื่อย ๆ — timestamp, id แบบ auto-increment — คือกับดักคลาสสิก
วัด candidate ด้วย analyzeShardKey
หัวข้อที่มีชื่อว่า “วัด candidate ด้วย analyzeShardKey”ตั้งแต่ MongoDB 7.0 analyzeShardKey “calculates metrics for evaluating a shard key for an unsharded or sharded collection” (analyzeShardKey, ตรวจเมื่อ 2026-09-30) — คือเครื่องมือที่บทเรียนเก่าเรียกว่า “Shard Key Advisor” คุณลักษณะของ key รันบนข้อมูลที่มีอยู่แล้ว (metric การกระจายอ่าน/เขียนต้องมี query sampling ผ่าน configureQueryAnalyzer เพิ่ม) สอง candidate บน 20 000 document เดียวกัน:
// mongosh/sh-analyze.js @skip-verify needs the sharded lab cluster (real output below)const lib = db.getSiblingDB("library");lib.readings.drop();lib.readings.insertMany(Array.from({ length: 20000 }, (_, i) => ({ ts: i, deviceId: "dev-" + (i % 5000) }))); // unsharded copy to analyselib.readings.createIndex({ ts: 1 });lib.readings.createIndex({ deviceId: 1 });function analyze(key) { const k = db.adminCommand({ analyzeShardKey: "library.readings", key }).keyCharacteristics; return { key, numDocsTotal: Number(k.numDocsTotal), numDistinctValues: Number(k.numDistinctValues), mostCommonFrequency: Number(k.mostCommonValues[0].frequency), monotonicity: k.monotonicity.type };}console.log(analyze({ ts: 1 }));console.log(analyze({ deviceId: 1 }));{ key: { ts: 1 }, numDocsTotal: 20000, numDistinctValues: 20000, mostCommonFrequency: 1, monotonicity: 'monotonic'}{ key: { deviceId: 1 }, numDocsTotal: 20000, numDistinctValues: 5000, mostCommonFrequency: 4, monotonicity: 'not monotonic'}ts ถูกติดธง monotonic: ทุก document ใหม่เรียงหลังทุกตัวก่อนหน้า การ insert ทั้งหมดจึงลงที่ chunk บนสุด deviceId เป็น not monotonic มี 5 000 ค่าที่ไม่ซ้ำ และค่าที่พบบ่อยที่สุดปรากฏ 4 ครั้ง — key ที่สมเหตุสมผล (เครื่องมือนี้อธิบายข้อมูล ไม่ได้ตัดสินแทนคุณ)
เปลี่ยนใจทีหลัง
หัวข้อที่มีชื่อว่า “เปลี่ยนใจทีหลัง”คำแนะนำเก่า — “การเปลี่ยน shard key ต้องสร้าง collection ใหม่” — ล้าสมัยแล้ว reshardCollection (MongoDB 5.0) เปลี่ยน key ของ collection แบบออนไลน์ และ 8.0 เพิ่ม moveCollection (ย้าย collection ที่ไม่ได้ shard ไปอีก shard) และ unshardCollection (resharding, moveCollection, unshardCollection, ตรวจเมื่อ 2026-09-30) ก็ยังมีต้นทุน: manual ระบุว่า “The minimum duration of a resharding operation is always 5 minutes” ทำ reshard ได้ทีละ collection และการเขียนมีช่วง critical section สั้น ๆ การรันจริง (collection 10 000 document):
// mongosh/sh-reshard.js @skip-verify needs the sharded lab cluster and ~5 minutes (real output below)const adm = db.getSiblingDB("admin");const lib = db.getSiblingDB("library");lib.tickers.drop();lib.tickers.insertMany(Array.from({ length: 10000 }, (_, i) => ({ ts: i, deviceId: "dev-" + (i % 5000) })));lib.tickers.createIndex({ ts: 1 }); // a populated collection needs an index on the key firstsh.shardCollection("library.tickers", { ts: 1 });let t = Date.now();console.log("reshardCollection ts:1 -> deviceId:hashed ok:", adm.runCommand({ reshardCollection: "library.tickers", key: { deviceId: "hashed" } }).ok, Math.round((Date.now() - t) / 1000) + " s");console.log(db.getSiblingDB("config").collections.find({ _id: "library.tickers" }, { key: 1 }).toArray());reshardCollection ts:1 -> deviceId:hashed ok: 1 331 s[ { _id: 'library.tickers', key: { deviceId: 'hashed' } } ]ใน Compass / Atlas: Compass แสดง shard key ของ collection ที่ shard แล้วในมุมมอง Indexes/Schema ส่วน Atlas เปิด sharding เป็นตัวเลือกของคลัสเตอร์ ทั้งสองขับคำสั่งเดียวกัน — analyzeShardKey และ reshardCollection — ที่แสดงข้างบน
เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”- เลือก key ให้ถูกตั้งแต่เนิ่น ๆ แต่รู้ว่าเปลี่ยนได้: resharding ใช้ได้ ใช้เวลาอย่างน้อยห้านาทีและมีต้นทุน
- อย่า shard เร็วเกินไป replica set เดียวรับทราฟฟิกได้มาก shard เมื่อมีหลักฐาน
- ใส่ shard key ใน query ที่ใช้บ่อย — วัดข้างบน:
SINGLE_SHARDแบบ targeted เทียบกับSHARD_MERGEบนทั้งสอง shard - sharding ไม่แก้ query ที่ไม่มี index; มันคูณการ scan ด้วยจำนวน shard
อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”ผู้ช่วย AI ที่ถูกขอให้ “shard collection readings สำหรับ query แบบ time-series” เลือก timestamp:
🐛 หา bug ในโค้ดนี้
// AI comment: "ts has 100000 distinct values (high cardinality) and supports time-range queries, so it spreads data evenly"sh.shardCollection("library.readings", { ts: 1 });เฉลย
cardinality สูงเป็นเงื่อนไขที่จำเป็น แต่ไม่เพียงพอ ts มีแต่ เพิ่มขึ้น ทุกการ insert จึงตกในช่วงสูงสุด ซึ่งอยู่บน shard เดียว การรันจริง — insert 100 000 document ผ่าน mongos ด้วยแต่ละ key:
// mongosh/sh-hotspot.js @skip-verify needs the sharded lab cluster (real output below)const lib = db.getSiblingDB("library");for (const c of ["readings_ranged", "readings_hashed"]) lib[c].drop();sh.shardCollection("library.readings_ranged", { ts: 1 }); // the AI's choicesh.shardCollection("library.readings_hashed", { deviceId: "hashed" }); // the fixlet a = [], b = [];for (let i = 0; i < 100000; i++) { a.push({ ts: i, deviceId: "dev-" + (i % 5000) }); b.push({ ts: i, deviceId: "dev-" + (i % 5000) }); if (a.length === 10000) { lib.readings_ranged.insertMany(a, { ordered: false }); lib.readings_hashed.insertMany(b, { ordered: false }); a = []; b = []; }}for (const c of ["readings_ranged", "readings_hashed"]) { console.log(c, lib[c].aggregate([{ $collStats: { storageStats: {} } }, { $project: { _id: 0, shard: 1, docs: "$storageStats.count" } }]).toArray());}readings_ranged [ { shard: 'shardB', docs: 100000 } ]readings_hashed [ { shard: 'shardB', docs: 51100 }, { shard: 'shardA', docs: 48900 } ]key แบบ ranged ของ timestamp ส่งทุกอย่างไป shard เดียว (primary shard ซึ่งคือ shardB ที่นี่); key แบบ hashed แบ่งได้ราวครึ่งต่อครึ่ง ทางแก้มีต้นทุนของมันเอง (วัดไว้ก่อนหน้า): time-range query บน hashed key กลายเป็น scatter-gather ถ้าต้องการทั้งสองอย่าง key แบบผสมเช่น { deviceId: 1, ts: 1 } หรือ hashed prefix คือคำตอบที่ใช้กันบ่อย — วิเคราะห์ก่อนตัดสินใจ
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”collection ที่ shard แล้วคือ รายการเรียงลำดับของช่วง key (chunk) โดยแต่ละช่วงเป็นของ shard หนึ่ง เก็บอยู่ใน config server; mongos คือตารางค้นหาที่อยู่ข้างหน้า ทุกอย่างสืบเนื่องจากวิธีที่ key ของคุณแมปลงบนรายการนั้น: equality ตกที่ช่วงเดียว (targeted), ช่วงของ key ตกที่ไม่กี่ช่วง ถ้า key มีลำดับ (ranged) หรือ ทุกช่วง ถ้า hash และค่าที่เพิ่มขึ้นเสมอจะตกที่ช่วงสุดท้ายตลอดไป (hotspot)
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | ประโยชน์ | ต้นทุน |
|---|---|---|
| Ranged sharding | ค่า key ใกล้กันอยู่ใน chunk เดียวกัน range บน key จึงใช้ chunk น้อย | key แบบ monotonic ทำให้เกิด hotspot: 100 000 จาก 100 000 document บน shard เดียว |
| Hashed sharding | การเขียนกระจายเท่ากัน (48 900 / 51 100 ในการรัน) | range query และ query ที่ไม่ใช้ key กลายเป็น SHARD_MERGE บนทุก shard |
| Shard เร็ว | มีที่ให้เติบโต | ความซับซ้อนในการดำเนินงาน (สี่โปรเซสที่นี่ มากกว่านั้นใน production); ทุก query ต้องมี key ไม่เช่นนั้นกระจายทั่ว |
| Reshard ทีหลัง | เปลี่ยน key ออนไลน์ได้ตั้งแต่ 5.0 | อย่างน้อย 5 นาทีต่อครั้ง ทีละ collection |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “ต้องรัน
sh.enableSharding()ก่อน” — ไม่ใช่ตั้งแต่ 6.0;shardCollectionครั้งแรกสร้าง database เอง - “cardinality สูงก็พอสำหรับ shard key” —
tsมีค่าไม่ซ้ำ 20 000 ค่าในการวิเคราะห์ (100 000 ในการโหลด) แต่ยังทำให้ทุก document อยู่บน shard เดียว - “เปลี่ยน shard key ไม่ได้” —
reshardCollectionเปลี่ยนได้ออนไลน์ (5.0+) ใช้เวลาอย่างน้อยห้านาที - “เพิ่ม shard ทำให้ทุก query เร็วขึ้น” — query ที่ไม่มี shard key ไปทุก shard; และ query ที่ไม่มี index ก็ scan บนแต่ละตัว
- “มีคำสั่ง Shard Key Advisor” — เครื่องมือฝั่ง server คือ
analyzeShardKey(7.0+); อะไรที่เรียกว่า “advisor” คือ UI ที่ครอบมัน - “
mongosเก็บข้อมูล” — มันคือ router ที่ไม่เก็บสถานะ ข้อมูลอยู่บน shard, metadata อยู่บน config server
แท็บ Python/Go/Rust สะท้อนการเรียกใน shell; ไม่ได้ถูกรันโดยตัวตรวจอัตโนมัติของคอร์ส