Microservices. Как правильно делать и когда применять?



    Автор: Вячеслав Михайлов



    Монолитные приложения и их проблемы



    Все прекрасно знают, что такое монолитное приложение: все мы делали такие двух- или трехслойные приложения с классической архитектурой:





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





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



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


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




    Развитие системы



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



    Три измерения масштабирования



    В книге “The Art of Scalability” есть понятие «куб масштабирования» (scale cube) — из книги “The Art of Scalability”. По этому кубу мы видим, что существует три ортогональных способа увеличения производительности приложения: sharding, mirrorring и microservices.





    1. Sharding («шардинг», «разбиение») — расположение однотипных, но разных данных на разных узлах. Те, кто работал с NoSQL-базами, знают, что это такое. У вас есть ключ шардирования, по которому вы определяете, что у вас, например, данные на А и Б хранятся на одном узле, на В и Г — на другом узле и т. д. Таким образом, используя интеллектуальный выравниватель нагрузки, вы можете ее распределять по вашей системе и добиться более высокой производительности.
    2. Mirroring («зеркалирование») — горизонтальное дублирование или клонирование всех данных, когда вы ставите рядом совершенно одинаковые хосты. Таким образом вы полностью копируете данные. Нужно это, прежде всего, чтобы система отвечала на запросы с каким-либо ожидаемым временем отклика.
    3. Microservices (микросервисы) — вы разбиваете функциональность по бизнес-задачам. Каждый сервис будет выполнять определенные задачи. Это и есть микросервисный подход, который мы здесь разберем.


    Теорема CAP



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



    • Как сделать систему доступной из разных регионов?
    • При условии, что система распределена, как обеспечить согласованность данных?
    • И как при этом еще и ускорить систему в N раз?


    Эти вопросы приводят нас к CAP-теореме, сформулированной Брюером в 2000 г.





    Теорема это состоит в том, что вы, теоретически, не можете обеспечить системе одновременно и согласованность (consistency), и доступность (availability), и разделяемость (partitioning). Поэтому приходится жертвовать одним из трех свойств в пользу двух других. Так же, как при выборе из «быстро, дешево и качественно» приходится выбирать только два варианта. Теперь рассмотрим разные варианты, которые у нас есть согласно теореме CAP.



    CA — consistency + availability



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



    Яркий пример CA — ACID-транзакции, присутствующие в классических монолитах.



    CP – consistency + partitioning



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



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



    AP — availability + partitioning



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



    Яркий пример — классические DNS-системы, которые синхронизируются с задержкой до дней (во всяком случае, раньше).



    Теперь, ознакомившись с теорией CAP, мы понимаем, как можем развивать систему так, чтобы она была быстрой, доступной и распределенной. Да никак! Придется выбрать только два свойства из трех.



    Микросервисная архитектура



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



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



    Microservices & SOA



    А теперь поговорим еще немного о теории. Вы все прекрасно знаете, что такое SOA — сервисно-ориентированная архитектура. И тут у вас наверняка возникнет вопрос, как же SOA соотносится с микросервисной архитектурой? Ведь, казалось бы, SOA — то же самое, о это не совсем так. На самом деле, микросервисная архитектура — частный случай SOA:





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



    Что такое микросервисы?



    Это архитектурный шаблон, в котором сервисы:



    1. маленькие (small),
    2. сфокусированные (focused),
    3. слабосвязанные (loosely coupled),
    4. высокосогласованные (highly cohesive).


    Теперь разберем эти понятия по отдельности.



    Что значит «маленький» сервис? Это значит, что сервис в микросервисной архитектуре не может разрабатываться больше чем одной командой. Обычно одна команда разрабатывает где-то 5 – 6 сервисов. При это каждый сервис решает одну бизнес-задачу, и его способен понять один человек. Если же не способен, сервис пора пилить. Потому что, если один человек способен поддерживать всю бизнес-логику одного сервиса, он построит действительно эффективное решение. Ведь бывает так, что зачастую люди, принимая решения в процессе написания кода, просто-напросто не понимают, что именно делают — не знают, как ведет себя система в целом. А если сервис маленький, все намного проще. Этот подход, кстати, мы можем применять отдельно, даже не следуя микросервисной архитектуре в целом.



    Что значит «сфокусированный» сервис? Это значит, что сервис решает только одну бизнес-задачу, и решает ее хорошо. Такой сервис имеет смысл в отрыве от остальных сервисов. Другими словами, вы его можете выложить в интернет, дописав security-обертку, и он будет приносить людям пользу.



    Что такое «слабосвязанный» сервис? Это когда изменение одного сервиса не требует изменений в другом. Вы связаны посредством интерфейсов, у вас есть решение через DI и IoC — это сейчас стандартная практика, применять которую нужно обязательно. Обычно разработчики знают, почему :)



    Что такое «высокосогласованный» сервис? Это значит, что класс или компонент содержит все нужные методы решения поставленной задачи. Однако тут часто возникает вопрос, чем высокая согласованность (high cohesion) отличается от SRP? Допустим, у нас есть класс, отвечающий за управление кухней. В случае SRP такой класс работает только с кухней и больше ни с чем, но при этом он может содержать не все методы по управлению кухней. В случае же высокой согласованности, все методы по управлению кухней содержатся только в этом классе, и больше нигде. Это важное различие.



    Характеристики микросервисов



    • Разделение на компоненты (сервисы).
    • Группировка по бизнес-задачам.
    • Сервисы имеют бизнес-смысл.
    • Умные сервисы и простые коммуникации.
    • Децентрализованное управление.
    • Децентрализованное управление данными.
    • Автоматизация развертывания и мониторинга.
    • Design for failure (Chaos Monkey).


    Разделение на компоненты (сервисы)



    Компоненты бывают двух видов: библиотеки и сервисы, которые взаимодействуют по сети. Мартин Фаулер определяет компоненты как независимо заменяемые и независимо развертываемые. Т. е., если вы можете взять что-то и спокойно заменить на новую версию, — это компонент. А если что-то связано с другим и их независимо заменить нельзя (нужно учитывать контракты, сборки, версии…) —- они вместе образуют один компонент. Если что-то нельзя развернуть независимо, и требуется логика откуда-то еще, это тоже не компонент.



    Группировка по бизнес-задачам (сервисы имеют бизнес-смысл)



    Вот стандартная компоновка монолита:





    Для повышения эффективности разработки вы также зачастую вынуждены делить по этим слоям и команды: есть команда, которая занимается UI, есть команда, которая занимается ядром, и есть команда, которая разбирается в БД.



    Если же вы переходите к микросервисной архитектуре, сервисы и команды делятся по бизнес-задачам:





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



    Умные сервисы и простые коммуникации



    Есть разные варианты взаимодействия сервисов. Бывает, что берут очень умную шину, которая знает и про роутинг, и про бизнес-правила (допустим, какой-нибудь BizTalk), и к сервисам прилетают уже готовые объекты. Тогда получается очень умный middleware и глупые endpoint’ы. Это, на самом деле, — антишаблон. Как показало время (на примере того же интернета), у нас очень простая и незатейливая среда передачи данных — ей абсолютно все равно, что вы передаете, она ничего не знает про ваш бизнес. Все мозги же сидят в сервисах. Это важно понимать. Если же вы будете все складывать в среду передачи, у вас получится умный монолит и тупые сервисы-обертки баз данных.



    Децентрализованное хранение



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





    Единственный вариант взаимодействия — сетевое взаимодействие между сервисами:





    Middleware здесь может быть разный — мы об этом еще поговорим. Обнаружение сервисов и взаимодействие между ними может происходить просто напрямую, через вызов RPC, а может и через какой-нибудь ESB.



    Автоматизация развертывания и мониторинга



    Автоматизация развертывания и мониторинга — то, без чего к микросервисной архитектуре лучше даже не подходить. Т. ч. вы должны быть готовы в это инвестировать и нанять DevOps-инженера. Вам обязательно понадобится автоматическое развертывание, непрерывная интеграция и поставка. Также вам понадобится непрерывный мониторинг, иначе вы просто не сможете уследить за всеми многочисленными сервисами, и все превратится в какой-то ад. Здесь полезно использовать всякие полезные штуки, которые помогают централизовать логгирование, — их можно не писать, т. к. есть хорошие готовые решения вроде ELK или Amazon CloudWatch.



    Design for Failure (Chaos Monkey)



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



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



    Как сервисы будут друг с другом взаимодействовать?



    Возьмем пример простого приложения. Картинки, приведенные ниже, я взял из блога Криса Ричардсона на NGINX — там детально рассказывается, что такое микросервисы.



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





    Казалось бы, все просто — клиент может обращаться ко всем этим сервисам. Но на деле это выливается в то, что конфигурация клиента становится очень большой. Поэтому существует очень простой шаблон API Gateway:





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



    Разные типы микросервисной архитектуры



    Итак, допустим, у нас есть UI, API Gateway и десяток сервисов за ним, но этого мало — так нормальное приложение не построишь. Ведь обычно сервисы как-то взаимосвязаны. Я вижу три способа связать сервисы:



    • Service Discovery (RPC Style) — сервисы знают друг о друге и общаются напрямую.
    • Message Bus (Event-driven) — если вы используете шаблон «издатель —подписчик», и ни «подписчик» не знает тех, кто на него подписан, ни «издатель» не знает, откуда приходит содержимое. Они заинтересованы только в содержимом определенного типа — они подписываются на сообщения. Это и называется message-driven- или event-driven-архитектура.
    • Hybrid — смешанный вариант, когда для одних случаев мы применяем RPC, а для других — message bus.


    Service Discovery



    Service Discovery (RPC Style)



    Вот простейший вариант Service Discovery:





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



    Server-Side Service Discovery



    При Server-Side Service Discovery ваш клиент взаимодействует не напрямую с конкретным сервисом, а с выравнивателем нагрузки (load balancer):





    Load balancer существует очень много: они есть у Amazon, у Azure и т. д. Load balancer на основании собственных правил решает, кому отдать вызов, если сервисов больше одного и неясно, где находится сервис.



    Тут есть еще один дополнительный сервис, service registry, который также бывает разных типов — в зависимости от того, кто, как и где у него регистрируется. Можно сделать так, чтобы service registry регистрировал все типы сервисов. Load balancer берет все данные у service registry. Таким образом, задача load balancer — просто брать данные о местоположении сервисов из service registry и раскидывать запросы к ним. А задача service registry — хранить регистрационные данные сервисов, и он это делает по-разному: может опрашивать сервисы сам, брать данные из внешнего конфига и т. д. Пространство для маневров здесь видится очень широким.



    Client-Side Service Discovery



    Client-Side Service Discovery — другой, радикально отличающийся способ взаимодействия.





    Здесь нет load balancer, и сервис обращается напрямую к service registry, откуда берет адрес сервиса. Чем это лучше? Тем, что на один запрос меньше — так все работает быстрее. Этот подход лучше предыдущего — но при условии, что у вас доверительная система, в которой клиент — внутренний и не будет использовать информацию, которую принимает от сервисов, во вред (например, для DDoS).



    В целом в Service Discovery все достаточно просто — используются достаточно известные технологии. Однако тут возникает сложность — как реализовать более-менее серьезный бизнес-процесс? Все равно сервисы при таком подходе слишком сильно связаны, хотя проблему с точки зрения разворачивания и масштабирования мы решаем. Service instance A знает, что за данными нужно идти к service instance B, а если завтра у вас поменяется половина приложения и функцию service instance B будет выполнять другой сервис, вам придется многое переписывать.



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



    Message Bus



    Message Bus нужно уметь готовить и нужно действительно знать, что это такое. Message Bus используется для вполне определенных задач, например, не надо делать по Message Bus запросы request–reply или передавать большие объемы данных. Message Bus (и в принципе паттерн Publish/Subscribe) разрывает поставщиков и потребителей информации: поставщики не знают, кому нужна информация, а потребители не знают, откуда она берется — у одной информации теоретически могут быть разные поставщики и потребители.



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



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



    Message Bus: «за» и «против»





    Достоинства Message Bus:



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


    Недостатки Message Bus:



    • Т. к. Message Bus диктует архитектуру, он диктует и контракты: вы обязаны описывать message-контракты, которые будете использовать. Поэтому контракты менять сложно, их нужно версионировать. Можно использовать различные механизмы расширяемых контрактов вроде ProtoBuf, который позволяет это делать удобно — расширенные сообщения читаются и предыдущими версиями за счет удобного формата.
    • Обычно асинхронные взаимодействия. Чтобы правильно с ними работать,
    • нужно иметь хорошую квалификацию.
    • Вы добавляете еще один элемент в инфраструктуру развертывания — так появляется еще одна зона риска. Тут нужны особые знания у DevOps-инженера.


    Event Driven Architecture — архитектура, управляемая событиями



    Когда наши сервисы взаимодействуют в стиле RPC, все понятно: у нас есть сервис, который связывает всю бизнес-логику, собирает данные из других сервисов и возвращает их, но что делать в случае архитектуры, управляемой событиями? Мы не знаем, куда идти, — у нас есть только сообщения.



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





    У этой проблемы есть два решения.



    Решение 1



    Когда вы инициируете процесс создания заказа, посылаете в шину сообщение о создании сущности:





    Сервис, который заинтересован в этих событиях, подписывается на них и получает идентификацию:





    Затем он выполняет какое-то внутреннее действие и возвращает ответ, который потом прилетает в сервис заказов, в шину:





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



    Если стандартная модель называется ACID, такая транзакционная модель называется BASE — Basically Available, Soft state, Eventual consistency, что расшифровывается примерно так: состояние, которое вы в итоге получаете, называется “soft state”, потому что вы не до конца уверены, что это состояние действительно актуально.



    Решение 2



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



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



    Есть еще и третий, очень серьезный подход — Event Sourcing. Это большая тема, требующая отдельного обсуждения. В Event Sourcing вы не храните состояния объектов — вы их строите в реальном времени, а храните только изменения объектов (фактически, намерения пользователей что-то поменять). Допустим, происходит что-то в UI, например, пользователь хочет сделать заказ. Тогда вы сохраняете не изменение заказа, не новое состояние, а отдельно сохраняете внешние запросы: от пользователя, от других сервисов и вообще откуда угодно. Зачем это нужно? Это нужно для ситуации компенсации — тогда вы можете откатить состояние системы назад и действовать иначе.



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



    Переход от монолита к микросервисам



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



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



    Когда вы разделяете монолит на сервисы, обращайте внимание, как у вас структурированы команды. Ведь есть эмпирический закон Конвея (Conway's law), который говорит, что структура вашего приложения повторяет структуру вашей организации. Если ваша организация построена на технологических иерархиях, построить микросервисную архитектуру будет очень трудно. Поэтому нужно выделить feature-команды, которые будут иметь все необходимые навыки, чтобы написать нужную логику от начала до конца.



    На самом деле, редко бывает, что у нас чистая микросервисная архитектура. Чаще всего мы имеем нечто среднее между монолитом и микросервисами. Обычно есть какой-то большой исторически сложившийся код, и мы понемногу стараемся распутывать его и отделять от него части.



    Если же мы делаем проект с нуля, нужно выбрать — монолит или микросервисы?



    Монолит лучше выбирать в следующих случаях:



    1. Если у вас новый домен и/или нет знаний в этом домене..
    2. Если вы делаете прототип или быстрое решение.
    3. Если команда не очень квалифицированная (все начинающие, например).
    4. Если вам требуется просто написать код и забыть о нем.
    5. Если мало денег на проект — микросервисы обойдутся дорого.


    Микросервисы лучше выбрать, если:



    1. Точно понадобится линейное масштабирование.
    2. Вы понимаете бизнес-домен, сможете выделить ограниченный контекст и сможете обеспечить согласованность на бизнес-уровне.
    3. Команда высококвалифицированная, есть опыт и пара загубленных проектов с микросервисами в прошлом (все равно с первого раза сделать микросервисы не выходит).
    4. Предстоит долгосрочное сотрудничество с заказчиком.
    5. Достаточно средств для инвестирования в инфраструктуру.


    Микросервисы: «за» и «против»



    Преимущества:



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



    1. Четкое модульное деление. Позволит усилить модульную структуру — в вашей команде будут люди, которые прекрасно знают, как работает та или иная часть кода. Это особенно важно для больших команд разработчиков.
    2. Высокая доступность. Сервисы могут работать не все — при этом все остальное будет работать.
    3. Разнообразие технологий, или возможность использовать правильный инструмент. Например, если нужно построить хранилище данных, вы подберете тот инструмент, который действительно умеет хранить большие объемы данных и быстро их выбирать. Микросервисы позволяют даже просто опробовать технологию на каком-нибудь сервисе, и это не повлияет на другие сервисы, т. к. контракт изолирован через сетевое взаимодействие.
    4. Независимое развертывание из-за слабой связанности сервисов: простые сервисы проще разворачивать, и меньше вероятность отказа системы.


    Недостатки:



    1. Сложность разработки.
    2. Конечная согласованность — бизнес вашего заказчика должен позволять работать с отложенными данными. Этим придется платить за высокую доступность. Классический пример конечной согласованности — банковские карты, транзакции между которыми могут занимать дня три. Из-за этого есть вероятность превышения кредитного лимита, и тогда начинает действовать простейший механизм компенсации — вам звонят из банка с просьбой погасить превышение.
    3. Сложность операционной поддержки — нужны грамотные DevOps-инженеры, непрерывное развертывание и автоматический мониторинг. Без всего этого микросервисы использовать не следует.


    Немного о мониторинге и тестировании мироксервисной системы



    Есть инструменты, которые позволяют развертывать такую систему и следить за ней, например, ZooKeeper, который решает проблему конфигурирования за нас. Также есть такие инструменты типа Logstash, Kibana, Elastic, Serilog, Amazon Cloud Watch. Все они следят за вашими сервисами.



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



    Манифест Джеффа Безоса (Amazon CEO)



    Напоследок приведу хороший пример крайне успешного применения микросервисной архитектуры — Amazon.



    Все началось с того, что в 2000 г. Джефф Безос, глава Amazon, написал для всех отделов своей компании следующий манифест:



    • All teams will henceforth expose their data and functionality through service interfaces.
    • Teams must communicate with each other through these interfaces.
      • no direct linking
      • no direct reads of another team’s data store
      • no shared-memory model
      • no back-doors whatsoever.

    • The only communication allowed is via service interface calls over the network.
    • It doesn’t matter what technology they use.
    • All service interfaces, without exception, must be designed from the ground up to be externalizable.
    • No exceptions.


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



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



    Источники


    • 0
    • 32,7k
    • 6
    DataArt
    122,00
    Технологический консалтинг и разработка ПО
    Поделиться публикацией

    Похожие публикации

    Комментарии 6

      0
      Добрый день, позвольте вопрос.

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


      Как вы решаете эту проблему, сажаете какой то timeout?
        0
        В принципе, для всего AMQP-based семейства подходы примерно похожи. Вот пример по надежной доставке в RabbitMQ – надеюсь, будет полезно.
          0
          Извините, не понял ответ. Как связан message broker с возможностью не доставки ответа и тем как это должен разруливать сервис? Если ответ не придет, т.к. сервис например упал во время подготовки ответа, то как должен клиент разруливать эту ситуацию?
            0
            Здесь – «клиентом» вы называете отправителя, а «сервисом» получателя, я буду использовать соответственно Consumer и Producer, чтобы окончательно всех запутать.

            Как связан message broker с возможностью не доставки ответа и тем как это должен разруливать сервис?

            Ну очевидно же :) Если брокер падает, то все переворачивается мехом наружу. Парочка примеров:

            1. Producer отправил сообщение, а на брокере очередь не durable. Брокер лёг. Сообщение потерялось до отправки. Ответ не пришел.

            2. Более интересный случай. Допустим, producer отправил сообщение, на брокере очередь durable, все хорошо. Брокер отправил сообщение. Consumer начал долгую обработку, ACK еще не отправил, и в этот момент брокер падает… Дальше возможны варианты по количеству (нет ответа, один ответ, два ответа) и правильности, потому что Consumer в любом случае получит два одинаковых сообщения. Второе он может не принимать к обработке, если это особо предусмотрено :)

            Если ответ не придет, т.к. сервис например упал во время подготовки ответа, то как должен клиент разруливать эту ситуацию?

            Минимально – нужно выставить таймаут на Queue, при необходимости переопределить таймаут на Message, и обработать таймаут на Producer.
              0
              Спасибо за ответ. Как работает Message brocker я знаю.

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

              Спасибо еще раз.
        0
        deleted

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

        Самое читаемое