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). Это не так здорово, потому что придётся тратить ресурсы на (де)маршалинг, плюс в зависимости от выбранного формата передачи данных можно и строгую типизацию потерять.
То, что я описал, пока не называют никак, к сожалению. Все используемые на данный момент термины я упомянул в статье. Термин микросервис тоже появился тоже далеко не сразу, сначала люди это просто делали, а потом (аж в 2011) это назвали микросервисами.
Мне тоже не очень нравится использовать и так перегруженный кучей существующих смыслов термин "модуль", может со временем появится термин по-лучше.
Плагины это всё-таки нечто иное. В первую очередь плагины — они к чему-то прилагаются. В описанном подходе никакого "основного приложения", по сути, просто нет — есть функция main()
на 30 строк занимающаяся разбором параметров и вызовом функций инициализации каждого модуля… и всё. Можно провести аналогию с sh-скриптом, который запускает кучку разных программ в фоновых процессах — эти программы не являются "плагинами" к sh-скрипту.
Никакого диспетчера внутри процесса у меня в описании нет, равно как нет и управления циклом жизни модулей или их взаимодействием — точно так же, как такого диспетчера нет между настоящими микросервисами. После запуска модули просто живут своей жизнью, обрабатывают входящие запросы (не важно откуда они пришли — по сети снаружи от другого микросервиса, или по каналу внутри от другого модуля), могут сами отправлять запросы (и в другие микросервисы по сети, и в другие модули внутри приложения), выполнять какие-то запланированные задачи по собственной инициативе.
Возможно вместо перегрузки термина "модуль" было бы лучше использовать термин "встроенный микросервис" (увидел у lega по ссылке из комментария ниже).
Просто поддержал беседу как мог. Пусть будет «встроенный микросервис», всё равно никаких критериев для выбора названия описанному в статье «подходу» нет, ибо он не является подходом вообще, это не решение какой-то практической проблемы в софтостроении, это просто каша в голове конкретного софтостроителя, увлёкшегося бесполезными аналогиями.
Функция main на 30 строк — это все-таки, как мне кажется, слишком мало. Я бы все-таки в ядро утянул, во-первых, те вещи которые обычно дублируются в каждом микросервисе (и хорошо если без копи-паста): логи, конфиги, создание потоков и управление ими. Сюда же попадает слой взаимодействие с другими (макро-)сервисами.
А во-вторых, саму по себе инициализацию стоило бы усложнить. У меня программы, которые написаны в архитектуре "ядро + плагины с бизнес-логикой" инициализаруются в три этапа:
- поиск и регистрация внутренних сервисов и их зависимостей (настройка IoC-контейнера);
- настройка плагинами друг друга и ядра;
- запуск плагинов.
"Модуль — это что-то вроде микросервиса" — как мне развидеть это? Завтра будет "функция — это что-то вроде http-запроса"?
Да я бы и сам это с удовольствием развидел. Но критика должна быть конструктивной, так что — предложите другое название.
Уже давно пишем почти весь код в "модульном" стиле. Модуль = папка в проекте. В папке весь код модуля. Если модуль по каким-то причинам больше не нужен, удаляем папку и забываем. Довольны.
Специально проверял значение термина «модуль» в Википедии. Этому определнию наш подход не противоречит. А стоит ли выделять отдельную сущность «компонент» и из них строить модули — вопрос организационный.
К примеру, при росте количества модулей не стоит забывать про увеличение времени необходимого на инициализацию самого приложения даже при отложенной (ленивой) инициализации модулей по факту запроса.
Как я упомянул в статье, я лично в таком стиле пока ещё не писал, но тут в комментариях отписывались люди, которые нечто подобное уже используют — вопрос про недостатки это скорее к ним. Я в статье постарался описать недостатки, которые очевидны сходу, и уменьшение скорости запуска приложения там есть.
Падение модуля приводит к падению всего приложения.Ничего не приводит, зависит от реализации.
Несколько лет использую подобный подход (позволяет снизить кривую сложности разработки), но с более мягкими ограничениями — грубо говоря нужно просто придерживаться того, что-бы было легко выдернуть модуль в микросервис без рефакторинга остальной части проекта, тут набросал некоторые правила/идеи.
Ну, учитывая отсутствие встроенного в приложение супервизора для встроенного микросервиса, который бы мог перезапустить его после падения — падение встроенного микросервиса должно приводить к падению всего приложения (чтобы его мог перезапустить внешний супервизор). Особого смысла встраивать в приложение ещё и супервизор я пока не вижу.
Конечно, в принципе встроенные микросервисы можно писать так, чтобы они сами обрабатывали свои исключения и восстанавливались после ошибок — но это их может заметно усложнить. Если приложению нельзя изредка падать и нужна высокая доступность — лучше использовать обычные микросервисы, а не пытаться написать невероятно устойчивый монолит.
Ваши правила/идеи почитал, спасибо (моя статья это скорее аналогичный рабочий документ, нежели настоящая статья). Мне всё-таки кажется, что для того, чтобы это работало не только для отдельных, достаточно дисциплинированных, разработчиков — нужен более сильный уровень изоляции встроенных микросервисов, который вряд ли смогут обеспечить скриптовые языки.
Тем не менее, раз у Вас есть реальный практический опыт — поделитесь проблемами, которые возникали в процессе использования этого подхода?
Наверно единственное — не перезагрузить сервис отдельно (хотя в питоне есть методы для перезагрузки модулей), но и это не проблема — пока перезагружается монолит все запросы балансируются на другие сервера.
Маштабирование — просто запуск монолита с некоторыми выключенными модулями, либо ленивой загрузкой ну либо на крайний случай — можно выдернуть в микросервис.
В случае неполадок сервис выбрасывает исключения которые ловятся где нужно.
Вместо «стандарных методов» инициализация/настройка модуля, я использую 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? :)
Конечно, вариант. Этот вариант называется "дисциплина". Но на практике он работает только для немногих разработчиков, поэтому и нужна дополнительная поддержка на уровне инструментов.
Это примерно то же самое, что языки с поддержкой сильной/слабой типизации — никто не мешает на языках со слабой типизацией не использовать неявные преобразования типов и никогда не хранить значения разных типов в одной и той же переменной, но это требует немалой дисциплины и внимательности (которые, кстати, можно направить в более полезное русло, если за типами будет автоматически следить язык).
Кроме того, когда прибегает менеджер, и начинает требовать срочно, вот прямо сейчас, сделать какое-то изменение (критичный багфикс, например), которое быстро-грязно делается наружным вмешательством в код или данные другого модуля, то в большинстве случаев это и будет сделано. Если такое вмешательство технически невозможно или слишком сложно реализовать (например потому, что у нас микросервисы, и прямого доступа ни к их коду ни к их БД снаружи просто нет физически), то вместо быстро-грязно приходится делать нормально — продумывать и ставить задачу команде разрабатывающей тот микросервис, менять его API, обновлять доку на API, тестировать и деплоить микросервис, и только потом делать нужный багфикс используя новое API. Это дольше, но зато в длительной перспективе позволяет удерживать сложность и скорость разработки системы под контролем.
А если у вас есть куча обезъян без дисциплины, которые кодят всякое гавно (и не могут даже код по папочкам разложить, это что вообще за девелоперы), а к ним прибегает менеджер, что бы сделать что-то срочно и грязно… какой тут вообще может быть хороший результат? Тут уже неважно какие технологии используются.
Это в теории. Может, ещё лично в Вашей практике, с конкретной командой/начальством. У меня вот с практикой тоже всё в порядке, особенно пока код кроме меня пишет 1-2 человека и я успеваю и свою работу делать и их работу проверять. Но это скорее исключение, а не правило.
"В теории, теория и практика отличаться не должны… но на практике они отличаются." На практике большинство монолитов заканчивают большими комками грязи, хотя пишут их самые разные команды, и там наверняка хватает разработчиков не глупее нас. А микросервисы обычно в большие комки грязи всё-таки не превращаются.
Существует множество техник, которые помогают писать хороший код — включая такие тривиальные как стараться избегать goto и глобальных переменных. Это не означает, что если эти техники игнорировать то нельзя написать хороший код — можно. Равно как и использование и goto и глобальных переменных в некоторых случаях необходимо чтобы код стал лучше. Но, в большинстве случаев, применение этих техник очень помогает, и без них было бы хуже. Раскладывать по папочкам это хороший старт, но если добавить к этому компилятор, который не даст лазить в чужие папочки — станет ещё лучше. Можно использовать соглашение "свойства объекта начинающиеся на подчёркивание снаружи класса использовать нельзя", а можно явно объявить их не экпортируемыми/приватными/защищёнными и не волноваться о том, что кто-то, специально ради быстро-грязной фичи или нечаянно, всё-таки в эти свойства полезет снаружи. Гвозди можно забивать подручными предметами, но молотком обычно удобнее.
Если у вас программисты не могут разложить код по папочкам… Может им на самом деле лучше молотки выдать? Настоящие? Пусть лучше на стройке забивают гвозди, а не программы пишут. Ведь молотки должны гвозди забивать, зачем писать программы не программистами.
То что к одному прикрутили, к другому забыли.
Такие ситуации обычно вообще не должны возникать. Зачастую они связаны с тем, что несколько микросервисов используют общие библиотеки — и не обычные опенсорсные библиотеки общего назначения, а что-то специфичное для вашего проекта. Этого просто нельзя делать, и обычно когда что-то такое требуется это говорит о неудачном разделении ответственности между микросервисами и необходимости менять архитектуру.
Один написали в одном стиле, а другой в другом
Это вообще нормально. В этом смысл микросервисов — как бы он не был написан, никому снаружи это не должно мешать. Это может раздражать конкретного разработчика, которому приходится сначала что-то доделывать в одном таком микросервисе, а потом в другом — но ведь это всё-равно случается при работе над разными проектами, все они написаны по-разному. Просто надо воспринимать отдельные микросервисы как полноценные и разные проекты.
А когда нужно что-то поменять что аффектит несколько микросервисов
Опять же, это либо признак неудачной архитектуры, либо значительного изменения бизнес-задачи. И решается проблема не изменением существующих сервисов, а реализацией новых, под новую архитектуру и/или бизнес-задачу (и да, новые зачастую на 90% состоят из копипаста кода старых и создаются очень быстро, но они новые, запускаются отдельно, и никак не ломают и не мешают работе старых).
Опять же, это либо признак неудачной архитектуры, либо значительного изменения бизнес-задачи. И решается проблема не изменением существующих сервисов, а реализацией новых, под новую архитектуру и/или бизнес-задачу
Это зачастую требуется даже при очень незначительно изменении бизнес задачи.
Гипотетический пример:
Есть клиенты, финансы, прокси внешних источников. Нужно назначать скоринг клиентов в зависимости от их оборотов в определенной валюте отличной от валюты расчетов. Вот вам изменение трех сервисов.
Совершенно не обязательно. Прокси внешних источников может раздавать события в какой-нить pubsub, а скоринг считать новый сервис подписанный на эти события. Вообще, когда в микросервисной архитектуре возникает задача "надо добавить фичу", то первая мысль должна быть "а можно ли её добавить новым микросервисом?".
А модифицировать сервисы все равно придется. Да и скоринг клиентов вполне вероятно уже есть, но на других переменных.
Количество сервисов расти будет. Так задумано. Часть сложности и большая часть архитектуры перемещается из кода приложения в связи между микросервисами. Модификация сервиса не представляет проблемы пока не меняется его область ответственности и он развивается эволюционно, обычно сохраняя совместимость со старым API.
Вовсе не обязательно приводят. Я лично наблюдал кучу успешных проектов, сделанных именно в таком стиле. Правильная изоляция и в тоже время взаимодействие компонентов приложения — это вообще один из важнейших факторов успеха. Если бы это ни у кого не получалось — успешных проектов вообще было бы ничтожно мало.
Ну т.е. например JavaEE (существует примерно с 2000 года) — вполне себе монолитный контейнер, где работают много изолированных приложений. Ну так вот, эти приложения могут быть огромными кусками грязи, или набором вполне независимых в достаточной степени изолированных модулей. Изоляция кроме всего прочего не бесплатна.
MEF, например, творчески расширенная идея привязки к интерфейсу реализующих его компонентов лежащих в соседних сборках(через рефлексию). Только модель привязки более сложная. Использование особых проблем не вызывало.
MAF по ощущениям, сложнее и высокоуровнее с загрузкой/выгрузкой/изоляцией модулей, но его пока не трогал.
Erlang+Otp такое, в принципе, может(и постулирует) из коробки.
То что вы пытаетесь описать очень напоминает процессы в Erlang/Elixir. Процессы приложения могут выполняться как внутри инстанса виртуальной машины, так и разноситься по нескольким, в том числе по сети. Процессы между собой общаются посредством сообщений. Никакого разделяемого доступа, данные иммутабельны по-умолчанию. Маршализация через внутренее бинарное представление, т.е. для любых типов, условно бесплатно.
На самом деле, когда я начал писать обычные микросервисы (в 2009, когда ещё и термина такого не существовало), я пытался симулировать архитектуру OS Inferno, где тоже лёгкие нити, каналы, процессов нет вообще а вместо них обычные модули выполняющиеся в лёгких нитях. Сеть тогда пришлось всунуть между модулями-микросервисами только потому, что больше ничего не давало необходимого уровня изоляции (я тогда писал на Perl). А сейчас хочется вернуться обратно к варианту, когда для взаимодействия микросервисов сеть не обязательна, благо у Go и OS Inferno немало общего. И да, Erlang тоже в этом смысле очень хорош и умеет всё необходимое из коробки.
Иначе получится монстр вроде OSGi.
А вы его интересно хоть в глаза видели?
Я не пишу на Java, но я прочитал про него очень много документации и отзывов тех, кто его использовал — и среди них не было ни одного положительного отзыва, если исключить отзывы тех, кто только-только начал его изучать/использовать. По сути, они реализовали внутри одного процесса вообще всю экосистему необходимую для микросервисов. Для той задачи, которую ставлю я (уменьшение сложности) это перебор — оно физически не может быть значительно проще, чем обычные микросервисы. Но это полезный пример того, что скорее всего получится, если попытаться засунуть микросервисы в монолит не придерживаясь каких-то строгих ограничений (баланса). Охотно верю, что у OSGi могут быть свои задачи и применения, где нужно именно что-то вроде OSGi, но мою задачу такой подход не решит.
Так я вам суммирую свой трехлетний опыт — забудьте всю ту ерунду, что вам наговорили.
Во-первых, это не монстр вовсе. У меня ушло примерно месяц на то, чтобы начать полноценно использовать караф, параллельно с разработкой прикладных частей.
Я участвовал в разработке одного проекта (10 человек), и делал в одиночку другой. В одном сотни микросервисов, в другом — десятки. Никаких серьезных проблем при этом фреймворк не создает, и ограничений реально не накладывает.
Оно значительно проще, чем куча отдельных микросервисов, по ряду причин:
- на сотню сервисов как правило приходилось примерно 10-20 коннектов к 10-20 базам данных, которые мы настраиваем 10 или 20 раз, а не в каждом микросервисе, которому они понадобились. Тоже самое для JMS/QPID/ActiveMQ коннектов и других ресурсов.
- общий JMX, где видна вся статистика (мониторинг).
- общее управление, в виде веб или ssh консолей.
- кластер практически задаром.
Очень просто они выглядели, ровно так же, как и сейчас:
- программка на любом языке
- запущенная под управлением какого-нибудь супервизора (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 отлично описана у того же Фаулера в боковой врезке.
Я вас понял так — вы более хотите подчеркнуть область ваших интересов, а не точно описать архитектуру создаваемых систем. Опять же не ищите подвоха. Уважаю вас и ваш труд.
Практический вопрос: вот представляют мне систему и говорят «у нас она на микросервисках», какой вопрос задать, чтобы он не повисал в воздухе? Потому что «докер используете» обычно неприятно повисает (если докер не используется).
Конкретные средства развёртывания и управления сервисами к микросервисной архитектуре отношения не имеют.
Слова "у нас архитектура микросервисов" в голове понимающего слушателя формируют примерно такое впечатление: "ага, ясно, кучка мелких проектов, так что с кодом возиться будет не очень сложно, очень вероятно что бардак с архитектурой и связями между микросервисами — надо обязательно уточнить как в этом проекте с eventual consistency и распределёнными транзакциями — плюс выяснить что из себя представляют местные девопсы, как справляются, и есть ли они вообще". :)
И NoSql в конейнере и рдбмс вне контейнера (это стандартная рекомендация docker) всё микросервисы. И одинокий сервис генерации id вдали от монолита — тоже типа микросервис.
Именно так. Всё это — микросервисы, и принципиальных отличий в контейнере оно или нет — нету. Основное действительно принципиальное отличие — это stateless/stateful. (При этом вариант когда формально он stateless только потому, что используемая этим микросервисом СУБД общего назначения запущена где-то отдельно — не считается.) Ещё одно важное отличие — это когда микросервисами называют группу разных приложений использующих общую БД — вот это вообще не микросервисы, это просто несчастный, порванный на куски монолит, которому очень больно.
Я вас понял так — вы более хотите подчеркнуть область ваших интересов, а не точно описать архитектуру создаваемых систем.
Вообще-то нет, я как раз пытаюсь описать именно архитектуру. А какое описание будет более подходящим, на Ваш взгляд? Ещё более общий чем микросервисы термин SOA?
Практический вопрос: вот представляют мне систему и говорят «у нас она на микросервисках», какой вопрос задать, чтобы он не повисал в воздухе? Потому что «докер используете» обычно неприятно повисает (если докер не используется).
Не обижайтесь, но это всё реально забавно. Технические вопросы обычно задают не ради того, чтобы произвести впечатление или поддержать разговор, а чтобы получить информацию. Спрашивайте о том, о чем Вам интересно узнать, конечно же. :) А вопрос про докер подвисает именно потому, что конкретный инструмент принципиального значения не имеет когда речь об архитектуре.
Я фрилансер и мне бывает надо выяснять «стоит ли ввязываться» (установить уровень культуры в организации, который лучше всего проявляется в используемых инструментах) и желательно за одну сессию, тянуть кота за хвост, тоже считается моветоном.
«А какое описание будет более подходящим, на Ваш взгляд?». Штатные средства деплоймента и орекстрации — дают гораздо больше конкретики.
В общем я для себя сделал вывод. На пороль «у нас микросервисы», правильный ответ: «используете ли какой-нибудь оркестратор?».
Для обычного прикладного разработчика в целом средства оркестрации дело десятое, если он ими не занимается непосредственно. А бардак с архитектурой и связями докер с оркестраторами не решают. Правильный "ответ" скорее "Как микросервисы взаимодействуют друг с другом? Схемы, протоколы, шины, шлюзы, агрегаторы, аутентификация и авторизация?"
Зачастую микросервизация монолита заключается в выносе его модулей в микросервисы без особого изменения его сильносвязанной архитектуры, когда зависимости и взаимодействие жёстко и явно прописаны в коде, просто локальные вызовы заменяются удаленными и хорошо если предпринимаются меры по обеспечению хоть какой-нибудь целостности.
А что у них общего? Там, где описана разница между SCS и обычными микросервисами все пункты актуальны и для встроенных микросервисов.
Полностью согласен.
Автору удалось реализовать модульный монолит на практике?
Golang не очень хорош для гарантий изоляции модулей, зато удобен да микросервисов. C#, Java имеют более развитые инструменты для этого. Но можно и на golang.
PHP, node.js инструментов гарантирующих изоляцию модулей практически не имеют совсем. И когда там принимается решение о разделении на микросервисы я их понимаю. И жалею.
Автору удалось реализовать модульный монолит на практике?
Плюс/минус. На практике в моих реальных проектах всё-равно требовались микросервисы (из соображения распределения нагрузки между серверами), так что в конечном итоге всё свелось к тому, что код всех микросервисов на Go находится в одном репо, и хотя формально/технически возможно скомпилировать их в одном монолите но практически нужды в этом обычно не возникает. Несколько устаревший пример такого монолита с несколькими микросервисами внутри можно увидеть в https://github.com/powerman/go-monolith-example. Можно ли (и нужно ли) в нём что-то изменить/упростить с целью получить обычный монолит (например реализовать вызовы между модулями через обычный вызов функции вместо полноценного сетевого API) пока неясно.
gRPC в монолите это прям спорно - ну если только жесткая перегрузка пойдет и надо сбрасывать повторные запросы - Circuit Breaker
я бы интерфейсы оставил
Модули вместо микросервисов