บทที่ 15 · Part 4 — Data & Storage

Replication & Sharding

Replication mode, เมื่อไหร่ควร shard, การเลือก shard key 4 แบบ, ปัญหา 4 ข้อที่ sharding สร้าง และ consistency model

Sharding เป็นเครื่องมือที่แพงที่สุดในกล่อง — คุณต้องหามันมาให้ได้ด้วยการพิสูจน์ว่าจำเป็น บทนี้เริ่มจาก replication ที่ถูกก่อน แล้วค่อยว่าด้วย sharding และปิดด้วย consistency model ที่ทำให้คุณคุยกับ stakeholder ได้

จบบทนี้คุณจะ

  • เลือก replication mode ได้ และรู้ว่าเงินต้องใช้แบบไหน
  • แก้ read-replica bug ที่ทุกทีมปล่อยขึ้น production ครั้งหนึ่งในชีวิต
  • รู้ลำดับสิ่งที่ต้องทำให้หมดก่อนจะ shard
  • เลือก shard key ได้จาก 4 กลยุทธ์ และรู้จัก logical shard ที่ทำให้ reshard รอดชีวิต
  • วางแผนรับปัญหา 4 ข้อที่ sharding สร้างขึ้น
  • คุยเรื่อง consistency กับคนที่ไม่ใช่ engineer ได้

Replication mode

Modeการรับประกันต้นทุนใช้กับ
Asynchronousprimary commit โดยไม่รอ RPO > 0 — primary ล้มแล้ว write ที่ ack ไปแล้วหายได้ถูกที่สุด, write latency ต่ำสุดread replica, สำเนาสำหรับ analytics, cross-region DR ที่ยอมเสียข้อมูลระดับวินาทีได้
Synchronous (quorum)commit หลัง replica N ตัว persist แล้ว → RPO 0 สำหรับ write ที่ถูก ack แล้ว ภายใน failure domain ที่ระบุเพิ่ม latency เท่ากับ round trip ไปหา replica ที่ช้าที่สุดใน quorum (ใน region เดียวกันมักอยู่ระดับ มิลลิวินาทีหลักหน่วย แต่ต้องวัดเอง) · ข้าม region แย่กว่ามากอะไรที่เกี่ยวกับเงิน · multi-AZ ใน region เดียวกัน
Semi-sync / "at least one"มี replica 1 ตัวได้ข้อมูลก่อน commit จะคืนทางสายกลางsetup ที่พบบ่อยของ MySQL บน production

"RPO = 0" เป็นคำกล่าวที่ต้องมีขอบเขต

คำกล่าวที่ถูกต้องคือ:

สำหรับ write ที่ถูก acknowledge แล้ว ภายใต้ failure scope ที่ระบุไว้ การได้ RPO 0 ต้องใช้ synchronous/quorum replication หรือกลไก durability ที่เทียบเท่า พร้อมนิยาม failure domain, เงื่อนไขการ acknowledge และขั้นตอน failover/fencing ให้ชัด

สิ่งที่ทำให้คำกล่าวนี้เป็นเท็จในทางปฏิบัติ:

  • failure ที่กินทั้ง failure domain (เช่นทั้ง region ล่ม ขณะที่ quorum อยู่ใน region เดียว)
  • failover ที่ promote replica ที่ยังตามไม่ครบ เพราะไม่มี fencing
  • การตั้งค่าให้ degrade กลับไปเป็น async เองเมื่อ replica ไม่พอ (บาง engine ทำแบบนี้เป็น default — ต้องไปอ่านของตัวที่ใช้)
  • ข้อมูลเสียหายเชิงตรรกะ (DELETE ผิด) ซึ่ง replication คัดลอกไปให้เรียบร้อย

ตัวเลข latency และการรับประกันต้องผูกกับ vendor, topology และ failure model ที่คุณวัดได้ ไม่ใช่ตัวเลขจากเอกสารทั่วไป

Read-replica bug ที่ทุกทีมปล่อยขึ้นครั้งหนึ่ง

1. ผู้ใช้กดโอนเงิน          -> เขียนลง PRIMARY, commit, ตอบ 200 OK
2. แอป redirect ไปหน้าประวัติทันที
3. Query ประวัติไปที่ READ REPLICA ที่ตามหลังอยู่ 40 ms
4. ไม่มีรายการนั้น
5. ผู้ใช้: "เงินผมหายไปไหน" -> ticket -> ความเชื่อมั่นหาย

ทางแก้ เรียงตามความน่าใช้:

#วิธีหมายเหตุ
ASticky-to-primary — route read ไป primary ช่วงสั้นๆ หลังผู้ใช้คนนั้นเขียน (N วินาที ต่อ session)ง่ายและได้ผล ต้องระวังโหลดที่ลง primary
Bคืน object ที่สร้างไปใน response ของ write แล้วให้ UI render จากมัน แทนที่จะ query ใหม่ถูกที่สุดและทนที่สุด
CRead-your-writes token — จับ log position ของ write (LSN/GTID), ส่งกลับให้ client, และบังคับให้ replica ต้องตามถึงจุดนั้นก่อนจะ serve ไม่งั้น fall back ไป primaryแม่นที่สุด แต่ต้องแก้ทั้ง client และ router
Dห้าม route read ไป replica ที่ lag เกิน thresholdควรทำอยู่แล้วไม่ว่าเลือกข้อไหน

และเสมอ

Alert ที่ replication lag และ ห้ามใช้ read replica สำหรับการตรวจ authorization, การเช็คยอดเงิน, หรือการตรวจ duplicate เด็ดขาด

3 อย่างนี้อ่านจาก primary เท่านั้น เพราะการตัดสินใจผิดจากข้อมูลเก่า ใน 3 เรื่องนี้แปลว่าเงินหายหรือสิทธิ์รั่ว

เมื่อไหร่ควร shard

ทำให้หมดก่อนตามลำดับนี้

  1. instance ใหญ่ขึ้น (vertical) — ไม่ต้องแก้โค้ด ได้ผลทันที
  2. read replica สำหรับเส้นทางอ่าน
  3. index ที่ดีขึ้น และกำจัด query ที่แย่
  4. caching
  5. archive ข้อมูลเย็นออก และ partition ตามเวลา
  6. ย้าย analytics ออกจาก OLTP
  7. vertical partitioning — ย้ายตารางทั้งตารางไปฐานข้อมูลของตัวเอง

Shard เมื่อ primary ตัวเดียวเก็บข้อมูลไม่ไหวหรือรับ write ไม่ทันแล้วเท่านั้น

4 วิธีเลือก shard key

กลยุทธ์วิธีดีไม่ดี
RangeA–F ที่ shard 1, G–M ที่ shard 2 …range scan ทำงานได้; เข้าใจง่ายhot spot (ทุกอย่างที่ขึ้นต้นด้วย prefix ที่พบบ่อย); โตไม่เท่ากัน
Hashhash(key) mod Nกระจายสม่ำเสมอไม่มี range scan; เปลี่ยน N แล้วข้อมูลย้ายเกือบทั้งหมด
Consistent hashkey และ node อยู่บนวงแหวน มี virtual nodeเพิ่ม node แล้วย้ายแค่ ~1/N ของ keyกลไกมากขึ้น; ยังต้องจัดการ hot key ด้วย bounded load
Directory / lookupshard-map service บอกว่าแต่ละ tenant อยู่ที่ไหนยืดหยุ่นสมบูรณ์ — ย้ายลูกค้ารายใหญ่ไป shard ของตัวเองได้map เป็น critical dependency — ต้อง cache และต้อง available สูง

Logical shard — เทคนิคที่ทำให้ reshard รอดชีวิต

ห้าม map key ไปที่ physical database ตรงๆ

map key -> LOGICAL shard จำนวนมาก -> physical database จำนวนน้อย

  hash(account_id) mod 1024  = logical shard   (คงที่ตลอดกาล)
  logical shard -> physical DB                 (map เล็กๆ ที่เปลี่ยนได้)

เริ่ม: 1024 logical shard กระจายบน 4 database (256 ต่อตัว)
โต:   ย้าย 128 logical shard ไป database ใหม่
      ข้อมูลย้ายแค่ส่วนนั้น ทุก key คง logical shard เดิม
      จึงไม่มีการ rehash และการเปลี่ยน routing เป็นแค่การแก้ config

เลือกจำนวน logical shard ครั้งเดียวและเผื่อเยอะ (1024 หรือ 4096)
เพราะการเปลี่ยน *ตัวเลขนั้น* คือ operation ที่เจ็บ

นี่คือวิธีที่ Notion, Vitess และระบบ sharded ที่โตแล้วส่วนใหญ่ทำ

[!TIP] Real case — Notion กับ Figma Notion shard Postgres ตัวเดียวด้วย workspace ID เป็น 480 logical shard กระจายบน 32 physical instance (15 ต่อตัว) เลือก workspace เป็น shard key เพราะแทบทุก query อยู่ในขอบเขต workspace และรัน migration แบบ double-write → backfill → verify → cutover บทความของพวกเขาครอบคลุมส่วนที่เจ็บ: ตารางไหนควร shard, วิธียืนยันว่าข้อมูลเท่ากัน, และวิธี cut over โดยไม่ต้องปิดระบบ (Herding elephants)

Figma เลือกทางแบบขั้นบันได: vertical partitioning ก่อน (ย้ายตารางทั้งตารางไปฐานข้อมูลของตัวเอง — ถูกและไม่ต้องแก้ logic ของ application), แล้ว read replica, แล้วท้ายสุด horizontal sharding พร้อม proxy layer (DBProxy) ที่ parse query แล้ว route และเลือก shard key อย่างระวังต่อกลุ่มตาราง (How Figma's databases team lived to tell the scale)

Playbook ที่เอาไปใช้ได้: vertical partitioning ก่อน (ซื้อเวลาได้เป็นปีด้วยความพยายามเสี้ยวเดียว) → logical shard → double-write พร้อมช่วง verification → shadow read → cut over ทีละ shard แบบย้อนกลับได้

และสังเกตว่าทั้ง 2บริษัทสร้าง routing/proxy layer แทนที่จะกระจาย shard logic ไปทั่วโค้ด application

ปัญหา 4 ข้อที่ sharding สร้าง — วางแผนรับทั้ง 4 ข้อ

1. Cross-shard query

"ทุก transfer ในชั่วโมงที่ผ่านมาของทุก account" กลายเป็น scatter-gather ข้าม N shard โดย shard ที่ช้าที่สุดกำหนด latency ของคุณ

คำตอบ: serve query แบบนี้จาก analytical store หรือ secondary index ที่สร้างมาเพื่อ pattern นั้น — ห้าม fan-out บน OLTP path

2. Cross-shard transaction

การโอนระหว่าง 2 account ที่อยู่ต่าง shard ใช้ local transaction ไม่ได้

ทางเลือก เรียงตามความน่าใช้:

  1. Co-locate ข้อมูลที่เกี่ยวข้องกัน โดยเลือก shard key ให้เคสปกติอยู่ shard เดียว
  2. Saga 2 เฟสแบบ reserve/commit พร้อม compensation (บทที่ 16)
  3. ฐานข้อมูลที่มี distributed transaction (Spanner, CockroachDB, TiDB, Vitess พร้อมข้อจำกัด) และยอมรับต้นทุน latency

3. Hot shard

Account ดัง, merchant รายใหญ่, หรือ key ที่เป็น default คล้าย NULL

คำตอบ: directory-based mapping เพื่อย้าย tenant นั้นไป capacity เฉพาะ · key-splitting สำหรับ counter · rate limit ต่อ tenant

4. Unique constraint ข้าม shard หยุดทำงาน

หมายเลขโทรศัพท์ที่ต้อง unique ทั้งระบบ บังคับด้วย UNIQUE ต่อ shard ไม่ได้

คำตอบ: ตาราง/service เล็กๆ สำหรับ uniqueness ที่ key ด้วยค่าที่ต้อง unique เอง (ซึ่งตัวมันเองก็ shard ด้วยค่านั้น)

-- ตาราง uniqueness แยก shard ด้วย hash(phone) ไม่ใช่ hash(account_id)
CREATE TABLE phone_claims (
  phone_e164  TEXT PRIMARY KEY,       -- unique ทั้งระบบได้จริง
  account_id  TEXT NOT NULL,
  claimed_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- flow: claim ที่นี่ก่อน แล้วค่อยสร้าง account บน shard ของมัน
-- ถ้าสร้าง account ล้ม -> ต้องมี sweeper ปล่อย claim ที่ค้าง

Consistency model เรียงตามลำดับที่ควรเรียน

Modelคำสัญญาแบบภาษาคนต้นทุนทั่วไปต้องใช้ที่ไหน
Linearizable (strong)ทุกคนเห็นลำดับเดียวกันแบบ real-time · read เห็น write ที่ commit ล่าสุดเสมอcoordination ทุก operation; latency; ใช้ไม่ได้ตอนเกิด partitionยอดเงิน, lock, leader election, uniqueness, limit
Sequentialมีลำดับรวมเดียวที่ทุก process เห็นเหมือนกัน และรักษา program order ของแต่ละ process — แต่ไม่ผูกกับเวลาจริงสูงระบบ replicated ที่ต้องการลำดับที่ตรงกันแต่ไม่ต้องการ real-time
Serializableผลของ transaction เทียบเท่ากับการรันแบบเรียงต่อกันในลำดับใดลำดับหนึ่งสูงworkload ที่เป็น transaction (บทที่ 14)
Causalถ้า A ทำให้ B เกิด ทุกคนเห็น A ก่อน B · เรื่องที่ไม่เกี่ยวกันเห็นลำดับไหนก็ได้ปานกลาง — track causality ไม่ต้องตกลงกันทั้งระบบcomment กับ reply, chat thread, collaborative editing
Read-your-writesคุณเห็นการเปลี่ยนแปลงของตัวเอง คนอื่นอาจช้าต่ำ — session stickiness หรือ version tokenแทบทุกแอปที่มีผู้ใช้ — model ที่ต้องใช้บ่อยที่สุดและถูกลืมบ่อยที่สุด
Monotonic readsเวลาไม่เดินย้อนหลัง (ค่าที่เคยเห็นไม่หายไป)ต่ำ — pin session ไว้กับ replica เดียวfeed, list, อะไรที่ผู้ใช้ scroll
Eventualถ้าหยุดเขียน replica ทั้งหมดจะบรรจบกัน แต่ไม่สัญญาว่าเมื่อไหร่ถูกที่สุด available ที่สุดcounter อย่างจำนวน view, cache, search index, recommendation

Sequential และ Serializable **ไม่เท่ากับ** linearizable

ทั้งคู่รับประกันว่า "มีลำดับหนึ่งที่อธิบายผลได้" แต่ไม่รับประกันว่าลำดับนั้น ตรงกับเวลาจริง — transaction ที่ commit ทีหลังอาจถูกจัดให้อยู่ก่อนในลำดับนั้นได้

ผลในทางปฏิบัติ: SERIALIZABLE ไม่ได้ ให้ read-your-writes ข้าม session หรือข้าม replica โดยอัตโนมัติ ถ้าคุณต้องการ "อ่านแล้วต้องเห็นของที่เพิ่งเขียน" คุณต้องการ linearizable read (อ่านจาก primary) หรือกลไก read-your-writes ที่ระบุไว้ในตารางด้านบน — ไม่ใช่การเพิ่ม isolation level

[!TIP] วิธีคุยเรื่องนี้กับ stakeholder ฝ่ายธุรกิจ ห้ามถามว่า "ต้องการ strong หรือ eventual consistency" — คำตอบจะเป็น "strong" ทุกครั้ง และมันจะแพงและมักไม่จำเป็น

ถามแบบนี้แทน:

  • "ถ้าตัวเลขนี้เก่าไป 5 วินาทีบนจอของคนอื่น จะเกิดอะไรขึ้น"
  • "ถ้า 2 คนทำสิ่งนี้ในวินาทีเดียวกัน ใครควรชนะ และคนที่แพ้ต้องรู้ไหม"
  • "อะไรแย่กว่า — แสดงว่าไม่มีข้อมูลชั่วคราว หรือแสดงข้อมูลที่เก่าไปหน่อย"

คำถามพวกนี้ได้คำตอบจริง และคำตอบ map ตรงเข้าตารางข้างบน

แล้วเขียนขอบเขตที่เลือกลงใน SLO: "มุมมองยอดเงินของผู้ใช้คนอื่นอาจช้าได้ถึง 5 วินาที; ผู้จ่ายเห็นผลของตัวเองทันทีเสมอ" — ประโยคเดียวนี้เป็นทั้งการตัดสินใจเชิง design, test case, และบทพูดของทีม support

[!CAUTION] 3 เรื่องที่ต้อง linearizable ในระบบเงิน ยอดเงินที่ใช้ตัดสินใจ, การตรวจ uniqueness ของ idempotency key, และ การเช็ค limit — ทั้ง 3 อย่างอ่านจาก primary ใน transaction เท่านั้น

ทุกอย่างที่เหลือ (การแสดงประวัติ, notification, dashboard, search) ใช้ eventual ได้และควรใช้ เพราะมันซื้อ availability กลับมาให้คุณ

อ้างอิง: Jepsen consistency model map — ลำดับชั้นที่เป็นมาตรฐาน พร้อมบอกว่า model ไหน available ตอนเกิด partition

Checklist replication & sharding

  • ข้อมูลการเงินใช้ synchronous replication (RPO 0) ใน region
  • มี alert ที่ replication lag
  • Authorization, ยอดเงิน และ duplicate check อ่านจาก primary เท่านั้น
  • มีกลไก read-your-writes (คืน object ที่สร้าง หรือ sticky-to-primary)
  • ทำครบทั้ง 7 ขั้นก่อน shard แล้ว และมีหลักฐานว่าไม่พอ
  • ถ้า shard — ใช้ logical shard จำนวนที่เผื่อไว้ (1024/4096)
  • มี routing/proxy layer ไม่ใช่ shard logic กระจายในโค้ด
  • Cross-shard query ไป analytical store ไม่ fan-out บน OLTP
  • Cross-shard transaction มีแผน (co-locate / saga / distributed DB)
  • Hot shard มีทางย้าย tenant ไป capacity เฉพาะ
  • Unique constraint ระดับ global มีตาราง/service แยก
  • Consistency model ของแต่ละ user journey เขียนไว้ใน SLO เป็นตัวเลขวินาที

สรุปบทนี้

เงินต้องใช้ synchronous replication · authorization/ยอดเงิน/duplicate check ห้ามอ่านจาก replica · ทำ 7 อย่างให้หมดก่อน shard · logical shard ทำให้ reshard รอดชีวิต · sharding สร้างปัญหา 4 ข้อที่ต้องวางแผนล่วงหน้าทั้ง 4 · และถามคำถามเรื่อง consistency ด้วยภาษาผลกระทบทางธุรกิจ ไม่ใช่ศัพท์วิชาการ