В этой статье мы не откроем ничего нового об устройстве монолита и микросервисной архитектуры. Про это сказано немало слов, написано ещё больше. Мы расскажем о том, как через это прошла наша компания и какие преимущества и недостатки микросервисной архитектуры мы для себя обнаружили.
Как и многие другие, IVI начинал свой путь в разработке с монолита. Изначально сервис был b2b-решением, которое предоставляло фильмы для размещения на других площадках. Одной из таких площадок был и собственный сайт IVI, который работал с нашим монолитом. И этот монолит отвечал за раздачу информации о контенте, ссылки на контент, подбор рекламы, загрузку и кодирование новых фильмов.
В 2011 году мы поняли, что запросов к сервису становится много и необходимо масштабировать наше решение. Причём основную массу составляли запросы от пользователей к монолиту за ссылками на фильмы и за рекламой. И не было необходимости масштабировать всё остальное. Мы решили вынести минимальную функциональность в микросервис и сделать некий «лёгкий» сервис. Так мы воспользовались первым преимуществом микросервисной архитектуры — лёгкостью масштабирования микросервисов.
К тому же мы получили бонус в виде простоты разработки. Этот сервис был небольшого размера и отвечал за небольшой набор функций, поэтому новому разработчику было просто в нём разобраться. К тому же у сервиса был небольшой набор тестов, которые быстро выполнялись и можно было быстро запустить их все и проверить, что доработки ничего не сломали. Да, этот сервис был не в полном смысле микросервисом. Он лежал в том же репозитории, что и монолит, имел с ним общий код, при правке которого необходимо было проверять всю функциональность, и, как следствие, имел общие языковые зависимости с монолитом. Но это был первый шаг.
Затем мы решили вынести из монолита систему управления контентом. Тут уже использовалась независимая кодовая база, оставалась только связь со схемой базы данных и средствами описания этой схемы. И примерно в то же время мы начали показывать фильмы на различных устройствах, для которых потребовался некий программный интерфейс. Так родился сервис, отвечающий за интерфейс общения с мобильными приложениями (MAPI). Он был совсем независимым, со своей кодовой базой и хранилищем, которое специальные скрипты синхронизировали с основным хранилищем.
Тут мы столкнулись с другой проблемой, которую, в том числе, позволяют решать микросервисы, правда мы этим не воспользовались. Название этой проблеме — монорепозиторий. С ростом команды разработка в одном репозитории становилась проблемой. Росло количество тестов, и нам пришлось усложнять CI, чтобы распараллелить запуск тестов различных независимых частей репозитория. Но даже это не сильно спасало. Дело в том, что из-за активной разработки скапливалась очередь задач на слияние веток, и разрабатываемая тобой ветка могла быть слита только через пол дня. Если повезёт и кто-то другой не объединит раньше тебя ветку, которая ломает твои тесты.
Поэтому следующие наши микросервисы создавались уже в отдельных репозиториях. И тут мы начали осознавать другое преимущество микросервисов: на них можно пробовать новые технологии, новые языки программирования, новые СУБД. Мы уже не были привязаны к монолиту. Так, например, в нашу разработку ворвался язык Go.
Но в это же время мы столкнулись и со множеством недостатков микросервисов. Первым таким недостатком была сложность разработки. Да, выше мы писали, что микросервисы когда-то позволили нам упростить разработку, так как сервис, отвечающий за маленький кусочек бизнес логики, проще понять, доработать и протестировать. Но микросервисы не работают сами по себе. Они работают в связке с другими микросервисами. И чтобы доработать какой-то один, нужно развернуть локально несколько других. Да и тестировать тоже стало сложно, потому что было несколько тестовых стендов, на которые очень быстро выстраивалась очередь из желающих протестировать доработку в том или ином микросервисе. Для решения этой проблемы мы собрали систему, предоставляющую каждому разработчику или тестировщику свой кластер со всеми необходимыми микросервисами — свой маленький IVI. Эта система позволяла создать такой кластер и быстро развёртывать в нём нужные версии микросервисов. И разработчикам уже не нужно было развёртывать всё локально, достаточно было создать разрабатываемый сервис и ходить из него в свой виртуальный кластер.
Другой проблемой микросервисов стали распределённые транзакции. Так, например, на IVI есть процедура объединения аккаунтов. Когда пользователь открывает только что установленное приложение, у него уже есть аккаунт, привязанный к устройству. И он может смотреть фильмы, добавлять их в очередь просмотра, оценивать. Но через некоторое время пользователь вспоминает, что у него есть зарегистрированный на сайте аккаунт, и он авторизуется под ним на устройстве. Наша задача — объединить пользовательские оценки, очереди со старого и нового аккаунта. Объединение этой информации происходит во множестве микросервисов, и, в теории, должно происходить или везде, или нигде. То есть нужны распределённые транзакции, которые привносят очень большие сложности в бизнес-логику и в разрабатываемую систему. Решать эту проблему мы в итоге не стали: максимально обезопасили критическую для пользователя информацию (его покупки), а если при работе с другими данными возникнут ошибки, то их обнаружат или скрипты валидации данных, или сам пользователь придёт с жалобой в поддержку и ему там помогут.
И ещё одной проблемой стала сложность добавления нового сервиса в production. Средства CD позволяли быстро обновлять существующие сервисы, но это не касалось первоначальной установки. Она подразумевала необходимость установить новые машины в ДЦ или создать виртуалки, установить всё необходимое ПО, настроить CD. И это был небыстрый процесс, а новые доработки обычно требовались довольно срочно. Да, в будущем будут появляться различные средства для решения этой проблемы, но в то время их ещё не было. Хотя даже когда у нас начали появляться средства виртуализации, контейнеризации, оркестрации и эта проблема стала исчезать, на смену пришла другая — сложность эксплуатации. Уже нельзя справиться с десятками сервисов без автоматизации доставки новых версий приложений. Сложность эксплуатации также возрастает в связи с повышением требований к мониторингу и управлению большим количеством сервисов. Решение таких эксплуатационных проблем требует множества навыков и инструментов.
Всё это очень сильно поубавило пыл в написании новых микросервисов. При реализации новых задач мы старались это сделать в рамках уже существующих сервисов. И выделяли микросервисы только в случае крайней необходимости, или если требования к реализуемой задаче совсем не укладывались ни в один из существующих сервисов.
Но компания развивалась, появлялись новые направления разработки, и среди прочего появились сервисы для аналитики. Тут мы столкнулись ещё с одной проблемой микросервисов — согласованность данных. Для различной аналитической обработки требуются данные из множества сервисов. И поддерживать их согласованность очень сложно. Например, мы формируем и отправляем пользователям письма с персональными рекомендациями, предложениями посмотреть тот или иной фильм. Но поскольку данные не согласованы, то может получиться так, что пользователь этот фильм уже посмотрел, и письмо будет неактуальным. Несогласованность данных усложняет бизнес-процессы для работы с этими данными. Например, мы должны были непосредственно перед отправкой проверять в оригинальном хранилище, посмотрел ли пользователь уже этот фильм или ещё нет.
Помимо появления новых микросервисов у нас активно дорабатывались старые микросервисы, туда добавляли больше функциональности, больше различной бизнес-логики. Некоторые сервисы настолько разрастались, что стали походить на монолиты, превратились в своего рода big ball of mud, с большой энтропией кода, сильной связанностью между различными частями сервиса, сложностью масштабирования.
Происходило это из-за того, что продукт активно развивался, в него интенсивно добавляли новые функции, и при этом не было какой-либо единой ответственности, не было небольшой группы человек, отвечающих за архитектуру сервиса, через которых проходили бы все правки. В результате в таких сервисах нередко появлялось дублирование реализации какой-либо функциональности. Например, в одном из сервисов мы нашли три различных варианта работы с AB-тестами.
При этом и наша компания разрасталась, менялась её структура, появлялись команды, развивавшие конкретные направления. И тут мы осознали ещё один недостаток монолитов и преимущество микросервисов — разграничение ответственности. Для каждого микросервиса у нас появились свои ответственные команды. Чего нельзя сказать о разросшихся в размерах сервисах: за них отвечали все и одновременно никто. Стали происходить диалоги вида: «Кто отвечает за сервис X? — Команда A. — У меня вопрос по функциональности F. — А, за этим тебе надо идти в команду B.».
Со временем обнаружились ещё некоторые проблемы монолита. Первой из них стали высокие требования к отказоустойчивости и многочисленные усилия по поддержанию этой отказоустойчивости. Если с каждым из микросервисов мы провели работу и добились деградации функциональности при полной потере микросервиса, то при потере «больших сервисов» мы теряем слишком много функциональности, и весь сервис становится неработоспособным.
Другой проблемой стала сложность поддержки актуального стека технологий. Уже давно закончилась поддержка второго Python, но один из наших сервисов до сих пор на нём работает. А другой сервис мы полгода переводили на третий Python. Да и в целом любой рефакторинг сервисов с большой кодовой базой — это очень сложная задача, которая требует больших усилий для организации плавного перехода на новые технологии, или может потребовать feature freeze на длительный период. К тому же после написания всего необходимого кода очень долгое время занимает тестирование таких изменений. Тогда как микросервисы переводились на Python 3 за пару дней и очень быстро тестировались.
Поэтому сейчас мы приветствуем новые микросервисы. Их становится всё больше. И тут мы начали замечать ещё один недостаток — сложность распространения общих практик на все микросервисы. Например, решили мы собирать все ошибки в Sentry. И теперь нужно пройтись по командам разработки и микросервисам и везде добавить необходимую функциональность. Да и с ростом компании и количества микросервисов их преимущество в виде возможности попробовать новые технологии приобрело негативный оттенок. Зоопарк технологий разрастается, переход людей из команды в команду сопровождается некоторым переобучением. Да и какие-то выбранные практики не всегда можно внедрить в стек технологий, используемый в отдельно выбранном микросервисе.
И несмотря на то, что у нас активно развиваются всё новые и новые микросервисы, их количество превысило 50 штук, у нас остаётся проблема с осознанием того, что некоторые новые куски бизнес-логики лучше выделить в отдельный микросервис, а не добавлять к уже существующим. Обычно это аргументируется тем, что создавать новый сервис долго, а доработки нужны уже сейчас. Мы даже как-то провели небольшой эксперимент и показали, что реализовать новый сервис, настроить его CI, добавить манифесты для Kubernetes и выгрузить в dev-кластер можно за несколько часов, что не сильно дольше, чем пытаться добавить эту же функциональность в уже существующий сервис.
Так что на текущий момент мы видим будущее нашей разработки за микросервисами. Да, у них есть свои проблемы, но каждая такая проблема — это интересный технологический, и местами организационный вызов.