# Аномальная нагрузка на VPS: блокируем crawler на уровне nginx

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

> Как Facebook crawler перегружал VPS и разгонял CPU до 60%. Настраиваем точечную блокировку тяжёлых URL каталога на уровне nginx.

Самая неприятная разновидность проблем с производительностью — та, где метрики выглядят плохо, а причина находится вне вашего кода. Профилировщик показывает, что каталог работает нормально. Запросы к базе укладываются в норму. Продажи не выросли. А CPU держится на шестидесяти процентах и периодически уводит сервер в ребут.

Разбор ниже — про то, как такая нагрузка ищется (в access-логах, а не в профилировщике) и почему блокировать её нужно на входе, а не в приложении.

## Симптомы проблемы

На проекте 1С-Битрикс появилась проблема: VPS-сервер держал утилизацию CPU на уровне **около 60%**, а в отдельные периоды уходил выше и падал в ребут.

Всплески не были привязаны к:
- Рекламной активности
- Обновлениям каталога
- Росту реального пользовательского трафика

Дополнительно рос объём системного кэша, а каталог товаров вёл себя как источник постоянной фоновой нагрузки.

## Анализ логов

Первым шагом стал анализ логов nginx:

```bash
awk -F\" '{print $6}' access.log | sort | uniq -c | sort -nr | head -50
```

В топе оказался:

```
115604 meta-externalagent/1.1 (+https://developers.facebook.com/docs/sharing/webmasters/crawler)
```

Значительная часть нагрузки связана не с пользователями, а с роботом Facebook, который активно обходит страницы сайта.

## Что именно делал crawler

Проблема была не просто в количестве обращений. По логам crawler активно перебирал URL каталога:

- Страницы разделов
- Страницы со smart.filter
- Страницы с пагинацией
- URL с сортировками
- URL с разными вариантами отображения

Каждый такой запрос запускал тяжёлую серверную обработку и порождал новые варианты кэша.

## Почему блокировка в PHP — плохая идея

При блокировке на уровне приложения:

1. Запрос уже дошёл до nginx
2. Затем до Apache или php-fpm
3. Затем до Битрикса
4. И только после этого сервер начал бы его отбрасывать

**Нагрузка уже возникла.** Защита должна стоять как можно раньше.

Стоимость запроса, отброшенного nginx, — микросекунды и ноль обращений к базе. Стоимость того же запроса, отброшенного в `init.php`, — полный старт ядра Битрикса: подключение модулей, соединение с MySQL, чтение настроек. Разница между этими двумя вариантами на потоке в сто тысяч запросов и есть те самые шестьдесят процентов CPU.

## Решение: блокировка на уровне nginx

Оптимальная точка — nginx, который стоит перед приложением.

Преимущества:

- Запрос отбрасывается **до запуска PHP**
- Apache и Битрикс не участвуют в обработке
- Не создаётся дополнительная нагрузка на CPU
- Можно точно ограничить только опасные сценарии

## Конфигурация nginx

### Определение условий

```nginx
# Определяем meta-externalagent
map $http_user_agent $is_meta_externalagent {
    default 0;
    ~*meta-externalagent 1;
}

# Определяем тяжёлые URL каталога
map $request_uri $is_heavy_catalog_url {
    default 0;
    ~^/catalog/.*/filter/ 1;
    ~^/catalog/.*(-from-|-to-) 1;
    ~^/catalog/.*[?&]PAGEN_[0-9]+= 1;
    ~^/catalog/.*[?&]sort= 1;
    ~^/catalog/.*[?&]display= 1;
    ~^/catalog/.*[?&]linerow= 1;
}

# Комбинируем условия
map "$is_meta_externalagent:$is_heavy_catalog_url" $block_meta_heavy_catalog {
    default 0;
    "1:1" 1;
}
```

### Применение блокировки

```nginx
server {
    listen 80;
    server_name example.com;
    
    # Блокируем crawler только на тяжёлых URL
    if ($block_meta_heavy_catalog) {
        return 403;
    }
    
    # ... остальная конфигурация
}
```

## Почему не блокируем crawler целиком

Полная блокировка могла бы повлиять на корректность генерации превью ссылок в соцсети. Вместо этого выбрана **точечная схема**: ограничиваем именно тяжёлые URL каталога.

| URL | Crawler | Результат |
|-----|---------|-----------|
| `/` | meta-externalagent | ✅ Доступ |
| `/catalog/category/` | meta-externalagent | ✅ Доступ |
| `/catalog/category/?PAGEN_1=5` | meta-externalagent | ❌ 403 |
| `/catalog/category/filter/price-from-1000/` | meta-externalagent | ❌ 403 |
| `/catalog/category/?sort=price` | meta-externalagent | ❌ 403 |

## Результаты

После включения блокировки:

| Метрика | До | После |
|---------|-----|-------|
| Утилизация CPU | ~60% | 10-15% |
| Фоновая нагрузка | Постоянная | Минимальная |
| Стабильность | Ребуты | Стабильно |
| Обход основных страниц | Работает | Работает |

## Дополнительно: блокировка других ботов

Аналогично можно настроить блокировку для других агрессивных ботов:

```nginx
map $http_user_agent $is_aggressive_bot {
    default 0;
    ~*meta-externalagent 1;
    ~*AhrefsBot 1;
    ~*SemrushBot 1;
    ~*MJ12bot 1;
    ~*DotBot 1;
}

map "$is_aggressive_bot:$is_heavy_catalog_url" $block_aggressive_bot_heavy {
    default 0;
    "1:1" 1;
}
```

## Мониторинг

Логирование заблокированных запросов стоит настроить сразу: без него вы не узнаете, отсекается ли лишнее и не попал ли под раздачу кто-то нужный.

```nginx
log_format blocked '$remote_addr - $http_user_agent - $request_uri';

server {
    # ...

    # Отдельный лог только для заблокированных запросов
    access_log /var/log/nginx/blocked_crawlers.log blocked if=$block_meta_heavy_catalog;

    if ($block_meta_heavy_catalog) {
        return 403;
    }
}
```

**`access_log` нельзя поместить внутрь `if` на уровне `server`.** Директива допустима в `http`, `server`, `location` и `if in location` — при попытке объявить её в блоке `if` уровня сервера nginx не стартует с ошибкой «directive is not allowed here». Правильный способ условного логирования — параметр `if=` у самой директивы: значение переменной, отличное от `0` и пустой строки, включает запись.

Анализ логов:

```bash
# Топ заблокированных User-Agent
awk '{print $3}' /var/log/nginx/blocked_crawlers.log | sort | uniq -c | sort -nr | head -10

# Топ заблокированных URL
awk '{print $4}' /var/log/nginx/blocked_crawlers.log | sort | uniq -c | sort -nr | head -20
```

## Ограничения этого подхода

Метод рабочий, но у него есть чёткая граница применимости, и о ней лучше знать заранее.

**Блокировка по User-Agent работает только с теми, кто честно представляется.** Facebook, Ahrefs, Semrush указывают себя в заголовке, потому что заинтересованы в том, чтобы их пускали. Парсер конкурента или скрапер для перепродажи данных подставит `Mozilla/5.0` и пройдёт мимо всех правил выше.

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

```nginx
# Отдельная зона для тяжёлых URL каталога
limit_req_zone $binary_remote_addr zone=catalog_heavy:10m rate=2r/s;

location /catalog/ {
    limit_req zone=catalog_heavy burst=10 nodelay;
    # ...
}
```

**`return 444` дешевле, чем `return 403`.** Специальный код nginx закрывает соединение без ответа: не тратится время на формирование страницы ошибки и не отдаётся трафик. Для явно нежелательных запросов это разумнее, но применять его к крупным поисковым роботам не стоит — лучше отдать им понятный статус.

**403 — сигнал «доступ запрещён навсегда».** Если бот вернётся к этим URL, он будет получать тот же ответ и в конце концов уберёт их из своей очереди — что нам и нужно. Но если вы ограничиваете временную нагрузку и хотите, чтобы бот вернулся позже, честнее отдавать `429` с заголовком `Retry-After`.

## Вывод

Если на Битриксе неожиданно растёт CPU без видимого роста продаж:

1. **Смотрите в access-логи nginx**, а не только в профилировщик PHP. Профилировщик отвечает на вопрос «что тормозит», логи — на вопрос «кто это заказал», а во внешней нагрузке важен именно второй.
2. **Блокируйте на входе**, а не в приложении: цена отброшенного запроса отличается на порядки.
3. **Блокируйте точечно.** Полный запрет для крупного краулера — это потеря превью ссылок в соцсетях или пропажа сайта из чьего-то индекса. Отсекать нужно конкретные тяжёлые сценарии, а не агента целиком.
4. **Настройте лимит частоты** как второй рубеж — он работает и против тех, кто не представляется.

**Следующий шаг:** После устранения внешней нагрузки займитесь внутренней проблемой — почему запросы к каталогу порождали неконтролируемый рост кэша catalog.section.