บทที่ 14 · Part 3 — Production & Advanced
Case Study & Checklist
สถาปัตยกรรม wallet แบบครบวงจร, ตารางแก้ปัญหา, anti-patterns และ checklist ก่อน go-live
13 บทที่ผ่านมาแยกกันอธิบายทีละเรื่อง บทนี้เอามาต่อกันเป็นระบบเดียว แล้วปิดท้ายด้วยสิ่งที่ต้องตรวจก่อนเปิดให้ลูกค้าใช้จริง
จบบทนี้คุณจะ
- เห็นภาพรวมว่าแต่ละส่วนของระบบ wallet ใช้ pattern ไหน
- มีตารางแก้ปัญหาที่ใช้หน้างานได้จริง
- รู้จัก anti-pattern ที่พบบ่อยและวิธีเลี่ยง
- มี checklist ก่อน go-live ที่ตรวจได้ทีละข้อ
สถาปัตยกรรมรวม
แยก worker เป็น 3 ชุดตามเหตุผลจากบทที่ 11: งาน orchestration เบาและเร็ว, งานคุยธนาคารต้องจำกัด rate ตามโควตา, งานกระทบยอดกินเวลานานและรันกลางดึก — ปนกันแล้วงานหนึ่งจะกิน slot ของอีกงาน
เส้นทางของ 1 ธุรกรรม
ตามรอยการเติมเงิน 500 บาทตั้งแต่ผู้ใช้กดปุ่มจนเงินเข้า
| ขั้น | เกิดอะไร | บทที่อธิบาย |
|---|---|---|
| 1 | API รับ request แล้วยิง Update-with-Start ไปที่ wallet-<id> | 7 |
| 2 | Entity workflow ของบัญชีตรวจ daily limit จาก state ในหน่วยความจำ | 8 |
| 3 | Validator ปฏิเสธทันทีถ้าเกิน limit — ไม่ถูกบันทึกลง history | 7 |
| 4 | ยอดสูงกว่าเกณฑ์ → ส่ง OTP แล้วรอ signal สูงสุด 5 นาที | 7 |
| 5 | เรียก ChargeBank บนคิว bank-io พร้อม idempotency key = transaction ID | 5 |
| 6 | ธนาคาร timeout → retry ตาม policy โดยใช้ key เดิม ไม่หักซ้ำ | 5 |
| 7 | สำเร็จ → เครดิตเข้า wallet, ล้มก่อน point of no return → compensate | 6 |
| 8 | Upsert search attribute TxnStage ทุกครั้งที่เปลี่ยนสถานะ | 11 |
| 9 | ยิง child workflow แบบ ABANDON ไปเช็ค settlement ที่ T+1 | 8 |
| 10 | ตี 2 Schedule ปลุกรอบกระทบยอด แตก child ต่อธนาคาร | 8 |
จุดที่ผู้เริ่มต้นมักพลาดคือขั้นที่ 6 — คิดว่า Temporal จัดการ retry ให้แล้วปลอดภัย แต่ Temporal รับประกันแค่ว่า activity จะถูกเรียกจนสำเร็จ ไม่ได้รับประกันว่าจะเรียกครั้งเดียว ความปลอดภัยเรื่องหักเงินซ้ำมาจาก idempotency key ที่เราส่งเอง
เลือก pattern ให้ถูกงาน
| ส่วนของระบบ | pattern | ทำไม |
|---|---|---|
| เติมเงิน / ถอนเงิน | Workflow ต่อ 1 ธุรกรรม | อายุสั้น, pin เวอร์ชันได้, เห็นทีละรายการชัด |
| บัญชี wallet | Entity Workflow + Continue-As-New | ได้ serialization ของคำสั่งฟรี ไม่ต้องล็อก DB |
| โอนออกธนาคาร | Saga + point of no return | ต้อง compensate ได้ก่อนจุดที่ย้อนไม่ได้ |
| ตรวจ settlement | Child workflow แบบ ABANDON + timer | อยู่ต่อหลัง parent จบ กินทรัพยากรแทบเป็นศูนย์ |
| กระทบยอดรายวัน | Schedule + child ต่อธนาคาร | pause/backfill ได้ และธนาคารหนึ่งล้มไม่ล้มทั้งรอบ |
| อนุมัติโดยคน | Signal (หรือ Async Activity Completion) | รอได้เป็นวันโดยไม่กิน resource |
| ตรวจสถานะธนาคาร | Poll ในตัว activity หรือ retry เป็นตัวจับเวลา | ไม่ให้ history บวมจากการ poll |
| อัตราแลกเปลี่ยน | Local Activity | เร็ว และทำซ้ำแล้วไม่เสียหาย |
ตารางแก้ปัญหา
| อาการ | รหัส/สัญญาณ | สาเหตุ | ทำอะไร |
|---|---|---|---|
| Workflow ค้าง Running ไม่เดิน | TMPRL1100 | โค้ดใหม่ replay history เก่าไม่ตรง | rollback worker ก่อน แล้วค่อยแก้ด้วย GetVersion |
| Workflow task fail ซ้ำ | TMPRL1101 | มีโค้ดบล็อกใน workflow (I/O, time.Sleep, mutex) | ย้ายไป activity ใช้ primitive ของ Temporal |
| จบ workflow ไม่ได้ | TMPRL1102 | signal/update handler ยังทำงานค้าง | รอ workflow.AllHandlersFinished ก่อน return |
| ส่งข้อมูลไม่ผ่าน | TMPRL1103 | payload เกิน 2MB | เก็บลง storage แล้วส่ง reference |
| Activity retry ไม่หยุด | — | error ที่ควรเป็น non-retryable ถูกจัดเป็น retryable | ใช้ NewNonRetryableApplicationError |
| ธุรกรรมช้าทั้งระบบ | schedule_to_start_latency สูง | worker ไม่พอ | เพิ่ม poller หรือ scale worker |
| CPU worker สูงแต่งานไม่เดิน | sticky_cache_miss พุ่ง | worker restart บ่อย → replay ซ้ำ | ลดการ restart, เพิ่ม cache size |
| ร้านเล็กรายการค้าง | — | ร้านใหญ่ครองคิว | เปิด Fairness ด้วย merchant ID |
| ลูกค้าถูกหักเงินซ้ำ | — | activity ไม่ idempotent | แก้ให้ส่ง idempotency key ที่ deterministic |
| Web UI แสดงเป็น byte | — | เปิด codec แล้วแต่ไม่มี codec server | ตั้ง codec server ให้ UI เรียก |
ลำดับการรับมือเมื่อ production มีปัญหา
- หยุดเลือดก่อน — rollback ให้ธุรกรรมเดินต่อได้ ยังไม่ต้องหาสาเหตุ
- ประเมินว่ามีเงินค้างอยู่ในสถานะกำกวมกี่รายการ
- ค่อยหาสาเหตุจาก history ของ execution ที่พัง
อย่าเริ่มด้วยการ terminate หรือ reset — ทั้ง 2 อย่างทำให้เสียข้อมูลสถานะ
ที่ต้องใช้ตอนกระทบยอด และ reset อาจทำให้ activity รันซ้ำจนโอนเงินซ้ำ
การกระทำเหล่านี้ต้องได้รับอนุมัติจากผู้มีอำนาจก่อนเสมอ
Anti-patterns
เอา business logic ที่เปลี่ยนบ่อยไว้ใน workflow
// ปัญหา: แก้ค่า fee ทีต้อง version ทีนึง
if req.AmountSatang > 10_000_00 {
fee = req.AmountSatang * 15 / 1000
}
ย้ายไป activity แล้วเปลี่ยนกฎได้โดยไม่กระทบธุรกรรมที่ค้างอยู่ (บทที่ 10)
ใช้ workflow เป็น queue หรือ database
Workflow ไม่ได้ออกแบบมาให้เก็บ record จำนวนมาก ถ้าพบว่า state ของ workflow โตขึ้นเรื่อยๆ ตามจำนวนรายการ แสดงว่าเลือกเครื่องมือผิด
แตก child workflow เพียงเพื่อจัดระเบียบโค้ด
ฟังก์ชัน Go ธรรมดาทำได้ฟรี child workflow มีต้นทุนทั้ง event, latency และการจัดการ ID ใช้เมื่อมีเหตุผลเชิงระบบเท่านั้น (บทที่ 8)
ส่งข้อมูลก้อนใหญ่ผ่าน workflow
ทุก byte ที่ผ่าน input/output จะอยู่ใน history ตลอด retention และถูกอ่านซ้ำทุกครั้งที่ replay ส่ง reference แทน (บทที่ 11)
ตั้งแค่ ScheduleToCloseTimeout แล้วคิดว่าครบ
ถ้าไม่ตั้ง StartToCloseTimeout ด้วย activity ที่ค้างจะกิน budget รวมไปจนหมด
โดยไม่มีการ retry เลย (บทที่ 5)
จับ error ทุกตัวเป็น retryable
Business rule ที่ผิด retry กี่ครั้งก็ผิดเหมือนเดิม แต่จะกิน quota ของ API ธนาคาร และหน่วงธุรกรรมจนหมดเวลา (บทที่ 6)
เชื่อว่า Temporal ทำให้ระบบ exactly-once
Temporal ให้ at-least-once สำหรับ activity ความเป็น exactly-once ต้องมาจาก idempotency ที่เราออกแบบเอง (บทที่ 5)
ใช้ Local Activity เพราะอยากให้เร็ว
มันไม่มี record ใน history จนกว่าจะเสร็จ worker ตายแล้วเริ่มใหม่หมด ห้ามใช้กับอะไรที่ทำให้เงินเคลื่อน (บทที่ 13)
Checklist ก่อน go-live
ความถูกต้องของธุรกรรม
- ทุก activity ที่แตะเงินรับ idempotency key ที่ deterministic (ไม่ใช้
info.Attempt) - ทุกขั้นก่อน point of no return มี compensation และมีเทสพิสูจน์ว่าทำงาน ครั้งเดียว
- ขั้นหลัง point of no return มีเทสยืนยันว่า ไม่ compensate
- Error ถูกแยก retryable / non-retryable ครบทุกเส้นทาง
- เคสที่ไม่รู้สถานะจริงของธนาคาร หยุดรอคนแทนที่จะเดา
- จำนวนเงินเป็น integer หน่วยสตางค์ทุกที่ ไม่มี float
ความทนทาน
- ทุก activity ตั้ง
StartToCloseTimeout - Activity ที่รันเกิน ~30 วินาที มี heartbeat และเช็ค
ctx.Err() - Loop ที่ไม่มีเงื่อนไขจบชัดเจน มีการเช็ค
GetContinueAsNewSuggested() - ก่อน Continue-As-New มี
AllHandlersFinishedและ drain signal channel ครบ - Child ที่แตะเงินใช้
ParentClosePolicyเป็นREQUEST_CANCEL - Schedule ตั้ง
TimeZoneNameและOverlapตรงกับความหมายทางบัญชี - ไม่มี payload เกินขนาดไหลผ่าน workflow
การเปลี่ยนแปลงโค้ด
- มี replay fixture อย่างน้อย 1 ไฟล์ต่อ 1 execution path สำคัญ
- Replay test เป็นด่านบังคับใน CI ไม่ใช่เทสที่ข้ามได้
- เลือกกลยุทธ์ versioning แล้ว และทีมเข้าใจตรงกัน
- ขั้นตอน rollback ถูกซ้อมจริง ไม่ใช่แค่เขียนไว้ในเอกสาร
การมองเห็น
- Metrics endpoint และ dashboard พร้อม โดยมี schedule-to-start latency เป็นตัวหลัก
- Alert บน
failure_reason="NonDeterminismError"ดังทันที - Log ผ่าน
workflow.GetLoggerทั้งหมด - Search Attributes ครอบคลุมคำถามที่ ops ถามบ่อย
- ทีม ops มี runbook และรู้ว่าเมื่อไหร่ควรส่ง signal เมื่อไหร่ควรเรียก engineer
ความปลอดภัย
- Payload Codec เปิดใช้บนทุก environment ที่มีข้อมูลจริง
- กุญแจมาจาก KMS มีแผน rotation และ payload เก็บ key ID ไว้
- Codec Server มี auth, authorization และ audit log
- Workflow ID และ Search Attributes ไม่มีข้อมูลส่วนบุคคล
- ไม่มีข้อมูลบัตรเต็มใน input/output ของ workflow
- แยก namespace ระหว่าง environment
- Credential ไม่อยู่ใน repo และมี alert ก่อน certificate หมดอายุ
- Retention ถูกตั้งตามที่ตกลงกับทีม compliance
ข้อที่ checklist ตรวจแทนไม่ได้
รายการข้างบนเป็นเรื่องเทคนิคที่ทีมพัฒนาตรวจเองได้ แต่มีอีกกลุ่มหนึ่งที่ต้อง ผ่านการอนุมัติจากผู้มีอำนาจ ก่อนขึ้น production เสมอ:
- การประเมินความสอดคล้องกับ PDPA, PCI-DSS และข้อกำหนดของ BOT
- ข้อตกลงกับธนาคารและ partner เรื่องโควตา, SLA และการกระทบยอด
- แผนรับมือเหตุการณ์ผิดปกติที่กระทบลูกค้า และเกณฑ์การแจ้งเหตุ
- สิทธิ์ของทีม ops ในการสั่งงานที่กระทบเงินของลูกค้า
อย่าถือว่า "โค้ดผ่านเทสแล้ว" เท่ากับ "พร้อมขึ้น production"
สิ่งที่ควรจำที่สุดจากคอร์สนี้
Temporal ไม่ได้ทำให้ระบบการเงินถูกต้องโดยอัตโนมัติ มันให้สิ่งเดียวคือ การรับประกันว่าโค้ดของคุณจะทำงานต่อจนจบ ไม่ว่าจะเกิดอะไรขึ้นระหว่างทาง
ส่วนที่เหลือยังเป็นงานของเรา:
- Idempotency — Temporal เรียกซ้ำได้ เราต้องทำให้การเรียกซ้ำไม่เสียหาย
- Compensation — Temporal จำได้ว่าทำอะไรไปแล้ว เราต้องบอกว่าจะย้อนยังไง
- Point of no return — Temporal ไม่รู้ว่าเงินออกจากระบบไปแล้วหรือยัง เราต้องเป็นคนบอก
- การยอมรับว่าไม่รู้ — เมื่อไม่แน่ใจสถานะจริง หยุดแล้วเรียกคน ดีกว่าเดาแล้วผิด
3 ข้อแรกคือวิศวกรรม ข้อสุดท้ายคือวิจารณญาณ — และในระบบที่จัดการเงินของคนอื่น ข้อสุดท้ายมักสำคัญที่สุด