معمارية الخدمات المصغّرة وتصميمها

مشروع: تصميم نظام خدمات مصغّرة

18 دقيقة الدرس 10 من 12

مشروع: تصميم نظام خدمات مصغّرة

كل مبدأ غطّيناه في هذا البرنامج التعليمي — السياقات المحدودة، وقاعدة بيانات لكل خدمة، والتواصل المتزامن وغير المتزامن، وتنسيق الـ Saga، والمخاوف المشتركة — يجب أن يتقاطع في نهاية المطاف في تصميم واحد متماسك. يشرح هذا الدرس بالتفصيل كيفية تحليل نطاق تجاري واقعي لبيئة تجارة إلكترونية وتقسيمه إلى خدمات ذات حدود محكمة، مع تبرير كل قرار بمستوى يحتاجه المطور في العمل الفعلي، ويُظهر كيف تتصل الخدمات الناتجة كمكوّنات Spring Boot 3 قابلة للتشغيل.

النطاق: سوق إلكتروني

يُدير النشاط التجاري سوقًا إلكترونيًا يضع فيه البائعون المنتجات للبيع، ويضع العملاء طلبات الشراء، ويُجري معالج المدفوعات عمليات الدفع ببطاقات الائتمان، ويتولى فريق التنفيذ شحن الطرود. تشمل القدرات الإضافية البحث والإشعارات وبوابة تقارير الإدارة. قبل كتابة فئة واحدة تحتاج إلى رسم خريطة النطاق باستخدام Event Storming — سرد كل حدث في النطاق بترتيب زمني، ثم تلوينها حسب قدرة الأعمال للكشف عن حدود الخدمات الطبيعية.

أبرز أحداث النطاق المُحدَّدة:

  • ProductListedBySeller, ProductUpdated, ProductDeactivated
  • CartItemAdded, CheckoutInitiated
  • OrderPlaced, OrderConfirmed, OrderCancelled
  • PaymentAuthorised, PaymentFailed, RefundIssued
  • ShipmentCreated, PackageDispatched, DeliveryConfirmed
  • CustomerRegistered, EmailVerified, PasswordChanged

يؤدي تجميع الأحداث حسب الفريق المسؤول عنها — لا حسب التشابه التقني — إلى السياقات المحدودة التالية:

التحليل إلى خدمات

تظهر ست خدمات من خريطة الأحداث. كل منها تمتلك قاعدة بياناتها الخاصة، وتُعرّض واجهة REST API للقراءات المتزامنة، وتنشر وتستهلك الأحداث عبر Kafka لعمليات تغيير الحالة.

  • Catalog Service — عمليات CRUD للمنتجات؛ فهرس Elasticsearch للبحث. ينشر ProductUpdated.
  • Order Service — عربة التسوق ودورة حياة الطلب. ينشر OrderPlaced وOrderCancelled؛ يستمع إلى PaymentAuthorised وShipmentCreated.
  • Payment Service — تكامل بوابة الدفع. ينشر PaymentAuthorised وPaymentFailed.
  • Inventory Service — حجز المخزون. يستمع إلى OrderPlaced؛ ينشر StockReserved وStockInsufficient.
  • Fulfilment Service — ملصقات الشحن والتكامل مع شركات الناقل. يستمع إلى PaymentAuthorised؛ ينشر PackageDispatched.
  • Identity Service — حسابات المستخدمين وإصدار JWT وإدارة كلمات المرور. تتحقق جميع الخدمات الأخرى من الرموز الصادرة عنه.
مقياس الحجم المناسب: تكون الخدمة ذات حجم مناسب حين يستطيع مطوّر واحد استيعاب نموذج نطاقها كاملًا في ذهنه، ويمكن لفريق نشرها باستقلالية، ولا يستلزم تعديلها أي تنسيق مع فرق أخرى. إذا فشل أي من هذه المعايير الثلاثة، فإن الحدود خاطئة.

Order Service — الهيكل الأساسي

تُوضّح خدمة الطلبات بنية Spring Boot 3 النموذجية. تُعرّض واجهة REST API لسير عملية الدفع وتنشر أحداث النطاق إلى Kafka للخدمات المنبثقة.

// OrderService.java — خدمة تطبيق package com.marketplace.order.application; import com.marketplace.order.domain.*; import com.marketplace.order.events.OrderEventPublisher; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor public class OrderService { private final OrderRepository orderRepository; private final OrderEventPublisher eventPublisher; @Transactional public Order placeOrder(PlaceOrderCommand cmd) { Order order = Order.create(cmd.customerId(), cmd.items()); orderRepository.save(order); // نشر الحدث بعد تأكيد قاعدة البيانات لتجنب الأحداث الوهمية eventPublisher.publish(new OrderPlacedEvent(order.getId(), order.getItems())); return order; } @Transactional public void confirmPayment(OrderId orderId) { Order order = orderRepository.findById(orderId) .orElseThrow(() -> new OrderNotFoundException(orderId)); order.markPaymentConfirmed(); orderRepository.save(order); } }
انشر الأحداث بعد الإيداع. إذا نشرت إلى Kafka داخل المعاملة وفشل الإيداع لاحقًا، فقد تصرّفت الخدمات المنبثقة بالفعل استجابةً لحدث وهمي. استخدم صندوق الصادر المعاملاتي (transactional outbox) أو @TransactionalEventListener(phase = AFTER_COMMIT) الخاص بـ Spring لضمان نشر الحدث مرة واحدة بالضبط نسبةً إلى الكتابة في قاعدة بياناتك.

عقود أحداث Kafka

مخططات الأحداث هي واجهتك البرمجية العامة. استخدم Avro مع Schema Registry حتى تُكتشف التغييرات غير المتوافقة في وقت البناء لا في وقت التشغيل على الإنتاج. مخطط Avro أدنى حد لـ OrderPlaced:

{ "namespace": "com.marketplace.events", "type": "record", "name": "OrderPlaced", "fields": [ { "name": "orderId", "type": "string" }, { "name": "customerId", "type": "string" }, { "name": "occurredAt", "type": "long", "logicalType": "timestamp-millis" }, { "name": "items", "type": { "type": "array", "items": { "type": "record", "name": "OrderItem", "fields": [ { "name": "productId", "type": "string" }, { "name": "quantity", "type": "int" }, { "name": "unitPrice", "type": "string" } ] } }} ] }

تستمع خدمة المخزون إلى الموضوع ذاته. مُستهلكها مُتسامح مع التكرار (idempotent) — إذا وصلت رسالة OrderPlaced ذاتها مرتين (Kafka تضمن التسليم مرة على الأقل)، تجد المحاولة الثانية الحجز قائمًا بالفعل وتُهمله:

// InventoryEventConsumer.java @Component @RequiredArgsConstructor public class InventoryEventConsumer { private final StockReservationRepository reservationRepo; private final StockReservationService reservationService; @KafkaListener(topics = "order.placed", groupId = "inventory-service") @Transactional public void onOrderPlaced(OrderPlaced event) { if (reservationRepo.existsByOrderId(event.getOrderId())) { return; // حارس التسامح مع التكرار } reservationService.reserve(event.getOrderId(), event.getItems()); } }

Saga الدفع عند الخروج

إتمام الطلب هو Saga متعددة الخدمات: Order → Inventory → Payment → Fulfilment. استخدم Saga قائمة على التنسيق الكوريوغرافي لهذا السير — لا منسّق مركزي، كل خدمة تتفاعل مع الأحداث وتنشر أحداث التعويض عند الفشل:

  1. Order Service ينشر OrderPlaced.
  2. Inventory Service يحجز المخزون → ينشر StockReserved أو StockInsufficient.
  3. Payment Service يُجري الدفع → ينشر PaymentAuthorised أو PaymentFailed.
  4. عند أي فشل، تُلغي أحداث التعويض الخطوات السابقة (StockReleased، OrderCancelled).
  5. عند النجاح الكامل، تنشئ Fulfilment Service شحنةً.

الأمان بين الخدمات: تمرير JWT

تُصدر Identity Service رمز JWT موقّعًا عند تسجيل الدخول. تتحقق كل خدمة أخرى من الرمز محليًا باستخدام المفتاح العام لـ Identity Service — لا رحلة ذهاب وإياب مع كل طلب. يُبسّط Spring Security 6 هذا إلى خاصية واحدة:

# application.yml — Catalog Service (وكذلك كل خدمة موارد أخرى) spring: security: oauth2: resourceserver: jwt: jwk-set-uri: http://identity-service/auth/.well-known/jwks.json
// SecurityConfig.java — مشترك بين جميع خوادم الموارد @Configuration @EnableWebSecurity public class SecurityConfig { @Bean SecurityFilterChain api(HttpSecurity http) throws Exception { return http .csrf(csrf -> csrf.disable()) // API عديم الحالة .sessionManagement(s -> s.sessionCreationPolicy(STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/actuator/health").permitAll() .anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())) .build(); } }
استدعاءات الخدمة إلى الخدمة يجب أن تحمل هي الأخرى رمز JWT. حين تستدعي Order Service نقطة نهاية REST الخاصة بـ Inventory Service مباشرةً (مثلًا للتحقق الفوري من المخزون)، يجب أن ترفق رمز JWT لحساب الخدمة، لا رمز المستخدم النهائي. استخدم منحة ClientCredentials (من آلة إلى آلة) حتى تتمكن Inventory Service من التدقيق في هوية المُستدعي.

Spring Cloud Gateway — نقطة الدخول الوحيدة

اعرض اسم مضيف عام واحد فقط. يوجّه Spring Cloud Gateway الطلبات إلى الخدمة الصحيحة، ويُنفّذ المصادقة عند الحافة، ويُزيل الترويسات الداخلية التي لا ينبغي للعملاء رؤيتها:

# gateway application.yml spring: cloud: gateway: routes: - id: catalog uri: lb://catalog-service # يُحلَّل عبر Eureka predicates: - Path=/api/v1/products/** filters: - RemoveRequestHeader=X-Internal-User-Id - AddRequestHeader=X-Gateway-Version, 1 - id: order uri: lb://order-service predicates: - Path=/api/v1/orders/** filters: - TokenRelay= # تمرير JWT إلى الخدمات الأدنى

ربط المراقبة

في النظام الموزّع يتفرّع طلب المستخدم الواحد عبر خدمات متعددة. بدون تتبع مترابط لا تستطيع إعادة بناء ما حدث. أضف Micrometer Tracing مع Zipkin إلى كل خدمة — يُهيّئه Spring Boot 3 تلقائيًا من تبعيّتين:

<!-- pom.xml — أضف إلى كل خدمة --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-tracing-bridge-otel</artifactId> </dependency> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-exporter-zipkin</artifactId> </dependency>

تتضمّن كل سطر في السجل تلقائيًا traceId وspanId. اجمع السجلات في مخزن مركزي (ELK أو Loki) وصفّ حسب traceId لمتابعة طلب الدفع عبر Order وInventory وPayment وFulfilment في استعلام واحد.

ملخّص قرارات التصميم

  • ست خدمات متوافقة مع قدرات الأعمال لا مع الطبقات التقنية.
  • قاعدة بيانات لكل خدمة: PostgreSQL لـ Order/Payment/Fulfilment، وMongoDB لـ Catalog، وRedis لـ Inventory (فحوصات الحجز السريعة)، وMySQL لـ Identity.
  • غير متزامن بشكل افتراضي: العمليات التي تُغيّر الحالة تمرّ عبر Kafka؛ يُحجز REST المتزامن للاستعلامات التي تحتاج ردًّا فوريًا.
  • Saga كوريوغرافي لسير الدفع؛ يكون Saga تنسيقي أفضل إذا تجاوزت الخطوات ستّة أو أصبحت منطق التعويض معقدًا.
  • JWT عند الحافة وبين الخدمات — لا حالة جلسة في أي مكان.
  • بوابة واحدة — لا يعرف العملاء قط تضاريس الخدمات؛ عناوين URL الداخلية معتمة.

مع هذه البنية في مكانها يمكن إعادة نشر أي خدمة منفردة أو تحجيمها أفقيًا أو استبدالها كليًّا دون المساس بالخدمات الأخرى — وهو الوعد المحدّد لأسلوب الخدمات المصغّرة.