المرونة والمراسلة والرصد

مشروع: خدمة مرنة وقابلة للمراقبة

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

مشروع: خدمة مرنة وقابلة للمراقبة

يجمع هذا الدرس الختامي كل الأنماط التي تناولناها في البرنامج التعليمي في خدمة Spring Boot 3 واحدة جاهزة للإنتاج. ستبني خدمة معالجة الطلبات (Order Processing Service) التي تستدعي خدمة المخزون (Inventory Service) عبر HTTP، وتنشر أحداث المجال إلى Kafka، وتعرض مقاييس Micrometer، وتدمج قواطع الدائرة (circuit breakers) وآليات إعادة المحاولة من Resilience4j، وتبعث آثار موزعة يمكن لـ Zipkin جمعها. بنهاية الدرس ستمتلك خدمة تستطيع تشغيلها وإيقافها ومشاهدة تعافيها — وهي المهارة الجوهرية لتشغيل الخدمات المصغرة على نطاق واسع.

معمارية المشروع

تتألف الخدمة من ثلاثة محاور مترابطة:

  • واجهة REST الواردة — تقبل طلبات POST /orders من العملاء.
  • استدعاء خارجي مرن — تستعلم inventory-service بقاطع دائرة وإعادة محاولة ومهلة زمنية.
  • نشر الأحداث — تبعث حدث OrderPlaced إلى موضوع Kafka بعد حجز المخزون بنجاح.

المراقبة ليست إضافةً لاحقة — بل هي مدمجة منذ السطر الأول عبر Micrometer وSpring Boot Actuator وMicrometer Tracing.

التبعيات (pom.xml)

<dependencies> <!-- الويب --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Resilience4j عبر Spring Cloud --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId> </dependency> <!-- Kafka --> <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> </dependency> <!-- Actuator + Micrometer Prometheus --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency> <!-- التتبع الموزع (Brave / Zipkin) --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-tracing-bridge-brave</artifactId> </dependency> <dependency> <groupId>io.zipkin.reporter2</groupId> <artifactId>zipkin-reporter-brave</artifactId> </dependency> </dependencies>

application.yml — الإعداد المركزي

الاحتفاظ بسياسة المرونة وإعداد Kafka وأخذ عينات المراقبة في ملف واحد يجعل سلوك الخدمة موثقًا ذاتيًا.

spring: application: name: order-service kafka: bootstrap-servers: localhost:9092 producer: key-serializer: org.apache.kafka.common.serialization.StringSerializer value-serializer: org.springframework.kafka.support.serializer.JsonSerializer management: endpoints: web: exposure: include: health,info,metrics,prometheus,circuitbreakers tracing: sampling: probability: 1.0 # 100% في بيئة التطوير؛ اضبطها على 0.1 في الإنتاج resilience4j: circuitbreaker: instances: inventory: slidingWindowSize: 10 failureRateThreshold: 50 waitDurationInOpenState: 10s permittedNumberOfCallsInHalfOpenState: 3 retry: instances: inventory: maxAttempts: 3 waitDuration: 500ms retryExceptions: - java.io.IOException - org.springframework.web.client.ResourceAccessException timelimiter: instances: inventory: timeoutDuration: 2s inventory: base-url: http://localhost:8081

نموذج المجال

public record OrderRequest(String productId, int quantity) {} public record OrderResult(String orderId, String status, String message) {} public record InventoryResponse(String productId, boolean available, int stock) {} public record OrderPlacedEvent(String orderId, String productId, int quantity, long timestamp) {}

InventoryClient — استدعاء HTTP مرن

يغلّف العميل RestTemplate بتعليقات Resilience4j التوضيحية. تُطبَّق @CircuitBreaker و@Retry على مستوى الدالة؛ أما @TimeLimiter فيُعدَّل من الإعداد. يجب أن يعكس توقيع دالة الاحتياط (fallback) الدالةَ المحمية بإضافة معامل Throwable.

@Component public class InventoryClient { private final RestTemplate restTemplate; @Value("${inventory.base-url}") private String baseUrl; public InventoryClient(RestTemplateBuilder builder) { this.restTemplate = builder.build(); } @CircuitBreaker(name = "inventory", fallbackMethod = "inventoryFallback") @Retry(name = "inventory") public InventoryResponse checkStock(String productId) { String url = baseUrl + "/inventory/" + productId; return restTemplate.getForObject(url, InventoryResponse.class); } // تُستدعى عند فتح الدائرة أو استنفاد جميع محاولات إعادة المحاولة public InventoryResponse inventoryFallback(String productId, Throwable ex) { // تدهور متحكم به — افترض عدم التوفر بدلًا من الانهيار return new InventoryResponse(productId, false, 0); } }
يجب ألا تنفّذ دالة الاحتياط أي عمليات إدخال/إخراج. إذا استدعت دالة الاحتياط قاعدة بيانات أو خدمة أخرى فأنت تخاطر بأعطال متتالية. أعد قيمة مخزنة مؤقتًا أو قيمة افتراضية آمنة أو استجابة خطأ تدل على فتح الدائرة — ولا تضف قط شبكة اتصال أخرى.

OrderService — منطق الأعمال مع المقاييس

@Service public class OrderService { private final InventoryClient inventoryClient; private final KafkaTemplate<String, OrderPlacedEvent> kafkaTemplate; private final MeterRegistry meterRegistry; private final Counter ordersPlaced; private final Counter ordersRejected; public OrderService(InventoryClient inventoryClient, KafkaTemplate<String, OrderPlacedEvent> kafkaTemplate, MeterRegistry meterRegistry) { this.inventoryClient = inventoryClient; this.kafkaTemplate = kafkaTemplate; this.meterRegistry = meterRegistry; this.ordersPlaced = Counter.builder("orders.placed") .description("الطلبات المنفّذة بنجاح") .register(meterRegistry); this.ordersRejected = Counter.builder("orders.rejected") .description("الطلبات المرفوضة بسبب المخزون") .register(meterRegistry); } @Observed(name = "order.place", contextualName = "placing-order") public OrderResult placeOrder(OrderRequest request) { InventoryResponse stock = inventoryClient.checkStock(request.productId()); if (!stock.available() || stock.stock() < request.quantity()) { ordersRejected.increment(); return new OrderResult(null, "REJECTED", "مخزون غير كافٍ"); } String orderId = UUID.randomUUID().toString(); OrderPlacedEvent event = new OrderPlacedEvent( orderId, request.productId(), request.quantity(), System.currentTimeMillis()); kafkaTemplate.send("orders.placed", orderId, event); ordersPlaced.increment(); // تسجيل ملخص توزيع لحجم الطلب meterRegistry.summary("order.quantity").record(request.quantity()); return new OrderResult(orderId, "PLACED", "تم قبول الطلب"); } }
@Observed تنشئ نطاقًا تلقائيًا. يلتقط Micrometer Tracing التعليق التوضيحي @Observed (عبر جانب AOP) ويغلّف الدالة في نطاق تتبع. تحصل على آثار موزعة دون كتابة أي كود Tracer في كل دالة.

OrderController

@RestController @RequestMapping("/orders") public class OrderController { private final OrderService orderService; public OrderController(OrderService orderService) { this.orderService = orderService; } @PostMapping public ResponseEntity<OrderResult> place(@RequestBody @Valid OrderRequest request) { OrderResult result = orderService.placeOrder(request); HttpStatus status = "PLACED".equals(result.status()) ? HttpStatus.CREATED : HttpStatus.UNPROCESSABLE_ENTITY; return ResponseEntity.status(status).body(result); } }

مشاهدة قاطع الدائرة أثناء العمل

يعرض Spring Boot Actuator نقطة نهاية مخصصة لذلك. أثناء توقف الخدمة المجاورة، أرسل عدة طلبات وراقب فتح الدائرة:

# التحقق من حالة قاطع الدائرة عبر Actuator curl http://localhost:8080/actuator/circuitbreakers # مثال على الاستجابة (حالة OPEN بعد تجاوز العتبة): # { # "circuitBreakers": { # "inventory": { # "failureRate": "60.0%", # "state": "OPEN", # "bufferedCalls": 10, # "failedCalls": 6 # } # } # }

نقطة نهاية مقاييس Prometheus

استعلم /actuator/prometheus لرؤية جميع عدّادات Micrometer بما فيها العدّادات المخصصة ومقاييس تكامل Resilience4j:

# عدّادات التطبيق المخصصة orders_placed_total 42.0 orders_rejected_total 7.0 # مقاييس قاطع الدائرة من Resilience4j (مسجَّلة تلقائيًا) resilience4j_circuitbreaker_calls_seconds_count{kind="successful",name="inventory"} 38.0 resilience4j_circuitbreaker_calls_seconds_count{kind="failed",name="inventory"} 4.0 resilience4j_circuitbreaker_state{name="inventory",state="closed"} 1.0 # توزيع حجم الطلب order_quantity_count 42.0 order_quantity_sum 187.0 order_quantity_max 15.0

تدفق الأثر الموزع

يحصل كل طلب HTTP وارد تلقائيًا على traceId يحقنه Micrometer Tracing. ينتقل الأثر إلى رأس سجل Kafka (عبر أجهزة Brave Kafka) وإلى استدعاء HTTP الصادر (عبر RestTemplate المُجهَّز). في Zipkin ترى:

  • النطاق 1: POST /orders — النطاق الجذر.
  • النطاق 2: placing-order — نطاق @Observed داخل OrderService.
  • النطاق 3: GET inventory/{productId} — الاستدعاء الصادر، بما فيه محاولات إعادة المحاولة من Resilience4j كنطاقات فرعية.
  • النطاق 4: orders.placed send — نطاق نشر Kafka.

اختبار المرونة

أنظف طريقة لاختبار قاطع الدائرة هي استخدام @SpringBootTest مع وهم WireMock يُعيد أخطاء 500:

@SpringBootTest(webEnvironment = RANDOM_PORT) @AutoConfigureWireMock(port = 8081) class OrderServiceResilienceTest { @Autowired TestRestTemplate client; @Autowired CircuitBreakerRegistry cbRegistry; @Test void circuitOpensAfterRepeatedFailures() { // وهم المخزون ليفشل دائمًا stubFor(get(urlPathMatching("/inventory/.*")) .willReturn(serverError())); // إرسال 10 طلبات عبر الخدمة (يتطابق مع slidingWindowSize) for (int i = 0; i < 10; i++) { client.postForEntity("/orders", new OrderRequest("PROD-1", 1), OrderResult.class); } CircuitBreaker cb = cbRegistry.circuitBreaker("inventory"); assertThat(cb.getState()).isEqualTo(CircuitBreaker.State.OPEN); } }
اختبر حالات الفشل وليس فقط المسارات السعيدة. خدمة تجتاز جميع اختبارات المسار الأخضر لكنها لم تُختبر قط في ظل فتح الدائرة أو انتهاء المهلة أو عدم توفر وسيط Kafka — لا تُعدّ جاهزة للإنتاج. اختبر خدمتك فوضويًا (chaos testing) في بيئة CI وليس في الإنتاج.

قائمة فحص الإنتاج

  • اضبط management.tracing.sampling.probability على 0.05–0.10 في الإنتاج — فأخذ عينات بنسبة 100% يُنشئ ضغطًا كبيرًا على الأداء وتكاليف تخزين مرتفعة.
  • لا تعرض /actuator/* للإنترنت العام أبدًا. ضعها على منفذ داخلي أو احمها بـ Spring Security.
  • أنشئ تنبيهات على تحولات resilience4j_circuitbreaker_state — فالدائرة المفتوحة حادثة إنتاجية وليست مجرد نقطة بيانات عابرة.
  • استخدم DeadLetterPublishingRecoverer لأعطال إرسال Kafka حتى لا يضيع أي حدث صامتًا.
  • أضف تسميات ذات معنى لجميع المقاييس المخصصة (region، env) حتى تستطيع لوحات المعلومات التصفية حسب بيئة النشر.

الخلاصة

لقد جمعت خدمةً تتحمل أعطال الخدمات المجاورة بأناقة عبر قواطع الدائرة وإعادة المحاولة، وتفصل الآثار الجانبية عبر أحداث Kafka، وتكشف حالتها الداخلية لفرق التشغيل عبر مقاييس Prometheus ونقاط نهاية Actuator وآثار Zipkin. كل نمط من الدروس السابقة له دور ملموس وقابل للاختبار هنا. هذا ما يبدو عليه كود الخدمات المصغرة بمستوى الإنتاج: ليس ذكاءً فرديًا، بل متانةً جماعيةً وشفافيةً كاملة.