“Async in Sync” Case อันน่าปวดหัว

Teerayut Hiruntaraporn
2 min readJun 29, 2023

--

มีใครเคยเขียน REST API ไปต่อ Service ชาวบ้านหรือ Service ภายในด้วยกันเอง แล้วเจอปัญหาแบบนี้บ้างไหมครับ

คือยิงไปครั้งแรก เจออะไรก็ไม่รู้ แต่พอยิงไปครั้งที่สอง ระบบก็ตี error กลับมาบอกว่า request ถูกทำไปแล้ว แล้วเราก็ไปต่อไม่ได้ เพราะ ปกติ request ประเภทนี้จะต้องคืน id บางอย่างเพื่อให้เรากลับไป reference ต่อ

ซึ่งถ้าเราอาจจะโชคดี เข้าใจการทำงานภายในของระบบ หลังบ้านเขา อาจจะเห็นรูปแบบประมาณนี้ ซึ่งในตัวอย่างคือ service นี้มีคุยหลังบ้านกับ service ข้างหลัง 3 ตัว

แล้วค่อยคืน result ออกมาให้ โดย service จะเป็นตัว hold connection ที่เป็น synchronous ระหว่าง client ให้

ลักษณะนี้โดยภาพรวม ระบบใช้รูปแบบ Distributed Transaction แบบ Choreography (แต่แบบ Orchestration ก็สามารถเกิดเคสนี้ได้เช่นกัน)​ ทีนี้ ปัญหา ที่อาจจะเกิดขึ้นได้ คือ

  • การ hold connection ระหว่าง client — service นานจน timeout
  • เกิด error ขึ้นที่ service ระหว่าง การทำงาน
  • ฯลฯ

อย่างไรก็ตาม เนื่องด้วยความเป็น asynchronous subsystem A, B, C ก็ยังคงทำงานต่อไป จนเสร็จ

แต่เนื่องจาก error ที่เกิดขึ้น service ได้ทำการส่ง ผลลัพธ์เป็น error กลับไปหา client เรียบร้อยแล้ว

สถานการณ์ตรงนี้ทำให้ สิ่งที่ client รับรู้ กับ สิ่งที่ service เข้าใจ เป็นคนละเรื่องกัน

เมื่อความเข้าใจใน state ของ client และ service แตกต่างกัน ก็จะทำให้การส่งคำสั่งมีปัญหาต่อไปได้

Root Concept (Incomplete Transaction Handling)

จริงๆ แล้ว กรณีนี้เกิดได้ แม้กระทั่ง ในระบบ ที่มี service ทำงานแค่เครื่องเดียว เช่น

  • service เขียน db 2 ครั้ง แต่ไม่ทำ TX แล้ว error
  • เขียน เสร็จแล้ว ไป error ในส่วน ปิด job

ทั้งหมดนี้จะทำให้เกิด state inconsistent ขึ้นในใน request นั้นๆ

และอาจจะส่งผลต่อ การส่ง request เดิมกลับมาได้

ปัญหาใน Distributed System

ส่วนใหญ่ จะเป็นปัญหาที่เกิดตอน Design ภาพรวม นั่นคือ ออกแบบให้แต่ละคนทำเฉพาะส่วนของตัวเอง ไม่ได้เอา use case มากางแล้ว เอา system ไปตอบ

ทำให้ ไม่สามารถตอบได้ว่า กรณีที่ timeout หรือ เกิด error ขึ้น ระหว่าง process แล้ว แต่ละส่วนในฐานะ system เดียวกัน ต้องทำยังไงต่อ

ทำให้แต่ละ subsystem จัดการตัวใครตัวมัน และไม่ respect สัญญาณความผิดพลาดจากระบบรอบตัว

ทั้งหมดทั้งสิ้นจะนำมาสู่ invalid state in Transaction ทั้งหมด

ซึ่งความเป็นไปได้ ใน Distributed System ก็จะวุ่นวายกว่า ระบบ single system เช่น

  • A สำเร็จ B ไม่สำเร็จ ต้องทำยังไง
  • A, B ,C กำลังจะสำเร็จ แต่ หัว error จะทำยังไง

ซึ่งพอเกิด invalid state ขึ้น ผลกระทบของมันก็จะมีความเป็นไปได้อยู่ดังนี้

  1. ไม่ส่งผลอะไร
  2. ไม่ส่งผลอะไร แต่สร้างขยะในระบบ ที่เคลียร์ไม่ได้
  3. ไม่ส่งผลตอนนี้ แต่สะสมไปเรื่อยๆ จนระเบิดสักวัน
  4. ส่งผลต่อ state consistency ระบบ เช่น อาจจะ process ต่อไม่ได้
  5. ส่งผลต่อความเข้าใจของ client นำมาสู่ invalid state ระหว่าง service กับ client

และถ้าเป็นที่ 5 เมื่อไหร่ แปลว่า เรากำลังสร้างปัญหาแบบ recursive ใน distributed system ให้ใหญ่โตขึ้นด้วยนะ

แนวทางแก้ปัญหา

  1. จัดการเรื่อง Distributed Transaction ให้ดี มองเคสในภาพรวมให้ได้ เวลามี error จะต้อง reset state ให้ครบทุก subsystem (Saga Pattern ถือเป็นพื้นฐานหนึ่งสำหรับเรื่องการจัดการ Distributed Transaction)
  2. ใน service ประมาณ Post อาจจะใช้คุณสมบัติ idempotent มาช่วย เช่น ถ้าเราเห็นว่า state มันสร้างไปแล้ว และ content ที่ส่งมาสร้างมันเหมือนกับของเดิม เราสามารถส่งข้อมูลแบบเดียวกับที่เป็นการสร้างครั้งแรกกลับไปให้อีกรอบได้ โดยไม่ต้องสร้างใหม่ ในกรณีนี้ ถ้าจะดี status code ไม่ควรจะเป็น 4xx หรือ 5xx เพื่อ inform client ว่าจริงมัน complete นะ ไม่ใช่ error client จะได้ไม่ต้องสร้าง exceptional case มา จัดการ

--

--

Teerayut Hiruntaraporn
Teerayut Hiruntaraporn

Responses (1)