จากโมโนลิทสู่ไมโครเซอร์วิส
ตัดสินใจว่าควรแยกส่วนใดและกำหนดขอบเขตบริการอย่างไร
จากโมโนลิทสู่ไมโครเซอร์วิส เป็นบทเรียน PHP Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน PHP Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส PHP Academy มีบทเรียนทั้งหมด 4 บทเรียน
จากโมโนลิทสู่ไมโครเซอร์วิส
ไมโครเซอร์วิสเป็นการตัดสินใจเชิงโครงสร้างองค์กรและการปฏิบัติงาน ไม่ใช่ตัวเลือกเริ่มต้น การแยกโมโนลิทที่เขียนด้วย PHP แลกความเรียบง่ายภายในกระบวนการกับการเรียกผ่านเครือข่าย ข้อมูลแบบกระจาย และความซับซ้อนในการนำขึ้นใช้งาน หากทำด้วยเหตุผลที่ไม่ถูกต้อง ทุกอย่างจะช้าลง
บทเรียนนี้มุ่งเน้นส่วนที่ยากที่สุด: การกำหนดขอบเขตบริการ หากกำหนดรอยต่อได้ถูกต้อง ส่วนที่เหลือก็เป็นเพียงงานโครงสร้างพื้นฐาน แต่หากกำหนดผิด คุณจะสร้างโมโนลิทแบบกระจาย — ได้รับต้นทุนทั้งหมดแต่ไม่ได้ประโยชน์ใดเลย
เหตุผลที่ควร (และไม่ควร) แยก
เหตุผลที่ดีในการแยก:
- ความสามารถในการนำขึ้นใช้งานและความรับผิดชอบของทีมที่แยกจากกันได้
- การขยายทรัพยากรของระบบย่อยที่มีภาระงานสูงได้อย่างอิสระ
- การแยกเทคโนโลยีและสภาพแวดล้อมขณะทำงาน รวมถึงการจำกัดผลกระทบเมื่อเกิดข้อขัดข้อง
เหตุผลที่ไม่ดีคือ “โค้ดยุ่งเหยิง” (ควรปรับโครงสร้างโมโนลิทก่อน) หรือการทำตามกระแส หากบริการสองแห่งต้องนำขึ้นใช้งานพร้อมกันเสมอ หรือต้องใช้ตารางฐานข้อมูลร่วมกัน บริการเหล่านั้นก็คือบริการเดียวที่สวมหมวกสองใบ
บริบทที่มีขอบเขต
การออกแบบที่ขับเคลื่อนด้วยโดเมนมีเครื่องมือสำหรับกำหนดขอบเขตที่ชัดเจนที่สุด นั่นคือ บริบทที่มีขอบเขต ภายในบริบทหนึ่ง คำศัพท์จะมีความหมายที่แม่นยำเพียงหนึ่งเดียว “ลูกค้า” ใน การเรียกเก็บเงิน (ผู้รับใบแจ้งหนี้) แตกต่างจาก “ลูกค้า” ใน ฝ่ายสนับสนุน (ผู้เขียนคำขอ)
บริบทที่มีขอบเขตแต่ละบริบทเป็นตัวเลือกที่เหมาะสมอย่างยิ่งสำหรับการเป็นบริการ ขอบเขตควรสอดคล้องกับภาษาทางธุรกิจและวิธีที่องค์กรสื่อสารกันจริง ซึ่งเป็นกฎของ Conway ในทางปฏิบัติ
การเกาะกลุ่มสูง การเชื่อมโยงต่ำ
ขอบเขตบริการที่ดีจะเก็บสิ่งที่ เปลี่ยนแปลงไปด้วยกันไว้ภายใน และผลักสิ่งที่เปลี่ยนแปลงอย่างอิสระออกไป วัดการแยกที่เสนอโดยพิจารณาจาก:
- มีฟีเจอร์กี่รายการที่ต้องแก้ไข สองบริการพร้อมกัน? (ควรมีน้อย)
- รูปแบบการเรียกใช้ระหว่างบริการทั้งสอง ถี่แค่ไหน? (ควรเป็นการเรียกที่รวมงานเป็นก้อนใหญ่)
หากการทำฟีเจอร์หนึ่งต้องคร่อมขอบเขตอยู่เสมอ แสดงว่ากำหนดขอบเขตไว้ผิดตำแหน่ง
<?php
// Chatty boundary smell: N network calls to render one view
foreach ($order->lineItems as $item) {
$product = $catalogApi->get($item->productId); // one call PER item!
$names[] = $product['name'];
}
// Coarse-grained: one batch call across the boundary
$ids = array_map(fn($i) => $i->productId, $order->lineItems);
$names = $catalogApi->getMany($ids); // single round-trip
echo count($names) . " products in one call\n";ฐานข้อมูลต่อบริการ
กฎที่ต่อรองไม่ได้คือ บริการแต่ละแห่ง เป็นเจ้าของข้อมูลของตนเอง และไม่มีบริการอื่นเข้าถึงตารางของบริการนั้น ฐานข้อมูลที่ใช้ร่วมกันจะสร้างการเชื่อมโยงแบบเดิมขึ้นมาอีกครั้ง ซึ่งเป็นสิ่งที่คุณแยกออกเพื่อหลีกหนี — การเปลี่ยนสคีมาครั้งเดียวอาจทำให้บริการที่ไม่เกี่ยวข้องเสียหาย
ดังนั้น การสืบค้นข้ามบริการที่เคยเป็น SQL JOIN ในโมโนลิท จะกลายเป็นการเรียก API หรือใช้แบบจำลองการอ่านที่จำลองข้อมูล นี่คือต้นทุนที่ต้องจ่าย และนั่นคือจุดประสงค์ของการแยก
<?php
// In the monolith: one JOIN across domains
$sql = 'SELECT o.id, c.email
FROM orders o JOIN customers c ON c.id = o.customer_id';
// After split: Orders service holds only the foreign id;
// it asks the Customers service for the rest (or keeps a local read model).
$order = $orderRepo->find($id); // local
$customer = $customersApi->get($order->customerId); // network call
echo $order->id . ' / ' . $customer->email . "\n";รูปแบบต้นไทรรัด
อย่าเขียนโมโนลิทใหม่ทั้งหมดในครั้งเดียว ให้ใช้รูปแบบ ต้นไทรรัดเพื่อแยกส่วนต่าง ๆ ออกมาอย่างค่อยเป็นค่อยไป: ส่งการรับส่งข้อมูลบางส่วนไปยังบริการใหม่ผ่านส่วนหน้า/พร็อกซี พัฒนาบริการนั้นต่อ และเลิกใช้เส้นทางโค้ดเดิมก็ต่อเมื่อเส้นทางใหม่รองรับความสามารถทั้งหมดแล้ว
พร็อกซีย้อนกลับ (หรือเกตเวย์ API) จะอยู่ด้านหน้าและตัดสินใจตามแต่ละเส้นทางว่าจะเรียกโมโนลิทรุ่นเดิมหรือบริการใหม่ การย้ายระบบดำเนินไปทีละความสามารถ โดยยังพร้อมนำขึ้นใช้งานได้เสมอ
<?php
// Facade routing: peel off one capability at a time
function route(string $path): string {
$migrated = ['/invoices', '/invoices/pdf']; // moved to billing-svc
foreach ($migrated as $prefix) {
if (str_starts_with($path, $prefix)) {
return 'http://billing-svc' . $path;
}
}
return 'http://legacy-monolith' . $path; // everything else, for now
}
echo route('/invoices/pdf'), "\n";
echo route('/users/42'), "\n";การแยกโมดูล
ลำดับการแยกที่เหมาะกับการปฏิบัติจริง:
- ค้นหาโมดูลที่มี การพึ่งพาจากภายนอกขาเข้าน้อยและมีความเป็นเจ้าของข้อมูลที่ชัดเจน
- ห่อหุ้มการเรียกภายในกระบวนการของโมดูลนั้นไว้หลังอินเทอร์เฟซในโมโนลิทก่อน
- ย้ายข้อมูลที่โมดูลเป็นเจ้าของไปไว้ในสคีมา/ฐานข้อมูลของโมดูลเอง
- เปลี่ยนการทำงานของอินเทอร์เฟซให้ใช้ไคลเอนต์เครือข่าย
- เปลี่ยนเส้นทางการรับส่งข้อมูลผ่านส่วนหน้า แล้วลบโค้ดเดิม
การห่อหุ้มด้วยอินเทอร์เฟซภายในโมโนลิทก่อนจะช่วยลดความเสี่ยงของขั้นตอนการเชื่อมต่อเครือข่าย
<?php
// Step 2: hide the implementation behind a port the monolith calls
interface InvoiceService {
public function generate(string $orderId): string; // returns invoice id
}
// Today: local class. Tomorrow: HTTP client to billing-svc.
// The monolith's calling code never changes.
final class LocalInvoiceService implements InvoiceService {
public function generate(string $orderId): string { return 'inv-1'; }
}ความสอดคล้องของข้อมูลแบบกระจาย
เมื่อแยกข้อมูลออกจากกันแล้ว คุณจะไม่สามารถใช้ธุรกรรม ACID ข้ามบริการได้อีกต่อไป ให้ยอมรับ ความสอดคล้องในท้ายที่สุด: บริการต่าง ๆ เผยแพร่เหตุการณ์เกี่ยวกับข้อมูลของตนเอง และบริการอื่น ๆ สร้าง แบบจำลองการอ่านเฉพาะที่จากเหตุการณ์เหล่านั้น
บริการคำสั่งซื้อไม่สืบค้นบริการลูกค้าในทุกคำขอ แต่จะเก็บแบบจำลองย่อยขนาดเล็กไว้ (เฉพาะฟิลด์ที่จำเป็น) และอัปเดตด้วยเหตุการณ์ CustomerUpdated วิธีนี้ตัดการพึ่งพาขณะทำงานและขั้นการส่งต่อที่เพิ่มเวลาแฝงออกไป
<?php
// Orders service keeps a tiny local projection of customer data
function onCustomerUpdated(PDO $db, array $evt): void {
$db->prepare(
'INSERT INTO customer_read_model (id, email)
VALUES (:id, :email)
ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email'
)->execute(['id' => $evt['id'], 'email' => $evt['email']]);
}ความเป็นจริงด้านการปฏิบัติงาน
ไมโครเซอร์วิสย้ายความซับซ้อนจากโค้ดไปอยู่ที่การปฏิบัติงาน ก่อนแยกบริการ คุณต้องมีสิ่งต่อไปนี้:
- การบันทึกแบบรวมศูนย์และการติดตามระบบแบบกระจาย (มีรหัสสหสัมพันธ์ข้ามแต่ละช่วงการเรียก)
- CI/CD ต่อบริการและการนำขึ้นใช้งานแยกกันโดยมีเวอร์ชัน
- การตรวจสอบสุขภาพ ระยะหมดเวลา การลองใหม่ และตัวตัดวงจรสำหรับทุกการเรียก
- การทดสอบสัญญา เพื่อไม่ให้ผู้ให้บริการทำลายผู้ใช้บริการโดยที่ไม่มีใครสังเกตเห็น
หากทีมของคุณยังดูแลบริการเดียวให้ดีไม่ได้ บริการสิบแห่งจะทำให้สถานการณ์แย่ลงสิบเท่า
การกำหนดขนาดบริการให้เหมาะสม
คำว่า “ไมโคร” ทำให้เข้าใจผิด — ควรกำหนดขนาดบริการจาก ความสามารถทางธุรกิจและความรับผิดชอบของทีม ไม่ใช่จากจำนวนบรรทัดโค้ด หากแบ่งละเอียดเกินไป (เป็นนาโนเซอร์วิส) ฟีเจอร์เดียวจะแตกแขนงเป็นการเรียกผ่านเครือข่ายจำนวนมาก หากแบ่งหยาบเกินไป คุณก็จะกลับไปมีโมโนลิทอีกครั้ง
หลักเกณฑ์โดยคร่าวที่ใช้ได้ดีคือ บริการหนึ่งควรมีทีมเดียวที่รับผิดชอบได้ สามารถนำขึ้นใช้งานได้ด้วยตนเอง และรองรับกรณีใช้งานหลักได้โดยไม่ต้องต่อห่วงโซ่การเรียกแบบรอผลผ่านเพื่อนบ้านจำนวนมาก
ทางเลือกโมโนลิทแบบโมดูล
ก่อนเปลี่ยนไปใช้ระบบแบบกระจาย ให้พิจารณา โมโนลิทแบบโมดูล: มีชุดเดียวที่นำขึ้นใช้งานได้ แต่ภายในแบ่งเป็นโมดูลที่มีขอบเขตชัดเจนและมีสคีมาของตนเอง โดยสื่อสารกันผ่านอินเทอร์เฟซที่เผยแพร่เท่านั้น — ห้ามเข้าถึงตารางข้ามโมดูล
คุณจะได้ขอบเขตที่สะอาดและปรับโครงสร้างได้ง่าย โดยไม่ต้องแบกรับต้นทุนส่วนเพิ่มของระบบแบบกระจาย เมื่อโมดูลใดต้องการการขยายทรัพยากรหรือความเป็นเจ้าของที่แยกจากกันอย่างแท้จริง โมดูลนั้นก็มีรูปทรงพร้อมแยกออกมาด้วยรูปแบบต้นไทรรัดอยู่แล้ว สำหรับทีมส่วนใหญ่ นี่คือจุดเริ่มต้นที่เหมาะสมที่สุด
<?php
// Modules talk only through interfaces, never each other's tables.
namespace App\Billing; // owns billing_* tables
interface BillingFacade {
public function invoiceForOrder(string $orderId): string;
}
namespace App\Sales; // owns sales_* tables
final class Checkout {
public function __construct(private \App\Billing\BillingFacade $billing) {}
// Sales never SELECTs from billing_* directly - only via the facade.
}ตรวจสอบอย่างรวดเร็ว
การสังเกตการแยกที่ไม่ดี
สรุปทบทวน
การกำหนดขอบเขตบริการให้ดี:
- แยกเพื่อ ความสามารถในการนำขึ้นใช้งาน การขยายทรัพยากร และความเป็นเจ้าของ — ไม่ใช่เพราะโค้ดยุ่งเหยิง
- จัดขอบเขตให้สอดคล้องกับ บริบทที่มีขอบเขต โดยมุ่งให้การเกาะกลุ่มสูงและการเชื่อมโยงต่ำ
- ฐานข้อมูลต่อบริการ — ไม่มีตารางร่วมกัน แทนที่ JOIN ด้วย API หรือแบบจำลองการอ่าน
- ย้ายระบบทีละขั้นด้วยรูปแบบ ต้นไทรรัด
- ยอมรับ ความสอดคล้องในท้ายที่สุด และลงทุนด้านการปฏิบัติงานก่อนขยายระบบ
ถัดไป: บริการเหล่านี้สื่อสารกันอย่างไรจริง ๆ — REST และ gRPC
คำถามที่พบบ่อย
บทเรียน “จากโมโนลิทสู่ไมโครเซอร์วิส” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “จากโมโนลิทสู่ไมโครเซอร์วิส” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส PHP Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส PHP Academy มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “จากโมโนลิทสู่ไมโครเซอร์วิส”
ตัดสินใจว่าควรแยกส่วนใดและกำหนดขอบเขตบริการอย่างไร คุณปฏิบัติ PHP Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน PHP Academy หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน PHP Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน
บทเรียน “จากโมโนลิทสู่ไมโครเซอร์วิส” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน PHP Academy นี้ได้ไหม
ได้ บทเรียน PHP Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- จากโมโนลิทสู่ไมโครเซอร์วิส
- การสื่อสารระหว่างบริการ: REST และ gRPC
- เกตเวย์ API และการค้นพบบริการ
- ความทนทาน: ตัวตัดวงจรและการลองใหม่