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


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


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



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


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


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


  • Должен иметь тщательно спроектированное и документированное API, и только оно должно быть доступно другому коду, выполняющемся в этом же приложении (контроль этого ограничения требует поддержки на уровне языка и/или инструментов разработки).
  • Должен выполняться независимо от кода других модулей и иметь возможность эффективно взаимодействовать с API других модулей без использования сетевых протоколов (это требует поддержки на уровне языка: нити/горутины/акторы, каналы/сообщения, etc.).
  • Должен использовать собственное хранилище данных (если оно ему нужно).
  • Может находиться в отдельном репо или монорепо приложения.
  • Может использоваться разными приложениями.
  • Может предоставлять доступ к своему API по сети (только модулю это делать не обязательно, да и доступ может быть не полным, как внутри приложения, а частичным).

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


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

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


  • Не должен разделять с использующим его API кодом никаких общих данных — все данные должны передаваться через API либо в виде копии, либо, если данных очень много и к ним нужен доступ только на чтение, в виде неизменяемой (immutable) структуры данных (требует поддержки на уровне языка).
  • Не предоставляет функций, которые могут вызывать другие модули — т.е. не выполняет никакого своего кода в чужом потоке выполнения (нити, горутине, etc.), таким образом изолируя от вызывающего кода даже исключения, которые могут возникать в коде модуля.
    • У модуля есть публичная функция инициализации и запуска модуля, которая вызывается при запуске приложения, но она не предназначена для (повторного) вызова из других модулей.
  • Имеет собственные, полностью изолированные от других модулей:
    • Конфигурацию (передаваемую ему при запуске приложения).
      • Она может включать общие для всех модулей настройки логирования.
    • Хранилище данных (если оно ему нужно).
      • Поддержка версионирования схемы этих данных и миграций при обновлениях — так же ответственность модуля, хотя запуск миграций данных каждого модуля может быть частью общего для всех модулей процесса деплоя приложения.

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


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


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


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


  • Необходимость разделять идемпотентные и не идемпотентные запросы.
  • Для всех запросов на клиенте:
    • Необходимость использовать таймауты (которые могут быть разными для разных запросов).
    • Необходимость использовать больше асинхронности для ускорения работы.
    • Больше ситуаций eventual consistency и вызванных этим сложностей.
      • В т.ч. необходимость иногда кешировать данные других сервисов.
    • Необходимость повторять некоторые запросы:
      • Используя ограничение по количеству или времени повторов.
      • Используя задержки между повторами.
  • Дополнительно для не идемпотентных запросов на клиенте:
    • Необходимость реализовать возможность определения дубликатов при повторе запросов.
  • Дополнительно для не идемпотентных запросов на сервере:
    • Необходимость определять и обрабатывать дубликаты запросов (по-разному, в зависимости от бизнес-логики каждого запроса).
  • В некоторых случаях необходимость выполнять запросы в том же порядке, в
    котором они были отправлены.

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


  • Падение модуля приводит к падению всего приложения.
  • Уменьшается скорость сборки и тестирования.
    • Не уверен, что скорость сборки это реальная проблема — есть много способов её ускорения.
    • В принципе возможно при тестировании ветки/PR запускать только тесты изменившегося модуля, а тесты всего проекта выполнять только перед деплоем — это в достаточной степени должно нивелировать эту проблему.
  • Уменьшается скорость запуска (разогрев кешей, etc.).
    • Здесь проблема не столько в реальном замедлении, сколько в том, что когда сервисов много время запуска "размазано" по нескольким сервисам, которые (пере)запускаются независимо друг от друга. Тем не менее, видимый эффект более медленного запуска этот факт не отменяет.

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


  • Увеличивается скорость взаимодействия между модулями-сервисами, что и позволяет значительно уменьшить необходимость в асинхронности, eventual consistency и кешировании.
  • Пока у модуля нет внешнего (сетевого) API его API намного проще изменять и деплоить эти изменения атомарно с деплоем использующих этот модуль клиентов (других модулей этого же приложения).
  • Совместную работу модулей проще тестировать, чем группу микросервисов.
  • Проще использовать монорепо (если хочется), хоть это и не обязательно.
    • Монорепо упрощает рефакторинг и изменение API, по крайней мере пока есть гарантия что у модуля нет сетевого интерфейса и внешних клиентов.

Резюме


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


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


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

Комментарии (86)


  1. poxvuibr
    25.10.2017 08:44

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


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

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


    1. mayorovp
      25.10.2017 09:52

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


      1. powerman Автор
        25.10.2017 14:30

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


        1. mayorovp
          25.10.2017 14:33

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


          1. powerman Автор
            25.10.2017 15:32

            Зависит от языка. Например, на 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
            }

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


            1. mayorovp
              25.10.2017 15:46

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


              1. powerman Автор
                25.10.2017 16:17

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


        1. poxvuibr
          25.10.2017 14:39

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


          1. powerman Автор
            25.10.2017 15:43

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


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


            1. poxvuibr
              25.10.2017 16:14

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


              1. powerman Автор
                25.10.2017 16:19

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


                1. poxvuibr
                  25.10.2017 17:37

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


                  1. powerman Автор
                    25.10.2017 17:48

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


  1. napa3um
    25.10.2017 09:22

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


    1. powerman Автор
      25.10.2017 15:31
      +2

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


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


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


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


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


      1. napa3um
        25.10.2017 15:43

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

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


      1. mayorovp
        25.10.2017 15:45

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


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


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


  1. aamonster
    25.10.2017 09:32

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


    1. saboteur_kiev
      25.10.2017 10:38

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


      1. napa3um
        25.10.2017 12:31

        Модуль — единица сборки компонента, микросервис — тип компонента.


    1. powerman Автор
      25.10.2017 15:46
      +1

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


      1. aamonster
        25.10.2017 17:09
        +1

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


  1. marshinov
    25.10.2017 10:16

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


    1. heleo
      25.10.2017 10:23

      А для модулей используете общие интерфейсы доступа?


      1. marshinov
        25.10.2017 11:22

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


    1. napa3um
      25.10.2017 11:16

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


      1. marshinov
        25.10.2017 11:26

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

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


        1. napa3um
          25.10.2017 11:27

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


  1. heleo
    25.10.2017 10:21

    Как-то сумбурно написано. При этом негусто с описанием получаемых недостатков при использовании модулей.

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


    1. powerman Автор
      25.10.2017 15:55

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


      1. heleo
        25.10.2017 16:09

        А можно поконкретнее где указали? Я просто явно это не увидел и повторно при беглом прочтении не нашёл.


        1. powerman Автор
          25.10.2017 16:20

          • Уменьшается скорость запуска (разогрев кешей, etc.).


          1. heleo
            25.10.2017 17:13

            Тогда стоит писать «Увеличение времени». Вы всё же не мотор меняете на более дохлый, а добавляете объём выполняемых действий и на скорость это ни как не сказывается.


  1. lega
    25.10.2017 11:55

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

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


    1. powerman Автор
      25.10.2017 16:10

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


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


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


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


      1. lega
        25.10.2017 21:47

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

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

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

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

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

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

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


  1. nApoBo3
    25.10.2017 13:56
    -1

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


    1. kozzztik
      25.10.2017 15:59

      Судя по всему автор пытался донести мысль, что грамотное разбитие проекта на умеренно изолированные модули в рамках монолита может оказаться выгоднее чем фигачить микросервисы.
      Мысль, казалось бы, более чем очевидная. Но практика говорит что нет )


      1. powerman Автор
        25.10.2017 16:16

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


        1. nApoBo3
          25.10.2017 18:36

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


          1. powerman Автор
            25.10.2017 18:44
            +2

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


            1. nApoBo3
              25.10.2017 21:38

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


              1. mayorovp
                25.10.2017 21:41
                +1

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


                1. napa3um
                  25.10.2017 21:45
                  -1

                  А как возникла эта проблема? Запилили сервисы ради сервисов?


                  1. lega
                    25.10.2017 21:54
                    +1

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


                    1. napa3um
                      25.10.2017 21:57

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


                    1. nApoBo3
                      25.10.2017 21:59

                      У монолитов с действительно крупными проектами тоже не все радужно, одни мэйнфрэймы чего стоят.


                1. nApoBo3
                  25.10.2017 21:58

                  В чем именно заключается инфраструктурный и организационный оверхед?

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

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


                  1. mayorovp
                    25.10.2017 22:08

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


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


                    1. nApoBo3
                      25.10.2017 23:01

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


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

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


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

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


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

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


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


                      1. mayorovp
                        26.10.2017 06:26

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


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

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


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

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


                        1. nApoBo3
                          26.10.2017 11:30

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


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

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


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

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


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

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


                          1. mayorovp
                            26.10.2017 11:51

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

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


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

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


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

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


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

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


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


                            1. nApoBo3
                              27.10.2017 22:02
                              -1

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


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

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


                              1. mayorovp
                                27.10.2017 22:08

                                Честно, ни вижу ни одного плюса подобного похода

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


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

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


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

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


                              1. powerman Автор
                                27.10.2017 23:40
                                +2

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


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

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


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


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


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


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


                                1. nApoBo3
                                  28.10.2017 13:07
                                  -1

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


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

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

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


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

                                  Circuit Breaker


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

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


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

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


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

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

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


                                  1. mayorovp
                                    28.10.2017 14:16

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

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


                                  1. powerman Автор
                                    28.10.2017 17:51

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

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


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

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


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

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


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


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

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


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

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


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


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


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

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


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

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


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

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


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

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


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

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


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

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


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


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


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

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


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

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


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

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


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

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


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

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


        1. kozzztik
          25.10.2017 18:38
          +1

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


          1. powerman Автор
            25.10.2017 19:01

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


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


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


            1. kozzztik
              25.10.2017 19:14

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


              1. powerman Автор
                25.10.2017 20:34

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


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


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


                1. kozzztik
                  25.10.2017 20:56

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


                  1. powerman Автор
                    25.10.2017 21:26

                    То что к одному прикрутили, к другому забыли.

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


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

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


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

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


                    1. nApoBo3
                      25.10.2017 21:47

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


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


                      1. powerman Автор
                        25.10.2017 22:19

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


                        1. nApoBo3
                          25.10.2017 23:07

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


                          1. powerman Автор
                            26.10.2017 00:02

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


        1. sshikov
          26.10.2017 22:20

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


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


  1. nightwolf_du
    25.10.2017 16:25

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

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


    1. mayorovp
      25.10.2017 16:29

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


  1. rumkin
    25.10.2017 17:41

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


    1. powerman Автор
      25.10.2017 17:54

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


      1. nApoBo3
        25.10.2017 18:42

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


  1. sshikov
    25.10.2017 20:49

    Иначе получится монстр вроде OSGi.

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


    1. powerman Автор
      25.10.2017 21:35

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


      1. sshikov
        25.10.2017 22:30

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


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


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


        • на сотню сервисов как правило приходилось примерно 10-20 коннектов к 10-20 базам данных, которые мы настраиваем 10 или 20 раз, а не в каждом микросервисе, которому они понадобились. Тоже самое для JMS/QPID/ActiveMQ коннектов и других ресурсов.
        • общий JMX, где видна вся статистика (мониторинг).
        • общее управление, в виде веб или ssh консолей.
        • кластер практически задаром.


  1. RomanPokrovskij
    25.10.2017 21:33
    -1

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


    1. powerman Автор
      25.10.2017 22:14

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


      • программка на любом языке
      • запущенная под управлением какого-нибудь супервизора (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 — вероятно до этого были в основном эксперименты, а не применение в реальных проектах.


      1. RomanPokrovskij
        26.10.2017 14:41
        -1

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


        1. powerman Автор
          26.10.2017 16:08

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


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


          1. RomanPokrovskij
            26.10.2017 23:13

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

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

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


            1. powerman Автор
              27.10.2017 01:22

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


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


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

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


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

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


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

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


              1. RomanPokrovskij
                27.10.2017 09:19

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

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

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

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


    1. VolCh
      25.10.2017 22:55

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