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

تحديث الإعدادات أثناء التشغيل

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

تحديث الإعدادات أثناء التشغيل

أظهر الدرس السابق كيف يُمركز Config Server خصائصك بحيث تقرأ كل خدمة من مكان واحد. لكن ماذا يحدث عند تغيير قيمة ما — علامة ميزة، حدّ سرعة، عنوان URL لخدمة خارجية؟ بدون التحديث أثناء التشغيل، يصبح كل تغيير عملية نشر كاملة: بناء، وسمة، ونشر، وإعادة تشغيل. في نظام مؤلف من عشرين خدمة هذا مؤلم؛ وفي نظام مؤلف من مئتي خدمة يكاد يكون مستحيلاً تشغيلياً.

يمنحك Spring Cloud Config آليتين متكاملتين للتحديث أثناء التشغيل: نقطة النهاية /actuator/refresh (لكل مثيل على حدة، عبر HTTP POST) وSpring Cloud Bus (بثّ لجميع المثيلات دفعةً واحدة عبر وسيط رسائل). يتناول هذا الدرس الآليتين معاً، والتداعيات الأمنية لكل منهما، ومقايضات الأنظمة الموزعة التي يجب أن تفهمها قبل اختيار إحداهما.

كيف تحتفظ حبوب Spring بقيم الإعداد

قبل تحديث أي شيء، يجب أن تفهم المشكلة. تُنشئ Spring الحبوب (beans) في وقت مبكر عند بدء التشغيل وتُحقن قيم الخصائص في تلك اللحظة. الحقل العادي @Value("${order.timeout}") يُعيَّن مرة واحدة فقط — ولن يتغير أبداً مهما أعاد Config Server إرجاع قيم جديدة لاحقاً.

تحل Spring Cloud هذه المشكلة عبر تعليق @RefreshScope. الحبوب المُزيَّنة بهذا التعليق تُلفَّف بوكيل (proxy). عند إطلاق حدث تحديث، يُدمّر الوكيل مثيل الحبة الأساسي ويُعيد إنشاءه من البيئة الحالية، فيلتقط قيم الخصائص الجديدة.

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

الخطوة 1 — تفعيل نقطة نهاية التحديث في Actuator

أضف مشغّلَي Actuator وSpring Cloud Config Client إلى خدمتك (يجب أن يكون لديك Config Client مسبقاً من الدرس السابق):

<!-- pom.xml --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>

ثم اكشف نقطة النهاية في application.yml:

management: endpoints: web: exposure: include: refresh,health,info endpoint: refresh: enabled: true
لا تكشف جميع نقاط النهاية بدون مصادقة. استخدام include: "*" يكشف /actuator/shutdown و/actuator/env ونقاط نهاية حساسة أخرى. في الإنتاج، اكشف فقط ما تحتاجه وأمّن كل نقطة نهاية في Actuator باستخدام Spring Security أو سياسة شبكة. إعداد Actuator الخاطئ هو أحد أكثر ثغرات أمان الخدمات المصغرة شيوعاً في اختبارات الاختراق.

الخطوة 2 — تعليق الحبة التي تحتفظ بالخاصية

package com.example.orders.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.stereotype.Component; @Component @RefreshScope public class OrderProperties { @Value("${order.timeout-seconds:30}") private int timeoutSeconds; @Value("${order.max-retries:3}") private int maxRetries; public int getTimeoutSeconds() { return timeoutSeconds; } public int getMaxRetries() { return maxRetries; } }

أي فئة تحقن OrderProperties ستَرى القيم المحدَّثة تلقائياً بعد التحديث التالي، لأن الوكيل يفوّض الاستدعاءات بشفافية إلى المثيل المُعاد إنشاؤه حديثاً.

الخطوة 3 — تشغيل التحديث

بعد تحديث القيمة في مستودع الإعداد المدعوم بـ Git وإيداعها، أرسل POST إلى نقطة نهاية التحديث للخدمة الجارية:

# تشغيل التحديث على مثيل محدد curl -X POST http://localhost:8081/actuator/refresh

تُعيد نقطة النهاية مصفوفة JSON بمفاتيح الخصائص التي تغيرت:

["order.timeout-seconds","order.max-retries"]

إذا لم يتغير شيء، تُعيد مصفوفة فارغة. الخدمة تعمل الآن بالقيم الجديدة — بدون إعادة تشغيل.

تأمين نقطة نهاية التحديث

في الإنتاج يجب حماية /actuator/refresh. الإرسال إليها يُجبر على قراءة جديدة من Config Server؛ مهاجم يستطيع الوصول إليها قد يُحقن قيم إعداد عشوائية إذا كان Config Server مخترقاً أيضاً، أو يُشغّل تحديثات متكررة كهجوم حرمان من الخدمة. أضف Spring Security واطلب على الأقل سراً مشتركاً:

package com.example.orders.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration public class ActuatorSecurityConfig { @Bean public SecurityFilterChain actuatorSecurity(HttpSecurity http) throws Exception { http .securityMatcher("/actuator/**") .authorizeHttpRequests(auth -> auth .requestMatchers("/actuator/health", "/actuator/info").permitAll() .anyRequest().hasRole("ACTUATOR_ADMIN") ) .httpBasic(org.springframework.security.config.Customizer.withDefaults()); return http.build(); } }

توسيع الحل: Spring Cloud Bus

نهج /actuator/refresh يعمل جيداً مع مثيل أو مثيلين. لكن مع عشرة مثيلات من order-service قيد التشغيل، ستحتاج إلى إرسال POST لكل العشرة نقاط نهاية بشكل فردي. هذا هشّ تشغيلياً ومن السهل نسيانه.

يحل Spring Cloud Bus هذه المشكلة بتوصيل جميع المثيلات بوسيط رسائل مشترك (RabbitMQ أو Kafka). تُرسل POST لنقطة نهاية واحدة فقط — على أي مثيل واحد، أو على Config Server نفسه — وتُذيع الحافلة حدث تحديث لكل مشترك.

أضف تبعية Bus (يُستخدم هنا متغير RabbitMQ):

<!-- pom.xml — على كل خدمة وعلى Config Server --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency>

اضبط RabbitMQ في application.yml:

spring: rabbitmq: host: localhost port: 5672 username: guest password: guest

اكشف نقطة نهاية bus-refresh على Config Server:

management: endpoints: web: exposure: include: busrefresh,health

بعد دفع تغيير الإعداد إلى Git، شغّل تحديثاً لجميع الأسطول باستدعاء واحد للـ Config Server:

# استدعاء واحد يُحدّث جميع مثيلات الخدمة المتصلة curl -X POST http://localhost:8888/actuator/busrefresh
حدّد نطاق تحديث الحافلة عند الإمكان. تقبل /actuator/busrefresh/{destination} نمط معرّف سياق تطبيق Spring (مثل order-service:**) بحيث تستهدف عائلة خدمة واحدة فقط دون إعادة تشغيل كل خدمة على الحافلة. هذا يقلل التشويش غير الضروري في الأساطيل الكبيرة.

خطافات Git الآلية: تحديث كامل التشغيل الذاتي

في الإعداد الناضج تُزيل الزناد اليدوي كلياً. اضبط مستضيف Git الخاص بك (GitHub أو GitLab أو Bitbucket) لإرسال POST إلى نقطة نهاية /monitor الخاصة بـ Config Server عند كل دفع. أضف تبعية Spring Cloud Config Monitor إلى Config Server:

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-monitor</artifactId> </dependency>

الآن يصبح التدفق الكامل: git push ← يُطلق الخطاف ← يُحلل Config Server الملفات المتغيرة ← تُذيع الحافلة التحديث ← تُعيد جميع مثيلات الخدمة المتأثرة تحميل حبوبها. تنتشر الإعدادات إلى كل مثيل جارٍ في غضون ثوانٍ من إيداع Git، بدون نشر ودون أي أوامر curl يدوية.

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

التحديث أثناء التشغيل قوي، لكنه ليس مجانياً:

  • نوافذ التحديث الجزئي: في الثواني بين أول وآخر مثيل يُحدَّث، تعمل مثيلات مختلفة بإعدادات مختلفة. صمّم خدماتك بحيث تتحمل التناسق الجزئي العابر، أو استخدم علامات الميزات الآمنة للقِدَم.
  • ليست كل الحبوب في نطاق التحديث: DataSource وسلاسل فلتر الأمان وحبوب البنية التحتية الأخرى التي أنشأتها الإعدادات التلقائية لا تُزيَّن بـ @RefreshScope. تغيير عنوان URL لقاعدة البيانات أثناء التشغيل لن يُعيد توصيل التجمّع — لا تزال هذه الخصائص تستلزم إعادة تشغيل.
  • وسيط الرسائل يصبح بنية تحتية حرجة: إذا اعتمدت Spring Cloud Bus، يُصبح الوسيط نقطة فشل وحيدة لتسليم الإعداد. استخدم وسيطاً متكاملاً في الإنتاج، وخطّط لما يحدث للخدمات إذا كان الوسيط غير متاح مؤقتاً.
  • التدقيق والتراجع: لأن إعداداتك تعيش في Git، فإن كل تغيير موثّق وقابل للتدقيق. للتراجع عن تغيير في الإعداد ارجع إلى الإيداع السابق في Git وشغّل تحديثاً آخر — لا حاجة لإعادة تشغيل الخدمة.
القاعدة العملية: استخدم @RefreshScope لإعدادات منطق الأعمال (المهل الزمنية، الحدود، العلامات، عناوين URL للشركاء الخارجيين). لا تستخدمه لإعداد البنية التحتية الهيكلية (أرقام المنافذ، بيانات اعتماد مصادر البيانات، مفاتيح الأمان). الأول آمن للتغيير أثناء التشغيل؛ الثاني يستلزم إعادة تشغيل متحكّم بها.

الخلاصة

يجعل @RefreshScope أي حبة Spring قابلة لإعادة التحميل الحي بدون إعادة تشغيل. تُشغّل نقطة النهاية /actuator/refresh إعادة تحميل لمثيل واحد؛ بينما يُذيع Spring Cloud Bus ذلك الزناد لجميع المثيلات دفعةً واحدة. أمّن نقاط نهاية التحديث بـ Spring Security، وأبقِ حبوب البنية التحتية خارج نطاق التحديث، واستخدم خطافات Git لأتمتة الانتشار. في الدرس التالي ستتعلم نمط API Gateway — الباب الأمامي الذي تُعرض من خلاله هذه الخدمات للعملاء.