คิดให้ดีก่อนลงทุนกับ shared component ใน microservices
https://www.monkeyuser.com/2021/reusable-components/
ในยุคที่การพัฒนา software ในระบบใหญ่ที่ซับซ้อน การนำแนวคิดของ microservices มาใช้งานถือเป็นเรื่องปกติกันไปแล้ว หนึ่งในนั้นคือการแยกส่วนของการทำงานออกเป็นส่วนย่อย ๆ เพื่อให้แต่ละทีมพัฒนาทำงานแยกกันได้ง่ายขึ้น ดูเหมือนจะเป็นไอเดียที่ดี แต่ปัญหาหนึ่งที่มักตามมาคือ
“แล้ว code ที่ share กันระหว่าง microservices จะทำยังไง?”
หนึ่งในคำตอบที่หลายคน (รวมถึงเราด้วย ฮ่า ๆๆ) คิดถึงคือ
“ก็ทำ library สิ”
“ก็ทำ shared service สิ”
จากประสบการณ์ในการปรับปรุงระบบ microservices ให้ดีขึ้น เลยอยากเขียน blog นี้เพื่อจะบอกว่าขอให้คิดอีกรอบก่อนที่จะเริ่มสร้าง shared component ในระบบ microservices เพราะจริง ๆ แล้วมันมักจะซับซ้อนกว่าที่คิดไว้เสมอ
Shared component คืออะไร
Shared component คือส่วนของงานที่ทำขึ้นที่รวมข้อมูลและ function ที่ใช้ร่วมกันเข้าไว้ด้วยกันเพื่อสามารถรองรับการใช้งานกับหลาย ๆ ส่วนในระบบมาเรียกใช้งาน โดยที่ยังสามารถ configure ให้รองรับกับ use case ที่แตกต่างกันบางส่วนนิด ๆ หน่อย ๆ ได้ เวลา application ต้องการใช้งาน library ก็สามารถหยิบไปใช้ใน refer ใน code ของ application เองผ่าน interface รูปแบบต่าง ๆ เช่น library, SDK, service ได้เลย
Use case ในการใช้งาน shared component โดยมากจะเกี่ยวข้องกับ cross-cutting concern หรือพวก requirement ที่มักมีร่วมกันในหลาย ๆ ส่วนของระบบ เช่น authentication & authorisation, logging ไปจนถึงการเชื่อมต่อ external dependencies ที่ซับซ้อน ไปจนถึง utilities บางอย่าง เป็นต้น
ประโยชน์ของ shared component คือ
- ช่วยให้การพัฒนา software โดยเฉพาะการสร้างส่วนงานใหม่ในระบบหรือปรับแก้ cross-cutting concern เร็วขึ้น เนื่องจากมันลดการซ้ำซ้อนของ code ลงไป (ตามหลักการ Don’t Repeat Yourself)
- สามารถควบคุม coding standards ได้ง่ายขึ้น ทำให้ code ที่เรียกใช้ component ในทีมต่าง ๆ มีความเหมือนคล้ายกัน ไม่ต้องสลับสมองให้ปวดหัวเล่น
ปัญหาที่ตามมาของการใช้ shared component
แม้ shared Library จะช่วยแก้ปัญหาหลายอย่าง แต่ก็เพิ่มปัญหาใหม่ ๆ เข้ามา เช่น
-
การทำ versioning เนื่องจากแต่ละส่วนงานอาจจะต้องการเรียกใช้ shared component คนละ version กัน ทำให้ทีมพัฒนาต้อง maintain หลาย version พร้อมกัน แล้วถ้าเกิด bug ใน version นึง แล้วอาจจะเจอในอีก version นึงด้วย ทำให้การ release บันเทิงเข้าไปกันใหญ่ นอกจากนั้นแล้วยังมีโอกาสเกิด transitive dependencies ขึ้นซึ่งมันคือนรกบนดินที่แท้จริง ยกตัวอย่างให้เห็นภาพ
- Product P ใช้ Library A (v1) ที่ใช้ Library B (v1)
- Product P ใช้ Library B (v2)
- อาจทำให้เกิดเหตุการณ์แปลก ๆ เช่น Product P ใช้งาน Library B ผิด version ทำให้ behavior ไม่สอดคล้องกันขึ้นอยู่กับว่า Library B ที่ถุกเรียกใช้มาจาก version ไหน
-
ความซับซ้อนในการปรับแก้สูงขึ้นในระยะยาว เนื่องจากมีหลาย ๆ ส่วนในระบบมาเรียกใช้งาน (แต่ถ้าไม่มีใครมาเรียกใช้อันนี้ก็จะเป็นอีกปัญหานึง ฮ่า ๆๆ) ทุก ๆ ครั้งที่มีการ upgrade จากการแก้ bug หรือเพิ่ม feature ก็ดีก็เพิ่มความซับซ้อนในการทดสอบเนื่องจากต้องทดสอบให้ครอบคลุมกับทุก use case + configuration ต่าง ๆ นา ๆ และการสืบค้นว่าส่วนไหนที่พังก็ซับซ้อนขึ้นไปอีกนิดหน่อย
แล้วจะตัดสินใจอย่างไรดี
ต้องขอบอกไว้ก่อนเลยว่า แม้ว่าเราจะเห็นการใช้งาน shared component ที่พังมากกว่าปัง แต่เราก็ไม่ได้บอกว่าอย่าสร้าง shared component นะ แค่ก่อนจะเริ่มอ่ะอย่างน้อยนี่คือสิ่งที่ควรคำนึงถึง
- Component ต้องเอาไว้ตอบโจทย์ปัญหาอันเดียวเท่านั้น หลีกเลี่ยงการทำ component เดียวเพื่อแก้ปัญหาทุกอย่าง (มักจะเจอในชื่อว่า utils หรือ common) เพราะมันจะยากต่อการดูแลและใช้งานในระยะยาว
- ลงทุนกับการทำความเข้าใจว่าแต่ละทีมเอาไปใช้งานอย่างไร สิ่งที่เราสร้างจะถูกใช้โดยทีมอื่น ๆ ที่อาจไม่ได้มีความรู้เชิงลึกเกี่ยวกับมัน ดังนั้นควรทำให้ใช้ง่ายที่สุด จะดีมากหากเราลงทุนกับการสร้างเอกสารประกอบที่เข้าใจง่าย
- ลงทุนกับการสื่อสารร่วมกันระหว่างทีมพัฒนา shared component กับทีมที่เรียกใช้งาน จากกฎ Conway’s law ซึ่งกล่าวถึง architecture รวมถึงแนวทางการส่งมอบงาน มันจะเป็นรูปเป็นร่างได้มันก็ต้องมาจากรูปแบบขององค์กรที่ตอบโจทย์ด้วย ถ้าทั้ง 2 ทีมทำงานกันเป็น silo โอกาสที่มันจะให้โทษมากกว่าให้คุณก็มากขึ้น
- ควบคุม versioning ให้ดี ไม่ว่าจะเป็นการทำ backward compatibility หรือตั้ง process ในการ retire หรือ deprecate version เก่า โดยนำ “คน” หรือทีมเป็นส่วนนึงในการสร้าง process ขึ้นมา เพราะการให้ทุกคนทุกทีมมา upgrade หรือ retire นี่ไม่ง่ายอย่างที่คิด โดยเฉพาะถ้า shared component นั้นไม่ได้มีผลกระทบตรง ๆ ต่อรายได้ขององค์กร
- หากลงทุนในรูปแบบ shared API, service ให้ระวังเรื่อง network latency ที่สูงขึ้นและ point of failure ที่เพิ่มขึ้น
- ตรวจสอบ dependency อย่างละเอียดเสมอ โดยเฉพาะ transitive dependencies ที่กล่าวไป
- อย่าเพิ่งรีบ ถ้าเรายังไม่มีความจำเป็นจริง ๆ เช่น มันลง lock กับทุก use case ที่กล่าวมา หรือมันเริ่มที่จะทำให้การพัฒนามันช้าลงเนื่องจากแต่ละทีมต้องมาปรับแก้อะไรที่มันซ้ำ ๆ กันอย่างมีนัยสำคัญ อย่าสร้างไว้เผื่ออนาคต เจ็บกันมาเยอะแล้วกับไอคำว่า “In the future, …”
ขอย้ำอีกครั้งว่า การสร้าง shared component ไม่ใช่เรื่องผิด แต่ต้องชั่งน้ำหนักระหว่าง ความเรียบง่าย กับ ต้นทุนการดูแลในระยะยาว ให้ดีก่อนตัดสินใจ