ชั่งน้ำหนักข้อแลกเปลี่ยนของ Microservices (Microservice Trade-Offs)
บทความนี้เป็น บทแปลภาษาไทยอย่างไม่เป็นทางการ ของ “Microservice Trade-Offs” โดย Martin Fowler เผยแพร่ครั้งแรกบน martinfowler.com เมื่อ 1 กรกฎาคม 2015
แปลและเรียบเรียงเพื่อการศึกษา — ไดอะแกรมเชิงแนวคิดวาดใหม่เป็นภาษาไทย (label ของ node คงไว้เป็นภาษาอังกฤษ) ส่วนศัพท์เทคนิคคงไว้เป็นภาษาอังกฤษตามต้นฉบับพร้อมคำอธิบายไทยเมื่อพบครั้งแรก ลิขสิทธิ์ของเนื้อหาต้นฉบับเป็นของผู้เขียน
หลายทีมพบว่าสถาปัตยกรรมแบบ microservices ช่วยยกระดับ productivity (ผลิตภาพ/การผลิตงาน) ได้เหนือสถาปัตยกรรมแบบ monolith (application ก้อนเดียวที่ deploy พร้อมกันทั้งหมด) แต่ก็มีอีกหลายทีมที่กลับพบว่ามันเป็นภาระที่ฉุดให้ช้าลง
เช่นเดียวกับรูปแบบสถาปัตยกรรมทุกแบบ microservices มาพร้อมทั้งข้อดีและข้อเสีย การจะตัดสินใจอย่างมีเหตุผลได้ เราต้องเข้าใจ trade-off (การแลกได้แลกเสีย) เหล่านี้ แล้วนำไปปรับใช้กับ context (บริบทเฉพาะ) ของเราเอง
บทความนี้ตั้งใจจะช่วยชี้ให้เห็นปัจจัยที่ควรพิจารณา มากกว่าจะฟันธงว่าทางไหน “ดีกว่า” แบบตายตัว
ต่อไปนี้ผมจะไล่ข้อดี (Pro) และข้อเสีย (Con) สลับกันไป เพราะในทางปฏิบัติหลายอย่างเป็นสองด้านของเหรียญเดียวกัน ข้อดีของ microservices ก็มักลากต้นทุนบางอย่างติดตามมาด้วยเสมอ
flowchart LR MS[Microservices] MS --> B[Benefits] MS --> C[Costs] B --> B1[Strong Module Boundaries] B --> B2[Independent Deployment] B --> B3[Technology Diversity] C --> C1[Distribution] C --> C2[Eventual Consistency] C --> C3[Operational Complexity]
ภาพ 1 — ภาพรวมของข้อแลกเปลี่ยน: ข้อดีสามข้อ (module boundaries ที่แข็งแรง, deploy อิสระ, ความหลากหลายทางเทคโนโลยี) ต้องถูกชั่งกับต้นทุนสามข้อ (distribution, eventual consistency, ความซับซ้อนด้านปฏิบัติการ) การเลือกที่ดีคือการรู้ว่าในระบบของเรา ด้านไหนหนักกว่ากัน
Strong Module Boundaries — ขอบเขตของ module ที่แข็งแรง (ข้อดี)
หัวข้อที่มีชื่อว่า “Strong Module Boundaries — ขอบเขตของ module ที่แข็งแรง (ข้อดี)”ข้อดีข้อแรกและสำคัญที่สุดของ microservices คือ module boundaries (ขอบเขตของ module) ที่แข็งแรง ฟังดูย้อนแย้งอยู่บ้าง เพราะในทางทฤษฎีแล้ว microservices ไม่ควรมี boundary ที่แข็งแรงไปกว่า module ในโปรแกรม monolith ที่ออกแบบมาดี ๆ เลย
เมื่อพูดถึง “boundary ที่แข็งแรง” ผมหมายถึงการที่ซอฟต์แวร์ถูกแบ่งเป็นก้อน ๆ ที่ decouple (ตัดการพึ่งพา) กันได้ดี จนเวลาจะแก้อะไรสักอย่าง เราต้องเข้าใจเพียงส่วนเล็ก ๆ ที่หาเจอได้ง่ายเท่านั้น modularity (ความเป็น module) นี้ยิ่งสำคัญแบบทวีคูณเมื่อระบบและทีมงานเติบโตขึ้น
เหตุผลหนึ่งที่ modularity มีน้ำหนักมากคือ Conway’s Law ที่บอกว่าโครงสร้างของซอฟต์แวร์มักสะท้อนโครงสร้างการสื่อสารขององค์กรที่สร้างมันขึ้นมา
เมื่อทีมกระจายตัวกันในเชิงภูมิศาสตร์ การสื่อสารข้ามทีมย่อมกลายเป็นแบบทางการมากขึ้น microservices จึงมีคุณค่าตรงที่มันช่วยให้แต่ละทีมดูแลหน่วยงานที่ค่อนข้างเป็นอิสระได้ สอดคล้องกับสายการสื่อสารของทีมนั้น ๆ
ถึง monolith จะสามารถรักษา modularity เอาไว้ได้ในทางทฤษฎี แต่ในทางปฏิบัติมันแทบไม่เคยเป็นเช่นนั้น รูปแบบที่พบบ่อยที่สุดกลับกลายเป็น Big Ball of Mud (ก้อนโคลนขนาดใหญ่ที่ทุกอย่างพันกันยุ่งเหยิง)
ต้นตอของปัญหาคือ monolith ทำให้การลัดข้าม boundary ของ module เป็นเรื่องง่ายเกินไป
การลัดแบบนี้ดูเหมือนจะมีประโยชน์เชิงกลยุทธ์ในระยะสั้นเมื่อมองทีละครั้ง แต่เมื่อรวมกันมาก ๆ เข้ากลับบ่อนทำลายโครงสร้างและ productivity การแยก module ออกไปเป็น service ต่างหากจึงสร้างกำแพงที่แข็งแรงกว่ามากในการกันไม่ให้เกิดการลัดเช่นนั้น
📝 หมายเหตุ: คำว่า “monolith” ในที่นี้ไม่ได้เป็นคำดูแคลนแต่อย่างใด ผมนิยามมันตรง ๆ ว่าคือ “application ที่ถูกสร้างขึ้นเป็นหน่วยเดียว” เท่านั้น
อีกจุดหนึ่งที่สำคัญต่อ boundary ของ microservices คือ decentralized data management (การจัดการข้อมูลแบบกระจายศูนย์) แต่ละ service ดูแลฐานข้อมูลของตัวเอง และ service อื่นต้องเข้าถึงข้อมูลผ่าน API เท่านั้น ไม่ใช่ไปอ่านเขียนฐานข้อมูลร่วมกันโดยตรง
วิธีนี้ช่วยกำจัด integration database (ฐานข้อมูลกลางที่หลายระบบใช้ร่วมกัน) ซึ่งเป็นตัวการก่อ coupling ที่ร้ายแรงในระบบขนาดใหญ่
flowchart TB
UI[Web UI] --> APP
subgraph APP[Monolith Process]
direction TB
M1[Orders Module]
M2[Billing Module]
M3[Shipping Module]
end
M1 --> DB[(Shared Database)]
M2 --> DB
M3 --> DB
ภาพ 2 — โครงสร้างแบบ monolith: ทุก module อยู่ใน process เดียว และมัก share integration database ร่วมกัน จุดนี้เองที่ทำให้ boundary ระหว่าง module ถูกลัดข้ามได้ง่าย เพราะไม่มีอะไรมาบังคับให้เคารพขอบเขต
flowchart TB UI[Web UI] --> G[API Gateway] G --> S1[Orders Service] G --> S2[Billing Service] G --> S3[Shipping Service] S1 --> D1[(Orders DB)] S2 --> D2[(Billing DB)] S3 --> D3[(Shipping DB)] S1 -- API --> S2 S2 -- API --> S3
ภาพ 3 — โครงสร้างแบบ microservices: แต่ละ service เป็นเจ้าของฐานข้อมูลของตัวเอง service อื่นเข้าถึงได้ผ่าน API เท่านั้น การแบ่ง service ที่ดีมักอิงกับ bounded context — ขอบเขตเชิงความหมายใน domain ที่ใช้กำหนดว่า module หนึ่ง ๆ ครอบคลุมอะไรบ้าง
อย่างไรก็ตาม boundary ที่แข็งแรงต้องอาศัย discipline (วินัย) ไม่ว่าจะในสถาปัตยกรรมแบบไหน เพียงแต่ microservices ทำให้การทำผิดกฎยากขึ้นเท่านั้นเอง
ข้อจำกัดสำคัญคือเราจะประเมิน modularity ได้จริงก็ต่อเมื่อระบบทำงานมาแล้วหลายปี และคนที่ลอง microservices เป็นกลุ่มแรก ๆ มักเป็นทีมที่เก่งกว่าค่าเฉลี่ย เราจึงยังสรุปแบบฟันธงไม่ได้จนกว่าจะเห็นทีมระดับกลาง ๆ สร้างระบบที่อยู่ยาว
หลักฐานเชิงเล่าขานจากผู้ที่มีประสบการณ์ตอนนี้ชี้ไปในทางที่ว่า microservices ช่วยรักษา modularity ได้ดีกว่าจริง แต่ก็ยังเป็นเพียงหลักฐานเบื้องต้น
ข้อดีนี้จะกลายเป็นข้อเสียทันทีถ้าตีเส้น boundary ผิดที่ นี่คือหนึ่งในสองเหตุผลหลักของกลยุทธ์ Monolith First และเป็นเหตุผลที่แม้แต่คนที่โน้มเอียงจะเริ่มด้วย microservices ตั้งแต่แรก ก็ยังย้ำว่าทำได้ก็ต่อเมื่อเข้าใจ domain เป็นอย่างดีเท่านั้น
📷 ภาพ 4 — ปกหนังสือ Building Microservices ของ Sam Newman — แหล่งอ้างอิงสำคัญหากต้องการศึกษาวิธีสร้างระบบ microservices · ดูภาพต้นฉบับ ↗
มีกรณีศึกษาหนึ่งที่น่าสนใจในเชิงตรงข้าม ทีมหนึ่งใช้ microservices ทั้งที่ระบบยังไม่ซับซ้อนพอ จนสุดท้ายต้องเรียกทีมกู้ภัยเข้ามาช่วย
ตอนที่ต้องรีบเสริมนักพัฒนาจำนวนมากเพื่อกู้ project กลับพบว่าสถาปัตยกรรม microservices ช่วยได้มาก เพราะทำให้รับคนใหม่เข้าทำงานได้ง่ายกว่า monolith ถึงสุดท้าย project นี้จะใช้ชั่วโมงคนรวมมากกว่าถ้าทำแบบ monolith แต่ microservices ก็ช่วยให้ขยายกำลังคนได้ดีกว่า
(หมายเหตุผู้แปล: จุดนี้น่าสนใจเพราะขัดกับสัญชาตญาณของ Brooks’s Law ที่ว่าการเพิ่มคนเข้า project ที่ล่าช้ายิ่งทำให้ล่าช้าลงไปอีก — แต่ Fowler เองไม่ได้อ้างถึงกฎนี้ในบทความต้นฉบับ)
Distribution — ต้นทุนของระบบกระจาย (ข้อเสีย)
หัวข้อที่มีชื่อว่า “Distribution — ต้นทุนของระบบกระจาย (ข้อเสีย)”microservices ใช้ระบบแบบกระจาย (distributed systems) เพื่อยกระดับ modularity แต่การกระจายตัวเองก็ลากความซับซ้อนก้อนใหญ่เข้ามาด้วย
ผมยอมรับว่าชุมชน microservices มีความเข้าใจในเรื่องนี้ลึกซึ้งกว่ายุคของขบวนการ distributed objects เมื่อก่อนอยู่มาก แต่ต่อให้เป็นเช่นนั้น ความซับซ้อนของการกระจายก็ยังไม่หายไปไหน
อย่างแรกคือ performance (สมรรถนะ/ประสิทธิภาพ) การเรียกข้ามเครื่อง (remote call) นั้นช้ากว่าการเรียก function ภายใน process เดียวกันมาก และเมื่อ service หนึ่งเรียก service อื่นต่อ ๆ กันเป็นทอด ๆ latency (เวลาหน่วง) ก็ยิ่งสะสมพอกพูนตลอดสายการเรียก จนกลายเป็นปัญหา performance ที่ร้ายแรงได้
เราสามารถบรรเทาได้ด้วยการปรับ granularity (ความหยาบ-ละเอียดของการเรียก) เช่นรวบการสื่อสารข้าม service ให้เป็นก้อนใหญ่ขึ้นเพื่อลดจำนวนครั้งที่เรียก แต่วิธีนี้ก็ทำให้ตรรกะของโปรแกรมซับซ้อนขึ้น และช่วยได้จำกัด เพราะอย่างไรเสีย service ก็ต้องเรียกใช้ service อื่นที่มันพึ่งพาอย่างน้อยหนึ่งครั้งอยู่ดี
อีกวิธีคือการทำงานแบบ asynchronous (ไม่ประสานเวลา) การยิงหลาย ๆ การเรียกแบบขนานพร้อมกันจะทำให้ latency รวมเท่ากับการเรียกที่ช้าที่สุดเพียงตัวเดียว แทนที่จะเป็นผลบวกของทุกตัว
วิธีนี้ให้ผลด้าน performance ที่ดีขึ้นอย่างมาก แต่ก็แลกมาด้วยภาระทางความคิดที่หนักขึ้น การเขียนโปรแกรมแบบ asynchronous ให้ถูกต้องนั้นทำได้ยาก และดี bug ยากเป็นพิเศษ
ระบบ microservices ที่จริงจังส่วนใหญ่ที่ผมเจอล้วนต้องพึ่ง asynchrony เพื่อให้ได้ performance ที่ยอมรับได้
นอกจากเรื่องความเร็ว ระบบกระจายยังนำปัญหาด้าน reliability (ความน่าเชื่อถือ) เข้ามาด้วย remote call ล้มเหลวได้แบบคาดเดาไม่ได้ และ microservices ยิ่งเพิ่มจุดที่อาจล้มเหลว นักพัฒนาที่ฉลาดจึงต้องออกแบบเผื่อความล้มเหลว (design for failure)
โชคดีที่รูปแบบการทำงานแบบ asynchronous นั้นช่วยรองรับการจัดการกับความล้มเหลวได้ดี จึงช่วยเพิ่ม resilience (ความทนทานต่อความผิดพลาด) ให้ระบบ แต่ก็หมายความว่านักพัฒนาต้องคิดให้ตกในทุก ๆ การเรียกข้ามเครื่องว่าถ้ามันล้มเหลวแล้วจะเกิดผลอย่างไร
และทั้งหมดนี้ก็เป็นเพียงสองข้อแรกจาก Fallacies of Distributed Computing (มายาคติแปดข้อเรื่องระบบกระจายที่มีชื่อเสียง) เท่านั้น
ควรกล่าวด้วยว่าปัญหาเหล่านี้จำนวนมากก็โผล่ขึ้นมากับ monolith เมื่อมันเติบโต เพราะมี monolith น้อยระบบมากที่อยู่ตัวเดียวได้จริง ๆ ส่วนใหญ่ต้องคุยกับระบบ legacy หรือระบบภายนอกผ่านเครือข่ายอยู่แล้ว ซึ่งก็ก่อปัญหาแบบเดียวกัน
ทีมที่มีประสบการณ์มากกว่าจะรับมือกับความซับซ้อนของการกระจายได้ดีกว่า แต่ไม่ว่าอย่างไร การกระจายก็มีต้นทุนเสมอ ผมจึงเตือนว่าอย่ารีบกระโดดเข้าสู่สถาปัตยกรรมแบบกระจายก่อนเวลาอันควร ซึ่งเป็นสิ่งที่ผมเห็นเกิดขึ้นบ่อยเกินไป
Eventual Consistency — ความสอดคล้องที่ตามมาทีหลัง (ข้อเสีย)
หัวข้อที่มีชื่อว่า “Eventual Consistency — ความสอดคล้องที่ตามมาทีหลัง (ข้อเสีย)”ลองนึกภาพประสบการณ์ที่หลายคนคงเคยเจอ เรากดอัปเดตข้อมูลบางอย่าง กด refresh แล้วการอัปเดตนั้นหายไป รอสักครู่แล้ว refresh อีกที คราวนี้มันโผล่กลับมา
นี่คืออาการของ eventual consistency (ความสอดคล้องที่ในที่สุดจะมาถึง) ข้อมูลถูกจัดการโดยหลาย node และมีช่วงเวลาหนึ่งก่อนที่ทุก node จะ sync ตรงกัน หากเราไปโดน node ที่ยังไม่อัปเดตก็จะเห็นข้อมูลเก่า
ความน่ารำคาญของผู้ใช้ยังเป็นเรื่องเล็ก ที่หนักกว่าคือช่วงเวลาที่ข้อมูลยังไม่สอดคล้องกันนี้อาจทำให้ตรรกะทางธุรกิจ (business logic) ตัดสินใจบนข้อมูลที่ล้าสมัย จนก่อความผิดพลาดร้ายแรง และปัญหานี้ diagnose ยากมาก เพราะกว่าจะมาสืบสวนก็มักเป็นเวลานานหลังจากช่วง inconsistency ปิดตัวลงไปแล้ว
microservices เปิดประตูให้ eventual consistency เข้ามาก็เพราะการยืนกรานเรื่อง decentralized data management นั่นเอง
กับ monolith เรามัดหลายการอัปเดตไว้ใน transaction (การทำธุรกรรมเป็นชุดเดียว) เดียวได้ ซึ่ง transaction ให้การรับประกันเรื่อง consistency แต่ต้องแลกด้วยการผูก resource หลายตัวไว้ด้วยกัน
แต่ microservices ที่ต้องอัปเดต resource หลายตัวโดยไม่ใช้ distributed transaction (ซึ่งชุมชนปฏิเสธด้วยเหตุอันสมควร) ทำให้นักพัฒนาต้องตระหนักถึงประเด็น consistency เอง และต้องหาทางตรวจจับว่าข้อมูลยังไม่ตรงกันก่อนที่ code จะไปตัดสินใจอะไรที่ก่อปัญหา
แน่นอนว่า monolith ก็ไม่ได้รอดพ้นเรื่องนี้ไปทั้งหมด เมื่อระบบโตขึ้น เราก็มักเจอ inconsistency เข้าจนได้ เช่น
- เราใส่ cache เพื่อเพิ่ม performance แต่ cache invalidation (การทำให้ cache หมดอายุอย่างถูกจังหวะ) ก็เป็นหนึ่งในปัญหาที่ขึ้นชื่อว่ายากที่สุด
- เราใช้ offline lock เพื่อเลี่ยง transaction ฐานข้อมูลที่ค้างยาว
- เราอัปเดตระบบภายนอกที่อยู่นอกการประสาน transaction
นอกจากนี้ กระบวนการทางธุรกิจหลายอย่างก็ทน inconsistency ได้มากกว่าที่เราคิด เพราะธุรกิจมักให้ความสำคัญกับ availability (ความพร้อมใช้งาน) มากกว่า consistency ซึ่งสะท้อนสัญชาตญาณเชิงธุรกิจที่มีต่อ CAP theorem มานานแล้ว
เช่นเดียวกับปัญหาอื่น ๆ ของการกระจาย monolith ไม่ได้หนีพ้น inconsistency ไปได้หมด แต่ก็เจอน้อยกว่ามาก โดยเฉพาะในระบบขนาดเล็ก
Independent Deployment — การ deploy ที่เป็นอิสระ (ข้อดี)
หัวข้อที่มีชื่อว่า “Independent Deployment — การ deploy ที่เป็นอิสระ (ข้อดี)”ในศตวรรษที่ยี่สิบ การปล่อยซอฟต์แวร์ขึ้น production เป็นเหตุการณ์ที่นาน ๆ เกิดที และเจ็บปวด ต้องอาศัยกะพิเศษของทีมงานคอยเฝ้า ตัดกับวิธีปฏิบัติสมัยใหม่ที่ทีมเก่ง ๆ deploy ขึ้น production ได้หลายครั้งต่อวัน การเปลี่ยนแปลงนี้เป็นความเปลี่ยนแปลงครั้งใหญ่ของวงการ และมันเชื่อมโยงลึกซึ้งกับ microservices
Microservices เป็นสถาปัตยกรรมยุคหลัง-DevOps-revolution ตัวแรก — Neal Ford
หลายกรณีที่หันมาใช้ microservices ก็เพราะความยากลำบากในการ deploy monolith ก้อนใหญ่ ที่การแก้เพียงเล็กน้อยก็อาจลุกลามจนล้มทั้งระบบได้
microservices ซึ่งนิยามว่าเป็นชิ้นส่วนที่ deploy ได้อย่างเป็นอิสระ (independently deployable) ช่วยให้นักพัฒนาทดสอบและ deploy การเปลี่ยนแปลงเฉพาะ service เดียวได้ และแม้ service หนึ่งจะล้มไปทั้งตัว ก็ไม่ควรทำให้ทั้งระบบล่ม เพราะเราออกแบบเผื่อความล้มเหลวไว้แล้ว
ความสัมพันธ์นี้เป็นแบบสองทาง microservices เอื้อให้ deploy ได้บ่อย แต่ความถี่ในการ deploy ก็เรียกร้องวุฒิภาวะด้านการ deploy กลับคืนด้วย
ความสามารถในการ deploy application และ provision (จัดเตรียม) โครงสร้างพื้นฐานได้อย่างรวดเร็วกลายเป็นสิ่งจำเป็น ไม่ใช่ของฟุ่มเฟือย และเหนือกว่าพื้นฐานนั้น continuous delivery (การส่งมอบต่อเนื่อง) ก็กลายเป็นสิ่งที่ขาดไม่ได้
ประโยชน์คือ cycle-time (เวลาตั้งแต่คิดจนได้ซอฟต์แวร์ขึ้น production) ที่สั้นลง มอบความได้เปรียบในการแข่งขันให้องค์กร เพราะตอบสนองตลาดได้เร็วขึ้นและออก feature ได้ถี่ขึ้น
มีข้อควรระวังสำคัญ หลายคนอ้าง continuous delivery เป็นเหตุผลในการใช้ microservices แต่ผมต้องย้ำว่าแม้แต่ monolith ก้อนใหญ่ก็ทำ continuous delivery ได้ (ดูตัวอย่างอย่าง Facebook และ Etsy)
และในทางกลับกัน มีความพยายามทำ microservices จำนวนมากที่ล้มเหลวเรื่องการ deploy อย่างเป็นอิสระ เพราะ service ต้องปล่อยพร้อม ๆ กันแบบประสานล็อกกัน
โดยรวมผมพบว่า continuous delivery ทำได้ง่ายกว่าด้วย microservices เมื่อเทียบกับ monolith แต่ไม่ได้ต่างกันดรามาติกเท่ากับข้อได้เปรียบด้าน modularity
📝 หมายเหตุ: ความสามารถในการ deploy อย่างเป็นอิสระเป็นส่วนหนึ่งของนิยาม microservices เอง ดังนั้น service ชุดหนึ่งที่ต้อง deploy พร้อมกันแบ block กันจึงมีเหตุผลที่จะบอกว่าไม่ใช่สถาปัตยกรรม microservices จริง ๆ
Operational Complexity — ความซับซ้อนด้านปฏิบัติการ (ข้อเสีย)
หัวข้อที่มีชื่อว่า “Operational Complexity — ความซับซ้อนด้านปฏิบัติการ (ข้อเสีย)”การ deploy หน่วยเล็ก ๆ ได้อย่างรวดเร็วและเป็นอิสระเป็นพรสำหรับฝ่ายพัฒนา แต่กลับเพิ่มแรงกดดันมหาศาลให้ฝ่ายปฏิบัติการ (operations) ที่ต้องดูแล microservices หลายสิบหรือหลายร้อยตัวที่เปลี่ยนแปลงตลอดเวลา หลายองค์กรพบว่าการจัดการฝูง service ที่วิวัฒน์เร็วขนาดนี้เป็นภาระที่แบกไม่ไหว
จุดนี้เองที่ continuous delivery เปลี่ยนสถานะจาก “มีก็ดี” สำหรับ monolith ไปเป็น “จำเป็นอย่างยิ่ง” สำหรับ microservices
เราไม่มีทางจัดการ service หลายสิบตัวได้เลยหากปราศจากระบบอัตโนมัติและการทำงานร่วมกันที่ continuous delivery มอบให้ ทั้งการจัดการและ monitoring เหล่านี้เรียกร้องวุฒิภาวะระดับที่ “มีประโยชน์” กับ monolith แต่ “จำเป็น” กับ microservices
ผู้สนับสนุน microservices ชอบชี้ว่า service ที่เล็กกว่าย่อมเข้าใจง่ายกว่าทีละตัว ซึ่งจริง แต่ความซับซ้อนไม่ได้ถูกกำจัดทิ้ง มันเพียงย้ายที่ ไปโผล่ตรงจุดเชื่อมต่อระหว่าง service ต่าง ๆ และปรากฏออกมาในรูปของความยากด้านปฏิบัติการ เช่นการ debug ข้าม service
การเลือก service boundary (ขอบเขตของ service) ที่ดีจะช่วยลดปัญหาเหล่านี้ ในทางกลับกันการเลือกที่แย่จะทำให้มันเลวร้ายลงอย่างรุนแรง
การรับมือกับความซับซ้อนด้านปฏิบัติการต้องอาศัยทักษะและเครื่องมือใหม่ ๆ โดยน้ำหนักตกอยู่ที่ “ทักษะ” เป็นพิเศษ เพราะเครื่องมือยังไม่สุกงอมนัก และแม้เครื่องมือจะดีขึ้น สภาพแวดล้อมแบบ microservices ก็ยังต้องการระดับทักษะพื้นฐานที่สูงกว่าอยู่ดี
เหนือไปกว่าทักษะและเครื่องมือ ยังต้องการ DevOps culture (วัฒนธรรม DevOps) นั่นคือความร่วมมืออย่างลึกซึ้งระหว่างนักพัฒนา ฝ่ายปฏิบัติการ และทุกฝ่ายที่เกี่ยวข้องกับการส่งมอบ
การเปลี่ยนวัฒนธรรมเป็นเรื่องยาก โดยเฉพาะในองค์กรใหญ่ที่มีมานาน หากขาดการเปลี่ยนวัฒนธรรมนี้ application แบบ monolith จะแค่ทำงานได้ไม่เต็มที่ แต่ application แบบ microservices จะเจอความเจ็บปวดขั้นรุนแรง
Technology Diversity — ความหลากหลายทางเทคโนโลยี (ข้อดี)
หัวข้อที่มีชื่อว่า “Technology Diversity — ความหลากหลายทางเทคโนโลยี (ข้อดี)”เพราะแต่ละ microservice deploy ได้อย่างเป็นอิสระ แต่ละตัวจึงใช้ภาษา library และ data store ที่ต่างกันได้ ทีมสามารถเลือกเครื่องมือที่เหมาะกับปัญหาเฉพาะหน้าที่สุด แทนที่จะบังคับใช้มาตรฐานเดียวกันทั้งองค์กร
บทสนทนามักโฟกัสที่ “เครื่องมือที่เหมาะกับงาน” (best tool for the job) แต่ผมกลับคิดว่าประโยชน์เชิงปฏิบัติที่ใหญ่ที่สุดอาจเป็นเรื่อง library versioning (การจัดการ version ของ library) มากกว่า
ใน monolith เราใช้ library ได้เพียง version เดียว จึงเกิดสถานการณ์ปวดหัวที่ส่วนหนึ่งของระบบต้องการอัปเกรด library แต่การอัปเกรดนั้นดันทำอีกส่วนพัง ปัญหานี้ยิ่งเลวร้ายแบบทวีคูณเมื่อ codebase โตขึ้น
แต่ก็ต้องระวังไม่ให้ความหลากหลายทางเทคโนโลยีมากเกินไปจนองค์กรรับไม่ไหว บริษัทส่วนใหญ่ที่ผมรู้จักจึงสนับสนุนให้ใช้ชุดเทคโนโลยีที่จำกัดไว้ เครื่องมือ monitoring ที่ใช้ร่วมกันและ platform กลางช่วยให้ service ต่าง ๆ ยังอยู่ในกรอบเทคโนโลยีที่บริหารจัดการได้
microservices ยังเอื้อต่อการ ทดลอง ซึ่งทำได้ยากใน monolith ที่การตัดสินใจเลือกภาษาและ framework ตั้งแต่แรกกลายเป็นสิ่งที่แก้กลับได้ยากและแพงเมื่อเวลาผ่านไปเป็นสิบปี เราสามารถทดลองเทคโนโลยีใหม่ใน service เดียว และค่อย ๆ ย้ายทั้ง service ไปสู่เทคโนโลยีที่ดีกว่าได้ทีละตัวแบบค่อยเป็นค่อยไป
Secondary Factors — ปัจจัยรอง
หัวข้อที่มีชื่อว่า “Secondary Factors — ปัจจัยรอง”นอกจากปัจจัยหลักข้างต้น ยังมีปัจจัยรองที่มักถูกหยิบยกขึ้นมา
Scaling ผู้สนับสนุนอ้างว่า microservices ช่วยให้ scale แบบเลือกเฉพาะจุดได้ (ขยายเฉพาะ service ที่รับโหลดหนัก) ซึ่งน่าจะประหยัดกว่าการ scale แบบ cookie-cutter (คือ duplicate ทั้ง application ขึ้นมาทั้งก้อนเมื่อต้องการกำลังเพิ่ม)
แต่ผมสารภาพว่ายังหากรณีศึกษาที่น่าเชื่อมายืนยันไม่ได้ว่าการ scale แบบเลือกจุดนั้นคุ้มค่ากว่าการ duplicate ทั้งก้อนจริง
Security microservices ช่วยให้แยกข้อมูลอ่อนไหว (sensitive data) ออกมาใส่การป้องกันที่เข้มขึ้น และบังคับให้การสื่อสารระหว่าง service ทุกเส้นต้องปลอดภัย ซึ่งอาจทำให้การเจาะระบบเมื่อเกิดช่องโหว่ทำได้ยากขึ้น
เมื่อความมั่นคงปลอดภัยทวีความสำคัญมากขึ้นเรื่อย ๆ ปัจจัยข้อนี้อาจกลายเป็นเหตุผลหลักในการเลือก microservices ได้ ถึงแม้ระบบที่เป็น monolith เป็นหลักก็มักแยก service สำหรับข้อมูลอ่อนไหวออกมาต่างหากอยู่แล้วเช่นกัน
Testing ฝ่ายวิจารณ์ชี้ว่า microservices ทดสอบยากกว่า monolith ซึ่งก็จริง เพราะเป็นธรรมชาติของความซับซ้อนในระบบกระจาย แต่ก็มีแนวทางการทดสอบที่ดีสำหรับ microservices อยู่แล้ว ประเด็นสำคัญคือ วินัยในการทดสอบ ต่างหากที่ชี้ขาดความสำเร็จ ยิ่งกว่าตัวเลือกเชิงสถาปัตยกรรม
Summing Up — สรุปส่งท้าย
หัวข้อที่มีชื่อว่า “Summing Up — สรุปส่งท้าย”คำแนะนำเชิงสถาปัตยกรรมแบบทั่วไปมีข้อจำกัดในตัวเองเสมอ บทความอย่างนี้จึงไม่อาจสั่งการแทนเราได้ว่าต้องเลือกทางไหน ทำได้เพียงช่วยชี้ปัจจัยที่ควรพิจารณาเท่านั้น
ข้อดีและต้นทุนแต่ละข้อมีน้ำหนักต่างกันไปในแต่ละระบบ บางครั้งถึงกับพลิกบทบาทกันได้ (เช่น boundary ที่แข็งแรงเป็นข้อดีสำหรับระบบซับซ้อน แต่กลับเป็นภาระถ่วงสำหรับระบบง่าย ๆ) การตัดสินใจจึงต้องนำเกณฑ์เหล่านี้ไปประเมินกับ context เฉพาะของเรา แล้วชั่งน้ำหนักว่าอะไรสำคัญกว่าอะไร
ประสบการณ์กับ microservices ยังค่อนข้างจำกัด การตัดสินสถาปัตยกรรมทำได้จริงก็ต่อเมื่อระบบเติบโตเต็มที่และผ่านการใช้งานมาหลายปี ตอนนี้เรายังมีเรื่องเล่าจากสถาปัตยกรรม microservices ที่อยู่ยาวไม่มากพอ
ผมอยากย้ำว่า monolith และ microservices ไม่ใช่ทางเลือกแบบขาว-ดำสองขั้ว มันเป็นนิยามที่พร่าเลือน มีขอบเขตคลุมเครือ ระบบจำนวนมากอยู่ตรงกลางระหว่างสองแบบ และบางระบบก็ไม่เข้าพวกทั้งคู่
ผมมองมันเป็น “region” (อาณาบริเวณ) ในพื้นที่ของการออกแบบ ที่ควรค่าแก่การตั้งชื่อไว้เพื่อพูดคุยถึงคุณลักษณะที่น่าสนใจ แต่ไม่ใช่การจัดหมวดหมู่สถาปัตยกรรมที่ครอบคลุมทุกอย่าง
ข้อสรุปหนึ่งที่ค่อนข้างเป็นฉันทามติคือ microservices มี microservice premium (ต้นทุนส่วนเพิ่มด้าน productivity ที่ต้องจ่ายเมื่อเลือกใช้ microservices) ซึ่งจะคุ้มก็ต่อเมื่อระบบซับซ้อนมากพอเท่านั้น ระบบที่ยังบริหารได้ดีด้วยสถาปัตยกรรม monolith ก็ไม่ควรหันไปใช้ microservices
ท้ายที่สุด ผมขอปิดด้วยข้อสังเกตที่มองข้ามไม่ได้ นั่นคือปัจจัย “อ่อน” (soft factors) อย่างคุณภาพของคนในทีม การทำงานร่วมกันของทีม และระดับการสื่อสารกับ domain expert (ผู้เชี่ยวชาญใน domain) ล้วนส่งผลต่อความสำเร็จของ project มากกว่าการเลือกว่าจะใช้ microservices หรือไม่
ในเชิงเทคนิค การใส่ใจกับ code ที่สะอาด การทดสอบที่รัดกุม และ evolutionary architecture (สถาปัตยกรรมที่วิวัฒน์ได้) ก็มีความหมายมากกว่าการเลือกใช้ microservices เสียอีก
Further Reading — อ่านต่อ
หัวข้อที่มีชื่อว่า “Further Reading — อ่านต่อ”หากต้องการศึกษาลึกลงไปว่าจะสร้างระบบ microservices อย่างไร แหล่งอ้างอิงหลักคือหนังสือ Building Microservices ของ Sam Newman โดยเฉพาะบทที่ 1 ซึ่งไล่เรียงข้อดีของ microservices ไว้อย่างครบถ้วน (ดูปกหนังสือได้ที่ภาพ 4 ในหัวข้อ “Strong Module Boundaries” ด้านบน)
สำหรับมุมมองด้านข้อเสีย บทความ “Microservices - Not A Free Lunch!” ของ Benjamin Wootton บน High Scalability เป็นหนึ่งในบทสรุปข้อเสียของ microservices ที่เก่าแก่และดีที่สุดชิ้นหนึ่ง
นอกจากนี้ยังสามารถอ่าน Microservice Resource Guide ของผมเพื่อเจาะลึกคำถามด้านการนำไปใช้งานเพิ่มเติมได้
📷 ภาพ 5 — โลโก้ Microservice Resource Guide ของ Martin Fowler · ดูภาพต้นฉบับ ↗
Acknowledgements — กิตติกรรมประกาศ
หัวข้อที่มีชื่อว่า “Acknowledgements — กิตติกรรมประกาศ”ขอบคุณสำหรับความคิดเห็นและการพูดคุยที่ช่วยขัดเกลาบทความนี้จาก Brian Mason, Chris Ford, Rebecca Parsons, Rob Miles, Scott Robinson, Stefan Tilkov, Steven Lowe และ Unmesh Joshi
📄 อ่านต้นฉบับภาษาอังกฤษ: Microservice Trade-Offs — Martin Fowler, martinfowler.com (1 กรกฎาคม 2015)