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

الحاجة إلى اكتشاف الخدمات

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

الحاجة إلى اكتشاف الخدمات

حين تقسّم التطبيق الكبير (monolith) إلى خدمات مصغّرة تكسب القدرة على النشر المستقل والتوسّع المستقل واستقلالية الفرق. لكنك تكسب أيضًا صنفًا جديدًا من المشاكل لم يعانِها التطبيق الكبير قط: كيف تجد الخدمة A الخدمةَ B في وقت التشغيل؟ في التطبيق الكبير الجواب هو استدعاء محلي مباشر. أما في النظام الموزّع فيجب أن يأخذ الجواب بعين الاعتبار إمكانية إعادة تشغيل أي خدمة أو توسيعها أو نقلها إلى مضيف مختلف أو استبدالها في أي لحظة — وغالبًا بدون أي تدخّل بشري.

فخّ العناوين الثابتة المضمّنة في الكود

أكثر الأفكار البديهية هي وضع عنوان URL مباشرة في ملف الإعداد:

# application.yml — النهج الساذج inventory: service: url: http://192.168.1.42:8081

ثم في الخدمة المُستدعِية:

@Service public class OrderService { private final RestClient restClient; // عنوان URL محقون من application.yml @Value("${inventory.service.url}") private String inventoryUrl; public OrderService(RestClient.Builder builder) { this.restClient = builder.build(); } public StockResponse checkStock(String sku) { return restClient.get() .uri(inventoryUrl + "/api/stock/{sku}", sku) .retrieve() .body(StockResponse.class); } }

هذا النهج يعمل بشكل مثالي في بيئة المختبر. لكنه ينهار فور الانتقال إلى بيئة تشبه الواقع.

لماذا تفشل العناوين الثابتة في السحابة

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

تخصيص عناوين IP ديناميكيًا

في كل مرة تُعاد فيها تشغيل حاوية Docker أو كبسولة Kubernetes، تُخصّص المنصة عنوان IP جديدًا من مجموعة عناوين. العنوان 192.168.1.42 الذي كان صحيحًا صباح اليوم قد ينتمي بعد الظهر لخدمة مختلفة تمامًا — أو لا توجد خدمة على الإطلاق. العنوان الثابت الذي أضفته في ملف الإعداد يُشير الآن إلى لا شيء، أو ما هو أسوأ، إلى الشيء الخطأ.

التوسّع الأفقي

لنفترض أن حركة المرور ارتفعت وقامت المنصة بالتوسّع التلقائي لخدمة المخزون من نسخة واحدة إلى ثلاث. أصبح لديك الآن ثلاثة عناوين صالحة — :8081 و:8082 و:8083 — لكن ملف إعدادك لا يذكر سوى العنوان الأصلي. الطاقة الإضافية غير مرئية كليًا للخدمات المُستدعِية. تدفع مقابل ثلاث نسخ وتستخدم واحدة فعلًا.

# لديك ثلاث نسخ تعمل: # http://10.0.0.11:8081 (الأصلية — المضمّنة في الكود) # http://10.0.0.12:8081 (جديدة — غير مرئية لخدمة الطلبات) # http://10.0.0.13:8081 (جديدة — غير مرئية لخدمة الطلبات) # # 100٪ من الطلبات لا تزال تذهب إلى 10.0.0.11. # النسختان الأخريان لا تتلقيان أي طلب.

عمليات النشر المتدرّج والتحديث دون توقف

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

الانجراف في الإعداد بين البيئات المختلفة

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

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

ما يحلّه اكتشاف الخدمات

يستبدل اكتشاف الخدمات العناوين الثابتة بـسجلّ (registry) — دليل مشترك دائم التحديث لنسخ الخدمات الجارية. بدلًا من السؤال "ما هو العنوان الثابت للمخزون؟"، يسأل المُستدعي "أعطني عنوان أي نسخة سليمة من خدمة المخزون الآن."

العقد الأساسي له وجهان:

  • التسجيل (Registration): حين تبدأ خدمة، تُعلن عن عنوانها ومنفذها وعنوان URL للتحقق من صحتها في السجلّ. حين تُغلق بشكل سليم، تلغي تسجيلها. حين تنهار، يطردها السجلّ بعد مهلة قابلة للضبط.
  • الاكتشاف (Discovery): حين يحتاج مُستدعٍ للوصول إلى خدمة، يستعلم السجلّ باسم منطقي (مثل inventory-service) ويحصل على قائمة بعناوين النسخ السليمة. يمكنه حينئذٍ اختيار واحدة — بالتناوب أو عشوائيًا أو حسب زمن الاستجابة — دون معرفة أي شيء عن البنية التحتية.
التحوّل الجوهري في طريقة التفكير: تتوقف عن التفكير في أين تعيش الخدمة (عنوان IP أو اسم مضيف) وتبدأ في التفكير في ما هي (اسم منطقي). السجلّ يمتلك الـأين؛ كودك يمتلك الـما.

الاكتشاف من جانب العميل مقابل الاكتشاف من جانب الخادم

ثمة نمطان رئيسيان لاستخدام السجلّ، ومعرفة أيهما ينفّذه إطار عملك أمر مهم:

  • الاكتشاف من جانب الخادم: يُرسل المُستدعي الطلب إلى موازن تحميل أو بوابة، تستعلم هي بدورها السجلّ وتُعيد توجيه الطلب. المُستدعي لا يعرف شيئًا عن السجلّ. موازن تحميل تطبيقات AWS وخدمات Kubernetes يعملان بهذه الطريقة.
  • الاكتشاف من جانب العميل: المُستدعي نفسه يستعلم السجلّ ويختار نسخة ويُجري استدعاء HTTP مباشرة. Spring Cloud LoadBalancer (بديل Ribbon) ينفّذ هذا النمط. أكثر مرونة لكنه يضع وعيًا بالسجلّ داخل كل خدمة.

Spring Cloud Gateway (الذي يُغطّى لاحقًا في هذا البرنامج التعليمي) يجمع الاثنين في الغالب: تعمل البوابة كنقطة دخول من جانب الخادم للحركة الخارجية لكنها تستخدم الاكتشاف من جانب العميل داخليًا للتوجيه إلى الخدمات الخلفية.

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

سجلّ الخدمات هو بحد ذاته مكوّن موزّع، مما يعني أنه يجب أن يكون متاحًا للغاية. إذا تعطّل السجلّ، فإن الخدمات التي تعتمد عليه في قرارات التوجيه لن تستطيع اكتشاف أندادها الجديدة. يعالج Spring Cloud Eureka هذا بـذاكرة تخزين مؤقتة محلية: يُخزّن كل عميل آخر لقطة معروفة من السجلّ ويستمر في التوجيه منها حتى حين يكون خادم السجلّ غير متاح مؤقتًا. هذه مقايضة متعمّدة بين التوافق (consistency) (دائمًا رؤية أحدث قائمة) والتوفّر (availability) (القدرة على التوجيه أصلًا) — تحديدًا الزاوية AP من نظرية CAP.

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

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

بدون اكتشاف الخدمات — كيف يبدو كود المُستدعي:

// قبل: عنوان ثابت، هشّ String url = "http://192.168.1.42:8081/api/stock/" + sku; ResponseEntity<StockResponse> response = restTemplate.getForEntity(url, StockResponse.class);

مع اكتشاف الخدمات و Spring Cloud LoadBalancer — كيف يبدو نفس الاستدعاء:

// بعد: الاسم المنطقي يُحلّ في وقت التشغيل عبر السجلّ @Bean @LoadBalanced // يُخبر Spring باعتراض الاستدعاء وحلّ اسم المضيف public RestTemplate restTemplate() { return new RestTemplate(); } // في الخدمة: String url = "http://inventory-service/api/stock/" + sku; ResponseEntity<StockResponse> response = restTemplate.getForEntity(url, StockResponse.class); // يُبحث عن "inventory-service" في Eureka ويُستبدل بعنوان نسخة سليمة

كود المُستدعي لا يعرف عنوان IP الحقيقي أبدًا. لا يتغيّر حين تُضاف نسخ أو تُزال أو تُستبدل. السجلّ وطبقة موازنة التحميل تُعالجان كل ذلك بشفافية.

الخلاصة

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