Страх и Ненависть в Ви.Tech: от монолита к не микросервисам
- вторник, 4 марта 2025 г. в 00:00:07
Представьте: у вас есть монолит на PHP. Большой, сложный, местами запутанный – но такой родной. Он верой и правдой служит бизнесу много лет. А потом случается неизбежное – компания растет, нагрузки увеличиваются, и ваш надежный монолит начинает... задыхаться.
Я – Кирилл Кузин, go-разработчик платформенной команды Ви.Tech (IT-дочка ВсеИнструменты.ру). Последние три года мы переводим высоконагруженные части нашего кода с монолита на Go-сервисы. За это время успели перенести критически важные компоненты, увеличить производительность в десятки раз и найти свой путь между монолитом и микросервисами. О нем я сегодня и расскажу.
В Ви.Tech мы начинали свою историю как и многие другие компании – с желанием распилить PHP-монолит на микросервисную архитектуру. Это казалось лучшим решением, о котором говорили коллеги, писали в статьях и книгах. Но в процессе работы мы поняли, что распил монолита – это почти комедия. Примерно такая, как «Страх и Ненависть в Лас-Вегасе».
В основе статьи – мой доклад на GolangConf 2024. Если интересна визуальная часть истории – запись доклада доступна по ссылке: https://www.youtube.com/watch?v=Xtg7uX0fLvs
Как и многие компании, мы столкнулись с типичной проблемой растущего монолита. PHP-приложение, которое годами верой и правдой служило бизнесу, начало задыхаться под возросшей нагрузкой. Чтобы объяснить, как это происходило, представьте команду сеньоров, едущих по пустынному хайвею. У них отличная машина, прекрасная погода, и дорога стелется до горизонта.
Их попутчик – монолит, подобранный в начале пути. Поначалу с ним комфортно: пусть не красавец, но проблем не создает.
Но постепенно ситуация меняется. Попутчик становится невыносимым: он растет, занимает все больше места, мешает управлять машиной. Каждое действие требует от команды все больше усилий, даже чтобы просто держаться в своей полосе.
В нашем случае проблема оказалась серьезнее – выросло аж три монолита на PHP.
Основной из них – монолит сайта, о котором сегодня и пойдет речь. В 2017-2019 годах, когда бизнес начал активно расти, кодовая база столкнулась с серьезным вызовом – масштабированием родительской компании. Несмотря на размещение в трех дата-центрах, использование kubernetes и так далее, ситуация ухудшалась с каждым кварталом.
Проблемы, с которыми мы столкнулись, типичны для монолитов:
Критическое падение производительности. Медленный сайт – это низкая конверсия и уход пользователей к конкурентам;
Непредсказуемое время разработки (Time-to-Market). Новые фичи стали требовать неоправданно много времени, в то время как конкуренты запускали обновления быстрее нас;
Деградация кодовой базы. Код превратился в запутанный клубок зависимостей, в котором сложно ориентироваться даже опытным разработчикам, не говоря уже о новичках.
Мы понимали: если какая-то часть архитектуры мешает бизнесу развиваться, её нужно менять. Но для принятия серьезных архитектурных решений требовались весомые бизнес-аргументы.
Поэтому следующим шагом стало создание прототипа.
Прототипы нужны, чтобы показать бизнесу верный путь решения накопившихся проблем. Новая архитектура должна принести измеримую пользу: сэкономить деньги, сохранить репутацию, дать х100-1000 к полезности существующего функционала. Без этого даже не стоит заводить разговор о замене монолита на что-то другое.
В кулуарах меня спрашивают: "У нас есть рабочая монолитная система на Python/PHP с очевидным запасом прочности. Хотим перейти на Go и микросервисы. Что делать?" Ответ прост: скорее ничего. Смена архитектуры должна решать реальные проблемы бизнеса, а не просто следовать трендам. Если вас все устраивает в текущем решении, то зачем искать добра от добра?
Нам же повезло – подходящий функционал для демонстрации нашелся быстро.
Допустим, какой-то товар неожиданно становится хитом продаж. Вроде бы это должно радовать – прибыль растет, покупатели получают то, чего хотят. Но когда количество товара на складе достигает нуля, монолит ставит подножку. Доезд наличия товара со склада на сайт занимает десятки минут, и все это время пользователи пытаются купить несуществующий товар. А это прямые репутационные и финансовые потери.
И вот вы заказали такой товар, а из поддержки сообщают – товара нет, техническая ошибка. Можете подождать пару дней, пока товар вновь прибудет на склад или отменить заказ. Даже предложенная скидка не исправит впечатление от покупки на крупной площадке. Скорее всего, вы уйдете к конкурентам – ведь нет гарантии, что ситуация не повторится, потому что проблема системная.
Так и было у нас. Когда доезд наличия на сайт занимал 40 минут, мы решили выделить эту логику в отдельный микросервис. Результат превзошел ожидания: время обновления сократилось до 30-40 секунд!
Это решение сразу сняло одну из критических проблем бизнеса. Мы понимали, что впереди долгий путь, но теперь у команды разработки и бизнеса появилось общее видение: оставлять всё как есть больше нельзя.
За годы разработки, начиная с 2006 года, когда зародились ВсеИнструменты.ру, мы потеряли контроль над кодовой базой и теперь хотели вернуть его обратно. Наше архитектурное решение – создание микросервисной архитектуры. Бизнес его одобрил, мы получили успешный прецедент и поверили, что этот путь нам подходит.
Основная проблема при проектировании микросервисов – правильный выбор их гранулярности. У нас сильные архитекторы, поэтому мы не особо переживали за результат. Они подготовили схемы и описания, чтобы команда распила монолита могла быстро разрабатывать микросервисы. Нам не нужно было погружаться в тонкости композиции кода монолита – все было готово заранее для каждой функциональной области. Но что это за команда распила монолита?
Когда пришло время, пилить начали все. Создание отдельных микросервисов на смену кодовым частям в монолите превратилось в техдолг, на который выделялось 25-30% времени от общей разработки. Параллельно нужно было развивать фичи и двигать бизнес вперед.
Но со временем выделились разработчики, которые погружались в процесс сильнее остальных. Они быстро росли в компетенциях декомпозиции монолита и, самое главное, им это было интересно. Заметив высокую эффективность этих коллег, разработка и бизнес объединили их в одну команду. Компания получила внутреннюю “аутсорсинговую” команду, на 100% сфокусированную на распиле монолита – ту самую платформенную команду техдолга, в которой я работаю.
Какие плюсы дает бизнесу такая команда:
Она занимается только выносом логики из монолита. С каждым новым сервисом понимание того, как правильно выпиливать функционал, эволюционирует. Опыт накапливается и формализуется. Распил идет быстрее и качественнее.
Продуктовые команды разгружаются. Да, они все еще выпиливают куски своей логики из монолита, но сложные или срочные сервисы могут отдать нам.
Когда у вас куча команд, недавно перешедших с PHP на Go и пилящих сервисы по наитию (особенно если еще не наняты профильные гоферы), страдает унификация кода. А когда одна команда раз за разом делает похожие сервисы - не получается зоопарка с разношерстным кодом.
Но выделение этой команды происходило в течение первого этапа работ по распилу. Как же он начался? Вернемся к нашим бодрым сеньорам. Они забурились в мотель, чтобы набраться сил. Только сны у них беспокойные.
Им снятся монолиты на Go, которые должны были стать микросервисами; сервисы с кодом на Go и PHP разом – ведь крон на Symphony написать быстрее, чем разобраться с новым языком. Видятся куски Go-кода внутри репозитория с монолитом, запускающиеся через скрипты после деплоя. Но ведь это все сны, верно?
У нас сильные не только архитекторы, но и разработчики. Однако когда контроль процесса с какой-то стороны ослабевает, успех проекта не гарантирован. Не стоит полагаться на кого-то одного. Когда распил только набирает обороты и несет много нового и неизвестного, важно максимально контролировать процесс – от разработчиков до руководителей отделов и архитекторов. Поэтому мы усилили контроль над ходом распила, чтобы планы не разъезжались с реальностью. Ведь превращение микросервисов в монолиты - уже даже не симптом проблемы.
Еще при формализации процессов разработки появилось наше главное правило: у бизнеса компании только одна глобальная доменная область. Она содержит все бизнес-процессы, независимо от количества их граней, и описывается базовыми моделями. А поскольку область и модели существуют в едином экземпляре, мы не придумываем для микросервисов новые сущности и объекты – просто проецируем нужную часть существующей модели.
Примером может служить товар, который содержит все поля и характеристики, нужные бизнесу. В каждом конкретном сервисе эта сущность может иметь разную структуру: какие-то поля включаем, какие-то нет. Всё согласовано с единой моделью внутри компании, подготовленной архитекторами. Благодаря такой проработанной базе распил идет дешевле и быстрее. Думаю, многие сталкивались с ситуацией, когда команда во время разработки получает огромное поле деятельности без всякой аналитики. К счастью, наличие единой модели защищает нас от таких проблем.
Но здесь техническая задача превращается в софтовую. Команда распила не поддерживает созданные микросервисы. Выкатив код в продакшен, мы отдаем его на поддержку целевой команде. Как нам продать созданный сервис той команде, для которой и происходила разработка?
Во-первых, мы постоянно на связи, согласуя продуктовые изменения в монолите и наши доработки в сервисе – ведь продуктовая разработка не останавливается. Фичи продолжают появляться в монолите, значит, их нужно поддержать и в новом коде. Так мы избегаем разъезда фичей. К тому же, нам самим нужны консультации по работе алгоритмов – мы каждый раз погружаемся в новую доменную область.
Во-вторых, мы обязаны предоставить документацию и инструкции по сервису для бесшовной передачи функционала. Целевой команде предстоит непростой квест – разобраться в новом сервисе, который писался другими разработчиками на языке, который все еще укореняется в компании.
Но тут наши герои просыпаются с вопросом: подходит ли микросервисная архитектура к правилам, которые мы создаем в процессе распила? Ведь чем дальше, тем сильнее особенности компании влияют на разработку. Сеньоры пока едут вместе с монолитом, но уже начали воплощать свой коварный план, откусывая от него по кусочку.
Когда монолит в очередной раз вывел их из себя, сеньоры проверяют свой набор инструментов: Docker, Kubernetes, Terraform. И пила. Они готовы пилить до конца, прямо здесь и сейчас, но вопрос размера составных частей остается открытым.
Когда мы говорим о монолите, сразу вспоминаем о микросервисах. Принято считать, что монолит всегда эволюционирует в микросервисы. Но в истории эволюции мы не сразу пришли к скальпелю – трепанацию когда-то делали камнями, более грубыми, но универсальными инструментами. Эта мысль привела нас к смене цели распила: переходу на сервис-ориентированную архитектуру – SOA.
По сути это более крупно-гранулированная микросервисная архитектура. И хотя микросервисы вышли именно из SOA, для нас это не шаг назад.
Да, SOA много кто ругает, о ней почти забыли, но мы вдруг осознали, что именно она-то у нас и получается вопреки попыткам сделать обратное. Что ж, если оно у нас работает и дает существенный профит здесь и сейчас, то может не надо биться в закрытые ворота? Кое-что подкрутим и ладно. В том числе, мы добавили SOA подходы, часто присваиваемые микросервисам, и к тому же, наша архитектура завязана на архитектуру, основанную на событиях. Плюс данного решения состоит еще и в том, что дробление на сервисы приносит нам отличные результаты. Абсолютно те, что мы ожидали от новой микросервисной архитектуры. А факт того, что SOA – это явная ступенька к микросервисам после монолита, мы можем прийти к ним позже, но при этом с полным осознанием того, что выжали все из сервисной архитектуры.
Поэтому, когда наши сеньоры бросили монолит на обочине и поехали счастливыми дальше, то они подобрали по пути нового попутчика. Он интересный и загадочный. Более классный собеседник, чем монолит. Наши герои довольны новым попутчиком. Но почему им так повезло его встретить?
Первым пунктом следует конфигурация нашего монолита. Даже тут он влияет на нас. А причина в том, то кодовая база слишком запутана и сложна – пара миллионов строк кода, сотни бандлов. В нашей практике постоянно всплывает то, что как бы архитекторы и аналитики не старались упростить разработчикам жизнь, в процессе распила все равно происходит ситуация, когда обнаруживается такой код, который никто не заметил, а он оказывается связанным с выпиливаемым функционалом. Что делать? Запихивать и найденный код в новый сервис, а с таким подходом микросервисы не получаются, как вы понимаете.
Далее стоит уточнить, что мы – компания, ориентированная на бизнес. По прикидкам с привязкой на долгосрочную стратегию получалось, что создание новой архитектуры займет достаточно много времени – 8-10 лет. Казалось, что это необходимое зло. Но когда стратегия распила была пересмотрена в пользу SOA, то оказалось, что мы сможем справиться быстрее за счет более крупной компоновки кода и откладывания решения некоторых инфраструктурных проблем. Срок распила сократился до 6-7 лет. Мы не пытаемся найти идеальную архитектуру. Мы решаем задачу для бизнеса, то есть выбираем подходящую архитектуру.
И третье – самое важное. Все-таки у нас существовали и существуют ограничения. Носили и носят они инфраструктурный и бюрократический характер. Если говорить про инфраструктуру, то команда девопсов стала внутренней относительно недавно, а с внешней командой приходится сосуществовать в рамках определенных ограничений. Плюс у нас были на момент начала распила определенные ограничения, связанные с собственной сетью. Но на данный момент с этим мы практически справились. Но вот какие-то бюрократические препоны все же остаются.
При выкладке сервиса в продакшен нам необходимо выполнить определенные действия по чек-листу – это необходимая часть разработки. Мы не идем путем стартапа, нам необходима безопасность с точки зрения SRE и хоть какая-то ее гарантия. Но все-таки использование перечня обязательных шагов увеличивает нам время деплоя сервиса в продакшен. Как вы видите, легче выкатить один сервис, чем два. И это хорошие причины с нашей точки зрения для того, чтобы использовать сервисы, вместо микросервисов. Но может у нас и не было выбора, кроме как SOA? Тут можно вспомнить закон Конвея.
В Ви.Tech пока прижился не продуктовый подход, а более проектный. У нас есть product-менеджеры и продуктовые команды, но внутри компании мы оперируем проектами. Тот же распил монолита – это вычленение отдельной команды под проект, который будет длиться до 2027 года. А раз у нас проектные процессы, то важным становится давать сроки и укладываться в них, то есть продуктовым командам приходится координировать развитие продукта с главным проектом компании.
Это не про то, что каждый спринт у нас начинается с новыми вводными. Микросервисы же – это про продуктовую разработку. Это о том, что одним микросервисом весь его жизненный цикл должна владеть одна команда (а у нас это не всегда так за счет отдельной команды, работающей на распил монолита). Это о том, что у каждого микросервиса свой ограниченный контекст. Но в моем понимании, существование глобальной доменной области и допущение, что сервисы только ее проекция – уже говорит о выходе за пределы парадигмы микросервисной архитектуры. То есть сама структура компании, сам подход к работе отодвигает понятие микросервисов на второй план по сравнению с тем, что нам необходимо достигать результатов по проектам, которые имеют начало и конец.
На этом моменте наши сеньоры доехали до мотеля, чтобы отдохнуть. Но им снова снятся странные сны, от которых они во всю ворочаются. Что на счет организации и композиции кода внутри крупных сервисов на Go? Им снится такой ответ на вопрос: монорепы. Монорепы, которые в себе содержат все: и консьюмеры, и сервисы, и вспомогательные команды, и кроны, и отдельные команды импорта данных, и миграции. Сеньоры просыпаются в смятении. И за завтраком узнают у SOA, откуда оно. А оно из Ви.Tech.
Да, мы используем монорепы. И, наверное, в это слово я вкладываю несколько иной смысл, чем обычно. Я – гошник, а поэтому для меня монорепа – это репозиторий, который занимается несколькими функциональностями в отдельных процессах. Это не самый популярный подход при работе с кодовой базой на Go, но почему нет? Однако, вы сразу можете меня спросить о том, почему выбор пал именно на такой вариант работы с кодовой базой. Ведь это неизбежно вызывает проблемы. Да, но мы с этими проблемами расквитались. У нас довольно прямолинейная логика на этот счет. Поясню на примере.
Примем, что существует сервис, имеющий api для какого-то внутреннего сервиса. Все отлично, все работает. Но что, если нам ставится задача на то, чтобы организовать вторую api в целевом сервисе для нового ui? Здесь есть два очевидных хода. И первый – создать второй main внутри нашего сервиса.
Это каноничный для Go путь, но опыт деплоя в продакшен показывает: большое число main-файлов создает проблемы. Это увеличивает общее количество файлов, необходимых для деплоя, это увеличивает количество команд для этой операции. А у нас часта ситуация, при которой число таких вот main-файлов достигает пяти-шести в рамках репозитория. Это означает существование нескольких бинарей, что влияет на все тот же деплой, а именно на наши пайплайны. С каждым новым файлом в монорепе надо менять не только код, но и скорее всего сам пайплайн, то есть таким образом мы будем влиять не только на сервис, но и на инфраструктуру, уходя от универсального решения. Нам это не надо. Нам нужен один бинарь.
Но мы ведь можем пойти другим путем и создать второй сервис. И это верно, только вот мы сознательно не хотим разделять модели между N репозиториями. Мы не хотим при необходимости внесения изменений, вносить их в три, в четыре, в восемь сервисов. Не хотим деплоить это же количество сервисов. А если что-то в одном из десяти сервисов забудем поправить и это скажется на пользователе? Нам честно удобнее все делать через один коммит в одну кодовую базу. Именно поэтому монорепы. Так мы порешали проблему с деплоем, только еще одну проблему пока не смогли решить – как запускать разный по функционалу код из одной монорепы?
Мы пошли по простому пути и используем пакет для создания консольных приложений Cobra. Он позволяет нам создавать под каждое конкретное поведение кода, будь то консьюмер или сам сервис, собственную команду, которая поднимает через один и тот же main-файл разные по назначению инстансы. Нужна миграция? Вот новая команда. Хотите разнести http- и grpc-серверы? Не проблема. В этом отношении Cobra для нас просто способ композиции кода и возможность вновь оставить один универсальный пайплайн. Еще она помогает нам в планировании ресурсов под каждый инстанс. Каждая команда – это отдельный сценарий запуска, под который выделяются собственные ресурсы. Если внешняя api довольно быстрая, а внутренняя нет, то мы поднимаем их разными инстансами с разными настройками по потреблению памяти, процессорного времени и т.д.
Тут не стоит забывать и о процессорах. Разнося две разные по скорости api на разные процессы, мы можем добиться уменьшения кэш-промахов на процессорах, в случае, когда долгие, но редкие запросы на тяжелую api вымывают кэш с данными для более быстрой api. С помощью разных процессов такая проблема становится не актуальной.
Но что же нам на самом деле дала сервисная архитектура с использованием всех тех подходов, о которых я уже рассказал?
Мы пилим функционал именно так, как нам удобно. Мы позволили себе пойти по эволюционному для нас пути, перестав оптимизировать ручки и затыкать дыры за счет решения, которое влияет на всю компанию целиком. При этом в процессе распила поменяв его стратегию и по пути создав платформенную команду распила, которая решает проблемы системно;
За счет того, что в крупных сервисах содержится смежный функционал, мы избегаем части сетевых вызовов, а значит проблем с сетью, с которыми скорее всего могли бы столкнуться при росте количества микросервисов. Также уходим от проблем с мониторингом множества микросервисов – контролировать меньшее количество несказанно удобнее;
В подходах мы себя тоже не ограничиваем: независимое развертывание, одна база для одного сервиса, kafka – все это и еще больше мы тоже конечно же используем;
Мы сохранили себе универсальный pipeline, и не ломаем его через создание крупных сервисов, из которых состоит архитектурная топология компании.
Но и у нашего подхода есть недостатки:
Без должного наблюдения и контроля сервисы на Go могут быстро замонолититься, и на первых порах у нас такая проблема, как я уже упоминал, происходила. Можно назвать сходу пару сервисов, чье ближайшее будущее – распил;
Сложные системы на Golang писать сложно само по себе, но и процессы разработки усложняются. В том числе и коммуникация между разработчиками, и планирование.
Но пока мы с вами касались именно архитектуры в ее верхнеуровневом смысле. Давайте уже перейдем к самим сервисам. Для начала стоит взглянуть на один проект.
Да, это тот самый гошный шаблон, с которого все начинали свой путь в Golang. Его многие критикуют, но постоянно оборачиваются на него, когда доходит до создания своей платформы и своего шаблона универсального сервиса.
Но в данном случае нас интересует директория internal сервиса, которым я занимался последние полгода – сервиса по работе с корзиной и оформлением заявки на заказ. При создании конкретно этого сервиса мы столкнулись с некоторой проблемой: как именно организовывать код? Сервис получился большим – в нем более 70 тысяч строк кода и больше полугода разработки (не считая доработок при его открытии для клиентов). И этот масштаб нас чуть было не погубил. Для первой внутренней области сервиса – корзины, мы решили использовать подход, похожий на golang-layout.
И потерпели неудачу. Посмотрите на это. Где-то на полпути мы столкнулись с тем, что подобная структура нас только путает, сплетая зависимости и усложняя навигацию. И ладно бы, мы ощутили подобное в конце разработки. Но нет – мы только дошли до экватора работы над первым модулем. Нужно было что-то делать с этой проблемой.
В очередной раз оказалось, что распил монолита и вынос какой-то функциональности в отдельный сервис – это не просто техническая задача. Технические задачи решаются механическими путями. Но распил – это еще и софтовая задача, в которой бодрым сеньорам надо взаимодействовать друг с другом и с другими командами. Внезапно умение договариваться и решать возникающие проблемы стало столь же важным, как и написание кода.
И умение команды договариваться в ситуациях, когда у каждого свой план, когда каждый сеньор имеет свое мнение, привело к чистой архитектуре. В нашем случае создания достаточно сложного и крупного сервиса оказалось, что лучше заранее подумать о том, что и где будет лежать, что и как будет друг с другом общаться при параллельной разработке. Откровенно говоря, таким образом количество багов во время интеграции оформления было заметно ниже, чем для корзины процентов на 15%. Конечно, мы вновь интерпретировали чистую архитектуру так, как нам лично удобно, но глядя на нее, у меня не скрипят зубы от агрессии на код и на себя лично.
А наши сеньоры все еще продолжают путь по пустыне. SOA путешествует с ними, и пока никто ни от кого не хочет отказываться. Компания подобралась, что надо. Но вот они заезжают на очередную ночевку, и им снова не везет. Вновь им мерещатся кошмары и что-то странное.
Сеньоры видят сущность, которая хранит в себе все подключения, все клиенты, все сервисы, которыми оперирует наш сервис корзины и заказа. Эта сущность вольна расти так, как ей хочется. Она похожа на god-object, чем очень пугает. Новые разработчики приходят и спрашивают – зачем? Сеньоры просыпаются в холодном поту. Но продолжают видеть эту сущность уже в реальности.
А сущность называется builder. Ее существование описывается фразой: так исторически сложилось. И она именно что отвечает за хранение и менеджмент всевозможных подключений сторонних сервисов и внутренних слоев друг к другу.
Сборка зависимостей – обычная же для нас вещь, согласитесь. Распределяем код по слоям, создавая в них конструкторы. Слои общаются через интерфейсы, как заведено. А инициализируем и собираем все в файле main.go. И это ок. До того момента, пока у нас только одна-две команды на запуск. Но их у нас точно больше четырех, потому что имеем монорепозиторий с Cobra, как мы помним. Поэтому под каждую команду нам надо собирать все слои ровно так, как нужно этой команде.
Можно сделать довольно канонично – использовать не один, а два main-файла. Три. Но что, если нам надо внести изменения? А что, если мы забудем куда-то внести эти изменения? Об этом мы уже говорили. Беда. В ее решении и помогает builder.
Хотя на первый взгляд это похоже на god-object, у builder'а только одна ответственность – сборка слоев. Builder занимается только этим и никак не задействуется в бизнес-логике. Его можно увидеть только в командах Cobra или в main. Эта структура работает через переиспользование кода. Вы могли заметить, что это именно структура, а поля внутри нее – наши слои. Именно это и является главным для нашего кода – командам нужна штука, которой они скажут – собери нам конкретную функциональность, и именно ее они и получат.
Builder имеет ряд экспортируемых методов, которые предоставляют конкретные реализации под запросы различных команд: grpc- и http-серверы, консьюмеры, джобы, внутренние сервисы, переиспользуемые подключения к БД. При первом создании слоя мы складываем его в поле структуры, чтобы затем переиспользовать в других слоях, если это потребуется. По факту за экспортируемым методом скрывается дерево вызовов слоев, на которых завязано создание интересующей функциональности. И это удобно – у нас всегда все одинаково.
И это еще не все – builder работает не только строителем, но и разборщиком. На его основе мы создали простую структуру шатдауна, которая запоминает что и в каком порядке было создано.
По факту мы получаем стек. Сначала собираем его снизу вверх, от условных репозиториев до серверов, а затем разбираем в обратном порядке. Потому что наш builder – это единственная сущность, которая в курсе, в каком порядке и что именно собиралось. Исходя из всего этого можно вывести еще одно правило работы с builder – никакой асинхронщины, чтобы не возникало проблем с гонками. В асинхронном коде просто нет нужды, поэтому вся работа с builder – это простая и предсказуемая работа с реализацией условного паттерна singleton.
И глядя на то, как наши герои продолжают свой путь, можно сказать, что у них все остается хорошо. Они едут и наслаждаются жизнью, решив все свои проблемы. Но действительно ли они решили все свои вопросы? На самом деле, остается последний: как организовать переключение трафика с монолита на отдельный сервис корзины и заказа?
Ведь мы переключаемся постепенно, а не сразу на 100%. То есть необходимо иметь возможность синхронизировать состояние корзин и заказов между разными поколениями кода. На ум сразу приходит паттерн anti-corruption layer. По сути это фасад между двумя системами, либо слой middleware. Отлично подходит для случаев переписывания систем на новый стек или взаимодействия с внешними legacy-системами. И тут мы кричим бинго! Потому что у нас 2 попадания из 2.
поддерживать в случае отдельного сервиса. В случае middleware – это слой, который затем придется удалять из кода. Для нас решение оказалось более трудозатратным, чем могло бы быть. А работы ведь и так хватает. Сроки поджимают. Проекты, чек-листы, ограничения, помните, да?
Что важнее, у нас нет уверенности в консистентности данных при синхронизации таким способом.
Вот простой пример, показывающий важность того, чтобы наши данные не отправлять через описанный паттерн: пускай во время пиковой активности пользователей у нас лег монолит на 5 минут. У части пользователей возникают проблемы работы с корзиной. Но еще какая-то часть не замечает проблемы и спокойно работает с корзиной через наш новый сервис, а затем отправляет заявку на оформление заказа.
В случае anti-corruption layer наши запросы просто бьются о стену и возвращают ошибку, теряя то, что должно было быть синхронизировано. Собрав корзину на работе, дома вы можете подключиться к монолиту, а не сервису, в котором корзина пуста, а вновь работающий синк через любое новое действие с корзиной затрет вам то, что лежит в БД, используемой сервисом.
Надо думать, как дополнительно защитить данные, потому что заказ показывается на сайте все еще из монолита, от того синк заявок абсолютно необходим. При падении какой-либо из частей системы мы должны обеспечить полную синхронизацию. Это важное условие. И для нас этот паттерн является неподходящим, а допиливать его функциональность времени нет.
Спасибо за то, что у нас есть классная Kafka. Логика проста – мы включаем двунаправленный синк между базами данных в том случае, если понимаем, что записи в БД изменились. Если какой-то сервис упадет – увидим алерт на рост лага в топиках. Потом оно все равно прорастет в нужную систему при ее оживлении. У вас здесь может возникнуть резонный вопрос: неужели в Ви.Tech Kafka такая надежная? Мы считаем, что да. И если у нас вдруг упадет Kafka, то с высокой долей вероятности у нас упало все остальное, а значит отсутствие синка корзин и заявок на оформление уже не будет самой большой проблемой. Да и по факту, мы используем то, что у нас уже хорошо работает. А если оно хорошо работает, то не обязательно от этого отказываться. К сожалению этому принципу в разработке не всегда следуют.
К каким выводам мы можем прийти после всего прочитанного, помимо того, что свой велосипед всегда лучше чужого?
Без проработанной доменной области распил не имеет смысла. Неважно, делаете вы это через общую область или через отдельные домены – главное описать то, с чем предстоит работать. Это не трата, а экономия времени и ресурсов в будущем;
Каким бы путем вы не пошли – сервисным, микросервисным, наносервисным – помните, что чем меньше связанность кода, тем лучше для системы в целом. Это необходимо обосновать бизнесу на определенном этапе развития, поэтому прототип, который покажет х100 к работе вашей системы – лучшее решение начать решать проблемы бизнеса более системно, параллельно создавая и красивую новую архитектуру, с которой будет удобнее работать разработчикам, желающим следовать трендам;
В конце концов решение всех технических задач сводится к людям. Разработчики просто обязаны уметь общаться и договариваться. Ведь построение архитектуры абсолютно всегда основано на компромиссах. Так что не гнушайтесь прокачивать софт-скиллы и не думайте, что их прокачка – упущенное время.
Но все-таки самым важным выводом сегодняшнего рассказа я бы хотел подсветить следующий: будьте рациональны во время всего процесса распила монолита. Если что-то пошло не по плану, если поехали сроки, если вы встретились с ограничениями, преодолевать которые – это незапланированные траты ресурсов и времени, то не надо биться головой об стену и оптимизировать частности. Лучше сменить стратегию. Этого не надо стесняться. Это нормально. Оставайтесь прагматичными людьми, которые решают задачи бизнеса. Именно за это нам платят. Мы занимаемся рефакторингом не в свое удовольствие. Если бизнесу что-то мешает, в том числе и какая-то архитектура – то надо ее выкинуть.
В нашем случае такими архитектурами стали и монолитная, и микросервисная. И в результате этой самой прагматичности бизнес поймет, что вы на его стороне и позволит выделять ресурсы на что-то иное. Как следствие, появятся и платформенные команды, которые в свою очередь принесут пользу разработке. А это – благо уже для нас. Ведь платформа важна, но это не всегда очевидно бизнесу. Позвольте ему убедиться в этом самому через ваши собственные действия.
Спасибо за прочтение! Статья получилась в некоторых местах спорной, но это наш опыт и реальная разработка. Буду рад обсудить в комментариях вопросы по подходам. До новых встреч!