مشكلة الفشل المتسلسل
عند فشل خدمة واحدة، يمكن أن ينهار النظام بأكمله — هذا يسمى Cascading Failure.
Circuit Breaker
نمط يمنع استدعاء خدمة فاشلة حتى تتعافى.
الاسم من قاطع الكهرباء في البيت: عند حمل زائد "يفصل" فورًا بدل أن يحترق السلك. بلا حماية، كل طلب لخدمة معطّلة ينتظر حتى انتهاء المهلة، ويحجز أثناء الانتظار اتصالًا وذاكرة في الخدمة المستدعية. مع آلاف الطلبات تنفد مواردها هي أيضًا فتسقط، ثم تسقط الخدمات التي تعتمد عليها. القاطع يراقب نسبة الفشل، وعند تجاوزها يرفض فورًا بدون محاولة اتصال، ثم يجرّب من جديد بعد مهلة.
import {
ServiceNotFoundError, ProductNotFoundError,
logContext, structuredLog,
} from './lib/base_repository';
import CircuitBreaker from 'opossum';
// Repository Protocol للخدمة الخارجية
interface UserServiceRepository {
getUser(id: string): Promise<User>;
}
// Implementation — الطبقة الوحيدة التي تستورد axios مباشرة
class HttpUserServiceRepository implements UserServiceRepository {
private readonly breaker: CircuitBreaker;
constructor() {
const callUserService = async (id: string) => {
const { data } = await axios.get(`http://user-service:3001/users/${id}`);
return data;
};
this.breaker = new CircuitBreaker(callUserService, {
timeout: 3000,
errorThresholdPercentage: 50,
resetTimeout: 30000,
});
this.breaker.on('open', () =>
structuredLog('warn', 'Circuit opened for user-service', { service: 'user-service' }));
this.breaker.on('halfOpen', () =>
structuredLog('info', 'Trying user-service again...', { service: 'user-service' }));
this.breaker.on('close', () =>
structuredLog('info', 'user-service is back', { service: 'user-service' }));
}
async getUser(id: string): Promise<User> {
return this.breaker.fire(id);
}
}
// Service Layer — Domain Exception في حال الفشل
class ResilientUserService {
constructor(private readonly userRepo: UserServiceRepository) {}
async getUser(id: string): Promise<User> {
try {
return await this.userRepo.getUser(id);
} catch (err) {
throw new ServiceNotFoundError(`user/${id}`);
}
}
}
الحالات الثلاث
Circuit Breaker States:
CLOSED: الدائرة مغلقة — الطلبات تمر بشكل طبيعي
OPEN: الدائرة مفتوحة — الطلبات ترفض فوراً
HALF_OPEN: محاولة جزئية — نرسل طلباً تجريبياً
Retry مع Exponential Backoff
async function withExponentialBackoff(fn, maxRetries = 3) {
for (let i = 0; i <= maxRetries; i++) {
try {
return await fn();
} catch (err) {
if (i === maxRetries) throw err;
const delay = Math.min(1000 * Math.pow(2, i), 10000);
console.log(`محاولة ${i + 1}/${maxRetries} بعد ${delay}ms`);
await new Promise(r => setTimeout(r, delay));
}
}
}
Fallback (البديل الآمن)
import { ProductNotFoundError, CacheRepository, ProductRepositoryProtocol } from './lib/base_repository';
class FallbackPricingService {
constructor(
private readonly pricingRepo: PricingRepositoryProtocol,
private readonly cache: CacheRepository,
) {}
async getProductPrice(productId: string): Promise<PriceResult> {
try {
const price = await this.pricingRepo.getPrice(productId);
// تخزين آخر سعر معروف في الكاش عبر Repository
await this.cache.set(`price:${productId}`, price, 3600);
return price;
} catch (err) {
structuredLog('warn', `فشل الحصول على سعر ${productId}`, { productId });
// Fallback 1: آخر سعر معروف من الكاش
const lastPrice = await this.cache.get<PriceResult>(`price:${productId}`);
if (lastPrice) return { ...lastPrice, cached: true };
// Fallback 2: قيمة افتراضية
return { productId, price: 0, currency: 'USD', estimated: true };
}
}
}
Timeout و Bulkhead
import { AxiosInstance } from 'axios';
import { AuthenticationError } from './lib/base_repository';
// Bulkhead — لكل Provider Client خاص به مع Timeout مستقل
// لا تشارك نفس الـ client بين الخدمات: بطء خدمة واحدة يجب ألا يستهلك اتصالات الأخرى
const serviceClients: Record<string, AxiosInstance> = {
user: axios.create({ baseURL: 'http://user-service', timeout: 2000 }),
order: axios.create({ baseURL: 'http://order-service', timeout: 5000 }),
payment: axios.create({ baseURL: 'http://payment-service', timeout: 10000, maxContentLength: 5000 }),
};
// Repository لكل خدمة — مع Domain Exceptions
class BulkheadUserRepository implements UserServiceRepository {
async getUser(id: string): Promise<User> {
try {
const { data } = await serviceClients.user.get(`/users/${id}`);
return data;
} catch (err) {
if (axios.isAxiosError(err) && err.response?.status === 401) {
throw new AuthenticationError('User service rejected token');
}
throw new ServiceNotFoundError(`user/${id}`);
}
}
}
الممارسات الجيدة
- ضع Timeout على كل طلب
- استخدم Circuit Breaker للخدمات الحيوية
- طبق Fallback عند فشل الخدمة
- سجل كل الفشل للتحليل
- اختبر تحمل الأخطاء (Chaos Engineering)
أخطاء شائعة
- قاطع بدون Timeout: إذا لم تنتهِ مهلة الطلب، لا يعرف القاطع أنه فشل أصلًا. لذلك
timeout: 3000جزء أساسي من الإعداد أعلاه. - قاطع بدون Fallback: فتح الدائرة يعني أن كل الطلبات ترجع بخطأ فوري. قرّر مسبقًا ماذا يرى المستخدم: قيمة مخزّنة، أو قيمة افتراضية، أو رسالة واضحة.
- عتبة فشل منخفضة جدًا: تذبذب شبكة طبيعي بسيط يفتح الدائرة ويقطع خدمة سليمة.
⚠️ بدون Circuit Breaker، فشل خدمة واحدة قد يودي بالنظام كاملاً
🎯 التالي: توثيق الخدمات