Node.js و Express

بنية الخدمات المصغرة مع Node.js

20 دقيقة الدرس 30 من 40

بنية الخدمات المصغرة مع Node.js

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

الأحادية مقابل الخدمات المصغرة

دعنا نفهم الاختلافات الأساسية:

البنية الأحادية

  • قاعدة كود واحدة تحتوي على جميع الوظائف
  • يتم نشر جميع المكونات معًا كوحدة واحدة
  • قاعدة بيانات واحدة مشتركة بين جميع الميزات
  • التوسع يتطلب تكرار التطبيق بأكمله
  • لغة برمجة/إطار عمل واحدة لكل شيء
// هيكل أحادي project/ ├── controllers/ │ ├── userController.js │ ├── productController.js │ ├── orderController.js │ └── paymentController.js ├── models/ ├── routes/ └── server.js // نقطة دخول واحدة

بنية الخدمات المصغرة

  • خدمات مستقلة متعددة، كل منها بقاعدة كودها الخاصة
  • يتم نشر كل خدمة بشكل مستقل
  • يمكن أن يكون لكل خدمة قاعدة بياناتها الخاصة
  • توسيع نطاق الخدمات الفردية بناءً على الطلب
  • يمكن أن تستخدم خدمات مختلفة تقنيات مختلفة
// هيكل الخدمات المصغرة microservices/ ├── user-service/ │ ├── src/ │ ├── package.json │ └── Dockerfile ├── product-service/ │ ├── src/ │ ├── package.json │ └── Dockerfile ├── order-service/ │ ├── src/ │ ├── package.json │ └── Dockerfile └── payment-service/ ├── src/ ├── package.json └── Dockerfile
ملاحظة: الخدمات المصغرة ليست دائمًا أفضل من الهياكل الأحادية. اختر بناءً على حجم فريقك وتعقيد التطبيق ومتطلبات قابلية التوسع.

تحليل الخدمة

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

// مثال على حدود الخدمة 1. خدمة المستخدم - المصادقة، الملفات الشخصية، التفضيلات 2. خدمة المنتج - كتالوج المنتجات، المخزون، البحث 3. خدمة الطلب - عربة التسوق، إدارة الطلبات 4. خدمة الدفع - معالجة الدفع، الفواتير 5. خدمة الإشعارات - البريد الإلكتروني، الرسائل النصية، الإشعارات 6. خدمة التحليلات - سلوك المستخدم، التقارير، المقاييس

إنشاء خدمة مصغرة بسيطة

دعنا ننشئ خدمة مستخدم أساسية:

// user-service/src/index.js const express = require('express'); const app = express(); app.use(express.json()); // مخزن مستخدمين في الذاكرة (استخدم قاعدة بيانات في الإنتاج) const users = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' } ]; // نقطة نهاية فحص الصحة app.get('/health', (req, res) => { res.json({ status: 'ok', service: 'user-service' }); }); // الحصول على جميع المستخدمين app.get('/users', (req, res) => { res.json(users); }); // الحصول على مستخدم حسب المعرف app.get('/users/:id', (req, res) => { const user = users.find(u => u.id === parseInt(req.params.id)); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json(user); }); // إنشاء مستخدم app.post('/users', (req, res) => { const newUser = { id: users.length + 1, name: req.body.name, email: req.body.email }; users.push(newUser); res.status(201).json(newUser); }); const PORT = process.env.PORT || 3001; app.listen(PORT, () => { console.log(`User service running on port ${PORT}`); });

التواصل بين الخدمات

تحتاج الخدمات للتواصل مع بعضها البعض. هناك نهجان رئيسيان:

1. التواصل عبر HTTP/REST

// order-service/src/index.js const express = require('express'); const axios = require('axios'); const app = express(); app.use(express.json()); const USER_SERVICE_URL = process.env.USER_SERVICE_URL || 'http://localhost:3001'; app.post('/orders', async (req, res) => { try { const { userId, productId, quantity } = req.body; // استدعاء خدمة المستخدم للتحقق من وجود المستخدم const userResponse = await axios.get(`${USER_SERVICE_URL}/users/${userId}`); const user = userResponse.data; // إنشاء طلب const order = { id: Date.now(), userId: user.id, userName: user.name, productId, quantity, createdAt: new Date() }; res.status(201).json(order); } catch (error) { if (error.response?.status === 404) { return res.status(404).json({ error: 'User not found' }); } res.status(500).json({ error: 'Internal server error' }); } }); const PORT = process.env.PORT || 3002; app.listen(PORT, () => { console.log(`Order service running on port ${PORT}`); });
تحذير: استدعاءات HTTP تخلق اقترانًا وثيقًا ويمكن أن تفشل إذا كانت الخدمة معطلة. قم دائمًا بتنفيذ معالجة أخطاء مناسبة ومهلات زمنية.

2. التواصل عبر قائمة انتظار الرسائل (غير متزامن)

استخدام قائمة انتظار رسائل مثل RabbitMQ أو Redis للتواصل غير المتزامن:

// استخدام Redis pub/sub const redis = require('redis'); const publisher = redis.createClient(); const subscriber = redis.createClient(); // خدمة الطلب - نشر حدث app.post('/orders', async (req, res) => { const order = { id: Date.now(), userId: req.body.userId, productId: req.body.productId, quantity: req.body.quantity }; // نشر حدث order.created await publisher.publish('order.created', JSON.stringify(order)); res.status(201).json(order); }); // خدمة الإشعارات - الاشتراك في الأحداث subscriber.subscribe('order.created'); subscriber.on('message', (channel, message) => { if (channel === 'order.created') { const order = JSON.parse(message); console.log('إرسال إشعار للطلب:', order.id); // إرسال إشعار بريد إلكتروني/رسالة نصية } });

نمط بوابة API

تعمل بوابة API كنقطة دخول واحدة لجميع طلبات العميل وتوجهها إلى الخدمات المصغرة المناسبة:

// api-gateway/src/index.js const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const app = express(); // التوجيه إلى خدمة المستخدم app.use('/api/users', createProxyMiddleware({ target: 'http://localhost:3001', changeOrigin: true, pathRewrite: { '^/api/users': '/users' } })); // التوجيه إلى خدمة الطلب app.use('/api/orders', createProxyMiddleware({ target: 'http://localhost:3002', changeOrigin: true, pathRewrite: { '^/api/orders': '/orders' } })); // التوجيه إلى خدمة المنتج app.use('/api/products', createProxyMiddleware({ target: 'http://localhost:3003', changeOrigin: true, pathRewrite: { '^/api/products': '/products' } })); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`API Gateway running on port ${PORT}`); });
نصيحة: يمكن لبوابة API أيضًا التعامل مع المصادقة وتحديد المعدل والتسجيل وتحويل الطلب/الاستجابة.

اكتشاف الخدمة

في البيئات الديناميكية، تحتاج الخدمات لاكتشاف بعضها البعض. إليك سجل خدمة بسيط:

// service-registry/src/index.js const express = require('express'); const app = express(); app.use(express.json()); const services = new Map(); // تسجيل خدمة app.post('/register', (req, res) => { const { name, host, port, healthCheck } = req.body; services.set(name, { name, host, port, healthCheck, registeredAt: new Date() }); console.log(`Service registered: ${name} at ${host}:${port}`); res.json({ message: 'Service registered successfully' }); }); // اكتشاف خدمة app.get('/discover/:serviceName', (req, res) => { const service = services.get(req.params.serviceName); if (!service) { return res.status(404).json({ error: 'Service not found' }); } res.json(service); }); // قائمة جميع الخدمات app.get('/services', (req, res) => { res.json(Array.from(services.values())); }); app.listen(3000, () => { console.log('Service registry running on port 3000'); });
// الخدمات تسجل نفسها عند البدء const axios = require('axios'); async function registerService() { try { await axios.post('http://localhost:3000/register', { name: 'user-service', host: 'localhost', port: 3001, healthCheck: '/health' }); console.log('تم التسجيل في سجل الخدمة'); } catch (error) { console.error('فشل التسجيل:', error.message); } } registerService();

أساسيات Docker لـ Node.js

يسمح لك Docker بتعبئة خدماتك المصغرة كحاويات:

# user-service/Dockerfile FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY src ./src EXPOSE 3001 CMD ["node", "src/index.js"]
# user-service/.dockerignore node_modules npm-debug.log .env .git
# بناء وتشغيل الحاوية docker build -t user-service . docker run -p 3001:3001 user-service # أو استخدم docker-compose لخدمات متعددة

Docker Compose لخدمات متعددة

# docker-compose.yml version: '3.8' services: user-service: build: ./user-service ports: - "3001:3001" environment: - NODE_ENV=production - DB_HOST=postgres depends_on: - postgres order-service: build: ./order-service ports: - "3002:3002" environment: - NODE_ENV=production - USER_SERVICE_URL=http://user-service:3001 depends_on: - user-service - redis api-gateway: build: ./api-gateway ports: - "3000:3000" depends_on: - user-service - order-service postgres: image: postgres:15 environment: - POSTGRES_PASSWORD=secret volumes: - postgres-data:/var/lib/postgresql/data redis: image: redis:7-alpine ports: - "6379:6379" volumes: postgres-data:
# بدء جميع الخدمات docker-compose up -d # عرض السجلات docker-compose logs -f # إيقاف جميع الخدمات docker-compose down

أفضل ممارسات الخدمات المصغرة

  • المسؤولية الواحدة: يجب أن تقوم كل خدمة بشيء واحد بشكل جيد
  • البيانات اللامركزية: يجب أن تمتلك كل خدمة قاعدة بياناتها
  • إصدار API: قم بإصدار واجهات برمجة التطبيقات لتجنب التغييرات المفاجئة
  • فحوصات الصحة: نفذ نقاط نهاية /health للمراقبة
  • التسجيل: استخدم التسجيل المركزي (ELK stack, Datadog)
  • المراقبة: تتبع المقاييس والتأخير ومعدلات الخطأ
  • قاطع الدائرة: الفشل السريع عندما تكون الخدمات التابعة معطلة
  • منطق إعادة المحاولة: نفذ التراجع الأسي للطلبات الفاشلة
  • الأمان: استخدم رموز JWT ومفاتيح API وشبكة الخدمة

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

const axios = require('axios'); class CircuitBreaker { constructor(threshold = 5, timeout = 60000) { this.failureCount = 0; this.threshold = threshold; this.timeout = timeout; this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN this.nextAttempt = Date.now(); } async call(fn) { if (this.state === 'OPEN') { if (Date.now() < this.nextAttempt) { throw new Error('قاطع الدائرة مفتوح'); } this.state = 'HALF_OPEN'; } try { const result = await fn(); this.onSuccess(); return result; } catch (error) { this.onFailure(); throw error; } } onSuccess() { this.failureCount = 0; this.state = 'CLOSED'; } onFailure() { this.failureCount++; if (this.failureCount >= this.threshold) { this.state = 'OPEN'; this.nextAttempt = Date.now() + this.timeout; console.log('تم فتح قاطع الدائرة'); } } } // الاستخدام const breaker = new CircuitBreaker(); async function callUserService(userId) { return breaker.call(async () => { const response = await axios.get(`http://user-service/users/${userId}`); return response.data; }); }

تمرين تطبيقي:

قم ببناء نظام خدمات مصغرة بسيط للتجارة الإلكترونية مع:

  • 3 خدمات: خدمة المستخدم، خدمة المنتج، خدمة الطلب
  • بوابة API لتوجيه الطلبات
  • التواصل بين الخدمات (خدمة الطلب تستدعي خدمات المستخدم والمنتج)
  • نقاط نهاية فحص الصحة لكل خدمة
  • حاويات Docker لكل خدمة
  • Docker Compose لتشغيل جميع الخدمات معًا
  • معالجة أخطاء وتسجيل أساسية

الخلاصة

في هذا الدرس، تعلمت:

  • الاختلافات بين البنية الأحادية وبنية الخدمات المصغرة
  • كيفية تحليل التطبيقات إلى خدمات مصغرة
  • أنماط التواصل بين الخدمات (HTTP وقوائم انتظار الرسائل)
  • نمط بوابة API للتوجيه المركزي
  • اكتشاف الخدمة والتسجيل
  • الحاويات باستخدام Docker
  • إدارة خدمات متعددة باستخدام Docker Compose
  • أفضل الممارسات لبنية الخدمات المصغرة
  • تنفيذ نمط قاطع الدائرة للمرونة