CAP Theorem, ACID vs BASE, และ NoSQL
เมื่อสำเนาข้อมูลอยู่หลายเครื่อง คำว่า “ข้อมูลปัจจุบัน” ไม่ได้ง่ายเหมือนเดิม หากเครือข่ายขาด เราจะหยุดตอบเพื่อรักษาความจริงชุดเดียว หรือจะตอบต่อจากข้อมูลที่แต่ละฝั่งมี? บทนี้ไม่ได้สอนให้เลือกฐานข้อมูลจากป้าย SQL/NoSQL แต่สอนให้มองเห็นสิ่งที่ระบบยอมแลกภายใต้ความล้มเหลว
1CAP Theorem
ประโยคยอดนิยมว่า “CAP เลือกได้เพียงสองในสาม” จำง่าย แต่ชวนให้คิดผิดว่า P เป็นตัวเลือกเหมือนติ๊กเมนู ในระบบที่สื่อสารผ่านเครือข่าย เราไม่สามารถสั่งว่า Partition ห้ามเกิด สิ่งที่ออกแบบได้คือ เมื่อ Partition เกิดแล้ว แต่ละ operation จะรักษา C หรือ A อย่างไร
| คุณสมบัติ | ความหมาย |
|---|---|
| Consistency | ในบริบท CAP มักตีความใกล้กับ single-copy/linearizable behavior: operation ดูเหมือนเกิดทีละจุดบนสำเนาเดียว และเคารพลำดับเวลาจริงที่เกี่ยวข้อง ไม่ใช่เพียง “ทุก node เหมือนกันทุก microsecond” |
| Availability | ทุกคำขอที่ส่งถึงโหนดซึ่งไม่ล้มต้องจบด้วยคำตอบที่ไม่ใช่ error แม้คำตอบนั้นอาจไม่ใช่ข้อมูลรุ่นล่าสุดภายใต้โมเดล AP คำว่า timeout หรือ “ลองใหม่ทีหลัง” จึงถือว่าเสีย Availability ในความหมายเข้มของ CAP |
| Partition tolerance | แบบจำลองยอมให้ข้อความระหว่างกลุ่มโหนดสูญหายหรือล่าช้าไม่มีกำหนด ระบบต้องนิยามพฤติกรรมในภาวะนี้ ไม่ได้หมายความว่าทุก operation ทุกฝั่งต้องสำเร็จ |
Availability ไม่ใช่แค่ “Server ตอบอะไรสักอย่าง”
ถ้าตอบ error ได้ก็นับว่า Available ระบบที่ปิดฐานข้อมูลแล้วตอบ HTTP 500 ทุกครั้งจะมี Availability 100% ซึ่งเห็นได้ชัดว่าไม่ใช่ความหมายของทฤษฎี คำตอบต้องเป็นผลที่ถูกต้องตามข้อกำหนดของโมเดลที่ระบบประกาศ แม้อาจเป็นข้อมูลเก่าในระบบที่ยอมรับ stale read
Consistency ใน CAP ใกล้ Linearizability
Linearizability ทำให้แต่ละ operation ดูเหมือนเกิดขึ้น ณ จุดหนึ่งระหว่างเวลาเริ่มกับเวลาจบ หาก write เสร็จก่อน read เริ่ม read ควรเห็น write นั้น การรับประกันนี้เข้าใจง่ายสำหรับโปรแกรมเมอร์ แต่เมื่อเครือข่ายแบ่งฝั่ง โหนดที่ติดต่อเจ้าของข้อมูลหรือ quorum ไม่ได้อาจต้องปฏิเสธคำขอ
Partition ไม่เหมือน Node Crash อย่างเดียว
โหนดอาจยังทำงานดีและตอบ client ในพื้นที่ของตน แต่สื่อสารกับอีกกลุ่มไม่ได้ ปัญหาจึงยากกว่าเครื่องดับ เพราะทั้งสองฝั่งอาจเชื่อว่าตนควรทำงานต่อ สถานการณ์นี้นำไปสู่ split brain หากไม่มี quorum, leader lease, fencing หรือกลไกป้องกันที่เหมาะสม
การเลือกไม่ได้เกิดระดับผลิตภัณฑ์อย่างเดียว
ระบบเดียวอาจให้ read concern, write concern หรือ consistency level หลายแบบ Operation อ่านหน้า feed อาจยอม stale ได้ ส่วนการตัดยอดสินค้าต้องรอ quorum การถามว่า “ฐานข้อมูลนี้เป็น CP หรือ AP” จึงเป็นจุดเริ่มต้น แต่คำถามที่ดีกว่าคือ:
- ภายใต้ failure ใด Operation นี้ยังตอบ
- คำตอบรับประกัน freshness หรือ ordering ระดับใด
- ต้องรอกี่ replica และรอข้าม region หรือไม่
- เมื่อข้อมูลขัดแย้ง ระบบตรวจพบและรวมกลับอย่างไร
CP: ยอมให้บางคำขอรอหรือถูกปฏิเสธ
ระบบที่รักษา linearizable write อาจกำหนดว่าต้องมี quorum หรือ leader ที่ถูกต้อง หากกลุ่มเสียงข้างมากติดต่อกันไม่ได้ ฝั่งเสียงข้างน้อยไม่รับ write ต่อ แม้เครื่องยังเปิดอยู่ จุดนี้ไม่ได้หมายความว่าทั้งระบบดับ บาง read หรือข้อมูลคนละ shard อาจยังให้บริการได้ Availability จึงควรวิเคราะห์ราย operation และ failure scope
AP: ตอบต่อ แล้วรับภาระ Reconciliation
ระบบที่ยอมให้หลายฝั่งรับ write ระหว่าง Partition ต้องมีวิธีจัดการ version เมื่อเครือข่ายกลับมา เช่น last-write-wins, vector clock, CRDT หรือ application-specific merge ไม่มีวิธีใดเหมาะทุกข้อมูล การเลือก AP ไม่ได้ทำให้ความขัดแย้งหายไป เพียงย้ายเวลาตัดสินจากตอนเขียนไปตอนรวมข้อมูลภายหลัง
CA มีประโยชน์เป็นภาพ แต่ไม่ใช่มุมของระบบกระจายเมื่อเกิด Partition
บนเครื่องเดียวไม่มี network partition ระหว่าง replicas จึงไม่อยู่ในเงื่อนไขที่ CAP บังคับให้เลือกระหว่าง C กับ A แต่เครื่องยังล้ม ดิสก์ยังเสีย และ transaction ยัง block ได้ คำว่า CA ไม่ได้แปลว่าฐานข้อมูลเครื่องเดียว Available ตลอดเวลา เพียงแต่ CAP trade-off ไม่ถูกกระตุ้นด้วย network partition ระหว่างสำเนา
| เมื่อ Partition เกิด | สิ่งที่รักษา | สิ่งที่ยอมเสีย |
|---|---|---|
| แนว CP | Operation ที่ยอมรับต้องยังรักษา consistency model ที่ประกาศ | บางคำขออาจ timeout, error หรือรอจน quorum กลับมา |
| แนว AP | โหนดที่ยังทำงานตอบ operation ต่อ | ผลอาจ stale หรือเกิด concurrent versions ที่ต้อง reconcile |
PACELC: แม้เครือข่ายปกติก็ยังมีสิ่งที่ต้องแลก
PACELC ขยายมุมมองว่า ถ้าเกิด Partition (P) เลือกระหว่าง Availability กับ Consistency; Else (E) เมื่อระบบปกติ ยังมักเลือกระหว่าง Latency กับ Consistency เช่นรอ replica ข้าม region มากขึ้นช่วยเพิ่มความมั่นใจแต่ response ช้าขึ้น ทำให้เห็นว่าการแลกไม่ได้เกิดเฉพาะวันระบบพัง
2ACID vs BASE: สองปรัชญาการออกแบบ Consistency
หัวข้อนี้มักวาง ACID กับ BASE ไว้คนละฝั่งราวกับต้องเลือกศาสนา แต่จริง ๆ แล้ว ACID อธิบายคุณสมบัติของ transaction ส่วน BASE เป็นคำย่อเชิงแนวคิดที่เน้น availability และ eventual convergence ระบบหนึ่งอาจมี transaction แบบ ACID ในขอบเขตหนึ่ง และใช้ asynchronous replication แบบ eventual ในอีกขอบเขตหนึ่งได้
- Atomicity — ทั้งหมดหรือไม่เลย
- Consistency — ข้อมูลถูกต้องตามกฎเสมอ
- Isolation — ธุรกรรมไม่รบกวนกัน
- Durability — สำเร็จแล้วไม่หายแม้ระบบล่ม
- Basically Available — ระบบพร้อมให้บริการเกือบตลอดเวลา
- Soft state — สถานะข้อมูลอาจเปลี่ยนแปลงได้แม้ไม่มี input ใหม่ (เพราะกำลัง sync กันอยู่)
- Eventual consistency — ข้อมูลจะตรงกันในที่สุด ถ้าไม่มี input ใหม่เข้ามาอีก
Consistency ใน ACID ไม่ใช่ C ใน CAP
| คำเดียวกัน | ความหมาย | ตัวอย่าง |
|---|---|---|
| ACID Consistency | Transaction พาฐานข้อมูลจากสถานะที่ไม่ละเมิด invariant ไปยังอีกสถานะที่ไม่ละเมิด invariant | ยอดห้ามติดลบ, foreign key ต้องอ้างแถวที่มีอยู่ |
| CAP Consistency | พฤติกรรมของสำเนาหลายชุดใกล้ single-copy/linearizable semantics ภายใต้ concurrent operations และ failure | read หลัง write ที่จบแล้วเห็นค่ารุ่นนั้นตาม real-time order |
ฐานข้อมูลอาจให้ transaction atomic แต่ replication read ไม่ linearizable หรือให้ linearizable register แต่ไม่มี multi-row invariant คำว่า Consistency จึงต้องตามด้วยคำถามว่า consistency model ใด และอยู่ในขอบเขต object, partition หรือ transaction ใด
Atomicity: ทั้งชุดสำเร็จหรือไม่มีผล
สมมุติโอนเงิน 100 บาทจาก A ไป B เราไม่ต้องการให้หัก A แล้วเครื่องล้มก่อนเพิ่ม B Atomicity ทำให้ transaction ทั้งชุด commit หรือ abort แต่ไม่ได้รับประกันเองว่าธุรกิจเขียนกฎถูก เช่นโปรแกรมอาจโอนไปบัญชีผิดอย่าง atomic มากก็ได้
Isolation มีหลายระดับ
คำว่า “Transaction ไม่รบกวนกัน” เป็นภาพเริ่มต้น แต่ระบบจริงมี isolation levels เช่น Read Committed, Snapshot Isolation และ Serializable ซึ่งยอม anomaly ต่างกัน ระดับสูงขึ้นมักเพิ่มการรอ การตรวจ conflict หรือการ abort การบอกเพียงว่า “รองรับ ACID” จึงยังไม่พอ ต้องดู isolation level และข้อจำกัดจริง
| ปรากฏการณ์ | ภาพง่าย ๆ | ผลกระทบ |
|---|---|---|
| Dirty Read | อ่านค่าที่ยังไม่ commit | ค่าที่เห็นอาจถูก rollback หายไป |
| Non-repeatable Read | อ่านแถวเดิมสองครั้งได้คนละค่า | รายงานใน transaction เดียวไม่คงที่ |
| Phantom | query เงื่อนไขเดิมพบจำนวนแถวใหม่ | ชุดข้อมูลเปลี่ยนระหว่าง transaction |
| Write Skew | สอง transaction อ่าน snapshot เดียวแล้วเขียนคนละแถว | invariant ข้ามแถวอาจถูกละเมิดแม้ไม่มี write conflict ตรง ๆ |
Durability ต้องถามว่าทนต่ออะไร
เมื่อระบบตอบว่า commit แล้ว ข้อมูลควรรอดจาก failure ตามขอบเขตที่ประกาศ แต่การเขียนลง memory, OS buffer, local journal, replica หนึ่งตัว หรือ quorum ข้าม region ให้ระดับความทนทานต่างกัน Write concern และ fsync policy จึงเป็นส่วนหนึ่งของความหมายคำว่า “สำเร็จ”
BASE ไม่ใช่ข้ออ้างให้ข้อมูลผิดโดยไม่มีเส้นตาย
Eventual consistency กล่าวว่าเมื่อหยุดมี update ใหม่ สำเนาจะ converge ในที่สุดภายใต้สมมุติฐานที่ระบบซ่อมแซมทำงานได้ แต่ไม่ได้บอกว่ากี่วินาที ไม่บอกว่าผู้อ่านหนึ่งคนจะเห็นค่าถอยหลังหรือไม่ และไม่บอกว่าจะรวม concurrent writes แบบใด Application ต้องกำหนด SLO, repair process และ user-visible semantics เพิ่มเติม
accepted ออกจาก completed ให้ชัด ไม่ควรแสดงว่า “สำเร็จแล้ว” หากงานยังมีโอกาสล้ม จากนั้นต้องมี idempotency key, retry, dead-letter handling และหน้าสถานะให้ตรวจสอบได้ การซ่อนความหน่วงไม่ควรกลายเป็นการซ่อนความจริงEventual Consistency มีหลายระดับประสบการณ์ผู้ใช้
- Read-your-writes: ผู้ใช้ควรเห็นสิ่งที่ตนเพิ่งเขียน แม้คนอื่นอาจยังไม่เห็น
- Monotonic reads: เมื่อเคยเห็น version ใหม่แล้ว ไม่ควรถอยกลับไปเห็น version เก่า
- Monotonic writes: write ของผู้ใช้คนเดียวถูกนำไปใช้ตามลำดับ
- Causal consistency: ถ้าเหตุการณ์ B เกิดเพราะเห็น A ผู้อื่นไม่ควรเห็น B ก่อน A
คุณสมบัติเหล่านี้อยู่ระหว่าง eventual แบบอ่อนกับ linearizability แบบเข้ม ระบบอาจให้ session guarantee เพื่อประสบการณ์ที่สมเหตุสมผลโดยไม่ต้องจ่ายค่าความเข้มสูงสุดทุก operation
Compensation แทน Rollback ข้ามบริการ
ใน Microservices การครอบทุกบริการด้วย distributed transaction อาจแพงหรือเปราะ จึงมี Saga ที่แต่ละขั้น commit ใน local transaction และมี compensating action หากขั้นหลังล้ม เช่นคืน stock หรือคืนเงิน Compensation ไม่ใช่การย้อนเวลา เพราะโลกภายนอกอาจเห็นเหตุการณ์ไปแล้ว จึงต้องออกแบบ business semantics ให้รับสถานะระหว่างทางได้
ACID กับ BASE ไม่ได้ผูกกับ SQL/NoSQL แบบหนึ่งต่อหนึ่ง
ฐานข้อมูล SQL แบบกระจายหลายระบบให้ ACID transactions ส่วน document หรือ wide-column store บางระบบมี transaction หรือ compare-and-set ในขอบเขตที่กำหนด ในทางกลับกัน SQL database ที่ใช้ asynchronous replica ก็อาจให้ stale read ได้ การเลือกควรดู guarantee จริง ไม่ใช่ตัดสินจาก query language
3NoSQL: สี่ประเภทหลัก
NoSQL เดิมสื่อถึงระบบที่ไม่ได้ใช้ relational model/SQL แบบดั้งเดิม ต่อมามักตีความว่า Not Only SQL แต่ไม่ว่าขยายคำอย่างไร NoSQL ไม่ใช่คุณสมบัติด้าน performance หรือ consistency โดยอัตโนมัติ มันเป็นกลุ่มระบบที่ data model และ query model ต่างกันมาก
| ประเภท | โมเดลข้อมูล | เหมาะกับ Access Pattern | ข้อจำกัดที่มักพบ |
|---|---|---|---|
| Key-Value Store | ค้น value ด้วย key ระบบอาจไม่เข้าใจโครงสร้างภายใน | session, cache, lookup ที่รู้ key | query ภายใน value หรือ join จำกัด |
| Wide-Column / Column Family | row ที่มีคอลัมน์ sparse จัดกลุ่มเป็น family และกระจายด้วย row/partition key | ข้อมูลปริมาณมากที่ query ตาม key/range ที่ออกแบบไว้ | schema ต้องเริ่มจาก query; ad-hoc query ไม่ใช่จุดเด่น |
| Document Store | เก็บ document เช่น BSON/JSON และ index field ภายในได้ | aggregate ที่อ่านเขียนร่วมกันและ schema เปลี่ยนตามรุ่น | document โตไม่จำกัด, cross-document join/transaction มีต้นทุน |
| Graph Database | node, edge และ properties เป็นหน่วยหลัก | traversal ความสัมพันธ์หลายทอดและ pattern matching | การกระจาย graph ข้ามเครื่องและ supernode อาจยาก |
NoSQL ไม่ได้แปลว่า Schemaless
แม้ฐานข้อมูลไม่บังคับ schema แบบตารางสัมพันธ์ Application ยังมี schema อยู่ใน code, validation, index และความคาดหวังของผู้อ่าน หากเอกสารรุ่นเก่ากับรุ่นใหม่มี field ต่างกัน เราต้อง version และ migrate logic การไม่มี DDL เข้มงวดไม่ได้ทำให้ความรับผิดชอบเรื่อง schema หายไป เพียงย้ายจาก database ไป application มากขึ้น
เริ่มออกแบบจาก Query ไม่ใช่เริ่มจาก Entity Diagram อย่างเดียว
ใน relational design เรามัก normalize เพื่อลดความซ้ำแล้วใช้ join ตอนอ่าน ในระบบที่กระจายข้อมูลตาม partition key การ join ข้าม partition แพง จึงอาจ denormalize และสร้างตารางตาม query เช่นตาราง “คำสั่งซื้อตามลูกค้า” กับ “คำสั่งซื้อตามวัน” เก็บข้อมูลซ้ำคนละมุม
Polyglot Persistence: ระบบเดียวอาจใช้หลายฐานข้อมูล
ตะกร้าสินค้าอาจใช้ key-value, รายการสั่งซื้อใช้ relational transaction, recommendation ใช้ graph และ event history อยู่ object storage การใช้ฐานข้อมูลเดียวลด operational complexity แต่บังคับ workload ต่างชนิดให้เข้ากับเครื่องมือเดียว การใช้หลายระบบยืดหยุ่นกว่าแต่เพิ่มงานด้าน replication, security, observability และความรู้ของทีม
เกณฑ์เลือกที่ควรถามก่อนชื่อผลิตภัณฑ์
- Key ที่ใช้ค้นคืออะไร และต้อง range scan หรือไม่
- หนึ่ง partition อาจโตได้เท่าใด มี hot key หรือไม่
- ต้อง transaction ข้าม record/partition หรือไม่
- ยอม stale read, lost update หรือ reordered events ได้แค่ไหน
- ต้อง query แบบ ad hoc หรือ query pattern คงที่
- ทีมรับ operational burden และ backup/repair ได้หรือไม่
HBase: ตัวอย่าง Column Family ในทางปฏิบัติ
// ภาพเชิงแนวคิดของข้อมูล HBase
Table People (Name, Home, Office) {
101: {
Name: {First="Florian", Last="Krepsbach"},
Home: {Phone="555-1212"},
Office: {Phone="666-1212"}
}
}
GET People:101:Name:First → "Florian"
Cell มี Version ตาม Timestamp
ตำแหน่งหนึ่งอธิบายด้วย row key, column family, qualifier และ version/timestamp HBase เก็บได้หลาย versions ตาม configuration คุณสมบัตินี้ช่วยงาน time-oriented และ rollback บางแบบ แต่เพิ่มพื้นที่และ compaction burden จึงควรกำหนดจำนวน version/TTL ให้ตรงงาน
Region: ช่วงของ Row Key ที่ถูกกระจาย
ตารางถูกแบ่งเป็น Regions ตามช่วง row key แล้ว RegionServer รับผิดชอบแต่ละช่วง เมื่อ region โตจะแตกเป็นช่วงย่อย การเรียงตาม row key ทำให้ scan ช่วงต่อเนื่องมีประสิทธิภาพ แต่ทำให้ key ที่เพิ่มตามเวลาส่ง write ใหม่ไปปลายช่วงเดียวจน hotspot ได้
Row key: จุดที่การออกแบบข้อมูลกลายเป็นการออกแบบการคำนวณ
HBase เรียงข้อมูลตาม row key และแบ่งข้อมูลเป็นช่วง ไม่ได้ hash ทุก key โดยอัตโนมัติ ถ้าใช้ key ที่เพิ่มขึ้นเรื่อย ๆ เช่น timestamp งานเขียนใหม่อาจไหลไปกองอยู่ Region เดียว เกิด hotspot ทั้งที่เครื่องอื่นยังว่าง
hash(userId)%8 | timestamp | userId เพื่อกระจาย write ไปแปดช่วง วิธีนี้ช่วย write concurrency แต่การอ่านตามช่วงเวลาทั้งหมดต้อง query แปดช่วงแล้ว merge ผล จึงไม่มี row key ที่ดีที่สุดโดยไม่รู้ access patternSalting แบบสุ่มกับ Hash Prefix ต่างกันเล็กน้อย
Random salt ช่วยกระจาย hot pattern แต่ถ้าจะอ่าน row เดิมต้องรู้ salt หรือค้นทุก bucket ส่วน deterministic hash prefix คำนวณ bucket จากส่วนของ key ได้ จึง point lookup ง่ายกว่า การเลือกจำนวน buckets น้อยเกินไปยังกระจายไม่พอ มากเกินไปทำให้ fan-out read สูง
Reverse Timestamp
บางงานต้องการรายการล่าสุดก่อน อาจใช้ค่า timestamp กลับทิศภายในส่วน key หลัง prefix เพื่อให้ scan ได้ตามลำดับที่ต้องการ แต่ถ้าเอา timestamp ไว้หน้าสุดก็ยัง hotspot อยู่ เทคนิคหนึ่งแก้ query order ไม่ได้แก้ distribution ทุกด้าน จึงต้องจัดองค์ประกอบ key เป็นลำดับตามความต้องการ
HBase Write Path แบบย่อ
Write ถูกบันทึกใน Write-Ahead Log เพื่อความทนทานและใส่ MemStore ในหน่วยความจำ เมื่อถึงเงื่อนไข MemStore flush เป็น HFile บน storage ต่อมามี compaction รวมไฟล์เพื่อลดจำนวนชิ้นและจัดการข้อมูลเก่า เส้นทางนี้ทำให้ write เป็นลำดับมากกว่าการแก้ตำแหน่งกระจัดกระจาย แต่ต้นทุนถูกเลื่อนไป compaction ภายหลัง
HBase Read Path แบบย่อ
Read อาจตรวจ BlockCache, MemStore และ HFiles หลายชุด Bloom Filter ช่วยตัดไฟล์ที่แน่ใจว่าไม่มี key แต่ถ้า Bloom Filter บอกว่า “อาจมี” ยังต้องตรวจจริง เพราะมี false positive ได้ การมี HFiles มากเกินไปเพิ่ม read amplification จึงต้องให้ compaction ทำงานอย่างสมดุล
ความสัมพันธ์กับ HDFS
HBase ใช้ distributed storage อย่าง HDFS เป็นฐาน แต่เพิ่มการเข้าถึง record แบบสุ่มและระบบจัด region เหนือไฟล์ก้อนใหญ่ HDFS เหมาะกับ streaming file access ส่วน HBase จัด index และ write path สำหรับ row-level access การเข้าใจ HDFS จากบทก่อนจึงช่วยเห็นว่า HBase ไม่ได้แทน HDFS แต่สร้างบริการข้อมูลอีกระดับบนมัน
4Cassandra: กรณีศึกษาระบบ Wide-Column แบบกระจาย
คำว่า “AP แบบเต็มรูปแบบ” ในหัวข้อเดิมทำให้ดูเหมือน Cassandra เลือก Availability โดยไม่สน Consistency ความจริง Cassandra ให้ผู้ใช้เลือก consistency level ต่อคำขอ มีทั้ง ONE, QUORUM, ALL และระดับเฉพาะ datacenter อีกทั้งมี Lightweight Transaction ที่ใช้ Paxos สำหรับ compare-and-set แบบ linearizable ในขอบเขตที่กำหนด จึงควรมองว่าเป็นระบบที่ tunable มากกว่าป้าย AP คำเดียว
Primary Key มีสองหน้าที่ในบรรทัดเดียว
CREATE TABLE events_by_user (
user_id text,
event_time timestamp,
event_type text,
payload text,
PRIMARY KEY ((user_id), event_time)
) WITH CLUSTERING ORDER BY (event_time DESC);
user_id เป็น Partition Key ทำให้ event ของผู้ใช้เดียวกันอยู่ partition เดียว ส่วน event_time เป็น Clustering Column ทำให้ query รายการล่าสุดของผู้ใช้ได้ตามลำดับ แต่ถ้าผู้ใช้คนหนึ่งมี events ไม่จำกัด partition จะโตเกินไป อาจต้อง bucket ตามเดือน เช่น ((user_id, month), event_time)
Partition เล็กเกินไปกับใหญ่เกินไปก็มีปัญหา
| การออกแบบ | ผลดี | ปัญหา |
|---|---|---|
| Partition เล็กมากจำนวนมหาศาล | กระจายได้ละเอียด | metadata, index และ request overhead สูง |
| Partition พอดีกับ query | อ่านต่อเนื่องในโหนดไม่กี่ตัว | ต้องคาด access pattern ล่วงหน้า |
| Partition ใหญ่มาก | query ช่วงกว้างใน partition เดียว | hot partition, compaction/read/repair หนัก |
ไม่มี Master สำหรับ Data Path แต่ไม่ได้แปลว่าไม่มี Coordination
Client ส่งคำขอไปยังโหนดใดก็ได้ โหนดนั้นทำหน้าที่ coordinator ของคำขอนั้น คำนวณว่า replica ใดรับผิดชอบ partition และรอคำตอบตาม consistency level ไม่มี master กลางที่รับทุก read/write แต่ยังมี gossip, failure detector, schema coordination และ repair process หลายชนิด
| กลไก | รายละเอียด |
|---|---|
| Partitioning | Partition Key ถูก hash เป็น token แล้ว token ranges ถูกกระจายให้โหนด/virtual nodes รับผิดชอบ |
| Gossip | โหนดแลกข้อมูลสถานะคลัสเตอร์แบบ peer-to-peer เป็นระยะ แต่ repair ข้อมูล replica เป็นกระบวนการอีกส่วน ไม่ควรเรียกทุกอย่างรวมว่า Gossip |
| Failure Detection | ใช้ค่า Φ (phi) accrual failure detector — ค่า suspicion เพิ่มขึ้นตามเวลาถ้าโหนดไม่ตอบสนอง แทนที่จะตัดสิน "ตายหรือไม่ตาย" แบบ binary ทันที |
| Write Path | เขียน Commit Log และ Memtable ก่อน จากนั้น flush เป็น SSTable ซึ่ง immutable แล้วใช้ Compaction จัดการไฟล์ภายหลัง |
| Read Path | รวมข้อมูลจาก Memtable และ SSTables ที่เกี่ยวข้อง ใช้ Bloom Filter, partition index, cache และ timestamp/tombstone เพื่อ reconcile |
แก่นของ Cassandra โดยไม่ต้องจำรายละเอียดทั้งระบบ
Cassandra นำ partition key ไปผ่าน hash เพื่อหา token แล้วกระจายข้อมูลไปยังโหนดต่าง ๆ เปรียบเหมือนการแบ่งถนนวงแหวนให้หลายสาขารับผิดชอบ เมื่อเพิ่มหรือลดสาขา ข้อมูลที่ต้องย้ายมีเพียงบางช่วง ไม่ต้องสับไพ่ทั้งสำรับใหม่ นี่คือแนวคิดของ consistent hashing
Virtual Nodes ช่วยกระจาย Token Ranges
แทนให้โหนดหนึ่งรับช่วงต่อเนื่องเพียงช่วงเดียว Virtual Nodes ทำให้โหนดรับผิดชอบหลาย token ranges กระจายบนวงแหวน ช่วย balance และย้ายข้อมูลเมื่อเพิ่ม/ลบโหนดได้ละเอียดขึ้น แต่จำนวน vnodes และ topology ยังต้องวางแผนตามรุ่นและ workload ไม่ใช่ค่ามากที่สุดเสมอไป
Replication Factor บอกจำนวนสำเนา
ถ้า RF=3 partition หนึ่งมี replicas สามชุดตาม strategy ที่กำหนด การวางข้าม rack/datacenter ช่วยทน failure domain แต่เพิ่ม latency และ bandwidth หาก write ต้องรอ replica ระยะไกล จึงมี consistency levels เช่น LOCAL_QUORUM เพื่อให้ quorum ภายใน datacenter
Tunable Consistency: R + W > RF
ในแบบจำลองง่าย หากจำนวน replica ที่รออ่าน (R) บวกจำนวนที่รอเขียน (W) มากกว่า Replication Factor ชุดอ่านกับชุดเขียนต้อง overlap อย่างน้อยหนึ่ง replica เช่น RF=3, QUORUM read=2 และ QUORUM write=2
เงื่อนไข overlap ช่วยเรื่อง freshness ภายใต้สมมุติฐานของระบบ แต่ไม่ควรถูกยกเป็นคำรับประกัน transaction ทุกชนิด Clock skew, concurrent writes, failed writes, tombstones และ consistency level ข้าม datacenter ยังมีรายละเอียด การอ่าน QUORUM ไม่เท่ากับ serializable transaction โดยอัตโนมัติ
Write Path: เร็วตอนเขียน เพราะเลื่อนงานบางส่วนไปทีหลัง
- Coordinator ส่ง mutation ไป replicas ตาม topology
- Replica บันทึก Commit Log เพื่อ recovery
- อัปเดต Memtable ที่จัดลำดับใน memory
- เมื่อถึงเกณฑ์ Memtable flush เป็น SSTable ซึ่งไม่แก้ทับ
- Compaction รวม SSTables, กำจัดข้อมูลเก่า และจัดการ tombstone ตามเงื่อนไข
การเขียนแบบ append/immutable SSTable ลด random write แต่ไม่ได้ลบงาน มันย้ายงานไป compaction ภายหลัง หาก write rate สูงเกิน compaction capacity จะเกิด backlog, read amplification และพื้นที่ชั่วคราวสูง นี่เชื่อมกับแนวคิด Immutability ในบท 5: ลด write lock ได้ แต่ต้องจ่ายด้วย version และการเก็บกวาด
Read Path: ต้องรวมความจริงจากหลายชั้น
ข้อมูลรุ่นล่าสุดอาจอยู่ Memtable รุ่นเก่าอยู่ใน SSTables หลายไฟล์ และบางค่าถูกลบด้วย tombstone Read จึงต้องค้น candidate files, อ่านข้อมูลและ reconcile ตาม key/timestamp Bloom Filter ช่วยตอบว่าไฟล์ “ไม่มีแน่นอน” ได้ แต่คำตอบว่า “อาจมี” ยังต้องเปิดดู
Bloom Filter ไม่คืน Record และมี False Positive
Bloom Filter เป็น probabilistic membership structure ถ้าตอบว่าไม่มี เราตัดไฟล์ได้อย่างปลอดภัย ถ้าตอบว่าอาจมี อาจเป็น false positive แต่ไม่มี false negative ภายใต้การใช้งานถูกต้อง การเพิ่ม bit ต่อ key ลด false positive แลกกับ memory
Tombstone: การลบที่ต้องเดินทางไปทุกสำเนา
ในระบบที่มี SSTable immutable และ replica อาจ offline เราไม่สามารถลบ byte เก่าทันทีแล้วจบ จึงเขียน marker ว่าข้อมูลถูกลบ เรียก tombstone เพื่อป้องกันค่ารุ่นเก่าฟื้นกลับจาก replica ที่ยังไม่เห็นการลบ Tombstone มากเกินไปทำให้ read ต้องสแกนและ compaction หนัก การใช้ TTL หรือ delete จำนวนมากจึงต้องออกแบบ
Hinted Handoff, Read Repair และ Anti-Entropy Repair
| กลไก | ทำงานเมื่อใด | ข้อจำกัด |
|---|---|---|
| Hinted Handoff | เก็บ hint ชั่วคราวเมื่อ replica ปลายทางไม่พร้อม แล้วส่งภายหลัง | เป็น best effort และไม่แทน full repair |
| Read Repair | เมื่อ read ที่เกี่ยวข้องพบ replicas ไม่ตรงกัน อาจ reconcile/repair ตาม setting | ซ่อมเฉพาะข้อมูลและ replicas ที่ read แตะ ไม่ได้สแกนทั้งคลัสเตอร์ |
| Anti-Entropy Repair | กระบวนการดูแลเชิงรุกเปรียบเทียบและซ่อมช่วงข้อมูล | ใช้ I/O/network และต้องมี operational schedule |
Timestamp และ Last-Write-Wins
Cassandra ใช้ timestamp เพื่อ reconcile cell versions หลายกรณี ทำให้ clock discipline สำคัญ Last-write-wins ใช้ง่ายแต่ concurrent update หนึ่งอาจหายจากมุมมอง application หาก timestamp ชนะ ไม่ได้ merge ความหมายทางธุรกิจให้ การนับยอดด้วย read-modify-write จึงเสี่ยง lost update และควรใช้ data type/pattern ที่เหมาะ
Lightweight Transactions ไม่ใช่ Transaction ทั่วไปที่ฟรี
คำสั่งเงื่อนไข เช่น IF NOT EXISTS ใช้ consensus/Paxos เพื่อ linearizable compare-and-set มี round trips และ latency สูงกว่า write ปกติ ควรใช้เมื่อ invariant ต้องการจริง ไม่ควรเปลี่ยนทุก write ให้เป็น LWT เพราะต้องการ “ความปลอดภัยเผื่อไว้”
Query-First Modeling และ Denormalization
Cassandra ไม่เหมาะกับการสร้าง table กลางหนึ่งชุดแล้วค่อย query ได้ทุกแบบ มักสร้าง table ต่อ access pattern เช่น events_by_user และ events_by_day Application หรือ pipeline ต้องเขียนข้อมูลซ้ำหลายตาราง การอ่านเร็วขึ้น แต่ write path และ consistency ระหว่าง views ซับซ้อนขึ้น
| MySQL | Cassandra | |
|---|---|---|
| Write เฉลี่ย | ~300 ms | 0.12 ms |
| Read เฉลี่ย | ~350 ms | 15 ms |
5เมื่อยุคเปลี่ยน: CAP และ NoSQL ในปี 2026
Consensus ไม่ได้ทำให้ได้ C+A ระหว่าง Partition
Raft หรือ Paxos ช่วยให้กลุ่มโหนดตกลงลำดับและเลือกค่าที่ปลอดภัยเมื่อยังมี quorum แต่ฝั่งที่ไม่ถึง quorum ต้องหยุดรับ operation บางชนิด นี่คือการรักษา Consistency ด้วยการยอมลด Availability ของฝั่งเสียงข้างน้อย ไม่ใช่ทางลัดผ่าน CAP
Multi-Region ทำให้ Latency กลายเป็นส่วนของ Semantics
หาก transaction ต้องรอ quorum ข้ามทวีป ความเร็วแสงและ network path สร้าง latency ขั้นต่ำ การวาง leader ใกล้ผู้ใช้บางกลุ่มช่วยกลุ่มนั้นแต่ทำให้อีกกลุ่มไกลขึ้น บางระบบให้ follower read ที่เร็วแต่ stale บางระบบให้ bounded staleness หรือ exact timestamp read ผู้ใช้จึงเลือกความสดกับ latency ตาม operation
Serverless/Managed ไม่ได้ตัด Trade-off ออก เพียงย้ายผู้ดูแล
บริการ managed ลดภาระติดตั้ง patch, backup และ scaling บางส่วน แต่ schema, hot key, consistency setting, cost model และ failure semantics ยังเป็นหน้าที่ของผู้ออกแบบ Application การกดเลือก “multi-region” ไม่ได้ตอบว่า concurrent writes จะรวมอย่างไร
CRDT: ออกแบบ State ให้ Merge ได้
Conflict-free Replicated Data Type ใช้โครงสร้างทาง Algebra ทำให้ replica merge concurrent updates แล้ว converge โดยไม่ต้องเลือกว่า write ใดหาย เช่น grow-only counter หรือ set บางชนิด แต่ CRDT ไม่ได้ใช้ได้กับ invariant ทุกแบบ การถอนเงินจากยอดจำกัดยังต้อง coordination หรือแบ่งสิทธิ์อย่างระมัดระวัง
จุดเชื่อมกับบท Functional Programming คือ merge operation ต้องมีคุณสมบัติ เช่น associative, commutative และ idempotent ตามแบบของ state-based CRDT หลายชนิด โครงสร้างทางคณิตศาสตร์จึงช่วยลด coordination ได้จริงเมื่อความหมายของข้อมูลรองรับ
Consistency เป็น Spectrum ไม่ใช่สวิตช์
ระหว่าง Eventual กับ Linearizable มี Sequential, Causal, Session Guarantees, Bounded Staleness และ Snapshot semantics หลายแบบ การบอกว่า “Strong” หรือ “Weak” โดยไม่ระบุ model มักไม่พอสำหรับวิเคราะห์ anomaly
6กรอบตัดสินใจเลือกฐานข้อมูลจาก Workload
การเริ่มจากรายชื่อผลิตภัณฑ์ทำให้เราเถียงเรื่องคุณสมบัติที่ยังไม่รู้ว่าจำเป็นหรือไม่ วิธีที่เป็นระบบกว่าคือเริ่มจากข้อมูล Operation และ Failure แล้วค่อยหา engine ที่ให้ guarantee นั้นด้วยต้นทุนที่ทีมรับได้
ขั้นที่ 1: เขียน Invariant ก่อน Schema
Invariant คือกฎที่ห้ามละเมิด เช่น coupon ใช้ได้ครั้งเดียว ยอดรวม debit เท่ากับ credit หรือ seat หนึ่งที่นั่งขายได้คนเดียว หาก invariant ข้ามหลาย records/partitions เราต้องรู้ว่าระบบรองรับ transaction ขอบเขตนั้นหรือ Application จะชดเชยอย่างไร
ขั้นที่ 2: ระบุ Access Pattern
| คำถาม | ผลต่อการออกแบบ |
|---|---|
| อ่านด้วย key หรือ scan ช่วง | กำหนด partition/shard key และ index |
| อ่านหลายความสัมพันธ์ | อาจต้อง graph traversal, join หรือ denormalized views |
| write ต่อ key สูงหรือไม่ | ตรวจ hot key และ contention |
| query ad hoc หรือคงที่ | Relational/columnar engine อาจเหมาะกับ ad hoc กว่า query-first store |
| ข้อมูลโตตามเวลา | ต้อง bucket, TTL, archive และ compaction policy |
ขั้นที่ 3: เขียน Anomaly Budget
แทนถามว่า “ต้อง Strong Consistency ไหม” ให้ระบุว่ายอมรับอะไรได้ เช่นอ่านช้าไม่เกิน 5 วินาที, ห้าม lost update, ยอม duplicate event แต่ consumer ต้อง idempotent, ผู้ใช้ต้อง read-your-writes การเขียนข้อกำหนดเป็นพฤติกรรมช่วยเลือก model ได้ตรงกว่า adjective
ขั้นที่ 4: ระบุ Failure Domain
ต้องทน process crash, node crash, rack outage, availability zone หรือ region outage? Replication Factor สามชุดใน rack เดียวไม่ได้ทน rack failure ส่วนสาม regions เพิ่มความทนทานแต่มี cost และ latency สูงขึ้น ความทนทานต้องวางสำเนาข้ามขอบที่ต้องการจริง
ขั้นที่ 5: ดู Operational Burden
ระบบที่ benchmark เร็วแต่ต้อง repair, compact และ tune ซับซ้อนอาจไม่คุ้มกับทีมเล็ก ต้องประเมิน backup/restore, schema evolution, monitoring, upgrade, incident response และความสามารถของทีม Database ที่ดีบนกระดาษอาจเป็นระบบที่แย่หากไม่มีใครดูแลเป็น
ขั้นที่ 6: Benchmark Guarantee เดียวกัน
อย่าเปรียบเทียบ write ที่รอ fsync+quorum กับ write ที่ตอบหลังใส่ memory ของ replica เดียว แล้วสรุปว่าตัวหลังเร็วกว่าร้อยเท่า ควรตั้ง durability, consistency, payload, concurrency และ failure behavior ให้ใกล้กัน รวม p95/p99 latency ไม่ใช่ดู average อย่างเดียว
ตัวอย่าง Mapping แบบไม่ผูกชื่อผลิตภัณฑ์
| Workload | สิ่งที่สำคัญ | โมเดลที่ควรพิจารณา |
|---|---|---|
| Ledger การเงิน | Invariant, audit, serializable transaction | Relational/Distributed SQL ที่ guarantee ชัด |
| Session Cache | Key lookup, TTL, latency ต่ำ | Key-value store |
| IoT Events ตาม Device/เวลา | write สูง, range ภายใน device bucket | Wide-column หรือ time-series store |
| Product Catalog | document fields ต่างตามประเภท, index หลาย field | Document store หรือ relational JSON ตาม query |
| Fraud Relationship | เดินความสัมพันธ์หลายทอด | Graph database/graph processing |
| Analytics ระยะยาว | scan คอลัมน์, aggregate ปริมาณมาก | Columnar files/warehouse/lakehouse |
7กรณีศึกษาเชิงความคิด
กรณีที่ 1: จำนวน Like
จำนวน Like อาจยอมให้แสดงคลาดเล็กน้อยและ converge ภายหลังได้ แต่การกด Like ของผู้ใช้เองควร read-your-writes ไม่เช่นนั้นผู้ใช้กดแล้วหัวใจดับทันทีจนคิดว่าระบบเสีย เราอาจใช้ counter ที่ merge ได้และ session guarantee ที่ขอบหน้าจอ
กรณีที่ 2: Stock สินค้าชิ้นสุดท้าย
ถ้าสินค้าเหลือหนึ่งชิ้นและสอง region ขายพร้อมกัน การตอบทั้งคู่รักษา Availability แต่ละเมิด invariant การแก้อาจใช้ leader/quorum, reservation token, escrow แบ่ง stock ต่อ region หรือยอม oversell แล้ว compensate แต่ละวิธีเปลี่ยน business semantics และประสบการณ์ลูกค้า
กรณีที่ 3: Timeline ของ Social Media
Timeline อาจสร้างแบบ fan-out on write หรือ fan-out on read คนดังมีผู้ติดตามมหาศาลทำให้ fan-out on write เกิด hot workload ระบบอาจผสมสองวิธี ยอมให้โพสต์ปรากฏช้าบางส่วน และจัดอันดับจากหลาย source Consistency ที่ต้องการจึงไม่ใช่ “ทุก timeline เหมือนกันทันที”
กรณีที่ 4: Profile กับ Permission
ชื่อเล่นหรือรูป profile stale ไม่กี่วินาทีอาจยอมรับได้ แต่ permission ที่ถูกถอนแล้ว stale อาจเปิดข้อมูลลับ ระบบเดียวกันจึงใช้ cache policy และ consistency ต่างกันตาม field/operation ไม่ควรใช้กฎเดียวกับทั้ง document เพราะจัดเก็บร่วมกัน
กรณีที่ 5: Event Processing อย่างน้อยหนึ่งครั้ง
Broker อาจส่ง event ซ้ำเมื่อไม่แน่ใจว่า consumer ทำสำเร็จหรือยัง หาก handler ใช้ idempotency key การรับซ้ำไม่สร้างผลซ้ำ นี่เป็นตัวอย่างที่ Application design ช่วยอยู่กับ Availability โดยไม่เรียกร้อง exactly-once จากทุกชั้น แต่ต้องกำหนด key และขอบ deduplication ให้ถูก
กรณีที่ 6: DNS
DNS ใช้ caching และ TTL ทำให้ข้อมูลเปลี่ยนแล้วไม่ได้กระจายทันที แต่ให้ระบบทนและตอบได้กว้าง การลด TTL ช่วยให้เปลี่ยนเร็วขึ้นแต่เพิ่ม query load การเพิ่ม TTL ลด load แต่ stale นานขึ้น เป็นตัวอย่าง Latency/Availability/Freshness trade-off ที่ผู้ใช้อินเทอร์เน็ตพบทุกวัน
8สรุปและขั้นตอนถัดไป
CAP ไม่ได้สอนว่าโลกมีฐานข้อมูลสามกล่องให้เลือก แต่สอนว่าขณะเครือข่ายแบ่ง เราไม่สามารถรับทุก operation ต่อและรักษา single-copy semantics พร้อมกันได้ การตัดสินใจจึงต้องลงไปถึง operation และ invariant
ACID กับ BASE ไม่ใช่คู่ตรงข้ามแบบ SQL กับ NoSQL ACID อธิบาย transaction properties ส่วน BASE ชี้แนวทางที่ยอมให้ state เปลี่ยนและ converge ภายหลัง ระบบสมัยใหม่มักผสม guarantee หลายระดับตามขอบข้อมูล
สิ่งที่ควรติดตัวไปมีดังนี้:
- P ไม่ใช่ของที่ “ไม่เลือก” ได้ในระบบเครือข่าย ต้องออกแบบพฤติกรรมเมื่อ Partition เกิด
- CAP Availability ต้องตอบผลที่ไม่ใช่ error ไม่ใช่ตอบอะไรก็ได้
- ACID Consistency กับ CAP Consistency ใช้คำเดียวกันแต่ตอบคนละเรื่อง
- Eventual Consistency ต้องมี conflict resolution, repair และขอบเวลาที่ Application รับได้
- NoSQL ไม่ได้แปลว่าไม่มี schema และไม่ได้แปลว่าเร็วหรือ Available โดยอัตโนมัติ
- HBase Row Key และ Cassandra Partition Key กำหนดทั้งตำแหน่งข้อมูลและต้นทุน query
- Log-structured write ทำให้เขียนต่อเนื่อง แต่ย้ายต้นทุนไป Compaction และ Read Amplification
- Quorum เพิ่มความมั่นใจแต่เพิ่ม latency และไม่ได้แทน transaction semantics ทุกชนิด
- เลือกฐานข้อมูลจาก Invariant, Access Pattern, Anomaly Budget และ Failure Domain
บทถัดไปจะพูดถึง Variety และ Data Integration เมื่อข้อมูลมาจากหลายระบบ ปัญหาไม่ได้มีเพียงสำเนาไม่ตรงกัน แต่ schema, หน่วย, ความหมาย และคุณภาพก็อาจไม่ตรงกันด้วย เราจะขยับจาก “หลายสำเนาของข้อมูลเดียวกัน” ไปสู่ “ข้อมูลหลายชนิดที่ดูเหมือนพูดเรื่องเดียวกัน แต่ใช้ภาษาคนละแบบ”
- แก้ประโยค “CAP เลือกได้เพียงสองในสาม” ให้แม่นยำขึ้น และอธิบายว่าทำไม Partition Tolerance ไม่ใช่ช่องที่ระบบกระจายจะไม่เลือก
- อธิบายว่าทำไมการตอบ error ทุกคำขอจึงไม่นับเป็น Availability ตามความหมายของ CAP
- เปรียบเทียบ ACID Consistency กับ CAP Consistency พร้อมยกตัวอย่างระบบที่มีอย่างหนึ่งแต่ไม่ได้หมายความว่ามีอีกอย่างโดยอัตโนมัติ
- อธิบาย Isolation Levels และยกตัวอย่าง Write Skew ที่ Snapshot Isolation อาจยอมให้เกิด
- Eventual Consistency ไม่ได้บอกอะไรบ้างเกี่ยวกับเวลา ลำดับ และการแก้ Concurrent Writes
- อธิบายว่า Row Key แบบเพิ่มตามเวลาทำให้ HBase เกิด Hotspot ได้อย่างไร และ Hash Prefix แลก Write Distribution กับ Range Scan อย่างไร
- เปรียบเทียบ HBase Region กับ Cassandra Partition ว่าทั้งคู่ใช้ key กำหนดตำแหน่งข้อมูลต่างกันอย่างไร
- ออกแบบ Cassandra Primary Key สำหรับ Event ตามผู้ใช้และเวลา พร้อมอธิบายว่าทำไมอาจต้องทำ Time Bucket
- อธิบาย Cassandra Write Path ตั้งแต่ Commit Log, Memtable, SSTable ถึง Compaction และชี้ว่าต้นทุนไม่ได้หายไปแต่ถูกย้ายไปไหน
- ใช้ RF=3 อธิบาย
R + W > RFพร้อมบอกว่าทำไม Quorum Read/Write จึงยังไม่เท่ากับ Serializable Transaction - เปรียบเทียบ Hinted Handoff, Read Repair และ Anti-Entropy Repair ว่าช่วยให้ Replica กลับมาตรงกันคนละช่วงอย่างไร
- เลือกฐานข้อมูลให้กรณี Stock ชิ้นสุดท้าย โดยเริ่มจาก Invariant, Access Pattern, Anomaly Budget และ Failure Domain แทนการเริ่มจากชื่อผลิตภัณฑ์