Distributed Transactions
บทที่แล้วรับประกัน atomicity ได้บนเครื่องเดียว — แต่ถ้า transaction หนึ่งต้องอัปเดตข้อมูลบนหลายเครื่องพร้อมกันล่ะ? บทนี้คือคำตอบคลาสสิกของปัญหานั้น (Two-Phase Commit) พร้อมข้อจำกัดที่ทำให้วงการ microservices เลือกเดินเส้นทางอื่น
1Flat Transaction เทียบกับ Nested Transaction
| รูปแบบ | ลักษณะ |
|---|---|
| Flat transaction | Client เรียก operation ไปยังหลาย server เรียงกันภายใต้ transaction เดียว ไม่มีโครงสร้างย่อยซ้อนกัน — ถ้า operation ใดล้มเหลว ทั้ง transaction ต้อง abort |
| Nested transaction | Transaction แม่ (parent) แบ่งงานเป็น sub-transaction ลูกที่ทำงานอิสระกันได้ในระดับหนึ่ง — sub-transaction ลูกอาจ commit/abort เองได้โดยไม่กระทบทั้งหมดในทันที เปิดโอกาสให้ทำงานแบบ concurrent ระหว่าง sub-transaction มากขึ้น แต่ซับซ้อนกว่ามากในการ implement |
2Atomic Commit Problem: ทำไม Commit ข้ามเครื่องถึงยาก
3Two-Phase Commit Protocol (2PC)
Phase 1 (canCommit?) Coordinator -> Participant A: canCommit? Coordinator -> Participant B: canCommit? Participant A -> Coordinator: Yes (เขียน prepare log แล้ว) Participant B -> Coordinator: Yes (เขียน prepare log แล้ว) Phase 2 (doCommit) Coordinator: ทุกคนตอบ Yes -> ตัดสินใจ commit Coordinator -> Participant A: doCommit Coordinator -> Participant B: doCommit Participant A -> Coordinator: haveCommitted Participant B -> Coordinator: haveCommitted
เมื่อ Participant ตอบ No
ถ้ามี participant ใดตอบ No ในระหว่าง phase 1 (เช่น constraint ถูกละเมิด หรือ lock ขอไม่ได้) coordinator จะตัดสินใจ abort ทั้ง transaction ทันที แล้วส่ง doAbort ไปยังทุก participant ที่เหลือ
4ความล้มเหลวใน 2PC และปัญหา Blocking
| สถานการณ์ล้มเหลว | ผลกระทบและการรับมือ |
|---|---|
| Participant ล่มก่อนตอบ Phase 1 | Coordinator รอไม่ได้คำตอบ ตัดสินใจ abort ทั้ง transaction (ปลอดภัย เพราะ participant ตัวนั้นยังไม่เคยสัญญาว่าจะ commit) |
| Participant ล่มหลังตอบ Yes แต่ก่อนได้รับ doCommit | เมื่อฟื้นตัว participant ต้องดู log ว่าตัวเองเคยตอบ Yes ไปแล้ว แต่ไม่รู้ว่า coordinator ตัดสินใจ commit หรือ abort ไปแล้ว — ต้องถาม coordinator ใหม่ (recovery) |
| Coordinator ล่มระหว่าง Phase 2 | Participant ที่ตอบ Yes ไปแล้วจะค้างรอคำสั่งอยู่ ปล่อย lock ที่ถืออยู่ไม่ได้ (เพราะยังไม่รู้ผลตัดสิน) — เรียกว่า blocking problem ซึ่งเป็นจุดอ่อนที่มีชื่อเสียงที่สุดของ 2PC |
Distributed Transaction Recovery
Distributed Deadlock
ต้องมี global deadlock detector รวบรวม local wait-for graph จากทุกเครื่องมาต่อกันเป็นภาพรวม หรือใช้เทคนิค edge chasing — ส่ง probe message ตามทิศทางการรอ ถ้า probe กลับมาหาผู้ส่งเอง แปลว่ามีวงจร (deadlock) จริง
5ทันสมัย: จาก 2PC สู่ Saga Pattern
นี่คือจุดเชื่อมที่สำคัญที่สุดของบทนี้กับภาคปฏิบัติ — microservices แทบไม่ใช้ 2PC กันแล้ว เพราะเหตุผลตรงจากที่อธิบายไปข้างต้น
| ประเด็น | 2PC | Saga (Network Computing หัวข้อที่ 4) |
|---|---|---|
| Consistency | Strong consistency — commit พร้อมกันจริง | Eventual consistency — สำเร็จทีละขั้น มีช่วงที่ข้อมูลยังไม่ตรงกัน |
| Availability | Blocking — service ที่ถือ lock รอ coordinator ต้อง "หยุดรับงานใหม่" ชั่วคราวได้ | ไม่ block — แต่ละ local transaction commit จริงทันทีในขั้นตอนของตัวเอง ไม่ต้องรอใคร |
| เมื่อขั้นตอนหนึ่งล้มเหลว | Rollback ได้จริง (ยังไม่ commit ที่ไหนเลยจนกว่าจะครบ phase 2) | ต้องใช้ compensating action "แก้ไขย้อนหลัง" เพราะ step ก่อนหน้า commit จริงไปแล้ว |
| Coupling ระหว่าง service | สูง — ทุก participant ต้องพร้อมพร้อมกัน ณ ขณะเดียวกัน | ต่ำกว่า — แต่ละ service ทำงานอิสระ ไม่ต้องรู้จักหรือพึ่งพา availability ของกันโดยตรงตลอดเวลา |
- อธิบาย Two-Phase Commit Protocol ทีละ phase พร้อมข้อความที่ส่งระหว่าง coordinator กับ participant
- อธิบายว่าทำไม participant ต้องเขียน log ก่อนตอบ "Yes" ใน phase 1 เสมอ
- อธิบาย blocking problem ของ 2PC และเงื่อนไขที่ทำให้เกิดปัญหานี้
- อธิบายว่าทำไม distributed deadlock detection ต้องใช้ global wait-for graph แทนที่จะพึ่ง local wait-for graph ของแต่ละเครื่องอย่างเดียว
- เปรียบเทียบ 2PC กับ Saga Pattern ในแง่ consistency, availability และวิธีจัดการเมื่อขั้นตอนหนึ่งล้มเหลว พร้อมอธิบายว่าทำไม microservices จึงเลือกใช้ Saga เป็นหลัก
6จาก Transaction เดียวไปสู่หลาย Participant
ในบทก่อนหน้า database หนึ่งตัวควบคุม lock, log และ commit ได้เอง เมื่อ transaction ข้ามหลายเครื่อง ไม่มีใครแก้ state ของทุก participant ได้ในคำสั่งเดียว แต่ละฝ่ายเห็นเพียง local state และข้อความที่ได้รับ จึงเกิดคำถามใหม่ว่า ทุกฝ่ายจะตัดสินผลเดียวกันได้อย่างไร หากบางฝ่ายพร้อม บางฝ่ายล้ม และ coordinator หายไปกลางทาง
| คำศัพท์ | หน้าที่ | สิ่งที่ต้องเก็บถาวร |
|---|---|---|
| Coordinator | เริ่ม protocol รวบรวม vote และประกาศ decision | transaction ID, participant, global decision |
| Participant/Cohort | ทำ local work, prepare และทำตาม decision | prepared state, locks/resources, final outcome |
| Transaction manager | จัด lifecycle ของ global transaction | log ที่ใช้ recovery |
| Resource manager | จัด database/queue/resource ใน local transaction | undo/redo information |
Atomicity ข้ามเครื่องไม่ได้เกิดจากการเรียก API ต่อกัน
การเขียนโค้ด debit(); credit(); ทำให้ลำดับดูชัด แต่ถ้า process crash ระหว่างสอง call เงินถูกหักโดยยังไม่เพิ่มปลายทาง การ catch exception แล้วเรียก refund ช่วยบางกรณี แต่ refund call ก็ล้มได้ และ caller อาจไม่รู้ debit สำเร็จหรือไม่เพราะ response หาย
Distributed transaction จึงต้องนิยาม state machine และ recovery behavior ไม่ใช่เพียง happy path ทุก transition ที่มีผลต่อ decision ต้องบันทึกลง stable storage ก่อนส่งข้อความบางชนิด เพื่อให้ process หลัง restart รู้ว่าตนสัญญาอะไรไว้แล้ว
7Two-Phase Commit แบบอ่านจาก State Machine
2PC แบ่งช่วงเป็น vote/prepare และ decision/commit การแบ่งสองช่วงไม่ใช่พิธีกรรม แต่แก้ความต่างระหว่าง “ฉันทำ local transaction ได้” กับ “ทุกคนต้องทำผลเดียวกัน” Participant ที่ vote YES ต้องเตรียม local state และสัญญาว่าจะ commit ได้ภายหลัง แม้ restart แล้วก็ตาม
Coordinator Participants
|--- PREPARE(tx) ------------------>| local validate + force PREPARED log
|<-- YES / NO ----------------------|
| decide COMMIT iff every vote YES
| force GLOBAL-COMMIT log
|--- COMMIT(tx) ------------------->| local commit + release locks
|<-- ACK ---------------------------|
| จุด | สิ่งที่ต้องเป็นจริงก่อนเดินต่อ | เหตุผล |
|---|---|---|
| Participant vote YES | local work valid และ prepared state durable | ห้ามกลับใจหลัง coordinator ตัดสิน commit |
| Coordinator ประกาศ COMMIT | ได้รับ YES ครบและ decision ลง log | restart แล้วต้องประกาศผลเดิม |
| Participant รับ decision | ทำ local outcome ตาม decision และ log | ปล่อย lock/resource อย่างปลอดภัย |
| Coordinator ลบ state | ได้รับ ACK ตามนโยบาย | มั่นใจว่าทุกฝ่ายเรียนรู้ผลแล้ว |
จุดที่ไม่มีทางกลับของ Participant
ก่อน vote YES participant ยัง abort ได้เอง แต่หลัง YES มันอยู่ใน uncertain/prepared state และห้ามตัดสินใจเอง หาก abort ขณะที่ global decision เป็น commit จะละเมิด atomicity ข้อแลกเปลี่ยนคือมันอาจต้องถือ lock และ resource รอ coordinator นานมาก
นี่คือหัวใจของ blocking 2PC ไม่ใช่เพราะ implementation ขาด timeout การใส่ timeout แล้ว participant abort เองหลัง vote YES ทำให้ไม่ block แต่ทำลาย atomic guarantee Timeout บอกว่า decision ยังไม่มาถึง ไม่ได้ให้ข้อมูลว่า decision เป็นอะไร
8Failure Matrix: ล้มตรงไหนแล้วใครรู้อะไร
| Failure | State ที่อาจค้าง | การกู้คืน | ความกำกวม |
|---|---|---|---|
| Participant ล้มก่อน vote | coordinator ยังไม่มี YES | timeout แล้ว global abort | ไม่มี commitment จาก participant |
| Participant ล้มหลัง YES | prepared durable | restart แล้วถาม coordinator/participant อื่น | ห้ามตัดสินเองจนรู้ decision |
| Coordinator ล้มก่อน decision | บาง participant prepared | อ่าน log; ถ้าไม่มี decision อาจ abort ตาม protocol | prepared participant อาจ block |
| Coordinator ล้มหลัง log COMMIT ก่อนส่ง | global decision คือ commit | restart แล้ว resend COMMIT | participant บางตัวรู้ บางตัวไม่รู้ |
| COMMIT/ACK หาย | ผลอาจเสร็จแล้ว | ส่ง decision ซ้ำ; operation ต้อง idempotent | coordinator ไม่รู้ว่า participant เรียนรู้ผลหรือยัง |
ตารางนี้แสดงหลักคิดสำคัญ: recovery decision มาจาก durable log และ state machine ไม่ได้มาจากการเดาจากเวลาที่ผ่านไป การส่ง COMMIT ซ้ำต้องปลอดภัย Participant ที่ commit แล้วรับ COMMIT เดิมควรตอบ outcome เดิม ไม่ commit business effect ซ้ำ
presumed abort และ presumed commit
Optimization บางแบบลด log/message โดยกำหนด default เมื่อไม่มี record Presumed abort ถือว่า transaction ที่ไม่มี commit evidence เป็น abort เหมาะเมื่อ abort บ่อยหรือต้องการลด abort logging Presumed commit ทำตรงข้ามและต้องจัด metadata ต่างกัน จุดสำคัญคือ “ไม่มีข้อมูล” มีความหมายได้ต่อเมื่อ protocol กำหนดไว้ ไม่ใช่สรุปเอง
Heuristic Decision
ระบบบางชนิดยอมให้ operator/participant ตัดสิน commit หรือ rollback เองเมื่อ block นานมาก เรียกว่า heuristic decision ซึ่งคืน availability แต่เสี่ยง global inconsistency จึงต้องบันทึก audit, แจ้งเตือน และมีกระบวนการ reconcile ไม่ควรซ่อนว่าเป็น success ปกติ
9Nested Transaction และ Open Nesting
Nested transaction แบ่งงานใหญ่เป็น subtransaction โครงต้นไม้ ลูกอาจ commit provisional ต่อ parent แต่ผลสุดท้ายยังขึ้นกับ root ถ้า parent abort ผลลูกต้องถูกยกเลิก โมเดลนี้ช่วย parallelism และแยก failure แต่ไม่ได้หมายความว่าลูกที่ commit แล้วเปิดเผยต่อโลกเสมอ
| แบบ | การเห็นผลของลูก | เมื่อ parent abort |
|---|---|---|
| Closed nesting | ผลลูกเห็นภายใน parent ยังไม่เปิดสู่ภายนอก | rollback ตาม atomic transaction |
| Open nesting | ลูก commit และเปิดผลก่อน root จบ | ใช้ compensating action แทน physical rollback |
| Flat transaction | มีขอบเขตเดียว | rollback ทั้งหมด |
Open nesting ใกล้โลกธุรกิจมากกว่า เช่นจองบริการภายนอกแล้วต้องยกเลิกภายหลัง แต่ compensation ไม่ใช่ inverse ที่สมบูรณ์เสมอ การส่งอีเมลย้อนกลับไม่ได้ เราทำได้เพียงส่งอีเมลแก้ไข การคืนเงินอาจใช้เวลาและมีค่าธรรมเนียม ดังนั้น state หลัง compensation อาจ business-equivalent แต่ไม่เหมือน state ก่อนเริ่มทุก byte
102PC ไม่เท่ากับ Consensus
2PC ทำ atomic commitment โดยมี coordinator และ participant vote ถ้า coordinator หายหลังทุกคน prepare ระบบอาจ block ส่วน consensus ให้สมาชิกตกลงค่าเดียวกันภายใต้ failure model และมักเดินหน้าต่อได้เมื่อมี quorum แม้สมาชิกบางส่วนล้ม ทั้งสองมีขั้นตอนส่งข้อความคล้ายกันบางส่วน แต่ตอบโจทย์คนละอย่าง
| มิติ | 2PC | Consensus/Raft-style |
|---|---|---|
| เป้าหมาย | ทุก participant commit/abort transaction เดียวกัน | replica ตกลงลำดับ/value เดียวกัน |
| การตัดสิน commit | ต้อง YES จาก participant ทุกคน | โดยทั่วไป quorum |
| Coordinator/leader ล้ม | prepared participant อาจ block | เลือก leader ใหม่เมื่อ quorum ติดต่อได้ |
| การแทนกัน | participant แต่ละคนถือ resource คนละส่วน | replica มักเก็บ state machine ซ้ำกัน |
ฐานข้อมูลสมัยใหม่อาจใช้ consensus ภายในแต่ละ shard เพื่อ replicate log แล้วใช้ 2PC ข้าม shard ดังนั้น “ใช้ Raft แล้วไม่ต้องมี distributed transaction” ไม่จริงโดยอัตโนมัติ Consensus ช่วยให้ coordinator/participant state ทน fault แต่ cross-resource atomicity ยังต้อง protocol เพิ่ม
11ข้อจำกัดเชิงปฏิบัติของ 2PC
นอกจาก blocking แล้ว 2PC เพิ่มรอบ network, force log, lock duration และ tail latency Participant ที่ช้าที่สุดกำหนดเวลาทั้ง transaction และ failure probability สะสมตามจำนวน participant จึงไม่เหมาะกับ workflow ข้าม service จำนวนมากหรือ external service ที่ไม่รองรับ prepare
แต่ไม่ควรสรุปว่า 2PC “ล้าสมัย” ใน database cluster ที่ควบคุม participant, transaction สั้น และต้อง strong atomicity มันยังเหมาะสม สิ่งที่ไม่เหมาะคือยืด 2PC ข้าม business service และคนละองค์กรโดยไม่รับต้นทุน availability/coupling
12Saga: เปลี่ยน Atomicity เป็นกระบวนการที่มองเห็นได้
Saga แบ่ง long-lived transaction เป็น local transaction หลายขั้น แต่ละขั้น commit จริงและอาจเผยผล จากนั้นเรียกขั้นถัดไป หากภายหลังล้ม ใช้ compensating transaction ย้อนผลทางธุรกิจเท่าที่ทำได้ Saga จึงไม่ให้ isolation/atomicity แบบ ACID ทั้งก้อน แต่ให้วิธีควบคุม workflow ที่มี intermediate state
| ขั้น | Forward action | Compensation | สิ่งที่ย้อนสมบูรณ์ไม่ได้ |
|---|---|---|---|
| 1 | สร้าง order PENDING | เปลี่ยนเป็น CANCELLED | เลข order/audit ยังคงอยู่ |
| 2 | กันวงเงิน | release/refund | เวลาคืนและค่าธรรมเนียม |
| 3 | จอง stock | คืน stock | ลูกค้าอื่นอาจเห็น stock ลดชั่วคราว |
| 4 | ส่ง notification | ส่งข้อความแก้ไข | ผู้ใช้เห็นข้อความแรกแล้ว |
Backward recovery กับ Forward recovery
Backward recovery ชดเชยกลับเมื่อขั้นล้ม Forward recovery retry/ซ่อมให้ workflow เดินต่อจนสำเร็จ งานบางชนิดควร forward เช่นบันทึกบัญชีที่ commit แล้วแต่ notification ล้ม ส่วนบางชนิดควร compensate เช่นกัน stock แล้ว payment ปฏิเสธ ต้องกำหนด pivot transaction ซึ่งหลังผ่านแล้วเลือกเดินหน้ามากกว่าย้อน
Choreography
แต่ละ service ฟัง event แล้วทำ local action/ปล่อย event ถัดไป ข้อดีคือลด coordinator กลางและเพิ่ม autonomy แต่ workflow กระจายอยู่หลายที่ ตามลำดับยาก เกิดวง event และเปลี่ยน process ยากเมื่อขั้นเพิ่ม ควรมี event contract, correlation ID และแผนภาพ workflow ที่เป็นเจ้าของร่วม
Orchestration
Orchestrator เก็บ state machine ส่ง command และรับผล ทำให้เห็น flow, timeout และ compensation ชัด แต่ orchestrator ต้องทน fault และไม่ควรกลายเป็นที่เก็บ business logic ทุก domain มันประสาน “ใครทำต่อ” ส่วน service ยังตัดสินกฎภายในตน
| คำถาม | Choreography | Orchestration |
|---|---|---|
| เห็นสถานะ workflow | ต้องประกอบจาก event หลายแหล่ง | มี state กลางชัด |
| coupling | ผูกผ่าน event/schema และ implicit flow | ผูก command กับ orchestrator |
| เปลี่ยนลำดับ | กระทบหลาย consumer | เปลี่ยน state machine ง่ายกว่า |
| failure ของตัวควบคุม | ไม่มีตัวเดียว แต่เกิด emergent behavior | ต้อง replicate/recover orchestrator |
13Dual Write Problem และ Transactional Outbox
Service ต้องเขียน database แล้ว publish event ถ้าทำสองคำสั่งแยกกัน ย่อมมีช่อง crash: เขียน DB สำเร็จแต่ publish ไม่สำเร็จ ทำให้โลกภายนอกไม่รู้ หรือ publish ก่อนแล้ว DB rollback ทำให้ event บอกสิ่งที่ไม่เกิดจริง นี่คือ dual write problem
Outbox เขียน business row และ outbox event ใน local transaction เดียว จากนั้น relay อ่าน outbox ไปส่ง broker เพราะสอง record commit/abort พร้อมกัน เราจึงไม่เสีย event ระหว่าง local commit แต่ relay อาจส่งซ้ำหาก crash หลัง publish ก่อน mark sent Consumer ยังต้อง idempotent
BEGIN; INSERT INTO orders(...); INSERT INTO outbox(event_id, type, payload, status) VALUES (...); COMMIT; Relay: read unsent → publish → mark sent # publish อาจเกิดซ้ำ
CDC เป็นอีกเส้นทาง
Change Data Capture อ่าน database log แล้วแปลง change เป็น stream ลด polling และอาจรักษาลำดับตาม log แต่ raw table change ไม่เท่ากับ domain event และ schema ภายในรั่วได้ Outbox + CDC ใช้ร่วมกันได้ โดย application เขียน domain event ลง outbox และ CDC ส่งออก
14Idempotency, Inbox และการรับ Event ซ้ำ
At-least-once delivery เป็นทางเลือกทั่วไปเพราะยอมส่งซ้ำดีกว่าทำหาย Consumer ใช้ Inbox/Dedup table เก็บ event ID ใน transaction เดียวกับ side effect ถ้า ID เคยถูกใช้ คืนผลเดิม/ack โดยไม่ทำซ้ำ ถ้าเก็บ ID หลัง side effect แยกคำสั่ง ยังมีช่อง crash
| ขอบเขต | คำถาม |
|---|---|
| Key scope | ID ไม่ซ้ำทั้งระบบ ต่อ producer หรือ operation |
| Retention | ต้องจำ duplicate นานเท่า retry/replay window ใด |
| Atomicity | dedupe record กับ business effect commit พร้อมกันหรือไม่ |
| Result | duplicate ต้องได้ผลเดิมหรือเพียงไม่ทำซ้ำ |
| Concurrency | สอง delivery ID เดียวกันเข้าพร้อมกันใช้ unique constraint หรือไม่ |
15Isolation ใน Saga และสิ่งที่ผู้ใช้อาจเห็น
เพราะ local transaction commit ทีละขั้น service อื่นอาจเห็น intermediate state เช่น order PENDING, เงิน HELD และ stock RESERVED พร้อมกัน ปัญหาได้แก่ dirty read เชิงธุรกิจ, lost update และ semantic lock จึงใช้ state machine, reservation, version และ policy ว่า operation ใดทำได้ในแต่ละ state
ตัวอย่างเช่นห้ามแก้รายการสินค้าหลัง payment hold หรือใช้ countermeasure แบบ reread ก่อน commit Compensation ต้องตรวจ version ไม่ย้อน state ที่ถูก workflow ใหม่เปลี่ยนไปแล้ว การชดเชยแบบ “ตั้ง stock กลับค่าเดิม” อาจทับ order อื่น ควรใช้ operation เพิ่ม/ลดเชิงสัมพันธ์พร้อม identity
16เลือก 2PC, Saga หรือออกแบบขอบเขตใหม่
| สถานการณ์ | แนวทางที่ควรพิจารณา | เหตุผล |
|---|---|---|
| หลาย table ใน DB เดียว | Local ACID transaction | ง่ายและ guarantee แข็ง ไม่ควรรีบกระจาย |
| หลาย shard/resource manager ที่ควบคุมได้ | 2PC/DB distributed transaction | ต้อง atomicity และ transaction สั้น |
| business workflow ยาว ข้าม service | Saga + explicit state | ไม่ถือ lock ยาว ยอม intermediate state |
| เขียน DB + publish event | Outbox/CDC | แก้ dual write ด้วย local atomicity |
| invariant ถูกแยกข้าม serviceบ่อย | ทบทวน service boundary | อาจแบ่ง ownership ผิด ไม่ควรแก้ทุกอย่างด้วย protocol |
การออกแบบที่ดีที่สุดบางครั้งไม่ใช่เลือก protocol เก่งขึ้น แต่ย้ายข้อมูลที่ต้อง atomic มาอยู่ ownership เดียวกัน ถ้าสอง service ต้อง lock/commit พร้อมกันแทบทุก operation ความเป็นอิสระอาจเป็นเพียงภาพบนสไลด์
17Workshop: ทำ Failure ให้ครบทุกช่อง
ส่วน A — 2PC State Machine
- จำลอง coordinator กับ participant สองตัว แต่ละตัวมี durable log
- หยุด coordinator ก่อน decision, หลัง log decision และระหว่างส่ง decision
- restart แล้วพิสูจน์ว่าประกาศ outcome เดิม
- ทำ participant crash หลัง YES และแสดงว่ามันตัดสินเองไม่ได้
- วัดเวลาถือ lockและอธิบาย blocking ไม่แก้ด้วย timeout อย่างไร
ส่วน B — Saga Order
- ออกแบบ state machine และ compensation ทุกขั้น
- ทำ failure หลัง local commit แต่ก่อน event publish แล้วเพิ่ม Outbox
- ทำ relay ส่งซ้ำและเพิ่ม Inbox/idempotent consumer
- ส่ง event ผิดลำดับและกำหนด version/state guard
- สร้างหน้าตรวจ workflow ที่ค้างและ manual resolution
18ทบทวนแกนสำคัญก่อนกรณีศึกษา
Distributed transaction ยากเพราะ local commit หลายตัวต้องกลายเป็น outcome เดียวทั้งที่สมาชิกและเครือข่ายล้มแยกกันได้ 2PC รักษา atomic commitment ด้วย prepared promise และ durable log แต่จ่ายด้วย blocking, รอบสื่อสาร และการถือ resource นาน
Saga ไม่ใช่ 2PC รุ่นเร็วกว่า มันเปลี่ยน guarantee: local step commit และเปิดเผย intermediate state แล้วใช้ compensation/forward recovery ทำให้ workflow ไปถึงสถานะปลายทาง Outbox แก้ช่อง dual write แต่ยังส่งซ้ำได้ จึงต้องจับคู่กับ idempotent consumer และ observability
ขั้นตอนถัดไปคือ Name Services เพราะระบบที่มี service, participant และ replica เปลี่ยนตำแหน่งตลอดต้องแยก “สิ่งนี้คือใคร” ออกจาก “ตอนนี้ติดต่อที่ไหน” Naming ทำให้ location เปลี่ยนได้โดย reference เชิงตรรกะยังอยู่ แต่ cache, TTL และ stale mapping จะสร้าง distributed consistency problem ชุดใหม่
19กรณีศึกษา: การโอนเงินที่คำตอบหาย
Client ขอให้ Transfer Service โอน 1,000 บาทจาก A ไป B Service สร้าง transaction ID T100 แล้วเรียก Ledger-A และ Ledger-B สมมุติว่า debit และ credit อยู่คนละ participant ทั้งคู่ vote YES Coordinator บันทึก COMMIT แต่ crash ก่อน B ได้ decision Client เห็น timeout และไม่รู้ผล
จุดนี้มีความจริงหลายระดับ Coordinator หลัง restart อ่าน log แล้วรู้ว่า global decision คือ COMMIT; A อาจ commit แล้ว; B prepared และรอ; Client ไม่รู้อะไรเกิน timeout ถ้า client สร้าง transaction ID ใหม่แล้ว retry ระบบอาจโอนอีกครั้ง แม้ 2PC ชุดแรกทำงานถูกต้อง จึงต้องมี idempotency ที่ boundary ของ business request นอกเหนือจาก atomicity ภายใน 2PC
| ฝ่าย | State หลัง crash | การกระทำที่ถูก | สิ่งที่ห้ามเดา |
|---|---|---|---|
| Client | deadline exceeded | query ด้วย T100 หรือ retry ID เดิม | timeout = abort |
| Coordinator | COMMIT durable | resend decision จน ACK | สร้าง decision ใหม่ |
| A | committed | ตอบ outcome เดิมเมื่อรับซ้ำ | debit ซ้ำ |
| B | prepared | รอ/ถาม decision แล้ว commit | abort เองเพราะรอนาน |
ถ้า Coordinator ไม่มี decision log และ crash ก่อนตัดสิน มันอาจเลือก abort ตาม presumed-abort rule แต่ถ้า log COMMIT แล้ว outcome เปลี่ยนไม่ได้ Durable log จึงไม่ได้มีไว้แค่ audit แต่เป็น memory ที่ทำให้ process incarnation ใหม่รักษาคำสัญญาของ incarnation เก่า
คำถามที่ควรใช้ตรวจการออกแบบ
- API ให้ผู้ใช้สอบถาม outcome ด้วย transaction ID ได้หรือไม่
- idempotency record กับการเริ่ม global transaction ผูกกันอย่างไร
- ถ้า participant prepared นาน มี alert และ manual resolution อย่างไร
- log เก็บนานเท่า retry/recovery window และลบเมื่อใด
- operator เห็นคำว่า UNKNOWN แยกจาก FAILED หรือไม่
20กรณีศึกษา: Order Saga ที่ย้อนกลับไม่หมด
Workflow รับ order ประกอบด้วย CreateOrder, AuthorizePayment, ReserveInventory, ArrangeDelivery และ NotifyCustomer ถ้า ArrangeDelivery ล้มหลังร้านเริ่มเตรียมสินค้า การคืน stock กับ release payment อาจทำได้ แต่เวลาของร้านและข้อความที่ส่งไปแล้วหายไม่ได้ Compensation จึงต้องสะท้อนนโยบายธุรกิจ เช่นคืนเงินพร้อมเครดิต ไม่ใช่แกล้งทำว่าเหตุการณ์ไม่เคยเกิด
| State | เหตุการณ์ที่พาเข้า | สิ่งที่อนุญาต | ทางออกเมื่อหมดเวลา |
|---|---|---|---|
| PENDING_PAYMENT | OrderCreated | authorize/cancel | cancel draft |
| PAYMENT_HELD | PaymentAuthorized | reserve/release | release hold |
| STOCK_RESERVED | InventoryReserved | arrange delivery | release + refund policy |
| CONFIRMED | DeliveryArranged | fulfil/cancel policy | compensation อาจมีค่าปรับ |
| MANUAL_REVIEW | compensation failed | operator action | ห้าม retry แบบไร้เพดาน |
State machine มีความสำคัญเพราะ event อาจมาช้าและซ้ำ เช่น PaymentAuthorized มาถึงหลัง order ถูก cancel Consumer ต้องตรวจ current state/version ไม่ใช่ทำตาม event ทุกฉบับแบบไร้บริบท หาก authorization เกิดจริงหลัง cancel อาจต้องส่ง ReleasePayment ใหม่แทนการเพิกเฉย
Poison Workflow
Workflow บางรายการไม่สามารถเดินหน้าหรือชดเชยได้เพราะข้อมูลผิด/บริการภายนอกปฏิเสธถาวร การ retry ต่อเนื่องสร้าง noise และค่าใช้จ่าย ต้องมี retry class, max attempt, dead-letter/work queue สำหรับคน และข้อมูลที่ช่วยตัดสินใจ การมี manual state ไม่ใช่ความพ่ายแพ้ แต่เป็นการยอมรับว่าธุรกิจบางกรณีไม่มี inverse อัตโนมัติที่ปลอดภัย
21ความสัมพันธ์ระหว่าง Transaction, Messaging และ Audit
ฐานข้อมูลตอบ state ปัจจุบัน Event log ตอบว่าเดินมาถึง state นี้อย่างไร Audit log ตอบว่าใครทำอะไรภายใต้สิทธิ์ใด ทั้งสามอาจเชื่อมกันแต่ไม่ควรใช้แทนกันโดยไม่กำหนด semantics Outbox event ที่สร้างเพื่อ integration อาจไม่มีรายละเอียดพอเป็น regulatory audit และ application log อาจถูกลบ/แก้ได้
| ข้อมูล | วัตถุประสงค์ | ข้อกำหนด |
|---|---|---|
| Business state | ให้บริการปัจจุบัน | invariant, query, update concurrency |
| Outbox/integration event | แจ้ง service อื่น | schema, delivery, idempotency |
| Workflow history | recover state machine | deterministic transition, retention |
| Audit record | ตรวจสอบผู้กระทำ/เหตุผล | tamper evidence, identity, policy |
Event Sourcing กับ Saga
Event-sourced aggregate บันทึก event เป็น source of truth แล้ว fold เป็น state ช่วย audit/rebuild และ correlation กับ Saga แต่ไม่ได้แก้ cross-aggregate transaction ฟรี Event append ต้อง concurrency control, projection lag และ schema evolution Saga ยังต้องประสาน local transaction หลาย aggregate/service และ event อาจส่งซ้ำ
22แบบฝึกวิเคราะห์ Failure ทีละบรรทัด
สำหรับแต่ละเหตุการณ์ต่อไปนี้ ให้ตอบห้าข้อ: local state คืออะไร, durable record ใดมีอยู่, message ใดอาจอยู่ระหว่างทาง, operation ใดทำซ้ำได้ และ safety/liveness ใดเสี่ยง
| เหตุการณ์ | ประเด็นที่ต้องค้นพบ |
|---|---|
| Coordinator ส่ง PREPARE ให้ P1 แต่ยังไม่ถึง P2 แล้ว crash | P1 อาจ vote/prepare ส่วน P2 ไม่รู้ transaction; ไม่มี global commit |
| P1 vote YES แต่ vote reply หาย | Coordinator timeout abort ได้ แต่ P1 ต้องเรียนรู้ ABORT เพื่อปล่อย lock |
| ทุกคน YES, coordinator log COMMIT แล้ว disk หายถาวร | single durable coordinator เป็น failure domain; ต้อง backup/replicate log |
| Saga publish PaymentAuthorized ซ้ำ | consumer dedupe และ state guard; event delivery ≠ business effect |
| Compensation Refund timeout | ไม่รู้ refund เกิด; query/retry ID เดิม ไม่ออก refund ใหม่ |
| Outbox relay publish แล้ว crash | row ยัง unsent จึง publish ซ้ำ; consumer inbox จำเป็น |
| Event รุ่นใหม่ถึง consumer เก่า | compatibility/unknown field; retry ไม่แก้ schema ถาวร |
| Operator สั่ง heuristic commit ฝ่ายหนึ่ง | ต้อง reconcile global state และบันทึก hazard |
คำถามเชิงออกแบบเพิ่มเติม
- ทำไม 2PC ต้องบังคับ participant เก็บ PREPARED ก่อน vote YES
- เหตุใด timeout หลัง YES ไม่อนุญาตให้ participant abort เอง
- อะไรทำให้ Saga มี availability สูงกว่า และ guarantee ใดที่เสียไป
- compensation ต่างจาก database rollback อย่างไรเมื่อคนภายนอกเห็นผลแล้ว
- Outbox แก้ event loss แต่เหตุใดไม่ให้ exactly-once consumer effect อัตโนมัติ
- เมื่อไรปัญหา distributed transaction เป็นสัญญาณว่า service boundary ผิด
- ถ้าค่าใช้จ่าย duplicate สูงมาก จะวาง idempotency ที่ boundary ใดบ้าง
- จะพิสูจน์ว่า workflow ที่ค้างถูกค้นพบ “ในที่สุด” ด้วย metric/scan ใด
23Checklist ก่อนนำ Workflow ขึ้นใช้งานจริง
- มี global/business ID ที่คงเดิมเมื่อ client retry
- ทุก command ระบุว่า idempotent หรือมี dedupe strategy
- local state change กับ outbox/inbox record atomic
- ทุก state มี timeout/next action และไม่มี state ที่ค้างโดยไม่มี owner
- compensation เอง retry ได้และมี idempotency
- แยก transient, permanent และ ambiguous failure
- event มี schema compatibility และ version policy
- trace/correlation ID ผ่านทุก command/event
- มี dashboard ของ age, retry, DLQ, compensation failure และ manual review
- ซ้อม crash หลังทุก durable write และก่อน/หลังทุก message send
- กำหนด retention ของ transaction log, dedupe และ audit จาก recovery window
- มี reconciliation job เปรียบเทียบ invariant ข้าม service โดยไม่ถือว่าไม่ผิดเพราะไม่มี alert
24คำศัพท์ที่ควรอธิบายเป็นประโยค ไม่ใช่จำเพียงชื่อ
| คำ | คำอธิบายที่ใช้วิเคราะห์ระบบได้ |
|---|---|
| Atomic commitment | การทำให้ participant ทุกคนได้ outcome commit/abort เดียวกัน ไม่ใช่การทำให้ทุกขั้นเกิดพร้อมกันทางกายภาพ |
| Prepared | participant ทำ local work และบันทึกคำสัญญาถาวรว่าจะ commit ได้หาก global decision สั่ง จึงถอนตัวเองไม่ได้ |
| Blocking | สถานะที่ participant ไม่มีข้อมูลพอตัดสินโดยไม่เสี่ยงละเมิด atomicity ไม่ใช่เพียง thread รอนานจากโค้ดไม่ดี |
| Compensation | ธุรกรรมใหม่ที่ลด/แก้ผลทางธุรกิจของสิ่งที่ commit แล้ว ไม่ใช่ physical undo ที่ทำให้ประวัติหาย |
| Pivot transaction | จุดใน Saga ที่หลังสำเร็จแล้ว workflow มักต้องเดินหน้าให้จบ มากกว่าย้อนทุกขั้นกลับ |
| Semantic lock | สถานะ/เครื่องหมายระดับธุรกิจที่จำกัด operation ระหว่าง Saga เช่น RESERVED ไม่ใช่ DB lock ที่ถือ connection ยาว |
| Reconciliation | การตรวจและซ่อม divergence จากหลักฐานหลายระบบภายหลัง เป็น safety net ไม่ควรแทน protocol หลักทั้งหมด |
| In-doubt | รู้ว่ามี transaction แต่ยังไม่รู้ final decision ต้องรักษาข้อมูลและห้ามรายงาน FAILED แบบมั่นใจเกินจริง |
ประโยคที่ควรหลีกเลี่ยง
- “ใช้ Saga จึงเป็น transaction เหมือนเดิม” — Saga เปลี่ยน isolation/atomic visibility
- “ส่งผ่าน Kafka จึง exactly once” — ต้องถาม effect ภายนอกและขอบเขต guarantee
- “timeout จึง rollback” — timeout เป็น observation ฝั่งผู้รอ ไม่ใช่ global outcome
- “มี compensation จึงย้อนทุกอย่างได้” — external effect และเวลาย้อนกลับไม่ได้
- “2PC ช้า จึงไม่ควรใช้” — ต้องดูขอบเขต, duration, participant control และ required invariant
แบบฝึกตัดสินใจสั้น ๆ
ระบบมหาวิทยาลัยต้องลงทะเบียนรายวิชา ตัดที่นั่ง และบันทึกค่าธรรมเนียม หากทั้งหมดอยู่ฐานเดียว ควรเริ่ม local transaction หากแยก service ให้เขียน invariant ก่อนว่า “ได้ที่นั่งแต่ยังไม่จ่าย” ยอมเป็นสถานะชั่วคราวได้หรือไม่ ถ้าได้ ออกแบบ reservation expiry/Saga ถ้าไม่ได้และ participant ควบคุมร่วม อาจพิจารณา distributed transaction หรือรวม ownership ใหม่
การเลือกคำตอบต้องบอก failure behavior ด้วย: นักศึกษาเห็น PENDING นานเท่าใด ที่นั่งคืนเมื่อใด payment response หายสอบถามอย่างไร และ reconciliation ตรวจที่นั่งกับใบเสร็จแบบใด คำว่าใช้ microservices หรือ queue ยังไม่ตอบคำถามเหล่านี้
25Design Review: คำถามที่เปิดช่องโหว่ได้เร็วกว่าดูแผนภาพ
เวลา review ระบบ อย่าเริ่มจากถามว่าใช้ Kafka หรือ workflow engine อะไร ให้เดินตามหนึ่ง business ID แล้วหยุดหลังทุก state change: ถ้า process ตายตรงนี้ state ใด commit แล้ว ข้อความใดอาจส่งแล้ว ใครจะตรวจพบ และการทำซ้ำสร้างผลอะไร วิธีนี้เรียบง่ายแต่เปิดช่อง dual write, missing timeout และ compensation ที่ไม่ idempotent ได้เร็วมาก
| จุดตรวจ | คำถามบังคับ |
|---|---|
| ก่อน local commit | abort ได้ครบหรือมี external effect เกิดแล้ว |
| หลัง commit ก่อน publish | Outbox/CDC รับประกัน event ไม่หายหรือไม่ |
| หลัง publish ก่อน mark | ส่งซ้ำแล้ว consumer ปลอดภัยหรือไม่ |
| หลัง consumer effect ก่อน ACK | Inbox/effect atomic หรือ duplicate เกิด |
| ระหว่าง compensation | forward workflow ใหม่เปลี่ยน state ไปแล้วหรือไม่ |
| เมื่อหมด deadline | รู้ outcome หรือเพียงหยุดรอ และมี query statusไหม |
ให้ตรวจ cardinality ด้วย Request หนึ่งอาจ fan-out หลาย participant แต่ retry ซ้อนชั้นทำให้จำนวน call โตแบบคูณ ควรมี retry budget, deadline และ metric attempts ต่อ business operation ไม่ใช่นับ HTTP request อย่างเดียว มิฉะนั้น dashboard อาจบอก throughputสูงขึ้นในวันที่ระบบเพียงทำงานเดิมซ้ำมากขึ้น
สุดท้ายให้ถามว่า invariant ใดถูกตรวจภายใน ownership เดียวและ invariant ใดอาศัย reconciliation หากคำตอบคือทุกอย่างตรวจภายหลัง ระบบอาจเปิด inconsistency มากเกินไป แต่ถ้าทุกอย่างต้อง global lock ระบบอาจไม่มี availability พอ การออกแบบคือการเลือกเส้นแบ่ง ไม่ใช่หลบการเลือกด้วยศัพท์ pattern
เกณฑ์ว่าระบบพร้อมหรือยัง
ระบบไม่ควรถูกเรียกว่าพร้อมเพียงเพราะ happy path จบครบ ต้องผ่านการทดสอบ kill หลังทุก commit/send, replay message, restart ด้วย log เดิม และตรวจ invariant หลังงานจำนวนมาก มี alert สำหรับ prepared/in-doubt ที่อายุเกินเกณฑ์ Saga ที่ค้าง compensation ที่ล้ม และ outbox/inbox ที่โตผิดปกติ
การทดสอบควรสุ่ม delay, duplicate, reorder และ dependency unavailable โดยรักษา business ID ให้ตามเรื่องได้ หลังจบให้ reconciliation เปรียบเทียบยอด/จำนวนข้าม service หากผลรวมถูกแต่มีรายการค้าง manual จำนวนมาก ระบบยังไม่พร้อมด้านปฏิบัติการ เพราะ correctness ที่ต้องพึ่งคนแก้ทุกคืนเป็น correctness ที่มีต้นทุนจริง
สุดท้ายต้องมีภาษาสถานะสำหรับผู้ใช้ ไม่รายงาน FAILED เมื่อ outcome ยัง UNKNOWN และไม่รายงาน SUCCESS หากยังมีขั้นสำคัญ pending หน้าจอที่ยอมแสดง “กำลังตรวจสอบผล” อย่างซื่อสัตย์มักปลอดภัยกว่าการบังคับทุกความไม่แน่นอนให้กลายเป็นสำเร็จหรือล้มเหลวทันที
อีกด้านหนึ่ง การเก็บ metric ต้องนับตาม business operation ไม่ใช่ message อย่างเดียว เช่นหนึ่งการชำระเงินมี delivery ห้าครั้งแต่ effect หนึ่งครั้ง ระบบ messaging อาจรายงาน throughput ห้ารายการ ขณะที่ธุรกิจมีงานเดียว หากไม่แยก attempt, delivery และ effect เราจะประเมิน capacity กับ error rate ผิด รวมถึงมอง retry storm เป็นปริมาณงานที่เติบโต ทั้งที่เป็นงานเดิมหมุนซ้ำ
สำหรับงานที่มีข้อกำกับ ควรเชื่อม transaction ID, user identity, policy version และเหตุผล manual override ไว้ใน audit ที่แก้ไขย้อนหลังไม่ได้ง่าย แต่ต้องระวังไม่เก็บ secret หรือข้อมูลส่วนตัวเกินจำเป็น การ trace ได้ไม่ได้แปลว่าต้องสำเนา payload ทุกชิ้นตลอดกาล Retention และการปกปิดข้อมูลเป็นส่วนหนึ่งของ design เช่นเดียวกับ commit protocol
คำถามสุดท้ายในการ review คือ “ถ้าทีมเจ้าของ service นี้หายไปหนึ่งวัน ระบบยังอธิบายและฟื้นงานค้างได้หรือไม่” ถ้าคำตอบพึ่งคนจำลำดับ call จากประสบการณ์ แสดงว่า state machine, observability หรือ runbook ยังไม่ครบ ระบบกระจายที่ดีต้องฝากความจำไว้ใน durable state และหลักฐาน ไม่ฝากไว้ในความจำของคนเวรเพียงคนเดียว
26สรุปและขั้นตอนถัดไป
บทนี้เริ่มจากความต้องการที่ดูเรียบง่ายว่า participant หลายตัวควร commit หรือ abort ให้ตรงกัน แล้วพบว่าความยากอยู่ที่แต่ละฝ่ายรู้ decision ไม่พร้อมกัน 2PC ใช้ prepared promise และ durable log รักษา atomic outcome แต่หลัง vote YES participant อาจต้อง block เพราะ timeout ไม่เปิดเผย decision
Saga เลือกอีกขอบเขตหนึ่ง: ให้ local transaction commit ทีละขั้น เปิด intermediate state และใช้ compensation หรือ forward recovery Outbox/Inbox ช่วยเชื่อม database กับ messaging โดยยอม duplicate และทำ effect ให้ idempotent สิ่งเหล่านี้ไม่ใช่การสร้าง exactly-once จากอากาศ แต่เป็นการจัดวางหลักฐานและ atomic boundary ให้ความซ้ำไม่ทำลาย invariant
ขั้นตอนถัดไปคือ Name Services เราจะเปลี่ยนจากคำถามว่า “ทุกฝ่ายตกลงผลอย่างไร” ไปสู่ “ทุกฝ่ายรู้ได้อย่างไรว่ากำลังอ้างถึง entity เดียวกัน” เมื่อ service และ replica ย้ายตำแหน่ง Naming จะคั่น identity ออกจาก address แต่ cache, TTL และ authority จะนำ consistency problem รูปแบบใหม่เข้ามา