Началось всё с алерта о свободном месте: диск на боевом сервере уходил в ноль, хотя контент почти не менялся. Виновником оказалась одна директория — bitrix/cache/s1/bitrix/catalog.section, разросшаяся до десятков гигабайт за пару недель.
Ниже — разбор того, почему кэш каталога способен расти неограниченно, и решение, которое мы в итоге внедрили: не «отключить кэш», а описать политику — где он полезен, а где работает против проекта.
Проблема
Высокая нагрузка на сайт от краулеров привела к обнаружению проблемы: каталог товаров активно раздувает кэш catalog.section.
Симптомы:
- В директории
bitrix/cache/s1/bitrix/catalog.sectionбыстро появлялись новые файлы - Объём кэша рос на десятки гигабайт
- Отдельные кэш-файлы были достаточно тяжёлыми
- Число комбинаций URL практически не имело ограничения
Почему именно catalog.section
Компонент catalog.section отвечает за выдачу списка товаров в разделе с фильтрами, сортировками, пагинацией и разными шаблонами отображения.
Когда одновременно включены:
- Фильтрация с
CACHE_FILTER = Y - Smart.filter в SEF-режиме
- Сортировка
- Пагинация
- Разные режимы отображения
Каждый новый вариант URL создаёт отдельный кэш.
Причина в самой природе кэширования: ключ кэша обязан включать всё, что влияет на результат. Для карточки товара это один идентификатор, и число вариантов конечно. Для списка с фильтрами это декартово произведение всех параметров — а произведение растёт быстрее, чем интуиция успевает за ним следить. Пять фильтров по десять значений, три сортировки и двадцать страниц — это уже сотни тысяч комбинаций, каждая со своим файлом на диске.
Анализ кэшей
Кэш множится по комбинации параметров:
| Параметр | Пример |
|---|---|
| Раздел каталога | /catalog/plastikovye-konteynery/ |
| Smart.filter | brand-is-tara_ru |
| Диапазонные фильтры | prop_shirina-from-330 |
| Пагинация | PAGEN_1=5 |
| Сортировка | sort=price&order=asc |
| Отображение | display=list |
| Колонки | linerow=3 |
Особенно опасны диапазонные фильтры:
prop_shirina-from-330
prop_shirina-from-331
prop_shirina-from-332
...Такие URL можно генерировать практически бесконечно.
Почему не отключить кэш полностью
Полный отказ от кэша имел бы негативные последствия:
- Увеличение нагрузки на БД
- Замедление выдачи для обычных пользователей
- Потеря преимуществ кэширования там, где оно полезно
Задача: Перестать кэшировать только самые дорогие и бесконтрольные варианты запросов.
Отключение кэша — это размен, а не победа. Кэша нет — значит, каждый запрос бота идёт в базу. Мы меняем неограниченный рост диска на дополнительную нагрузку на MySQL, и на достаточно агрессивном краулере второе способно оказаться хуже первого. Поэтому политика кэширования — половина решения; вторая половина в том, чтобы такие URL вообще не отдавали 200: валидация значений фильтра с отдачей 404, noindex на комбинациях фильтров и ограничение частоты для ботов.
Решение: CatalogCachePolicy
Централизованный класс, который на уровне запроса принимает решение о включении/отключении кэша:
<?php
namespace Local\Catalog;
class CatalogCachePolicy
{
private static array $config = [
'rules' => [
'disable_for_ajax' => true,
'disable_for_range_filters' => true,
'disable_for_page_gt' => 5,
'disable_for_filtered_page_gt' => 3,
'disable_for_non_default_sort' => true,
'default_sort_field' => 'sort',
'default_sort_order' => 'asc',
'disable_for_non_default_display' => true,
'default_display' => 'block',
'disable_for_non_default_linerow' => true,
'default_linerow' => '4',
],
'section_rules' => [],
];
public static function configure(array $config): void
{
// array_replace_recursive, а не array_merge_recursive:
// последний для скалярных ключей не заменяет значение,
// а превращает его в массив — [5, 3] вместо 3
self::$config = array_replace_recursive(self::$config, $config);
}
public static function getCatalogComponentCacheOptions(): array
{
$cacheType = 'A';
$cacheFilter = 'Y';
$isRangeFilter = self::containsRangeFilter();
$page = self::resolvePageNumber();
$hasNonDefaultSort = self::hasNonDefaultSort();
$hasNonDefaultDisplay = self::hasNonDefaultDisplay();
$hasNonDefaultLineRow = self::hasNonDefaultLineRow();
$isAjax = self::isAjaxRequest();
if (
($isRangeFilter && self::isEnabled('disable_for_range_filters'))
|| $page > self::$config['rules']['disable_for_page_gt']
|| ($hasNonDefaultSort && self::isEnabled('disable_for_non_default_sort'))
|| ($hasNonDefaultDisplay && self::isEnabled('disable_for_non_default_display'))
|| ($hasNonDefaultLineRow && self::isEnabled('disable_for_non_default_linerow'))
|| ($isAjax && self::isEnabled('disable_for_ajax'))
) {
$cacheType = 'N';
$cacheFilter = 'N';
}
return [
'CACHE_TYPE' => $cacheType,
'CACHE_FILTER' => $cacheFilter,
];
}
private static function isEnabled(string $rule): bool
{
return !empty(self::$config['rules'][$rule]);
}
private static function containsRangeFilter(): bool
{
$uri = $_SERVER['REQUEST_URI'] ?? '';
return preg_match('/-(from|to)-\d+/i', $uri) > 0;
}
private static function resolvePageNumber(): int
{
foreach ($_GET as $key => $value) {
if (preg_match('/^PAGEN_\d+$/i', $key)) {
return (int) $value;
}
}
return 1;
}
private static function hasNonDefaultSort(): bool
{
$sort = $_GET['sort'] ?? self::$config['rules']['default_sort_field'];
$order = $_GET['order'] ?? self::$config['rules']['default_sort_order'];
return $sort !== self::$config['rules']['default_sort_field']
|| $order !== self::$config['rules']['default_sort_order'];
}
private static function hasNonDefaultDisplay(): bool
{
$display = $_GET['display'] ?? self::$config['rules']['default_display'];
return $display !== self::$config['rules']['default_display'];
}
private static function hasNonDefaultLineRow(): bool
{
$linerow = $_GET['linerow'] ?? self::$config['rules']['default_linerow'];
return $linerow !== self::$config['rules']['default_linerow'];
}
private static function isAjaxRequest(): bool
{
return !empty($_SERVER['HTTP_X_REQUESTED_WITH'])
&& strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
}
}Про флаги в конфиге. В первой версии этого класса флаги disable_for_ajax, disable_for_range_filters и подобные были объявлены, но нигде не проверялись — условие срабатывало безусловно. Конфиг, который выглядит рабочим и молча не влияет на поведение, обходится дороже отсутствующего: следующий разработчик будет менять значения и не понимать, почему ничего не меняется. Если правило настраивается — оно должно читаться из конфига; если не настраивается — его не должно быть в конфиге.
Что отключаем от кэширования
1. Диапазонные фильтры
Если URL содержит -from- или -to-, кэш отключается. Это наиболее важный пункт.
2. Глубокая пагинация
Лимит по пагинации (например, кэш выключается при PAGEN_1 > 5).
3. Нестандартная сортировка
Если запрос использует сортировку, отличную от стандартной.
4. Нестандартный display/linerow
Изменение типа отображения или числа колонок.
5. Ajax-запросы
Ajax-ответы каталога не создают тяжёлые кэши.
Использование в шаблоне
<?php
// В шаблоне каталога
$catalogCacheOptions = \Local\Catalog\CatalogCachePolicy::getCatalogComponentCacheOptions();
$APPLICATION->IncludeComponent(
"bitrix:catalog",
"main",
[
"CACHE_TYPE" => $catalogCacheOptions["CACHE_TYPE"],
"CACHE_FILTER" => $catalogCacheOptions["CACHE_FILTER"],
// ... остальные параметры
]
);Почему именно эти пять правил
Объединяет их одно: все они описывают запросы, которых не бывает у живого пользователя в заметном количестве. Человек смотрит первые страницы, стандартную сортировку и предустановленный режим отображения. Диапазонные фильтры с точностью до единицы, сотая страница выдачи и linerow=7 — это почти гарантированно бот или чей-то парсер.
Отсюда практический критерий, по которому стоит добавлять новые правила: смотрите не на то, «тяжёлый ли запрос», а на то, повторится ли он. Кэш имеет смысл только для повторяющихся запросов; для уникального URL, который больше никогда не запросят, кэш — это чистые накладные расходы плюс файл на диске навсегда.
Настройка для разных разделов
Секция section_rules в примере класса выше не используется. Она объявлена в конфиге, её можно заполнить — и она ни на что не повлияет, потому что getCatalogComponentCacheOptions() читает только rules. Чтобы правила по разделам заработали, определите текущий раздел (например, из $_SERVER['REQUEST_URI'] или из параметров компонента) и накладывайте его настройки поверх общих перед проверкой условий. Оставляю это явно, а не «в следующей версии»: неработающий конфиг в статье — та самая деталь, которая потом стоит кому-то вечера отладки.
CatalogCachePolicy::configure([
'section_rules' => [
'lineynye-svetilniki' => [
'disable_for_page_gt' => 3,
'disable_for_filtered_page_gt' => 2,
],
'plastikovye-konteynery' => [
'disable_for_filtered_page_gt' => 2,
],
],
]);Результаты
После внедрения политики кэширования:
| Метрика | До | После |
|---|---|---|
| Рост директории catalog.section | Неконтролируемый | Стабильный |
| Кэш для обычных страниц | Работает | Работает |
| Кэш для токсичных URL | Создаётся | Отключён |
| Нагрузка от фильтров | Высокая | Низкая |
Заключение
Проблему разрастания catalog.section редко можно решить одним флажком. Правильный подход — централизованная политика кэширования, которая:
- Оставляет кэш там, где он полезен
- Отключает его там, где он работает против проекта
Практический порядок внедрения, если у вас та же проблема:
- Сначала измерьте.
du -sh bitrix/cache/*/bitrix/* | sort -hпокажет, какой компонент реально виноват, — это не всегдаcatalog.section. - Посмотрите, кто генерирует URL. Если это один агрессивный бот, вопрос закрывается на уровне nginx за пять минут и без единой строки PHP.
- Отключайте кэш только для заведомо неповторяющихся запросов. Каждое правило — это обмен диска на нагрузку к базе, и обмен должен быть осознанным.
- Параллельно закрывайте источник. Пока несуществующие комбинации фильтров отдают 200, боты будут находить новые.
Для полного решения недостаточно ограничить только кэш. Нужно ещё научить каталог корректно обрабатывать несуществующие URL smart.filter — см. статью про валидацию и 404.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.