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

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

«Геометрическое совершенство монолита воспринималось людьми как некий безмолвный вызов, оно поражало не меньше, чем другие свойства загадочной находки».

Артур Чарльз Кларк. 2001: Космическая одиссея (1968)

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

Актуальная схема Учи.ру и распределение сервисов
Актуальная схема Учи.ру и распределение сервисов

Начиналось же все с маленького сайта около десяти лет назад.

С чего все начиналось

Эра мини-монолитов

Первые версии Учи.ру появились в начале второго десятилетия ХХI века. Это были очень простые сайты с отдельными базами данных, образовательным контентом и менеджментом: старая платформа Учи.ру с уроками, «Учи.ру Столбики» и «Учи.ру Колонки». Спустя десятилетие они выглядят немного старомодно, но все равно мило — мы храним их во внутреннем музее славы Учи.ру и публичный доступ к ним не предоставляем.

Так выглядело задание на сайте «Учи.ру Столбики» — там дети учились считать в столбик.
Так выглядело задание на сайте «Учи.ру Столбики» — там дети учились считать в столбик.
Еще несколько исторических документов.
В «Столбиках» уже можно было получать ачивки за учебу.
В «Столбиках» уже можно было получать ачивки за учебу.
А список уроков на старой платформе Учи.ру выглядел довольно просто.
А список уроков на старой платформе Учи.ру выглядел довольно просто.

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

Эра зарождения первого монолита

До 2012 года сайты существовали обособленно. Затем мы создали новую платформу с единой точкой входа — Login, которая поглотила старую Учи.ру и «Колонки». «Столбики» ждало скорое забвение. 

Изменения и перенос контента породили проблемы вроде конфликта стилей, что сказывалось на качестве. Тогда команда впервые задумалась о создании эталонной системы управления контентом — CMS.

Эра CMS

Через два года системная архитектура выглядела скромно: CMS, основная платформа, а также разные прикладные модули, например, для аудита, управления персоналом, временного трекинга.   

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

Расширение функционала основного модуля

На первых стадиях разработка шла очень быстро, преимущественно мы использовали модель «Водопад». На том этапе усложнять архитектуру не было смысла, ведь никто не знал, к чему мы придем в итоге.  

Изначально мы старались разделить ответственность. Так, часть задач мы реализовали в виде функций CMS, таких как управление уроками или разработка методических материалов. Но когда темпы выросли, практически все новое стало добавляться в uchiru-login, просто потому что так было быстрее. Постепенно усложняя этот компонент, мы получили монолит. Чем дальше, тем больше его размеры и высокая степень связности затрудняли дифференциацию различных функциональных элементов. 

Из-за сложностей с любыми модификациями в монолите в начале 2019 года мы работали с устаревшими версиями Rails (4.1) и Ruby (2.1.5). Webpack с небольшими вкраплениями React-компонентов не позволял легко обновить зависимости без существенных рисков отказа в обслуживании. Нужно было что-то делать…

Выделение функций из состава монолита

Чтобы прощупать возможности разделения, мы начали проводить эксперимент по разъединению фронтенда и бэкенда. К этому моменту интерфейс представлял собой классические Rails-шаблоны с примесью старого Webpack. И хотя мы не ставили глобальной задачи по переходу на микросервисы, этот опыт все же стал большим шагом для нашей команды сразу в техническом, инфраструктурном и организационном плане. Но самое главное, мы начали выделять компоненты из uchiru-login. Стало понятно, что обуздать монолит реально. Отправной точкой стало выделение откровенно обособленного функционала с минимальной степенью связности. На подобных некорневых блоках мы и сосредоточили усилия.

  1. Генератор PDF-сертификатов. Попрактиковались мы на микросервисе, который должен выдавать готовый документ по входным параметрам. Фактически мы сделали классический распределенный монолит с синхронным сетевым взаимодействием.

  2. Олимпиадная платформа. Ее отделение от «большого брата» стало важным шагом. Стоит пояснить, что олимпиады выросли в отдельное направление: они бурно расширялись, и разработчикам приходилось постоянно оглядываться на другие части системы из-за большого риска сломать имеющийся функционал. Эта скованность мешала развиваться —  хотелось независимости. В итоге команда олимпиад повысила свои компетенции и создала прецедент выделения реально большого куска из монолита.

  3. Развивающие игры. Используя опыт «олимпиадников», мы создавали игры как отдельный компонент, связанный с монолитом авторизацией, но способный развиваться самостоятельно.

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

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

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

    Для разработки этого модуля мы использовали библиотеку JSONAPI Resources, которая сочетает в себе гибкость, низкую скорость и синтаксис, очень похожий на классический ActiveRecord. Залогом успеха стало стремление группы энтузиастов, которые во что бы то ни стало хотели достичь нужного эффекта.

Справляемся с высокими нагрузками

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

  1. Кеширование. Это было первое, что пришло в голову. В качестве инструмента мы использовали Redis. Чтобы не заниматься поддержкой кластера в облаке, мы решили заменить его урезанной версией Redis от Envoy с шардированием по ключу хранения. В результате система стала держать нагрузку лучше, но это было только начало.

  2. Реплики БД. После очередного скачка трафика мы уперлись в лимит пула соединений с БД. Теоретически работа с микросервисами должна была помочь в масштабировании самых нагруженных частей системы или вовсе избежать этой проблемы. Но это в идеальном мире. 

    Мы попытались выйти из ситуации с помощью реплик БД, к которым поступали бы запросы на чтение. Звучит просто и круто, но те, кто знаком с «рельсовой» кухней и большой связностью данных, моделей и ассоциаций, назвали бы нас психами. Все дело в контекстах инициализации объектов и механизме ассоциаций. Поэтому первым делом под нож пошли запросы статистического характера, большие списки и отчеты.

  3. Octopus. К счастью, на помощь нам пришел Octopus, но и он не обрабатывал все случаи, особенно те версии, которые были совместимы с устаревшей платформой. Например, мы обнаружили, что запись в режиме «реплики» тянет за собой все ассоциативные связи в том же режиме. Это порождает конфликты, связанные с попытками записи в read-only-транзакциях. Пришлось заниматься monkey-патчингом ORM-компонентов. Методом проб и ошибок проблему решили.

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

    Это сыграло с нами злую шутку, потому что обработка таких запросов стала происходить крайне медленно. Впрочем, именно решение этой проблемы вывело нас на качественно новый уровень. 

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

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

Боевое крещение Circuit breaker.
Боевое крещение Circuit breaker.

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

Разделение на микросервисы действительно возможно

«Но ведь верилось же! Казалось, еще немного усилий — и то самое светлое будущее, которое каждый представлял себе по-разному, в зависимости от воспитания и фантазии, обязательно наступит».

Артур Чарльз Кларк. 2001: Космическая одиссея (1968)

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

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