Pattern: Synchronous Blocking (รูปแบบ: Synchronous Blocking)
ใน synchronous blocking call microservice ตัวหนึ่งส่ง call บางอย่างไปยัง downstream process (มักเป็นอีก microservice หนึ่ง) และ block การทำงานจนกว่า call จะเสร็จสิ้น และอาจรวมถึงจนกว่าจะได้รับ response ใน Figure 4-2 Order Processor ส่ง call ไปยัง Loyalty microservice เพื่อแจ้งให้ทราบว่าควรเพิ่ม point ให้บัญชีของลูกค้า
Figure 4-2. Order Processor ส่ง synchronous call ไปยัง Loyalty microservice, block และรอ response
โดยทั่วไปแล้ว synchronous blocking call คือ call ที่รอ response จาก downstream process นี่อาจเป็นเพราะผลลัพธ์ของ call นั้นจำเป็นสำหรับการทำงานต่อไป หรือเพียงเพราะต้องการให้แน่ใจว่า call ทำงานสำเร็จและจะทำ retry บางอย่างถ้าไม่สำเร็จ ดังนั้น synchronous blocking call เกือบทุกตัวที่ผมเห็นก็เป็น request-response call ด้วยเช่นกัน ซึ่งเป็นสิ่งที่เราจะดูกันต่อไป
Advantages (ข้อดี)
blocking synchronous call นั้นมีความเรียบง่ายและคุ้นเคย พวกเราหลายคนเรียนเขียนโปรแกรมด้วยสไตล์ synchronous เป็นพื้นฐาน คืออ่านโค้ดเหมือนอ่าน script ที่แต่ละบรรทัดทำงานทีละบรรทัด และบรรทัดถัดไปก็รอคิวที่จะทำงาน สถานการณ์ส่วนใหญ่ที่คุณเคยใช้ inter-process call น่าจะทำในสไตล์ synchronous, blocking เช่น รัน SQL query บน database หรือทำ HTTP request ไปยัง downstream API
เมื่อย้ายจาก architecture ที่ distributed น้อยกว่า เช่น single process monolith การยึดติดกับแนวคิดที่คุ้นเคยในขณะที่มีสิ่งใหม่ ๆ เกิดขึ้นมากมายก็เป็นเรื่องสมเหตุสมผล
Disadvantages (ข้อเสีย)
ความท้าทายหลักของ synchronous call คือ temporal coupling ที่เกิดขึ้นโดยธรรมชาติ หัวข้อที่เราสำรวจสั้น ๆ ใน Chapter 2 เมื่อ Order Processor เรียก Loyalty ในตัวอย่างก่อนหน้า Loyalty microservice ต้องเข้าถึงได้เพื่อให้ call ทำงานสำเร็จ ถ้า Loyalty microservice ไม่พร้อมใช้งาน call จะล้มเหลว และ Order Processor ต้องคิดว่าจะทำ compensating action แบบไหน ซึ่งอาจเป็นการ retry ทันที บัฟเฟอร์ call ไว้ retry ทีหลัง หรืออาจเลิกล้มไปเลย
coupling นี้เกิดขึ้นสองทาง ด้วยรูปแบบ integration นี้ response มักถูกส่งกลับผ่าน network connection ขาเข้าเดียวกันไปยัง upstream microservice ดังนั้นถ้า Loyalty microservice ต้องการส่ง response กลับไปยัง Order Processor แต่ instance ต้นทางนั้นตายไปแล้ว response ก็จะหายไป temporal coupling ที่นี่ไม่ใช่แค่ระหว่าง microservice สองตัว แต่เป็นระหว่าง instance เฉพาะสองตัวของ microservice เหล่านี้
เนื่องจากผู้ส่ง call กำลัง block และรอ downstream microservice ตอบกลับ จึงตามมาด้วยว่าถ้า downstream microservice ตอบช้า หรือมีปัญหาเรื่อง latency ของเครือข่าย ผู้ส่ง call ก็จะถูก block เป็นเวลานานเพื่อรอ response ถ้า Loyalty microservice กำลังรับ load หนักและตอบ request ช้า สิ่งนี้ก็จะทำให้ Order Processor ตอบช้าไปด้วย
ดังนั้นการใช้ synchronous call ทำให้ระบบเปราะบางต่อปัญหา cascading ที่เกิดจาก downstream outage ได้ง่ายกว่าการใช้ asynchronous call
Where to Use It (ควรใช้เมื่อไหร่)
สำหรับ microservice architecture ที่เรียบง่าย ผมไม่มีปัญหาอะไรมากกับการใช้ synchronous, blocking call ความคุ้นเคยของมันสำหรับคนหลายคนเป็นข้อได้เปรียบเมื่อต้องทำความเข้าใจ distributed system
สำหรับผม call แบบนี้เริ่มมีปัญหาเมื่อคุณเริ่มมี chain ของ call ยาวขึ้น ใน Figure 4-3 ยกตัวอย่างเช่น เรามีตัวอย่าง flow จาก MusicCorp ที่เรากำลังตรวจสอบ payment ว่ามีความเสี่ยงเป็น fraud หรือไม่ Order Processor เรียก Payment service เพื่อรับ payment Payment service ก็ต้องการตรวจสอบกับ Fraud Detection microservice ว่าควรอนุญาตหรือไม่ Fraud Detection microservice ก็ต้องการข้อมูลจาก Customer microservice อีกที
Figure 4-3. การตรวจสอบพฤติกรรมที่อาจเป็น fraud ในฐานะส่วนหนึ่งของ order processing flow
ถ้า call ทั้งหมดนี้เป็น synchronous และ blocking เราอาจเจอปัญหาหลายอย่าง ปัญหาใน microservice ทั้งสี่ตัวที่เกี่ยวข้อง หรือใน network call ระหว่างพวกมัน อาจทำให้การทำงานทั้งหมดล้มเหลว นี่ยังไม่นับว่า chain ยาว ๆ แบบนี้อาจทำให้เกิด resource contention อย่างมีนัยสำคัญ เบื้องหลัง Order Processor น่าจะมี network connection เปิดค้างไว้รอฟังจาก Payment ส่วน Payment ก็มี network connection เปิดค้างไว้รอ response จาก Fraud Detection และเป็นแบบนี้ต่อไปเรื่อย ๆ การมี connection จำนวนมากที่ต้องเปิดค้างไว้ย่อมส่งผลกระทบต่อระบบที่กำลังทำงาน คุณมีแนวโน้มที่จะเจอปัญหา connection ไม่พอใช้หรือ network congestion เพิ่มขึ้นมากกว่าเดิม
เพื่อปรับปรุงสถานการณ์นี้ เราอาจกลับไปพิจารณาปฏิสัมพันธ์ระหว่าง microservice ใหม่ตั้งแต่ต้น เช่น อาจย้าย Fraud Detection ออกจาก main purchase flow ดังที่แสดงใน Figure 4-4 แล้วให้มันทำงานเบื้องหลังแทน ถ้ามันเจอปัญหากับลูกค้ารายใดรายหนึ่ง record ของลูกค้านั้นจะถูกอัปเดต และนี่คือสิ่งที่สามารถตรวจสอบได้ก่อนหน้าในขั้นตอน payment ผลก็คือเราทำงานบางส่วนนี้แบบ parallel โดยการลดความยาวของ call chain เราจะเห็น latency โดยรวมของการทำงานดีขึ้น และเราจะเอา Fraud Detection ออกจาก critical path ของ purchase flow ทำให้เหลือ dependency ที่ต้องกังวลน้อยลงหนึ่งตัวสำหรับ operation ที่สำคัญนี้
Figure 4-4. การย้าย Fraud Detection ไปเป็น background process ช่วยลดความกังวลเรื่องความยาวของ call chain ได้
แน่นอนว่าเราอาจแทนที่การใช้ blocking call ด้วย nonblocking interaction บางรูปแบบโดยไม่ต้องเปลี่ยน workflow ตรงนี้เลยก็ได้ ซึ่งเป็นแนวทางที่เราจะสำรวจกันต่อไป