İçeriğe geç
Node.js

Node.js Microservices: Mimari Pattern'ler

Tarık Tunç
Node.js Microservices: Mimari Pattern'ler

Node.js Microservices: Mimari Pattern'ler

Microservices mimarisi, büyük ve karmaşık uygulamaları bağımsız, küçük servislere ayırarak geliştirme, dağıtım ve ölçekleme esnekliği kazandırır. Node.js, hafif yapısı ve event-driven doğası ile microservices geliştirme için ideal bir platformdur. Bu rehberde temel mimari pattern'leri, iletişim stratejilerini ve Node.js ile microservices tasarımını ele alacağız.

1. Monolith vs Microservices

Her iki yaklaşımın avantaj ve dezavantajlarını anlamak, doğru kararı vermek için önemlidir:

ÖzellikMonolithMicroservices
KarmaşıklıkDüşük (başlangıçta)Yüksek
DeploymentTek birimBağımsız servisler
ÖlçeklemeTüm uygulamaServis bazlı
TeknolojiTek stackPolyglot (karma)
Takım yapısıTek büyük takımKüçük bağımsız takımlar
Veri yönetimiTek veritabanıServis başına veritabanı
Uyarı: Microservices her proje için uygun değildir. Küçük projeler ve startup'lar için monolith ile başlayıp gerektiğinde microservices'e geçiş yapmak genellikle daha akıllıca bir stratejidir.

2. API Gateway Pattern

API Gateway, tüm istemci isteklerini tek bir noktadan karşılayan ve ilgili servislere yönlendiren bir katmandır.

// gateway/server.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const rateLimit = require('express-rate-limit');

const app = express();

// Rate limiting
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));

// Auth middleware
app.use('/api', async (req, res, next) => {
  const token = req.headers.authorization;
  if (!token && !req.path.startsWith('/auth')) {
    return res.status(401).json({ error: 'Yetkisiz' });
  }
  next();
});

// Servis yönlendirmeleri
app.use('/api/users', createProxyMiddleware({
  target: process.env.USER_SERVICE_URL || 'http://user-service:3001',
  changeOrigin: true,
  pathRewrite: { '^/api/users': '' },
}));

app.use('/api/orders', createProxyMiddleware({
  target: process.env.ORDER_SERVICE_URL || 'http://order-service:3002',
  changeOrigin: true,
  pathRewrite: { '^/api/orders': '' },
}));

app.use('/api/products', createProxyMiddleware({
  target: process.env.PRODUCT_SERVICE_URL || 'http://product-service:3003',
  changeOrigin: true,
  pathRewrite: { '^/api/products': '' },
}));

app.listen(3000, () => console.log('API Gateway: port 3000'));

3. Service Communication Pattern'leri

Synchronous — HTTP/REST

// order-service/services/userClient.js
const axios = require('axios');

class UserClient {
  constructor() {
    this.baseURL = process.env.USER_SERVICE_URL || 'http://user-service:3001';
    this.client = axios.create({
      baseURL: this.baseURL,
      timeout: 5000,
    });
  }

  async getUser(userId) {
    try {
      const { data } = await this.client.get(`/users/${userId}`);
      return data;
    } catch (error) {
      if (error.response?.status === 404) return null;
      throw new Error(`User service iletişim hatası: ${error.message}`);
    }
  }
}

module.exports = new UserClient();

Asynchronous — Message Queue (RabbitMQ)

// shared/messageQueue.js
const amqp = require('amqplib');

class MessageQueue {
  constructor() {
    this.connection = null;
    this.channel = null;
  }

  async connect() {
    this.connection = await amqp.connect(process.env.RABBITMQ_URL || 'amqp://localhost');
    this.channel = await this.connection.createChannel();
  }

  async publish(queue, message) {
    await this.channel.assertQueue(queue, { durable: true });
    this.channel.sendToQueue(queue, Buffer.from(JSON.stringify(message)), {
      persistent: true,
    });
  }

  async subscribe(queue, handler) {
    await this.channel.assertQueue(queue, { durable: true });
    this.channel.prefetch(1);
    this.channel.consume(queue, async (msg) => {
      try {
        const data = JSON.parse(msg.content.toString());
        await handler(data);
        this.channel.ack(msg);
      } catch (error) {
        this.channel.nack(msg, false, false); // Dead letter queue'ya gönder
      }
    });
  }
}

module.exports = new MessageQueue();
// order-service — sipariş oluşturulduğunda event yayınla
const mq = require('../shared/messageQueue');

async function createOrder(orderData) {
  const order = await Order.create(orderData);

  // Event yayınla
  await mq.publish('order_events', {
    type: 'ORDER_CREATED',
    data: order,
    timestamp: new Date().toISOString(),
  });

  return order;
}

// notification-service — event'i dinle
const mq = require('../shared/messageQueue');

await mq.subscribe('order_events', async (event) => {
  if (event.type === 'ORDER_CREATED') {
    await sendOrderConfirmationEmail(event.data);
  }
});

4. Circuit Breaker Pattern

Bir servis hata verdiğinde, sürekli istek göndererek sistemi daha da kötüleştirmek yerine devreyi kesin.

// shared/circuitBreaker.js
class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;
    this.resetTimeout = options.resetTimeout || 30000;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.failureCount = 0;
    this.lastFailureTime = null;
  }

  async execute(fn) {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailureTime >= this.resetTimeout) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Circuit breaker OPEN — servis geçici olarak devre dışı');
      }
    }

    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++;
    this.lastFailureTime = Date.now();
    if (this.failureCount >= this.failureThreshold) {
      this.state = 'OPEN';
    }
  }
}

// Kullanım
const breaker = new CircuitBreaker({ failureThreshold: 3, resetTimeout: 10000 });

async function getUserSafe(userId) {
  return breaker.execute(() => userClient.getUser(userId));
}

5. Saga Pattern — Dağıtık İşlem Yönetimi

Birden fazla servisi kapsayan işlemlerde, hata durumunda telafi (compensating) işlemleri çalıştırmak için Saga pattern kullanılır.

// saga/createOrderSaga.js
class CreateOrderSaga {
  constructor(orderService, paymentService, inventoryService) {
    this.steps = [
      {
        name: 'reserve_inventory',
        execute: (data) => inventoryService.reserve(data.items),
        compensate: (data) => inventoryService.release(data.items),
      },
      {
        name: 'process_payment',
        execute: (data) => paymentService.charge(data.userId, data.total),
        compensate: (data) => paymentService.refund(data.userId, data.total),
      },
      {
        name: 'create_order',
        execute: (data) => orderService.create(data),
        compensate: (data) => orderService.cancel(data.orderId),
      },
    ];
  }

  async run(orderData) {
    const completedSteps = [];

    for (const step of this.steps) {
      try {
        const result = await step.execute(orderData);
        completedSteps.push(step);
        orderData = { ...orderData, ...result };
      } catch (error) {
        // Tamamlanan adımları geri al
        for (const completed of completedSteps.reverse()) {
          try {
            await completed.compensate(orderData);
          } catch (compError) {
            console.error(`Telafi hatası: ${completed.name}`, compError);
          }
        }
        throw new Error(`Saga başarısız: ${step.name} — ${error.message}`);
      }
    }

    return orderData;
  }
}

6. Service Discovery ve Docker Compose

# docker-compose.yml
version: '3.8'

services:
  gateway:
    build: ./gateway
    ports:
      - "3000:3000"
    environment:
      - USER_SERVICE_URL=http://user-service:3001
      - ORDER_SERVICE_URL=http://order-service:3002
    depends_on:
      - user-service
      - order-service

  user-service:
    build: ./user-service
    environment:
      - DATABASE_URL=postgresql://user:pass@user-db:5432/users
    depends_on:
      - user-db

  order-service:
    build: ./order-service
    environment:
      - DATABASE_URL=postgresql://user:pass@order-db:5432/orders
      - RABBITMQ_URL=amqp://rabbitmq
    depends_on:
      - order-db
      - rabbitmq

  user-db:
    image: postgres:16-alpine

  order-db:
    image: postgres:16-alpine

  rabbitmq:
    image: rabbitmq:3-management-alpine
    ports:
      - "15672:15672"
Bilgi: Docker Compose ağında servisler birbirlerine servis adıyla erişir. Kubernetes'te ise DNS tabanlı service discovery otomatik çalışır.

Sonuç

Microservices mimarisi güçlü ama karmaşık bir yaklaşımdır. API Gateway ile istekleri yönetin, servisler arası iletişimde sync (HTTP) ve async (message queue) pattern'leri duruma göre seçin. Circuit Breaker ile hata yayılmasını engelleyin ve Saga pattern ile dağıtık işlemleri yönetin. Her servisin kendi veritabanına sahip olması (database per service) bağımsızlığı sağlar. Node.js'in hafif yapısı ve zengin ekosistemi, bu pattern'leri uygulamak için mükemmel bir temel sunar.

Kaynaklar

İlgili Yazılar