Введение: Почему микросервисы на JavaScript?
Современная веб-разработка всё чаще отходит от монолитных приложений в пользу микросервисной архитектуры. JavaScript, а точнее его серверная среда Node.js, идеально подходит для построения микросервисов благодаря асинхронности, событийно-ориентированной модели и огромному экосистемному сообществу. В этой статье мы разберём, как спроектировать, разработать и развернуть микросервисы на JavaScript — от теории до практических примеров кода.
1. Основы архитектуры микросервисов
Микросервисная архитектура — это подход, при котором приложение разбивается на небольшие, независимые сервисы. Каждый сервис отвечает за свою бизнес-логику, имеет собственное хранилище данных и может быть развёрнут, масштабирован и обновлён отдельно.
Преимущества микросервисов на Node.js
- Масштабируемость: Node.js легко обрабатывает тысячи одновременных соединений благодаря неблокирующему вводу-выводу.
- Быстрое прототипирование: npm-пакеты и фреймворки (Express, Fastify, NestJS) позволяют быстро создавать REST/GraphQL API.
- Изоляция ошибок: Сбой в одном сервисе не парализует всё приложение.
- Технологическая гибкость: Разные сервисы могут использовать разные версии Node.js или даже разные языки.
Ключевые компоненты
- API Gateway — единая точка входа, маршрутизирующая запросы к нужным сервисам.
- Брокер сообщений (RabbitMQ, Redis, Kafka) — для асинхронного обмена данными.
- Сервисный реестр (Consul, etcd) — для обнаружения сервисов.
- Контейнеризация (Docker, Kubernetes) — для стандартизации окружения.
2. Практика: Создаём два микросервиса
Разберём пример системы заказов. У нас будет два микросервиса: user-service (управление пользователями) и order-service (управление заказами). Они будут общаться через REST API и асинхронно через RabbitMQ.
2.1. Базовый сервис на Express
Начнём с user-service. Установите зависимости:
npm init -y
npm install express body-parser
Создайте файл index.js:
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const PORT = process.env.PORT || 3001;
app.use(bodyParser.json());
const users = [];
app.post('/users', (req, res) => {
const { name, email } = req.body;
const user = { id: users.length + 1, name, email };
users.push(user);
res.status(201).json(user);
});
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.listen(PORT, () => {
console.log(`User service running on port ${PORT}`);
});
Аналогично создайте order-service на порту 3002. Он будет содержать массив заказов и два эндпоинта: POST /orders и GET /orders/:id.
2.2. API Gateway
Создадим простой шлюз на порту 3000, который перенаправляет запросы в зависимости от пути:
const express = require('express');
const httpProxy = require('http-proxy-middleware');
const app = express();
app.use('/users', httpProxy.createProxyMiddleware({ target: 'http://localhost:3001', changeOrigin: true }));
app.use('/orders', httpProxy.createProxyMiddleware({ target: 'http://localhost:3002', changeOrigin: true }));
app.listen(3000, () => console.log('API Gateway on port 3000'));
Теперь все запросы к /users идут на user-service, а к /orders — на order-service.
2.3. Асинхронное взаимодействие с RabbitMQ
Допустим, при создании заказа нужно проверить, существует ли пользователь. Вместо синхронного вызова (который замедлит ответ) используем RabbitMQ. Установите amqplib:
npm install amqplib
В order-service при создании заказа отправляем сообщение в очередь:
const amqp = require('amqplib');
async function sendValidation(userId) {
const conn = await amqp.connect('amqp://localhost');
const ch = await conn.createChannel();
const queue = 'user_validation';
await ch.assertQueue(queue);
ch.sendToQueue(queue, Buffer.from(JSON.stringify({ userId })));
console.log('Sent validation request');
}
// Вызываем в POST /orders
В user-service запускаем потребителя:
async function consume() {
const conn = await amqp.connect('amqp://localhost');
const ch = await conn.createChannel();
const queue = 'user_validation';
await ch.assertQueue(queue);
ch.consume(queue, msg => {
const { userId } = JSON.parse(msg.content.toString());
const user = users.find(u => u.id === userId);
if (user) console.log(`User ${userId} exists`);
else console.log(`User ${userId} NOT found`);
ch.ack(msg);
});
}
consume();
Таким образом, сервисы общаются асинхронно, не блокируя друг друга.
3. Контейнеризация и развёртывание
Для надёжного запуска микросервисов используем Docker. Создайте Dockerfile для каждого сервиса:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3001
CMD ["node", "index.js"]
Соберите образы и запустите через docker-compose.yml:
version: '3.8'
services:
user-service:
build: ./user-service
ports:
- "3001:3001"
order-service:
build: ./order-service
ports:
- "3002:3002"
gateway:
build: ./gateway
ports:
- "3000:3000"
depends_on:
- user-service
- order-service
rabbitmq