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

URL: https://dmeshcheryakov.ru/blog/bitrix-catalog-cache-policy/
Раздел: 1С-Битрикс
Теги: 1С-Битрикс, Кэширование, Performance, Каталог
Опубликовано: 2026-05-22
Обновлено: 2026-09-07
Автор: Дмитрий Мещеряков (https://dmeshcheryakov.ru)

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

Началось всё с алерта о свободном месте: диск на боевом сервере уходил в ноль, хотя контент почти не менялся. Виновником оказалась одна директория — `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
<?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.