تطوير واجهات REST API

اتصال API للخدمات المصغرة

15 دقيقة الدرس 27 من 35

فهم اتصال الخدمات المصغرة

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

نظرة عامة على أنماط الاتصال

الاتصال المتزامن

تقوم الخدمات بإجراء طلبات HTTP مباشرة وتنتظر الاستجابات. البروتوكولات الشائعة تشمل REST و GraphQL و gRPC.

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

الاتصال غير المتزامن

تتواصل الخدمات من خلال قوائم انتظار الرسائل أو ناقلات الأحداث دون انتظار الاستجابات. أمثلة تشمل RabbitMQ و Apache Kafka و AWS SQS.

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

مصادقة خدمة إلى خدمة

تأمين الاتصال بين الخدمات المصغرة أمر بالغ الأهمية. توجد عدة نهج، كل منها له مقايضات مختلفة.

1. مفاتيح API (الأسرار المشتركة)

بسيط ولكنه فعال للاتصال الداخلي بين الخدمات. كل خدمة لها مفتاح API فريد.

// الخدمة A تستدعي الخدمة B بمفتاح API const axios = require('axios'); async function fetchUserOrders(userId) { try { const response = await axios.get( `http://order-service:3001/orders?userId=${userId}`, { headers: { 'X-API-Key': process.env.ORDER_SERVICE_API_KEY, 'X-Service-Name': 'user-service', 'X-Request-ID': generateRequestId() } } ); return response.data; } catch (error) { console.error('فشل في جلب الطلبات:', error.message); throw error; } } // في الخدمة المستقبلة (خدمة الطلب) const authenticateService = (req, res, next) => { const apiKey = req.headers['x-api-key']; const serviceName = req.headers['x-service-name']; // التحقق من صحة مفتاح API مقابل المفاتيح المخزنة const validKeys = { 'user-service': process.env.USER_SERVICE_API_KEY, 'product-service': process.env.PRODUCT_SERVICE_API_KEY, 'notification-service': process.env.NOTIFICATION_SERVICE_API_KEY }; if (!apiKey || validKeys[serviceName] !== apiKey) { return res.status(401).json({ error: 'غير مصرح', message: 'بيانات اعتماد خدمة غير صالحة' }); } req.callingService = serviceName; next(); }; app.use('/orders', authenticateService);
اعتبار أمني: يجب تخزين مفاتيح API في متغيرات البيئة أو أنظمة إدارة الأسرار (AWS Secrets Manager، HashiCorp Vault)، لا تكتبها مباشرة في الكود أبدًا. قم بتدوير المفاتيح بانتظام ونفذ إصدارات المفاتيح.

2. رموز خدمة JWT

تقوم الخدمات بالمصادقة مع بعضها البعض باستخدام رموز JWT مع مطالبات خاصة بالخدمة.

// توليد رمز الخدمة const jwt = require('jsonwebtoken'); function generateServiceToken(serviceName) { const payload = { sub: serviceName, iss: 'api-gateway', aud: ['order-service', 'user-service', 'product-service'], iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + (5 * 60), // 5 دقائق service: true, permissions: getServicePermissions(serviceName) }; return jwt.sign(payload, process.env.SERVICE_JWT_SECRET); } function getServicePermissions(serviceName) { const permissions = { 'user-service': ['read:orders', 'read:products'], 'order-service': ['read:users', 'read:products', 'write:notifications'], 'notification-service': ['read:users'] }; return permissions[serviceName] || []; } // إجراء مكالمات خدمة مصادق عليها const axios = require('axios'); async function callOrderService(endpoint, data) { const token = generateServiceToken('user-service'); return await axios({ method: 'GET', url: `http://order-service:3001${endpoint}`, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, data }); } // التحقق من صحة رمز الخدمة المستقبلة const verifyServiceToken = (req, res, next) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'لم يتم توفير رمز' }); } try { const decoded = jwt.verify(token, process.env.SERVICE_JWT_SECRET); // تحقق من أنه رمز خدمة if (!decoded.service) { return res.status(403).json({ error: 'ليس رمز خدمة' }); } // تحقق من أن جمهور الرمز يتضمن هذه الخدمة if (!decoded.aud.includes('order-service')) { return res.status(403).json({ error: 'الرمز غير صالح لهذه الخدمة' }); } req.callingService = decoded.sub; req.servicePermissions = decoded.permissions; next(); } catch (error) { return res.status(403).json({ error: 'رمز غير صالح' }); } };

3. Mutual TLS (mTLS)

كل من العميل والخادم يقومان بمصادقة بعضهما البعض باستخدام شهادات SSL/TLS. الأكثر أمانًا ولكن معقد للتنفيذ.

// تكوين عميل Node.js mTLS const https = require('https'); const fs = require('fs'); const options = { hostname: 'order-service', port: 3001, path: '/orders', method: 'GET', // شهادة العميل cert: fs.readFileSync('./certs/user-service-cert.pem'), key: fs.readFileSync('./certs/user-service-key.pem'), // شهادة CA للتحقق من الخادم ca: fs.readFileSync('./certs/ca-cert.pem'), // رفض الشهادات غير المصرح بها rejectUnauthorized: true }; const req = https.request(options, (res) => { let data = ''; res.on('data', (chunk) => data += chunk); res.on('end', () => console.log('الاستجابة:', data)); }); req.on('error', (error) => console.error('خطأ mTLS:', error)); req.end();
أفضل ممارسة: استخدم حلول شبكة الخدمة مثل Istio أو Linkerd لأتمتة تنفيذ mTLS عبر جميع الخدمات. إنها تتعامل مع تدوير الشهادات والمصادقة المتبادلة والاتصال المشفر بشكل شفاف.

نمط قاطع الدائرة

منع الفشل المتتالي من خلال الفشل السريع عندما تكون الخدمة غير متاحة. يراقب قاطع الدائرة الفشل ويمنع مؤقتًا الطلبات إلى الخدمات الفاشلة.

حالات قاطع الدائرة

  • مغلق: عملية عادية، تمر الطلبات
  • مفتوح: الخدمة تفشل، الطلبات تفشل فورًا دون استدعاء الخدمة
  • نصف مفتوح: اختبار إذا تعافت الخدمة، يسمح بطلبات محدودة
// تنفيذ قاطع الدائرة مع opossum const CircuitBreaker = require('opossum'); const axios = require('axios'); // دالة لاستدعاء خدمة خارجية async function callProductService(productId) { const response = await axios.get( `http://product-service:3001/products/${productId}`, { timeout: 3000, headers: { 'X-API-Key': process.env.PRODUCT_SERVICE_API_KEY } } ); return response.data; } // تكوين قاطع الدائرة const breakerOptions = { timeout: 3000, // مهلة الطلب errorThresholdPercentage: 50, // افتح الدائرة عند نسبة فشل 50% resetTimeout: 30000, // حاول مرة أخرى بعد 30 ثانية volumeThreshold: 10, // الحد الأدنى للطلبات قبل التعثر name: 'productServiceBreaker', fallback: (productId) => { console.log(`الدائرة مفتوحة، إرجاع البيانات المخزنة مؤقتًا للمنتج ${productId}`); return getCachedProduct(productId); } }; const productBreaker = new CircuitBreaker(callProductService, breakerOptions); // معالجات الأحداث productBreaker.on('open', () => { console.error('قاطع الدائرة مفتوح - خدمة المنتج معطلة'); // إرسال تنبيه إلى نظام المراقبة alerting.send('قاطع دائرة خدمة المنتج مفتوح'); }); productBreaker.on('halfOpen', () => { console.log('قاطع الدائرة نصف مفتوح - اختبار خدمة المنتج'); }); productBreaker.on('close', () => { console.log('قاطع الدائرة مغلق - تعافت خدمة المنتج'); alerting.send('قاطع دائرة خدمة المنتج مغلق'); }); productBreaker.on('fallback', (result) => { console.log('تم تنفيذ البديل، إرجاع:', result); }); // الاستخدام في الخدمة async function getProductDetails(productId) { try { const product = await productBreaker.fire(productId); return product; } catch (error) { // التعامل مع الفشل الكامل console.error('خدمة المنتج غير متاحة تمامًا:', error); throw new Error('معلومات المنتج غير متاحة مؤقتًا'); } } // تنفيذ ذاكرة التخزين المؤقت للبديل const productCache = new Map(); async function getCachedProduct(productId) { const cached = productCache.get(productId); if (cached) { return { ...cached, cached: true }; } throw new Error('لا توجد بيانات مخزنة مؤقتًا'); }
مقاييس قاطع الدائرة: راقب إحصائيات قاطع الدائرة (معدل الفشل، أحداث الفتح/الإغلاق، تنفيذات البديل) لتحديد مشاكل صحة الخدمة وتحسين إعدادات المهلة/العتبة.

استراتيجيات إعادة المحاولة

إعادة محاولة الطلبات الفاشلة تلقائيًا مع استراتيجيات تراجع ذكية للتعامل مع الفشل العابر.

التراجع الأسي مع التذبذب

// تنفيذ إعادة المحاولة مع التراجع الأسي const axios = require('axios'); async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) { let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error; // لا تعيد المحاولة على أخطاء 4xx (أخطاء العميل) if (error.response?.status >= 400 && error.response?.status < 500) { throw error; } // آخر محاولة، ارمي الخطأ if (attempt === maxRetries) { console.error(`فشلت جميع المحاولات الـ ${maxRetries + 1}`); throw lastError; } // احسب التأخير مع التراجع الأسي والتذبذب const exponentialDelay = initialDelay * Math.pow(2, attempt); const jitter = Math.random() * 1000; // عشوائي 0-1000ms const delay = exponentialDelay + jitter; console.log(`فشلت المحاولة ${attempt + 1}، إعادة المحاولة في ${delay}ms...`); await sleep(delay); } } throw lastError; } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // الاستخدام async function fetchUserOrders(userId) { return await retryWithBackoff(async () => { const response = await axios.get( `http://order-service:3001/orders?userId=${userId}`, { timeout: 5000, headers: { 'X-API-Key': process.env.ORDER_SERVICE_API_KEY } } ); return response.data; }, 3, 1000); // حد أقصى 3 إعادات محاولة، بدءًا من تأخير ثانية واحدة }
أهمية التذبذب: إضافة تذبذب عشوائي يمنع مشكلة "القطيع الرعدي" حيث يعيد العديد من العملاء المحاولة في وقت واحد، مما قد يطغى على الخدمة المتعافية.

إعادة المحاولة المتقدمة مع axios-retry

const axios = require('axios'); const axiosRetry = require('axios-retry'); // تكوين axios مع منطق إعادة المحاولة axiosRetry(axios, { retries: 3, retryDelay: axiosRetry.exponentialDelay, // تراجع أسي مدمج retryCondition: (error) => { // إعادة المحاولة على أخطاء الشبكة أو استجابات 5xx return axiosRetry.isNetworkOrIdempotentRequestError(error) || (error.response?.status >= 500 && error.response?.status < 600); }, onRetry: (retryCount, error, requestConfig) => { console.log(`محاولة إعادة ${retryCount} لـ ${requestConfig.url}`); } }); // الآن جميع طلبات axios تعيد المحاولة تلقائيًا async function getProduct(productId) { const response = await axios.get( `http://product-service:3001/products/${productId}`, { timeout: 3000, headers: { 'X-API-Key': process.env.PRODUCT_SERVICE_API_KEY } } ); return response.data; }

إدارة المهلة

التكوين الصحيح للمهلة يمنع الانتظار إلى أجل غير مسمى والتأخيرات المتتالية عبر الخدمات.

// استراتيجية مهلة متعددة المستويات const axios = require('axios'); // إنشاء نسخة axios مع مهلات افتراضية const serviceClient = axios.create({ timeout: 5000, // مهلة افتراضية 5 ثوانٍ headers: { 'X-API-Key': process.env.API_KEY, 'X-Service-Name': 'user-service' } }); // مهلة الطلب (الوقت الإجمالي بما في ذلك إعادات المحاولة) const AbortController = require('abort-controller'); async function fetchWithTotalTimeout(url, totalTimeout = 10000) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), totalTimeout); try { const response = await serviceClient.get(url, { signal: controller.signal }); return response.data; } catch (error) { if (error.name === 'AbortError') { throw new Error(`انتهت مهلة الطلب بعد ${totalTimeout}ms`); } throw error; } finally { clearTimeout(timeoutId); } } // مهلات مختلفة لعمليات مختلفة async function getUserProfile(userId) { // عملية سريعة - مهلة ثانيتين return await serviceClient.get(`/users/${userId}`, { timeout: 2000 }); } async function generateReport(userId) { // عملية بطيئة - مهلة 30 ثانية return await serviceClient.post('/reports/generate', { userId }, { timeout: 30000 }); }
أفضل ممارسات المهلة: اضبط المهلات أقصر من مهلات موازن التحميل/بوابة API الخاصة بك. قاعدة جيدة: مهلة الخدمة < مهلة البوابة < مهلة العميل. راقب أوقات استجابة P95/P99 لتعيين مهلات مناسبة.

gRPC للاتصال عالي الأداء

gRPC هو إطار عمل RPC عالي الأداء يستخدم HTTP/2 وProtocol Buffers. مثالي للاتصال بين الخدمات.

لماذا gRPC؟

  • الأداء: بروتوكول ثنائي، تعدد إرسال HTTP/2، حمولات أصغر
  • سلامة النوع: عقود قوية النوع مع Protocol Buffers
  • البث ثنائي الاتجاه: دعم بث الخادم والعميل
  • توليد الكود: توليد تلقائي لكود العميل/الخادم بلغات متعددة
// user.proto - تعريف Protocol Buffer syntax = "proto3"; package user; service UserService { rpc GetUser (GetUserRequest) returns (User); rpc ListUsers (ListUsersRequest) returns (stream User); rpc UpdateUser (UpdateUserRequest) returns (User); } message GetUserRequest { int32 id = 1; } message ListUsersRequest { int32 page = 1; int32 page_size = 2; } message UpdateUserRequest { int32 id = 1; string name = 2; string email = 3; } message User { int32 id = 1; string name = 2; string email = 3; string created_at = 4; }
// خادم gRPC (Node.js) const grpc = require('@grpc/grpc-js'); const protoLoader = require('@grpc/proto-loader'); // تحميل ملف proto const packageDefinition = protoLoader.loadSync('user.proto', { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true }); const userProto = grpc.loadPackageDefinition(packageDefinition).user; // تنفيذ طرق الخدمة const getUser = async (call, callback) => { const userId = call.request.id; try { const user = await db.users.findById(userId); callback(null, user); } catch (error) { callback({ code: grpc.status.NOT_FOUND, message: `المستخدم ${userId} غير موجود` }); } }; const listUsers = (call) => { const { page, page_size } = call.request; // بث المستخدمين إلى العميل db.users.findAll({ page, page_size }).forEach(user => { call.write(user); }); call.end(); }; // إنشاء وبدء الخادم const server = new grpc.Server(); server.addService(userProto.UserService.service, { getUser, listUsers, updateUser }); server.bindAsync( '0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), (err, port) => { if (err) { console.error('خطأ في الخادم:', err); return; } console.log(`خادم gRPC يعمل على المنفذ ${port}`); server.start(); } );
// عميل gRPC (Node.js) const grpc = require('@grpc/grpc-js'); const protoLoader = require('@grpc/proto-loader'); const packageDefinition = protoLoader.loadSync('user.proto'); const userProto = grpc.loadPackageDefinition(packageDefinition).user; // إنشاء العميل const client = new userProto.UserService( 'user-service:50051', grpc.credentials.createInsecure() ); // استدعاء طريقة getUser client.getUser({ id: 123 }, (error, user) => { if (error) { console.error('خطأ gRPC:', error); return; } console.log('المستخدم:', user); }); // بث المستخدمين const call = client.listUsers({ page: 1, page_size: 10 }); call.on('data', (user) => { console.log('مستخدم مستلم:', user); }); call.on('end', () => { console.log('انتهى البث'); }); call.on('error', (error) => { console.error('خطأ في البث:', error); });
أفضل ممارسات gRPC: استخدم gRPC للاتصال الداخلي بين الخدمات حيث تتحكم في كل من العميل والخادم. استخدم REST لواجهات برمجة التطبيقات العامة وعمليات التكامل مع أطراف ثالثة. ضع في اعتبارك gRPC-Web لعملاء المتصفح.

الجمع بين الأنماط: اتصال خدمة مرن

// عميل خدمة جاهز للإنتاج يجمع بين جميع الأنماط const axios = require('axios'); const CircuitBreaker = require('opossum'); const axiosRetry = require('axios-retry'); class ResilientServiceClient { constructor(serviceName, baseURL, options = {}) { this.serviceName = serviceName; this.baseURL = baseURL; // إنشاء نسخة axios this.client = axios.create({ baseURL, timeout: options.timeout || 5000, headers: { 'X-API-Key': process.env[`${serviceName.toUpperCase()}_API_KEY`], 'X-Service-Name': process.env.SERVICE_NAME } }); // إضافة منطق إعادة المحاولة axiosRetry(this.client, { retries: options.retries || 3, retryDelay: axiosRetry.exponentialDelay, retryCondition: axiosRetry.isNetworkOrIdempotentRequestError }); // التفاف في قاطع الدائرة this.breaker = new CircuitBreaker( (config) => this.client.request(config), { timeout: options.breakerTimeout || 10000, errorThresholdPercentage: 50, resetTimeout: 30000, name: `${serviceName}Breaker` } ); this.setupEventHandlers(); } setupEventHandlers() { this.breaker.on('open', () => { console.error(`[${this.serviceName}] قاطع الدائرة مفتوح`); }); this.breaker.on('close', () => { console.log(`[${this.serviceName}] قاطع الدائرة مغلق`); }); } async get(url, config = {}) { return await this.breaker.fire({ method: 'GET', url, ...config }); } async post(url, data, config = {}) { return await this.breaker.fire({ method: 'POST', url, data, ...config }); } async put(url, data, config = {}) { return await this.breaker.fire({ method: 'PUT', url, data, ...config }); } async delete(url, config = {}) { return await this.breaker.fire({ method: 'DELETE', url, ...config }); } } // الاستخدام const orderService = new ResilientServiceClient( 'order-service', 'http://order-service:3001' ); const productService = new ResilientServiceClient( 'product-service', 'http://product-service:3002' ); // إجراء مكالمات مرنة async function getUserOrders(userId) { try { const response = await orderService.get(`/orders?userId=${userId}`); return response.data; } catch (error) { console.error('فشل في جلب الطلبات:', error.message); return []; } }
تمرين: قم ببناء نظام اتصال خدمات مصغرة مرن مع المتطلبات التالية:
  1. قم بإنشاء ثلاث خدمات: خدمة المستخدم، خدمة الطلب، وخدمة الإشعارات
  2. نفذ مصادقة خدمة إلى خدمة قائمة على JWT
  3. أضف قواطع الدائرة بعتبة خطأ 50% ومهلة إعادة ضبط 30 ثانية
  4. نفذ إعادة محاولة التراجع الأسي (حد أقصى 3 إعادات محاولة) مع التذبذب
  5. اضبط مهلات مناسبة: 3 ثوانٍ لطلبات GET، 10 ثوانٍ لطلبات POST/PUT
  6. عندما يتم استدعاء خدمة الطلب، يجب أن:
    • تجلب تفاصيل المستخدم من خدمة المستخدم
    • تنشئ الطلب
    • تخطر المستخدم عبر خدمة الإشعارات (fire-and-forget، لا تفشل إنشاء الطلب إذا فشل الإشعار)
  7. سجل جميع تغييرات حالة قاطع الدائرة ومحاولات إعادة المحاولة

نفذ نقطة نهاية إنشاء الطلب مع معالجة الأخطاء المناسبة وآليات البديل.