редакции
Как мы продлевали жизнь монолита для краудфандинг-платформы
Да, они уступают микросервисам в легкости масштабирования и автономности, но на первых порах монолитную архитектуру куда проще и дешевле поддерживать. Микросервисы вносят дополнительную сложность в управление инфраструктурой и требуют более сложной координации между сервисами.
Поэтому новые продукты часто начинаются именно как монолиты из-за относительной простоты в разработке, а также меньшей требовательности к ресурсам. Когда становится ясно, что продукт нашел свою аудиторию, его команда может сменить тип архитектуры или продолжить развивать монолит.
Наш проект RallyUp оказался перед таким выбором после смены своего позиционирования и последовавшего роста популярности. Изначально он задумывался как платформа для онлайн-краудфандинга на любые цели. Но затем стартап сфокусировался на аудитории благотворительных организаций и быстро добрался до лимитов существующей архитектуры.
Предыстория проекта
Клиент планировал создать аналог Kickstarter, и первоначальное видение проекта включало в себя почти полное копирование возможностей этой платформы: пользователи могли рассказывать о своих проектах, запрашивать средства на их поддержку и получать деньги на воплощение замыслов. За несколько лет существования RallyUp сформировалась новая концепция, и стартап сосредоточился на работе с благотворительными организациями.
В 2015, когда произошла смена направления RallyUp, некоммерческие организации уделяли мало внимания своему присутствию в интернете. Несмотря на удобство и возможности интернет-маркетинга, благотворительные онлайн-кампании практически не проводились. Отчасти это объяснялось отсутствием необходимых функций на платформах по сбору средств.
Чтобы лучше совпадать с запросами новой целевой аудитории, функциональность RallyUp была доработана с учетом специфики благотворительной деятельности. Проекты пользователей превратились в кампании по сбору средств на нужды благотворительности, в том числе в формате аукциона. Была добавлена возможность создавать аккаунты компаний с доступом для нескольких пользователей. Появился стриминг видео для проведения эфиров, освещающих кампании. Благодаря быстрому внедрению этих изменений RallyUp стал одной из первых платформ по переводу деятельности некоммерческих организаций в онлайн.
Однако с приходом на платформу крупных благотворительных организаций, которые часто привлекают для рекламы кампаний звезд вроде Тома Холланда или Эда Ширана, нагрузка на систему резко возросла. Частота запросов иногда достигала 30 000 RPS, и существующая архитектура уже не справлялась с их обработкой.
Проблемы масштабирования монолитной системы
Одной из основных сложностей роста приложения на монолите является увеличение затрат на поддержку кода. Добавление новых технологий или обновление компонентов становится нетривиальной задачей из-за тесной связанности всех частей приложения.
Масштабирование монолитной архитектуры требует увеличения ресурсов всего приложения, даже если только отдельные его части нуждаются в улучшениях. Также существует риск, что при увеличении числа пользователей и объема данных пострадает производительность. При этом любой сбой может привести к отказу всего сервиса из-за единой кодовой базы.
Решение этих проблем может включать постепенный переход к микросервисной архитектуре, разделение приложения на модули и использование инструментов для управления сложностью кода и мониторинга производительности.
В случае с RallyUp, архитектура приложения уже была реализована по методу распределенного монолита. Нам требовалось обеспечить стабильную работу системы при пиковых нагрузках, потратив минимальное количество времени и усилий. Учитывая эти ограничения, мы отложили переход на микросервисы и решили увеличить степень распределения монолита.
Постановка задачи
Итак, на момент повышения нагрузки приложение представляло из себя распределенный монолит на фреймворке с EF/SQL Server. Перед нами стояла цель переделать архитектуру таким образом, чтобы она выдерживала нагрузку в 40.000 RPS.
Анализ производительности
Сперва мы провели анализ каждого запроса с использованием dotTrace — профилировщика, специализирующегося на выявлении проблем производительности приложений на платформе .NET. dotTrace позволяет определить, какие участки кода занимают больше всего времени при обработке запросов.
После выявления узких мест в производительности приложения с помощью dotTrace, мы начали искать утечки памяти с применением dotMemory. Проблемы с памятью в приложении могут иметь различные источники. В нашем случае утечки памяти чаще всего происходили из-за неоптимального использования Change Tracker.
Change Tracker в Entity Framework отслеживает изменения в объектах, связанных с контекстом DbContext. Однако он только регистрирует каждое изменение объекта, а не сохраняет информацию о них в базу данных. Сохранение происходит только при вызове метода DbContext.SaveChanges() . До этого момента Change Tracker накапливает данные об изменениях во время работы приложения, и такая особенность может существенно снизить производительность. При обработке большого количества запросов CPU придется затратить много времени на обход всех изменений объектов во время сохранения.
Существует несколько стратегий для уменьшения этого негативного эффекта, например, отсоединение объектов. В результате анализа мы определили, что для нашей задачи лучше всего подходит использование короткоживущих контекстов, которые хранят минимальное количество изменений.
Оптимизация работы приложения
Сперва мы провели анализ возможностей кэширования на стороне сервера. Затем были определены части кода, которые можно грузить для всех пользователей, и мы отделили их от частей, которые хранят пользовательские данные. Общедоступные части были закэшированы в Redis — это помогло сократить количество обращений к базе данных и SQL-серверу. Такой подход приблизил нас к цели снизить нагрузку на архитектуру и при этом не навредил безопасности пользовательских данных.
Мы также внедрили гибридное кэширование in-memory, при котором данные временно хранятся в оперативной памяти (RAM). Они кэшируются в рамках обработки определенного запроса и могут использоваться для улучшения его производительности. Это дополнительно сократило количество обращений к Redis.
У выбранного нами решения есть как плюсы, так и минусы. С одной стороны, мы улучшили производительность запросов, сократили нагрузку на сервер и повысили масштабируемость. Однако у гибридного кэширования есть недостатки, ключевой из которых — повышенная требовательность к объему оперативной памяти. Помимо этого, данные из кэша могут оказаться не согласованными с базой данных или вовсе потеряться.
Для того, чтобы избежать эти ситуации, мы используем системы мониторинга и автоматического управления кэшем. Они помогают отслеживать объем использованной оперативной памяти, следить за согласованностью данных между кэшем и базой данных, а также проводить регулярную очистку устаревших или малоиспользуемых данных из кэша.
Кроме того, для минимизации потерь данных при перезапусках или сбоях системы, мы регулярно создаем резервные копии критически важных данных из кэша. Это помогает восстановиться после непредвиденных ситуаций и сохранить целостность данных.
Такой подход позволяет нам максимально использовать преимущества гибридного кэширования, минимизируя его недостатки и риски. Регулярное обслуживание и мониторинг играют ключевую роль в эффективной работе данной технологии, обеспечивая баланс между производительностью и надежностью системы.
Проверка отправляемых запросов
После кэширования необходимо уделить внимание нескольким ключевым аспектам.
Важно начать с анализа всех SQL-запросов, которые отправляет приложение, используя Entity Framework. Это позволит выявить возможные проблемные моменты в работе с базой данных. Добавление механизма логирования запросов поможет в последующей оптимизации и контроле процесса выполнения запросов. На данном этапе нужно сделать транзакции максимально короткими, чтобы избежать большого количества эксклюзивных блокировок и не дать пользователям встретиться с проблемами конкуренции при большом потоке обновлений базы данных.
После кэширования всех доступных контрактов стоит тщательно проанализировать стоимость выполнения запросов. При необходимости можно провести оптимизацию структуры данных, добавив индексы или осуществив денормализацию.
Несмотря на наличие гибридного кэша, полагаться на него во всем не стоит: первый раз пользователи запросят данные не из кэша, поэтому важно, чтобы эти данные были быстро получены. В противном случае кэш необходимо будет прогревать заранее и держать постоянно горячим с актуальной информацией.
Частью распределенного монолита RallyUp является процессинговый сервер, обрабатывающий jobs. Важно провести проверку сервера при высоких нагрузках, чтобы избежать возможных проблем с блокировками данных и перегрузкой. Для предотвращения таких сценариев проверяем процессинг-сервер и оптимизируем обрабатываемые им jobs.
Для мониторинга и анализа наиболее частых запросов и типов ожиданий мы пользуемся инструментами AWS. Это помогает выявлять узкие места в работе системы, которые можно оптимизировать, чтобы повысить ее эффективность.
Масштабирование инфраструктуры
В случае с RallyUp мы наблюдали, что при 40 000 RPS нагрузка на Redis составляла 5%, а на базу данных — 20%. Не зная, каким будет пиковое количество запросов, мы увеличили емкость RDS в 4 раза. Для данной системы это увеличение оказалось избыточным, поскольку многие запросы кэшировались в памяти RDS, минуя дисковую систему, и практически не вызывали задержек.
Дополнительно, мы расширили парк веб-серверов, подключенных к балансеру, чтобы избежать возможных ограничений по CPU. Это решение позволило эффективно справиться с потенциальной нагрузкой и обеспечило плавную работу системы даже в условиях роста запросов.
Нагрузочное тестирование
Мы проводили нагрузочное тестирование на облегченной конфигурации RDS, чтобы убедиться, что запросы не будут слишком сильно кэшироваться в оперативной памяти. Это помогло нам понять, как нагрузка может воздействовать на систему. Такое тестирование на облегченной конфигурации RDS дало нам возможность более точно прогнозировать, какую нагрузку система сможет выдержать при масштабировании RDS.
Этот подход позволил нам предугадывать и оценивать реакцию системы на увеличение ее емкости, что было ключевым фактором при принятии решений о масштабировании RDS. Таким образом, благодаря проведенному нагрузочному тестированию на облегченной конфигурации, мы смогли более точно определить возможности системы и успешно подготовиться к ее масштабированию.
Разогрев балансера
При хостинге системы на AWS необходимо учитывать, что балансер нагрузки требует предварительного «разогрева». При необычных или резких изменениях нагрузки балансер обычно не успевает адаптироваться и обработать все поступающие запросы. Из-за таких сбоев система может работать с задержками или даже стать недоступной.
Предварительный «разогрев» балансера помогает ему адаптироваться к изменениям нагрузки заранее, уменьшая риск возникновения проблем и обеспечивая более стабильную работу системы при аномальных или пиковых нагрузках.
Результат
Мы распространили описанный подход на все API, что позволило системе спокойно обрабатывать нагрузку до 40 000 RPS — без промедлений обработки и зависаний.
Благодаря оптимизации благотворительные организации получили возможность приглашать знаменитостей мирового уровня, не беспокоясь о возможных сбоях системы. Также мы успешно проверили свои навыки в оптимизации и масштабировании приложения в условиях постоянно растущей нагрузки.