Микросервисы на JavaScript: архитектура и практика продвинутого уровня

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Микросервисы на JavaScript: от монолита к распределенной системе

Переход от монолитного приложения к микросервисной архитектуре — это не просто технический рефакторинг, а изменение философии разработки. На продвинутом уровне работы с Node.js мы сталкиваемся с необходимостью не просто разбить код на модули, а создать устойчивую, масштабируемую и отказоустойчивую распределенную систему. В этой статье мы разберем ключевые архитектурные решения и практические паттерны, которые помогут вам построить production-ready микросервисы на JavaScript.

Основные вызовы, с которыми сталкивается разработчик: синхронизация данных между сервисами, поддержание целостности транзакций, обнаружение сервисов и эффективная коммуникация. Ошибка на этапе проектирования может привести к хаосу в продакшене. Давайте рассмотрим, как этого избежать.

1. Коммуникация между сервисами: RabbitMQ и gRPC

В мире микросервисов ключевым является выбор протокола общения. Для Node.js наиболее популярны два подхода: асинхронная очередь сообщений (RabbitMQ) и синхронный высокопроизводительный RPC (gRPC).

Асинхронная очередь (RabbitMQ)

Используется для событийно-ориентированной архитектуры. Сервис-производитель не ждет ответа, а публикует событие. Это снижает связанность. Рассмотрим пример отправки заказа в сервис доставки.

// Сервис "Заказы" — продюсер
const amqp = require('amqplib');

async function publishOrderCreated(order) {
  const connection = await amqp.connect('amqp://rabbitmq:5672');
  const channel = await connection.createChannel();
  const exchange = 'order.events';
  await channel.assertExchange(exchange, 'topic', { durable: true });

  channel.publish(exchange, 'order.created', Buffer.from(JSON.stringify(order)), {
    persistent: true
  });

  console.log(`[x] Sent order: ${order.id}`);
  await channel.close();
  await connection.close();
}

// Вызов при создании заказа
publishOrderCreated({ id: '123', userId: '456', amount: 1500 });

Сервис-потребитель (доставка) подписывается на очередь:

// Сервис "Доставка" — потребитель
const amqp = require('amqplib');

async function consumeOrders() {
  const connection = await amqp.connect('amqp://rabbitmq:5672');
  const channel = await connection.createChannel();
  const exchange = 'order.events';
  await channel.assertExchange(exchange, 'topic', { durable: true });

  const q = await channel.assertQueue('', { exclusive: true });
  await channel.bindQueue(q.queue, exchange, 'order.created');

  channel.consume(q.queue, (msg) => {
    if (msg.content) {
      const order = JSON.parse(msg.content.toString());
      console.log(`[x] Received order: ${order.id}`);
      // Логика создания доставки
      channel.ack(msg);
    }
  });
}

consumeOrders();

Синхронный gRPC

Для сценариев, где требуется мгновенный ответ (например, проверка баланса пользователя), gRPC обеспечивает строгую типизацию и высокую скорость через HTTP/2. Определяем прото-файл:

// user.proto
service UserService {
  rpc GetUserBalance (UserRequest) returns (UserResponse);
}

message UserRequest {
  string user_id = 1;
}

message UserResponse {
  double balance = 1;
}

Реализация сервера на Node.js:

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const PROTO_PATH = './user.proto';

const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true
});
const userProto = grpc.loadPackageDefinition(packageDefinition).userservice;

function getBalance(call, callback) {
  const userId = call.request.user_id;
  // Имитация запроса к БД
  const balance = userId === '456' ? 5000 : 0;
  callback(null, { balance });
}

const server = new grpc.Server();
server.addService(userProto.UserService.service, { GetUserBalance: getBalance });
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
  console.log('gRPC server running on port 50051');
  server.start();
});

2. Паттерн Saga: управление распределенными транзакциями

В микросервисах ACID-транзакции невозможны. Паттерн Saga решает проблему целостности данных через цепочку локальных транзакций с компенсирующими действиями. Рассмотрим хореографическую сагу для оформления заказа.

Сценарий: Создать заказ → Зарезервировать товар → Списать деньги → Отправить уведомление. Если списание денег не удалось — отменить резервирование.

// Сервис "Заказы" (координатор событий)
const EventEmitter = require('events');
const amqp = require('amqplib');

class OrderSaga extends EventEmitter {
  async start(order) {
    console.log('Saga started for order: ', order.id);
    // Шаг 1: Создать заказ
    await this.saveOrder(order);
    this.emit('order.created', order);
  }

  async handlePaymentFailed(order) {
    console.log('Payment failed, compensating inventory...');
    // Компенсация: освободить товары
    await this.releaseInventory(order.items);
    await this.cancelOrder(order.id);
    this.emit('saga.failed', order);
  }
}

// Подписка через RabbitMQ (упрощенно)
const saga = new OrderSaga();
saga.on('order.created', async (order) => {
  // Сервис инвентаря резервирует товары
  await reserveInventory(order.items);
});

saga.on('inventory.reserved', async (order) => {
  // Сервис платежей списывает деньги
  const success = await processPayment(order);
  if (!success) {
    saga.handlePaymentFailed(order);
  }
});

3. Паттерн CQRS: разделение чтения и записи

CQRS (Command Query Responsibility Segregation) предполагает использование разных моделей для операций чтения (Query) и записи (Command). Это особенно полезно, когда нагрузка на чтение и запись сильно различается.

Пример: Создаем два сервиса: order-write (принимает команды) и order-read (отвечает на запросы). Синхронизация данных происходит через событие.

// Сервис записи (Command)
app.post('/api/orders', async (req, res) => {
  const order = { id: uuid(), ...req.body, status: 'pending' };
  await writeDb.insert(order);                

Блоги

Рекомендаций по книгам