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

المبادئ الأساسية للخدمات المصغّرة

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

المبادئ الأساسية للخدمات المصغّرة

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

المبدأ الأول: المسؤولية الواحدة

مبدأ المسؤولية الواحدة (SRP) ليس جديدًا — صاغه روبرت مارتن للفئات في تسعينيات القرن الماضي. في معمارية الخدمات المصغّرة يرتقي هذا المبدأ إلى مستوى أعلى: ينبغي أن يكون لكل خدمة سببٌ واحد فقط للتغيير. وعادةً ما يرتبط هذا السبب بقدرة تجارية واحدة.

لنأخذ منصة تجارة إلكترونية مثالًا. قد يبدو التقسيم البسيط هو "خدمة الواجهة الأمامية" و"خدمة الواجهة الخلفية"، لكن هذا لا يعكس توزيع المسؤوليات؛ فكلتاهما تتغيّر عند أي متطلب تجاري. أما التقسيم المبني على المسؤوليات فيبدو كالتالي:

  • خدمة الطلبات (Order Service) — تمتلك دورة حياة الطلب (تم الطلب، مؤكَّد، شُحن، مُلغى).
  • خدمة المخزون (Inventory Service) — تمتلك مستويات المخزون والحجز وإعادة التوريد.
  • خدمة الإشعارات (Notification Service) — تمتلك كيفية ووقت التواصل مع العملاء (بريد إلكتروني، SMS، إشعار فوري).
  • خدمة التسعير (Pricing Service) — تمتلك قواعد الأسعار والخصومات والعروض الترويجية.

بهذا الهيكل، إضافة نوع عرض ترويجي جديد يعني تعديل خدمة التسعير فقط. تغيير مزوّد البريد الإلكتروني يعني تعديل خدمة الإشعارات فقط. لا يؤثر أي من التغييرين في الخدمات الأخرى.

اختبار مبدأ المسؤولية الواحدة: إذا وصف المطوّر ما تفعله الخدمة باستخدام كلمة "و" — "تتعامل مع الطلبات وتُرسل رسائل بريدية وتُدير المخزون" — فالخدمة تنتهك المبدأ وينبغي تقسيمها.

المسؤولية الواحدة عمليًا: خدمة الطلبات بـ Spring Boot

تعرض خدمة الطلبات المصمَّمة بشكل صحيح عمليات الطلبات فحسب. يتعامل الـ@RestController مع أوامر واستعلامات الطلبات؛ لا يحتوي على منطق البريد الإلكتروني أو منطق تخفيض المخزون. تُنشر هذه الأحداث ليتعامل معها الآخرون بشكل مستقل.

// order-service/src/main/java/com/example/order/OrderController.java @RestController @RequestMapping("/api/orders") @RequiredArgsConstructor public class OrderController { private final OrderService orderService; @PostMapping @ResponseStatus(HttpStatus.CREATED) public OrderResponse placeOrder(@Valid @RequestBody PlaceOrderRequest request) { return orderService.place(request); } @GetMapping("/{id}") public OrderResponse getOrder(@PathVariable Long id) { return orderService.findById(id); } @PatchMapping("/{id}/cancel") public OrderResponse cancelOrder(@PathVariable Long id) { return orderService.cancel(id); } }

لاحظ ما هو غائب: لا EmailSender، لا StockRepository، ولا استدعاء لبوابة الدفع. تُصدر الخدمة حدثًا خاصًا بالنطاق (domain event) بعد تغييرات الحالة، وتتعامل معه الخدمات المجاورة.

// ضمن OrderService.place(): Order saved = orderRepository.save(order); // نشر الحدث — تستهلكه خدمتا الإشعارات والمخزون بشكل مستقل applicationEventPublisher.publishEvent(new OrderPlacedEvent(saved.getId(), saved.getCustomerId())); return toResponse(saved);

المبدأ الثاني: الاستقلالية والقابلية للنشر المستقل

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

للاستقلالية متطلبات عملية محددة:

  • مخزن بيانات خاص: تمتلك الخدمة مخطط قاعدة بياناتها الخاص (أو نسخة قاعدة بيانات مستقلة). لا يحق لأي خدمة أخرى الوصول إلى جداولها مباشرةً — التواصل يكون دائمًا عبر الواجهة البرمجية (API) أو تدفق الأحداث.
  • خط بناء ونشر خاص: تمتلك الخدمة ملف Dockerfile الخاص بها وخط CI/CD الخاص بها ومانيفست نشر Kubernetes الخاص بها.
  • تغييرات API متوافقة مع الإصدارات السابقة: إضافة حقل إلى الاستجابة أمرٌ آمن؛ حذفه أو إعادة تسميته يكسر المستهلكين. تطوّر الخدمات المستقلة واجهاتها بعناية (اختبار العقد المُوجَّه بالمستهلك يساعد في ذلك).
  • عزل وقت التشغيل: تعطّل خدمة الإشعارات يجب ألّا يتسرّب إلى خدمة الطلبات. تفرض قواطع الدارة (Resilience4j) والمهلات الزمنية هذا الحاجز.
التوافق مع قانون كونواي: تعمل الخدمات المستقلة بشكل أفضل حين تتوافق حدود الفرق مع حدود الخدمات. إذا كان الفريق نفسه يمتلك خمس خدمات فبإمكانه التحرك بسرعة. أما إذا اشترك خمسة فرق في خدمة واحدة فكل نشر يحتاج تنسيقًا — وبذلك عدت عمليًا إلى المنوليث.

القابلية للنشر المستقل في خدمة Spring Boot

يُجمّع تطبيق Spring Boot كل ما يحتاجه في ملف JAR قابل للتنفيذ (يُعرف بـ "fat JAR" أو "uber JAR"). هذا ما يجعل النشر المستقل ممكنًا عمليًا.

# order-service/Dockerfile FROM eclipse-temurin:21-jre-alpine WORKDIR /app COPY target/order-service-*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]

يحتوي ملف application.yml لكل خدمة على تهيئتها الخاصة فقط. لا تشترك في ملف إعدادات عام مع الخدمات الأخرى (وإن كانت قد تسحب قيمًا من Config Server — إلا أن كل خدمة تقرأ قسمها الخاص).

# order-service/src/main/resources/application.yml spring: application: name: order-service datasource: url: ${DB_URL:jdbc:postgresql://localhost:5432/orders} username: ${DB_USER:orders_user} password: ${DB_PASS:changeme} jpa: hibernate: ddl-auto: validate server: port: 8080 management: endpoints: web: exposure: include: health,info,metrics

الانعكاسات الأمنية للاستقلالية

حين تكون الخدمات مستقلة، تُصبح كل خدمة حدًا أمنيًا قائمًا بذاته. لهذا تبعات مباشرة:

  • المصادقة موزَّعة: يجب على كل خدمة التحقق من الرموز المميزة (tokens) بشكل مستقل. في Spring Security 6 تُهيَّأ كل خدمة كـ OAuth 2.0 Resource Server يتحقق من JWT مقابل نقطة JWKS الخاصة بخادم التفويض.
  • أسرار خاصة بكل خدمة: تمتلك كل خدمة بيانات اعتماد قاعدة بياناتها ومفاتيح API وشهادات TLS الخاصة بها. اختراق خدمة واحدة لا ينبغي أن يُفضي إلى كشف بيانات اعتماد خدمة أخرى.
  • مبدأ الحد الأدنى من الصلاحيات على مستوى الشبكة: لا ينبغي أن تملك خدمة الطلبات وصولًا شبكيًا إلى قاعدة بيانات خدمة الإشعارات. تفرض سياسات الشبكة في Kubernetes أو شبكة الخدمات (service mesh) هذا القيد.
// كل خدمة تُهيّئ Spring Security بشكل مستقل كـ Resource Server @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/actuator/health").permitAll() .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt.jwkSetUri("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")) ); return http.build(); } }

مقايضات الأنظمة الموزَّعة

المسؤولية الواحدة والاستقلالية ليستا مجانيتين. كل تقسيم يُدخل قفزات شبكية حيث كانت استدعاءات الدوال كافية. ثمة حقائق يجب على كل مطوّر عملي القبول بها:

  • استدعاءات الشبكة قد تفشل. ربط قاعدة بيانات مباشر كان يستغرق ميكروثانية أصبح استدعاءً HTTP قد ينتهي بمهلة زمنية. يجب تصميم النظام لاستيعاب الأعطال الجزئية منذ البداية.
  • الاتساق أصعب. يمكن للمنوليث تضمين عمليتين في معاملة قاعدة بيانات واحدة. أما الخدمتان المستقلتان فلا تستطيعان ذلك — تحتاج إلى نمط Saga أو نمط Outbox أو الاتساق التدريجي (نتناول هذا في دروس لاحقة).
  • التكلفة التشغيلية حقيقية. تشغيل عشر خدمات يعني عشر مجموعات من السجلات وعشر لوحات تحكم وعشر مجموعات من مانيفستات النشر. استثمر في قابلية الملاحظة (التتبع الموزَّع، السجلات المنظَّمة، المقاييس) مبكرًا.
لا تُقسّم مبكرًا. لم تبدأ Amazon وNetflix وUber بمئات من الخدمات المصغّرة. بدأت بمنوليثات واستخرجت الخدمات عبر خطوط انفصال مُثبتة. التقسيم المبكر قبل فهم حدود النطاق جيدًا ينتج خدمات مرتبطة ارتباطًا وثيقًا ببعضها — أسوأ العالمَين.

الخلاصة

المبدأان الجوهريان — المسؤولية الواحدة والاستقلالية مع القابلية للنشر المستقل — هما الأساس الذي تُبنى عليه كل أنماط الخدمات المصغّرة الأخرى. تُبقي المسؤولية الواحدة الخدمات صغيرة ومجموعات التغييرات محدودة. تُبقي الاستقلالية الفرق حرّة والمخاطر المرتبطة بالنشر منخفضة. تتجلى هذه المبادئ في Spring Boot 3 عبر: وحدات تحكم مركّزة، ونشر أحداث النطاق، وقواعد بيانات وإعدادات مستقلة لكل خدمة، وملفات Dockerfile منفردة، وتهيئة Spring Security لكل خدمة. في الدرس التالي ننتقل من المبادئ إلى الحدود: كيف نُحدّد أين تنتهي خدمة وتبدأ أخرى باستخدام مفهوم السياقات المحدودة (Bounded Contexts) في التصميم المُوجَّه بالنطاق.