Сразу оговорюсь, тут не будет инструкции как сделать типовое кеширование в битриксе. Если оно вам надо, то сюда. Cмена системного кеширования opcache или memcached на Redis не дает никакого преимущества в производительности. Все эти системы позволяют хранить кеш в оперативной памяти, их производительность находится в одном порядке - сотни тысяч операций чтения/записи в секунду. Ниже будет речь о том, как использовать Redis для хранения данных и как эти данные использовать, что это дает. Мы уйдем от кеширования в битриксе в принципе и посмотрим, что из этого получится в плане производительности.
Проблема производительности при фильтрации Возьмем популярный сайт, созданный на 1С-Битрикс, и популярный раздел на нем, пусть это будет Hoff и раздел "Диваны". Чтобы увидеть скорость генерации страницы битриксом, нужно в URL добавить параметр show_page_exec_time=Y, внизу страницы появится время генерации. На Hoff, кажется, они вырезают этот блок через JS, но в исходном коде страницы он всегда виден. Так даже удобнее. Для объективности лучше делать несколько обновлений страниц и получить некий средний результат. Конкретно в данный момент мне показывается время генерации страницы от 0.23 до 0.5 с для первой страницы раздела диванов.
Казалось бы, 0.5 секунды для генерации ключевой страницы на сайте - это уже катастрофа, но нет. Катастрофа начинается, когда вы будете менять значения фильтра. Например, по цене. Я поставил значение от 15000, время генерации страницы было более 5 секунд. Как видите, ценовой диапазон задается параметром в URL вида prices=15000_399990. Повторное обновление страницы с тем же ценовым диапазоном показало время 0.5 секунд. И все последующие обновления страницы с той же ценой показывают примерно такое же нормальное время генерации страницы. Поменяйте ценовой диапазон на 1 и у вас история повторится - 5 секунд для первого запроса и 0.5 для всех последующих.
Проблема заключается в том, что любое изменение параметров фильтрации, а также сортировки или изменения количества выводимых товаров на страницу вызывают перегенерацию кеша компонента bitrix:catalog.section или его кастомного аналога, который отвечает за вывод товаров. На hoff.ru еще включена опция "Кешировать при установленном фильтре" - это значит, что при любом значении фильтра будет создаваться новый кеш, именно поэтому повторные запросы с установленным ценовым диапазоном выполнялись за относительно небольшое время. Это не проблема именно Hoff, она наблюдается на многих сайтах, использующих 1С-Битрикс: "Читай-город", "Красное&Белое", "ЦУМ" и т.д.
Как кеширует битрикс
Это был иронический пролог, эта цитата меня всегда умиляла.
Сама концепция кеширования 1С-Битрикс заключается в том, что кешируется результат целиком. Будь то $arResult в работе компонентов или готовый кусок html-кода. Любое изменение входящих переменных образует новый кеш, потому что меняется ключ, формируемый из параметров, а с ним и множество запросов к БД, даже если результат остается таким же. Кеш сохраняется в отдельных файлах и при неверной настройке кеширования количество этих файлов может быть очень большим. Использование opcache или memcached существенно ускоряют работу кеша, но принцип его работы остается тем же, только данные хранятся уже не на диске, а в оперативной памяти. Кроме того, весь кеш генерируется на пользовательских хитах, а не где-то там фоновым процессом темными ночами. Если на сайте низкая посещаемость, то пользователи неизбежно столкнутся с высоким временем генерации страниц, а владелец сайта при низком трафике будет видеть высокую нагрузку на сервере, потому что многие пользователи получают некешированные данные. Особенно, если код на сайте неоптимальный и кеширование используется для его сокрытия (когда без кеша 6000 запросов к БД на хите - я такое видел часто).
Решение кеширования на Redis Почему бы тогда не кешировать отдельные части результата? Например, не список товаров на страницу, а каждый товар отдельно? Отдельно цены, отдельно порядок следования элементов, отдельно свойства инфоблока и т.д. Divide et impera.
Для решения этой проблемы решено было использовать Redis. Данная NoSQL-СУБД широко распространена, быстрая, обладает хорошим функционалом для хранения как строк, так и структур (наборы, списки, хэш-таблицы). Касательно производительности, то, как и многие подобные системы, хранящие данные в оперативной памяти, он быстрый. Допустим, на моем обычном офисном компе показывает 150 тысяч операций записи/чтения (set/get) в секунду. На сервере на базе 2-х Xeon 4114 / DDR4 более 200 тысяч для одного потока. Ограничения запредельные, допустим, строка может быть максимум 512 Мб, а длина списков более 4 млрд. элементов. Если мы работаем не с BigData, то такие пределы даже для большого интернет-магазина являются достаточными.
Для эксперимента я создал типовой интернет-магазин на базе редакции "Бизнес", загрузил в него реальные товары из партнерских XML-файлов "Озон". Загружено было более 12 тысяч товаров, с ценами, названиями, описаниями и фото. Заполнено было лишь 4 свойства инфоблока, на остальные руки уже не дошли. Данная модель не совсем корректна, потому что у многих интернет-магазинов десятки и сотни свойств инфоблока могут быть заполнены различной информацией. В них часто хранят кроме общей информации (бренд, артикул, штрихкод и т.д.) еще и специфические характеристики для типов товаров (диагональ экрана, процессор и т.д.), что по-моему, совсем неправильно. Встречал интернет-магазины, где использовалось более 2000 свойств инфоблока - редактировать инфоблок и элементы стало невозможным.
Следующий шаг - это подготовка данных. Мы будем генерировать весь кеш в фоне, а пользовательские хиты не будут вызывать перегенерации кеша ни в коем случае. И прежде чем генерировать в фоне кеш, мы должны определиться с его структурой. Она получилась такой:
Кеш свойств инфоблока - он с большим TTL, можно неделю, храним поля свойств инфоблока, справочники списков и связанных элементов (например, связанный бренд).
Кеш разделов - я много разделов не создавал, но на обычном сайте пара тысяч разделов инфоблока может быть вполне нормой. Будем хранить всё дерево разделов, все их связи, описания.
Кеш сортировок - мы можем получить из MySQL последовательный список ID товаров во всех используемых на сайте сортировках. По названию, по цене, по рейтингу и т.д. Тут был задействован тип данных List в Redis. Он позволяет хранить упорядоченные ряды
Кеш элементов - список с описанием товаров, все поля элемента инфоблока, цены и значения свойств инфоблока
Кеш привязок элементов к разделам - список ID товаров, привязанных к конкретным разделам. Использованы ряды (Sets) - этот тип данных позволяет хранить неупорядоченный набор значений. Нам просто надо знать список доступных ID товаров для конкретного раздела,
а за сортировку отвечает другой кеш.
Кеш цен - список цен для товаров. Использованы упорядоченные ряды (Sorted sets), данный функционал в Redis позволяет хранить уникальные значения, упорядоченные по оценке (Score), в качестве оценки выступает цена. Очень удобно, когда нужно выбрать значения от и до или получить цену по конкретному ID товара.
Кеш фильтров - это список ID товаров, доступных для каждого значения фильтруемых свойств. Например, в отдельном ключе хранится список ID товаров, где brand=samsung. Также со всеми фильтруемыми свойствами. Если у вас 1000 вариантов значений для 10 свойств, то у вас будет 1000 ключей с рядами допустимых ID товаров. Опять же использованы неупорядоченные наборы (Sets).
Пока это выглядит громоздко, столько разных кешей, но на самом деле всё очень просто. Все эти кеши - это простые выборки ID через API битрикса буквально каждый в одну строку и запись в Redis по определенным ключам. Заполнение кеша для 12 тысяч товаров занимает 27 секунд. Все связи реализованы как списки и наборы, и хранятся только ID, а значит их выборка и операции с ними должны быть быстрыми. Реально большие структурированные данные хранятся только в кеше с товарами, разделами и свойствами, но к ним запросы будут очень простые - просто выбрать значения по указанным ключам (по сути по ID).
Теперь нам необходимо создать свой компонент для вывода списка товаров. Он не будет использовать кеш битрикса вообще. Основная логика компонента - это узнать список ID подходящих товаров, выбрать эти товары и отобразить шаблон с данными.
Компонент в качестве параметров может получать: ID инфоблока, ID раздела, фильтр по значениям свойств инфоблока, сортировку, количество выводимых товаров на страницу. Я не нашел правильного способа высчитывать пересечения множеств различного типа в Redis прямо в нем, поэтому решено было использовать расчет пересечений функциями PHP, благо количество элементов измеряется лишь десятками тысяч, а в качестве значений выступают только целые числа (ID товаров).
Логика компонента получилась очень простой:
Узнать подходящие ID товаров для раздела
Узнать подходящие ID товаров для фильтра, если есть
Узнать порядок ID товаров
Найти пересечение всех множеств, определенных ID и получить упорядоченный список ID
Выбрать информацию о товарах из определенного выше списка и отдать в шаблон
На выходе получаем вывод товаров из компонента, где каждый товар является отдельным блоком из кеша, а не весь результат цельным кешем. На картинке ниже в красных рамках обозначены отдельные блоки кеша и ключи для них.
Пример работы фильтра С фильтром нужно дать небольшое пояснение как он будет работать. Например, у нас есть раздел телевизоров, у телевизоров есть два свойства: бренд и диагональ. При подготовке кеша мы должны создать индекс значений обоих характеристик. В случае бренда - это справочник, нам надо для каждого бренда создать свой набор (Set) ID товаров, принадлежащих конкретному бренду. В итоге получатся ключи примерно такого вида:
i:1:f:BRAND:v:SAMSUNG = 1, 2, 3, 4, 5...
i:1:f:BRAND:v:SONY = 6, 7, 8, 9, 10...
i:1:f:BRAND:v:LG = 11, 12, 13, 14...
и т.д.
Диагональ телевизора является числовым значением, поэтому ее удобнее будет хранить в виде Sorted Set, то есть упорядоченный ряд, в качестве Score будет использована сама диагональ, а в качестве значения ID товара. И все значения можно будет записать в один ключ вида: i:1:f:DIAGONAL = 50 1, 55 2, 55 3, 49 4, 43 5... В дальнейшем из этого ключа будет удобно выбирать ID товаров по диагонали от и до. Или даже считать среднее, если оно вам вдруг понадобится.
Выбор телевизора определенного бренда и определенной диагонали - это будет пересечение двух массивов с ID товаров. Допустим, нам нужен Samsung диагональю от 50 до 55 дюймов. Выбираем два массива в PHP: все ID из ключа i:1:f:BRAND:v:SAMSUNG и все ID из ключа i:1:f:DIAGONAL, где Score больше 50 и меньше 55. Ну а далее array_intersect и получаем список допустимых ID товаров для вывода.
Если же вы хотите выбрать телевизор не только Samsung, но и Sony, то механизм будет немного другим. Для бренда нам уже нужен список из двух ключей, его уже можно получить с помощью функции SUNION. Хотя вы также можете получить 2 массива в PHP и соединить их с помощью array_merge. Скорость работы на сотнях и тысячах товаров примерно одинаковая и будет измеряться миллионными долями секунд.
Ну и дальше уже отфильтрованный список ID накладывается на отсортированный полный список и по нему выбираются товары.
Итоги Сайт с прототипом запущен здесь - https://test2.keng.ru Постраничную навигацию не прикручивал пока.
Главной целью эксперимента было увеличение производительности страницы со списком товаров на произвольных запросах с различными фильтрами и сортировками. Были получены следующие цифры:
Вся хранимая информация для 12 тысяч товаров в Redis заняла 40 Мб. То есть даже если грузить сотни тысяч товаров, тысячи разделов, сотни значений характеристик для каждого товара, то использование памяти будет на уровне пары-тройки Гб, что по современным меркам очень незначительно. И это финальный размер кеша, он не будет расти из-за нагрузки.
Время полного обновления кеша - 27 секунд. При увеличении количества товаров и сложности данных это время будет расти пропорционально. Но пусть даже обновление всего кеша займет 300 секунд - это всё равно лучше, чем выполнять обновление кеша на хитах посетителей. И обновлять можно по ночам, раз в сутки. Также можно обновлять части кеша, например, по событиям изменения разделов, товаров, цен, свойств инфоблока, прав доступа и т.д.
Время генерации компонента со списком товаров составило несколько тысячных, от 0.005 до 0.008 с.
При фильтрации время генерации компонентом увеличивается в 3-7 раз. 15-35 тысячных вместо 5-8 тысячных секунды - это небольшая разница, но наверняка есть варианты оптимизации хранения данных и расчета пересечений, которые позволят убрать это увеличение времени работы компонента.
Расходы компонента по памяти около 2-3 Мб для вывода 60 товаров, они стабильны при любом фильтре.
А самый главный результат - при изменении параметров фильтрации и/или сортировки, время генерации остается стабильным. Можно выбрать бренд, изменить цену, указать часть названия товара, изменить сортировку - не важно что и в каких комбинациях, список товаров всегда генерируется несколько тысячных долей секунды, занимает те же пару Мб в памяти и никогда не создается новый кеш.
Эта картинка выше показывает тайминги для работы различных частей компонента.
getSorting_TIMER - это время получения упорядоченного списка ID товаров, используя все выборки. Это основной функционал компонента и он занимает почти половину времени работы компонента. В нем по сути ищется пересечение и разница множества массивов, один из которых содержит 12 тысяч элементов, остальные чуть меньше. В реальных условиях в разделах не хранится по 12 тысяч товаров, поэтому время работы компонента должно быть хотя бы на четверть быстрее.
getItems_TIMER - время получения информации о товарах
Template_TIMER - время обработки шаблона
COMPONENT_TIMER - время работы всего компонента, включая шаблон
COMPONENT_MEMORY_USAGE - использование оперативной памяти процессом PHP для компонента (изменение пикового значения)
И еще по генерации кеша тайминги:
Property BRAND dictionary ready
0.098195 properties ready (3) - время подготовки кеша для 3 свойств инфоблока и создания справочника по брендам
0.009647 sections ready (7) - время подготовки кеша для всех разделов, сейчас их всего 7
0.367995 items-sections ready (12819) - время подготовки кеша связей товаров с разделами
23.35159 items ready (0) - время подготовки кеша с товарами, их около 12 тысяч
3.348402 sorting items ready - время пересчета всех доступных сортировок товаров
Нагрузочное тестирование Также было проведено нагрузочное тестирование с помощью ab и siege. Под тестовую виртуальную машину было выделено 16 ядер процессора и 32 Гб памяти, используется php_fpm + nginx, opcache, PHP 7.0.23, Redis 4.0.2, Centos 7. Оценка производительности битрикса выдает 220-240 попугаев. При тестировании нагрузка на CPU была около 85%. На Redis идет около 35% нагрузки и только 1 ядра из 16.
Запросы к первой странице без фильтрации - около 300 запросов/секунду.
Запросы со случайными значениями ценового диапазона - 120 запросов/ в секунду.
Я не возьмусь оценивать это в реальной посещаемости для реального сайта, но наверняка порядок в десятки тысяч посетителей в сутки точно выдерживает при нагрузке на CPU до 10%. Большинство хитов выполнялись вообще без запросов к БД.
Прочие наблюдения при работе с Redis:
Redis может работать через сокет и по TCP. Через сокет получается в 1.5-2 раза быстрее.
Master-slave репликация примерно на 30% снизила скорость работы master и на 10-20% для slave.
Есть возможность записывать и читать данные сразу по множеству ключей, но на небольшом количестве данных (тысячи ключей разом) скорость почти такая же. И нельзя установить TTL сразу на пачку ключей.
Бонусом вы можете в Redis хранить php-сессии
Redis позволяет использовать несколько баз данных, в них можно разделить логические части. Например, если вы будете использовать Redis для хранения PHP-сессий, то они будут записываться в базу с индексом 0. Базу с индексом 1 вы можете использовать для системного кеша битрикса на Redis. Ну и базу с индексом 2 уже для хранения каких-то своих данных. Это удобно для очистки. Вы можете очистить либо всё (FLUSHALL), либо всю базу (FLUSHDB). Если у вас всё хранилось бы в одной базе с индексом 0, то при очистке кеша у вас бы очищались и сессии. А так вы можете очистить только базу с кешем битрикса или только со своими данными.
По возможности используйте сериализацию igbinary - она сильно быстрее
Другие итоги Пересмотр архитектуры значительной части сайта дает существенный прирост в производительности и решает серьезные проблемы. Лично для меня результаты работы с Redis в битриксе похожи на переход с обычного HDD на SSD - тот же эффект wow! Лучший апгрейд, который мог произойти. Да, сильно дороже в производстве и поддержке, усложняется логика, повышается требование к квалификации разработчиков, но результаты того стоят. Если вы не шарашкина контора с полутора сотрудниками, то такая разработка вам по карману. То, что сейчас опубликовано и работает как концепт по времени разработки заняло 24 рабочих часа программиста. Даже если взять топовые зарплаты - это несколько десятков тысяч рублей на рабочий концепт. Ну пусть далее требуется доработка до готового решения, надо написать другие компоненты, оптимизировать, еще месяц-другой, все равно суммы здесь небольшие на разработку, вполне доступные таким компаниям, которые покупают Enterprise за полтора миллиона рублей или сервера для кластера в надежде решить проблемы с производительностью. И это только вершина айсберга. Использование Redis, его возможности работы с данными, открывают действительно новый потенциал для Битрикса. Нулевое количество обращений к БД на нагруженном сайте - вполне реально. Сотни тысяч посещений на обычном сервере в аренде - запросто. Всё это дает сокращение инфраструктурных расходов в разы.
Я не нашел правильного способа высчитывать пересечения множеств различного типа в Redis прямо в нем, поэтому решено было использовать расчет пересечений функциями PHP
и тут ставится под сомнение необходимость в redis. я проделывал такую же штуку храня наборы id товаров под свойства в массивах php. просто и дешево.
Semenov Roman, скорость подсчета пересечений массивов на PHP и наборов в Redis будет примерно одинакова. Но Redis - это хранилище в первую очередь, очень подходящее как раз для тех типов данных, которые используются.
Vitaly, если данные переносятся в php для расчетов на стороне php то хранилище бесполезно и неоправдано. если все данные есть в инфоблоках. тогда зачем прослойка redis? берем и индексируем в файлы в виде php массивов . которые подключаем через include, обрабатываем, opcache используется.
Группы на сайте создаются не только сотрудниками «1С-Битрикс», но и партнерами компании. Поэтому мнения участников групп могут не совпадать с позицией компании «1С-Битрикс».