Кейс основан на реальных событиях, однако без упоминания компаний в связи с политикой конфиденциальности и профессиональной этикой.
Содержание
Контекст задачи: заказчик, стек разработки, сроки
Расскажу немного о задаче, чтобы читатель получил о ней примерное представление. В ИТ-компанию обратился один из крупных игроков российского e-commerce рынка с запросом создать «с нуля» продукт, который позволил бы автоматизировать определенные бизнес-процессы.
С заказчиком согласовали следующий стек разработки: язык PHP, фреймворк Symfony, СУБД PostgreSQL, key-value хранилище Redis, контейнеризация через Docker, фронтенд в виде SPA на Vue.js, который взаимодействует с бэкэндом по REST API. Это стандартный набор, на основе которого сделаны тысячи веб-приложений. Казалось бы, проблемам появиться было неоткуда.
Заказчик сразу обозначил дедлайн, и сроки были сильно сжаты. Поэтому быстро собрали проектную команду и также быстро спроектировали архитектуру будущего решения. К задаче подключили наименее занятых в тот момент разработчиков и тестировщиков с других проектов без горящих сроков. Также пригласили в команду внештатного аналитика, а если кого-то не хватало — дополняли аутсорсерами. И в атмосфере стартапа принялись за работу.
Запуск проекта и настройка мониторинга
Забегая вперед, скажу, что команда запустила проект почти вовремя. И несмотря на то, что это был не highload-продукт в строгом смысле слова, все равно снабдила его системами мониторинга и алертинга, которые позволяют отслеживать большинство параметров системы и своевременно реагировать на них.
Так, после ввода платформы в промышленную эксплуатацию, хорошо отслеживалось состояние всех подсистем: потребление ресурсов (процессорные мощности, память, операции ввода-вывода, нагрузка на сеть); количество запросов; коды ответов; скорость ответа и многое другое — вплоть до логов с ошибками.
Более того, все показатели были отражены в динамике — в виде графиков, которые помогали не только оценить текущее состояние системы, но и предсказать тренды, увидеть проблемы задолго до их появления. Именно благодаря системам мониторинга удалось своевременно обнаружить те проблемы, о которых дальше и пойдет речь.
Первые сигналы о проблемах
Впервые некорректное поведение приложения обнаружилось, когда на каналы срочного информирования в Telegram стали поступать уведомления о превышении квот по ресурсам. А точнее — дисковая система превышает квоты IOPS.
Это было удивительно, поскольку чтение-запись на диск не должно было так активно использоваться. Такой логики в коде просто не было.
Тогда решили углубиться в статистику потребления диска и увидели огромное количество операций записи при совершенно незначительном количестве операций чтения. Это было ненормально для продукта: в нем не было потокового сбора больших данных или другой функциональности, которая бы требовала постоянной записи на диск.
Ложный след
Тогда я исследовал систему более детально и, как мне показалось, понял, в чем дело. Redis использовался в режиме персистентного хранилища. Это подразумевает, что он время от времени, в зависимости от настроек, записывает данные на диск, чтобы не потерять их.
Поскольку в Redis хранился только кэш приложения и пользовательские сессии, удалось значительно смягчить требования к их хранению. Ничего страшного не случилось бы, если пара таких записей удалилась после плановой перезагрузки. Поэтому я переконфигурировал Redis, чтобы он значительно реже сохранял данные на диск.
Результат не заставил себя долго ждать — нагрузка на запись сократилась примерно вдвое.
Но кардинально ситуация не изменилась: диск был по-прежнему аномально «перекошен» в сторону записи. А значит, настройки были здесь ни при чем.
Тогда стали изучать, какие данные хранились в Redis. Оказалось, что максимально доступный лимит памяти в 10Gb был полностью исчерпан, несмотря на то, что в хранилище находились всего ~7-8 тысяч элементов.
Это позволило сделать сразу несколько важных выводов:
-
Из-за того, что место в Redis исчерпано, хранилище постоянно пытается освободить память под новые записи согласно своим внутренним алгоритмам. Это требовало немало вычислительных ресурсов платформы, и это нужно было исправить.
-
Несложно подсчитать, что средний размер одной записи составляет ~1,25 Мб — а это очень много. Стали понятны расходы IOPS на графике. Нужно было разобраться и с размером записей.
-
По-прежнему непонятно, почему нагрузка шла преимущественно на запись. Ведь кэш должен считываться гораздо чаще, чем записываться, иначе он будет неэффективен. Это тоже нужно было исследовать.
-
В системе на тот момент были зарегистрированы примерно 3500 пользователей, из которых, согласно статистике, ежедневно работали с приложением только 30%. Количество записей в хранилище (количество сохраненных сессий) при этом было в пять раз больше. Поэтому предстояло также проверить, как они появлялись.
Вершки и корешки. Докопались до истины
Чтобы решить эти проблемы, нужно было сначала понять, что за объемные данные приложение пытается сохранить в кэше.
Ошибка №1: Лишние связи
Сначала изучили содержимое записей в базе данных Redis. К счастью, с кэшем приложения всё было в порядке: небольшие записи с полезной информацией. А вот в записях с сессионными данными пользователя хранилось гигантское количество данных!
Фреймворк Symfony устроен так, что сохраняет в сессию сериализованный объект сущности User. В то же время сущность User — это модель, в которой описаны все необходимые поля. В нашем случае это еще и связи пользователя с другими сущностями, поскольку сложная бизнес-логика нашего приложения завязана на пользователя, его права и другие параметры.
Кроме того, в приложении была реализована собственная система аутентификации для API, основанная на принципе выдачи временного токена доступа. В этой системе вместе с особенностью хранения сессий в Symfony и заключалась основная проблема.
Поскольку дедлайн у проекта был жесткий, команда торопилась. Аутентификацию по токенам быстро написал один из разработчиков, и ее не успели хорошо протестировать. Так в модели пользователя появилась множественная связь one-to-many с выданными ему токенами доступа.
При наличии такой связи объект User нёс в себе все выданные ранее пользователю токены доступа. Более того, «ленивая» загрузка здесь не срабатывала, поскольку все токены считывались из связанной таблицы в процессе аутентификации пользователя. Считывание производилось с помощью следующей конструкции в модели:
Здесь в память загружаются абсолютно все привязанные к пользователю токены. Затем токены фильтруются по совпадению сессионного идентификатора и из полученного набора используется только один. В результате у любого аутентифицированного пользователя «на борту» модели 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. Теперь можно не выделять столько памяти под хранилище.
-
В несколько раз сократились показатель IOPS.
-
Исчез «перекос» по записи в работе с диском.
-
Сократилась вычислительная нагрузка на сервер — стал использоваться ресурс CPU.
-
Сократилась внутренняя сетевая нагрузка. Исчезла необходимость передавать огромные объемы данных между VPS.
-
За счет суммарного снижения потребления ресурсов удалось повысить скорость работы продукта и сформировать потенциал для масштабирования при возможном увеличении нагрузки.
Выводы
Проблему обнаружили вовремя, и это не привело к остановке промышленной эксплуатации платформы. Эта поучительная история позволила провести командную работу над ошибками и сделать несколько полезных выводов, которыми теперь делюсь с вами:
-
На любом проекте система мониторинга жизненно необходима, даже если в начале кажется, что она не нужна. Лучше потратить немного дополнительного времени и внедрить инструменты мониторинга, которые позволят заблаговременно обнаружить потенциальные проблемы.
-
«Быстро поднятое не считается упавшим». Устранить проблему удалось до того, как она привела к аварийной остановке системы, а это огромная выгода для бизнеса и репутации.
-
Спешка при создании продукта приводит к ошибкам. Вывод достаточно «капитанский», но на это, в силу субъективных и объективных причин, многие не обращают внимание. Лучше сразу уделить время корректному проектированию, чем потом в разы больше потратить на исправление ошибок.
-
Несыгранная команда, особенно если она постоянно меняется, хуже реализует проект, чем постоянные квалифицированные сотрудники. Теряется контекст, фокус, качество. Нужно заранее проработать состав команды перед тем, как ввязываться в срочные проекты.
-
Ошибки могут усиливать друг друга. Иногда единичные ошибки трудно заметить, но если они накладываются друг на друга, то это может привести к значительной проблеме. Это как синергия, только в обратную сторону.
***
Команда «КОРУС Консалтинг» может выполнить e-commerce-проект любой сложности. Если у вас остались вопросы или требуются партнеры в разработке e-commerce-проекта для бизнеса — оставьте заявку в форме ниже или напишите на адрес omni@korusconsulting.ru, мы с вами свяжемся и проконсультируем.