Из цикла "Микросервисы или смерть"
Решаемая проблема: монолитное приложение на Node.js раньше, в развернутом состоянии, занимало 0.2 Гб всего. Теперь же, разбитое на 33 микросервиса, занимает 33 * 0.1 = 3.3 Гб. Можно ли избежать подобной издержки? -- можно! В статье мы избавимся от лишнего веса.
Решение, представленное в статье подходит больше для контура разработки, но и для продуктового контура его использовать никто не запрещал.
Допустим есть микросервис MsA с зависимостями: express, axios, nats ; и микросервис MsB с зависимостями: express, axios и mongoose. Рассмотрим докер файл для MsA (упрощено для краткости):
FROM node:alpine
COPY . .
RUN npm install
CMD ["npm", "start"]
Получившиеся слои образа:
node:alpine у нас используется для всех микросервисов, и, соответственно, этот слой (слой №1) будет общий. Но два следующих слоя будут отличатся от микросервиса к микросервису. Именно они будут сказываться на общем размере. У наших микросервисов есть общие зависимости - express, axios При npm install они будут скопированы в каждый образ. Отсюда общее увеличение. Общие зависимости дублируются во всех микросервисах, увеличивая общий размер.
Есть много способов прийти к такому результату, не буду ходить вокруг да около, рассматривая плюсы и минусы, а сразу приведу решение. В микросервисах MsA и MsB есть общие зависимости (express, axios) но есть и отличающиеся (mongoose и nats). В нашем решении мы просто возьмём и объединим их всех в один package.json. С некоторыми особенностями. Нам нужны только зависимости, работающие на этапе эксплуатации. Зависимости на этапе сборки нам не нужны. Т.е. jest, webpack, typescript и т.п. зависимости в devDependences каждого микросервиса могут отличаться. Но вот раздел dependences будет одинаков для всех микросервисов. Вне зависимости, используют ли они nats или mongoose, эта зависимость там будет. Вторая особенность это общая практика использования зависимостей со строго фиксированными версиями зависимостей. Чтобы билд при создании образа надёжно проходил, а не падал от минорных апдейтов.
Т.е. у нас будет проект BaseMS (типа базовый образ для всех микросервисов) с package.json примерно таким (упрощено для краткости):
{
"name": "basems",
"dependencies": {
"axios": "0.21.1",
"express": "4.17.1",
"mongoose": "5.12.8",
"node-nats-streaming": "0.3.2"
}
}
и Dockerfile примерно таким:
FROM node:alpine
COPY . .
RUN npm install
Для MsA package.json будет таким (упрощено для краткости):
{
"name": "markups",
"main": "index.js",
"scripts": {
"build": "webpack --config webpack.config.js",
"test": "jest --watchAll --no-cache"
},
"devDependencies": {
"jest": "^26.1.0",
"mongodb-memory-server": "^6.9.6",
"supertest": "^6.1.2",
"ts-jest": "^26.1.3",
"typescript": "^4.1.3",
"webpack": "^5.21.2",
"webpack-cli": "^4.5.0",
"webpack-node-externals": "^2.5.2"
},
"dependencies": {
"axios": "0.21.1",
"express": "4.17.1",
"mongoose": "5.12.8",
"node-nats-streaming": "0.3.2"
}
}
Для MsB package.json будет аналогичен, но набор devDependencies может отличаться.
const nodeExternals = require('webpack-node-externals');
...
module.exports = {
target: 'node',
...
externals: [nodeExternals()],
...
};
# Этап сборки
FROM node:alpine
COPY package.json .
RUN npm install
COPY . .
RUN npm run build
# Рабочий уровень
FROM localhost:5000/basems
COPY --from=0 app/dist/index.js .
CMD ["node", "index.js"]
Образ localhost:5000/basems - это как раз наш базовый образ, собранный выше. Он называется так, т.к. мы его пушим в локальный хаб registry
Для всех микросервисов файл докера будет одинаковый. Давайте рассмотрим получившиеся слои:
Источник статьи: https://habr.com/ru/post/564136/
Решаемая проблема: монолитное приложение на Node.js раньше, в развернутом состоянии, занимало 0.2 Гб всего. Теперь же, разбитое на 33 микросервиса, занимает 33 * 0.1 = 3.3 Гб. Можно ли избежать подобной издержки? -- можно! В статье мы избавимся от лишнего веса.
Решение, представленное в статье подходит больше для контура разработки, но и для продуктового контура его использовать никто не запрещал.
Откуда лишний размер?
Во-первых разберемся, откуда берётся лишний размер при переходе от монолита к микросервисной архитектуре.Допустим есть микросервис MsA с зависимостями: express, axios, nats ; и микросервис MsB с зависимостями: express, axios и mongoose. Рассмотрим докер файл для MsA (упрощено для краткости):
FROM node:alpine
COPY . .
RUN npm install
CMD ["npm", "start"]
Получившиеся слои образа:
- FROM node:alpine --> 100 Mb
- COPY . . --> 0.5 Mb
- RUN npm install --> 50-100+ Mb
node:alpine у нас используется для всех микросервисов, и, соответственно, этот слой (слой №1) будет общий. Но два следующих слоя будут отличатся от микросервиса к микросервису. Именно они будут сказываться на общем размере. У наших микросервисов есть общие зависимости - express, axios При npm install они будут скопированы в каждый образ. Отсюда общее увеличение. Общие зависимости дублируются во всех микросервисах, увеличивая общий размер.
Решение
Приходит на ум решение, что должен быть некий общий образ, который будет базовым для всех микросервисовЕсть много способов прийти к такому результату, не буду ходить вокруг да около, рассматривая плюсы и минусы, а сразу приведу решение. В микросервисах MsA и MsB есть общие зависимости (express, axios) но есть и отличающиеся (mongoose и nats). В нашем решении мы просто возьмём и объединим их всех в один package.json. С некоторыми особенностями. Нам нужны только зависимости, работающие на этапе эксплуатации. Зависимости на этапе сборки нам не нужны. Т.е. jest, webpack, typescript и т.п. зависимости в devDependences каждого микросервиса могут отличаться. Но вот раздел dependences будет одинаков для всех микросервисов. Вне зависимости, используют ли они nats или mongoose, эта зависимость там будет. Вторая особенность это общая практика использования зависимостей со строго фиксированными версиями зависимостей. Чтобы билд при создании образа надёжно проходил, а не падал от минорных апдейтов.
Т.е. у нас будет проект BaseMS (типа базовый образ для всех микросервисов) с package.json примерно таким (упрощено для краткости):
{
"name": "basems",
"dependencies": {
"axios": "0.21.1",
"express": "4.17.1",
"mongoose": "5.12.8",
"node-nats-streaming": "0.3.2"
}
}
и Dockerfile примерно таким:
FROM node:alpine
COPY . .
RUN npm install
Для MsA package.json будет таким (упрощено для краткости):
{
"name": "markups",
"main": "index.js",
"scripts": {
"build": "webpack --config webpack.config.js",
"test": "jest --watchAll --no-cache"
},
"devDependencies": {
"jest": "^26.1.0",
"mongodb-memory-server": "^6.9.6",
"supertest": "^6.1.2",
"ts-jest": "^26.1.3",
"typescript": "^4.1.3",
"webpack": "^5.21.2",
"webpack-cli": "^4.5.0",
"webpack-node-externals": "^2.5.2"
},
"dependencies": {
"axios": "0.21.1",
"express": "4.17.1",
"mongoose": "5.12.8",
"node-nats-streaming": "0.3.2"
}
}
Для MsB package.json будет аналогичен, но набор devDependencies может отличаться.
Webpack (опционально)
Допустим мы используем typescript и хотим еще всё пожать webpack. Webpack можно использовать для бэкэнда, надо лишь не включать в итоговый бандл node_modules. В этом нам поможет небольшой пакет для webpack -- webpack-node-externals. Надо просто добавить в webpack.config.js следующий код:const nodeExternals = require('webpack-node-externals');
...
module.exports = {
target: 'node',
...
externals: [nodeExternals()],
...
};
Dockerfile для микросервисов
Для микросервисов теперь будем использовать мультистейдж билд в докере. На первом этапе собираем итоговый файл index.js. На втором создаем итоговый образ, просто копируя итоговый index.js в базовый образ# Этап сборки
FROM node:alpine
COPY package.json .
RUN npm install
COPY . .
RUN npm run build
# Рабочий уровень
FROM localhost:5000/basems
COPY --from=0 app/dist/index.js .
CMD ["node", "index.js"]
Образ localhost:5000/basems - это как раз наш базовый образ, собранный выше. Он называется так, т.к. мы его пушим в локальный хаб registry
Для всех микросервисов файл докера будет одинаковый. Давайте рассмотрим получившиеся слои:
- FROM localhost:5000/basems --> 150 Mb
- COPY --from=0 app/dist/index.js . --> 0.5 Mb
Достоинства и недостатки подхода
Недостатки:- Если необходимо изменить набор рабочих зависимостей для отдельного микросервиса, придется ребилдить/репушить все микросервисы. Но на деле набор зависимостей быстро устаканится в процессе разработки и это не будет проблемой.
- При изменении зависимостей в одном микросервисе (рабочих) необходимо менять package.json файлы всех микросервисов. На деле такие изменения легко автоматизировать, это может делаться по кнопке в доли секунду и, к тому же, даже будет плюсом, т.к. поможет не допустить ошибок при ручном изменении.
- Подобный трюк работает только на Node.js основанных микросервисах. Но ничего страшного, если у вас на ноде лишь часть микросервисов -- это сработает и для них. А остальные останутся как и раньше.
- Итоговый размер слоев минимален. Можно делать хоть тысячу микросервисов с микрофункциональностью.
- Мы обновляем рабочие зависимости в одном месте. Устраняем vulnerabilities в одном месте. А затем по кнопке накатываем на все микросервисы.. Это, кстати, достоинство даже круче первого
Источник статьи: https://habr.com/ru/post/564136/