Со временем, каждый проект растет и реализовывать новый функционал в существующий монолит становится все сложнее, дольше и дороже для бизнеса.
Один из вариантов решения данной проблемы — использование микросервисной архитектуры. Для новичков или для тех, кто впервые сталкиваются с данной архитектурой, может быть сложно понять, с чего начать, что нужно делать, а что делать не стоит.
В этой статье будет написан простейший микросервис на Nodejs & RabbitMQ, а также показан процесс миграции монолита на микросервисы.
Что есть в микросервисной архитектуре?
- Gateway. Главный сервер, который принимает запросы и перенаправляет их нужному микросервису. Чаще всего, в gateway нет никакой бизнес-логики.
- Microservice. Сам микросервис, который обрабатывают запросы пользователей с четко заданной бизнес-логикой.
- Транспорт. Это та часть, через которую будут общаться Gateway & Microservice. В качестве транспорта могут выступать HTTP, gRPC, RabbitMQ и т.д.
Почему именно RabbitMQ?
Разумеется, можно не использовать RabbitMQ, есть другие варианты общения между микросервисами. Самый простой — HTTP, есть gRPC от Google.
Я использую RabbitMQ, поскольку считаю его достаточно простым для старта написания микросервисов, надежным и удобным в том плане, что отправляя сообщение в очередь, можно быть уверенным в том, что сообщение дойдет до микросервиса (даже если он выключен в данный момент, а потом включился). Благодаря этим преимуществам можно писать надежные микросервисы и использовать бесшовный деплой.
Начало
Для начала реализуем простой gateway, который будет принимать запросы по HTTP, слушая определенный порт.
Разворачиваем RabbitMQ (через него наши микросервисы и gateway будут общаться):
$ docker run -d -p 5672:5672 rabbitmq
Инициализируем проект и устанавливаем NPM-пакет micromq:
$ npm init -y $ npm i micromq -S
Пишем gateway
// импортируем класс Gateway из раннее установленного пакета micromq const Gateway = require('micromq/gateway'); // создаем экземпляр класса Gateway const app = new Gateway({ // названия микросервисов, к которым мы будем обращаться microservices: ['users'], // настройки rabbitmq rabbit: { // ссылка для подключения к rabbitmq (default: amqp://guest:guest@localhost:5672) url: process.env.RABBIT_URL, }, }); // создаем два эндпоинта /friends & /status на метод GET app.get(['/friends', '/status'], async (req, res) => { // делегируем запрос в микросервис users await res.delegate('users'); }); // начинаем слушать порт app.listen(process.env.PORT);
Как это будет работать:
- Запускается сервер, он начинает слушать порт и получать запросы
- Пользователь отправляет запрос на https://mysite.com/friends
- Gateway, согласно логике, которую мы описали, делегирует запрос:
3.1 Идет отправка сообщения (параметры запроса, заголовки, информация о коннекте и др.) в очередь RabbitMQ
3.2. Микросервис слушает эту очередь, обрабатывает новый запрос
3.3. Микросервис отправляет ответ в очередь
3.4. Gateway слушает очередь ответов, получает ответ от микросервиса
3.5. Gateway отправляет ответ клиенту - Пользователь получает ответ
Пишем микросервис
// импортируем класс MicroService из раннее установленного пакета micromq const MicroMQ = require('micromq'); // создаем экземпляр класса MicroService const app = new MicroMQ({ // название микросервиса (оно должно быть таким же, как указано в Gateway) name: 'users', // настройки rabbitmq rabbit: { // ссылка для подключения к rabbitmq (default: amqp://guest:guest@localhost:5672) url: process.env.RABBIT_URL, }, }); // создаем эндпоинт /friends для метода GET app.get('/friends', (req, res) => { // отправляем json ответ res.json([ { id: 1, name: 'Mikhail Semin', }, { id: 2, name: 'Ivan Ivanov', }, ]); }); // создаем эндпоинт /status для метода GET app.get('/status', (req, res) => { // отправляем json ответ res.json({ text: 'Thinking...', }); }); // начинаем слушать очередь запросов app.start();
Как это будет работать:
- Микросервис запускается, начинает слушать очередь запросов, в которую будет писать Gateway
- Микросервис получает запрос, обрабатывает его, прогоняя через все имеющиеся middlewares
- Микросервис отправляет ответ в Gateway
3.1. Идет отправка сообщения (заголовки, HTTP-код тело ответа) в очередь RabbitMQ
3.2. Gateway слушает эту очередь, получает сообщение, находит клиента, которому нужно отправить ответ
3.3 Gateway отправляет ответ клиенту
Миграция монолита на микросервисную архитектуру
Предположим, что у нас уже есть приложение на express, и мы хотим начать его переносить на микросервисы.
Оно выглядит следующим образом:
const express = require('express'); const app = express(); app.get('/balance', (req, res) => { res.json({ amount: 500, }); }); app.get('/friends', (req, res) => { res.json([ { id: 1, name: 'Mikhail Semin', }, { id: 2, name: 'Ivan Ivanov', }, ]); }); app.get('/status', (req, res) => { res.json({ text: 'Thinking...', }); }); app.listen(process.env.PORT);
Мы хотим вынести из него 2 эндпоинта: /friends и /status. Что нам для этого нужно сделать?
- Вынести бизнес-логику в микросервис
- Реализовать делегирование запросов на эти два эндпоинта в микросервис
- Получать ответ из микросервиса
- Отправлять ответ клиенту
В примере выше, когда мы создавали микросервис users, мы реализовали два мето��а /friends и /status, который делают то же самое, что делает наш монолит.
Для того, чтобы проксировать запросы в микросервис из gateway, мы воспользуемся тем же пакетом, подключив middleware в наше express приложение:
const express = require('express'); // импортируем класс Gateway из раннее установленного пакета micromq const Gateway = require('micromq/gateway'); const app = express(); // создаем экземпляр класса Gateway const gateway = new Gateway({ // название микросервиса (оно должно быть таким же, как указано в Gateway) microservices: ['users'], // настройки rabbitmq rabbit: { // ссылка для подключения к rabbitmq (default: amqp://guest:guest@localhost:5672) url: process.env.RABBIT_URL, }, }); // подключаем middleware в монолит, который позволит нам делегировать запросы app.use(gateway.middleware()); // не трогаем этот эндпоинт, потому что мы не планируем переносить его в микросервис app.get('/balance', (req, res) => { res.json({ amount: 500, }); }); // создаем два эндпоинта /friends & /status на метод GET app.get(['/friends', '/status'], async (req, res) => { // делегируем запрос в микросервис users // метод res.delegate появился благодаря middleware, которую мы подключили выше await res.delegate('users'); }); // слушаем порт app.listen(process.env.PORT);
Это работает так же, как в примере выше, где мы писали чистый Gateway. В этом примере разница лишь в том, что запросы принимает не Gateway, а монолит, написанный на express.
Что дальше
- RPC (удаленный вызов действия) из микросервиса в монолит/gateway (например, для авторизации)
- Общаться между микросервисами через очереди RabbitMQ для получения дополнительной информации, ибо у каждого микросервиса своя база данных
Это я уже рассказал в статье «Учимся об��аться между микросервисами на Node.js через RabbitMQ».
