Proceed with Caution (ดำเนินการอย่างระมัดระวัง)

เรื่อง asynchronous พวกนี้ดูสนุกใช่ไหมล่ะ? event-driven architecture ดูเหมือนจะนำไปสู่ระบบที่ decoupled และ scalable มากขึ้นอย่างมีนัยสำคัญ และมันก็ทำได้จริง แต่ communication style เหล่านี้ก็นำมาซึ่งความซับซ้อนที่เพิ่มขึ้นด้วย ไม่ใช่แค่ความซับซ้อนที่จำเป็นในการจัดการ publish และ subscribe message อย่างที่เราพึ่งพูดถึงไป แต่ยังรวมถึงความซับซ้อนในปัญหาอื่น ๆ ที่เราอาจเจอด้วย เช่น เมื่อพิจารณา long-running async request-response เราต้องคิดว่าจะทำอย่างไรเมื่อ response กลับมา มันกลับมายัง node เดียวกันที่เริ่ม request หรือไม่? ถ้าใช่ จะเกิดอะไรขึ้นถ้า node นั้น down? ถ้าไม่ใช่ ผมต้องเก็บข้อมูลไว้ที่ไหนสักแห่งเพื่อให้ตอบสนองได้อย่างเหมาะสมหรือไม่? short-lived async อาจจัดการง่ายกว่าถ้าคุณมี API ที่เหมาะสม แต่ถึงอย่างนั้นมันก็เป็นวิธีคิดที่ต่างออกไปสำหรับ programmer ที่คุ้นเคยกับ intra-process synchronous message call

ถึงเวลาเล่าเรื่องเตือนใจสักเรื่อง ย้อนกลับไปในปี 2006 ผมกำลังทำงานสร้างระบบ pricing ให้ธนาคารแห่งหนึ่ง เราจะดู market event และคำนวณว่า item ไหนใน portfolio ต้องถูก reprice เมื่อเราหา list ของสิ่งที่ต้องทำงานแล้ว เราจะใส่ทั้งหมดนี้ลงใน message queue เราใช้ grid เพื่อสร้าง pool ของ pricing worker ทำให้เรา scale pricing farm ขึ้นลงได้ตามคำสั่ง worker เหล่านี้ใช้ competing consumers pattern แต่ละตัวกิน message ให้เร็วที่สุดเท่าที่จะทำได้จนไม่มีอะไรเหลือให้ประมวลผล

ระบบทำงานอยู่ และเรารู้สึกค่อนข้างพอใจในตัวเอง แต่วันหนึ่ง หลังจากที่เราเพิ่ง push release ออกไป เราเจอปัญหาที่ร้ายแรง worker ของเราตายไปเรื่อย ๆ ตายแล้วตายอีก

ในที่สุดเราก็ตามหาปัญหาเจอ มี bug แอบเข้ามาที่ทำให้ pricing request ประเภทหนึ่งทำให้ worker crash เราใช้ transacted queue อยู่ เมื่อ worker ตาย lock ของมันบน request หมดเวลาลง และ pricing request ก็ถูกใส่กลับเข้า queue อีกครั้ง แล้วก็มีอีก worker หนึ่งมารับมันไปแล้วก็ตายอีก นี่คือตัวอย่างคลาสสิกของสิ่งที่ Martin Fowler เรียกว่า catastrophic failover

นอกจาก bug เองแล้ว เรา ยังไม่ได้กำหนด maximum retry limit ให้กับ job บน queue ดังนั้นเราจึงแก้ bug และตั้งค่า maximum retry แต่เรายังตระหนักด้วยว่าเราต้องการวิธีดูและอาจ replay message ที่แย่เหล่านี้ ท้ายที่สุดเราต้อง implement message hospital (หรือ dead letter queue) ที่ message จะถูกส่งไปถ้ามันล้มเหลว เรายังสร้าง UI เพื่อดู message เหล่านั้นและ retry ได้ถ้าจำเป็น ปัญหาแบบนี้ไม่ชัดเจนในทันทีถ้าคุณคุ้นเคยแค่กับ synchronous point-to-point communication

ความซับซ้อนที่เกี่ยวข้องกับ event-driven architecture และ asynchronous programming โดยรวม ทำให้ผมเชื่อว่าคุณควรระมัดระวังว่าคุณจะเริ่มนำแนวคิดเหล่านี้มาใช้อย่างกระตือรือร้นแค่ไหน ต้องแน่ใจว่าคุณมี monitoring ที่ดี และพิจารณาอย่างจริงจังในการใช้ correlation ID ซึ่งช่วยให้คุณ trace request ข้าม process boundary ได้ อย่างที่เราจะพูดถึงในรายละเอียดใน Chapter 10

ผมยังแนะนำอย่างยิ่งให้ลองอ่าน Enterprise Integration Patterns โดย Gregor Hohpe และ Bobby Woolf 4 ซึ่งมีรายละเอียดมากมายเกี่ยวกับ messaging pattern ต่าง ๆ ที่คุณอาจอยากพิจารณาในพื้นที่นี้

แต่เราก็ต้องซื่อสัตย์กับตัวเองเกี่ยวกับ integration style ที่เราอาจมองว่า "ง่ายกว่า" ด้วยเช่นกัน ปัญหาที่เกี่ยวข้องกับการรู้ว่าสิ่งต่าง ๆ ทำงานสำเร็จหรือไม่นั้นไม่ได้จำกัดอยู่แค่รูปแบบ asynchronous ของ integration ด้วย synchronous, blocking call ถ้าคุณเจอ time-out นี่เกิดขึ้นเพราะ request หายไปและฝ่าย downstream ไม่ได้รับมันหรือเปล่า? หรือ request ผ่านไปได้ แต่ response หายไป? คุณจะทำอย่างไรในสถานการณ์นั้น? ถ้าคุณ retry แต่ request เดิมผ่านไปแล้วล่ะ จะเกิดอะไรขึ้น? (นี่คือจุดที่ idempotency เข้ามามีบทบาท ซึ่งเป็นหัวข้อที่เราจะพูดถึงใน Chapter 12 )

อาจกล่าวได้ว่าในเรื่องของ failure handling synchronous blocking call ก็สามารถสร้างปัญหาปวดหัวให้เราได้พอ ๆ กันเมื่อต้องหาว่าสิ่งต่าง ๆ เกิดขึ้นจริงหรือไม่ (หรือไม่ได้เกิด) แค่ว่าปัญหาปวดหัวเหล่านั้นอาจคุ้นเคยกับเรามากกว่าเท่านั้นเอง!