Главная/Статьи/Рост кэша catalog.section в Битрикс: политика кэширования

Рост кэша catalog.section в Битрикс: политика кэширования

Как остановить неограниченный рост кэша catalog.section. Создаём централизованную политику кэширования для контроля токсичных URL каталога.

ДМ
Дмитрий Мещеряков
📅 22 мая 2026 г.📖 7 мин чтения

Началось всё с алерта о свободном месте: диск на боевом сервере уходил в ноль, хотя контент почти не менялся. Виновником оказалась одна директория — 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.filterbrand-is-tara_ru
Диапазонные фильтрыprop_shirina-from-330
ПагинацияPAGEN_1=5
Сортировкаsort=price&order=asc
Отображениеdisplay=list
Колонкиlinerow=3
⚠️ Важно

Особенно опасны диапазонные фильтры:

text
prop_shirina-from-330
prop_shirina-from-331
prop_shirina-from-332
...

Такие URL можно генерировать практически бесконечно.

Почему не отключить кэш полностью

Полный отказ от кэша имел бы негативные последствия:

  • Увеличение нагрузки на БД
  • Замедление выдачи для обычных пользователей
  • Потеря преимуществ кэширования там, где оно полезно

Задача: Перестать кэшировать только самые дорогие и бесконтрольные варианты запросов.

⚠️ Важно

Отключение кэша — это размен, а не победа. Кэша нет — значит, каждый запрос бота идёт в базу. Мы меняем неограниченный рост диска на дополнительную нагрузку на MySQL, и на достаточно агрессивном краулере второе способно оказаться хуже первого. Поэтому политика кэширования — половина решения; вторая половина в том, чтобы такие URL вообще не отдавали 200: валидация значений фильтра с отдачей 404, noindex на комбинациях фильтров и ограничение частоты для ботов.

Решение: CatalogCachePolicy

Централизованный класс, который на уровне запроса принимает решение о включении/отключении кэша:

php
<?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
<?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'] или из параметров компонента) и накладывайте его настройки поверх общих перед проверкой условий. Оставляю это явно, а не «в следующей версии»: неработающий конфиг в статье — та самая деталь, которая потом стоит кому-то вечера отладки.

php
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 редко можно решить одним флажком. Правильный подход — централизованная политика кэширования, которая:

  • Оставляет кэш там, где он полезен
  • Отключает его там, где он работает против проекта

Практический порядок внедрения, если у вас та же проблема:

  1. Сначала измерьте. du -sh bitrix/cache/*/bitrix/* | sort -h покажет, какой компонент реально виноват, — это не всегда catalog.section.
  2. Посмотрите, кто генерирует URL. Если это один агрессивный бот, вопрос закрывается на уровне nginx за пять минут и без единой строки PHP.
  3. Отключайте кэш только для заведомо неповторяющихся запросов. Каждое правило — это обмен диска на нагрузку к базе, и обмен должен быть осознанным.
  4. Параллельно закрывайте источник. Пока несуществующие комбинации фильтров отдают 200, боты будут находить новые.
💡 Совет

Для полного решения недостаточно ограничить только кэш. Нужно ещё научить каталог корректно обрабатывать несуществующие URL smart.filter — см. статью про валидацию и 404.

🚀

Хотите такое же решение?

Настрою окружение под ваш проект, учту специфику инфраструктуры и обучу команду.

Обсудить проект →
Бесплатная консультация · Ответ в течение дня

Комментарии

Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.