Pattern: Event-Driven Communication (รูปแบบ: Event-Driven Communication)
Event-driven communication ดูค่อนข้างแปลกเมื่อเทียบกับ request-response call แทนที่ microservice จะขอให้อีก microservice หนึ่งทำบางอย่าง microservice จะยิง event ออกมาซึ่งอาจถูกรับหรือไม่ถูกรับโดย microservice อื่น ๆ ก็ได้ มันเป็นปฏิสัมพันธ์แบบ asynchronous โดยธรรมชาติ เพราะ event listener จะทำงานบน thread of execution ของตัวเอง
event คือข้อความบอกว่ามีบางอย่างเกิดขึ้น เกือบทุกครั้งจะเป็นสิ่งที่เกิดขึ้นภายในโลกของ microservice ที่ยิง event นั้นออกมา microservice ที่ยิง event ไม่รู้เจตนาของ microservice อื่น ๆ ที่จะใช้ event นั้น และจริง ๆ แล้วอาจไม่รู้ด้วยซ้ำว่ามี microservice อื่นอยู่ มันแค่ยิง event เมื่อจำเป็น และนั่นคือจุดสิ้นสุดของความรับผิดชอบของมัน
ใน Figure 4-11 เราเห็น Warehouse ยิง event ที่เกี่ยวข้องกับกระบวนการแพ็ก order event เหล่านี้ถูกรับโดย microservice สองตัว คือ Notifications และ Inventory ซึ่งตอบสนองตามนั้น Notifications microservice ส่งอีเมลเพื่ออัปเดตลูกค้าของเราเกี่ยวกับการเปลี่ยนแปลงสถานะ order ในขณะที่ Inventory microservice สามารถอัปเดตระดับ stock เมื่อสินค้าถูกแพ็กเข้า order ของลูกค้า
Figure 4-11. Warehouse ยิง event ที่ microservice downstream บางตัว subscribe อยู่
Warehouse แค่ broadcast event โดยสมมติว่าฝ่ายที่สนใจจะตอบสนองตามนั้น มันไม่รู้ว่าใครเป็นผู้รับ event ทำให้ event-driven interaction มีความ loosely coupled มากกว่าโดยทั่วไป เมื่อเทียบกับ request-response call คุณอาจต้องใช้เวลาสักพักเพื่อทำความเข้าใจการกลับด้านของความรับผิดชอบนี้ ด้วย request-response เราอาจคาดหวังให้ Warehouse บอก Notifications microservice ให้ส่งอีเมลเมื่อเหมาะสมแทน ใน model แบบนั้น Warehouse ต้องรู้ว่า event ไหนต้องแจ้งลูกค้า แต่ด้วย event-driven interaction เรากลับผลักความรับผิดชอบนั้นเข้าไปใน Notifications microservice แทน
เจตนาเบื้องหลัง event อาจถือได้ว่าตรงข้ามกับ request ผู้ยิง event ปล่อยให้ผู้รับตัดสินใจเองว่าจะทำอะไร ด้วย request-response microservice ที่ส่ง request รู้ว่าควรทำอะไรและกำลังบอก microservice อื่นว่าคิดว่าอะไรควรเกิดขึ้นต่อไป แน่นอนว่านี่หมายความว่าใน request-response ผู้ร้องขอต้องมีความรู้ว่า downstream recipient ทำอะไรได้บ้าง ซึ่งบ่งบอกถึง domain coupling ที่มากกว่า ด้วย event-driven collaboration ผู้ยิง event ไม่จำเป็นต้องรู้ว่า downstream microservices ใด ๆ ทำอะไรได้บ้าง และจริง ๆ แล้วอาจไม่รู้ด้วยซ้ำว่ามันมีอยู่ ผลก็คือ coupling ลดลงอย่างมาก
การกระจายความรับผิดชอบที่เราเห็นใน event-driven interaction ของเราสะท้อนการกระจายความรับผิดชอบที่เราเห็นในองค์กรที่พยายามสร้างทีมที่มีความเป็นอิสระมากขึ้น แทนที่จะเก็บความรับผิดชอบทั้งหมดไว้ที่ศูนย์กลาง เราอยากผลักมันเข้าไปในทีมเองเพื่อให้ทีมทำงานได้อย่างอิสระมากขึ้น ซึ่งเป็นแนวคิดที่เราจะกลับมาพูดถึงอีกครั้งใน Chapter 15 ที่นี่ เรากำลังผลักความรับผิดชอบจาก Warehouse ไปยัง Notifications และ Inventory ซึ่งช่วยลดความซับซ้อนของ microservice อย่าง Warehouse และนำไปสู่การกระจาย "ความฉลาด" ในระบบของเราอย่างสม่ำเสมอมากขึ้น เราจะสำรวจแนวคิดนี้ในรายละเอียดมากขึ้นเมื่อเปรียบเทียบ choreography และ orchestration ใน Chapter 6
Events and Messages (Event และ Message)
บางครั้ง ผมเคยเห็นคำว่า message และ event ถูกใช้สับสนกัน event คือข้อเท็จจริง คือข้อความบอกว่ามีบางอย่างเกิดขึ้น พร้อมข้อมูลบางอย่างว่าเกิดอะไรขึ้นบ้าง ส่วน message คือสิ่งที่เราส่งผ่านกลไก asynchronous communication อย่าง message broker
ด้วย event-driven collaboration เราต้องการ broadcast event นั้น และวิธีทั่วไปในการ implement กลไก broadcast นี้คือใส่ event ลงใน message message คือตัวกลาง ส่วน event คือ payload
ในทำนองเดียวกัน เราอาจต้องการส่ง request เป็น payload ของ message ซึ่งในกรณีนั้นเรากำลัง implement รูปแบบหนึ่งของ asynchronous request-response
Implementation (การ Implement)
มีสองประเด็นหลักที่เราต้องพิจารณาที่นี่ คือวิธีให้ microservice ของเรายิง event ออกมา และวิธีให้ consumer ของเรารู้ว่า event เหล่านั้นเกิดขึ้น
โดยทั่วไปแล้ว message broker อย่าง RabbitMQ พยายามจัดการทั้งสองปัญหานี้ producer ใช้ API เพื่อ publish event ไปยัง broker broker จัดการ subscription ทำให้ consumer ได้รับแจ้งเมื่อ event มาถึง broker เหล่านี้ยังสามารถจัดการ state ของ consumer ได้ด้วย เช่น ช่วยติดตามว่า message ไหนที่พวกเขาเคยเห็นมาก่อน ระบบเหล่านี้มักถูกออกแบบมาให้ scalable และ resilient แต่นั่นก็ไม่ได้มาฟรี ๆ มันสามารถเพิ่มความซับซ้อนให้กับกระบวนการพัฒนา เพราะเป็นอีกระบบหนึ่งที่คุณอาจต้องรันเพื่อพัฒนาและทดสอบ service ของคุณ อาจต้องใช้เครื่องเพิ่มและความเชี่ยวชาญเพิ่มเติมเพื่อให้ infrastructure นี้ทำงานต่อไปได้ แต่เมื่อมันพร้อมใช้งานแล้ว มันสามารถเป็นวิธีที่มีประสิทธิภาพอย่างมากในการ implement architecture แบบ loosely coupled, event-driven โดยรวมแล้วผมเป็นแฟนของมัน
แต่ก็ต้องระวังเรื่องโลกของ middleware ซึ่ง message broker เป็นแค่ส่วนเล็ก ๆ queue เองก็เป็นสิ่งที่สมเหตุสมผลและมีประโยชน์อยู่แล้ว อย่างไรก็ตาม vendor มักอยากจะ package ซอฟต์แวร์เยอะ ๆ มาพร้อมกับมัน ซึ่งอาจนำไปสู่การผลัก "ความฉลาด" เข้าไปใน middleware มากขึ้นเรื่อย ๆ ดังที่เห็นได้จากสิ่งอย่าง enterprise service bus ต้องแน่ใจว่าคุณรู้ว่ากำลังได้อะไรมา เก็บ middleware ให้ dumb ไว้ และเก็บความฉลาดไว้ที่ endpoint
อีกแนวทางหนึ่งคือพยายามใช้ HTTP เป็นวิธี propagate event Atom เป็น specification ที่ compliant กับ REST ที่นิยาม semantics (รวมถึงสิ่งอื่น ๆ) สำหรับการ publish feed ของ resource มี client library มากมายที่ให้เราสร้างและ consume feed เหล่านี้ ดังนั้น customer service ของเราก็แค่ publish event ไปยัง feed แบบนี้ทุกครั้งที่ customer service เปลี่ยนแปลง consumer ของเราแค่ poll feed เพื่อหาการเปลี่ยนแปลง ในแง่หนึ่ง การที่เราสามารถใช้ Atom specification และ library ที่เกี่ยวข้องซ้ำได้ก็มีประโยชน์ และเรารู้ว่า HTTP รองรับการ scale ได้ดีมาก แต่การใช้ HTTP แบบนี้ไม่เหมาะกับ low latency (ซึ่ง message broker บางตัวทำได้ดีกว่า) และเรายังต้องจัดการกับความจริงที่ว่า consumer ต้องติดตามว่า message ไหนที่เห็นมาแล้วและจัดการตาราง polling ของตัวเอง
ผมเคยเห็นคนใช้เวลานานมากในการ implement พฤติกรรมต่าง ๆ เพิ่มขึ้นเรื่อย ๆ ที่คุณได้มาแบบสำเร็จรูปจาก message broker ที่เหมาะสม เพื่อให้ Atom ใช้งานได้กับบาง use case เช่น competing consumer pattern อธิบายวิธีการที่คุณเปิด worker instance หลายตัวมาแข่งกันรับ message ซึ่งใช้ได้ดีสำหรับการ scale จำนวน worker เพื่อจัดการ list ของงานที่เป็นอิสระจากกัน (เราจะกลับมาพูดถึงเรื่องนี้ใน the next chapter ) อย่างไรก็ตาม เราอยากหลีกเลี่ยงกรณีที่ worker สองตัวขึ้นไปเห็น message เดียวกัน เพราะเราจะจบลงด้วยการทำงานเดิมซ้ำมากกว่าที่จำเป็น ด้วย message broker queue มาตรฐานจะจัดการเรื่องนี้ให้ ส่วนกับ Atom ตอนนี้เราต้องจัดการ shared state ของตัวเองระหว่าง worker ทั้งหมด เพื่อพยายามลดโอกาสที่จะทำงานซ้ำ
ถ้าคุณมี message broker ที่ดีและ resilient อยู่แล้ว ให้พิจารณาใช้มันจัดการการ publish และ subscribe event ถ้าคุณยังไม่มี ลองดู Atom แต่ต้องระวัง sunk cost fallacy ถ้าคุณพบว่าตัวเองต้องการ support ที่ message broker มอบให้มากขึ้นเรื่อย ๆ ถึงจุดหนึ่งคุณอาจอยากเปลี่ยนแนวทาง
ในแง่ของสิ่งที่เราส่งจริง ๆ ผ่าน asynchronous protocol เหล่านี้ ข้อพิจารณาเดียวกันนี้ใช้ได้กับ synchronous communication ด้วย ถ้าตอนนี้คุณพอใจกับการ encode request และ response ด้วย JSON ก็ใช้มันต่อไป
What's in an Event? (Event มีอะไรอยู่ข้างใน?)
ใน Figure 4-12 เราเห็น event ถูก broadcast จาก Customer microservice เพื่อแจ้งฝ่ายที่สนใจว่ามีลูกค้าใหม่ลงทะเบียนกับระบบ microservice downstream สองตัวคือ Loyalty และ Notifications สนใจ event นี้ Loyalty microservice ตอบสนองต่อการรับ event นี้ด้วยการตั้งบัญชีให้ลูกค้าใหม่เพื่อให้พวกเขาเริ่มสะสม point ได้ ในขณะที่ Notifications microservice ส่งอีเมลต้อนรับลูกค้าที่เพิ่งลงทะเบียนสู่ความน่าอัศจรรย์ของ MusicCorp
Figure 4-12. Notifications และ Loyalty microservice ได้รับ event เมื่อลูกค้าใหม่ลงทะเบียน
ด้วย request เรากำลังขอให้ microservice ทำบางอย่าง และให้ข้อมูลที่จำเป็นสำหรับ operation ที่ร้องขอนั้นดำเนินการได้ ด้วย event เรากำลัง broadcast ข้อเท็จจริงที่ฝ่ายอื่น อาจจะ สนใจ แต่เนื่องจาก microservice ที่ยิง event ไม่สามารถและไม่ควรรู้ว่าใครรับ event เราจะรู้ได้อย่างไรว่าฝ่ายอื่นต้องการข้อมูลอะไรบ้างจาก event นั้น จริง ๆ แล้วควรมีอะไรอยู่ข้างใน event?
Just an ID (แค่ ID)
ตัวเลือกหนึ่งคือให้ event มีแค่ identifier ของลูกค้าที่เพิ่งลงทะเบียน ดังที่แสดงใน Figure 4-13 Loyalty microservice ต้องการแค่ identifier นี้เพื่อสร้าง loyalty account ที่ตรงกัน ดังนั้นมันมีข้อมูลทั้งหมดที่ต้องการแล้ว แต่ในขณะที่ Notifications microservice รู้ว่ามันต้องส่งอีเมลต้อนรับเมื่อได้รับ event ประเภทนี้ มันจะต้องการข้อมูลเพิ่มเติมเพื่อทำงาน อย่างน้อยก็ที่อยู่อีเมล และอาจรวมถึงชื่อลูกค้าเพื่อให้อีเมลมีความเป็นส่วนตัวมากขึ้น เนื่องจากข้อมูลนี้ไม่ได้อยู่ใน event ที่ Notifications microservice ได้รับ มันจึงไม่มีทางเลือกนอกจากต้องไปดึงข้อมูลนี้จาก Customer microservice ซึ่งเราเห็นได้ใน Figure 4-13
Figure 4-13. Notifications microservice ต้องขอรายละเอียดเพิ่มเติมจาก Customer microservice ที่ไม่ได้รวมอยู่ใน event
แนวทางนี้มีข้อเสียอยู่บ้าง อย่างแรก Notifications microservice ตอนนี้ต้องรู้จัก Customer microservice เพิ่ม domain coupling เข้าไปอีก แม้ domain coupling ตามที่เราพูดถึงใน Chapter 2 จะอยู่ในฝั่งที่หลวมกว่าของสเปกตรัม coupling แต่เราก็ยังอยากหลีกเลี่ยงมันเมื่อทำได้ ถ้า event ที่ Notifications microservice ได้รับมีข้อมูลทั้งหมดที่มันต้องการ callback นี้ก็ไม่จำเป็น callback จาก microservice ผู้รับยังนำไปสู่ข้อเสียใหญ่อีกอย่างหนึ่งได้ นั่นคือในสถานการณ์ที่มี microservice ผู้รับจำนวนมาก microservice ที่ยิง event อาจถูกถล่มด้วย request ผลของมัน ลองนึกภาพว่า microservice ห้าตัวต่างรับ event สร้างลูกค้าเดียวกัน และทุกตัวต้องการข้อมูลเพิ่มเติม พวกมันทั้งหมดจะต้องส่ง request ไปยัง Customer microservice ทันทีเพื่อรับสิ่งที่ต้องการ ยิ่งจำนวน microservice ที่สนใจ event หนึ่งเพิ่มขึ้น ผลกระทบของ call เหล่านี้ก็อาจมีนัยสำคัญมากขึ้น
Fully detailed events (Event ที่มีรายละเอียดครบถ้วน)
ทางเลือกอื่นที่ผมชอบมากกว่าคือใส่ทุกอย่างที่คุณยินดีจะแชร์ผ่าน API อยู่แล้วลงไปใน event ถ้าคุณยอมให้ Notifications microservice ขอที่อยู่อีเมลและชื่อของลูกค้าคนหนึ่งได้ ทำไมไม่ใส่ข้อมูลนั้นลงใน event ไปเลยตั้งแต่แรก? ใน Figure 4-14 เราเห็นแนวทางนี้ — Notifications ตอนนี้พึ่งพาตัวเองได้มากขึ้นและสามารถทำงานได้โดยไม่ต้องสื่อสารกับ Customer microservice จริง ๆ แล้วมันอาจไม่จำเป็นต้องรู้ด้วยซ้ำว่า Customer microservice มีอยู่จริง
Figure 4-14. event ที่มีข้อมูลมากขึ้นช่วยให้ microservice ผู้รับทำงานได้โดยไม่ต้องเรียกเพิ่มเติมไปยังต้นทางของ event
นอกจากข้อเท็จจริงที่ว่า event ที่มีข้อมูลมากขึ้นช่วยให้เกิด coupling ที่หลวมกว่าแล้ว event ที่มีข้อมูลมากขึ้นยังทำหน้าที่เป็น historical record ของสิ่งที่เกิดขึ้นกับ entity หนึ่ง ๆ ได้ด้วย นี่อาจช่วยคุณในการ implement ระบบ auditing หรืออาจให้ความสามารถในการสร้าง entity ขึ้นใหม่ ณ จุดเวลาที่กำหนด นั่นหมายความว่า event เหล่านี้สามารถใช้เป็นส่วนหนึ่งของ event sourcing ได้ ซึ่งเป็นแนวคิดที่เราจะสำรวจสั้น ๆ ในอีกสักครู่
แม้แนวทางนี้จะเป็นสิ่งที่ผมชอบอย่างแน่นอน แต่ก็ไม่ใช่ว่าไม่มีข้อเสีย อย่างแรก ถ้าข้อมูลที่เกี่ยวข้องกับ event มีขนาดใหญ่ เราอาจต้องกังวลเรื่องขนาดของ event message broker สมัยใหม่ (สมมติว่าคุณใช้มัน implement กลไก broadcast event ของคุณ) มี limit ขนาด message ที่ค่อนข้างใจกว้าง ขนาด message สูงสุดโดย default ใน Kafka คือ 1 MB และ RabbitMQ เวอร์ชันล่าสุดมี upper limit ตามทฤษฎีที่ 512 MB สำหรับ message เดียว (ลดลงจาก limit เดิมที่ 2 GB!) แม้ว่าจะคาดเดาได้ว่ามีปัญหาด้าน performance ที่น่าสนใจกับ message ขนาดใหญ่แบบนี้ แต่แม้แต่ 1 MB ที่ Kafka ให้เป็นขนาดสูงสุดของ message ก็ให้พื้นที่มากพอสำหรับส่งข้อมูลได้พอสมควร ท้ายที่สุดแล้ว ถ้าคุณเริ่มเข้าสู่พื้นที่ที่เริ่มกังวลเรื่องขนาดของ event ผมแนะนำแนวทางแบบ hybrid ที่ข้อมูลบางส่วนอยู่ใน event แต่ข้อมูล (ที่ใหญ่กว่า) อื่น ๆ สามารถถูก lookup ได้เมื่อจำเป็น
ใน Figure 4-14 Loyalty ไม่จำเป็นต้องรู้ที่อยู่อีเมลหรือชื่อของลูกค้า แต่ก็ยังได้รับมันผ่าน event อยู่ดี สิ่งนี้อาจนำไปสู่ความกังวลถ้าเราพยายามจำกัดขอบเขตว่า microservice ไหนเห็นข้อมูลประเภทไหนได้ เช่น ผมอาจอยากจำกัดว่า microservice ไหนเห็น personally identifiable information (หรือ PII) รายละเอียดบัตร payment หรือข้อมูลที่ sensitive คล้ายกันได้ วิธีแก้ปัญหานี้อาจเป็นการส่ง event สองประเภทที่ต่างกัน คือแบบหนึ่งที่มี PII และให้ microservice บางตัวเห็นได้ กับอีกแบบหนึ่งที่ไม่มี PII และ broadcast ได้กว้างกว่า วิธีนี้เพิ่มความซับซ้อนในการจัดการ visibility ของ event ต่าง ๆ และต้องมั่นใจว่า event ทั้งสองแบบถูกยิงจริง ๆ จะเกิดอะไรขึ้นถ้า microservice ส่ง event ประเภทแรกแล้วตายก่อนที่ event ที่สองจะถูกส่งได้?
อีกข้อพิจารณาหนึ่งคือเมื่อเราใส่ข้อมูลลงใน event แล้ว มันจะกลายเป็นส่วนหนึ่งของ contract ของเรากับโลกภายนอก เราต้องตระหนักว่าถ้าเราลบ field ออกจาก event เราอาจทำให้ฝ่ายภายนอกพัง information hiding ยังคงเป็นแนวคิดสำคัญใน event-driven collaboration ยิ่งเราใส่ข้อมูลลงใน event มากเท่าไหร่ ฝ่ายภายนอกก็จะยิ่งมีสมมติฐานเกี่ยวกับ event นั้นมากขึ้นเท่านั้น กฎทั่วไปของผมคือ ผมโอเคที่จะใส่ข้อมูลลงใน event ถ้าผมยินดีจะแชร์ข้อมูลเดียวกันนี้ผ่าน request-response API
Where to Use It (ควรใช้เมื่อไหร่)
Event-driven collaboration เหมาะสมอย่างยิ่งในสถานการณ์ที่ข้อมูลต้องการถูก broadcast และในสถานการณ์ที่คุณยินดีจะกลับด้านเจตนา การเปลี่ยนจาก model ที่บอกสิ่งอื่นว่าต้องทำอะไร มาเป็นการปล่อยให้ microservice downstream คิดหาทางเองมีความน่าสนใจอย่างมาก
ในสถานการณ์ที่คุณให้ความสำคัญกับ loose coupling มากกว่าปัจจัยอื่น ๆ event-driven collaboration ย่อมมีเสน่ห์ที่ชัดเจน
ข้อควรระวังคือมักมีแหล่งความซับซ้อนใหม่ ๆ ปรากฏขึ้นมากับ collaboration style นี้ โดยเฉพาะถ้าคุณมีประสบการณ์กับมันจำกัด ถ้าคุณไม่แน่ใจเกี่ยวกับรูปแบบ communication นี้ จำไว้ว่า microservice architecture ของเราสามารถ (และมักจะ) มี style ปฏิสัมพันธ์ผสมกันหลายแบบ คุณไม่จำเป็นต้องทุ่มสุดตัวกับ event-driven collaboration อาจเริ่มจาก event เดียวก่อนแล้วค่อยไปต่อจากตรงนั้น
ส่วนตัวผมพบว่าตัวเองมักโน้มเอียงไปทาง event-driven collaboration เกือบจะเป็นค่า default สมองของผมดูเหมือนจะถูก rewire ในแบบที่ communication ประเภทนี้ดูเป็นเรื่อง ธรรมดา สำหรับผม สิ่งนี้ไม่ได้ช่วยอะไรมากนัก เพราะมันยากที่จะอธิบาย ว่าทำไม ถึงเป็นแบบนั้น นอกจากบอกว่ามัน รู้สึก ถูกต้อง แต่นั่นก็แค่ bias ที่มีอยู่ในตัวผมเอง ผมโน้มเอียงตามธรรมชาติไปทางสิ่งที่ ผม รู้จัก จากประสบการณ์ของผมเอง มีความเป็นไปได้สูงว่าความชื่นชอบของผมต่อปฏิสัมพันธ์รูปแบบนี้ขับเคลื่อนเกือบทั้งหมดโดยประสบการณ์แย่ ๆ ในอดีตของผมกับระบบที่ coupled มากเกินไป ผมอาจแค่เป็นแม่ทัพที่สู้ศึกเดิมซ้ำแล้วซ้ำเล่าโดยไม่พิจารณาว่าครั้งนี้อาจแตกต่างจริง ๆ ก็ได้
สิ่งที่ผมจะพูดโดยไม่เอา bias ของตัวเองมาเกี่ยวข้องคือ ผมเห็นทีมจำนวนมากกว่ามากที่เปลี่ยนจาก request-response interaction มาเป็น event-driven interaction มากกว่าในทางกลับกัน