What Makes a Good Microservice Boundary? (อะไรที่ทำให้ขอบเขตของ Microservice ดี)

ก่อนที่ ทีมงานของ MusicCorp จะรีบร้อนสร้าง service ต่อ service เพื่อพยายามส่งเทป eight-track ไปให้ทุกคน เรามาชะลอความเร็วและพูดถึงแนวคิดสำคัญที่เราต้องคำนึงถึงกันก่อน เราต้องการให้ microservices ของเราสามารถเปลี่ยนแปลง deploy และปล่อยฟังก์ชันการทำงานให้ผู้ใช้ได้อย่างเป็นอิสระจากกัน ความสามารถในการเปลี่ยนแปลง microservice หนึ่งโดยไม่กระทบกับอีกตัวหนึ่งนั้นสำคัญมาก แล้วเราต้องคำนึงถึงอะไรบ้างเมื่อคิดเรื่องการกำหนดขอบเขตรอบ ๆ มัน

โดยพื้นฐานแล้ว microservices ก็เป็นเพียงอีกรูปแบบหนึ่งของการแบ่งโมดูล (modular decomposition) เพียงแต่มีการสื่อสารผ่านเครือข่ายระหว่างโมเดลต่าง ๆ พร้อมกับความท้าทายที่ตามมา โชคดีที่สิ่งนี้หมายความว่าเราสามารถอาศัยองค์ความรู้เดิมในด้านซอฟต์แวร์แบบโมดูลและ structured programming มาช่วยชี้แนะเราได้ว่าจะกำหนดขอบเขตอย่างไร ด้วยแนวคิดนี้ เรามาดูกันลึกขึ้นถึงแนวคิดสำคัญสามอย่างที่เราได้แตะไปบ้างแล้วใน Chapter 1 และเป็นสิ่งสำคัญที่ต้องเข้าใจเมื่อพูดถึงสิ่งที่ทำให้ขอบเขตของ microservice ดี นั่นคือ information hiding, cohesion และ coupling

Information Hiding (การซ่อนข้อมูล)

Information hiding เป็นแนวคิดที่พัฒนาขึ้นโดย David Parnas เพื่อหาวิธีที่มีประสิทธิภาพที่สุดในการกำหนดขอบเขตของโมดูล 1 Information hiding อธิบายถึงความต้องการที่จะซ่อนรายละเอียดให้มากที่สุดเท่าที่จะทำได้ไว้หลังขอบเขตของโมดูล (หรือในกรณีของเรา คือ microservice) Parnas มองไปที่ประโยชน์ที่โมดูลควรจะมอบให้เราได้ในทางทฤษฎี นั่นคือ

Improved development time (เวลาในการพัฒนาที่ดีขึ้น)

การทำให้โมดูลถูกพัฒนาแยกจากกันได้ ช่วยให้ทำงานคู่ขนานกันได้มากขึ้นและลดผลกระทบจากการเพิ่มนักพัฒนาเข้ามาในโปรเจกต์

Comprehensibility (ความเข้าใจง่าย)

แต่ละโมดูลสามารถพิจารณาและทำความเข้าใจแยกจากกันได้ ซึ่งทำให้เข้าใจภาพรวมของระบบทั้งหมดได้ง่ายขึ้น

Flexibility (ความยืดหยุ่น)

โมดูลสามารถเปลี่ยนแปลงแยกจากกันได้ ทำให้เปลี่ยนฟังก์ชันการทำงานของระบบได้โดยไม่ต้องเปลี่ยนโมดูลอื่น นอกจากนี้ยังสามารถนำโมดูลมาผสมผสานกันในรูปแบบต่าง ๆ เพื่อสร้างฟังก์ชันการทำงานใหม่ ๆ ได้ด้วย

รายการคุณสมบัติที่พึงประสงค์นี้สอดคล้องเป็นอย่างดีกับสิ่งที่เรากำลังพยายามบรรลุด้วยสถาปัตยกรรม microservice และจริง ๆ แล้วตอนนี้ผมมองว่า microservices ก็เป็นเพียงอีกรูปแบบหนึ่งของสถาปัตยกรรมแบบโมดูล Adrian Colyer ได้ย้อนกลับไปดูงานเขียนหลายชิ้นของ David Parnas ในยุคนั้นและวิเคราะห์เทียบกับ microservices ซึ่งบทสรุปของเขาน่าอ่านมาก 2

ความจริงที่ Parnas สำรวจตลอดงานเขียนของเขาคือ การมีโมดูลไม่ได้แปลว่าคุณจะบรรลุผลลัพธ์เหล่านี้ได้จริง มันขึ้นอยู่กับ วิธีการ ที่ขอบเขตของโมดูลถูกกำหนดขึ้นมาเป็นอย่างมาก จากงานวิจัยของเขาเอง information hiding เป็นเทคนิคสำคัญที่ช่วยดึงประโยชน์สูงสุดจากสถาปัตยกรรมแบบโมดูล และด้วยมุมมองยุคปัจจุบัน แนวคิดเดียวกันนี้ก็ใช้ได้กับ microservices เช่นกัน

จากงานเขียนอีกชิ้นหนึ่งของ Parnas 3 เรามีประโยคเด็ดนี้

การเชื่อมโยงระหว่างโมดูลก็คือข้อสมมติฐานที่โมดูลเหล่านั้นมีต่อกัน

การลดจำนวนข้อสมมติฐานที่โมดูล (หรือ microservice) หนึ่งมีต่ออีกโมดูลหนึ่งลง จะส่งผลโดยตรงต่อการเชื่อมโยงระหว่างกัน การรักษาจำนวนข้อสมมติฐานให้น้อยจะทำให้มั่นใจได้ง่ายขึ้นว่าเราสามารถเปลี่ยนโมดูลหนึ่งได้โดยไม่กระทบกับโมดูลอื่น ถ้านักพัฒนาที่กำลังเปลี่ยนแปลงโมดูลเข้าใจอย่างชัดเจนว่าโมดูลนั้นถูกใช้งานโดยฝ่ายอื่นอย่างไร นักพัฒนาก็จะสามารถเปลี่ยนแปลงได้อย่างปลอดภัยมากขึ้น โดยที่ผู้เรียกใช้งานฝั่ง upstream ไม่ต้องเปลี่ยนแปลงตามไปด้วย

เรื่องนี้ก็ใช้ได้กับ microservices เช่นกัน เพียงแต่เรายังมีโอกาส deploy microservice ที่เปลี่ยนแปลงนั้นได้โดยไม่ต้อง deploy อย่างอื่นเพิ่มเติม ซึ่งอาจกล่าวได้ว่ายิ่งขยายคุณสมบัติที่พึงประสงค์สามข้อของ Parnas ให้เด่นชัดขึ้นไปอีก นั่นคือเวลาในการพัฒนาที่ดีขึ้น ความเข้าใจง่าย และความยืดหยุ่น

ผลกระทบของ information hiding ปรากฏออกมาในหลากหลายรูปแบบ และผมจะหยิบยกประเด็นนี้มาพูดถึงอีกตลอดทั้งเล่ม

Cohesion (ความเกาะกลุ่มกัน)

หนึ่ง ในคำจำกัดความที่กระชับที่สุดที่ผมเคยได้ยินสำหรับอธิบาย cohesion คือ "โค้ดที่เปลี่ยนแปลงไปด้วยกัน ก็ควรอยู่ด้วยกัน" 4 สำหรับจุดประสงค์ของเรา นี่เป็นคำจำกัดความที่ดีทีเดียว อย่างที่เราได้พูดถึงไปแล้ว เรากำลังปรับสถาปัตยกรรม microservice ของเราให้เหมาะกับการเปลี่ยนแปลงฟังก์ชันทางธุรกิจได้ง่าย ดังนั้นเราจึงต้องการให้ฟังก์ชันการทำงานถูกจัดกลุ่มในลักษณะที่ทำให้เราเปลี่ยนแปลงได้ในจุดที่น้อยที่สุดเท่าที่จะเป็นไปได้

เราต้องการให้พฤติกรรมที่เกี่ยวข้องกันอยู่ด้วยกัน และพฤติกรรมที่ไม่เกี่ยวข้องกันอยู่แยกจากกัน ทำไมน่ะหรือ ก็เพราะถ้าเราต้องการเปลี่ยนพฤติกรรมบางอย่าง เราอยากเปลี่ยนมันในที่เดียวและปล่อย (release) การเปลี่ยนแปลงนั้นให้เร็วที่สุดเท่าที่จะทำได้ ถ้าเราต้องเปลี่ยนพฤติกรรมนั้นในหลาย ๆ ที่ เราก็ต้อง release service หลายตัว (อาจจะพร้อมกันด้วยซ้ำ) เพื่อส่งมอบการเปลี่ยนแปลงนั้น การเปลี่ยนแปลงในหลาย ๆ ที่นั้นช้ากว่า และการ deploy หลาย service พร้อมกันก็มีความเสี่ยง เราจึงอยากหลีกเลี่ยงทั้งสองอย่างนี้

ดังนั้นเราจึงต้องการหาขอบเขตภายใน problem domain ของเราที่ช่วยให้พฤติกรรมที่เกี่ยวข้องกันอยู่ในที่เดียวกัน และสื่อสารกับขอบเขตอื่น ๆ ให้หลวมที่สุดเท่าที่จะทำได้ ถ้าฟังก์ชันการทำงานที่เกี่ยวข้องกันกระจายอยู่ทั่วทั้งระบบ เราจะบอกว่า cohesion นั้นอ่อน ในขณะที่สำหรับสถาปัตยกรรม microservice ของเรา เรากำลังมุ่งไปสู่ cohesion ที่แข็งแรง

Coupling (การเชื่อมโยงกัน)

เมื่อ service ต่าง ๆ มีการ couple กันแบบหลวม (loosely coupled) การเปลี่ยนแปลง service หนึ่งก็ไม่ควรต้องทำให้อีก service หนึ่งต้องเปลี่ยนตาม ประเด็นสำคัญของ microservice ก็คือความสามารถในการเปลี่ยนแปลง service หนึ่งและ deploy มันได้โดยไม่ต้องเปลี่ยนส่วนอื่นของระบบ นี่เป็นเรื่องที่สำคัญมากจริง ๆ

อะไร ที่ทำให้เกิด tight coupling ล่ะ ความผิดพลาดคลาสสิกอย่างหนึ่งคือการเลือกรูปแบบการเชื่อมต่อ (integration style) ที่ผูก service หนึ่งเข้ากับอีก service หนึ่งอย่างแน่นหนา จนการเปลี่ยนแปลงภายใน service หนึ่งทำให้ผู้ใช้งาน (consumer) ต้องเปลี่ยนตามไปด้วย

Service ที่ couple กันแบบหลวมจะรู้เกี่ยวกับ service ที่มันทำงานร่วมด้วยเท่าที่จำเป็นเท่านั้น นี่ยังหมายความว่าเราน่าจะต้องการจำกัดจำนวนประเภทการเรียกที่แตกต่างกันจาก service หนึ่งไปยังอีก service หนึ่งด้วย เพราะนอกจากปัญหาเรื่องประสิทธิภาพที่อาจเกิดขึ้นแล้ว การสื่อสารที่ถี่เกินไป (chatty communication) ก็อาจนำไปสู่ tight coupling ได้เช่นกัน

อย่างไรก็ตาม coupling มีอยู่หลายรูปแบบ และผมเคยเห็นความเข้าใจผิดหลายอย่างเกี่ยวกับธรรมชาติของ coupling ในบริบทของสถาปัตยกรรมแบบ service-based ด้วยเหตุนี้ ผมคิดว่ามันสำคัญที่เราจะสำรวจหัวข้อนี้ให้ละเอียดขึ้น ซึ่งเราจะทำกันในไม่ช้านี้

The Interplay of Coupling and Cohesion (ความสัมพันธ์ระหว่าง Coupling และ Cohesion)

อย่าง ที่เราได้แตะไปแล้ว แนวคิดเรื่อง coupling และ cohesion นั้นมีความเกี่ยวข้องกันอย่างชัดเจน ถ้าพูดตามหลักตรรกะแล้ว ถ้าฟังก์ชันการทำงานที่เกี่ยวข้องกันกระจายอยู่ทั่วระบบของเรา การเปลี่ยนแปลงฟังก์ชันนี้ก็จะส่งผลกระทบข้ามขอบเขตเหล่านั้น ซึ่งหมายถึง coupling ที่แน่นขึ้น กฎของ Constantine ซึ่งตั้งชื่อตาม Larry Constantine ผู้บุกเบิกด้าน structured design สรุปเรื่องนี้ไว้อย่างกระชับว่า

โครงสร้างจะมีเสถียรภาพ ถ้า cohesion แข็งแรงและ coupling ต่ำ 5

แนวคิดเรื่องเสถียรภาพตรงนี้สำคัญกับเรามาก เพื่อให้ขอบเขตของ microservice ของเราสามารถส่งมอบคำมั่นสัญญาเรื่อง independent deployability ทำให้เราทำงานกับ microservices แบบขนานกันได้ และลดการประสานงานระหว่างทีมที่ทำงานกับ service เหล่านี้ เราจำเป็นต้องมีเสถียรภาพในระดับหนึ่งในตัวขอบเขตเอง ถ้า contract ที่ microservice หนึ่งเปิดเผยออกมาเปลี่ยนแปลงตลอดเวลาในแบบที่ไม่ backward-compatible สิ่งนี้ก็จะทำให้ผู้ใช้งานฝั่ง upstream ต้องเปลี่ยนแปลงตามอยู่เรื่อย ๆ

Coupling และ cohesion มีความเกี่ยวข้องกันอย่างมาก และในระดับหนึ่งก็อาจกล่าวได้ว่าเป็นสิ่งเดียวกัน เพราะทั้งสองแนวคิดต่างอธิบายความสัมพันธ์ระหว่างสิ่งต่าง ๆ Cohesion นั้นใช้กับความสัมพันธ์ระหว่างสิ่งต่าง ๆ ที่อยู่ ภายใน ขอบเขต (ในบริบทของเราคือ microservice) ในขณะที่ coupling อธิบายความสัมพันธ์ระหว่างสิ่งต่าง ๆ ที่อยู่ ข้าม ขอบเขต ไม่มีวิธีการจัดระเบียบโค้ดของเราที่ ดีที่สุด แบบตายตัว coupling และ cohesion เป็นเพียงวิธีหนึ่งในการอธิบายข้อแลกเปลี่ยน (trade-off) ต่าง ๆ ที่เราเลือกเมื่อจัดกลุ่มโค้ด และเหตุผลเบื้องหลัง สิ่งที่เราทำได้คือพยายามหาสมดุลที่เหมาะสมระหว่างสองแนวคิดนี้ ที่เหมาะสมที่สุดกับบริบทและปัญหาที่คุณกำลังเผชิญอยู่ในขณะนั้น

อย่าลืมว่าโลกไม่ได้หยุดนิ่ง เป็นไปได้ว่าเมื่อความต้องการของระบบคุณเปลี่ยนไป คุณอาจพบเหตุผลที่ต้องทบทวนการตัดสินใจของคุณใหม่ บางครั้งบางส่วนของระบบคุณอาจกำลังเผชิญกับการเปลี่ยนแปลงมากจนเสถียรภาพเป็นไปไม่ได้เลย เราจะดูตัวอย่างเรื่องนี้ใน Chapter 3 เมื่อผมเล่าประสบการณ์ของทีมพัฒนาผลิตภัณฑ์เบื้องหลัง Snap CI ให้ฟัง