На входе: маркетплейс на 1C-Битрикс с большим количеством легаси, в сезон около 1 млн уникальных посетителей в месяц, RAM - 54GB выделено на сервере под работу сайта, rps - ~150. 

Текущая проблематика: неоптимальный механизм фильтрации, а именно не хватает ресурсов памяти, постоянно вылетаем в 502/503/504 ошибки, падает конверсия, блокируется работа фильтрации. 

Тимлид - @guriianova

Изначальный механизм работы

Ресурсозатратность. Redis

Работа с товарами на сайте происходила не напрямую через mySQL, а с помощью Redis. При обновлении/добавлении товаров, они добавляются в БД, затем обновляются в Redis. 

Список товаров в Redis  дублирует многомерную структуру (рис. 1) CIBlockElement::GetList (API Bitrix). Но поскольку фильтровать товары с такой структурой было не очень удобно (так как на основе ключа нужно и фильтровать товары, и в последующем рендерить), был реализован еще один список в одномерном представлении (рис. 2) “ключ-значение”, где ключ - код свойства, а значение - непосредственно значение этого ключа. На этом этапе около 7 лет назад объем занимаемой оперативной памяти увеличивается из-за дубля данных. Стоит отметить, что тогда номенклатура насчитывала ~50к товаров, а сейчас  их ~300к, что уже становится ресурсозатратным.

Рисунок 1. Многомерная структура
Рисунок 1. Многомерная структура
Рисунок 2. Одномерная структура
Рисунок 2. Одномерная структура

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).

Рисунок 3. Структура таблиц для настроек фильтрации
Рисунок 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 процесса:

  1. Обработка правил, т.е. если кто-то из контент-менеджеров меняет/редактирует какое-то правило нам нужно переиндексировать до 300к. товаров (прогнать под это правило товары, 300к - если фильтр общий и имеет отношение ко всем разделам каталога)

  2. Обработка изменений диапазонных характеристик товара - переиндексация в ElasticSearch. Изменение диапазонных характеристик товара (цена, размер и тд), т.е. необходимо переиндексировать товар под все правила для сохранения актуальной выдачи фильтров

  3. Обработка изменений чекбокс характеристик товара - переиндексация в Redis.

Таким образом контент-менеджер при сохранении свойства увидит статус “в обработке” и сможет приступить к редактированию других фильтров. Когда сообщение будет обработано брокером, статус свойства изменится на “обработано” и результат можно будем проверить в публичной части.

Вместо заключения

Почему мы не переложили всю работу фильтра на ElasticSearch? В таком случае нам пришлось бы индексировать в elastic все свойства каталога, а поскольку все товары у нас уже есть в redis - в этом нет смысла, мы только продублируем данные, поэтому нам достаточно проиндексировать товары и сохранить фасетные индексы в redis.

Полностью переработав механизм фильтрации мы:

  1. Упростили поддержку и, как следствие, сократили затраты на последующую доработку

  2. Обеспечили контент-менеджерам удобство редактирования

  3. Избавились от блокера для дальнейшего масштабирования системы

  4. Ну и, конечно, освободили в среднем 40% RAM (от 17,5 GB до 40GB)

На текущий момент фоновые процессы по переиндексации правил при наличии сообщений в очереди потребляют в пределах 190MB на переиндексацию товаров по правилам.

Комментарии (37)


  1. vooft
    02.05.2022 14:37
    +12

    А почему бы совсем не выкинуть редис и заменить его эластиком? Все равно ведь уже втащили.


    1. IQ_Dev Автор
      02.05.2022 15:07
      -5

      В заключении статьи написали, что «нам пришлось бы индексировать в elastic все свойства каталога, а поскольку все товары у нас уже есть в redis - в этом нет смысла, мы только продублируем данные, поэтому нам достаточно проиндексировать товары и сохранить фасетные индексы в redis.»


      Немного раскроем: 

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


      1. vassabi
        02.05.2022 17:13
        +5

        вы же и так пересекаете множества из редиса с эластиком? Ну так перенесите всю логику в эластик и пересекайте "всё из редиса" с "то что нужно из эластика"


        1. IQ_Dev Автор
          02.05.2022 19:45
          -2

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


          1. vooft
            02.05.2022 21:27
            +9

            Ну т.е. в качестве решения накинули еще техдолга.


  1. Akuma
    02.05.2022 14:54
    +12

    Соглашусь с комментарием выше. Всю статью думал, почему просто не выкинуть нафиг все самодельные прослойки и не поставить тот же эластик?

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


    1. IQ_Dev Автор
      02.05.2022 15:07
      -5

      В ответе выше раскрыли вопрос


      1. edo1h
        02.05.2022 18:53
        +2

        вопрос «почему не обойтись одним мускулом» не раскрыли.


        1. IQ_Dev Автор
          02.05.2022 20:36

          У нас была цель избавиться от ресурсоемкого приложения (на nodejs) как можно скорее, и мы решили переместить фасетный индекс в redis. Конечно, можно было это реализовать на базе mysql, но redis будет побыстрее


          1. Lexicon
            03.05.2022 01:36
            +1

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

            Чем нода хуже остального зоопарка, как технология, пытались ли ее оптимизировать, пропала ли она из проекта, - все за кадром.


            1. IQ_Dev Автор
              03.05.2022 15:27

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


  1. OkunevPY
    02.05.2022 17:38
    +19

    Почему бы не выкинуть битрикс, развели зоопарк во главе с химерой в лице битрикса, и героически бьються с мельницами и франкештейном в лице битрикса. Время потраченное на ковырянее в плохой архитектуре можно было с лёгкостью потратить на написание хорошо спроектированного портала для которого ни жалкие 150 rps, а даже 1500 rps будут не проблемой. Вообще в нашем мире говорить о 150 rps говорить мягко говоря уже не прилично, лет 10-20 назад когда только начал активно набирать обороты e-commerc и когда негде было черпать ответы как же это делаеться, тогда ещё да.

    Вообще не понятно о чём и для кого статья, выглядит как, мы не очень умеем готовить redis, поэтому нам понадобился эластик, но мы не понимаем зачем, поэтому теперь есть и то и другое, и ещё mySql. И какие мы молодцы, не понимая как выжать максимум из того что есть, размазали везде по чуть-чуть, и стало сильно лучше.


    1. guriianova
      02.05.2022 20:38
      +1

      >> Почему бы не выкинуть битрикс? 

      Клиент на текущий момент не готов отказаться от битрикс.


      >>говорить о 150 rps говорить мягко говоря уже не прилично

      это было не с целью "похвастаться", а просто рассказать о вводных данных проекта


      >>поэтому нам понадобился эластик, но мы не понимаем зачем, поэтому теперь есть и то и другое, и ещё mySql

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


  1. DrPass
    02.05.2022 17:51
    +10

    нам нужно проверить 300к товаров, подходит ли каждый из них под это правило. Но мы не можем позволить себе делать такое количество запросов в БД

    (ведь нужно проверить под 1 правило около 300к. товаров, а эта операция в худшем случае занимает 3 минуты)

    Вот за это я и недолюбливаю некоторый современный инструментарий. 15 лет назад подобная операция занимала секунды на сервере с 2Гб памяти, и требовала один запрос в БД. И полагаю, сейчас тоже так можно :)


    1. 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 запроса


      худший случай - это правило с большим количеством условий


      1. tuxi
        02.05.2022 22:42

        Сколько в этом проекте товарных категорий?


        1. IQ_Dev Автор
          03.05.2022 15:27

          ~9 тысяч


      1. JordanCpp
        03.05.2022 01:20
        +3

        У вас свойства хранятся как EAV? Вы не рассматривали просто настроить mysql, что бы все данные поместились в кеш бд? По крайней мере это лучше, чем закат солнца вручную) Ну и Битрикс как бы такое себе. Но понимаю, что Легаси и сопутствующие проблемы.


        1. IQ_Dev Автор
          03.05.2022 15:26

          на фильтры накладываются определенные бизнес правила, и тут недостаточно стандартной битриксовой реализации фильтров на модели EAV, и их кешированием, (если мы вас правильно понимаем), нужно было “готовить” фасетный поиск по требованиям бизнеса, и оперативно реагировать на их изменения


  1. x-tray
    02.05.2022 23:29

    Вывод не использовать битрикс а переписать на (laravel, yii)

    Битрикс это зло ...


  1. denisshabr
    03.05.2022 00:01
    +2

    Кому лень гуглить, речь о 100sp.ru


    1. IQ_Dev Автор
      03.05.2022 15:29

      нет, речь не об этом проекте


  1. JordanCpp
    03.05.2022 02:04
    +3

    Я думаю вы жертвы Легаси) Но плюсую за то, что с помощью палок, смогли улучшить, то что уже как бы и не предполагает улучшения и даже уже не живо.:) Это тоже не плохой опыт. Желаю вам запилить ещё статью, как вы выпилили Битрикс, перешли на современный стек opensource и улучшили rps на +100500.


    1. IQ_Dev Автор
      03.05.2022 15:34

      Спасибо! Мы двигаемся в этом направлении с клиентом


  1. nikweter
    03.05.2022 06:44
    +9

    Блин, чего вы такие злые? Минусуете автора еще… Ясно же что не программисты принимали решение где и чего костылять. Клиент ставит совершенно определенную задачу — вот эта штука работает медленно, а надо чтобы быстро. И выделяется на решение определенная сумма золотых. Никто не будет переписывать все. Не возьметесь вы — возьмется кто-то другой. И все равно в итоге получится вот такой франкенштейн.
    Ну и в этих рамках, в принципе, сделано в итоге нормально.


  1. antonkrechetov
    03.05.2022 10:43

    А могли бы вместо вот этого всего просто положить данные в реляционную БД (тот же MySQL, ага).


    1. IQ_Dev Автор
      03.05.2022 15:38

      весь каталог уже хранился в redis, мы просто использовали то, что есть от redis и добавили лишь фасетные индексы


  1. aceofspades88
    03.05.2022 12:11

    Два раза начинал читать и так и не смог осилить до конца, в чем проблема то? Могу ошибаться, но 150rps и 300k записей в бд это же дефолтный мускул с адекватной схемой на дешевеньком сервере и больше ничего


    1. IQ_Dev Автор
      03.05.2022 15:33

      проблема была в прежней некорректной реализации фильтров, нескольких лишних сервисах и 17-40Gb потребляемой оперативной памяти на процесс фильтрации товаров


      1. aceofspades88
        03.05.2022 17:44

        так, а о чем тогда статья? какая техническая ценность?


  1. tuxi
    03.05.2022 12:22
    +1

    Заказчик увяз в битрексе — в этом проблема. Насколько я понимаю, отказаться от битрикса — это значит рисовать свою систему управления товарами, со всеми этими формочками и свистоперделками плюс все равно требуется интеграция с учетной системой.

    это был ответ к этому сообщению


    1. IQ_Dev Автор
      03.05.2022 15:32

      да, отказаться от битрикса на текущий момент невозможно, это потребует огромное количество ресурсов, так как все процессы на нем завязаны


  1. danSamara
    03.05.2022 14:03

    Решал подробную задачу, когда надо было "дёшево и сердито", без добавления новых сервисов, БД и в краткие сроки.

    Добавил товарам новую колонку hstore, в которой хранил значения фильтров (filter_id: value). В результате, работа с каталогом не потребовала особых модификаций, однако для администрирования фильтров пришлось писать фоновую задачу переиндексации товаров - фильтры, кроме категорий и eav атрибутов, были завязаны на дополнительные условия.


  1. mSnus
    03.05.2022 14:18

    Очень поучительная статья о том, что бывает, если городить костыль на костыле.


    1. IQ_Dev Автор
      03.05.2022 15:31

      в целом - да, мы хотели поделится опытом, к каким проблемам это может приводить и как непросто потом это исправлять в рамках ограничений клиента


  1. citius
    03.05.2022 22:41

    Почему не Manticore (форк Sphinx Search)? Гладко ложится на mysql, умеет в фасетный поиск, лишних ресурсов не жрет, работает очень быстро. Если использовать как прозрачное прокси между клиентом и базой не требует индексации.


    1. IQ_Dev Автор
      05.05.2022 11:02

      На проекте уже был elastic, что вполне удовлетворяло требованиям