- การรีบแยกโค้ดออกเป็นไมโครเซอร์วิสในสตาร์ตอัประยะเริ่มต้น ก่อให้เกิด ประสิทธิภาพการทำงานที่ลดลงอย่างมาก และความซับซ้อนที่เพิ่มขึ้น
- สถาปัตยกรรมแบบโมโนลิทิก (ระบบเดียว) ให้การเพิ่มประสิทธิภาพเพื่อการอยู่รอดผ่านการดีพลอยที่ง่าย การปล่อยฟีเจอร์ใหม่ได้รวดเร็ว และการทำงานร่วมกันอย่างมีประสิทธิภาพ
- ไมโครเซอร์วิส ให้ประโยชน์จากการแยกออกจากกันก็ต่อเมื่อมีความต้องการด้านการขยายระบบขนาดใหญ่ เวิร์กโหลดที่หลากหลาย หรือข้อกำหนดด้านรันไทม์ที่แตกต่างกันเท่านั้น
- การแยกบริการมากเกินไป การมีรีโพซิทอรีจำนวนมาก สภาพแวดล้อมการพัฒนาในเครื่องที่ไม่เสถียร และเทคสแต็กที่ไม่สอดคล้องกัน ล้วนเชื่อมโยงไปสู่ ความเร็วที่ช้าลงและขวัญกำลังใจทีมที่ถดถอย
- สตาร์ตอัปควรเริ่มจากโมโนลิท และค่อยแยกเมื่อเกิดคอขวดที่ชัดเจนเท่านั้น ซึ่งเป็นแนวทางที่เหมาะสมที่สุด
บทนำและภูมิหลัง
- การอยู่รอด ของสตาร์ตอัปถูกกำหนดโดยการทำซ้ำอย่างรวดเร็ว การส่งมอบฟีเจอร์ใหม่ และการสร้างคุณค่าให้ผู้ใช้
- การเลือกสถาปัตยกรรมพื้นฐานของโปรเจกต์ เทคสแต็ก และภาษาโปรแกรม มีผลต่อ ความเร็วของทีม
- การนำไมโครเซอร์วิสมาใช้เร็วเกินไปอาจดูหรูหราในเชิงภาพลักษณ์ แต่ในความเป็นจริงกลับก่อให้เกิด ผลิตภาพที่ลดลง บริการที่ยังไม่เสร็จสมบูรณ์ และความซับซ้อนเกินจำเป็น
- ข้อมูล: เกิดต้นทุนในการพัฒนาหลากหลายด้าน เช่น การออร์เคสเทรตบริการ ปัญหา Docker/สคริปต์ CI/CD ที่ซ้ำซ้อน การผูกกันระหว่างบริการ ต้นทุนด้าน observability และการกระจายตัวของการทดสอบ
- แทนที่จะพุ่งไปสู่สถาปัตยกรรมที่ซับซ้อนโดยไม่จำเป็น บทความนี้เน้นความสำคัญของ สถาปัตยกรรมที่คุ้มค่าและใช้งานได้จริง
จุดแข็งของโมโนลิท
- ไม่ว่าจะเป็น SaaS หรือเพียงตัวครอบฐานข้อมูลแบบง่าย เมื่อเวลาผ่านไปแอปย่อมซับซ้อนขึ้น แต่ สถาปัตยกรรมแบบโมโนลิทมักคงความเรียบง่ายและยืดหยุ่นได้ง่ายกว่า
- ดีพลอยได้ง่าย ได้รับการสนับสนุนจากเฟรมเวิร์กยอดนิยม (Django, ASP.Net, Nest.js เป็นต้น) และได้ประโยชน์มากจากชุมชนโอเพนซอร์ส
- กรณีจริง: สตาร์ตอัปด้านอสังหาริมทรัพย์แห่งหนึ่งใช้ Laravel แบบโมโนลิทเพื่อขยายฟีเจอร์และเชื่อมต่อกับ third-party จำนวนมากได้อย่างราบรื่น
- ทำให้ทีมสามารถโฟกัสกับความต้องการทางธุรกิจและความคาดหวังของผู้ใช้ได้ โดยไม่ต้องรีบนำ อินฟราสตรักเจอร์ที่ซับซ้อน หรือการแยกเป็นไมโครเซอร์วิสเข้ามาใช้
- บทเรียน: ความกระชับของสถาปัตยกรรมช่วยให้ทีมโฟกัสกับการดีพลอยได้ดีขึ้น และตราบใดที่ไม่ล้มเหลวในการทำโมดูลภายใน ระบบก็ยังรองรับการเติบโตได้เพียงพอ
ไมโครเซอร์วิสดีที่สุดเสมอหรือไม่?
- วิศวกรจำนวนมากคิดว่า ไมโครเซอร์วิสคือคำตอบมาตรฐาน แต่ในทางปฏิบัติ มันแสดงคุณค่าได้จริงก็ต่อเมื่อมีเหตุผลพิเศษ เช่น ความต้องการด้านการขยายระบบ
- ในช่วงที่ทีมยังเล็ก ระบบยังเล็ก และการเปลี่ยนแปลงเกิดขึ้นเร็ว ผลลัพธ์กลับตรงกันข้าม คือเกิด อินฟราซ้ำซ้อน การพัฒนาในเครื่องที่ช้า และรอบการทำซ้ำที่ช้าลง
- แม้แต่บริษัทอย่าง Segment ก็เคย เปลี่ยนผ่านเพราะโครงสร้างที่ไม่มีประสิทธิภาพ
- บทเรียน: ไมโครเซอร์วิสเป็นเพียงเครื่องมือสำหรับแก้คอขวด ไม่ใช่เทมเพลตเริ่มต้นสำหรับระยะแรก
เหตุผลที่ไมโครเซอร์วิสมักล้มเหลว โดยเฉพาะในระยะเริ่มต้น
1. ขอบเขตของบริการที่กำหนดขึ้นเองโดยพลการ
- ความพยายามแบ่งบริการตาม business logic โดยอ้างอิงแนวคิด domain-driven design หรือ clean architecture มักลงเอยที่ ตรรกะจริงกับขอบเขตของบริการไม่สอดคล้องกัน
- ตัวอย่าง: การแยกผู้ใช้ การยืนยันตัวตน และสิทธิ์การเข้าถึงออกจากกัน ทำให้ความซับซ้อนในการดีพลอยและความยากของการพัฒนา API เพิ่มสูงขึ้น
- การแยกในช่วงที่ยังไม่มีคอขวดจริง ๆ จะยิ่ง ทำให้ระบบไม่เสถียรและช้าลง
- การจำลองการแยกในอนาคตด้วยแฟลกหรือท็อกเกิลภายใน และค่อย ๆ สำรวจขอบเขตที่เหมาะสมแบบเป็นธรรมชาติ มีประสิทธิภาพกว่าการรีบทำงานอินฟรา
- บทเรียน: ตัดสินใจแยกโดยอิงจากคอขวดที่เกิดขึ้นจริง ไม่ใช่จากทฤษฎี
2. รีโพซิทอรี/อินฟราที่มากเกินไป
- สไตล์โค้ด การทดสอบ การตั้งค่า เอกสาร และ CI/CD ล้วนเพิ่มขึ้นตามจำนวนบริการ
- หากใช้โครงสร้าง monorepo จะสามารถจัดการการตั้งค่าทั้งหมดจากจุดเดียว เพิ่มความสม่ำเสมอของโค้ดและประสิทธิภาพการทำงานร่วมกัน
- ในกรณีของ Node.js เครื่องมืออย่าง
nx หรือ turborepo ช่วยให้การจัดการ dependency และ build ระหว่างบริการภายใน ง่ายขึ้น
- ข้อเสียคืออาจมีความสัมพันธ์ของ dependency ที่ซับซ้อน ต้องปรับจูนประสิทธิภาพของ CI และอาจต้องใช้เครื่องมือ build ที่เร็วขึ้น
- สำหรับ ecosystem ของ Go ก็เช่นกัน ช่วงแรกอาจจัดการใน workspace เดียวก่อน แล้วค่อยพิจารณาแยกโมดูลเมื่อระบบเติบโต
- บทเรียน: ทีมเล็กสามารถประหยัดเวลาได้มากด้วย monorepo และอินฟราที่ใช้ร่วมกัน
3. สภาพแวดล้อมการพัฒนาในเครื่องที่ไม่เสถียร
- การรันในเครื่องที่ใช้เวลามากเกินไป สคริปต์ที่ซับซ้อน และ dependency ที่ต่างกันในแต่ละระบบ ล้วนทำให้ การ onboarding ช้าลงและผลิตภาพลดลง
- การขาดเอกสาร ปัญหาความเข้ากันได้ และการแก้เฉพาะทางของแต่ละ OS (เช่น สคริปต์ที่ใช้ได้เฉพาะ macOS) ล้วนเป็นอุปสรรค
- ในโปรเจกต์หนึ่ง มีการใช้ Node.js proxy เพื่อลดความซับซ้อนของ Docker และลดเวลา onboarding ของนักพัฒนา
- บทเรียน: ถ้าแอปรันได้แค่บน OS เดียว ผลิตภาพของทีมก็จะขึ้นอยู่กับความน่าเชื่อถือของแล็ปท็อปเพียงเครื่องเดียวในท้ายที่สุด
4. เทคสแต็กที่ไม่สอดคล้องกัน
- Node.js และ Python เหมาะกับการทำซ้ำอย่างรวดเร็ว แต่ในสภาพแวดล้อมแบบไมโครเซอร์วิสมักเกิดปัญหา ความไม่สอดคล้องกันระหว่าง build และ runtime บ่อยครั้ง
- Go มีข้อดีในเรื่องไบนารีแบบ static การ build ที่รวดเร็ว และความเรียบง่ายในการปฏิบัติการ
- ควรเลือกเทคสแต็กอย่างรอบคอบตั้งแต่ต้น และหากจำเป็นก็สามารถใช้โปรโตคอลอย่าง gRPC เพื่อรองรับการใช้หลายภาษาได้
- หากไม่มีความต้องการพิเศษอย่าง ML หรือ ETL การใช้หลายสแต็กมักเพิ่มแต่ความซับซ้อนโดยไม่จำเป็น
- บทเรียน: เลือกสแต็กให้เหมาะกับความเป็นจริงของทีม ไม่ใช่ความฝันที่ดูดี
5. ความซับซ้อนที่ซ่อนอยู่: การสื่อสารและการมอนิเตอร์
- ในระบบไมโครเซอร์วิส service discovery, API versioning, distributed tracing และการจัดการล็อกแบบศูนย์กลาง เป็นสิ่งจำเป็น
- การตามหาบั๊กหรือเหตุขัดข้องในโมโนลิทอาจดูจาก stack trace เดียวได้ แต่ในระบบกระจายจะซับซ้อนกว่ามาก
- หากจะทำให้ถูกต้อง จำเป็นต้องนำเครื่องมือเฉพาะทางอย่าง OpenTelemetry มาใช้ และสร้าง observability stack ขึ้นมา
- ต้องตระหนักว่าระบบกระจายคือการยอมรับภาระลงทุนด้านวิศวกรรมเพิ่มเติมโดยตรง
สถานการณ์ที่ไมโครเซอร์วิสมีประโยชน์จริง
- การแยกเวิร์กโหลด: การแยกงาน asynchronous บางประเภท เช่น การประมวลผลภาพหรือ OCR ออกมา จะช่วยให้มีประสิทธิภาพมากขึ้น
- ความต้องการในการขยายระบบที่ไม่เท่ากัน: หากเว็บ API และ ML workload มีข้อกำหนดด้านฮาร์ดแวร์และการปฏิบัติการต่างกัน ก็ควรแยกออกจากกัน
- ต้องการรันไทม์ที่ต่างออกไป: องค์ประกอบที่ไม่เข้ากันกับรันไทม์หลักของแอป เช่น โค้ด C++ แบบ legacy ควรถูกเก็บไว้เป็นบริการแยก
- จากกรณีขององค์กรวิศวกรรมขนาดใหญ่ (เช่น Uber) จะเห็นว่าเหมาะสมก็ต่อเมื่อมี ความจำเป็นเชิงองค์กรที่ชัดเจน และมีความสามารถในการปฏิบัติการที่สุกงอมแล้ว
- แม้ในทีมเล็ก บางครั้งการแยกก็อาจใช้ได้จริง เช่น บริการวิเคราะห์ภายนอกที่จัดการได้ไม่ยุ่งยาก
- บทเรียน: ควรเลือกใช้เฉพาะกับเวิร์กโหลดที่มีประโยชน์จากการแยกอย่างชัดเจนในทางปฏิบัติเท่านั้น
คู่มือภาคสนามสำหรับสตาร์ตอัป
- เริ่มต้นด้วยโมโนลิท และใช้เฟรมเวิร์กที่พิสูจน์แล้วเพื่อโฟกัสกับงานหลัก
- รีโพซิทอรีเดียว ให้ประโยชน์กับทีมระยะเริ่มต้นมากกว่า ทั้งในด้านประสิทธิภาพการดำเนินงาน การจัดการ และความปลอดภัย
- การทำให้สภาพแวดล้อมการพัฒนาในเครื่องเรียบง่าย เป็นเรื่องสำคัญ และหากยังยาก ควรมีเอกสารหรือวิดีโอแนะนำอย่างละเอียด
- ควร ลงทุนกับ CI/CD ตั้งแต่เนิ่น ๆ เพื่อทำงานซ้ำให้เป็นอัตโนมัติและลดภาระทางจิตใจของทีม
- แยกเฉพาะเมื่อมีคอขวดที่ชัดเจนเท่านั้น และหากยังไม่มี ก็ควรโฟกัสกับการทำโมดูลภายในโมโนลิทและเสริมการทดสอบให้แข็งแรง
- เป้าหมายสูงสุดคือการรักษาความเร็วในการพัฒนา
- บทเรียน: เริ่มจากความเรียบง่าย แล้วค่อยสเกลตามความจำเป็นของการแยก
ถ้าจำเป็นต้องใช้ไมโครเซอร์วิสจริง ๆ
- ประเมินเทคสแต็กและลงทุนกับเครื่องมือด้านประสบการณ์นักพัฒนา: ต้องมีระบบอัตโนมัติในแต่ละบริการ สคริปต์ที่ชัดเจน และเครื่องมือจัดการการดีพลอยแบบรวมศูนย์
- กำหนดโปรโตคอลการสื่อสารระหว่างบริการที่เชื่อถือได้และมีมาตรฐาน: ต้องเข้าใจงานเพิ่มเติมที่เกี่ยวข้อง เช่น ความสม่ำเสมอของ message schema การทำเอกสาร และการจัดการข้อผิดพลาด
- ทำให้โครงสร้างการทดสอบเสถียร: การทดสอบระดับ unit, integration และ E2E ต้องขยายตามการแยกบริการได้อย่างเหมาะสม
- พิจารณาใช้ไลบรารีร่วม: โค้ดส่วนกลางสำหรับ observability และการสื่อสารควรถูกจำกัดให้เล็กที่สุด เพื่อหลีกเลี่ยงการ rebuild ทุกบริการบ่อยเกินไป
- ควรเริ่มต้น observability ตั้งแต่เนิ่น ๆ: อย่างน้อยควรมี structured JSON logs และ correlation ID เป็นเครื่องมือพื้นฐานในการล็อก
- สรุปคือ เมื่อยอมรับความซับซ้อนแล้ว ก็ต้องออกแบบระบบที่สามารถจัดการมันได้อย่างจริงจังและรอบด้าน
บทสรุป
- การนำไมโครเซอร์วิสมาใช้แบบรีบร้อนจะเหลือไว้เพียงภาระ ดังนั้นควรให้ความสำคัญกับความเรียบง่ายเป็นอันดับแรก
- อย่าแยกโดยไม่มีจุดเจ็บปวดที่ชัดเจน และควรเพิ่ม ความซับซ้อนให้น้อยที่สุดเท่าที่จำเป็น เพื่อการอยู่รอดและการเติบโต
- ต้องอยู่รอดให้ได้ก่อน แล้วค่อยขยายในภายหลัง
10 ความคิดเห็น
โดยรวมแล้วผมเห็นด้วยกับเนื้อหาในต้นฉบับ
ผมคิดว่านี่เป็นเรื่องของประสบการณ์ขององค์กร
ลองนึกภาพจากการขายอาหารด้วยฟู้ดทรักแล้วค่อย ๆ เติบโตกลายเป็นร้านอาหารดู
หากจะคำนึงถึงการแบ่งงานและความเชี่ยวชาญเฉพาะทางตั้งแต่แรก ประสบการณ์ของผู้มีส่วนได้ส่วนเสียยังไม่เพียงพออย่างยิ่ง
ผมคิดว่าสตาร์ทอัพควรเลือกวิธีที่ใช้ต้นทุนน้อยกว่าเพื่อยืดเวลาการอยู่รอดของธุรกิจ ไมโครเซอร์วิสไม่ได้ถูกเลยจริง ๆ เมื่อเอาไปใช้ในภาคปฏิบัติจะก่อให้เกิดต้นทุนค่อนข้างมาก ถ้าเป็นไปได้ การออกแบบสถาปัตยกรรมให้เหมาะกับบริการของบริษัทเองน่าจะเป็นวิธีที่ได้ผลลัพธ์ใกล้เคียงกันด้วยต้นทุนที่ต่ำกว่า
ไม่ได้หมายความว่าไมโครเซอร์วิสไม่ดี แต่มันเป็นโมเดลที่ต้องใช้ต้นทุนสูง
ผมคิดว่าแค่มีโมโนลิทิกแบบ synchronous ล้วน กับโมโนลิทิกแบบ asynchronous ล้วน แค่ 2 แบบก็เพียงพอแล้ว... ผมมองว่าการนำ microservices มาใช้นั้นสุดท้ายขึ้นอยู่กับขนาดของตารางที่ต้องจัดการด้วย DB ถ้าจำนวนตารางเยอะและซับซ้อนแบบเกินเหตุ ก็ค่อยต้องกังวลเรื่อง MSA แต่ถ้าเรียบง่าย โมโนลิทิกนี่แหละเหมาะที่สุด
เมื่อคลื่นทั้งหมดนี้ผ่านพ้นไป คนรุ่นหลังจะจดจำยุคสมัยนี้อย่างไร
ตอนนั้นก็เป็นอีกระลอกคลื่นของตอนนั้น...
ผมคิดว่าในสตาร์ตอัป ไมโครเซอร์วิสก็มีข้อดีอยู่มากเหมือนกัน อย่างแรกเลยคือข้อดีของการใช้ monorepo นั้นผมแนะนำมากจริง ๆ
ผมเห็นด้วยว่าในยุคการพัฒนา AI การออกแบบให้เป็นหน่วยเล็ก ๆ และมีความรับผิดชอบเดียวเป็นสิ่งจำเป็น
มีพูดไว้เล็กน้อยในคอมเมนต์เหมือนกัน แต่สาย beam/otp นี่ค่อนข้างยืดหยุ่นและดีทีเดียวครับ ในกรณีของ Gleam นั้น ด้วยการผสานไวยากรณ์ที่ดีของทั้ง go และ rust เข้ากับเสถียรภาพของ beam เลยกลายเป็นภาษาที่น่าประทับใจมากพอสมควร อยากลองค่อย ๆ เอามาใช้กับโปรเจ็กต์ขนาดเล็กดูเหมือนกันครับ
ถ้าแบ่งทีมยิบย่อยเกินไป แม้แต่การมารวมตัวกันเพื่อแลกเปลี่ยนความเห็นก็จะกลายเป็นงานใหญ่มหาศาล
ความเห็นจาก Hacker News