ทุกทีมวิศวกรรมต้องการระบบที่เติบโตได้โดยไม่สร้างปัญหา เราจินตนาการถึงทราฟฟิกที่เพิ่มขึ้นอย่างราบรื่น เซิร์ฟเวอร์ที่ทำงานได้อย่างลื่นไหล และรายได้ที่ค่อยๆ ขยับสูงขึ้น แต่แล้วความจริงก็ตบหน้าเรา เมื่อแคมเปญการตลาดที่กลายเป็นไวรัลส่งผู้ใช้งานจำนวนมหาศาลเข้ามา ฐานข้อมูลเกิดการล็อก และใครบางคนต้องรีสตาร์ทบริการอย่างบ้าคลั่งตอนตีสาม สัญชาตญาณแรกคือการโทษเครื่องมือ เราบอกตัวเองว่าเราต้องการคอร์ที่มากขึ้น ดิสก์ที่เร็วขึ้น หรือเลเยอร์การทำแคช (caching layer) อีกชั้น แต่การเติบโตไม่ได้มาจากฮาร์ดแวร์ แต่มันมาจากโครงสร้าง หากรากฐานของคุณไม่สามารถกระจายน้ำหนักได้ ผู้ใช้ใหม่ทุกคนจะกลายเป็นภาระแทนที่จะเป็นชัยชนะ

ทำไมเครื่องมือจึงไม่สามารถช่วยรากฐานที่พังทลายได้

คุณสามารถเปิดใช้งาน cloud instances เป็นร้อยรายการ เพิ่ม load balancers ระหว่างภูมิภาคต่างๆ และทำแคชให้กับ static asset ทุกอย่างใน global content delivery network สิ่งเหล่านี้คือตัวคูณประสิทธิภาพ (force multipliers) แต่การนำศูนย์ไปคูณก็ยังคงได้ศูนย์อยู่ดี แอปพลิเคชันแบบ monolithic ที่มี dependencies พันกันยุ่งเหยิงจะสำลักน้ำหนักของตัวเอง ไม่ว่าจะมีทรัพยากรฮาร์ดแวร์มากแค่ไหนมารองรับก็ตาม

ลองนึกภาพร้านค้าออนไลน์ที่แคตตาล็อกสินค้า การประมวลผลการชำระเงิน และการยืนยันตัวตนผู้ใช้ ทั้งหมดรวมอยู่ใน codebase เดียวกัน เมื่อขั้นตอนการ checkout ช้าลง เว็บไซต์ทั้งเว็บก็อืดตามไปด้วย หน้าล็อกอินเริ่มติดขัด ประสบการณ์การท่องเว็บแย่ลง คุณไม่สามารถ scale เฉพาะจุดที่เป็นคอขวดได้โดยไม่ scale ส่วนอื่นๆ ตามไปด้วย ซึ่งนั่นทั้งสิ้นเปลือง ไร้ประสิทธิภาพ และเปราะบาง สุดท้ายคุณต้องจ่ายค่า compute power ที่ไม่ได้สร้างประโยชน์ให้ใคร ในขณะที่ผู้ใช้ต้องรอหน้าเว็บที่ควรจะโหลดขึ้นมาได้ทันที

Architecture คือคำตอบสำหรับกับดักนี้ มันคือโครงร่างที่มองไม่เห็นซึ่งเป็นตัวกำหนดว่าเครื่องมือของคุณจะช่วยหรือจะทำร้ายคุณ

ความหมายที่แท้จริงของสถาปัตยกรรมที่แข็งแกร่ง

สถาปัตยกรรมที่แข็งแกร่งเป็นเพียงแผนการว่าความรับผิดชอบต่างๆ ควรอยู่ที่ไหน มันจะตั้งคำถามที่ตอบยากตั้งแต่เนิ่นๆ เช่น จะเกิดอะไรขึ้นเมื่อส่วนใดส่วนหนึ่งพัง? คุณสามารถเปลี่ยน billing logic โดยไม่กระทบกับ recommendation engine ได้หรือไม่? การพุ่งสูงขึ้นของทราฟฟิกในมุมหนึ่งของแอปพลิเคชันจะทำให้ส่วนที่เหลือของระบบยังทำงานได้ตามปกติหรือไม่? คำถามเหล่านี้สำคัญกว่าการเลือกภาษาโปรแกรม เฟรมเวิร์ก หรือผู้ให้บริการคลาวด์เสียอีก

Architecture ที่ดีช่วยให้คุณมีพื้นที่ในการเปลี่ยนใจ มันกำหนดขอบเขตที่ชัดเจน เพื่อไม่ให้การทดลองของทีมหนึ่งไปทำให้ภาระงานใน production ของอีกทีมหนึ่งไม่เสถียร มันมองว่าความล้มเหลวเป็นสภาวะการทำงานปกติ ไม่ใช่เรื่องน่าตกใจ เมื่อคุณออกแบบโดยคำนึงถึงความล้มเหลว คุณจะเลิกสร้างบ้านกระจกและเริ่มสร้างโครงสร้างที่ยืดหยุ่นได้แทน

Microservices ในฐานะรูปแบบที่นำไปใช้ได้จริง

วิธีหนึ่งที่นำไปใช้ได้จริงเพื่อให้ได้โครงสร้างแบบนั้นคือการแบ่งแอปพลิเคชันของคุณออกเป็น microservices แทนที่จะเป็น codebase ขนาดใหญ่เพียงอันเดียว คุณจะแบ่งแอปออกเป็นส่วนเล็กๆ โดยแต่ละส่วนจะจัดการงานเฉพาะอย่าง เช่น payment service ทำหน้าที่ประมวลผลธุรกรรม, inventory service ทำหน้าที่ติดตามสต็อก และ notification service ทำหน้าที่ส่งอีเมลและข้อความ พวกมันสื่อสารกันผ่าน interfaces ที่กำหนดไว้ แทนที่จะเป็นการเข้าถึงหน่วยความจำโดยตรงหรือใช้ตารางฐานข้อมูลร่วมกัน

การแยกส่วนแบบนี้ช่วยสร้างพื้นที่ในการจัดการได้อย่างแท้จริง ทั้งในด้านเทคนิคและด้านการบริหารจัดการองค์กร

อัปเดตส่วนเล็กๆ โดยไม่ทำให้ระบบทั้งหมดพัง

เมื่อบริการมีขนาดเล็กและมุ่งเน้นเฉพาะด้าน คุณสามารถ patch ส่วนใดส่วนหนึ่งได้โดยไม่ต้องเสี่ยงต่อการเกิด cascade failure หากทีมของคุณพบ bug ในอัลกอริทึมการคำนวณค่าขนส่ง คุณก็แค่แก้ไขบริการนั้นและ deploy มันแยกต่างหาก ส่วนที่เหลือของแอปพลิเคชันยังคงทำงานต่อไปได้ ผู้ใช้ยังคงเลือกดูสินค้า ล็อกอิน และเพิ่มสินค้าลงในตะกร้าได้ตามปกติ รัศมีการทำลายล้าง (blast radius) ของการเปลี่ยนแปลงใดๆ จะยังคงเล็กมาก เมื่อเทียบกับระบบแบบ monolith ที่การพิมพ์ผิดเพียงจุดเดียวใน helper function อาจทำให้ระบบ checkout, การลงทะเบียน และการรายงานผลพังลงพร้อมกันทั้งหมด

ขยายขนาดเฉพาะฟังก์ชันเมื่อทราฟฟิกเพิ่มขึ้น

ทราฟฟิกไม่ได้กระจายตัวเท่ากันทั่วทั้งแอปพลิเคชัน ในช่วง flash sale ระบบจัดการคำสั่งซื้อของคุณอาจทำงานหนัก ในขณะที่ content management system แทบจะไม่ได้ใช้งานเลย ในระบบที่ผูกติดกันแน่น (tightly coupled) คุณต้อง scale ทุกอย่างหรือไม่งั้นก็ไม่ต้อง scale เลย แต่ด้วย microservices คุณสามารถกำหนดทรัพยากรได้อย่างแม่นยำ คุณสามารถเปิดใช้งาน checkout service เพิ่มขึ้นหลายๆ instances ในขณะที่ให้ product catalog ทำงานด้วยทรัพยากรเท่าเดิม ในช่วงการเปิดตัวสินค้า image processing workers ของคุณอาจต้องจัดการคิว thumbnails นับพันรายการ ในขณะที่ search index ยังคงทำงานได้อย่างราบรื่น ไม่มีเหตุผลที่คุณต้องขยาย search cluster เพียงเพื่อรองรับการทำงานของ image workers คุณใช้เงินในจุดที่ผู้ใช้สัมผัสได้ถึงความแตกต่าง และระบบของคุณจะยังคงตอบสนองได้ดีภายใต้ความกดดัน

Deploy โค้ดใหม่โดยไม่ต้องมี Downtime นาน

บริการขนาดเล็กช่วยให้สามารถใช้รูปแบบการปรับใช้ (deployment patterns) ที่ทำให้ช่วงเวลาการบำรุงรักษา (maintenance windows) กลายเป็นเรื่องล้าสมัย คุณสามารถใช้การปรับใช้แบบ rolling deployments โดยการส่งโค้ดใหม่ไปยังอินสแตนซ์เพียงบางส่วน ในขณะที่ส่วนที่เหลือยังคงให้บริการต่อไป คอยเฝ้าดูอัตราข้อผิดพลาดของคุณ และหากพบสิ่งผิดปกติ คุณสามารถเปลี่ยนเส้นทางคำขอ (request) กลับไปยังเวอร์ชันก่อนหน้าได้ภายในไม่กี่วินาที ส่วนการปรับใช้แบบ blue-green deployments จะช่วยให้คุณสามารถสร้างสภาพแวดล้อมใหม่ทั้งหมด ตรวจสอบความเรียบร้อย และสลับการใช้งาน (traffic) ได้โดยมีความเสี่ยงต่ำที่สุด ระบบไม่จำเป็นต้องหยุดทำงานเป็นเวลาหลายชั่วโมงเพื่อให้ใครบางคนมาทำ database migrations ด้วยตัวเอง

สร้างฟีเจอร์ใหม่ได้เร็วขึ้น

ฐานโค้ดขนาดใหญ่ทำให้เกิดความระมัดระวังจนเกินเหตุ การเปลี่ยนแปลงเพียงครั้งเดียวต้องอาศัยความเข้าใจในตรรกะที่ไม่เกี่ยวข้องกันนับพันบรรทัด ต้องผ่านการทดสอบการถดถอย (regression tests) ที่ใช้เวลานานหลายชั่วโมง และมีกำหนดการปรับใช้ที่ให้ความรู้สึกเหมือนการปล่อยจรวด บริการขนาดเล็กจะช่วยขจัดความกลัวเหล่านั้นออกไป ทีมสามารถสร้างฟีเจอร์ใหม่ได้โดยการแก้ไขโค้ดเพียงไม่กี่ร้อยบรรทัดในบริการที่พวกเขารู้จักอย่างลึกซึ้ง พวกเขาสามารถ commit, ทดสอบ และส่งมอบงานได้ภายในวันเดียวกัน ความเร็วนี้จะทวีคูณขึ้น เมื่อบริการต่างๆ ถูกจำกัดด้วยขอบเขตความรับผิดชอบที่ชัดเจน ทีมต่างๆ ก็จะไม่ทำงานทับซ้อนกัน พวกเขาจะเป็นเจ้าของโดเมนของตนเองตั้งแต่ต้นจนจบ

ความเป็นอิสระช่วยป้องกันการหยุดชะงักครั้งใหญ่

แต่ละบริการทำงานด้วยตัวเอง ความเป็นอิสระนี้ไม่ใช่เพียงเพื่อความสะดวกในการจัดการองค์กรเท่านั้น แต่ยังเป็นเหมือนประกันเชิงโครงสร้าง หากระบบแนะนำ (recommendation engine) ล่ม ร้านค้าก็ยังคงต้องขายสินค้าได้ หากระบบประมวลผลข้อมูล (analytics pipeline) ติดขัดเนื่องจากเหตุการณ์ที่ผิดรูปแบบ บริการล็อกอินก็ยังต้องสามารถยืนยันตัวตนผู้ใช้ได้ คุณต้องออกแบบ circuit breakers และเส้นทางสำรอง (fallback paths) ระหว่างบริการ เพื่อไม่ให้ความล้มเหลวเพียงจุดเดียวลุกลามจนทำให้ระบบล่มทั้งหมด (full outage) ระบบจะเติบโตไปพร้อมกับผู้ใช้งานของคุณ เพราะมันสามารถรองรับความกดดันได้โดยไม่แตกสลายออกจากกัน

ข้อควรระวัง: อย่าแยกส่วนอย่างสุ่มสี่สุ่มห้า

ทั้งหมดนี้ไม่ได้หมายความว่าคุณควรแยกฐานโค้ดของคุณออกเป็นส่วนๆ ตั้งแต่วันแรก Microservices ต้องการขอบเขตที่ชัดเจน หากทีมของคุณยังไม่รู้ว่าโดเมนหนึ่งสิ้นสุดตรงไหนและอีกโดเมนหนึ่งเริ่มต้นตรงไหน พวกเขาจะสร้างความวุ่นวายแบบกระจายศูนย์ (distributed mess) แทนที่จะเป็นระบบแบบกระจายศูนย์ (distributed system) คุณจะเปลี่ยนความซับซ้อนของโค้ดไปเป็นความซับซ้อนในการดำเนินงาน (operational complexity) แทน และทันใดนั้นคุณก็ต้องมาจัดการกับความหน่วงของเครือข่าย (network latency), การทำธุรกรรมแบบกระจาย (distributed transactions), retry storms และความสามารถในการสังเกตการณ์ (observability) ผ่าน log streams นับสิบสาย การดีบั๊กการชำระเงินที่ล่าช้าอาจหมายถึงการต้องไล่สายคำขอเพียงรายการเดียวผ่าน network hops ถึงสี่ครั้ง และผ่านแหล่งเก็บข้อมูลที่แตกต่างกันถึงสามแห่ง

หากทีมของคุณยังไม่พร้อมสำหรับภาระที่ตามมานี้ การรักษาอาจแย่ยิ่งกว่าตัวโรค บางครั้งทางเลือกที่ฉลาดกว่าคือการเริ่มด้วย modular monolith รักษาตรรกะการชำระเงินแยกออกจากตรรกะคลังสินค้าไว้ภายในฐานโค้ด แม้ว่าพวกเขาจะปรับใช้พร้อมกันก็ตาม บังคับใช้ขอบเขตด้วย internal APIs และแยก database schemas ภายในเอนจินเดียวกัน เมื่อรอยต่อเหล่านั้นพิสูจน์แล้วว่ามีความเสถียรและรูปแบบการใช้งาน (traffic patterns) คุ้มค่ากับค่าใช้จ่ายที่เพิ่มขึ้น จึงค่อยแยกออกมาเป็นบริการหนึ่ง สถาปัตยกรรมควรเป็นเหมือนประตูที่ถูกออกแบบมาอย่างตั้งใจ ไม่ใช่กำแพงที่ถูกสร้างขึ้นชั่วข้ามคืนเพียงเพราะคุณไปอ่านบล็อกโพสต์มา

เริ่มต้นด้วยความตั้งใจ

สถาปัตยกรรมที่แข็งแกร่งไม่ใช่เรื่องของการคาดการณ์ปริมาณการใช้งาน (traffic) ในอีกห้าปีข้างหน้า แต่เป็นเรื่องของการสร้างทางเลือกให้กับตัวเอง คุณไม่สามารถพึ่งพาเพียงแค่เครื่องมือในการขยายเว็บแอปของคุณได้ แต่คุณสามารถใช้ความคิดเพื่อหาทางออกก่อนที่ความกดดันจะเพิ่มขึ้น เคารพขอบเขตระหว่างความรับผิดชอบ สร้างส่วนประกอบขนาดเล็กที่มุ่งเน้นเฉพาะจุดและสามารถควบคุมชะตากรรมของตัวเองได้ มอบอิสระ (autonomy) ให้กับทีมในการเคลื่อนที่อย่างรวดเร็วโดยไม่ทำให้ส่วนอื่นพัง เมื่อคุณเริ่มต้นด้วยสถาปัตยกรรมที่มั่นคง คุณจะประหยัดเวลาและแรงในภายหลัง เพราะคุณไม่ต้องมาเขียนตรรกะหลักใหม่ในขณะที่เว็บไซต์กำลังวิกฤต

บทสรุปที่แท้จริง

Scalability ไม่ใช่ฟีเจอร์ที่คุณจะนำมาติดตั้งเพิ่มเข้าไปทีหลังเมื่อการเติบโตมาถึง แต่มันคือผลลัพธ์ตามธรรมชาติจากการตัดสินใจที่คุณทำไว้ตั้งแต่เนิ่นๆ เกี่ยวกับวิธีการไหลเวียนของความรับผิดชอบในระบบของคุณ เลือกจุดเชื่อมต่อที่เหมาะสม แยกส่วนความล้มเหลว (isolate failure) ขยายส่วนที่ติดขัด และปล่อยส่วนที่ทำงานได้ดีอยู่แล้วไว้ตามเดิม หากคุณทำเช่นนั้น เครื่องมือที่คุณเพิ่มเข้ามาในภายหลังจะมีฐานที่มั่นคงให้รองรับอย่างแท้จริง