На входе: маркетплейс на 1C-Битрикс с большим количеством легаси, в сезон около 1 млн уникальных посетителей в месяц, RAM - 54GB выделено на сервере под работу сайта, rps - ~150.
Текущая проблематика: неоптимальный механизм фильтрации, а именно не хватает ресурсов памяти, постоянно вылетаем в 502/503/504 ошибки, падает конверсия, блокируется работа фильтрации.
Тимлид - @guriianova
Изначальный механизм работы
Ресурсозатратность. Redis
Работа с товарами на сайте происходила не напрямую через mySQL, а с помощью Redis. При обновлении/добавлении товаров, они добавляются в БД, затем обновляются в Redis.
Список товаров в Redis дублирует многомерную структуру (рис. 1) CIBlockElement::GetList (API Bitrix). Но поскольку фильтровать товары с такой структурой было не очень удобно (так как на основе ключа нужно и фильтровать товары, и в последующем рендерить), был реализован еще один список в одномерном представлении (рис. 2) “ключ-значение”, где ключ - код свойства, а значение - непосредственно значение этого ключа. На этом этапе около 7 лет назад объем занимаемой оперативной памяти увеличивается из-за дубля данных. Стоит отметить, что тогда номенклатура насчитывала ~50к товаров, а сейчас их ~300к, что уже становится ресурсозатратным.
Node.js
Механизм фильтрации был реализован на Node.js причем достаточно неоптимально. Приложение обращалось в Redis, считывало всю информацию по товарам в переменную для последующего использования в фильтрации. Фильтрация происходила на уровне приложения, оперируя данными по товарам в переменной: итеративно пробегает по каждому свойству каждого товара для проверки совпадения по выбранному пользователем фильтру. Когда товарная база была небольшой, один экземпляр приложения вполне справлялся с этой задачей, но к 2021г. мы пришли к пяти(!) запущенным экземплярам. А поскольку каждый из них занимает объем оперативной памяти для хранения информации по товарам в переменной - мы имеем 5 дублей данных в памяти (по одному на каждый запущенный экземпляр приложения). Один экземпляр приложения при старте потреблял ~8Gb, далее во время работы 3,5-4,5Gb, что в сумме давало ~17,5 - 40Gb занятых только на процесс фильтрации.
Также бизнесу важно иметь техническую возможность по увеличению товарной базы и трафика, что приведет к тому, что и пять запущенных экземпляров нам не хватит.
А что по поддержке?
Поддержка также была трудозатратной. Например, чтобы просто добавить тултип к значению фильтра, нужно было:
добавить свойство обмена на стороне bitrix в процесс обновления ключа, содержащего настройки фильтрации (настройки, определяющие фильтры к разделам, их названия и типы)
внести изменения в компонент работы с фильтром на стороне bitrix
внести изменения в интерфейс и логику приложения настройки фильтров (yii)
внести изменения в приложении Node.js при получении данных от сервиса настройки фильтров и передаче их в bitrix.
Помимо вышеперечисленных задач внешний сервис, на котором контент-менеджеры настраивали фильтрацию был интуитивно непонятен.
И самой большой проблемой стало то, что при некорректной работе фильтра найти исходную проблему и отладить ее достаточно проблемно. То есть контент-менеджер приходит с проблемой “не работает”, а где именно и что не работает - идешь и ищешь, не представляя с чего начать.
В связи с вышеперечисленными проблемами было принято решение перерабатывать процесс фильтрации.
Выделили 3 основных цели переработки фильтрации:
уменьшить затрачиваемые ресурсы сервера
упростить поддержку
не усложнять процесс редактирования фильтров
В качестве технологий было принято решение использовать:
mySQL как хранилище настроек фильтров для отображения колонки фильтров по разделам под кешем
Redis как хранилище товаров, соответствующих фильтру или набору фильтров
RabbitMQ как брокер очередей, чтобы убрать с хитов индексацию товаров подходящих под тот или иной фильтр
Спойлер: в дальнейшем нам понадобится ElasticSearch, но мы пока об этом не знаем.
Процесс переработки.
Логическая модель данных.
Поскольку у нас не было задачи визуально менять публичный интерфейс, было принято решение построить структуру в базе на основе текущего. В соответствии с чем мы получили 4 таблички в mySQL (рис. 3).
Группа фильтров: основные группы фильтрации для визуального разграничения фильтров по группам в публичной части. Например, размеры (ширина, длина), производитель (страна, бренд) и т.д.
Свойства фильтров: таблица с содержанием самих фильтров с типами. Например, цена - диапазон, цвет - чекбокс и т.д.
Значения свойств фильтров: значения чекбокс фильтров. Например, фильтр “цвет” содержит значения “синий”, “красный” и т.д.
Привязка фильтров к разделам: указание фильтров для отображения в разделе. Например, в разделе “люстры” отображается фильтр “цоколь лампы”, а в разделе комоды “цвет фасада” и тд.
Корневым разделам (на текущем сайте это “свет”, “мебель” и тд) задаются фильтры для отображения. Если каким-то из дочерних разделов (например, “настольные лампы” или “шкафы”) нужны иные фильтры, их можно переопределить. Признак наличия привязки фильтров у раздела отображается соответствующей иконкой.
Управление настройками фильтров
Контент-менеджеры используют человеко-понятные правила для создания фильтров на основе базовых свойств. Это решение хорошо прижилось, позволяет настраивает абсолютно любые фильтры в абсолютно любом разделе, поэтому было принято оставить текущую логику по настройкам правил подбора товаров к фильтру. Именно по этой причине при принятии решения о переработке мы не могли воспользоваться готовыми решениями типа “умный фильтр” bitrix. Это решение позволяет на основе базовых свойств создавать любые фильтры. Например, комоды имеют свойство “цвет”, используя его, мы можем создать фильтр “оттенок” со значениями “светлый”, “темный”, “детский”, используя правило и последующую индексацию товаров под правило.
Правила контент-менеджерами пишутся, используя определенные конструкции, а впоследствии проверяются на синтаксис и переводятся в синтаксис PHP.
Как происходит индексация товаров
На этапе, когда контент-менеджер сохраняет данные (например, правило для фильтра “оттенок светлый” ) нам нужно проверить 300к товаров (если фильтр задан для всех разделов каталога): подходит ли каждый из них под это правило. Но мы не можем позволить себе делать такое количество запросов в БД, так как это даст большую нагрузку на саму БД.
В соответствии с чем, было принято решение хранить подходящие для фильтра товары в Redis. При сохранении/обновлении фильтра мы имеем php правило, по которому можем проверить подходит ли под правило каждый из товаров каталога. Пачками получаем товары (опять же из Redis) и итеративно проверяем соответствие товара правилу.
В итоге в redis по каждому из фильтров мы имеем множество подходящих товаров. %filter.id%:{2,3,5,7,168}. При запросе пользователя на сервер во время фильтрации приходит список запрашиваемых id фильтров (%filter.id%), на сервере выполняется запрос к Redis по получению множеств (с пересечением и/или объединением в зависимости от запрашиваемых фильтров) с соответствующими ключами (%filter.id%), в результате мы получим одно множество подходящих товаров под запрос пользователя.
И казалось бы всё ок, но что делать с диапазонами, ведь Redis не умеет отдавать товары по запросам ><=, а мы помним, что пользователю нужны диапазоны в таких фильтрах как “цена”, “ширина”, “длина” и тд.
Мы могли бы выделить ключи диапазонов по каждому из свойств 0-5000 уе, 5001-10000 уе, забирать по ним множества, а далее фильтровать на основе прямых значений по свойствам, но это все слишком сложно и увеличит время операции подбора товара под диапазон, так как внутри диапазона придется отбирать товары на ходу, потому что если пользователь выберет ценовой диапазон, к примеру, 2500-3600, нам придется забрать все товары из ключа-диапазона “0-5000” и далее отсеивать лишнее на уровне php. Вариант оставить доступные диапазоны чекбоксами вместо ввода “от” и “до” для пользователя нам тоже не подходит, так как это ограничит пользователя в выборе диапазона и маркетинг не одобрил такой подход.
Тут на помощь к нам приходит ElasticSearch. Собираем товары со свойствами типа “диапазон”, создаем индекс, индексируем товары со свойствами диапазонов. При запросе диапазона пользователем, например, “цена от 3000 до 5000 руб.” генерируем запрос в elasticsearch ( ><= ), полученное множество пересекаем с дальнейшими полученными из Redis, если такие есть в запросе.
Таким образом мы избавились от необходимости перебирать товары на ходу.
Вынесение индексации на фон
При всех доработках выше мы получаем список необходимых к выполнению процессов:
индексация всех товаров, подходящих под правила при их изменении/добавлении и для первого релиза
индексация товара под все правила при изменении/добавлении товаров
Такие процессы мы не можем оставить на хит по факту происхождения события, так как они достаточно ресурсозатратны (ведь нужно проверить под 1 правило около 300к. товаров, а эта операция в худшем случае занимает 3 минуты). Контент-менеджеру при этих условиях, редактируя фильтры, придется ждать по несколько часов. Поэтому было принято решение унести их на фон с помощью брокера очередей.
При работе мы выделили 3 процесса:
Обработка правил, т.е. если кто-то из контент-менеджеров меняет/редактирует какое-то правило нам нужно переиндексировать до 300к. товаров (прогнать под это правило товары, 300к - если фильтр общий и имеет отношение ко всем разделам каталога)
Обработка изменений диапазонных характеристик товара - переиндексация в ElasticSearch. Изменение диапазонных характеристик товара (цена, размер и тд), т.е. необходимо переиндексировать товар под все правила для сохранения актуальной выдачи фильтров
Обработка изменений чекбокс характеристик товара - переиндексация в Redis.
Таким образом контент-менеджер при сохранении свойства увидит статус “в обработке” и сможет приступить к редактированию других фильтров. Когда сообщение будет обработано брокером, статус свойства изменится на “обработано” и результат можно будем проверить в публичной части.
Вместо заключения
Почему мы не переложили всю работу фильтра на ElasticSearch? В таком случае нам пришлось бы индексировать в elastic все свойства каталога, а поскольку все товары у нас уже есть в redis - в этом нет смысла, мы только продублируем данные, поэтому нам достаточно проиндексировать товары и сохранить фасетные индексы в redis.
Полностью переработав механизм фильтрации мы:
Упростили поддержку и, как следствие, сократили затраты на последующую доработку
Обеспечили контент-менеджерам удобство редактирования
Избавились от блокера для дальнейшего масштабирования системы
Ну и, конечно, освободили в среднем 40% RAM (от 17,5 GB до 40GB)
На текущий момент фоновые процессы по переиндексации правил при наличии сообщений в очереди потребляют в пределах 190MB на переиндексацию товаров по правилам.
Комментарии (37)
Akuma
02.05.2022 14:54+12Соглашусь с комментарием выше. Всю статью думал, почему просто не выкинуть нафиг все самодельные прослойки и не поставить тот же эластик?
Учитывая, что у вас строгая фильтрация, то и MySQL должна справляться, но ладно уж. Эластик будет делать все что вы сами понаписали через Redis, только эффективнее и без всего это геморроя с перестроением индексов. Просто при обновлении товаров помещайте их в эластик - дальше делайте запрос напрямую в него.
IQ_Dev Автор
02.05.2022 15:07-5В ответе выше раскрыли вопрос
edo1h
02.05.2022 18:53+2вопрос «почему не обойтись одним мускулом» не раскрыли.
IQ_Dev Автор
02.05.2022 20:36У нас была цель избавиться от ресурсоемкого приложения (на nodejs) как можно скорее, и мы решили переместить фасетный индекс в redis. Конечно, можно было это реализовать на базе mysql, но redis будет побыстрее
Lexicon
03.05.2022 01:36+1Я может уже сплю в час ночи, но после абзаца про то, что в Node делается фильтрация прямо в памяти, что есть 5 копий, что почему-то рост трафика и числа товаров сделает больше копий, и пункта в следующем абзаце, что мол хочется "внести изменения в приложении Node.js", нет ни одного вхождения о дальнейшей судьбе ноды в статье.
Чем нода хуже остального зоопарка, как технология, пытались ли ее оптимизировать, пропала ли она из проекта, - все за кадром.
IQ_Dev Автор
03.05.2022 15:27в случае данного проекта избавились от ресуемкого приложения, большой его минус был в трудной поддержке, оно было не расширяемо, потому что проект легаси, без документации, тестов, изменения в коде влияло на многие вещи и приводило к непредвиденным ошибкам, плюсом его роль была просто посредником между сайтом и редис, что было лишним, так как сайт при наличии фасетных индексов может сам получать отфильтрованные множества без необходимости перебирать итеративно товары на удовлетворение условиям фильтра
OkunevPY
02.05.2022 17:38+19Почему бы не выкинуть битрикс, развели зоопарк во главе с химерой в лице битрикса, и героически бьються с мельницами и франкештейном в лице битрикса. Время потраченное на ковырянее в плохой архитектуре можно было с лёгкостью потратить на написание хорошо спроектированного портала для которого ни жалкие 150 rps, а даже 1500 rps будут не проблемой. Вообще в нашем мире говорить о 150 rps говорить мягко говоря уже не прилично, лет 10-20 назад когда только начал активно набирать обороты e-commerc и когда негде было черпать ответы как же это делаеться, тогда ещё да.
Вообще не понятно о чём и для кого статья, выглядит как, мы не очень умеем готовить redis, поэтому нам понадобился эластик, но мы не понимаем зачем, поэтому теперь есть и то и другое, и ещё mySql. И какие мы молодцы, не понимая как выжать максимум из того что есть, размазали везде по чуть-чуть, и стало сильно лучше.
guriianova
02.05.2022 20:38+1>> Почему бы не выкинуть битрикс?
Клиент на текущий момент не готов отказаться от битрикс.
>>говорить о 150 rps говорить мягко говоря уже не приличноэто было не с целью "похвастаться", а просто рассказать о вводных данных проекта
>>поэтому нам понадобился эластик, но мы не понимаем зачем, поэтому теперь есть и то и другое, и ещё mySqlвся архитектура в таком виде живет на проекте уже более 7 лет и реализована не нами, нам нужно было просто в кратчайшие сроки привести проект в стабильное состояние, внося минимальное количество изменений, мы добавили только индексацию range свойств в эластик. Идея была в том, чтобы избавиться от лишнего сервиса управления фильтрами и лишнего, отжирающего много памяти, node.js
DrPass
02.05.2022 17:51+10нам нужно проверить 300к товаров, подходит ли каждый из них под это правило. Но мы не можем позволить себе делать такое количество запросов в БД
(ведь нужно проверить под 1 правило около 300к. товаров, а эта операция в худшем случае занимает 3 минуты)
Вот за это я и недолюбливаю некоторый современный инструментарий. 15 лет назад подобная операция занимала секунды на сервере с 2Гб памяти, и требовала один запрос в БД. И полагаю, сейчас тоже так можно :)IQ_Dev Автор
02.05.2022 19:43-2>>15 лет назад подобная операция занимала секунды на сервере с 2Гб памяти, и требовала один запрос в БД. И полагаю, сейчас тоже так можно
такой запрос в БД учитывая изначальное правило фильтра, например, синий"( ( group_color НЕ ПУСТОЙ ) И ( ( group_color СОДЕРЖИТ "Синяя" ) ) ) ИЛИ ( ( group_color ПУСТОЙ ) И ( ( ( tabletop_group_color СОДЕРЖИТ "Синяя" ) ) ИЛИ ( ( ( tabletop_group_color ПУСТОЙ ) ИЛИ ( tabletop_group_color = "Зеркальная" ) ИЛИ ( tabletop_group_color = "Неокрашенная" ) ) И ( ( ( facade_group_color_m СОДЕРЖИТ "Синяя" ) ) ИЛИ ( ( ( facade_group_color_m ПУСТОЙ ) ИЛИ ( facade_group_color_m = "Зеркальная" ) ИЛИ ( facade_group_color_m = "Неокрашенная" ) ) И ( ( ( covering_group_color СОДЕРЖИТ "Синяя" ) ) ИЛИ ( ( ( covering_group_color ПУСТОЙ ) ИЛИ ( covering_group_color = "Зеркальная" ) ИЛИ ( covering_group_color = "Неокрашенная" ) ) И ( ( armat_group_color_m СОДЕРЖИТ "Синяя" ) ) ) ) ) ) ) ) )"
будет достаточно сложно написать, учитывая архитектуру таблиц битрикса и количество свойств у товаров, тем более когда множественные свойства лежат в отдельной таблице. Такие запросы дадут большую нагрузку на таблицы каталога, а с ними непрерывно работает обмен товаров. А есть свойства фильтра, у которых правила сложнее, причем условие не полное равенство, а вхождение, что потребует like запроса
худший случай - это правило с большим количеством условийJordanCpp
03.05.2022 01:20+3У вас свойства хранятся как EAV? Вы не рассматривали просто настроить mysql, что бы все данные поместились в кеш бд? По крайней мере это лучше, чем закат солнца вручную) Ну и Битрикс как бы такое себе. Но понимаю, что Легаси и сопутствующие проблемы.
IQ_Dev Автор
03.05.2022 15:26на фильтры накладываются определенные бизнес правила, и тут недостаточно стандартной битриксовой реализации фильтров на модели EAV, и их кешированием, (если мы вас правильно понимаем), нужно было “готовить” фасетный поиск по требованиям бизнеса, и оперативно реагировать на их изменения
x-tray
02.05.2022 23:29Вывод не использовать битрикс а переписать на (laravel, yii)
Битрикс это зло ...
JordanCpp
03.05.2022 02:04+3Я думаю вы жертвы Легаси) Но плюсую за то, что с помощью палок, смогли улучшить, то что уже как бы и не предполагает улучшения и даже уже не живо.:) Это тоже не плохой опыт. Желаю вам запилить ещё статью, как вы выпилили Битрикс, перешли на современный стек opensource и улучшили rps на +100500.
nikweter
03.05.2022 06:44+9Блин, чего вы такие злые? Минусуете автора еще… Ясно же что не программисты принимали решение где и чего костылять. Клиент ставит совершенно определенную задачу — вот эта штука работает медленно, а надо чтобы быстро. И выделяется на решение определенная сумма золотых. Никто не будет переписывать все. Не возьметесь вы — возьмется кто-то другой. И все равно в итоге получится вот такой франкенштейн.
Ну и в этих рамках, в принципе, сделано в итоге нормально.
antonkrechetov
03.05.2022 10:43А могли бы вместо вот этого всего просто положить данные в реляционную БД (тот же MySQL, ага).
IQ_Dev Автор
03.05.2022 15:38весь каталог уже хранился в redis, мы просто использовали то, что есть от redis и добавили лишь фасетные индексы
aceofspades88
03.05.2022 12:11Два раза начинал читать и так и не смог осилить до конца, в чем проблема то? Могу ошибаться, но 150rps и 300k записей в бд это же дефолтный мускул с адекватной схемой на дешевеньком сервере и больше ничего
IQ_Dev Автор
03.05.2022 15:33проблема была в прежней некорректной реализации фильтров, нескольких лишних сервисах и 17-40Gb потребляемой оперативной памяти на процесс фильтрации товаров
tuxi
03.05.2022 12:22+1Заказчик увяз в битрексе — в этом проблема. Насколько я понимаю, отказаться от битрикса — это значит рисовать свою систему управления товарами, со всеми этими формочками и свистоперделками плюс все равно требуется интеграция с учетной системой.
это был ответ к этому сообщениюIQ_Dev Автор
03.05.2022 15:32да, отказаться от битрикса на текущий момент невозможно, это потребует огромное количество ресурсов, так как все процессы на нем завязаны
danSamara
03.05.2022 14:03Решал подробную задачу, когда надо было "дёшево и сердито", без добавления новых сервисов, БД и в краткие сроки.
Добавил товарам новую колонку hstore, в которой хранил значения фильтров (filter_id: value). В результате, работа с каталогом не потребовала особых модификаций, однако для администрирования фильтров пришлось писать фоновую задачу переиндексации товаров - фильтры, кроме категорий и eav атрибутов, были завязаны на дополнительные условия.
citius
03.05.2022 22:41Почему не Manticore (форк Sphinx Search)? Гладко ложится на mysql, умеет в фасетный поиск, лишних ресурсов не жрет, работает очень быстро. Если использовать как прозрачное прокси между клиентом и базой не требует индексации.
vooft
А почему бы совсем не выкинуть редис и заменить его эластиком? Все равно ведь уже втащили.
IQ_Dev Автор
В заключении статьи написали, что «нам пришлось бы индексировать в elastic все свойства каталога, а поскольку все товары у нас уже есть в redis - в этом нет смысла, мы только продублируем данные, поэтому нам достаточно проиндексировать товары и сохранить фасетные индексы в redis.»
Немного раскроем:
в разрезе этого проекта это достаточно трудозатратный процесс, а мы были очень ограничены по времени, чтобы наладить стабильную работу сайта. Чтобы уйти от редис как от хранилища данных, придётся переписать весь каталог и все процессы оформления заказа, так как весь каталог работает из редис, иначе мы просто продублируем данные, что не есть хорошо
vassabi
вы же и так пересекаете множества из редиса с эластиком? Ну так перенесите всю логику в эластик и пересекайте "всё из редиса" с "то что нужно из эластика"
IQ_Dev Автор
не совсем поняли, что вы имеете ввиду, если мы все перенесем в эластик, тогда не нужно ничего будет пересекать с редисом, в таком случае мы получим дубль данных по товарам в эластике, а чтобы от него избавиться придется переписать каталог с редиса на эластик - это пеработка, которая не вписывается в трудозатраты по текущим приоритетам клиента
vooft
Ну т.е. в качестве решения накинули еще техдолга.