Модули вместо микросервисов

Термин "модуль" (module) взят из статьи Modules vs. microservices. Так же для описания чего-то среднего между микросервисами и монолитами иногда используют термины "микролит" (microlith) или "моносервис" (monoservice). Но, не смотря на то, что термин "модуль" и так уже нагружен общеизвестным смыслом, на мой взгляд он подходит лучше других вариантов. Update: В комментарии lega использовал термин "встроенный микросервис" — он лучше описывает суть подхода, чем "модуль".


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


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


Что такое модуль


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


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



В отличие от микросервисов, модуль:



В отличие от обычных библиотек, модуль:



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


Где нужны модули


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


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



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



Так же у модульного подхода появляются новые достоинства:



Резюме


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


Необходимая поддержка этого подхода в данный момент есть далеко не во всех языках, но в некоторых есть: автор статьи "Modules vs. microservices" писал о поддержке модульности в Java 9, в Go уже пару лет есть поддержка internal-пакетов, в Erlang судя по статье на эту же тему Dawn of the Microlith — Monoservices with Elixir всё хорошо, …. Я не уверен, насколько на скриптовых языках возможно обеспечить реальную изоляцию модулей, но попытки есть: micromono на NodeJS, в комментарии lega ссылка на подход для Python, …


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

@powerman
25.10.2017 05:55 UTC
Первоисточник