Pattern: Request-Response Communication (รูปแบบ: Request-Response Communication)
ด้วย request-response microservice ส่ง request ไปยัง downstream service เพื่อขอให้ทำบางอย่าง และคาดหวังจะได้รับ response พร้อมผลลัพธ์ของ request นั้น ปฏิสัมพันธ์นี้ทำได้ผ่าน synchronous blocking call หรือ implement แบบ asynchronous nonblocking ก็ได้ ตัวอย่างง่าย ๆ ของปฏิสัมพันธ์นี้แสดงอยู่ใน Figure 4-8 ที่ Chart microservice ซึ่งรวบรวม CD ขายดีที่สุดของแต่ละแนวเพลง ส่ง request ไปยัง Inventory service เพื่อขอระดับ stock ปัจจุบันของ CD บางรายการ
Figure 4-8. Chart microservice ส่ง request ไปยัง Inventory เพื่อขอระดับ stock
การดึงข้อมูลจาก microservice อื่นแบบนี้เป็น use case ทั่วไปของ request-response call แต่บางครั้งคุณก็แค่ต้องการให้แน่ใจว่างานบางอย่างถูกทำเสร็จ ใน Figure 4-9 Warehouse microservice ได้รับ request จาก Order Processor ให้จอง stock Order Processor แค่ต้องรู้ว่า stock ถูกจองสำเร็จก่อนที่จะทำงานต่อในการรับ payment ถ้าจอง stock ไม่ได้ อาจเพราะสินค้าหมดแล้ว payment ก็สามารถยกเลิกได้ การใช้ request-response call ในสถานการณ์แบบนี้ที่ call ต้องเสร็จตามลำดับที่กำหนดเป็นเรื่องปกติทั่วไป
Figure 4-9. Order Processor ต้องมั่นใจว่าจอง stock ได้ก่อนที่จะรับ payment
Commands Versus Requests (Command เทียบกับ Request)
ผมเคย ได้ยินบางคนพูดถึงการส่ง command แทนที่จะเป็น request โดยเฉพาะในบริบทของ asynchronous request-response communication เจตนาเบื้องหลังคำว่า command ก็อาจกล่าวได้ว่าเหมือนกับ request คือ upstream microservice กำลังขอให้ downstream microservice ทำบางอย่าง
แต่ส่วนตัวผมชอบคำว่า request มากกว่า command สื่อถึงคำสั่งที่ต้องเชื่อฟัง และอาจนำไปสู่สถานการณ์ที่คนรู้สึกว่า command ต้องถูกดำเนินการ request สื่อถึงสิ่งที่ปฏิเสธได้ เป็นเรื่องถูกต้องที่ microservice จะพิจารณา request แต่ละอันตามคุณค่าของมัน และตัดสินใจโดยอิงจาก internal logic ของตัวเองว่าควรดำเนินการ request นั้นหรือไม่ ถ้า request ที่ส่งมาละเมิด internal logic microservice ควรปฏิเสธมัน แม้จะเป็นความแตกต่างที่ละเอียดอ่อน แต่ผมไม่รู้สึกว่าคำว่า command สื่อความหมายแบบเดียวกัน
ผมจะยึดใช้คำว่า request แทน command ต่อไป แต่ไม่ว่าคุณจะเลือกใช้คำไหน แค่จำไว้ว่า microservice มีสิทธิ์ปฏิเสธ request/command นั้นได้ถ้าเหมาะสม
Implementation: Synchronous Versus Asynchronous (การ Implement: Synchronous เทียบกับ Asynchronous)
request-response call แบบนี้ implement ได้ทั้งแบบ blocking synchronous หรือ nonblocking asynchronous style ด้วย synchronous call สิ่งที่คุณมักเห็นคือ network connection ถูกเปิดกับ downstream microservice โดย request ถูกส่งไปตาม connection นี้ connection จะถูกเปิดค้างไว้ในขณะที่ upstream microservice รอ downstream microservice ตอบกลับ ในกรณีนี้ microservice ที่ส่ง response ไม่จำเป็นต้องรู้อะไรเกี่ยวกับ microservice ที่ส่ง request เลย มันแค่ส่งข้อมูลกลับผ่าน inbound connection ถ้า connection นั้นตาย อาจเพราะ instance ของ upstream หรือ downstream microservice ตาย เราก็อาจมีปัญหา
ด้วย asynchronous request-response เรื่องนี้ไม่ตรงไปตรงมาเท่า ลองย้อนกลับไปดูกระบวนการจอง stock ใน Figure 4-10 request ในการจอง stock ถูกส่งเป็น message ผ่าน message broker บางแบบ (เราจะสำรวจ message broker ต่อไปในบทนี้) แทนที่ message จะไปยัง Inventory microservice โดยตรงจาก Order Processor มันกลับไปนั่งอยู่ใน queue แทน Inventory consume message จาก queue นี้เมื่อมันพร้อม มันอ่าน request ทำงานที่เกี่ยวข้องในการจอง stock แล้วต้องส่ง response กลับไปยัง queue ที่ Order Processor กำลังอ่านอยู่ Inventory microservice ต้องรู้ว่าจะ route response ไปที่ไหน ในตัวอย่างของเรา มันส่ง response นี้กลับผ่าน queue อีกตัวหนึ่งที่ถูก consume โดย Order Processor ต่อไป
Figure 4-10. การใช้ queue เพื่อส่ง stock reservation request
ดังนั้นด้วย nonblocking asynchronous interaction microservice ที่รับ request ต้องรู้โดย implicit ว่าจะ route response ไปที่ไหน หรือไม่ก็ต้องถูกบอกว่า response ควรไปที่ไหน เมื่อใช้ queue เรามีข้อดีเพิ่มเติมคือ request หลายตัวสามารถถูก buffer ไว้ใน queue รอการจัดการได้ นี่ช่วยได้ในสถานการณ์ที่ request จัดการไม่ทันเวลา microservice สามารถ consume request ถัดไปเมื่อมันพร้อม แทนที่จะถูก call จำนวนมากท่วมท้น แน่นอนว่าท้ายที่สุดก็ขึ้นอยู่กับว่า queue รองรับ request เหล่านี้ได้มากแค่ไหน
เมื่อ microservice ได้รับ response แบบนี้ มันอาจต้องเชื่อมโยง response กับ request เดิม นี่อาจท้าทาย เพราะเวลาอาจผ่านไปมากแล้ว และขึ้นอยู่กับธรรมชาติของ protocol ที่ใช้ response อาจไม่กลับมายัง instance เดียวกันของ microservice ที่ส่ง request ในตัวอย่างของการจอง stock เพื่อวาง order เราต้องรู้วิธีเชื่อมโยง response "stock reserved" กับ order ที่กำหนด เพื่อให้เราทำงานต่อกับ order นั้นได้ วิธีง่าย ๆ ในการจัดการเรื่องนี้คือเก็บ state ใด ๆ ที่เกี่ยวข้องกับ request เดิมลงใน database เพื่อว่าเมื่อ response เข้ามา instance ที่รับสามารถโหลด state ที่เกี่ยวข้องกลับมาและดำเนินการต่อได้
ข้อสังเกตสุดท้าย ปฏิสัมพันธ์แบบ request-response ทุกรูปแบบล้วนต้องการการจัดการ time-out บางแบบ เพื่อหลีกเลี่ยงปัญหาที่ระบบถูก block รอสิ่งที่อาจไม่เกิดขึ้นเลย วิธี implement functionality time-out นี้อาจแตกต่างกันไปตาม implementation technology แต่มันจำเป็นต้องมี เราจะดู time-out ในรายละเอียดมากขึ้นใน Chapter 12
Parallel Versus Sequential Calls (Call แบบ Parallel เทียบกับ Sequential)
เมื่อ ทำงานกับปฏิสัมพันธ์แบบ request-response คุณมักจะเจอสถานการณ์ที่ต้องทำ call หลายครั้งก่อนที่จะทำงานต่อไปได้ บางอย่าง
ลองพิจารณาสถานการณ์ที่ MusicCorp ต้องตรวจสอบราคาของสินค้าชิ้นหนึ่งจาก stockist สามรายที่ต่างกัน ซึ่งเราทำได้โดยยิง API call เราต้องการรับราคากลับมาจาก stockist ทั้งสามรายก่อนที่จะตัดสินใจว่าจะสั่ง stock ใหม่จากรายไหน เราอาจตัดสินใจทำ call สามครั้งแบบเรียงลำดับ รอให้แต่ละอันเสร็จก่อนที่จะทำอันถัดไป ในสถานการณ์แบบนี้ เราจะต้องรอผลรวมของ latency ของแต่ละ call ถ้า API call ไปยังแต่ละ provider ใช้เวลาหนึ่งวินาทีในการตอบกลับ เราจะต้องรอสามวินาทีก่อนที่จะตัดสินใจได้ว่าควรสั่งจากใคร
ตัวเลือกที่ดีกว่าคือรัน request ทั้งสามนี้แบบ parallel แล้ว latency โดยรวมของ operation จะขึ้นอยู่กับ API call ที่ช้าที่สุด แทนที่จะเป็นผลรวมของ latency ของแต่ละ API call
Reactive extension และกลไกอย่าง async/await มีประโยชน์มากในการช่วยรัน call แบบ parallel และสิ่งนี้สามารถช่วยปรับปรุง latency ของ operation บางอย่างได้อย่างมีนัยสำคัญ
Where to Use It (ควรใช้เมื่อไหร่)
request-response call สมเหตุสมผลอย่างยิ่งสำหรับสถานการณ์ใด ๆ ที่ผลลัพธ์ของ request จำเป็นก่อนที่จะทำงานต่อไปได้ มันยังเหมาะกับสถานการณ์ที่ microservice ต้องการรู้ว่า call ไม่สำเร็จ เพื่อจะได้ดำเนิน compensating action บางอย่าง เช่น retry ถ้าสถานการณ์ของคุณตรงกับข้อใดข้อหนึ่งนี้ request-response ก็เป็นแนวทางที่สมเหตุสมผล คำถามที่เหลือแค่ตัดสินใจว่าจะ implement แบบ synchronous หรือ asynchronous ซึ่งมี trade-off เดียวกับที่เราพูดถึงไปก่อนหน้านี้