Pull to refresh

Comments 104

Не совсем понял вот этот момент


Не предоставляет функций, которые могут вызывать другие модули — т.е. не выполняет никакого своего кода в чужом потоке выполнения (нити, горутине, etc.), таким образом изолируя от вызывающего кода даже исключения, которые могут возникать в коде модуля.

Не могли бы вы привести пример нарушения этого правила?

Добавлю. Чем же является в таком случае API модуля если не функциями которые могут вызывать другие модули?

Зависит от средств, предоставляемых языком. Например, на Go API модуля является набором типизированных каналов, куда/откуда можно передавать данные. Ведут эти каналы в другую, параллельно работающую горутину(ы), принадлежащую модулю. Таким образом получается обмен данными между двумя независимыми потоками выполнения без явного вызова функций чужого модуля в своём потоке выполнения.

А как будет выглядеть запрос информации у такого модуля? Что-то мне кажется, что вызов функции намного удобнее.

Зависит от языка. Например, на Go, на низком уровне это может выглядеть вот так:


// создаём канал, по которому придёт ответ на конкретно этот запрос
replyc := make(chan SomeRPCReply)
// отправляем наш запрос (включающий параметры конкретного RPC и
// только что созданный канал для ответа) в канал ведущий в другой модуль
someModule.SomeRPC <- SomeRPCParams{ 42, replyc }
// ожидаем и принимаем ответ - содержащий либо ошибку либо результат
reply := <- replyc

Как Вы понимаете, нет никакой проблемы завернуть всё это в функцию, чтобы сделать удобно:


func someModule_SomeRPC(id int) (SomeRPCResult, error) {
    replyc := make(chan SomeRPCReply)
    someModule.SomeRPC <- SomeRPCParams{ id, replyc }
    reply := <- replyc
    return reply.Result, reply.Error
}

Более того, создать такого рода функцию всё-равно придётся в тот момент, когда возникнет потребность вырезать вызываемый модуль из общего приложения и запустить его как отдельный микросервис — только тогда эта функция будет заниматься отправкой запроса по сети.

Ну а если все равно надо заворачивать такое взаимодействие в функцию — то не лучше ли разместить ее в самом модуле, чтобы не дублировать в каждом модуле-клиенте?

Может и лучше. Надо экспериментировать на практике.

Это получается, что модули предполагается писать именно на Go?

Нет. Но чтобы получить описанный в статье уровень изоляции модулей друг от друга — понадобится язык в котором есть возможность запускать код в независимых потоках выполнения и передавать между этими потоками сообщения. Из коробки это умеет не только Go, а на языках, которые это не умеют — можно либо реализовать нечто подобное самостоятельно, либо просто ослабить изоляцию между модулями и вызывать их как обычные функции (на скриптовых языках других вариантов обычно нет).


Проблема с ослаблением уровня изоляции в том, что тогда изоляцию нужно поддерживать не с помощью языка и/или инструментов, а исключительно внутренней дисциплиной разработчиков. К сожалению, факты говорят о том, что если писать монолитное приложение полагаясь на дисциплину — в большинстве случаев всё-равно со временем получается big ball of mud.

То есть концептуально можно было бы и через loopback интерфейс общаться?

Нельзя. Через loopback — это всё-равно сеть. И все ошибки, которые бывают при обычном сетевом взаимодействии, бывают и на loopback (может не все, а почти все, но это не помогает).

Не, ну практически — понятно, что нельзя. Но судя по примеру из Go — как-то также можно было бы сокет слушать.

А, в этом смысле. Да, можно вместо каналов использовать, например, pipe(2) или socketpair(2). Это не так здорово, потому что придётся тратить ресурсы на (де)маршалинг, плюс в зависимости от выбранного формата передачи данных можно и строгую типизацию потерять.

На Go я как-то тоже пытался применить идеи, подобные вашим. В результате пришел к MMOA (Monolithic Message-Oriented Application). Но как уже отмечено, производительность приложения снижается, хотя очень весело было общение частей монолита организовать через сообщения, и при таком подходе конечно, выделить «наружу» часть приложения при необходимости очень легко. Впрочем, и в обычном приложении уровень изоляции можно сделать на высоте, как Вы уже верно подметили.
Терминологическая каша. Модули — это не «что-то типа микросервисов», это перпендикулярное микросервисности понятие (микросервис вполне себе может состоять — и в подавляющем количестве случаев состоит — из модулей). То, что вы описали, обычно называют плагинной системой. Т.е., вы вместо использования некой общей сервисной шины со стандартизованным протоколом (например, HTTP) предлагаете реализовать собственный in-process-диспетчер, управляющий циклом жизни плагинов и их взаимодействия. Как вы понимаете, это вовсе не «микросервисы VS модули», это «какие у людей, оказывается, разные задачи бывают, что иногда им нужно осилить микросервисную архитектуру, иногда добавить систему плагинов, а иногда вообще нейросеть встроить».

То, что я описал, пока не называют никак, к сожалению. Все используемые на данный момент термины я упомянул в статье. Термин микросервис тоже появился тоже далеко не сразу, сначала люди это просто делали, а потом (аж в 2011) это назвали микросервисами.


Мне тоже не очень нравится использовать и так перегруженный кучей существующих смыслов термин "модуль", может со временем появится термин по-лучше.


Плагины это всё-таки нечто иное. В первую очередь плагины — они к чему-то прилагаются. В описанном подходе никакого "основного приложения", по сути, просто нет — есть функция main() на 30 строк занимающаяся разбором параметров и вызовом функций инициализации каждого модуля… и всё. Можно провести аналогию с sh-скриптом, который запускает кучку разных программ в фоновых процессах — эти программы не являются "плагинами" к sh-скрипту.


Никакого диспетчера внутри процесса у меня в описании нет, равно как нет и управления циклом жизни модулей или их взаимодействием — точно так же, как такого диспетчера нет между настоящими микросервисами. После запуска модули просто живут своей жизнью, обрабатывают входящие запросы (не важно откуда они пришли — по сети снаружи от другого микросервиса, или по каналу внутри от другого модуля), могут сами отправлять запросы (и в другие микросервисы по сети, и в другие модули внутри приложения), выполнять какие-то запланированные задачи по собственной инициативе.


Возможно вместо перегрузки термина "модуль" было бы лучше использовать термин "встроенный микросервис" (увидел у lega по ссылке из комментария ниже).

Модуль — это единица сборки, в самом абстрактном инженерном смысле, даже вне ИТ. Компонент — это единица архитектуры проекта, тоже в самом абстрактном смысле. А вот плагины — это уже из «сортировки» компонентов по типам в терминах решаемой прикладной задачи, и в этом контексте куча разных программ, запускаемых в фоне скриптом, вполне могут называться плагинами к этому скрипту (если они имеют некий обобщённый интерфейс управления ими, специфичный не только для платформы, но и для прикладной задачи).

Просто поддержал беседу как мог. Пусть будет «встроенный микросервис», всё равно никаких критериев для выбора названия описанному в статье «подходу» нет, ибо он не является подходом вообще, это не решение какой-то практической проблемы в софтостроении, это просто каша в голове конкретного софтостроителя, увлёкшегося бесполезными аналогиями.

Функция main на 30 строк — это все-таки, как мне кажется, слишком мало. Я бы все-таки в ядро утянул, во-первых, те вещи которые обычно дублируются в каждом микросервисе (и хорошо если без копи-паста): логи, конфиги, создание потоков и управление ими. Сюда же попадает слой взаимодействие с другими (макро-)сервисами.


А во-вторых, саму по себе инициализацию стоило бы усложнить. У меня программы, которые написаны в архитектуре "ядро + плагины с бизнес-логикой" инициализаруются в три этапа:


  1. поиск и регистрация внутренних сервисов и их зависимостей (настройка IoC-контейнера);
  2. настройка плагинами друг друга и ядра;
  3. запуск плагинов.

"Модуль — это что-то вроде микросервиса" — как мне развидеть это? Завтра будет "функция — это что-то вроде http-запроса"?

Похоже это те же микросервисы, с условием — всегда запускать весь набор на том же сервере приложений. Тогда и деплоить их отдельно нельзя будет и перезапускать нельзя будет — вот и модуль =)

Да я бы и сам это с удовольствием развидел. Но критика должна быть конструктивной, так что — предложите другое название.

Да нет, это вполне себе модули (ну, некий частный случай с определёнными свойствами, которые вам — и не только вам — нужны). Я глумился именно над попыткой объяснять простое понятие (модуль — простая, базовая вещь) через сложное.

Уже давно пишем почти весь код в "модульном" стиле. Модуль = папка в проекте. В папке весь код модуля. Если модуль по каким-то причинам больше не нужен, удаляем папку и забываем. Довольны.

А для модулей используете общие интерфейсы доступа?
У нас этот подход применяется к веб-разработке. Модуль — семантическая единица. Area — модуль, папка — модуль. В папке контроллер, DTO, все необходимое для работы модуля. В этом смысле мы полагаемся на соглашения MVC-фреймворка про «интерфейс доступа». Точками входа являются контроллеры, а MVC-фреймворк знает как их найти.
Скорее всего, папкой у вас является компонент, а внутри папки модули этого компонента — логика, конфигурация, шаблоны, например. Модули — это нарезка «по смыслу платформы» (предоставляется используемым языком программирования, например, или инструментом сборки), компоненты — нарезка по смыслу прикладной задачи и архитектуры (пилится самим программистом в рамках конкретного проекта). Конечно, эти термины очень нестрого используются разными программистами, и часто на разных уровнях возникают синонимичные названия элементов декомпозиции, но общая концепция примерно такова.
Ответил выше. От термина «компонент» решили отказаться на бекенде, чтобы не вводить путаницу с «компонентами» на фронте. В итоге решили, что есть модули и они компонуются. Это вопрос терминалогии уже, кому как удобнее.

Специально проверял значение термина «модуль» в Википедии. Этому определнию наш подход не противоречит. А стоит ли выделять отдельную сущность «компонент» и из них строить модули — вопрос организационный.
Да, прежде всего терминология должна быть удобной в команде проекта, с этим не поспоришь.
Как-то сумбурно написано. При этом негусто с описанием получаемых недостатков при использовании модулей.

К примеру, при росте количества модулей не стоит забывать про увеличение времени необходимого на инициализацию самого приложения даже при отложенной (ленивой) инициализации модулей по факту запроса.

Как я упомянул в статье, я лично в таком стиле пока ещё не писал, но тут в комментариях отписывались люди, которые нечто подобное уже используют — вопрос про недостатки это скорее к ним. Я в статье постарался описать недостатки, которые очевидны сходу, и уменьшение скорости запуска приложения там есть.

А можно поконкретнее где указали? Я просто явно это не увидел и повторно при беглом прочтении не нашёл.
• Уменьшается скорость запуска (разогрев кешей, etc.).
Тогда стоит писать «Увеличение времени». Вы всё же не мотор меняете на более дохлый, а добавляете объём выполняемых действий и на скорость это ни как не сказывается.
Падение модуля приводит к падению всего приложения.
Ничего не приводит, зависит от реализации.

Несколько лет использую подобный подход (позволяет снизить кривую сложности разработки), но с более мягкими ограничениями — грубо говоря нужно просто придерживаться того, что-бы было легко выдернуть модуль в микросервис без рефакторинга остальной части проекта, тут набросал некоторые правила/идеи.

Ну, учитывая отсутствие встроенного в приложение супервизора для встроенного микросервиса, который бы мог перезапустить его после падения — падение встроенного микросервиса должно приводить к падению всего приложения (чтобы его мог перезапустить внешний супервизор). Особого смысла встраивать в приложение ещё и супервизор я пока не вижу.


Конечно, в принципе встроенные микросервисы можно писать так, чтобы они сами обрабатывали свои исключения и восстанавливались после ошибок — но это их может заметно усложнить. Если приложению нельзя изредка падать и нужна высокая доступность — лучше использовать обычные микросервисы, а не пытаться написать невероятно устойчивый монолит.


Ваши правила/идеи почитал, спасибо (моя статья это скорее аналогичный рабочий документ, нежели настоящая статья). Мне всё-таки кажется, что для того, чтобы это работало не только для отдельных, достаточно дисциплинированных, разработчиков — нужен более сильный уровень изоляции встроенных микросервисов, который вряд ли смогут обеспечить скриптовые языки.


Тем не менее, раз у Вас есть реальный практический опыт — поделитесь проблемами, которые возникали в процессе использования этого подхода?

Как я уже упомянул — я использую более мягкие ограничения, «сервис» может напрямую вызать апи другого сервиса (просто вызов функции), главное что-бы можно было выкусить любой во внешний микросервис без рефакторинга, (что на практике я не припомню), т.к. подобные сервисы я сразу делаю как модуль-интерфейс + его внешний микросервис.

Наверно единственное — не перезагрузить сервис отдельно (хотя в питоне есть методы для перезагрузки модулей), но и это не проблема — пока перезагружается монолит все запросы балансируются на другие сервера.

Маштабирование — просто запуск монолита с некоторыми выключенными модулями, либо ленивой загрузкой ну либо на крайний случай — можно выдернуть в микросервис.

В случае неполадок сервис выбрасывает исключения которые ловятся где нужно.

Вместо «стандарных методов» инициализация/настройка модуля, я использую pubsub — сервис подписывается на нужные события.

Вообщем особо проблем нет, как обычный монолит с особой архитектурой, но содержит некоторые плюсы микросервисов. Как показала практика, «выкусывание» в микросервис нужно не часто, за счет этого происходит большая экономия по сравнению с реальными микросервисами, наверное поэтому один умный дядька (Фаулер?) написал статью «Сначала монолит/Монолит вперед».

До этого, 6-7 лет назад использовал «плагинную» архитектуру, где любой подключенный плагин публиковал свои методы и расширял опубликованные методы других плагинов, в результате все упростилось до «встроенных микросервисов».
Мне кажется вы зачем-то взяли минусы из обоих концепции выкинув их плюсы.
Взяли монолит, «написали» его в «стиле» микросервисов в итоге получили недомикросервисы которые имеют все недостатки микросервисов, но не имеют их достоинств, зачем не понятно.
Судя по всему автор пытался донести мысль, что грамотное разбитие проекта на умеренно изолированные модули в рамках монолита может оказаться выгоднее чем фигачить микросервисы.
Мысль, казалось бы, более чем очевидная. Но практика говорит что нет )

Практика говорит, что попытки такое сделать всё-равно приводят к обычному монолиту в виде большого комка грязи. Но на мой взгляд эту проблему возможно решить обеспечив действительно жёсткую изоляцию между модулями (встроенными микросервисами). Просто далеко не каждый язык предоставляет необходимые для такого уровня изоляции инструменты, плюс очень важно определить баланс — что взять из микросервисов а что из монолита. Найдём баланс — тогда есть шанс, что практика подтвердит эту очевидную мысль.

Если нам нужна действительно жестка изоляция, зачем нам монолит?

Затем, что зачастую нужна именно изоляция (с целью уменьшить сложность разработки и увеличить скорость разработки), а не распределённая система. Распределённая система решает проблему изоляции, но при этом несёт с собой слишком много дополнительных сложностей, в т.ч. специфичных для бизнес-логики и растущих вместе с проектом — вот от этой сложности и можно избавиться если запихнуть микросервисы обратно в монолит. Фокус в том, чтобы сделать это так, чтобы не получить в результате обычный монолит с его обычными проблемами.

ОК. Какие проблемы микросервисов вы решаете данным архитектурным подходом? Чем он лучше микросервисов?

Не знаю что решает таким подходом автор, но я таким образом решаю проблему огромного инфраструктурного и организационного оверхеда.

А как возникла эта проблема? Запилили сервисы ради сервисов?
Проблемы возникают при реализации крупных проектов, в калькуляторах и бложиках вы их не увидите.
С чем проблемы? С тем, что микросервисы используются ради микросервисов, а не для решения архитектурных задач проекта? Думаю, такое даже в калькуляторе может проявиться вполне.
У монолитов с действительно крупными проектами тоже не все радужно, одни мэйнфрэймы чего стоят.
В чем именно заключается инфраструктурный и организационный оверхед?

Согласен, что некоторые дополнительные действия предпринимать придется, но они либо незначительные, либо изначально желательные для любого подхода.

Не следует забывать, что множество вещей окружающих микросервисы являются отнюдь не обязательными, а часто просто дань моде или не умение использовать «не модные» инструменты.

Каждому микросервису нужны свой план сборки, свой план развертывания, своя виртуалка (ну хорошо, контейнер). Ему нужно писать свой конфиг. Данные он будет хранить в своей базе...


Если бы я все написанные мною модули делал микросервисами — я бы до сих пор писал к ним конфиги и планы развертывания.

Каждому микросервису нужны свой план сборки


Если говорить о java или c# это не слишком большая проблема. При этом план сборки вероятно будет проще чем у монолита.
План развертывания. Вопрос холиварный, в монолите вы деплоите все изменения разом, деплой дольше( а значит нужно решать проблему даунтайма ), любая проблема исправляется дольше( хотя бы просто из-за времени сборки ). Плюс проблемы с зависимостями, которые в микросервисах можно скрыть за контейнером.

своя виртуалка (ну хорошо, контейнер)


Это опционально, можете хоть в rpm пакеты собирать. Да хоть прям исполняемыми файлами по сети раскидывать.
Контейнеры дают заметный бонус если нужна изоляция окружения или ресурсов в остальных случаях они скорее «модная штучка», чем реальная необходимость. Плюс контейнеров большое кол-во статей именно по деплою и управлению парком, сделать тоже самое без них можно, но придется поискать информацию.

Ему нужно писать свой конфиг


Это скорее плюс, конфиги сервисов изолированы, вы можете их настроить по разному при этом размер конфига скорее всего будет меньше. Если уж очень нужно, можно сделать общий конфиг для сервисов, хуже чем в монолите по предложенной архитектуре не будет.

Данные он будет хранить в своей базе...


Автор предлагает изолировать данные.

Разворачивание заново всех микросервисов будет заметно дольше чем одного монолита. А если разворачивать их частично, только изменившиеся — то и в "монолите" с модулями можно сделать точно так же.


Плюс проблемы с зависимостями, которые в микросервисах можно скрыть за контейнером.

… а в монолите их вообще нет. Или я не понимаю о каких проблемах вы говорите.


Это скорее плюс, конфиги сервисов изолированы, вы можете их настроить по разному при этом размер конфига скорее всего будет меньше.

Суммарный размер конфига меньше никак не будет, а мне же их все писать в итоге.

Разворачивание заново всех микросервисов будет заметно дольше чем одного монолита. А если разворачивать их частично, только изменившиеся — то и в «монолите» с модулями можно сделать точно так же.


Если взять суммарное время необходимое от кода до продакшена думаю на сервисах оно будет меньше, не стоит забывать, что сборка монолита у разработчика тоже занимает заметное время.
В сервисах не принято деплоит разом все сервисы, в этом важное преимущество сервисов изменения деплоятся по мере разработки для каждого сервиса отдельно.

… а в монолите их вообще нет. Или я не понимаю о каких проблемах вы говорите.


Я говорю про dependency hell. И вообще про зависимости проекта от среды. Как вариант, вам в одной части проекта нужно изменить версию внешнего пакета( какого-нибудь openssl ) потому как только в этой части проекта новая версия имеет существенные преимущества, или пересесть с java7 на java9, в рамках монолита это проблема.

Суммарный размер конфига меньше никак не будет, а мне же их все писать в итоге.


Не очень понятно, у вас команда из одного разработчика, почему «все их писать мне»? На мой взгляд как раз проще 10 маленьких конфигов для 10 сервисов которые пишут 10 разработчиков, чем одни большой конфиг для монолита который пишут 10 разработчиков.

Плюс не совсем понятно как у вас все модули деплоятся раздельно, если у них общий конфиг.
Вообще странный монолит получается:
Данные изолированны.
Код изолирован обмен только по api.
Деплоится по модульно без рестарта.
Зависимости изолированы.
Осталось сказать собирается помодульно, тестится помодульно, добавить раздельный конфиг и вот уже и микросервисы, какой же это монолит?

В сервисах не принято деплоит разом все сервисы, в этом важное преимущество сервисов изменения деплоятся по мере разработки для каждого сервиса отдельно.

Чтобы так делать — надо сначала рассказать заказчику что у нас микросервисы. А это означает заведение на каждый микросервис ТЗ, проектной документации, инструкции для сисадмина и инструкции для пользователя (последнюю заменяет описание API).


Не очень понятно, у вас команда из одного разработчика, почему «все их писать мне»? На мой взгляд как раз проще 10 маленьких конфигов для 10 сервисов которые пишут 10 разработчиков, чем одни большой конфиг для монолита который пишут 10 разработчиков.

Команда командой, но модулей больше чем разработчиков. Задача-то в том чтобы реализовать то что хочет заказчик, а не всех знакомых трудоустроить.


Плюс не совсем понятно как у вас все модули деплоятся раздельно, если у них общий конфиг.

Конфиг отражает выбранную схему развертывания. Обычно он пишется один раз и в дальнейшем меняется довольно редко (раз в год, к примеру). На некоторых унаследованных проектах конфиги долгое время даже не были включены ни в какую в систему контроля версий — они просто лежали на сервере и после каждого развертывания использовался старый конфиг.


Осталось сказать собирается помодульно, тестится помодульно, добавить раздельный конфиг и вот уже и микросервисы, какой же это монолит?

Теперь, когда вы это осознали, предлагаю вам еще раз прочитать обсуждаемый хабрапост. :-)


Именно что это никакой не монолит. А встроенные микросервисы, которые общаются друг с другом без сетевого взаимодействия.

Именно что это никакой не монолит. А встроенные микросервисы, которые общаются друг с другом без сетевого взаимодействия.


Так с этого и началась ветка, недомикросервисы, со всеми их недостатками, но без многих достоинств. Честно, ни вижу ни одного плюса подобного похода( по сравнению с микросервисами ), который не был бы не лучшей практикой в микросервисах.
Например единый конфиг, никаких проблем сделать единый конфиг для микросервисов нет, только мне кажется очевидным, что это плохая идея, поскольку теряется независимость сервисов( модулей, проектов, не принципиально как назвать ), появляются издержки на коммуникацию в команде, на мерджи изменений, осложняется выделение сервиса, проекта, в отдельную команду/отсуорс/отдельному сотруднику.

Плюс куча ограничение на язык и среду разработки, чтобы поддерживать искусственно созданную изоляцию внутри монолита, сложности вводом новых людей в проект, проблемы которой вообще нет в микросервисах.
Честно, ни вижу ни одного плюса подобного похода

ЕПРСТ, а я что тут перечислял?


Плюс куча ограничение на язык и среду разработки, чтобы поддерживать искусственно созданную изоляцию внутри монолита

Это не является недостатком если выбранная среда разработки уже поддерживает все что требуется.


сложности вводом новых людей в проект, проблемы которой вообще нет в микросервисах

Наоборот, никакой сложности. Открыл проект и можно начинать фиксить баги или дорабатывать какие-то мелочи. Если нужно заводить новый модуль — не нужно искать как создавать новый проект, все уже есть рядом.

Ну что тут сказать… я смотрю в эту сторону (как совместить монолит и микросервисы) уже минимум года три. До этого момента какого-то понимания как это сделать чтобы получить достаточно стоящие преимущества ценой приемлемых недостатков у меня не было, поэтому идея оставалась в голове, а я продолжал писать обычные микросервисы. Статья, собственно, и является перечислением как достоинств, так и недостатков — исключительно ради их обсуждения и писалась. Так что странно, что Вы достоинств до сих пор не увидели:


  • Возможность упростить кучу кода, занимающегося отправкой запросов в другие сервисы: выкинуть таймауты, обработку сетевых ошибок, разделение на идемпотентные и не идемпотентные запросы с разной логикой их повтора при ошибках и защиту от дубликатов, саму логику повтора с задержками и защитой от бесконечного повтора, асинхронное выполнение запросов.
  • Возможность упростить архитектуру, реализацию и улучшить юзабилити — во многих (а то и всех, если повезёт) местах, где в обычных микросервисах возникает eventual consistency.

Это — для меня основное. Есть ещё всякие мелочи, вроде упрощения: рефакторинга проекта в целом, использования монорепо, изменения внутренних API между встроенными микросервисами, интеграционного тестирования.


И есть серая зона: паттерн Circuit Breaker часто будет не нужен, за отсутствием циклических зависимостей между встроенными микросервисами смогут следить язык/инструменты — но возможно из-за смешивания обычных и встроенных микросервисов в одном проекте большой пользы от этого не будет, или распределённые транзакции — нужда в них полностью не исчезает, но интуиция подсказывает, что делать это будет намного проще. Тут только практика покажет.


Основной недостаток — неприменимость этого подхода в большинстве проектов, где действительно необходима высокая доступность и/или масштабирование (в меньшинстве возможно удастся выкрутиться запустив несколько одинаковых монолитов).


Что касается мелких подробностей вроде общего конфига — я вообще не понимаю, в чём проблема. Чаще всего конфиг находится либо в переменных окружения, либо передаётся аргументами при запуске приложения (а если он был в файле или распределённой БД — там его и оставьте, ничего страшного что у монолита будет кучка конфиг-файлов вместо одного). Встраивание нескольких микросервисов в монолит означает только то, что выбирая имена для переменных окружения и/или аргументов командной строки нужно использовать префиксы-namespace чтобы исключить конфликты между разными встроенными микросервисами. Это — проблема, достойная обсуждения и отказа от возможности выкинуть всю вышеупомянутую сложность? Серьёзно? Странно, а больше похоже на троллинг. Мержи изменений? Каких? Кода в main() разбирающего аргументы и вызывающего Init() каждого микросервиса? Это — проблема? А как Вы вообще код пишете больше чем одним разработчиком на проекте? Где Вы увидели издержки на коммуникацию — я не понял, вся коммуникация находится на уровне обсуждения публичного API микросервисов, как всегда. Выделить проект отдельному сотруднику ровно настолько же просто — встроенный микросервис вполне возможно писать и тестировать в отдельном репо, как обычную библиотеку. Деплоить его отдельно нельзя — это правда, но разработке это не сильно мешает.


Вот что реально, так это требования к языку/инструментам для обеспечения изоляции. Но, во-первых, это не проблема для тех, кто сейчас и так уже использует подходящие языки (как уже упомянул mayorovp), а во-вторых меня тут в соседней ветке вообще убеждали, что для изоляции хватает отдельных папочек и немного дисциплины.

Возможность упростить кучу кода, занимающегося отправкой запросов в другие сервисы: выкинуть таймауты, обработку сетевых ошибок, разделение на идемпотентные и не идемпотентные запросы с разной логикой их повтора при ошибках и защиту от дубликатов, саму логику повтора с задержками и защитой от бесконечного повтора, асинхронное выполнение запросов.


Все эти проблемы в случае высоко изолированных сервисов у вас будут. Только не в сетевом слое, который обсосан со всех сторон и как решать данные проблемы в общем понятно,, вам придется для всех этих проблем изобретать свои велосипеды, и натыкаться на потерю запросов, зависание запросов, нарушения порядка запросов в самых неожиданных местах, поскольку куча разного стороннего кода на такую работу не рассчитано.

Плюс сложности с разделением ресурсов, как аппаратных, когда у вас один модуль повесил остальные, так и программными, различными пулами и т.д.
К тому же сетевое взаимодействие у вас все равно осталось, с клиентами, с вешними сервисами, между инсталляциями и т.д., так что код для сетевого взаимодействия у вас никуда не делся.

Возможность упростить архитектуру, реализацию и улучшить юзабилити — во многих (а то и всех, если повезёт) местах, где в обычных микросервисах возникает eventual consistency


У вас декларировано разделение данных, а значит все те же проблемы, что и в микросервисах, я бы даже сказал их ключевые проблемы, консистентность данных. eventual consistency, распределенные транзакции или просто нарушение консистентности, это уже решается для каждого проекта индивидуально.

Circuit Breaker


Тут согласен, пожалуй это плюс. Но:
1) Зависимость от инструментов разработки, поскольку далеко не везде это работает.
2) Если инструменты разработки это проблему не решают проблема усугубляется по сравнению с микросервисами.

Основной недостаток — неприменимость этого подхода в большинстве проектов, где действительно необходима высокая доступность и/или масштабирование


Вот, это как мне кажется ключевой момент. В тех проектах где такой подход можно использовать он избыточен, а где уже актуально, там не достаточен.

Что касается мелких подробностей вроде общего конфига — я вообще не понимаю, в чём проблема.


Я тоже не понимаю. Но mayorovp считает, что она есть.

То, что я вижу,
плюсы: отсутствие сетевого слоя
минусы: отсутствие раздельного деплоя, отсутствие разделение аппаратных и программных ресурсов, распределенные системы только полной установкой, длительная сборка и деплой, ограничение на выбор инструментов и языков, только единая среда выполнения и общие зависимости, отказ от многих стандартных инструментов которые активно используются в микросервисах.

Ключевые проблемы с данными остались и там и там, консистентность и агрегация.
Все эти проблемы в случае высоко изолированных сервисов у вас будут. Только не в сетевом слое, который обсосан со всех сторон и как решать данные проблемы в общем понятно,, вам придется для всех этих проблем изобретать свои велосипеды, и натыкаться на потерю запросов, зависание запросов, нарушения порядка запросов в самых неожиданных местах, поскольку куча разного стороннего кода на такую работу не рассчитано.

Но откуда? Как может потеряться вызов функции?

и натыкаться на потерю запросов, зависание запросов, нарушения порядка запросов

С чего бы это? Это просто вызовы локальных функций, в том или ином виде (напр. в виде передаче данных через каналы/отправке сообщений) — они не теряются, зависают не больше чем любые другие локальные функции, и нарушение порядка возникает только если приложить для этого специальные усилия и отправлять и обрабатывать запросы специально асинхронно.


Только не в сетевом слое, который обсосан со всех сторон и как решать данные проблемы в общем понятно,, вам придется для всех этих проблем изобретать свои велосипеды

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


Плюс сложности с разделением ресурсов, как аппаратных, когда у вас один модуль повесил остальные, так и программными, различными пулами и т.д.

А в чём разница с тем, что несколько микросервисов запущены на одном сервере, и шарят ровно те же ресурсы? Да, конечно, можно ограничивать их по памяти, использовать дисковые квоты… но по факту на практике всё это используется не так часто, и реальная необходимость в этом есть довольно редко. Кроме того — всё это применимо к любому монолиту, и крайне редко является проблемой на практике.


В общем, возможность назначить разным микросервисам отдельные лимиты на CPU и диск — это приятный бонус, как и возможность писать микросервисы на разных языках, etc. — но на практике в большинстве проектов всё это не нужно и всё-равно не используется.


К тому же сетевое взаимодействие у вас все равно осталось, с клиентами, с вешними сервисами, между инсталляциями и т.д., так что код для сетевого взаимодействия у вас никуда не делся.

Безусловно. Поэтому я в статье разделил добавляемую микросервисами сложность на две группы — константная, которая реализуется один раз и при развитии проекта не увеличивается, и зависящую от бизнес-логики, которая постоянно добавляется по мере развития проекта. И встраивание микросервисов позволяет избавиться от (значительной) части сложности второй группы.


У вас декларировано разделение данных, а значит все те же проблемы, что и в микросервисах, я бы даже сказал их ключевые проблемы, консистентность данных. eventual consistency, распределенные транзакции или просто нарушение консистентности, это уже решается для каждого проекта индивидуально.

Не совсем так. Невозможность полезть напрямую в базу другой части монолита не приводит к eventual consistency — ведь для доступа к данным всё-равно вызывается какой-то локальный метод, и какой части монолита он принадлежит — значения не имеет. Eventual consistency возникает не из-за разделения данных, а из-за уменьшения скорости доступа к ним: там, где вызов локальной функции не проблема, делать сетевой запрос так часто, как часто нужны данные, обычно нет возможности.


А вот распределённые транзакции другое дело — они действительно возникают из-за разделения данных, и встраивание микросервисов эту сложность не убирает.


Упомянутое Вами нарушение консистентности — слишком общее понятие. Там, где оно было вызвано eventual consistency — оно исчезнет. Там, где оно было вызвано необходимостью делать транзакции — если транзакции есть, пусть даже и распределённые — с консистентностью данных всё в порядке, а там, где распределённые транзакции решили не делать — с консистентностью будут одинаковые проблемы и в обычном и во встроенном микросервисе.


В тех проектах где такой подход можно использовать он избыточен

Он избыточен только в том случае, если Вы не испытываете сложностей в разработке традиционных монолитов — т.е. если Ваши монолиты не имеют тенденции превращаться в большой комок грязи да и вообще значительно замедляться в разработке со временем из-за общего роста сложности. В этом случае — Вы абсолютно правы, Вам встроенные микросервисы только добавят лишних проблем. К сожалению, обычно для монолитов эта проблема вполне актуальна, и встроенные микросервисы вполне в состоянии её решить — причём намного проще обычных микросервисов.


плюсы: отсутствие сетевого слоя

Не отсутствие, как уже описано выше.


минусы: отсутствие раздельного деплоя,

Да. Но я не думаю, что это действительно проблема — возможность деплоить независимо реально необходима только для распределённой системы, а для монолиту это обычно не важно.


отсутствие разделение аппаратных и программных ресурсов,

Да. Но, опять же, как описано выше — это обычно для всех монолитов и даже многих микросервисных систем, и редко является реальной проблемой.


распределенные системы только полной установкой,

Да. И почему это проблема? Если нужна полноценная распределённая система, то пишите обычные микросервисы. Если нужно распределить нагрузку только на небольшую часть монолита — ну так выкусите из него нужный встроенный микросервис и сделайте из него обычный микросервис — это будет относительно несложно. В общем, по сравнению с обычными монолитами — единственное отличие это новая возможность относительно просто переместить часть монолита в отдельный микросервис.


длительная сборка и деплой,

Сборка всего монолита — да. Деплой — тут как посмотреть, нередко деплой одного большого приложения делается намного быстрее чем кучи мелких. Но есть важные нюансы.


Например, поскольку разработка разных встроенных микросервисов будет вестись независимо, точно так же как если бы они были не встроенными, то каждый PR как правило будет изменять только код одного встроенного микросервиса, а значит тесты можно гонять тоже только для этого микросервиса.


Кроме того, в том же Go сборка работает нереально быстро, так что разница между сборкой одного или десятка микросервисов — будет в несколько секунд… а в других языках для ускорения сборки большого проекта есть много других средств, от make до ccache.


ограничение на выбор инструментов и языков,

Да. Но это не проблема, если всё это и так уже используется.


только единая среда выполнения

Да. Как и в любом монолите, и редко это создаёт сложности.


и общие зависимости,

Здесь чуть сложнее. Если разным встроенным микросервисам необходимы разные версии одной библиотеки — это действительно может создать проблему. Но это зависит от языка — в том же Go вендоринг вполне решает эту проблему. Но в общем и целом — да, такой свободы как с обычными микросервисами здесь не будет, но может это и к лучшему — должны быть очень веские причины почему в одном проекте нам нужны разные версии одной и той же сторонней библиотеки.


отказ от многих стандартных инструментов которые активно используются в микросервисах.

Если проект стал проще, и пропала нужда в инструменте который помогал выживать при большей сложности проекта — почему это плохо?


Ключевые проблемы с данными остались и там и там, консистентность и агрегация.

Нет. В этом-то вся фишка. Эти проблемы встраивание микросервисов как раз и решает!

я в статье разделил добавляемую микросервисами сложность на две группы — константная, которая реализуется один раз и при развитии проекта не увеличивается, и зависящую от бизнес-логики, которая постоянно добавляется по мере развития проекта. И встраивание микросервисов позволяет избавиться от (значительной) части сложности второй группы.

Как по мне, то как раз от новой/измененной бизнес-логики сложности от микросервисов сильно не добавляется в общем случае, после того как вся инфраструктура была переведена на микросервисы.


Но я не думаю, что это действительно проблема — возможность деплоить независимо реально необходима только для распределённой системы, а для монолиту это обычно не важно.

Скорее, это возможное преимущество распределенной системы, а у монолита врожденный недостаток. Я бы выделил возможность раздельного деплоя модулей (в широком смысле слова) системы в один из основных признаков того является система монолитной или нет. А как модули между собой общаются, через сеть, локальными вызовами, локальным IPC или ещё как — дело десятое. Как только возникает ситуация, что один из модулей может быть не готов обработать запрос или обработает его не в соотвествии с кодовой базой (одновременная работа модулей из разных коммитов в случае монорепо), то система перестаёт быть монолитной.


Например, поскольку разработка разных встроенных микросервисов будет вестись независимо, точно так же как если бы они были не встроенными, то каждый PR как правило будет изменять только код одного встроенного микросервиса, а значит тесты можно гонять тоже только для этого микросервиса.

Как минимум, изменение общих зависимостей потребует прогона всего набора тестов.


должны быть очень веские причины почему в одном проекте нам нужны разные версии одной и той же сторонней библиотеки.

Как раз нет. Если мы точно знаем, что фича А работает на библиотеке версии 1, а новая фича Б использует возможности версии 2, то нам нужна веская причина для обновления библиотеки для фичи А при внедрении фичи Б и всегда будут риски, что фича А перестанет работать, какое бы покрытие тестами не было. В случае монолита такой причиной часто выступает организационный запрет или техническая невозможность (очень большая сложность) использовать разные версии одной библиотеки.


Нет. В этом-то вся фишка. Эти проблемы встраивание микросервисов как раз и решает!

Если модули реально изолированы друг от друга, то в общем не решает. Модуль А владетт свой таблицей, модуль Б владеет своей. В монолите мы легко можем заджоинить таблицу другого модуля в запросе своего. При изолированности модулей в случае если нам нужны данные из обеих таблиц мы обязаны получать их двумя разными запросами и агрегировать результат. То же и с консистентностью — обновление данных каждый модуль обязан исполнять в своей транзакции. По сути, концепция сильно изолированных модулей, работающих в рамках одного процесса, решает проблему межпроцессного (чаще всего сетевого) взаимодействия, но не более.

То же и с консистентностью — обновление данных каждый модуль обязан исполнять в своей транзакции.

Вот это как раз необязательно, при желании можно построить систему со сквозными транзакциями. Ведь модуль, в отличии от микросервиса, не может внезапно стать недоступным посреди транзакции.

При раздельном деплое как раз может. Или вообще может оказаться недоступным, или доступной будет не та версия, что ожидается. Или даже части модуля могут оказаться не в консистетном состоянии, например, объявление интерфейса уже обновилось, а реализации нет.

Это не раздельный деплой, это горячая перезагрузка модулей уже.

Технически эти процессы могут ничем не отличаться.

Технически это совершенно разные процессы, выполняемые разными инструментами и реализуемые независимо.

Чем они отличаются в случае, например, PHP? :)

Вероятно, вы имели в виду "умирающую" версию PHP. Тут различие между раздельным деплоем и горячей заменой модулей принципиальная: первый все еще надо отдельно настраивать, а второй не имеет смысла, поскольку перезагрузить можно только то что что лежит в памяти постоянно.

тут все какие то особые инструменты и возможности языков обсуждают. А просто разложить по разным папочкам и и не вызывать друг друга через задницу не вариант? мне как-то всю жизнь хватало. И все языки это позволяют. В QuickBasic только вроде с этим были какие то сложности, но это так давно было.

Конечно, вариант. Этот вариант называется "дисциплина". Но на практике он работает только для немногих разработчиков, поэтому и нужна дополнительная поддержка на уровне инструментов.


Это примерно то же самое, что языки с поддержкой сильной/слабой типизации — никто не мешает на языках со слабой типизацией не использовать неявные преобразования типов и никогда не хранить значения разных типов в одной и той же переменной, но это требует немалой дисциплины и внимательности (которые, кстати, можно направить в более полезное русло, если за типами будет автоматически следить язык).


Кроме того, когда прибегает менеджер, и начинает требовать срочно, вот прямо сейчас, сделать какое-то изменение (критичный багфикс, например), которое быстро-грязно делается наружным вмешательством в код или данные другого модуля, то в большинстве случаев это и будет сделано. Если такое вмешательство технически невозможно или слишком сложно реализовать (например потому, что у нас микросервисы, и прямого доступа ни к их коду ни к их БД снаружи просто нет физически), то вместо быстро-грязно приходится делать нормально — продумывать и ставить задачу команде разрабатывающей тот микросервис, менять его API, обновлять доку на API, тестировать и деплоить микросервис, и только потом делать нужный багфикс используя новое API. Это дольше, но зато в длительной перспективе позволяет удерживать сложность и скорость разработки системы под контролем.

Когда нужно сделать быстро и грязно, оно будет сделано быстро и грязно. Просто не тем способом, который оказался закрыт. И результат может оказаться гораздо печальнее. То, что сделано по быстрому, в любом случае придется переделывать. Я в таком случае всегда ставлю две таски — одну сделать по быстрому в текущую итераци, и вторую — переделать как надо. И ее либо в ту же итерацию, либо в следующую. И возможность сделать грязный хак быстро и легко, позволяет оперативно выпустить фичу, и сэкономить время для ее нормальной реализации, все в плюсе.
А если у вас есть куча обезъян без дисциплины, которые кодят всякое гавно (и не могут даже код по папочкам разложить, это что вообще за девелоперы), а к ним прибегает менеджер, что бы сделать что-то срочно и грязно… какой тут вообще может быть хороший результат? Тут уже неважно какие технологии используются.

Это в теории. Может, ещё лично в Вашей практике, с конкретной командой/начальством. У меня вот с практикой тоже всё в порядке, особенно пока код кроме меня пишет 1-2 человека и я успеваю и свою работу делать и их работу проверять. Но это скорее исключение, а не правило.


"В теории, теория и практика отличаться не должны… но на практике они отличаются." На практике большинство монолитов заканчивают большими комками грязи, хотя пишут их самые разные команды, и там наверняка хватает разработчиков не глупее нас. А микросервисы обычно в большие комки грязи всё-таки не превращаются.


Существует множество техник, которые помогают писать хороший код — включая такие тривиальные как стараться избегать goto и глобальных переменных. Это не означает, что если эти техники игнорировать то нельзя написать хороший код — можно. Равно как и использование и goto и глобальных переменных в некоторых случаях необходимо чтобы код стал лучше. Но, в большинстве случаев, применение этих техник очень помогает, и без них было бы хуже. Раскладывать по папочкам это хороший старт, но если добавить к этому компилятор, который не даст лазить в чужие папочки — станет ещё лучше. Можно использовать соглашение "свойства объекта начинающиеся на подчёркивание снаружи класса использовать нельзя", а можно явно объявить их не экпортируемыми/приватными/защищёнными и не волноваться о том, что кто-то, специально ради быстро-грязной фичи или нечаянно, всё-таки в эти свойства полезет снаружи. Гвозди можно забивать подручными предметами, но молотком обычно удобнее.

ну зато микросервисы превращаются множество маленьких и очень разнообразных кусков грязи и копипасты. То что к одному прикрутили, к другому забыли. Один написали в одном стиле, а другой в другом, потому что писали их два разных человека, которые между собой могут быть не связаны никак и с использованием разных практик. Три микросервиса обновили, четверный забыли. А когда нужно что-то поменять что аффектит несколько микросервисов на проекте с большой командой и длительной разработкой, это нревращается в боль и страдания, даже когда изменение само по себе очень небольшое.
Если у вас программисты не могут разложить код по папочкам… Может им на самом деле лучше молотки выдать? Настоящие? Пусть лучше на стройке забивают гвозди, а не программы пишут. Ведь молотки должны гвозди забивать, зачем писать программы не программистами.
То что к одному прикрутили, к другому забыли.

Такие ситуации обычно вообще не должны возникать. Зачастую они связаны с тем, что несколько микросервисов используют общие библиотеки — и не обычные опенсорсные библиотеки общего назначения, а что-то специфичное для вашего проекта. Этого просто нельзя делать, и обычно когда что-то такое требуется это говорит о неудачном разделении ответственности между микросервисами и необходимости менять архитектуру.


Один написали в одном стиле, а другой в другом

Это вообще нормально. В этом смысл микросервисов — как бы он не был написан, никому снаружи это не должно мешать. Это может раздражать конкретного разработчика, которому приходится сначала что-то доделывать в одном таком микросервисе, а потом в другом — но ведь это всё-равно случается при работе над разными проектами, все они написаны по-разному. Просто надо воспринимать отдельные микросервисы как полноценные и разные проекты.


А когда нужно что-то поменять что аффектит несколько микросервисов

Опять же, это либо признак неудачной архитектуры, либо значительного изменения бизнес-задачи. И решается проблема не изменением существующих сервисов, а реализацией новых, под новую архитектуру и/или бизнес-задачу (и да, новые зачастую на 90% состоят из копипаста кода старых и создаются очень быстро, но они новые, запускаются отдельно, и никак не ломают и не мешают работе старых).

Опять же, это либо признак неудачной архитектуры, либо значительного изменения бизнес-задачи. И решается проблема не изменением существующих сервисов, а реализацией новых, под новую архитектуру и/или бизнес-задачу


Это зачастую требуется даже при очень незначительно изменении бизнес задачи.
Гипотетический пример:
Есть клиенты, финансы, прокси внешних источников. Нужно назначать скоринг клиентов в зависимости от их оборотов в определенной валюте отличной от валюты расчетов. Вот вам изменение трех сервисов.

Совершенно не обязательно. Прокси внешних источников может раздавать события в какой-нить pubsub, а скоринг считать новый сервис подписанный на эти события. Вообще, когда в микросервисной архитектуре возникает задача "надо добавить фичу", то первая мысль должна быть "а можно ли её добавить новым микросервисом?".

Можно и так, но тогда у вас будет дикими темпами расти кол-во сервисов и кол-во зависимостей между ними.
А модифицировать сервисы все равно придется. Да и скоринг клиентов вполне вероятно уже есть, но на других переменных.

Количество сервисов расти будет. Так задумано. Часть сложности и большая часть архитектуры перемещается из кода приложения в связи между микросервисами. Модификация сервиса не представляет проблемы пока не меняется его область ответственности и он развивается эволюционно, обычно сохраняя совместимость со старым API.

Вовсе не обязательно приводят. Я лично наблюдал кучу успешных проектов, сделанных именно в таком стиле. Правильная изоляция и в тоже время взаимодействие компонентов приложения — это вообще один из важнейших факторов успеха. Если бы это ни у кого не получалось — успешных проектов вообще было бы ничтожно мало.


Ну т.е. например JavaEE (существует примерно с 2000 года) — вполне себе монолитный контейнер, где работают много изолированных приложений. Ну так вот, эти приложения могут быть огромными кусками грязи, или набором вполне независимых в достаточной степени изолированных модулей. Изоляция кроме всего прочего не бесплатна.

Для .Net подобную функциональность/идею модульных расширений можно делать через MEF и MAF.
MEF, например, творчески расширенная идея привязки к интерфейсу реализующих его компонентов лежащих в соседних сборках(через рефлексию). Только модель привязки более сложная. Использование особых проблем не вызывало.
MAF по ощущениям, сложнее и высокоуровнее с загрузкой/выгрузкой/изоляцией модулей, но его пока не трогал.

Erlang+Otp такое, в принципе, может(и постулирует) из коробки.

А еще для той же цели можно использовать любой IoC-контейнер: Autofac, Unity, Ninject, SimpleInjector, Castle Windsor...

То что вы пытаетесь описать очень напоминает процессы в Erlang/Elixir. Процессы приложения могут выполняться как внутри инстанса виртуальной машины, так и разноситься по нескольким, в том числе по сети. Процессы между собой общаются посредством сообщений. Никакого разделяемого доступа, данные иммутабельны по-умолчанию. Маршализация через внутренее бинарное представление, т.е. для любых типов, условно бесплатно.

На самом деле, когда я начал писать обычные микросервисы (в 2009, когда ещё и термина такого не существовало), я пытался симулировать архитектуру OS Inferno, где тоже лёгкие нити, каналы, процессов нет вообще а вместо них обычные модули выполняющиеся в лёгких нитях. Сеть тогда пришлось всунуть между модулями-микросервисами только потому, что больше ничего не давало необходимого уровня изоляции (я тогда писал на Perl). А сейчас хочется вернуться обратно к варианту, когда для взаимодействия микросервисов сеть не обязательна, благо у Go и OS Inferno немало общего. И да, Erlang тоже в этом смысле очень хорош и умеет всё необходимое из коробки.

Если говорить о концепции микросервисов сеть не обязательна, обмен может быть реализован через любой другой слой. Другое дело, что на практике обычно под микросервисами понимают, что-то работающее на базе http, https и т.п.
Иначе получится монстр вроде OSGi.

А вы его интересно хоть в глаза видели?

Я не пишу на Java, но я прочитал про него очень много документации и отзывов тех, кто его использовал — и среди них не было ни одного положительного отзыва, если исключить отзывы тех, кто только-только начал его изучать/использовать. По сути, они реализовали внутри одного процесса вообще всю экосистему необходимую для микросервисов. Для той задачи, которую ставлю я (уменьшение сложности) это перебор — оно физически не может быть значительно проще, чем обычные микросервисы. Но это полезный пример того, что скорее всего получится, если попытаться засунуть микросервисы в монолит не придерживаясь каких-то строгих ограничений (баланса). Охотно верю, что у OSGi могут быть свои задачи и применения, где нужно именно что-то вроде OSGi, но мою задачу такой подход не решит.

Так я вам суммирую свой трехлетний опыт — забудьте всю ту ерунду, что вам наговорили.
Во-первых, это не монстр вовсе. У меня ушло примерно месяц на то, чтобы начать полноценно использовать караф, параллельно с разработкой прикладных частей.


Я участвовал в разработке одного проекта (10 человек), и делал в одиночку другой. В одном сотни микросервисов, в другом — десятки. Никаких серьезных проблем при этом фреймворк не создает, и ограничений реально не накладывает.


Оно значительно проще, чем куча отдельных микросервисов, по ряду причин:


  • на сотню сервисов как правило приходилось примерно 10-20 коннектов к 10-20 базам данных, которые мы настраиваем 10 или 20 раз, а не в каждом микросервисе, которому они понадобились. Тоже самое для JMS/QPID/ActiveMQ коннектов и других ресурсов.
  • общий JMX, где видна вся статистика (мониторинг).
  • общее управление, в виде веб или ssh консолей.
  • кластер практически задаром.
Вопрос не много в сторону, но давно хотел задать кому-нибудь смелому… Так вот, до 2015 года (появления статьи microservices в wiki), или до 2014 года, одноименной статьи Martin Fowler или до 2013 года, первой версии Docker'а, как выглядили микросервисы и как вы их от просто web/rest services отличали (утверждаете что пишите с 2009 года)? Не ищите подвоха, просто хочу разобраться в терминологии, раньше не было никаких микросервисов — а теперь вроде как все проекты стали «с микросервисной архитектурой», а когда переспросишь «доккер что ли используете?» — не понимают связи вопроса, хотя для меня связь очевидна (на собственной правоте не настаиваю).

Очень просто они выглядели, ровно так же, как и сейчас:


  • программка на любом языке
  • запущенная под управлением какого-нибудь супервизора (runit, daemontools, s6, …), который будет её гарантированно перезапускать после падения
  • зарегистрировавшаяся в каком-нибудь сервисе выполняющем роль реестра сервисов и использующая его для обнаружения других сервисов
  • предоставляющая REST или RPC API на нестандартном порту, возможно с nginx перед группой таких сервисов
  • имеющая личное хранилище данных (обычно либо в файлах, либо в SQL/NoSQL базе) если оно ей вообще нужно
  • записывающая лог в файл (обычно через дополнительную утилиту вроде собственного syslog-совместимого сервиса или просто тулзы вроде svlogd), с опциональным дублированием записей лога через UDP в сторону сетевого syslog-сервиса для агрегирования логов
  • сервисы старался делать как можно меньше (например есть у меня сервис, который выдаёт уникальные ID для остальных сервисов, и больше ничего не делает)

Придумал я этот подход пытаясь симулировать то, как работают приложения в OS Inferno — только в OS Inferno были лёгкие нити и каналы для взаимодействия приложений запущенных на одной машине, плюс файловый сетевой протокол Styx для взаимодействия приложений запущенных где угодно — хоть на одной машине, хоть на разных. А на Perl у меня тогда ничего этого не было, поэтому для получения нужного уровня изоляции и коммуникации между сервисами пришлось использовать сетевые RPC-протоколы (в основном, иногда REST). В качестве сервиса реестра-сервисов я запускал приложение файл-сервер registry из OS Inferno, добавив к нему обёртку которая предоставляла доступ к его файлам (файлы были внутри виртуальной машины OS Inferno) по TCP, плюс написал модуль для Perl Inferno::RegMgr чтобы подключаться к этому реестру из своих Perl-сервисов (плюс использовал ещё один сервис из Inferno для авторизации между сервисами).


Позднее, когда сформировалось понимание как писать в этом стиле и я погуглил как это делают и называют другие — я нашёл SOA и описание архитектуры Амазона, которое выглядело очень похожим (исключая масштабы, конечно). И считал что я делаю SOA, до появления статьи Фаулера про микросервисы.


Кстати, докер штука совершенно не обязательная, хотя и заметно упрощает деплой кучи разных микросервисов.


P.S. Что касается подвоха — когда я сам задался вопросом "а сколько же лет я уже пишу микросервисы" то я сам выяснял это по датам своих же коммитов, например того же Inferno::RegMgr. У меня воспоминания что я это делал и до 2009, но фактические подтверждения удалось найти только начиная с 2009 — вероятно до этого были в основном эксперименты, а не применение в реальных проектах.

Вот вы правильно сказали «делал SOA», это с 2005ого года. Это как раз то о чем я спросить хотел: когда отказываются от SOA описывая свой проект и выбирают описание microservices architecture что хотят подчеркнуть? Очевидно вычлеянется какой-то один самый важный и мне кажется технический аспект, поскольку впечатление хотят произвести на технарей. Да и «одна ответсвенность» — это сильно субъективно, но может так. Или что брендовой шины нет (BizTalk, TIbco)? Но в любом случае управление микросервисами возможно только каким-то штатным инструментом (а значит и упаковкой в штатные контейнеры), так почему просто не назвать этот штатный инструмент? Что «личное хранилище данных» (NoSQL, 2009)? Но опять же обычно есть нормальный дб сервер (не в контейнере) имитирующий эту самую шину. Как вам кажется, какой аспект самый важный?

Причём тут "произвести впечатление", что за глупости? Причина использования более корректного термина в том, чтобы точнее описать суть, а не в том, что этот термин на кого-то может произвести впечатление.


Основная причина выбора термина "микросервис" вместо SOA отлично описана у того же Фаулера в боковой врезке.

Разве суть лучше всего не описало бы указание штатных средств развертывания и управления сервисами? А впечатление это не плохо, зря агритесь, просто ну что еще можно сформировать в голове слушателея говоря «у нас архитектура микросервисов». Фоулеру можно было бы поверить, но если включить режим скептика, а такие времена что он всё время включен, микросервисы тоже озанчают too many different things. И NoSql в конейнере и рдбмс вне контейнера (это стандартная рекомендация docker) всё микросервисы. И одинокий сервис генерации id вдали от монолита — тоже типа микросервис.

Я вас понял так — вы более хотите подчеркнуть область ваших интересов, а не точно описать архитектуру создаваемых систем. Опять же не ищите подвоха. Уважаю вас и ваш труд.

Практический вопрос: вот представляют мне систему и говорят «у нас она на микросервисках», какой вопрос задать, чтобы он не повисал в воздухе? Потому что «докер используете» обычно неприятно повисает (если докер не используется).

Конкретные средства развёртывания и управления сервисами к микросервисной архитектуре отношения не имеют.


Слова "у нас архитектура микросервисов" в голове понимающего слушателя формируют примерно такое впечатление: "ага, ясно, кучка мелких проектов, так что с кодом возиться будет не очень сложно, очень вероятно что бардак с архитектурой и связями между микросервисами — надо обязательно уточнить как в этом проекте с eventual consistency и распределёнными транзакциями — плюс выяснить что из себя представляют местные девопсы, как справляются, и есть ли они вообще". :)


И NoSql в конейнере и рдбмс вне контейнера (это стандартная рекомендация docker) всё микросервисы. И одинокий сервис генерации id вдали от монолита — тоже типа микросервис.

Именно так. Всё это — микросервисы, и принципиальных отличий в контейнере оно или нет — нету. Основное действительно принципиальное отличие — это stateless/stateful. (При этом вариант когда формально он stateless только потому, что используемая этим микросервисом СУБД общего назначения запущена где-то отдельно — не считается.) Ещё одно важное отличие — это когда микросервисами называют группу разных приложений использующих общую БД — вот это вообще не микросервисы, это просто несчастный, порванный на куски монолит, которому очень больно.


Я вас понял так — вы более хотите подчеркнуть область ваших интересов, а не точно описать архитектуру создаваемых систем.

Вообще-то нет, я как раз пытаюсь описать именно архитектуру. А какое описание будет более подходящим, на Ваш взгляд? Ещё более общий чем микросервисы термин SOA?


Практический вопрос: вот представляют мне систему и говорят «у нас она на микросервисках», какой вопрос задать, чтобы он не повисал в воздухе? Потому что «докер используете» обычно неприятно повисает (если докер не используется).

Не обижайтесь, но это всё реально забавно. Технические вопросы обычно задают не ради того, чтобы произвести впечатление или поддержать разговор, а чтобы получить информацию. Спрашивайте о том, о чем Вам интересно узнать, конечно же. :) А вопрос про докер подвисает именно потому, что конкретный инструмент принципиального значения не имеет когда речь об архитектуре.

Я вижу здесь противоречие: «вероятно… что бардак с архитектурой и связями между микросервисами » (согласен) и «конкретный инструмент принципиального значения не имеет» — это когда доккер вместе с его оркестратором как раз решают проблему (создавая новые).

Я фрилансер и мне бывает надо выяснять «стоит ли ввязываться» (установить уровень культуры в организации, который лучше всего проявляется в используемых инструментах) и желательно за одну сессию, тянуть кота за хвост, тоже считается моветоном.

«А какое описание будет более подходящим, на Ваш взгляд?». Штатные средства деплоймента и орекстрации — дают гораздо больше конкретики.

В общем я для себя сделал вывод. На пороль «у нас микросервисы», правильный ответ: «используете ли какой-нибудь оркестратор?».

Для обычного прикладного разработчика в целом средства оркестрации дело десятое, если он ими не занимается непосредственно. А бардак с архитектурой и связями докер с оркестраторами не решают. Правильный "ответ" скорее "Как микросервисы взаимодействуют друг с другом? Схемы, протоколы, шины, шлюзы, агрегаторы, аутентификация и авторизация?"


Зачастую микросервизация монолита заключается в выносе его модулей в микросервисы без особого изменения его сильносвязанной архитектуры, когда зависимости и взаимодействие жёстко и явно прописаны в коде, просто локальные вызовы заменяются удаленными и хорошо если предпринимаются меры по обеспечению хоть какой-нибудь целостности.

По сути, микросервис — это сервис с единственной ответственностью. Зачастую, проекты с сервисной архитектурой начинались с сервисов, которые сейчас бы назвали микросервисами, которые потом разрастались в полноценные монолиты.
Модули («микролиты» (microlith), «моносервисы» (monoservice), «встроенный микросервисы») из данной статьи — это то же самое, что и Self-Contained Systems (SCS, Составные приложения) (http://scs-architecture.org/, habrahabr.ru/post/176851)? Или это разные понятия? Если это разные понятия, то в чем различие?
Надо же, примерно в это время я и писал свой Antinite, а коллеги с хабра говорят «нечего выкладывать такое в интернеты, забирай обратно».
Ведь перспективная тема же?
Ваша статья очень полезна, помогла разобраться в понятиях.
Кажется, вы как-то неправильно прочитали тот комментарий, на который дали ссылку! Там были претензии к реализации, а не к идее.

Полностью согласен.

Автору удалось реализовать модульный монолит на практике?

Golang не очень хорош для гарантий изоляции модулей, зато удобен да микросервисов. C#, Java имеют более развитые инструменты для этого. Но можно и на golang.

PHP, node.js инструментов гарантирующих изоляцию модулей практически не имеют совсем. И когда там принимается решение о разделении на микросервисы я их понимаю. И жалею.

Автору удалось реализовать модульный монолит на практике?

Плюс/минус. На практике в моих реальных проектах всё-равно требовались микросервисы (из соображения распределения нагрузки между серверами), так что в конечном итоге всё свелось к тому, что код всех микросервисов на Go находится в одном репо, и хотя формально/технически возможно скомпилировать их в одном монолите но практически нужды в этом обычно не возникает. Несколько устаревший пример такого монолита с несколькими микросервисами внутри можно увидеть в https://github.com/powerman/go-monolith-example. Можно ли (и нужно ли) в нём что-то изменить/упростить с целью получить обычный монолит (например реализовать вызовы между модулями через обычный вызов функции вместо полноценного сетевого API) пока неясно.

gRPC в монолите это прям спорно - ну если только жесткая перегрузка пойдет и надо сбрасывать повторные запросы - Circuit Breaker

я бы интерфейсы оставил

Ну, оно там изначально появилось из соображений "если будет нужно, то из этого монолита мы за час любой модуль выносим в отдельный микросервис". Но в целом, да, для настоящего монолита это определённо перебор.

Sign up to leave a comment.

Articles