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

موازنة الأحمال من جانب العميل

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

موازنة الأحمال من جانب العميل

في التطبيق الأحادي التقليدي تتصل بمضيف قاعدة بيانات واحد أو بخدمة واحدة في المصب. في بيئة الخدمات المصغّرة تعمل الخدمة المنطقية ذاتها على هيئة نسخ متطابقة متعددة — ثلاث نسخ من order-service، وخمس نسخ من inventory-service، وهكذا. لا بد من جهة ما تقرّر أيّ نسخة تستقبل كل طلب. يمكن أن يحدث هذا القرار في مكانين: عند عملية موازنة الأحمال المخصصة الجالسة في الوسط (الموازنة من جانب الخادم)، أو داخل الخدمة الطالِبة نفسها (الموازنة من جانب العميل). هذا الدرس مكرّس للأخيرة.

موازنة الأحمال من جانب الخادم مقابل موازنة الأحمال من جانب العميل

موازنة الأحمال من جانب الخادم تعني أن كل طلب صادر يتجه إلى IP افتراضي ثابت أو اسم DNS. أداة خارجية (HAProxy، أو AWS ALB، أو كتلة upstream في Nginx) تمتلك قائمة النسخ والخوارزمية. لا يعرف العميل سوى عنوان واحد.

موازنة الأحمال من جانب العميل تنقل تلك المسؤولية إلى الطالب. يقوم الطالب بما يلي:

  1. يجلب قائمة النسخ الحية من سجل الخدمات (Eureka في هيكلنا).
  2. يطبّق خوارزمية اختيار محلية لانتقاء نسخة واحدة.
  3. يرسل طلب HTTP مباشرةً إلى المضيف والمنفذ الخاصَّين بالنسخة المختارة.

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

Spring Cloud LoadBalancer هو موازن الأحمال الرسمي من جانب العميل منذ Spring Cloud 2020. Netflix Ribbon كان سلفه وهو الآن في وضع الصيانة فقط — لا تبدأ مشاريع جديدة باستخدام Ribbon.

إضافة التبعية

تُحزَم موازنة الأحمال من جانب العميل ضمن المشغّل الخاص بعميل اكتشاف الخدمات. إذا كنت تستخدم مشغّل عميل Eureka بالفعل فلديك كل ما تحتاجه:

<!-- pom.xml -- مشغّل عميل Eureka فقط؛ LoadBalancer مضمَّن --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>

حزمة spring-cloud-starter-loadbalancer هي تبعية انتقالية لمشغّل عميل Eureka وتُشحَن تلقائيًا. لا تحتاج إلى إضافتها بشكل منفصل إلا إذا كنت تستخدم آلية اكتشاف مختلفة.

كيف يعمل Spring Cloud LoadBalancer

عند بناء عميل HTTP باستخدام WebClient (التفاعلي) أو RestClient / RestTemplate (المتزامن)، تُضيف التعليق التوضيحي @LoadBalanced على الـ @Bean الذي يبنيه. يُغلّف Spring Cloud العميل الناتج بمعترض (interceptor). عند وقت الاستدعاء يقوم هذا المعترض بما يلي:

  1. يكتشف اسم مضيف يطابق اسم خدمة مسجّل (مثل http://order-service/...).
  2. يستعلم عن ذاكرة التخزين المؤقت المحلية لـ ServiceInstanceListSupplier، التي تُحدَّث دوريًا من Eureka.
  3. يشغّل الخوارزمية المضبوطة — round-robin افتراضيًا — لاختيار نسخة.
  4. يعيد كتابة عنوان URL إلى host:port الحقيقي للنسخة المختارة ثم يمضي قُدُمًا.

استخدام WebClient ذي موازنة الأحمال

يُعدّ WebClient التفاعلي العميل HTTP الموصى به لخدمات Spring Boot 3. اضبط bean لـ builder ذي موازنة أحمال مرة واحدة في فئة @Configuration:

import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; @Configuration public class WebClientConfig { @Bean @LoadBalanced public WebClient.Builder loadBalancedWebClientBuilder() { return WebClient.builder(); } }

أدرج الـ builder في أي bean خدمة يحتاج إلى استدعاء خدمة أخرى. استخدم الاسم المنطقي للخدمة بالضبط كما هو مسجّل في Eureka — حالة الأحرف مهمة:

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; @Service public class OrderService { private final WebClient webClient; @Autowired public OrderService(WebClient.Builder builder) { // "inventory-service" يُحلَّل في وقت التشغيل عبر Eureka + LoadBalancer this.webClient = builder.baseUrl("http://inventory-service").build(); } public Mono<Integer> getStock(Long productId) { return webClient.get() .uri("/api/inventory/{id}/stock", productId) .retrieve() .bodyToMono(Integer.class); } }
أبقِ عنوان URL الأساسي على اسم الخدمة فقط. لا تُضمّن منفذًا ثابتًا في عنوان URL (http://inventory-service:8082). يستبدل LoadBalancer الجزء الكامل الخاص بالمضيف؛ وإضافة منفذ تجعل الاستبدال غامضًا وتؤدي إلى فشل في وقت التشغيل.

استخدام RestClient ذي موازنة الأحمال (Spring Boot 3.2 فأحدث)

RestClient هو البديل المتزامن الذي قُدِّم في Spring Framework 6.1. تحصل على نفس نمط التعليق التوضيحي @LoadBalanced:

import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestClient; @Configuration public class RestClientConfig { @Bean @LoadBalanced public RestClient.Builder loadBalancedRestClientBuilder() { return RestClient.builder(); } }
@Service public class CatalogService { private final RestClient restClient; public CatalogService(RestClient.Builder builder) { this.restClient = builder.baseUrl("http://catalog-service").build(); } public Product findById(Long id) { return restClient.get() .uri("/api/products/{id}", id) .retrieve() .body(Product.class); } }

تغيير خوارزمية موازنة الأحمال

الخوارزمية الافتراضية هي round-robin: يُوزَّع الحمل بالتساوي على جميع النسخ الصحية بالتناوب. يشحن Spring Cloud LoadBalancer أيضًا خوارزمية عشوائية. يمكنك التبديل لكل خدمة على حدة بتوفير فئة إعداد مخصصة وربطها عبر @LoadBalancerClient:

import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer; import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer; import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; // ملاحظة: لا تضع هذه الفئة داخل الحزمة الجذر لـ @ComponentScan. // أبقِها خارج حزمة التطبيق الرئيسية كي يطبّقها Spring فقط // على الخدمة المسمّاة في @LoadBalancerClient. public class RandomLBConfig { @Bean public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer( Environment env, LoadBalancerClientFactory factory) { String name = factory.getName(env); return new RandomLoadBalancer( factory.getLazyProvider(name, ServiceInstanceListSupplier.class), name); } }

ثم أشِر إلى فئة الإعداد تلك على نقطة الدخول للتطبيق أو أي فئة @Configuration:

@SpringBootApplication @LoadBalancerClient(name = "inventory-service", configuration = RandomLBConfig.class) public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } }

جميع استدعاءات inventory-service ستستخدم الاختيار العشوائي الآن؛ واستدعاءات كل خدمة أخرى ستبقى على round-robin.

ذاكرة التخزين المؤقت للنسخ وتصفية الحالة الصحية

يخزّن Spring Cloud LoadBalancer قائمة النسخ مؤقتًا في خط أنابيب ServiceInstanceListSupplier. بشكل افتراضي يُحدَّث كل 35 ثانية. يمكنك ضبط هذا في application.yml:

spring: cloud: loadbalancer: cache: ttl: 30s # مدة الاحتفاظ بقائمة النسخ المخزّنة مؤقتًا capacity: 256 # الحد الأقصى لأسماء الخدمات القابلة للتخزين المؤقت

يدعم خط أنابيب المورّد أيضًا مرشّح الفحص الصحي. فعّله لاستبعاد النسخ التي تُبلّغ عن حالة DOWN في Eureka تلقائيًا:

spring: cloud: loadbalancer: health-check: initial-delay: 0 interval: 25s
ذاكرة التخزين المؤقت المنتهية الصلاحية هي سيناريو فشل حقيقي. إذا تعطّلت نسخة ولم تنتشر الإزالة بعد في Eureka، فسيظل موازن الأحمال يوجّه بعض الطلبات إلى النسخة الميتة. اجمع دائمًا موازنة الأحمال من جانب العميل مع سياسة إعادة المحاولة (التي تُغطّى في درس المرونة) كي تُعاد المحاولة بشفافية على نسخة مختلفة عند الفشل الفردي.

الاعتبار الأمني: mTLS عبر النسخ

موازنة الأحمال من جانب العميل تعني أن خدمتك تفتح اتصال TCP مباشرًا لكل نسخة. في بيئة الإنتاج يجب تشفير تلك الاتصالات والتحقق المتبادل من هويتها — وإلا يمكن لخدمة داخلية مخترَقة انتحال هوية أي نسخة. الأنماط الشائعة هي:

  • شبكة الخدمات (Istio / Linkerd): يُعالَج mTLS بشفافية على مستوى البروكسي الجانبي (sidecar)؛ يرى كود تطبيقك HTTP عاديًا.
  • ضبط TLS للعميل في Spring Boot: اضبط truststore و keystore على WebClient / RestClient لـ mTLS مباشر بدون شبكة خدمات.
  • نشر JWT: مرّر رمز bearer الخاص بالطالب في المصب مع كل استدعاء بين الخدمات كي تتمكّن الخدمات في المصب من تطبيق التفويض باستقلالية.

الخلاصة

تمنح موازنة الأحمال من جانب العميل مع Spring Cloud LoadBalancer كل خدمة موجّهًا محليًا مدعومًا بالسجل. تُضيف التعليق التوضيحي @LoadBalanced على bean الخاص بـ WebClient.Builder أو RestClient.Builder، وتخاطب الخدمات في المصب باسم خدمتها في Eureka، فيعمل إطار العمل على التحليل والتناوب بين النسخ الحقيقية في وقت الاستدعاء. round-robin هو الافتراضي؛ الاختيار العشوائي والخوارزميات المخصصة تُربط لكل خدمة على حدة عبر @LoadBalancerClient. اضبط مدة TTL لذاكرة تخزين النسخ المؤقتة، وفعّل تصفية الفحص الصحي، وادمج مع آلية إعادة المحاولة لمعالجة سيناريوهات الإدخالات المنتهية الصلاحية والنسخ المتعطلة الحتمية في الإنتاج.