تصميم أنظمة للفشل (لأنها راح تفشل)
أنماط استجابة الأعطال لأنظمة الإنتاج. Circuit Breakers، استراتيجيات Retry، التدهور الأنيق، Dead Letter Handling، ميزانيات Timeout، وChaos Engineering للفرق الصغيرة.
تصنيف الأعطال
الأنظمة تفشل بأربع طرق. كل واحدة تحتاج استجابة مختلفة.
| النوع | الوصف | مثال | الاستجابة الصحيحة |
|---|---|---|---|
| عابر | خلل قصير، يحل نفسه بنفسه | 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 | التنبيه يشتغل قبل الانتهاء |
الأخطاء الشائعة
-
نفس استراتيجية الـ Retry لكل الأعطال. الـ Timeout يحتاج Backoff. الإدخال الغلط يحتاج صفر Retries. الـ Rate Limit يحتاج Delay ثابت.
-
Circuit Breaker بدون Fallback. فتح الـ Circuit وإرجاع 500 مش تحمل أعطال. قدم بيانات محفوظة، نتائج مخفضة، أو رد في الـ Queue.
-
بدون Jitter على الـ Retries. كل الـ Clients يعيدون المحاولة بنفس الوقت، ويغرقون الخدمة اللي قاعدة تتعافى. أضف Jitter عشوائي.
-
Timeouts لا نهائية. طلب يستنى للأبد يحجز اتصال و Thread. كل عملية تحتاج ميزانية Timeout.
-
اختبار الـ Happy Path بس. إذا ما اختبرت يوم شو يصير لما قاعدة البيانات تقع، ما تعرف إذا الـ Fallbacks حقتك تشتغل.
-
أعطال متتالية من تبعيات مشتركة. إذا الخدمات 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 بسيطة (تأخير، أخطاء، اتصالات معلقة) تتحقق إن أنماط المرونة حقتك فعلاً تشتغل.
نصمم أنظمة مرنة كجزء من ممارساتنا في البرمجيات المخصصة والسحابة. إذا تحتاج مساعدة في هندسة الموثوقية، تكلم مع فريقنا أو اطلب عرض سعر.
المواضيع المغطاة
أدلة ذات صلة
أنماط فشل الذكاء الاصطناعي: دليل هندسة الإنتاج
دليل تقني لأعطال أنظمة الذكاء الاصطناعي في الإنتاج. تعرف على الهلوسات، حدود السياق، حقن البرومبت وانحراف النموذج.
اقرأ الدليلالبنية المدفوعة بالأحداث عملياً: شو اللي فعلاً يخرب
أنماط بنية مدفوعة بالأحداث من الإنتاج الفعلي. عواصف الأحداث، حلقات المزامنة الثنائية، dead letters، مخازن الـ idempotency، والاختيار بين Kafka وRabbitMQ وBullMQ وSymfony Messenger.
اقرأ الدليلالدليل الشامل لأنظمة الذكاء الاصطناعي الوكيلي
دليل تقني لأنظمة الذكاء الاصطناعي الوكيلي في بيئات الأعمال. تعرف على البنية والقدرات والتطبيقات العملية للوكلاء المستقلين.
اقرأ الدليلجاهز لبناء أنظمة ذكاء اصطناعي جاهزة للإنتاج؟
فريقنا متخصص في بناء أنظمة ذكاء اصطناعي جاهزة للإنتاج. خلينا نحكي كيف نقدر نساعد.
ابدأ محادثة