Перейти к содержимому

Производительность 1С-Битрикс - почему тормозит и что делать

Тормозит обычно не база, а запросы, которые к ней делает код. Разберём уровни оптимизации по порядку - от кода к инфраструктуре - и сквозные правила, которые не дают создать узкое место на ровном месте.

Как это работает

Три причины проблем на больших объёмах: конфликт одновременных запросов к базе, очередь даже из быстрых запросов и тяжёлые запросы, которые выполняются часто. Все три упираются в качество кода.

Оптимизировать нужно снизу вверх, по слоям.

  1. Код и запросы. Первый и главный уровень: не делать запросов в цикле, не тянуть лишние поля, фильтровать в запросе, а не в PHP. Об этом статья.
  2. Автокеширование компонентов. Штатные компоненты кешируют результат и на повторных запросах не идут в базу вовсе.
  3. Управляемый и тегированный кеш, собственный кеш данных. Точечный сброс по ключу или тегу при изменении данных.
  4. Композитный сайт. Готовый HTML отдаётся мгновенно, динамика догружается отдельно.
  5. Инфраструктура. Акселератор кода, настройка веб-сервера и базы, а при исчерпании одного сервера - веб-кластер.

Диагностика на всех уровнях - модуль «Монитор производительности»: он измеряет время и число запросов по компонентам, находит медленные запросы и подсказывает индексы.

Важное правило постановки задачи: цель должна быть измеримой. Не «сделать быстрее», а «главная отдаётся за столько-то миллисекунд при такой-то нагрузке». Иначе оптимизация не имеет конца.

Правила и примеры

1. Никогда не делайте запросов в цикле

Это причина номер один в проектах, которые «внезапно» легли под нагрузкой.

// ПЛОХО: один запрос на каждый элемент
foreach ($items as $item) {
$author = CIBlockElement::GetByID($item['PROPERTY_AUTHOR_VALUE'])->Fetch();
$item['AUTHOR_NAME'] = $author['NAME'];
}
// ХОРОШО: собрать идентификаторы, сделать один запрос, связать по ключу
$authorIds = array_unique(array_column($items, 'PROPERTY_AUTHOR_VALUE'));
$authors = [];
$rs = CIBlockElement::GetList([], ['IBLOCK_ID' => 8, 'ID' => $authorIds], false, false, ['ID', 'NAME']);
while ($row = $rs->Fetch()) {
$authors[$row['ID']] = $row['NAME'];
}
foreach ($items as &$item) {
$item['AUTHOR_NAME'] = $authors[$item['PROPERTY_AUTHOR_VALUE']] ?? '';
}

Критерий простой: число запросов не должно расти вместе с числом элементов.

Ещё лучше - вообще не делать второй запрос, а получить связанные данные сразу:

CIBlockElement::GetList([], $filter, false, false,
['ID', 'NAME', 'PROPERTY_AUTHOR.NAME', 'PROPERTY_AUTHOR.PREVIEW_PICTURE']);

2. Выбирайте только нужные поля

// ПЛОХО: тянет все поля и свойства элемента
$element = CIBlockElement::GetByID($id)->Fetch();
// ХОРОШО: явный список полей
$element = CIBlockElement::GetList([], ['ID' => $id], false, false,
['ID', 'NAME', 'DETAIL_PAGE_URL'])->Fetch();

Точная выборка снижает расход памяти базы при сортировке и заметно ускоряет запрос на больших таблицах.

3. Фильтруйте в запросе, а не в PHP

// ПЛОХО: выбрали всех и отфильтровали в цикле
// ХОРОШО: условие в запросе
$rs = CUser::GetList('ID', 'ASC', ['GROUPS_ID' => [5]]);

Сюда же относится фильтрация инфоблока по идентификатору, а не по символьному коду: код требует дополнительного запроса и джойна.

4. Не превращайте сравнение в поиск подстроки

// это LIKE '%xxx%'
['CODE' => 'xxx']
// это точное сравнение
['=CODE' => 'xxx']

В ORM метод where() генерирует точное сравнение сам. В легаси-фильтре оператор нужно писать явно - иначе платформа сделает поиск по подстроке, который не может использовать индекс целиком.

5. Не считайте COUNT, если он не нужен

// два запроса: COUNT и SELECT
['nPageSize' => 20]
// один запрос с LIMIT
['nTopCount' => 20]

Подсчёт общего количества нужен только для постраничной навигации. Для блока «последние новости» он лишний.

6. Специализированные методы вместо универсальных

Смена статуса оплаты заказа делается специальным методом, а не общим обновлением полей: он гарантирует вызов всех обработчиков оплаты. Универсальный метод формально сработает, но бизнес-логика вокруг него не выполнится.

7. Тяжёлое - из хита

Пересчёт счётчиков, агрегаты, обмен с внешними системами не должны выполняться при каждом показе страницы. Их место - агент по расписанию, фоновая задача после отдачи ответа или очередь сообщений.

Справочник

ПриёмЧто даётГде подробнее
Один запрос вместо циклалинейный рост запросов превращается в константуэта статья
Явный список полейменьше данных и памяти при сортировкеэта статья
Связанные данные в одном запросеустраняет проблему N+1Инфоблоки
Точный оператор фильтразапрос использует индексD7 ORM
nTopCount вместо nPageSizeодин запрос вместо двухэта статья
Автокеш компонентаповторные показы без обращения к базеКеширование
Тегированный кешточечный сброс при изменении данныхКеширование
Композитный сайтмгновенная отдача готового HTMLКомпозитный сайт
Монитор производительностиизмерение и поиск узких местМонитор
Веб-кластермасштабирование за пределы одного сервераКластер

Частые ошибки

Оптимизируют инфраструктуру вместо кода. Более мощный сервер лечит симптом: запрос в цикле останется запросом в цикле, просто порог падения сдвинется.

Кеш вместо исправления запроса. Кеш скрывает проблему до первого сброса. При инвалидации весь трафик разом уходит в базу с теми же тяжёлыми запросами.

Выборка «всего на всякий случай». Универсальный метод получения элемента тянет все поля и свойства, включая те, что не нужны на странице.

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

Фильтрация в PHP после выборки. База умеет фильтровать эффективнее, к тому же не придётся тащить лишние строки по сети и держать их в памяти.

Оптимизация без измерений. Без монитора производительности и профилирования легко потратить неделю на участок, который занимал два процента времени.

Частые вопросы

С чего начинать оптимизацию тормозящего сайта?

С измерения. Включите монитор производительности и посмотрите, где реально уходит время: число запросов на страницу, самые медленные запросы, распределение по компонентам. Дальше по порядку: убрать запросы в цикле, сократить выборки, добавить недостающие индексы, и только потом - кеширование и инфраструктура. Обратный порядок обычно заканчивается покупкой сервера, который лечит симптомы.

Разве кеш не решает проблему медленных запросов?

Только пока он валиден. В момент сброса - после изменения данных, публикации новости, обновления цен - весь трафик приходит в базу одновременно и с теми же тяжёлыми запросами. Кеш надо ставить поверх нормального кода, а не вместо него.

Как понять, что в коде проблема N+1?

Признак в мониторе: число запросов на странице растёт вместе с количеством выводимых элементов - двадцать товаров дают двадцать с лишним запросов. В коде это выглядит как обращение к API внутри цикла: получение свойств, автора, картинки или цены для каждого элемента отдельно. Лечится сбором идентификаторов и одним запросом либо получением связанных полей сразу в основной выборке.

Что быстрее - ORM или старое API?

Сама по себе разница между ними мало что решает: медленным запрос делает не выбор API, а лишние поля, отсутствие индексов, джойны и запросы в цикле. ORM удобнее тем, что явно выражает фильтры и связи и не даёт случайно получить поиск по подстроке вместо сравнения. Для нового кода это аргумент в её пользу, но переписывание рабочего легаси ради скорости обычно не окупается.

Связанные темы

Первоисточники