Свяжитесь с нами
Спасибо

Мы получили заявку и свяжемся с вами в ближайшее время

ВЕРНУТЬСЯ НА ГЛАВНУЮ
Свяжитесь с нами
Ошибка

Не удалось отправить заявку, повторите позже

ПОПРОБОВАТЬ ЕЩЁ РАЗ
Как ошибки проектирования при...
ВРЕМЯ ЧТЕНИЯ
~ 8 мин.
205

Как ошибки проектирования при разработке на Symfony могут привести к перерасходу ресурсов, замедлению работы системы — и как их исправить

Head of development департамент e-commerce
ГК «КОРУС Консалтинг»
Сегодня хочу рассказать о том, какие ошибки можно допустить на начальном этапе создания e-commerce проекта в проектировании модели данных и в разработке веб-приложения. И, самое главное, как эти ошибки исправить: снизить потребление памяти в 1000 раз и кратно уменьшить нагрузку на дисковую систему.

Кейс основан на реальных событиях, однако без упоминания компаний в связи с политикой конфиденциальности и профессиональной этикой. 
1_Нормально делай нормально будет.jpg

Контекст задачи: заказчик, стек разработки, сроки 

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

С заказчиком согласовали следующий стек разработки: язык PHP, фреймворк Symfony, СУБД PostgreSQL, key-value хранилище Redis, контейнеризация через Docker, фронтенд в виде SPA на Vue.js, который взаимодействует с бэкэндом по REST API. Это стандартный набор, на основе которого сделаны тысячи веб-приложений. Казалось бы, проблемам появиться было неоткуда. 

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

Запуск проекта и настройка мониторинга

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

Так, после ввода платформы в промышленную эксплуатацию, хорошо отслеживалось состояние всех подсистем: потребление ресурсов (процессорные мощности, память, операции ввода-вывода, нагрузка на сеть); количество запросов; коды ответов; скорость ответа и многое другое — вплоть до логов с ошибками. 

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

Первые сигналы о проблемах

Впервые некорректное поведение приложения обнаружилось, когда на каналы срочного информирования в Telegram стали поступать уведомления о превышении квот по ресурсам. А точнее — дисковая система превышает квоты IOPS.

Это было удивительно, поскольку чтение-запись на диск не должно было так активно использоваться. Такой логики в коде просто не было. 

2_Disk operation.png

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

Ложный след 

Тогда я исследовал систему более детально и, как мне показалось, понял, в чем дело. Redis использовался в режиме персистентного хранилища. Это подразумевает, что он время от времени, в зависимости от настроек, записывает данные на диск, чтобы не потерять их.

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

3_ложный след.png

Результат не заставил себя долго ждать — нагрузка на запись сократилась примерно вдвое.

4_сокращение нагрузки на запись.png

Но кардинально ситуация не изменилась: диск был по-прежнему аномально «перекошен» в сторону записи. А значит, настройки были здесь ни при чем.

Тогда стали изучать, какие данные хранились в Redis. Оказалось, что максимально доступный лимит памяти в 10Gb был полностью исчерпан, несмотря на то, что в хранилище находились всего ~7-8 тысяч элементов. 

Это позволило сделать сразу несколько важных выводов:

  1. Из-за того, что место в Redis исчерпано, хранилище постоянно пытается освободить память под новые записи согласно своим внутренним алгоритмам. Это требовало немало вычислительных ресурсов платформы, и это нужно было исправить.

  2. Несложно подсчитать, что средний размер одной записи составляет ~1,25 Мб — а это очень много. Стали понятны расходы IOPS на графике. Нужно было разобраться и с размером записей.

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

  4. В системе на тот момент были зарегистрированы примерно 3500 пользователей, из которых, согласно статистике, ежедневно работали с приложением только 30%. Количество записей в хранилище (количество сохраненных сессий) при этом было в пять раз больше. Поэтому предстояло также проверить, как они появлялись. 

Вершки и корешки. Докопались до истины

Чтобы решить эти проблемы, нужно было сначала понять, что за объемные данные приложение пытается сохранить в кэше. 

Ошибка №1: Лишние связи 

Сначала изучили содержимое записей в базе данных Redis. К счастью, с кэшем приложения всё было в порядке: небольшие записи с полезной информацией. А вот в записях с сессионными данными пользователя хранилось гигантское количество данных!

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

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

Поскольку дедлайн у проекта был жесткий, команда торопилась. Аутентификацию по токенам быстро написал один из разработчиков, и ее не успели хорошо протестировать. Так в модели пользователя появилась множественная связь one-to-many с выданными ему токенами доступа.

код_1.png

При наличии такой связи объект User нёс в себе все выданные ранее пользователю токены доступа. Более того, «ленивая» загрузка здесь не срабатывала, поскольку все токены считывались из связанной таблицы в процессе аутентификации пользователя. Считывание производилось с помощью следующей конструкции в модели:

код_2.png

Здесь в память загружаются абсолютно все привязанные к пользователю токены. Затем токены фильтруются по совпадению сессионного идентификатора и из полученного набора используется только один. В результате у любого аутентифицированного пользователя «на борту» модели User были все выданные ранее токены.

Ошибка №2: Неактуальные токены

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

Ошибка №3: Забыли про режим stateless

Но и это еще не все. Проявиться двум предыдущим ошибкам “помог” третий промах: из-за него ускорялся рост числа выданных токенов.

В рамках платформы использовались два API. Первое — основное REST API для взаимодействия фронтенда с бэкэндом. Второе — API, которое реализует протокол SCIM для управления пользователями, проходящими аутентификацию через сторонний сервис SSO.  Заказчик хотел управлять такими пользователями (создавать, удалять, изменять) посредством другой подсистемы с помощью вызова back-to-back запросов в специально разработанное API. Эти запросы не должны были сохранять сессию, поскольку они идемпотентны и не передают друг другу контекста. 

SCIM API в продукте создавал внешний разработчик. Он забыл написать всего одну строчку в файле конфигурации, которая бы отключила создание и сохранение сессий — то есть, сделала бы API stateless. При этом API закрыто «на замок» с помощью «белых» IP и отдельной аутентификации, чтобы неавторизованные пользователи им не воспользовались. 

Внутренняя система заказчика активно использовала SCIM API, каждый запрос которой в результате при аутентификации служебного пользователя генерировал новую сессию. Затем в таблицу выданных токенов записывался только что выданный новый токен. А поскольку на каждый запрос создаются и затем сохраняются данные пользователя со всеми существующими токенами, то в сессиях копилось всё больше и больше бесполезных данных. Так и произошел перекос в сторону операций записи.

Решение проблемы

По мере нахождения ошибок, предпринимались соответствующие меры по их исправлению. И, в течение пары недель последовательных изменений, проблема была полностью решена:

  • Переписали систему выдачи токенов, чтобы в БД хранились только актуальные токены;

  • Отвязали токены от модели пользователя и сделали связь односторонней,  чтобы в сессию попадал более чистый объект пользователя; 

  • Добавили в SCIM API режим stateless, чтобы вызовы больше не генерировали новые сессии.

В результате этой работы получилось неплохое «комбо» профита:

  • Более чем в 1000 раз сократилось использование памяти под хранилище Redis. Теперь можно не выделять столько памяти под хранилище.

5_сокращение использования памяти.png
  • В несколько раз сократились показатель IOPS. 

6_сокращение показателя IOPS.png

  • Исчез «перекос» по записи в работе с диском.

7_перкоса по записи.png

  • Сократилась вычислительная нагрузка на сервер — стал использоваться ресурс CPU. 

8_вычислительная нагрузка на сервер.png

  • Сократилась внутренняя сетевая нагрузка. Исчезла необходимость передавать огромные объемы данных между VPS. 

9_внутренняя сетевая нагрузка.png

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

Выводы

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

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

  2. «Быстро поднятое не считается упавшим». Устранить проблему удалось до того, как она привела к аварийной остановке системы, а это огромная выгода для бизнеса и репутации.

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

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

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

Опубликовано на Хабре

***

Команда «КОРУС Консалтинг» может выполнить e-commerce-проект любой сложности. Если у вас остались вопросы или требуются партнеры в разработке e-commerce-проекта для бизнеса — оставьте заявку в форме ниже или напишите на адрес omni@korusconsulting.ru, мы с вами свяжемся и проконсультируем.
Закажите бесплатную консультацию эксперта

Подпишитесь на наши обновления

Раз в месяц присылаем полезные материалы и новые статьи из блога.

Некорректно заполнен e-mail

Читайте также

Архитектура маркетплейсов: всё, что нужно знать об организации и запуске ~14 мин. 20 апреля 2024
E-COMMERCEМАРКЕТПЛЕЙСЫE-GROCERY
КОРУС Консалтинг
Что такое OMS-система и как она помогает бизнесу расти ~14 мин. 28 марта 2024
E-COMMERCEИНТЕРНЕТ-МАГАЗИНЫB2B E-COMMERCE
КОРУС Консалтинг
Зачем нужен клиентский портал и чем он отличается от торговой площадки в B2B ~ 8 мин. 21 декабря 2023
E-COMMERCEB2B E-COMMERCEOMNICHANNELCUSTOMER EXPERIENCE
КОРУС Консалтинг
Подпишитесь на наши обновления

Раз в месяц присылаем полезные материалы и новые статьи из блога.

Некорректно заполнен e-mail


наверх