Всегда приятно начинать новый проект. Простые классы, четкие границы, ясная архитектура — все логично и красиво. Новая функциональность добавляется легко и быстро.

Идет время, проект развивается, поступают новые требования. Но приходит день, когда вы обнаруживаете в коде что-то плохое. Кто-то срезал угол и сделал небольшой костыль. Бывает, что вы сами делаете что-то на скорую руку, честно вставляя в код “todo” — просто потому, что эта функциональность нужна для ближайшего релиза, а времени сделать все правильно нет. “Это технический долг, который мы обязательно исправим после очередного релиза, но сейчас надо выдать версию” — произносим мы при этом.

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

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

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

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

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

Очевидно, в развитии системы возникает момент, когда цена микросервисов окупается за счёт уменьшения издержек от снижения производительности команды при усложнении системы. Мартин Фаулер (Martin Fowler) хорошо проиллюстрировал этой в своей заметке Microservice Premium:

Также хорошо видно, что для проектов малой и средней сложности монолитный подход более выигрышен в плане производительности команды. А ведь это как раз тот этап, когда зачастую определяется, пойдёт ли проект в большую жизнь. Например, в случае стартапа или proof-of-concept проекта. Тратить на этом этапе ресурсы, забирая их у разработки фич может привести к тому, что эта большая жизнь и не придет. А если проект не выстрелит, то ресурсы просто будут выброшены.

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

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

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

 

Характеристика Модули  Мсервисы
Декомпозиция и строгие границы + +
Возможность наличия собственной БД + +
Простота рефакторинга и перенос границ +
Отсутствие накладных расходов на networking +
Отсутствие накладных расходов на инфраструктуру +
Типизация и проверка компилятором передаваемых данных +
Проверка компонентных зависимостей на этапе компиляции +
Поддержание схемы взаимодействия компонентов в явном виде +
Синхронное взаимодействие +
Возможность выполнения транзакций между компонентами +
Использование разных языков для разных компонентов +
Независимое развертывание / раздельные процессы (~отказоустойчивость)    +
Возможность независимого масштабирования компонентов +
Различный жизненный цикл для разных компонентов +


Итак, мы хотим разделить нашу системы на части, определив интерфейсы на границах, которые будут обеспечивать контракты взаимодействующих компонентов. В принципе, мы могли бы попробовать ввести соответствующее разделение на пакеты (java packages). Но быстро станет ясно, что это не поможет в соблюдении границ, так как достаточный уровень инкапсуляции при этом не обеспечивается. Всегда можно обратится к любому public классу в обход интерфейса. А с помощью reflection API можно получать доступ даже к private методам/полям. Это приводит к тому, что части приложения взаимодействуют уже не на основе контрактов, а на основе знания о внутреннем устройстве друг друга. При этом любое изменение, даже не меняющее контракт компонента, будет ломать все зависящие от него компоненты.

Как же обеспечить строгую инкапсуляцию? С одной стороны, для Java платформы уже давно существует индустриальный стандарт компонентной архитектуры приложения OSGi, который решает эту задачу и даже позволяет динамическую загрузку/выгрузку компонентов без перезапуска JVM. На его основе реализован ряд систем. Однако, за долгие 17 лет существования он не пошёл в массы, возможно по причине своей сложности, которая не позволяет кардинально снизить издержки по сравнению с теми же микросервисами.

Хорошая новость в том, что в рамках грядущей Java 9 запланирована реализация модульной системы на уровне JVM. Это, с одной стороны, позволит модуляризовать саму Java-платформу, а с другой — предоставит конструкцию, обеспечивающую строгое соблюдение границ модулей как при компиляции, так и во время исполнения.

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

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