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

الاهتمامات المشتركة عبر الخدمات

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

الاهتمامات المشتركة عبر الخدمات

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

الإعداد المركزي باستخدام Spring Cloud Config

تخزين القيم الخاصة بالبيئة داخل ملف الثنائي للخدمة ينتهك مبدأ تطبيق الاثني عشر عاملاً "خزّن الإعداد في البيئة". يُخارج Spring Cloud Config Server الإعداد إلى مستودع Git (أو Vault أو S3 وغيرها) ويقدّمه عبر HTTP لكل خدمة عميلة عند بدء التشغيل — والأهم — أثناء التشغيل عبر إشارة تحديث.

أضف تبعية الخادم إلى تطبيق Spring Boot مخصص لخادم الإعداد:

<!-- pom.xml الخاص بتطبيق config-server --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency>
// ConfigServerApplication.java @SpringBootApplication @EnableConfigServer public class ConfigServerApplication { public static void main(String[] args) { SpringApplication.run(ConfigServerApplication.class, args); } }
# application.yml الخاص بـ config-server server: port: 8888 spring: cloud: config: server: git: uri: https://github.com/acme/microservices-config default-label: main clone-on-start: true

تُعلن الخدمات العميلة عن هويتها وعنوان خادم الإعداد في ملف bootstrap.yml (أو عبر متغيرات البيئة للحاويات). يحل الخادم {application}/{profile}/{label} إلى ملف في مستودع Git:

# bootstrap.yml الخاص بـ order-service spring: application: name: order-service # يحل إلى order-service.yml في المستودع config: import: "configserver:http://config-server:8888" cloud: config: fail-fast: true # توقف عند بدء التشغيل إذا كان الإعداد غير متوفر retry: max-attempts: 6 initial-interval: 2000
لا تخزّن الأسرار كنص صريح في Git أبدًا. استخدم تشفير Spring Cloud Config (عبر encrypt.key) لتخزين النص المشفّر في المستودع، أو ادعم خادم الإعداد بـ HashiCorp Vault (عبر spring.cloud.config.server.vault.*). تاريخ Git المسرَّب يكشف كل سر لأي شخص يستنسخ المستودع.

لدفع تغيير الإعداد إلى الخدمات الجارية دون إعادة تشغيل، أرسل طلب POST إلى نقطة النهاية /actuator/refresh لكل خدمة عميلة (أو استخدم Spring Cloud Bus مع وسيط رسائل لبث إشارة واحدة إلى جميع الحالات).

التسجيل الهيكلي الموزع

الطلب الذي يمر عبر خمس خدمات ينتج خمسة سطور سجل متفرقة في خمسة ملفات منفصلة. بدون معرّف ارتباط وحقول هيكلية لا يمكنك ربطها. الحل المعتمد في الصناعة هو: إصدار سجلات JSON تحتوي على حقل traceId مشترك وإرسالها إلى مخزن مركزي (ELK أو Loki أو Datadog وغيرها).

يأتي Spring Boot 3 مع Logback. اضبطه لإنتاج JSON وتضمين حقول MDC الخاصة بـ Micrometer Tracing تلقائيًا:

<!-- pom.xml --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-tracing-bridge-brave</artifactId> </dependency> <dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> <version>7.4</version> </dependency>
<!-- logback-spring.xml --> <configuration> <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="net.logstash.logback.encoder.LogstashEncoder"> <!-- يكتب Micrometer Tracing traceId/spanId في MDC تلقائيًا --> <includeMdcKeyName>traceId</includeMdcKeyName> <includeMdcKeyName>spanId</includeMdcKeyName> </encoder> </appender> <root level="INFO"> <appender-ref ref="JSON"/> </root> </configuration>

يبدو كل سطر سجل الآن كالتالي:

{ "@timestamp": "2026-06-08T14:22:01.123Z", "level": "INFO", "logger_name": "com.acme.order.OrderService", "message": "Order 99 created for customer 42", "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", "spanId": "00f067aa0ba902b7", "service": "order-service" }
أضف السياق التجاري إلى MDC عند حدود الخدمة. يمكن لفلتر أو AOP Advice في Spring دفع userId وtenantId وrequestId إلى MDC.put() في بداية كل طلب حتى يحمل كل سطر سجل ضمن ذلك الطلب هذه البيانات تلقائيًا. امسح MDC في كتلة finally لتجنب تسريب السياق عبر إعادة استخدام مجمّع الخيوط.

تمرير سياق الأمان عبر الخدمات

عندما تستدعي الخدمة A الخدمة B، تحتاج B إلى معرفة هوية المستخدم الأصلي حتى تتمكن من تطبيق قواعد التفويض الخاصة بها وإنتاج سجلات تدقيق صحيحة. الآلية المعيارية في الخدمات المصغرة المبنية على REST هي تمرير JWT (رمز JSON المميّز) موقَّع في ترويسة Authorization: Bearer <token> لكل طلب HTTP بين الخدمات.

تُعدّ كل خدمة خادم موارد باستخدام Spring Security 6 يتحقق من JWT محليًا بمفتاح الخادم العام لخادم المصادقة (أو نقطة نهاية JWKS):

// SecurityConfig.java (نفس النمط في كل خدمة) @Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/actuator/health").permitAll() .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt.jwkSetUri( "https://auth-server/.well-known/jwks.json" )) ); return http.build(); } }
# application.yml — إعداد JWT لخادم الموارد spring: security: oauth2: resourceserver: jwt: jwk-set-uri: https://auth-server/.well-known/jwks.json issuer-uri: https://auth-server

عندما تستدعي الخدمة A الخدمة B باستخدام RestTemplate أو WebClient، تُمرّر نفس الـ JWT من الطلب الوارد:

// BearerTokenRelay.java — فلتر يُخزّن الرمز الخام ويُمرّره @Component public class BearerTokenRelay implements ExchangeFilterFunction { @Override public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) { // ReactiveSecurityContextHolder يوفر الـ JWT في المكدس التفاعلي؛ // للمكدسات المتزامنة اقرأه من SecurityContextHolder بدلاً من ذلك return ReactiveSecurityContextHolder.getContext() .map(ctx -> (JwtAuthenticationToken) ctx.getAuthentication()) .map(auth -> auth.getToken().getTokenValue()) .defaultIfEmpty("") .flatMap(token -> { ClientRequest forwarded = ClientRequest.from(request) .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) .build(); return next.exchange(forwarded); }); } }
لا تُصدر رموزًا جديدة داخل خدمة لاستدعاء خدمات تالية أبدًا. فهذا يُخفي هوية المستخدم الأصلي من سجل تدقيق الخدمة التالية ويُعطّل سلاسل المساءلة. مرّر الـ JWT الأصلي؛ وإن كانت مدة صلاحيته قصيرة جدًا لسير العمل طويلة الأمد، استخدم تبادل رمز OAuth 2.0 (RFC 8693) للحصول على رمز تالٍ محدود النطاق لا يزال يحمل مطالبة الموضوع الأصلي.

التتبع الموزع باستخدام Micrometer Tracing و Zipkin

السجلات تخبرك بما حدث في خدمة واحدة. التتبع الموزع يخبرك كيف تدفق طلب مستخدم واحد عبر كل خدمة مع توقيت كل خطوة. التتبع (Trace) هو الرحلة الكاملة؛ الامتداد (Span) هو وحدة عمل واحدة ضمن تلك الرحلة. كل خدمة تشارك في نفس الطلب تشترك في نفس traceId لكنها تنشئ spanId فرعيًا خاصًا بها.

يستخدم Spring Boot 3 Micrometer Tracing كطبقة تجريد فوق Brave (Zipkin) أو OpenTelemetry:

<!-- pom.xml --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-tracing-bridge-brave</artifactId> </dependency> <dependency> <groupId>io.zipkin.reporter2</groupId> <artifactId>zipkin-reporter-brave</artifactId> </dependency>
# application.yml — إعداد التتبع management: tracing: sampling: probability: 1.0 # 100% في التطوير؛ استخدم 0.1 (10%) في الإنتاج zipkin: tracing: endpoint: http://zipkin:9411/api/v2/spans spring: application: name: order-service # يظهر كاسم الخدمة في واجهة Zipkin

يُضيف Spring Boot أدوات قياس تلقائية لطلبات HTTP الواردة واستدعاءات RestTemplate/WebClient الصادرة وخيوط @Async. للعمليات التجارية المخصصة، لفّها في امتداد يدوي:

@Service public class InventoryClient { private final Tracer tracer; private final WebClient webClient; public InventoryClient(Tracer tracer, WebClient.Builder builder) { this.tracer = tracer; this.webClient = builder.baseUrl("http://inventory-service").build(); } public Mono<Integer> checkStock(Long productId) { Span span = tracer.nextSpan().name("inventory.checkStock").start(); try (Tracer.SpanInScope ws = tracer.withSpan(span)) { span.tag("product.id", String.valueOf(productId)); return webClient.get() .uri("/api/stock/{id}", productId) .retrieve() .bodyToMono(Integer.class) .doOnError(span::error) .doFinally(sig -> span.end()); } } }
التتبع والتسجيل والمقاييس متكاملة لا بديلة. يُظهر التتبع طوبولوجيا الطلب البطيء أو الفاشل. المقاييس (Prometheus/Micrometer) تُظهر معدلات الإنتاجية والأخطاء المجمّعة. السجلات الهيكلية تحمل السياق التجاري في كل عقدة. تحتاج مراقبة الإنتاج إلى الثلاثة مرتبطين معًا: نفس traceId يظهر في سجلاتك وفي واجهة التتبع وفي مقاييس Prometheus (عبر Exemplars)، حتى تتمكن من الانتقال بينها فورًا.

الخلاصة

تتطلب الاهتمامات المشتركة عبر الخدمات تطبيقًا متعمدًا ومتسقًا على كل خدمة في المنظومة. يُمركز Spring Cloud Config الإعداد المُخارَج ويُتيح التحديثات دون توقف. التسجيل الهيكلي بصيغة JSON مع حقول MDC المشتركة يُمكّن ارتباط السجلات على نطاق واسع. تمرير JWT ينقل سياق الأمان دون إعادة المصادقة في كل خطوة، مما يُبقي قواعد التفويض دقيقة ومسار التدقيق مكتملاً. يربط Micrometer Tracing سجلات الخدمات الفردية في تتبع موحد من البداية إلى النهاية يكشف نقاط الاختناق في زمن الاستجابة وحالات الفشل عبر الرسم البياني الكامل للاستدعاءات. هذه الركائز الأربع مجتمعةً هي الأساس التشغيلي الذي بدونه يصبح نظام الخدمات المصغرة مستحيل التأمين والتشخيص والتشغيل بشكل موثوق.