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

Часть 1: теория. Что такое микросервисная архитектура

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

В какой-то момент объём кода вырос до полного собрания сочинений Толстого. Как следствие – обновления разрабатываются все дольше, а тестировать их все сложнее. Это стандартная проблема для сложного программного обеспечения. Решить проблему узких мест на проекте можно используя микросервисы: везде, где есть «тяжелые» процессы, теоретически их можно вынести в специальное окружение.

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

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

В чём проблема монолитной архитектуры?

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

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

  • Чтобы внести одно изменение, необходимо пересобрать всё приложение.
  • Невозможно масштабировать отдельный модуль – придётся переделывать всё приложение.
  • Разработка ограничена изначально выбранным набором языков программирования, фреймворков и других инструментов. Хочется использовать что-то альтернативное - извините, у нас такого нет, работайте с тем, что есть.

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

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

Чем хороша микросервисная архитектура?

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

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

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

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

Часть 2: практика. Микросервисы в заказной разработке

Работать с микросервисами мы умеем уже очень давно. Году так в 2014 мы разрабатывали на Symfony логистический проект – расчет маршрутов доставки. Когда пользователь выбирает, например, доставку из Копенгагена в Токио, и ему предлагается несколько маршрутов по разным ценам.

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

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

Пользователь выбирал город. Задача попадала на сервер очередей. Демоны забирали эту задачу и рассчитывали каждый свой маршрут. То, что в один поток рассчитывалось бы 5-10 минут, мы получали в среднем за 15 секунд за счет параллельного расчета. Для сегодняшних реалий это очень много, но на момент разработки проекта – это был прорыв.

Микросервисы удобно реализуются при разработке на фреймворках – мы любим Laravel, Symfony – тоже ок. А вот для сайтов на Битрикс – все не так просто. Интернет-магазин нашего постоянного клиента Ormatek работает именно на Битриксе. На начало 2021 года там было около 200 тысяч товарных предложений.

Товарное предложение (или SKU) – это вариант товара. Например, кровать модели Dario Classic. Чтобы её купить, надо выбрать размер, основание и вариант исполнения (цвет и материал обивки). Товарное предложение – это кровать, для которой уже выбраны все доступные параметры.

Может показаться странным, что у магазина матрасов и мебели такой объемный каталог. Но каждая кровать может быть представлена в десятке размеров. С десятком вариантов оснований. А еще в 40+ вариантах исполнения. Если всё это перемножить – получается 4000+ вариантов на одну кровать. Очень серьезно.

Такой объем каталога создавал две проблемы:

  • Во-первых, импорт товаров на сайт из 1С занимал до 8 часов.
  • Во-вторых, каталог местами работал, мягко говоря, не спеша.

Мы вынесли поиск, фильтрацию, сортировки и импорт в микросервисы. Для этого внедрили поисковую систему Elasticsearch. При старой архитектуре (использовался штатный модуль поиска 1С-Битрикс) поиск занимал около 10 секунд, что для обычного пользователя слишком много.

При поиске на такой объемной базе товаров Битрикс генерирует очень тяжелые запросы, которые нельзя поменять. Такие запросы получаются, например, когда Битриксу надо сформировать страницу списка кроватей из 120 тысяч SKU. Или при фильтрации, когда по свойствам надо сформировать и вывести нужные карточки товаров. Помимо свойств товаров, запрос должен учитывать ещё и их изображения. Какую именно картинку для товара выводить, заказчик задает через отдельное свойство-флаг.

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

Итак, мы решили перенести сортировку и фильтры в Elasticsearch. Если очень примитивно и схематично, то вот что у нас происходит теперь:

  • есть MySQL (база данных сайта), в нём по прежнему хранятся SKU,
  • есть Elasticsearch, в него мы тоже поместили все SKU,
  • когда нам нужны определенные SKU с определенными свойствами, мы эти параметры передаем в Elasticsearch и за 0,5 сек. получаем не сами SKU, а список их id-шников. Эти id мы отдаем в MySQL и говорим: «Выведи их на страницу».

В рамках оптимизации мы написали каждому параметру фильтрации/сортировки во всех разделах товаров свой запрос в Elastic. Суммарно, страницы списка товаров, в том числе кроватей, стали загружаться менее, чем за 3 секунды.

Микросервисы в продуктовой разработке

У нас есть собственный продукт, разработкой которого занимается отдельная команда – SingularityApp, планировщик дел и задач. Десктоп, iOS, Android и PWA-версия. Он состоит из множества различных решений для пользователей, которых становится все больше – новые фичи добавляются несколько раз в месяц.

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

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

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

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

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

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

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

По сути докер – это виртуальный сервер. Можно создать в рамках одного физического сервера (компьютера) много виртуальных серверов и разделить ресурсы между ними так, как надо нам. А потом разместить микросервисы в докерах, чтобы они не толкались и не дрались друг с другом за ресурсы.

В качестве бонуса – в каждом докере можно использовать свое программное обеспечение. Например, у нас есть сервер разработки – dev. Там докеры используются, в том числе, для сайтов с разными версиями PHP, чтобы ничего не конфликтовало.

Еще добавилась такая такая штука, как балансировщик нагрузки (мы используем Docker Swarm). Это, по сути, система-оркестратор, которая позволяет управлять всем зоопарком микросервисов и гибко перераспределять ресурсы между ними.

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

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

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

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

Сначала мы оставили одновременно два сервера SingularityApp: новые устройства пользователей автоматически заходили на микросервисный сервер, а старые – на монолитный. Никакой практической разницы для пользователей не было, приложение на обоих серверах работало одинаково.

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

0
14 комментариев
Написать комментарий...
Evgeny Afanasev

2021 год - рассказываем преимущества микросервисов

P.S. Все уже с ними поигрались, споткнулись десятки раз и снова возвращаются к монолитам там где надо, а не как раньше лендинг на 100 сервисов (утрирую).

P.S. видимо люди на VC пишут, когда для хабра слишком глупы

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

Ответить
Развернуть ветку
Дмитрий Козлов

2024 на курсах продолжают рассказывать а на собесах требовать.
Между тем, стоит сходить почитать доклады с хайлоад ру и волосы начнут шевелиться на разных местах - как многие жалеют что они перешли на микросервисы.
Тем более, что вертикальное масштабирование сейчас хоть до 200 потоков одновременных.
Если убогость архитектора не позволяет смотреть на бизнес процесс в целом, то да, лучше разбиться на команды, каждый делает свой кусочек. И всем молиться.

Ответить
Развернуть ветку
Ivan Priorov

Позвольте полюбопытствовать, а по сравнению с какими протоколами Http вдруг стал "лёгким"?

Ответить
Развернуть ветку
Anna Kozhevina

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

“Большое делят на малые части не как удобно, а как оно лучше делится само. А если нет необходимости, то и не разделяют вовсе. Разделенное без нужды соединить бывает труднее, чем разделенное от нужды, когда нужда проходит.” — Владимир Тарасов.

Ответить
Развернуть ветку
Alexander D

Более 95% веб приложухам микросервисная архитектура не нужна от слова совсем.

Ответить
Развернуть ветку
Victor Pomortseff
 Сервисы связываются между собой и с клиентами с использованием лёгких протоколов, например, HTTP. В результате...

Каждый сервис тратит процессорное время на кодирование-декодирование данных. Это вроде бы мизер в рамках одного сервисе, но когда их становится слишком много...

есть MySQL (база данных сайта), в нём по прежнему хранятся SKU,

есть Elasticsearch, в него мы тоже поместили все SKU, когда нам нужны определенные SKU с определенными свойствами, мы эти параметры передаем в Elasticsearch и за 0,5 сек. получаем не сами SKU, а список их id-шников. Эти id мы отдаем в MySQL и говорим: «Выведи их на страницу».

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

Я не спорю, что микросервисы где-то и в чем-то хороши, но это не абсолютно. Честно говоря, я как-то затруднюсь себе представить более-менее крупную 100% монолитную систему. Это вообще как? Одна программа, которая делает все-все-все? Такое вообще бывает?

Зато могу себе представить (ибо сам работаю с такой) достаточно крупную систему, работающую на многопользовательской платформе, где каждый процесс выполняется в отдельном задании (job). Задания полностью изолированы друг от друга - в каждом может быть свое окружение построено. У каждого своя память (в нашей системе каждому заданию выделяется 16Гб памяти). Полное падение одного задания никак не влияет на все остальные. Общаться между собой задания могут любыми доступными средствами - сокеты, пайпы, очереди (не те, которые кафки различные, а системные, где они есть, очереди сообщений, очереди данных...). И каждый процесс в своем задании выполняет свою часть общей работы. Одновременно может крутиться тысячи и более различных заданий.

Хотите распараллелить обработку? Да не вопрос. Пишем классическую батчмашину, где родительское задание запускает сколько нужно обработчиков (каждый в своем отдельном фоновом задании), создает конвейер (ту же "очередь данных" - есть у нас такой тип объекта в системе), а дальше делает выборку нужных для обработки данных, формирует их в пакеты и выкладывает на конвейер. Обработчики оттуда подхватывают и делают что нужно.

Это микросервисы? Если да, то "изобрели" их очень давно. Еще во времена System/36..38 и всяких разных ЕС ЭВМ и БЭСМ. Если нет, то что это? Уж точно не монолит...

Ответить
Развернуть ветку
Аккаунт удален

Комментарий недоступен

Ответить
Развернуть ветку
Victor Pomortseff

Помилуйте, сударь, я старый и немодный и слов таких не знаю :-)
Мне бы как-нибудь на пальцах...

Ответить
Развернуть ветку
Аккаунт удален

Комментарий недоступен

Ответить
Развернуть ветку
Victor Pomortseff

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

Т.е. вроде как и не микросервисы в прямом смысле, но и ни в коем случае не монолит.

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

Ответить
Развернуть ветку
Дмитрий Козлов

лучше обычные сервисы в монолите
изолированные, стабильные, параллельные

лично у меня сеть падала в разы чаще чем приложение

единственное, где горизонтальное масштабирование потребно это БД

Ответить
Развернуть ветку
Mur K

Ну круто. видимо, что вы дошли до MSA в конце 2021 года=) Чувствуется гордость отдельно взятых экспертов/менеджеров, которые изучили, научились (?) использовать подход и внедрили его во всё куда только можно.
Если подход решил ваши проблемы - ну молодцы, почему только сразу не продумали архитектуру..
PS по поводу расчета маршрутов.. прям вот только в микросеврсиах смогли повычить производительнсть? Прям как-то крупными мазками дивагаетесь, попробовали бы потоки например, serverless функции и тп.. или map-reduce или добавили горизонт-масштабирование и все полетело? 

Ответить
Развернуть ветку
Андрей Китаев

Абзац про докеры получился так себе)

Ответить
Развернуть ветку
Дмитрий Козлов

псс... а параллельное программирование не пробовали прежде чем переходить к микросервисам?
а еще уйти с ПХП на Си шарп, например

Ответить
Развернуть ветку
11 комментариев
Раскрывать всегда