The Monolith Trap
แพลตฟอร์ม B2B ทุกแห่งที่ฉันเคยสืบทอดมา เริ่มต้นจากการเป็น monolith ไม่ใช่การวิจารณ์ monolith ที่มีโครงสร้างดีจะส่งมอบได้เร็วกว่าระบบแบบกระจายใดๆ ในปีแรก ปัญหาเกิดขึ้นในปีที่สอง เมื่อทีมการชำระเงินสามคนต้องการปล่อยรุ่น 6 ครั้งต่อวัน ในขณะที่ทีมรายงานห้าคนยังอยู่ในช่วงกลางของรอบทำงานเพื่อปล่อยประจำไตรมาส ทันใดนั้น คุณก็ประสานงานการปล่อยรุ่นเหมือนการทำสงคราม และทุกการแก้ไขด่วนจะกลายเป็นการสนทนาในระดับคณะผู้บริหาร
ทางออกนี้ไม่ได้หมายถึงการเขียนใหม่ทั้งหมด มันเกี่ยวกับการดึงขอบเขตที่เป็นสิ่งที่เจ็บปวดที่สุด
Identifying Service Boundaries
หลักการกฎหมายของ Conway นั้นจริง สถาปัตยกรรมของคุณจะสะท้อนแผนผังองค์กรของคุณไม่ว่าจะวางแผนหรือไม่ก็ตาม ฉันได้หยุดการต่อสู้กับเรื่องนี้และเริ่มใช้มันโดยเจตนา
ก่อนที่จะวาดขอบเขตบริการใดๆ ฉันจะเรียกใช้คำถามสามข้อกับทีม:
- ใครเป็นเจ้าของข้อมูลนี้เฉพาะเจาะจง หากทีมสองทีมโต้แย้งว่าใครเขียนลงในตาราง ตารางนั้นควรอยู่ในบริการข้อมูลที่ใช้ร่วมกัน ไม่ใช่ในตารางแยกกันสองตาราง
- ช่วงการเสื่อมราคาของการปล่อยรุ่นที่นี่คืออะไร Billing และ user-auth สมควรได้รับการแยก Static content rendering ไม่ได้
- ลักษณะมาตราส่วนคืออะไร บริการสร้าง PDF ที่เพิ่มขึ้นในช่วงสิ้นเดือน มีความต้องการทรัพยากรที่แตกต่างไปจากเกตเวย์ websocket แบบเรียลไทม์อย่างสิ้นเชิง
สำหรับแพลตฟอร์ม B2B ส่วนใหญ่ ฉันจบลงด้วยการแบ่งหลักที่คล้ายกันโดยประมาณ บริการ auth/identity บริการ billing/subscription บริการ core domain (สิ่งที่ผลิตภัณฑ์ทำจริง) บริการ notification และบริการ reporting/analytics ที่อ่านจากกระแสอีเวนต์แทนที่จะเป็น DB หลัก
The Communication Layer
การเลือกระหว่าง synchronous REST/gRPC และ asynchronous event streaming คือจุดที่ทีมส่วนใหญ่ทำผิดพลาดครั้งแรกที่ใหญ่ พวกเขาทุ่มเททั้งหมดให้กับอย่างใดอย่างหนึ่ง
Synchronous calls (ฉันชอบ gRPC สำหรับการสื่อสาร service-to-service ภายใน) จะถูกต้องเมื่อผู้เรียกต้องการการตอบสนองเพื่อดำเนินการต่อ เช่น การสร้างเซสชั่นชำระเงิน Async events เหมาะสำหรับทุกอย่างที่สามารถทนต่อความสอดคล้องที่ยังมา เช่น การส่งอีเมลต้อนรับหรือการอัปเดตตารางรายงานที่ denormalized
รูปแบบที่ฉันใช้คือ saga สำหรับการดำเนินการธุรกิจขั้นตอนหลายขั้นตอนที่ข้ามขอบเขต service ขณะที่ลูกค้าใหม่ลงทะเบียน การไหลไปดังนี้: auth-service สร้างตัวตน → emits user.created → billing-service provisions the free tier → emits subscription.created → notification-service ส่ง welcome email ไม่มี service ใดเรียกใช้ service อื่นโดยตรง แต่ละขั้นตอนสามารถลองใหม่ได้อย่างอิสระ
// Example: Publishing a domain event with metadata
interface DomainEvent<T = unknown> {
id: string;
type: string;
aggregateId: string;
payload: T;
occurredAt: string;
correlationId: string;
}
async function publishEvent<T>(
topic: string,
event: Omit<DomainEvent<T>, 'id' | 'occurredAt'>
): Promise<void> {
const fullEvent: DomainEvent<T> = {
...event,
id: crypto.randomUUID(),
occurredAt: new Date().toISOString(),
};
await messageBus.publish(topic, fullEvent);
}// Domain event interface and publisher
import java.time.Instant;
import java.util.UUID;
public record DomainEvent<T>(
String id,
String type,
String aggregateId,
T payload,
String occurredAt,
String correlationId
) {}
@Service
public class EventPublisher {
private final KafkaTemplate<String, Object> kafkaTemplate;
public <T> void publishEvent(String topic,
String type, String aggregateId, T payload, String correlationId) {
var event = new DomainEvent<>(
UUID.randomUUID().toString(),
type,
aggregateId,
payload,
Instant.now().toString(),
correlationId
);
kafkaTemplate.send(topic, aggregateId, event);
}
}# Domain event dataclass and publisher
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Generic, TypeVar
T = TypeVar("T")
@dataclass
class DomainEvent(Generic[T]):
type: str
aggregate_id: str
payload: T
correlation_id: str
id: str = field(default_factory=lambda: str(uuid.uuid4()))
occurred_at: str = field(
default_factory=lambda: datetime.now(timezone.utc).isoformat()
)
async def publish_event(topic: str, event_data: DomainEvent) -> None:
await message_bus.publish(topic, event_data)// Domain event record and publisher
using MediatR;
public record DomainEvent<T>(
string Id,
string Type,
string AggregateId,
T Payload,
string OccurredAt,
string CorrelationId
);
public class EventPublisher
{
private readonly IMessageBus _messageBus;
public EventPublisher(IMessageBus messageBus) => _messageBus = messageBus;
public async Task PublishEventAsync<T>(
string topic, string type, string aggregateId,
T payload, string correlationId)
{
var evt = new DomainEvent<T>(
Id: Guid.NewGuid().ToString(),
Type: type,
AggregateId: aggregateId,
Payload: payload,
OccurredAt: DateTimeOffset.UtcNow.ToString("O"),
CorrelationId: correlationId
);
await _messageBus.PublishAsync(topic, evt);
}
}Deployment and Observability
นี่คือจุดที่ข้อผิดพลาดที่สองเกิดขึ้น: ทีมคิดว่าคุณต้องการเครื่องมือที่ซับซ้อนเพื่อจัดการ microservices คุณไม่ต้อง คุณต้องการการปล่อยรุ่นที่น่าเบื่อ observable และสามารถทำซ้ำได้ Kubernetes เป็นมาตรฐานอุตสาหกรรม แต่คุณสามารถสร้างสิ่งนี้ได้ด้วย Docker Swarm, ECS หรือแม้แต่การปล่อยรุ่นแบบมือเดินได้ตราบเท่าที่คุณมี:
- Immutable deployments: แต่ละ artifact ถูกสร้าง versioned ทดสอบและส่งเสริม ไม่เคยสร้างใหม่หรือแท็กใหม่
- Blue-green deploys: ให้ทั้งสองสภาพแวดล้อมทำงาน สลับการไหลของข้อมูลอย่างอะตอม Rollback ใน seconds หากจำเป็น
- Health checks and graceful shutdown: Services ต้องส่งสัญญาณความพร้อมก่อนรับการไหลของข้อมูลและการระบายส่วนที่มีอยู่ระหว่าง shutdown
- Logging to stdout: ไม่มีไฟล์ ผลักดัน logs ไปยังระบบรวมศูนย์เช่น DataDog หรือ ELK ให้โทษ 12-factor app manifesto หากใครโต้แย้ง
สำหรับแพลตฟอร์ม B2B ฉันพบว่ารูปแบบง่ายๆ ทำงาน: เรียกใช้ cron job ทุก 5 นาทีเพื่อเปรียบเทียบสถานะที่ต้องการ (กำหนดใน git) กับสถานะจริง (สิ่งที่กำลังทำงาน) หากไม่ตรงกัน ให้ใช้การเปลี่ยนแปลง น่าเบื่อ แต่มันอยู่รอดจากการหยุดชะงักจำนวนมากและการปล่อยรุ่นดึก ๆ โดยไม่มีเหตุการณ์ที่บังเอิญ
Growing Beyond This
เมื่อแพลตฟอร์มของคุณปรับขนาดขึ้น คุณจะเผชิญกับปัญหาใหม่: distributed tracing, rate limiting ข้ามขอบเขต service, circuit breakers และ compensation logic สำหรับ sagas ที่ล้มเหลว สิ่งเหล่านี้สามารถแก้ไขได้ด้วยเฟรมเวิร์ก mature (ฉันชอบ NestJS สำหรับทีม Node.js) แต่มันเป็นปัญหาที่คุณต้องการมี หมายความว่าผลิตภัณฑ์ของคุณเติบโตเร็วพอที่จะเข้าใจ
สถาปัตยกรรมที่ฉันอธิบายจะพาคุณไปถึงตัวเลขเจ็ดหลักในรายได้ประจำปีโดยไม่มีความเครียด เกินกว่านั้น แพลตฟอร์มมักจะแบ่ง: ชั้น API, ตรรมชาติ business logic และชั้น reporting แต่ละอันเป็น “universe” ของตนเอง ที่ปรับขนาดอย่างอิสระ นั่นคือปัญหาในอนาคต สำหรับตอนนี้ ให้มุ่งเน้นที่ความชัดเจนด้านการจัดการและความปลอดภัยในการปล่อยรุ่น