Привет! Я тимлид команды «Добро» в «Сравни.ру», мы занимаемся разработкой сервиса по подбору кредитных продуктов.
Сервис, над которым мы работаем, помогает нашим клиентам подобрать кредитные продукты с высокой степенью одобрения. Для этого мы придумали алгоритм, который аккумулирует необходимый объем данных, обрабатывает их и подбирает кредитные продукты от банков, которые с высокой долей вероятности одобрят заявку конкретного пользователя. Нетипичное название команды («Добро») произошло от слова «одобрение».
Сейчас все наши решения работают на базе микросервисной архитектуры, но так было не всегда. Сегодня я расскажу о том, как мы переезжали с монолита на микросервисы, какие при этом возникали сложности и как мы их решали.
С чего всё начиналось
На момент принятия решения о переезде проект «Сравни.ру» существовал уже около семи лет. Сайт был написан на .NET и представлял собой один большой монолит. Как можно догадаться, чтобы выкатить очередное обновление, приходилось немного пошаманить. У нас было несколько админок, в которых нужно было время от времени перезагружать данные. Все данные собирались из одной базы, выполнялся экспорт, и они обновлялись пачкой. Это занимало кучу времени. Вместо этого мы хотели получить реал-таймовые админки (когда поправил раздел — и он моментально обновился на сайте).
Еще одной проблемой было то, что проект получился большим, долго компилировался, на это могло уходить по несколько часов. У нас был релизный цикл. Например, через две недели мы выпускаем новую версию сайта — и туда войдут такие-то фичи по таким-то разделам.
Мы же хотели перейти к другой схеме: разбить всех на продуктовые команды, каждая из которых отвечает за свой раздел, отдельно релизит и может быстро проводить итерации. Даже если они выкатят код с багом, должна быть возможность это быстро исправить или откатить на предыдущую версию. И это никак не должно затрагивать другие разделы сайта, потому что процесс отката или фикса мог занимать несколько часов, а хотелось, чтобы это занимало максимум минуту.
В какой-то момент нам надоело тратить кучу времени, и мы захотели перейти к более гибкой разработке и коротким итерациям, быстрее релизить разные фичи, проводить A/B-тесты и так далее. К тому же сайт морально устарел с точки зрения пользовательского опыта — и много что нужно было переделывать. Тогда-то мы и решили, что раз уж мы всё переделываем, то надо распиливать наш монолит на отдельные сервисы и переходить на более гибкий стек, который позволяет делать итерации гораздо быстрее.
Из технологий выбрали Node.js для серверной части, React — для фронтенда и MongoDB — в качестве базы данных, а для хостинга — Azure и Яндекс.Облако. Перевести хотели в облако, чтобы всё крутилось в Kubernetes. Это открывало дополнительную универсальную схему: так и команды, и DevOps-инженеры могли легко это всё поддерживать.
Как мы переезжали
Проект начали делить по разделам. Логически хотелось вынести и фронтенд, и бэкенд в ведение команд, чтобы у них была полная свобода действий: как и что релизить, что использовать и так далее. Но очень скоро эти разделы стали разрастаться и угрожали превратиться в новые монолиты.
Некоторые команды могли отвечать за несколько разделов. В рамках одного репозитория они начали работать над разделами, за которые отвечают. Стало понятно, что такая схема не будет работать, так как сегодня одна команда отвечает за один раздел, а завтра это может быть передано другой команде. Мы быстро вмешались и начали четко делить всё на небольшие логические сервисы, которые обычно шли в группах. В итоге пришли к схеме, когда есть фронтенд, бэкенд и админка в виде отдельных репозиториев.
Начали использовать MongoDB в качестве системы управления базами данных, но возникли проблемы. В части сервисов нужно было производить сложные выборки и вычисления. По философии и некоторым технических ограничениям MongoDB нам не очень подходила, так как приходилось делать «хитрые» агрегации на большом объеме данных. Например, считать процентные ставки. Это всё работало медленно, и мы поняли, что инструмент не подходит для таких задач. Сейчас MongoDB мы также используем, но PostgreSQL лучше подходит под наши задачи с точки зрения логики. Чтобы не грузить базу, часть расчетов мы вынесли либо на клиентскую, либо на серверную часть.
Что касается деплоя, то, как я сказал раньше, мы сделали несколько репозиториев под разные проекты. Например, репозитории под наш сервис по подбору кредитов выглядели так: credit_selection_frontend, credit_selection_service, credit_selection_backoffice.
Реализовали всё это через TeamCity. Заходя в TeamCity, любой разработчик или тестировщик может выкатить один сервис или сразу несколько. Выкатывается всё либо в тестовый, либо в продакшен Kubernetes. Это дает возможность изолированно тестировать функционал сразу в нескольких микросервисах. Добавляем в несколько микросервисов и смотрим, как это вместе работает и как будет выглядеть для пользователя. Как минимум это посмотрят тестировщики, а в некоторых случаях — продакт-оунеры. Грубо говоря, тестовые стенды создаются двумя кликами мыши через TeamCity.
В конце концов мы перенесли всё в Kubernetes, это позволило легко создавать и деплоить новые сервисы. Сейчас у нас есть несколько нод Kubernetes для разных задач.
Проблемы и их решение
При переезде на микросервисную архитектуру мы поняли, что нельзя целиком переписывать код, так как это огромный проект, у которого могут быть свои баги и недоработки. Поэтому пришлось выкатывать всё по частям и переписывать код по разделам.
В процессе переезда запросы шли в монолит. В монолите был код, который эти запросы роутил либо в свои обработчики, либо на микросервисы, если запрашиваемый раздел был уже на новой архитектуре. Проблема была в том, что, например, когда мы выкатывали новый микросервис, могла произойти ошибка в таблице маршрутизации урлов. Это приводило к тому, что мы выкатывали сервис, а он не работал.
Из-за того что проксирование шло через монолит, это возвращало нас к необходимости снова пересобирать проект несколько часов. Пользователи не могли нормально использовать сайт в течение этого времени, что для нас было критично.
В итоге решили разруливать запросы с помощью nginX, который служил единой точкой входа, а затем перекидывал запросы на монолит и микросервисы. Если раньше на этот процесс уходило от одного до нескольких часов, то теперь он стал занимать всего несколько минут. Такое решение помогло переезжать в более спокойном режиме.
Немного про тестирование и деплой
В Kubernetes у нас есть стейджинг, куда мы деплоим наши задачи для тестирования, причем все эти задачи создаются в отдельной ветке. Тестировщики заходят в тестовый стенд, разворачивают сервисы и проверяют, как они работают.
Разворачивание происходит по нажатию на одну кнопку в соответствующей ветке. По завершении тестирования мы мерджим ветку в мастер, и после этого она автоматом выкатывается на основной тестовый стенд, чтобы продакты и вообще все желающие могли посмотреть на новую фичу перед релизом.
По нажатию еще одной кнопки происходит мердж в мастер и выкатка на продакшен. Мы специально не делали этот процесс полностью автоматизированным, чтобы можно было в ручном режиме подтвердить деплой в нужное время.
Для непрерывной интеграции и деплоя мы долгое время использовали TeamCity, сейчас потихоньку переезжаем на GitHub Actions.
Что в итоге?
В результате переезда на микросервисы мы ушли от крайне неприятной ситуации, когда одна команда что-то меняет в коде, а у другой в это время что-то отваливается. Теперь все сервисы изолированы, что позволяет каждой команде быстрее выкатывать (и, что немаловажно, откатывать) фичи оперативно, и это никак не влияет на работоспособность системы. В итоге наша текущая инфраструктура достаточно стабильна.
Сейчас внедряем ML и тоже деплоим в Kubernetes. Но об этом расскажем в следующий раз. Буду рад ответить на комментарии и вопросы!
Комментарии (25)
Guzergus
17.02.2022 20:33+3Интересуют некоторые детали.
Из технологий выбрали Node.js для серверной части, React — для фронтенда и MongoDB — в качестве базы данных
Какие причины переезда с .NET и почему именно Node? Рассматривал обе технологии, не заметил огромных преимуществ у ноды, которые бы обосновывали смену технологии после 7 лет опыта работы с другой (рассуждение применимо и к переезду Node -> .NET).
Аналогичный вопрос про Mongo. Какие именно недостатки были у предыдущей базы данных и почему была выбрана Mongo? Используется ли шардирование? Репликация?
Так же, не нашёл в статье, какая база изначально была у монолит решения. Postgres?Semenov Автор
17.02.2022 21:33Не утверждаю, что технология X лучше технологии Y. Вот про мнолит vs микроскервисы готов поспорить :) Дотнет у нас используется и сейчас в целом ряде проектов, просто это уже не монолит как было раньше, а микросервисы. Про Монго — до этого использовался MS SQL, он не очень идеологически вписывался в наш новый опесорсный стек. Была выбрана Монга как простое решение, но смростом требований и фич оказалось, что она не очень подходит под наши новые задачи.
Guzergus
17.02.2022 21:53+6Вот про мнолит vs микроскервисы готов поспорить :)
Плюсы и минусы есть у обоих подходов, распределённый монолит в разы хуже обычного, поэтому я предпочитаю распиливать на сервисы когда уже есть опыт и понимание доменной области, иначе будут только проблемы. Но это так, полноценный анализ необходимости микросервисов и их пользы занимает огромное количество времени.Дотнет у нас используется и сейчас в целом ряде проектов, просто это уже не монолит как было раньше, а микросервисы
Ну, в тех проектах, где он перестал использоваться, в чём была причина?
Выглядит как будто вы переехали на «крутые» штуки, особенно учитывая тот же выбор монго, хотя по факту ни у реляционных баз, ни у.нет нет никаких недостатков по сравнению с вашим предыдущим стеком.
Matisumi
18.02.2022 08:56+2То есть вы перешли с реляционной БД на no-sql только по идеологическим соображениям? Ведь есть же mysql, postgresql и тд.
cross_join
17.02.2022 21:09+15Для слоя БД описана типовая "история успеха":
Возьмем Монгу - это модно, стильно и молодежно
Уррра! Легко загрузили все данные. Но, блин, что-то с запросами совсем беда...
Переходим на Постгрес - это хоть и не молодежно, но модно и, типа, бесплатно
Ура! Запросы, наконец, стало удобно писать, язык человеческий. Но, блин, почему так медленно? Неужели Оракл и Микрософт не зря хотят денег за свои СУБД с оптимизаторами, вылизанными за десятки лет?
Нет, СУБД из Big-3 нам не потянуть, придется перенести часть вычислений из запросов и хранимых процедур в код служб...
Продолжение следует...
Guzergus
17.02.2022 22:04+4Уррра! Легко загрузили все данные.
Ещё смешнее, что некоторые грузят данные в режиме j:false или вообще w:0, что значит, что они по сути не durable и могут потеряться даже после того, как клиент получил подтверждение об успешной записи. Более того, до версии 5 подобная настройка была дефолтной для монго.
Если же настроить монго корректно, мы обнаружим, что данные грузятся не сильно быстрее. А разгадка одна — в монго нет магии, которая позволяет durable сохранять данные в разы быстрее, чем это делает реляционная база. Мой опыт работы с постгре показывает, что при прочих равных она не сильно проседает на примерно таких же нагрузках.cross_join
17.02.2022 22:39Если вставлять данные в РСУБД правильным образом, то это не будет медленнее даже не настроенной на durable Монги.
Guzergus
17.02.2022 23:31Расскажите, пожалуйста, подробнее, что значит правильный образ. С трудом представляю себе ситуацию, когда пересылка данных по сети и мгновенный ack может быть медленнее, чем пересылка + запись на диск.
Даже в случае bulk insert, не настроенная монго ответит вам как только получит сами данные, в то время как реляционной базе нужно их непосредственно вставить.
Можно, конечно, реляционную субд привести к поведению монги — in-memory tables, delayed transaction durability, но мне кажется вы не об этом.cross_join
17.02.2022 23:45Давайте сравнивать яблоки с яблоками, а не с теннисными мячиками. В вашем примере можно замерить передачу данных по сети и снова окажется, что она быстрее у РСУБД за счет в разы более компактного табличного формата. Сравнивать время выполнения BULK INSERT корректно не с получением сигнала о передаче пакета (это легко изобразить с SqlBulkCopy.WriteToServerAsync), а с реальной вставкой данных. То, что она не гарантирована - следующий уровень игры.
Guzergus
18.02.2022 02:27Давайте более предметно.
Вы утверждаете, что вставляя данные в субд «правильный образом» можно добиться скорости не меньше, чем с write concern 0 для монго. Распишите, пожалуйста, что вы понимаете под «правильный образом» и о каких данных мы говорим. Я с радостью проверю ваш способ на postgresql/ms sql, если он для них работает.cross_join
18.02.2022 02:38Если все-таки желаете сравнивать теплое с мягким, проверяйте (с поправкой на объемы 2008 года)
Using SSIS to load 1TB data into SQL Server in 30 mins, with simplified settings
vagon333
18.02.2022 08:52+1Таки да - sql server bulk insert позволяет заливать данные не перестраивая индексы и закрывая транзакцию в момент окончания импорта.
Этот подход и скорость вставки позволила нам отстоять производительность RDBMS при импорте больших данных, когда бодались с большими клиентами.
cross_join
18.02.2022 14:59Если условия позволяют использовать minimally logged (пустая таблица с ключом, индексы строим позже, заливка в порядке кластерного ключа), то даже простой INSERT SELECT отрабатывает так же быстро, как BULK.
SkryabinD
17.02.2022 22:08+3PostgreSQL не такой уж и медленный, нормальная альтернатива Oracle, если не нужно чего-то специфического.
tuxi
17.02.2022 21:36Если не секрет, какой размер кода у бекенда? Меньше 1 млн строк или больше?
Semenov Автор
17.02.2022 22:06Если честно, очень сложно так навскидку оценить в строках. У нас очень много команд (~30), и репозиториев (~500). Каких-то готовых даже приблизительных оценок у меня на данный момент нет.
abutorin
17.02.2022 21:40Разворачивание происходит по нажатию на одну кнопку в соответствующей ветке. По завершении тестирования мы мерджим ветку в мастер, и после этого она автоматом выкатывается на основной тестовый стенд, чтобы продакты и вообще все желающие могли посмотреть на новую фичу перед релизом.
По нажатию еще одной кнопки происходит мердж в мастер и выкатка на продакшен. Мы специально не делали этот процесс полностью автоматизированным, чтобы можно было в ручном режиме подтвердить деплой в нужное время.
У вас разные репозитории для продакшена и "основной" разработки?
Semenov Автор
17.02.2022 22:10Спасибо, что заметили ошибку, я поправил в статье. Во втором абзаце речь о выкатке в прод кода из мастера по кнопке.
SkryabinD
17.02.2022 22:10В TeamCity нельзя разграничить доступ по веткам - если дать билдить программисту, то он не только в тест, но и на прод может запустить билд. У вас это как-то решалось?
AndreyTS
18.02.2022 00:23Спасибо за статью, рад что проект живёт. Я в свое время приложил руку к разработке этого монолита :-)
Хотел спросить, тк не очень понял из статьи, на какие всё-таки конкретно микросервисы вы порезали его?
Stas911
19.02.2022 05:04Если вы из команды "Добро", то можно поинтресоваться, чем у вас занята команда "Зло"? Подбором коллекторов? :)
CatalogLoader
Хочется спросить про то как вам nodejs после .net ? ????