اكتشاف الخدمات والإعداد والبوّابة

سجل الخدمات مع Eureka

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

سجل الخدمات مع Eureka

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

يأتي Spring Cloud بدعم متكامل لـ Netflix Eureka الذي اختُبر في بيئات إنتاج واسعة النطاق. يتبع Eureka نموذجًا بسيطًا: Eureka Server مخصص يحتضن السجل، وتُشغّل كل خدمة مصغرة Eureka Client يُسجّل نفسه ويجدد النبضة القلبية ويسترجع السجل لاستخدامه في موازنة الحمل من جانب العميل.

تشغيل Eureka Server

أنشئ مشروع Spring Boot 3 جديدًا وأضف تبعيتين:

<!-- pom.xml --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>

ثم أضف التعليق التوضيحي @EnableEurekaServer على الفئة الرئيسية:

package com.example.registry; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer public class RegistryApplication { public static void main(String[] args) { SpringApplication.run(RegistryApplication.class, args); } }

يحتاج الخادم إلى application.yml بسيط يُخبره بعدم تسجيل نفسه لدى ذاته (فهو السجل وليس عميلًا):

server: port: 8761 spring: application: name: eureka-server eureka: client: register-with-eureka: false fetch-registry: false server: wait-time-in-ms-when-sync-empty: 0 # تخطّ الانتظار عند أول إقلاع

شغّل الخادم وافتح http://localhost:8761. ستظهر لوحة تحكم Eureka — فارغة حاليًا وجاهزة لاستقبال التسجيلات.

لماذا المنفذ 8761؟ هو المنفذ الافتراضي لـ Eureka الذي يستخدمه Spring Cloud تلقائيًا. الإبقاء عليه يعني أن الخدمات العميلة لا تحتاج إلى تحديد eureka.client.service-url في بيئة التطوير، مما يزيل فئة كاملة من أخطاء الإعداد.

تسجيل خدمة: عميل Eureka

تُضيف كل خدمة تريد أن تكون قابلة للاكتشاف تبعية عميل Eureka:

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>

لا حاجة لأي تعليق توضيحي في Spring Boot 3 / Spring Cloud 2023 — وجود المُبدّئ في مسار الفئات يُنشّط الإعداد التلقائي. كل ما تحتاجه هو اسم الخدمة في application.yml:

server: port: 8081 spring: application: name: order-service # يصبح هذا اسم المضيف الافتراضي في السجل eureka: client: service-url: defaultZone: http://localhost:8761/eureka/ instance: prefer-ip-address: true # يحل العملاء عنوان IP لا اسم المضيف lease-renewal-interval-in-seconds: 10 # نبضة قلبية كل 10 ثوانٍ lease-expiration-duration-in-seconds: 30 # يُخرج الخادم النسخة بعد 30 ثانية من الصمت

شغّل هذه الخدمة وأعد تحميل لوحة تحكم Eureka. ستجد ORDER-SERVICE ظاهرة في جدول "Instances currently registered with Eureka" مع عنوان IP والمنفذ والحالة UP.

ما يحدث عند التسجيل

عند بدء تشغيل عميل Eureka يُرسل طلب HTTP من نوع POST إلى /eureka/apps/{appName} على الخادم، حاملًا حمولة JSON تسمى InstanceInfo. تحتوي على:

  • اسم التطبيق (order-service بأحرف كبيرة)
  • المضيف والمنفذ
  • عنوان URL لفحص الصحة (يُعيَّن افتراضيًا لنقطة نهاية Actuator /actuator/health إن وُجدت)
  • الحالة — تبدأ بـ STARTING ثم تصبح UP فور جهوزية السياق
  • خريطة بيانات وصفية — أزواج مفتاح/قيمة عشوائية يمكن استخدامها لتلميحات التوجيه

بعد التسجيل يُرسل العميل نبضة قلبية (PUT) لتجديد العقد كل lease-renewal-interval-in-seconds ثانية. إذا لم يتلقَّ الخادم أي تجديد خلال lease-expiration-duration-in-seconds ثانية يُعلّم النسخة بـ DOWN ثم يحذفها في النهاية.

حافظ على نسب متسقة بين فترة النبضة القلبية وانتهاء الصلاحية. الإعداد الافتراضي في الصناعة (30 ث فترة / 90 ث انتهاء) يمنح ثلاث نبضات فائتة قبل الحذف. تضييق الفترة إلى 10 ث / 30 ث يجعل السجل أسرع استجابةً في التطوير، لكن في الإنتاج قد تتسبب وقفة GC عابرة في حذف نسخة خاطئ. اضبط بتحفظ في الإنتاج.

اكتشاف الخدمات برمجيًا

بمجرد تسجيل خدمة، يمكن لأي عميل Eureka آخر في المجموعة ذاتها تحليل عنوانها بالاسم. أبسط طريقة هي DiscoveryClient الذي يُحقنه Spring:

import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.stereotype.Component; import java.util.List; @Component public class OrderServiceClient { private final DiscoveryClient discoveryClient; public OrderServiceClient(DiscoveryClient discoveryClient) { this.discoveryClient = discoveryClient; } public String getOrderServiceBaseUrl() { List<ServiceInstance> instances = discoveryClient.getInstances("order-service"); if (instances.isEmpty()) { throw new IllegalStateException("No instances of order-service are registered"); } // اختر الأولى؛ الدرس 3 يتناول موازنة الحمل الصحيحة ServiceInstance instance = instances.get(0); return instance.getUri().toString(); // مثال: http://192.168.1.42:8081 } }

يُعيد استدعاء getInstances() جميع النسخ الحية المسجّلة تحت ذلك الاسم. عنوان URI مُجمَّع بالكامل من بيانات النسخة الوصفية. من الناحية العملية نادرًا ما تستدعي DiscoveryClient مباشرةً — بل تترك لـ Spring Cloud LoadBalancer (الدرس القادم) اختيار النسخة — لكن فهم هذه الواجهة البرمجية الخام ضروري للتنقيح وللمنطق المخصص للتوجيه.

اعتبارات أمنية للسجل

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

الإجراء الوقائي المعياري هو إضافة Spring Security لمشروع Eureka Server وطلب مصادقة HTTP Basic، ثم تضمين بيانات الاعتماد في عناوين URL للعملاء:

# Eureka Server — application.yml spring: security: user: name: eureka password: ${EUREKA_PASSWORD} # مقروءة من البيئة، لا ترميز مباشر أبدًا # Eureka Client — application.yml eureka: client: service-url: defaultZone: http://eureka:${EUREKA_PASSWORD}@localhost:8761/eureka/
لا تعرّض لوحة تحكم Eureka على واجهة عامة دون مصادقة أبدًا. قائمة السجل الكاملة تكشف للمهاجم كل اسم خدمة داخلية وعنوان IP ومنفذ ونقطة نهاية فحص صحة في نظامك — كل ما يلزم لرسم خريطة بنية الخدمات المصغرة كاملةً. في الإنتاج احتفظ بـ Eureka على شبكة داخلية وأضف TLS مع بيانات اعتماد كحد أدنى.

وضع الحفاظ الذاتي

يمتلك Eureka آلية مدمجة تُسمى وضع الحفاظ الذاتي (self-preservation mode). إذا توقف الخادم عن تلقي تجديدات من نسبة كبيرة من النسخ المسجّلة (تتجاوز عتبة قابلة للإعداد)، يفترض وجود تجزئة في الشبكة لا أن تلك الخدمات انهارت جميعًا، ويوقف حذف النسخ. هذا يحفظ مدخلات قديمة لكنها ربما لا تزال حية بدلًا من إفراغ السجل أثناء اضطراب الشبكة.

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

# Eureka Server — application.yml (ملف تعريف dev فقط) eureka: server: enable-self-preservation: false eviction-interval-timer-in-ms: 2000 # فحص العقود المنتهية كل 2 ثانية
نظرية CAP عمليًا: يُعطي Eureka الأولوية للـ توفّر (Availability) على حساب الاتساق (Consistency). أثناء تجزئة الشبكة يستمر في إعادة بيانات السجل (ربما قديمة) حتى تستمر العملاء في استدعاء الخدمات بدلًا من الفشل. هذا مختلف عمدًا عن سجلات CP كـ Consul التي تُفضّل إرجاع خطأ على بيانات قديمة. اعرف أي نموذج يحتاجه نظامك قبل اختيار السجل.

الخلاصة

خادم Eureka هو تطبيق Spring Boot مُعلَّق بـ @EnableEurekaServer. تضيف كل خدمة عميلة المُبدّئ وتضبط spring.application.name؛ والتسجيل والنبضات القلبية تلقائيان. تكتشف الخدمات الأخرى النسخ الحية بالاسم عبر DiscoveryClient أو عمليًا من خلال Spring Cloud LoadBalancer. أمّن نقطة نهاية السجل دائمًا ببيانات اعتماد وTLS في أي بيئة غير محلية، وافهم وضع الحفاظ الذاتي كيلا يفاجئك أثناء الحوادث.