دليل تقني

تصميم أنظمة للفشل (لأنها راح تفشل)

أنماط استجابة الأعطال لأنظمة الإنتاج. Circuit Breakers، استراتيجيات Retry، التدهور الأنيق، Dead Letter Handling، ميزانيات Timeout، وChaos Engineering للفرق الصغيرة.

31 مارس 202614 دقيقة للقراءةفريق هندسة أورنتس

تصنيف الأعطال

الأنظمة تفشل بأربع طرق. كل واحدة تحتاج استجابة مختلفة.

النوعالوصفمثالالاستجابة الصحيحة
عابرخلل قصير، يحل نفسه بنفسهTimeout شبكة، انقطاع اتصالRetry مع Backoff
دائممكسور لين أحد يصلحهإعدادات غلط، عدم تطابق Schemaفشل سريع، تنبيه، بدون Retry
جزئيبعض الوظائف شغالةواحد من ثلاث موردين واقفتدهور أنيق، قدم اللي يشتغل
متتاليعطل واحد يسبب أعطال ثانيةحمل زايد على قاعدة البيانات يسبب Timeout لكل الخدماتCircuit Breaker، تخفيف الحمل

أخطر غلطة: تعامل مع كل الأعطال بنفس الطريقة. إعادة المحاولة لعطل دائم تضيع موارد. عدم إعادة المحاولة لعطل عابر يخرب تجربة المستخدم. عدم اكتشاف العطل المتتالي يوقف النظام بالكامل.

كيف نتعامل مع الأعطال خصوصاً في الأنظمة المبنية على الأحداث، شوف دليل Event-Driven Architecture. لأعطال أنظمة الذكاء الاصطناعي، شوف دليل أنماط فشل الذكاء الاصطناعي.

Circuit Breaker

الـ Circuit Breaker يمنع خدمة فاشلة من إنها تسحب كل شي يعتمد عليها معاها. لما خدمة downstream تفشل بشكل متكرر، الـ Circuit Breaker "ينفتح" ويقطع الطلبات فوراً بدل ما يستنى Timeouts.

class CircuitBreaker {
    private failures = 0;
    private lastFailure = 0;
    private state: 'closed' | 'open' | 'half-open' = 'closed';

    constructor(
        private threshold: number = 5,      // عدد الأخطاء قبل الفتح
        private resetTimeout: number = 30000, // مللي ثانية قبل المحاولة مرة ثانية
    ) {}

    async execute<T>(fn: () => Promise<T>): Promise<T> {
        if (this.state === 'open') {
            if (Date.now() - this.lastFailure > this.resetTimeout) {
                this.state = 'half-open'; // جرب طلب واحد
            } else {
                throw new CircuitOpenError('Circuit is open');
            }
        }

        try {
            const result = await fn();
            this.onSuccess();
            return result;
        } catch (error) {
            this.onFailure();
            throw error;
        }
    }

    private onSuccess() {
        this.failures = 0;
        this.state = 'closed';
    }

    private onFailure() {
        this.failures++;
        this.lastFailure = Date.now();
        if (this.failures >= this.threshold) {
            this.state = 'open';
        }
    }
}

// الاستخدام
const supplierBreaker = new CircuitBreaker(5, 30000);

async function checkAvailability(productId: string) {
    return supplierBreaker.execute(async () => {
        return await supplierApi.checkAvailability(productId);
    });
}

الغلطة الشائعة

أغلب الفرق تنفذ الـ Circuit Breaker لكن ما تتعامل مع حالة الفتح. لما الـ Circuit مفتوح، شو يشوف المستخدم؟ خطأ 500 هو الجواب الغلط. الجواب الصح يعتمد على السياق:

السياقلما الـ Circuit ينفتحالسلوك الصحيح
بحث المنتجاتمورد واحد واقفاعرض نتائج من موردين ثانيين
فحص السعرخدمة التسعير واقفةاعرض السعر المحفوظ مع تسمية "حتى تاريخ X"
الدفعبوابة الدفع واقفةحط الطلب في Queue، عالجه لاحقاً
التوصياتخدمة ML واقفةاعرض المنتجات الشائعة بدالها
خدمة الصورالـ CDN واقفاعرض صورة بديلة

استراتيجيات الـ Retry

مش كل الـ Retries متساوية. الاستراتيجية تعتمد على نوع العطل.

interface RetryConfig {
    maxAttempts: number;
    strategy: 'immediate' | 'fixed' | 'exponential' | 'none';
    baseDelay: number;      // مللي ثانية
    maxDelay: number;        // مللي ثانية
    jitter: boolean;         // عشوائية لمنع Thundering Herd
}

const RETRY_CONFIGS: Record<string, RetryConfig> = {
    // عابر: Retry مع Exponential Backoff
    network_timeout: {
        maxAttempts: 3,
        strategy: 'exponential',
        baseDelay: 1000,     // 1 ثانية، 2 ثانية، 4 ثواني
        maxDelay: 10000,
        jitter: true,
    },
    // Rate Limited: Retry مع Delay ثابت
    rate_limited: {
        maxAttempts: 5,
        strategy: 'fixed',
        baseDelay: 5000,     // انتظر 5 ثواني بين المحاولات
        maxDelay: 5000,
        jitter: false,
    },
    // تعارض Optimistic Lock: أعد المحاولة فوراً
    lock_conflict: {
        maxAttempts: 3,
        strategy: 'immediate',
        baseDelay: 0,
        maxDelay: 0,
        jitter: false,
    },
    // عطل دائم: لا تعيد المحاولة
    validation_error: {
        maxAttempts: 1,
        strategy: 'none',
        baseDelay: 0,
        maxDelay: 0,
        jitter: false,
    },
};

async function retryWithStrategy<T>(
    fn: () => Promise<T>,
    config: RetryConfig,
): Promise<T> {
    let lastError: Error;

    for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
        try {
            return await fn();
        } catch (error) {
            lastError = error;
            if (attempt === config.maxAttempts - 1) break;
            if (config.strategy === 'none') break;

            let delay = config.baseDelay;
            if (config.strategy === 'exponential') {
                delay = Math.min(config.baseDelay * Math.pow(2, attempt), config.maxDelay);
            }
            if (config.jitter) {
                delay += Math.random() * delay * 0.5; // 0-50% Jitter
            }

            await sleep(delay);
        }
    }
    throw lastError;
}

الـ Jitter حاسم. بدونه، كل الـ Clients يعيدون المحاولة بنفس الوقت بعد العطل (Thundering Herd). الـ Jitter يوزع الـ Retries على فترة زمنية، ويقلل الضغط على الخدمة اللي قاعدة تتعافى.

التدهور الأنيق

لما تقع تبعية، قدم اللي تقدر عليه بدل ما تفشل بالكامل.

async function getProductPage(productId: string): Promise<ProductPageData> {
    // بيانات أساسية: لازم تنجح
    const product = await productService.getById(productId);
    if (!product) throw new NotFoundError();

    // بيانات غير حرجة: تدهور أنيق
    const [reviews, recommendations, availability] = await Promise.allSettled([
        reviewService.getForProduct(productId),
        recommendationService.getSimilar(productId),
        inventoryService.checkStock(productId),
    ]);

    return {
        product,
        reviews: reviews.status === 'fulfilled' ? reviews.value : [],
        recommendations: recommendations.status === 'fulfilled' ? recommendations.value : [],
        availability: availability.status === 'fulfilled'
            ? availability.value
            : { status: 'unknown', message: 'Check availability in store' },
    };
}

Promise.allSettled هو المفتاح. بخلاف Promise.all، ما يفشل إذا واحد من الـ Promises انرفض. كل نتيجة تنحل بشكل مستقل. صفحة المنتج تترندر مع البيانات المتاحة.

بيانات قديمة أحسن من بدون بيانات

async function getProductPrice(productId: string): Promise<PriceInfo> {
    try {
        const livePrice = await pricingService.getPrice(productId);
        await cache.set(`price:${productId}`, livePrice, { ttl: 300 });
        return livePrice;
    } catch (error) {
        // خدمة التسعير واقفة: قدم السعر المحفوظ
        const cached = await cache.get(`price:${productId}`);
        if (cached) {
            return { ...cached, stale: true, staleSince: cached.cachedAt };
        }
        // ما في Cache: رجع سعر الكتالوج
        const product = await productService.getById(productId);
        return { price: product.listPrice, stale: true, approximate: true };
    }
}

سعر قديم من الـ Cache أحسن من خطأ 500. سعر تقريبي من الكتالوج أحسن من ما في سعر خالص. دايماً خلي عندك Fallback، حتى لو أقل دقة.

ميزانيات الـ Timeout

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

const TIMEOUT_BUDGETS = {
    api_request: 5000,          // 5 ثواني لأي طلب API
    database_query: 2000,       // 2 ثانية لأي استعلام قاعدة بيانات
    external_api: 3000,         // 3 ثواني لاستدعاءات الخدمات الخارجية
    llm_generation: 30000,      // 30 ثانية لتوليد الذكاء الاصطناعي (Streaming)
    search_query: 1000,         // 1 ثانية للبحث
    cache_operation: 100,       // 100 مللي ثانية لعمليات قراءة/كتابة الـ Cache
};

async function withTimeout<T>(promise: Promise<T>, budget: number, label: string): Promise<T> {
    const timeout = new Promise<never>((_, reject) => {
        setTimeout(() => reject(new TimeoutError(`${label} exceeded ${budget}ms budget`)), budget);
    });
    return Promise.race([promise, timeout]);
}

// الاستخدام
const results = await withTimeout(
    searchService.query(userQuery),
    TIMEOUT_BUDGETS.search_query,
    'product_search',
);

حماية الـ Timeout المتتالي

لما خدمة A تنادي خدمة B واللي تنادي خدمة C، كل خدمة لازم تطرح وقت المعالجة حقها من الميزانية المتبقية:

API Gateway (ميزانية 5 ثواني)
  └── فحص Auth (100 مللي ثانية مستخدمة، 4.9 ثانية باقية)
       └── خدمة المنتج (200 مللي ثانية مستخدمة، 4.7 ثانية باقية)
            └── خدمة التسعير (Timeout: 4.7 ثانية، مش الـ 5 ثواني الأصلية)

بدون ميزانيات متتالية، خدمة التسعير تستخدم الـ 3 ثواني Timeout كاملة حتى لو الـ API Gateway ما بقى عنده إلا 4.7 ثانية. إذا التسعير أخذ 3 ثواني، الـ Gateway يعمل Timeout قبل ما الرد يوصل، وكل الشغل يروح بلاش.

اختبار الأعطال

Chaos Engineering للفرق الصغيرة

ما تحتاج Netflix Chaos Monkey عشان تختبر التعامل مع الأعطال. ابدأ بـ Fault Injection بسيطة:

// Middleware: حقن أعطال في Staging
function chaosMiddleware(req: Request, res: Response, next: NextFunction) {
    if (process.env.NODE_ENV !== 'staging') return next();

    const chaos = req.headers['x-chaos'];
    if (chaos === 'latency') {
        setTimeout(next, 3000); // أضف 3 ثواني تأخير
    } else if (chaos === 'error') {
        res.status(500).json({ error: 'Chaos: injected failure' });
    } else if (chaos === 'timeout') {
        // لا ترد خالص (محاكاة خدمة معلقة)
    } else {
        next();
    }
}

سيناريوهات الاختبار المهمة:

السيناريوكيف تختبرشو تتحقق منه
قاعدة البيانات واقفةأوقف قاعدة البيانات في Stagingالـ Circuit Breaker ينفتح، بيانات محفوظة تتقدم
تبعية بطيئةاحقن 5 ثواني تأخيرالـ Timeout يشتغل، رد مخفض يرجع
الـ Queue ممتلئةإملا الـ Queue برسائل اختبارBackpressure ينطبق، بدون فقدان بيانات
ضغط الذاكرةحدد ذاكرة الـ Containerمعالجة OOM، إعادة تشغيل نظيفة
انتهاء الشهادةاستخدم شهادة قصيرة العمر في Stagingالتنبيه يشتغل قبل الانتهاء

الأخطاء الشائعة

  1. نفس استراتيجية الـ Retry لكل الأعطال. الـ Timeout يحتاج Backoff. الإدخال الغلط يحتاج صفر Retries. الـ Rate Limit يحتاج Delay ثابت.

  2. Circuit Breaker بدون Fallback. فتح الـ Circuit وإرجاع 500 مش تحمل أعطال. قدم بيانات محفوظة، نتائج مخفضة، أو رد في الـ Queue.

  3. بدون Jitter على الـ Retries. كل الـ Clients يعيدون المحاولة بنفس الوقت، ويغرقون الخدمة اللي قاعدة تتعافى. أضف Jitter عشوائي.

  4. Timeouts لا نهائية. طلب يستنى للأبد يحجز اتصال و Thread. كل عملية تحتاج ميزانية Timeout.

  5. اختبار الـ Happy Path بس. إذا ما اختبرت يوم شو يصير لما قاعدة البيانات تقع، ما تعرف إذا الـ Fallbacks حقتك تشتغل.

  6. أعطال متتالية من تبعيات مشتركة. إذا الخدمات A و B و C كلها تعتمد على نفس قاعدة البيانات، وقاعدة البيانات بطيئة، كل الثلاث خدمات تصير بطيئة. Circuit Breakers على التبعيات المشتركة تمنع التتالي.

أهم النقاط

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

  • الـ Circuit Breakers يحتاجون Fallbacks. فتح الـ Circuit مش الحل. تقديم بيانات محفوظة، نتائج بديلة، أو ردود في الـ Queue هو الحل.

  • الـ Jitter يمنع Thundering Herd. عشوائي تأخيرات الـ Retry. بدون Jitter، الـ Retries المتزامنة تصعب التعافي.

  • بيانات قديمة أحسن من بدون بيانات. سعر محفوظ من قبل 5 دقائق أحسن من خطأ 500. دايماً خلي عندك مسار Fallback.

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

  • اختبر الأعطال في Staging. Fault Injection بسيطة (تأخير، أخطاء، اتصالات معلقة) تتحقق إن أنماط المرونة حقتك فعلاً تشتغل.

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

المواضيع المغطاة

أنماط المرونةCircuit Breakerاستراتيجية Retryالتدهور الأنيقموثوقية الأنظمةميزانية Timeoutهندسة الفوضىDead Letter Queue

جاهز لبناء أنظمة ذكاء اصطناعي جاهزة للإنتاج؟

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

ابدأ محادثة