نمط الحاجز: تصميم الخدمات الصغيرة المتسامحة مع الأخطاء

نمط الحاجز: تصميم الخدمات الصغيرة المتسامحة مع الأخطاء

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

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

يُعرف هذا الفشل المتتالي بتأثير الدومينو. ولمنع ذلك، يستخدم مهندسو النظام نمط الحاجز.

في هذا الدليل، سوف نستكشف ما هو نمط Bulkhead وكيف يعمل وكيفية تنفيذه باستخدام تشبيهات بسيطة ومفاهيم معمارية وأمثلة برمجية في Java (Resilience4j) وGo.


تشبيه العالم الحقيقي: حواجز السفن المقاومة للماء

اسم هذا النمط يأتي من صناعة بناء السفن.

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

يُظهر الرسم التخطيطي لهندسة نمط الحاجز تجمعات الخيوط المشتركة والمعزولة

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

بدون الحواجز، سوف يتدفق الماء بحرية في جميع أنحاء الهيكل بأكمله، مما يؤدي في النهاية إلى غرق السفينة.

في هندسة البرمجيات:

  • السفينة هي تطبيقك أو خدمتك بأكملها.
  • المقصورات عبارة عن تجمعات موارد معزولة (الخيوط والاتصالات ووحدة المعالجة المركزية).
  • اختراق الهيكل هو فشل أو تباطؤ في الخدمة الصغيرة النهائية.
  • الفيضان هو استنفاد الموارد.

المشكلة: تجمعات الموارد المشتركة واستنفاد الخيوط

لفهم سبب أهمية الحواجز، دعونا نلقي نظرة على ما يحدث عندما تتم مشاركة الموارد عالميًا.

تخيل بوابة API أو خادم ويب يتعامل مع طلبات المستخدم. يحتوي على مجموعة مؤشرات ترابط عالمية واحدة مكونة من 100 سلسلة معالجة لجميع المكالمات الواردة. يتفاعل الخادم مع ثلاث خدمات المصب:

  1. خدمة الكتالوج (سريعة، وقراءة قائمة المنتجات)
  2. خدمة الدفع (سريعة، إجراءات الدفع)
  3. خدمة التوصية (بطيئة، وتحسب العناصر المخصصة)

عادة، كل شيء يعمل بشكل جيد. ولكن لنفترض أن خدمة التوصية تعاني من حالة توقف تام لقاعدة البيانات وتبدأ في استغراق 30 ثانية للاستجابة بدلاً من 200 مللي ثانية.

وهنا ما يحدث:

  1. يستمر المستخدمون في زيارة الصفحة الرئيسية، مما يؤدي إلى تقديم طلبات إلى خدمة التوصية.
  2. يقوم الخادم بتعيين مؤشر ترابط من التجمع العالمي لكل طلب.
  3. نظرًا لأن خدمة التوصيات بطيئة، فإن هذه المواضيع تنتظر الردود.
  4. في غضون ثوانٍ، تكون كافة الخيوط الـ 100 الموجودة في المجموعة في انتظار خدمة التوصية.
  5. عندما يحاول مستخدم جديد الخروج أو عرض الكتالوج، لا يوجد لدى الخادم أي سلاسل متبقية لمعالجة طلبه.

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


الحل: نمط الحاجز

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

بدلاً من تجمع عمومي واحد، نقوم بتخصيص مجموعات منفصلة ومحدودة لكل خدمة أو تبعية المصب.

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

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


أنواع عزل الحاجز

هناك طريقتان أساسيتان لتنفيذ الحواجز في أنظمة البرمجيات:

1. عزل تجمع الخيوط

في هذا النموذج، يتم تعيين كل تبعية تابعة لمجمع مؤشرات الترابط المخصص وقائمة انتظار التنفيذ.

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

2. عزل الإشارة

بدلاً من إنشاء تجمعات سلاسل رسائل جديدة، يستخدم عزل الإشارة عدادًا (إشارة) للحد من عدد المكالمات المتزامنة المسموح بها لخدمة معينة.

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

أمثلة التنفيذ

دعونا نلقي نظرة على كيفية تنفيذ الحواجز في لغتين خلفيتين شائعتين.

1. جافا (Resilience4j وSpring Boot)

Resilience4j هي ​​مكتبة خفيفة الوزن وسهلة الاستخدام للتسامح مع الأخطاء مصممة لـ Java. فيما يلي كيفية تكوين حاجز لخدمة الدفع النهائية في تطبيق Spring Boot.

التكوين (application.yml)

resilience4j.bulkhead:
  instances:
    paymentService:
      maxConcurrentCalls: 10
      maxWaitDuration: 10ms

resilience4j.threadpoolbulkhead:
  instances:
    paymentService:
      maxThreadPoolSize: 10
      coreThreadPoolSize: 5
      queueCapacity: 20

تنفيذ التعليمات البرمجية

import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class OrderService {

    private final RestTemplate restTemplate;

    public OrderService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    // Apply semaphore bulkhead
    @Bulkhead(name = "paymentService", fallbackMethod = "paymentFallback")
    public String processPayment(OrderDetails details) {
        return restTemplate.postForObject("http://payment-service/charge", details, String.class);
    }

    // Fallback method executed when the bulkhead is full
    public String paymentFallback(OrderDetails details, Throwable throwable) {
        return "Payment service is currently busy. Please try again later.";
    }
}

2. اذهب (جولانج)

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

package main

import (
	"errors"
	"fmt"
	"net/http"
	"time"
)

// Bulkhead represents a concurrency limiter
type Bulkhead struct {
	semaphore chan struct{}
}

// NewBulkhead initializes a bulkhead with a max concurrency limit
func NewBulkhead(maxConcurrency int) *Bulkhead {
	return &Bulkhead{
		semaphore: make(chan struct{}, maxConcurrency),
	}
}

// Execute runs the task if resource permit is available, otherwise returns error
func (b *Bulkhead) Execute(task func() error) error {
	select {
	case b.semaphore <- struct{}{}:
		// Acquired permit
		defer func() { <-b.semaphore }() // Release permit
		return task()
	default:
		// Bulkhead is full, reject immediately
		return errors.New("bulkhead is full: request rejected")
	}
}

func main() {
	// Allow maximum of 3 concurrent calls
	paymentBulkhead := NewBulkhead(3)

	mockTask := func() error {
		fmt.Println("Processing payment...")
		time.Sleep(2 * time.Second) // Simulate network delay
		return nil
	}

	// Simulate 5 rapid requests
	for i := 1; i <= 5; i++ {
		go func(reqID int) {
			err := paymentBulkhead.Execute(mockTask)
			if err != nil {
				fmt.Printf("Request %d failed: %v\n", reqID, err)
			} else {
				fmt.Printf("Request %d completed successfully\n", reqID)
			}
		}(i)
	}

	// Keep main alive to watch output
	time.Sleep(3 * time.Second)
}

حالات الاستخدام الشائعة لنموذج الحاجز

فيما يلي بعض السيناريوهات النموذجية التي يكون فيها تنفيذ نمط الحاجز أمرًا بالغ الأهمية:

  • توجيه بوابة واجهة برمجة التطبيقات: عزل المسارات لخدمات الواجهة الخلفية المختلفة. إذا تعطلت خدمة التوصيات، فستظل مسارات خدمة الطلب على البوابة قيد التشغيل بكامل طاقتها.
  • Database Connection Pools: Dividing database connection pools by service or tenant. لن تؤدي زيادة عدد الاستعلامات التحليلية الثقيلة من مستأجر واحد إلى استنفاد كافة مقابض الاتصال المتاحة، مما يؤدي إلى حفظ استعلامات المعاملات للمستأجرين الآخرين.
  • تطبيقات SaaS متعددة المستأجرين: فصل موارد الحوسبة أو قوائم انتظار التنفيذ للمستأجرين المميزين مقابل المستأجرين المجانيين. لن يؤدي ارتفاع موارد الطبقة المجانية إلى تجويع طلبات الطبقة المتميزة لوحدة المعالجة المركزية أو الذاكرة.
  • Third-Party API Integrations: Dedicating separate HTTP client pools for external payment gateways, shipping providers, or notification engines. إذا تباطأت إحدى خدمات الطرف الثالث، فستستمر التفاعلات الخارجية الأخرى دون عوائق.

لماذا لا يستطيع وسطاء كافكا/الرسائل استبدال نمط الحاجز

السؤال الشائع هو: “إذا كان لدينا وسطاء رسائل مثل Apache Kafka، فلماذا نحتاج إلى نمط Bulkhead؟ ألا يمكننا استخدام قوائم الانتظار لتخزين الطلبات مؤقتًا؟”

بينما يقوم وسطاء الرسائل بفصل الأنظمة، لا يمكنهم استبدال نمط الحاجز. هنا هو السبب:

1. الاتصال المتزامن مقابل الاتصال غير المتزامن

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

2. تجويع الخيط داخل مستهلكي كافكا

حتى لو كان نظامك مدفوعًا بالأحداث بالكامل ويستخدم كافكا، فأنت لا تزال بحاجة إلى حواجز! لنفترض أن خدمة صغيرة واحدة للمستهلك تستمع إلى موضوعات كافكا المتعددة (على سبيل المثال، user-registrations و video-transcoding). إذا قام المستهلك بتخصيص كافة سلاسل العمليات الداخلية الخاصة به لمعالجة مجموعة كبيرة من مهام video-transcoding البطيئة، فسوف يواجه تجويع سلسلة الرسائل. لن يتمكن المستهلك من معالجة رسائل user-registrations خفيفة الوزن، على الرغم من أن هذا القسم سليم. لا تزال بحاجة إلى حواجز داخلية (تجمعات ترابط منفصلة) داخل خدمة المستهلك لعزل العمل.

3. المتطلبات العامة من جانب العميل ومتطلبات سرعة الفشل

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

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


أفضل الممارسات عند استخدام الحواجز

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

خاتمة

يعد Bulkhead Pattern أحد أنماط التصميم الأساسية لبناء أنظمة مرنة على نطاق السحابة. من خلال تقسيم مواردك، يمكنك عزل حالات الفشل ومنع التأثيرات المتتالية والتأكد من أن الخطأ المحلي لا يتحول إلى انقطاع عالمي.