Добрая четверть моего рабочего времени за последний год ушла на обновление архитектуры Учи.ру. С ростом продуктов и количества пользователей увеличился и клубок зависимостей в монолите. Выделяя из него части и набивая на этом пути шишки, я не раз задумывался о том, как мы к этому пришли. Волей-неволей вспоминаешь, с чего все начиналось.
В этом посте я попробовал собрать историю архитектуры Учи.ру. В нем нет фрагментов кода и исчерпывающих технических подробностей. О нашем опыте выделения микросервисов для решения некоторых задач образовательной платформы — в следующих публикациях.
«Геометрическое совершенство монолита воспринималось людьми как некий безмолвный вызов, оно поражало не меньше, чем другие свойства загадочной находки».
Артур Чарльз Кларк. 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. Стало понятно, что обуздать монолит реально. Отправной точкой стало выделение откровенно обособленного функционала с минимальной степенью связности. На подобных некорневых блоках мы и сосредоточили усилия.
Генератор PDF-сертификатов. Попрактиковались мы на микросервисе, который должен выдавать готовый документ по входным параметрам. Фактически мы сделали классический распределенный монолит с синхронным сетевым взаимодействием.
Олимпиадная платформа. Ее отделение от «большого брата» стало важным шагом. Стоит пояснить, что олимпиады выросли в отдельное направление: они бурно расширялись, и разработчикам приходилось постоянно оглядываться на другие части системы из-за большого риска сломать имеющийся функционал. Эта скованность мешала развиваться — хотелось независимости. В итоге команда олимпиад повысила свои компетенции и создала прецедент выделения реально большого куска из монолита.
Развивающие игры. Используя опыт «олимпиадников», мы создавали игры как отдельный компонент, связанный с монолитом авторизацией, но способный развиваться самостоятельно.
Модуль доступа к данным. Перед тем как что-то отделить, мы проделали большую работу по устранению связности компонентов системы. Только в процессе выпиливания олимпиадной платформы мы нашли около десяти устаревших таблиц и сущностей и удалили их, когда выделили функционал в отдельный проект, а ведь во всем монолите есть и другие.
Сначала монолит и новый сервис использовали общую базу данных. Мы старались по возможности переносить операции записи ответственных данных в новые БД, а из монолита использовать их в режиме чтения.
Внутренний системный интерфейс доступа к данным создали в виде отдельного программного модуля, транспортный уровень которого располагался в защищенной подсети, что избавило от необходимости вводить авторизацию и подписи. Идея заключалась в том, чтобы через новый модуль предоставить доступ компонентам «большого брата» и избавиться от необходимости делить общую базу между монолитом и олимпиадами.
Для разработки этого модуля мы использовали библиотеку JSONAPI Resources, которая сочетает в себе гибкость, низкую скорость и синтаксис, очень похожий на классический ActiveRecord. Залогом успеха стало стремление группы энтузиастов, которые во что бы то ни стало хотели достичь нужного эффекта.
Справляемся с высокими нагрузками
Дальше на пути разделения сервиса нас настиг карантин, который стал новым испытанием для компании в целом и нашей команды в частности. Нелинейный рост трафика обнажил узкие места синхронной распределенной архитектуры и монолита. Система переставала справляться с потоком запросов, а времени на реагирование было очень мало, потому что на рынке началась жесткая конкуренция и борьба за выживание.
Кеширование. Это было первое, что пришло в голову. В качестве инструмента мы использовали Redis. Чтобы не заниматься поддержкой кластера в облаке, мы решили заменить его урезанной версией Redis от Envoy с шардированием по ключу хранения. В результате система стала держать нагрузку лучше, но это было только начало.
Реплики БД. После очередного скачка трафика мы уперлись в лимит пула соединений с БД. Теоретически работа с микросервисами должна была помочь в масштабировании самых нагруженных частей системы или вовсе избежать этой проблемы. Но это в идеальном мире.
Мы попытались выйти из ситуации с помощью реплик БД, к которым поступали бы запросы на чтение. Звучит просто и круто, но те, кто знаком с «рельсовой» кухней и большой связностью данных, моделей и ассоциаций, назвали бы нас психами. Все дело в контекстах инициализации объектов и механизме ассоциаций. Поэтому первым делом под нож пошли запросы статистического характера, большие списки и отчеты.
Octopus. К счастью, на помощь нам пришел Octopus, но и он не обрабатывал все случаи, особенно те версии, которые были совместимы с устаревшей платформой. Например, мы обнаружили, что запись в режиме «реплики» тянет за собой все ассоциативные связи в том же режиме. Это порождает конфликты, связанные с попытками записи в read-only-транзакциях. Пришлось заниматься monkey-патчингом ORM-компонентов. Методом проб и ошибок проблему решили.
Оптимизация обработки запросов. Следующим отказало синхронное межсервисное взаимодействие. Поскольку основная часть трафика традиционно проходила через главную страницу сайта и кабинет ученика, сборка страницы осуществлялась на сервере. При формировании страничек подобного типа приходится дергать несколько сервисов для получения нужной информации, например, недавно выделенный олимпиадный.
Это сыграло с нами злую шутку, потому что обработка таких запросов стала происходить крайне медленно. Впрочем, именно решение этой проблемы вывело нас на качественно новый уровень.
Пришлось экспериментировать с сегментацией входящего трафика на различные инстансы одного приложения. Вариант с кешированием горячих данных в этом случае уже не зашел на ура, как это было ранее. Через эту страницу проходило огромное количество пользователей, и вероятность попадания данных в кэш оказалась крайне низкой.
Более того, внешние сервисы не обладали необходимыми данными для обработки входящих запросов типа «сервер — сервер» и были вынуждены запрашивать их. Все это приводило к появлению каскадных зависимостей, образованию очередей и, как следствие, отказу в обслуживании.
Но зато, набив эти шишки, мы познакомились с Circuit Breaker и научились его применять, что позволило стабилизировать ситуацию. В этот момент мы поняли, что более гибкая асинхронная архитектура с дублированием данных значительно бы улучшила положение вещей, хотя и не решила бы проблемы полностью.
Разделение на микросервисы действительно возможно
«Но ведь верилось же! Казалось, еще немного усилий — и то самое светлое будущее, которое каждый представлял себе по-разному, в зависимости от воспитания и фантазии, обязательно наступит».
Артур Чарльз Кларк. 2001: Космическая одиссея (1968)
Хочу еще раз подчеркнуть, что разделение системы на микросервисы пока проходит в экспериментальном режиме — это инициатива нескольких команд, которые делают продукт практически с нуля. Но мы уже получили положительные результаты, которые помогут платформе развиваться в ближайшем будущем. Конечно, помимо перечисленных трудностей возникают и другие — о них я постараюсь рассказать в следующих постах.
В любом случае мы оценили преимущества асинхронной архитектуры и распределенных систем, а главное — убедились на своем опыте, что распилить запутанный монолит вполне реально. При этом стало понятно, что ответственность — это не одностороннее понятие и для успешного развития платформы бизнес должен с пониманием относиться к проблемам технических отделов, оставляя время и ресурсы на устранение долгов. А IT-специалистам не стоит скромничать — нужно заявлять о такой потребности, чтобы была возможность вовремя провести «субботник». Лично я теперь убежден, что выявление некоторого стабильного фундамента системы из нескольких сервисов позволит нам унифицировать платформы, а также решить проблему мертвого кода.