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

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 และไม่ได้ถูกรันโดยตัวตรวจ

scripts/shard-lab.sh
docker network create mongodb-shard-lab
run() { 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.25
run shard-a mongod --shardsvr --replSet shardA --bind_ip_all --wiredTigerCacheSizeGB 0.25
run shard-b mongod --shardsvr --replSet shardB --bind_ip_all --wiredTigerCacheSizeGB 0.25
docker 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_all
docker 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

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 เท่านั้น ซึ่งใช้แผนที่ของ config server เพื่อส่งคำขอไปยัง shard ที่ถูกต้อง

คลัสเตอร์ในสายตาของ 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: full

บทช่วยสอนเก่า (และบทเรียนเวอร์ชันเก่านี้) เริ่มด้วย 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 first
console.log("shardCollection ok:", r.ok);
console.log(db.getSiblingDB("config").databases.find({ _id: "library" }, { primary: 1 }).toArray());
shardCollection ok: 1
[ { _id: 'library', primary: 'shardB' } ]

database ถูกสร้างตามต้องการและได้ primary shard (shardB ในการรันนี้; คลัสเตอร์เลือกให้) — บ้านของ collection ที่ไม่ได้ shard นั่นคือทั้งหมดที่ enableSharding ยังทำอยู่

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
shard key ของ document ตัดสิน chunk และ chunk ตัดสิน shard; balancer ปรับจำนวน chunk ให้เท่ากันเมื่อเวลาผ่านไป

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 ที่ไม่ดีแก้แบบเงียบ ๆ ทีหลังไม่ได้ (ดู resharding ด้านล่าง) สามคุณสมบัติที่สำคัญ: cardinality สูง (ค่าแตกต่างกันมาก จึงมี chunk มาก), การกระจายสม่ำเสมอ (ไม่มีค่าใดครอบงำ) และ ไม่มี hotspot ตอนเขียน key ที่เพิ่มขึ้นเรื่อย ๆ — timestamp, id แบบ auto-increment — คือกับดักคลาสสิก

ตั้งแต่ 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 analyse
lib.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 first
sh.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 ที่ถูกขอให้ “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 choice
sh.shardCollection("library.readings_hashed", { deviceId: "hashed" }); // the fix
let 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 คือคำตอบที่ใช้กันบ่อย — วิเคราะห์ก่อนตัดสินใจ

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
  • “ต้องรัน 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; ไม่ได้ถูกรันโดยตัวตรวจอัตโนมัติของคอร์ส

แอปพลิเคชันเชื่อมต่อกับอะไรใน sharded cluster?
บน MongoDB 8.3 คุณรัน sh.shardCollection("library.loans", { memberId: 1 }) บนคลัสเตอร์ใหม่โดยไม่มี enableSharding เกิดอะไรขึ้น?
คุณ shard บน { ts: 1 } ที่ ts เป็นตัวเลขที่เพิ่มขึ้นเรื่อย ๆ แล้ว insert 100 000 document พวกมันไปอยู่ที่ไหน?
query ไหนถูกส่งไปที่ shard เดียวบน collection ที่ hash ด้วย deviceId?
จริงหรือไม่ที่การเปลี่ยน shard key ต้องสร้าง collection ใหม่?