Play
online · 30 мин
Туториалы 25 мин чтения 13.05.2026

Тормозит обмен 1С с Битрикс: как замерить через события и найти узкое место

Ночной обмен из 1С может растянуться на часы, а менеджеры утром видят вчерашние остатки. Разбираем, как замерять время через события и находить узкое место, вместо того чтобы вслепую апгрейдить сервер.

Тормозит обмен 1С с Битрикс: с чего начать разбор

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

Причина простая: обмен 1С с Битрикс — это длинная цепочка шагов, и тормозить может любой из них. Чтение XML-файла, разбор его в таблицы, переиндексация, запись товаров, свойств, цен и остатков, финальные пересчёты. Пока не замерено, где именно теряется время, любая правка — гадание. Мы видели проекты, где полдня настраивали индексы MySQL, а тормозил слишком маленький read_size при чтении файла. И наоборот — где «оптимизировали» размер шага, а проблема была в индексах.

Хорошая новость: у модуля catalog есть штатные события, которые позволяют расставить метки времени по фазам импорта и увидеть реальную картину.

Порядок разбора, которого мы придерживаемся:

  1. Замерить фазы импорта через события — получить базовые цифры «до».
  2. Найти узкое место: какая фаза занимает непропорционально много времени.
  3. Применить одну из пяти типовых причин торможения — именно ту, что бьёт по найденной фазе.
  4. Проверить результат тем же замером и сравнить с базовыми цифрами.

Дальше разберём это по шагам. Посмотрим на официальные события модуля catalog - OnBeforeCatalogImport1C и OnSuccessCatalogImport1C — и покажем, как на них построить логгер фаз. Затем разберём метод CIBlockXMLFile::ReadXMLToDatabase, от которого напрямую зависит скорость чтения файла, и роль параметра read_size. Отдельно пройдёмся по пошаговому импорту — когда он спасает, а когда только добавляет накладных расходов. И в конце — типичные ошибки инфраструктуры, из-за которых обмен идёт часами при формально «нормальном» коде.

Замер времени импорта 1С: логгер на событиях OnBeforeCatalogImport1C и OnSuccessCatalogImport1C

Самый дешёвый способ понять, сколько именно идёт обмен и на каком файле он провисает, — повесить логгер на штатные события модуля catalog. Не нужен профайлер вроде Xdebug, не нужно править ядро и рисковать при обновлении, не нужно останавливать прод. События OnBeforeCatalogImport1C и OnSuccessCatalogImport1C официальные и документированные, стреляют они ровно в те моменты, которые нам интересны: старт сессии обмена и завершение обработки одного XML-файла. Пара ABS_FILE_NAME + arParams даёт привязку тайминга к конкретному файлу, а не абстрактное «обмен шёл 5 часов».

У нас на проектах это первый шаг перед любым профилированием по фазам. Сначала получаем карту «файл → длительность», потом уже лезем в ReadXMLToDatabase, индексы и read_size. Логгер ставится за пять минут и живёт на проде хоть год — обмену он не мешает, если писать в отдельный файл или свою таблицу.

Регистрируем оба хендлера в /local/php_interface/init.php через D7-обёртку EventManager:

PHP
<?php
// /local/php_interface/init.php
use Bitrix\Main\EventManager;

$eventManager = EventManager::getInstance();

$eventManager->addEventHandler(
    'catalog',
    'OnBeforeCatalogImport1C',
    ['ImportTimingLogger', 'onBefore']
);

$eventManager->addEventHandler(
    'catalog',
    'OnSuccessCatalogImport1C',
    ['ImportTimingLogger', 'onSuccess']
);

class ImportTimingLogger
{
    private const LOG_FILE = '/local/var/log/1c_import_timing.log';

    public static function onBefore(array $arParams, $ABS_FILE_NAME): void
    {
        $GLOBALS['IMPORT_1C_START'][$ABS_FILE_NAME] = [
            'time'   => microtime(true),
            'params' => $arParams,
        ];
    }

    public static function onSuccess(array $arParams, $ABS_FILE_NAME): void
    {
        $start = $GLOBALS['IMPORT_1C_START'][$ABS_FILE_NAME]['time'] ?? null;
        if ($start === null) {
            return; // старт не поймали — файл пришёл из другого процесса
        }

        $delta = microtime(true) - $start;
        $line  = sprintf(
            "[%s] %s | %.3f сек | шаг=%s\n",
            date('Y-m-d H:i:s'),
            $ABS_FILE_NAME,
            $delta,
            $arParams['STEP'] ?? '-'
        );
        file_put_contents(
            $_SERVER['DOCUMENT_ROOT'] . self::LOG_FILE,
            $line,
            FILE_APPEND | LOCK_EX
        );

        // дублируем в свою таблицу для агрегатов
        \Bitrix\Main\Application::getConnection()->add(
            'INSERT INTO b_import_timing (FILE_NAME, DURATION, DATE_START) VALUES (?, ?, ?)',
            [$ABS_FILE_NAME, $delta, ConvertTimeStamp($start, 'FULL')]
        );

        unset($GLOBALS['IMPORT_1C_START'][$ABS_FILE_NAME]);
    }
}

Старт мы держим в $GLOBALS, а не в статике класса — обмен 1С может идти в несколько параллельных процессов, и глобальная карта по ключу ABS_FILE_NAME не путает тайминги между ними. Если у вас один процесс и один файл за сессию, можно заменить на статическое свойство. Но глобальный массив универсальнее.

Таблицу b_import_timing создайте заранее — минимальный набор полей:

SQL
CREATE TABLE b_import_timing (ID INT AUTO_INCREMENT PRIMARY KEY,
                                                    FILE_NAME VARCHAR(500) NOT NULL,
                                                                           DURATION DECIMAL(10, 3) NOT NULL,
                                                                                                   DATE_START DATETIME NOT NULL,
                                                                                                                       INDEX ix_file (FILE_NAME),
                                                                                                                             INDEX ix_date (DATE_START));

Как читать лог: дельта между OnBeforeCatalogImport1C и OnSuccessCatalogImport1C — это длительность обработки одного XML-файла. Обмен обычно состоит из нескольких файлов (товары, цены, остатки, свойства), и общая длительность сессии складывается из них. Если файлов десятки, не смотрите лог глазами — агрегируйте по ABS_FILE_NAME за сутки или неделю:

SQL
SELECT FILE_NAME,
       COUNT(*) AS runs,
       AVG(DURATION) AS avg_sec,
       MAX(DURATION) AS max_sec
FROM b_import_timing
WHERE DATE_START >= NOW() - INTERVAL 7 DAY
GROUP BY FILE_NAME
ORDER BY avg_sec DESC;

Выбросы в max_sec при стабильном avg_sec — это либо тяжёлый файл с новыми товарами, либо конкуренция за блокировки MySQL с другим процессом (например, ночным бэкапом). Если avg_sec растёт от недели к неделе — каталог раздувается, пора смотреть индексы и read_size, об этом ниже в статье. Дальше режем по DATE_START и сопоставляем с расписанием cron — сразу видно, если обмен накладывается сам на себя.

Не пишите тайминги в b_iblock_element или в инфоблок. На каждый файл обмена это лишняя запись в горячую таблицу, а на каталоге в десятки тысяч SKU — ещё и триггер на обновление индексов поиска. Отдельная таблица b_import_timing или файл в /local/var/log/ — оба варианта бесплатные по нагрузке. Если нужен только разовый замер — хватит файла, если планируете мониторинг — таблица с индексами по FILE_NAME и DATE_START.

Профилирование обмена 1С: как найти узкое место по фазам

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

Типичный сценарий: администратор жалуется, что ночной обмен не укладывается в окно, разработчик открывает код, видит вызов CIBlockXMLFile::ReadXMLToDatabase() и решает, что виновато чтение XML. А на деле чтение проходит за минуты, а всё остальное время процесс молча стоит на перестроении индексов. У нас в практике такое расхождение между «кажется» и «» — самое частое, с чем приходят на разбор. Профилирование обмена стоит начинать не с оптимизации, а с честного замера по фазам.

Как разложить сессию на фазы без правки ядра

Ядро модуля catalog не отдаёт наружу разбивку по фазам — но её и не нужно выковыривать из ядра. Достаточно обернуть сессию обмена своими событиями и логировать переходы между шагами. Мы делаем это через OnBeforeCatalogImport1C и OnSuccessCatalogImport1C: первое даёт точку старта, второе — точку финиша конкретного XML-файла, а между ними пишем в лог отметки времени.

Ключевой момент — не править ядро и не патчить cml2.php. Все точки, которые вам нужны, уже доступны через штатные события модуля. Если нужна более тонкая разбивка, чем старт/финиш файла, в сообществе принято цепляться за промежуточные события импорта и логировать их отдельно. Об этом ниже.

Что логировать на каждом шаге:

  • время старта фазы и её имя — чтобы потом отсортировать по длительности;
  • имя текущего XML-файла и его размер — крупный файл сам по себе объясняет провисание;
  • количество обработанных элементов на момент отметки — видно, идёт ли процесс или стоит.

Логи складывайте в отдельный файл, а не в bitrix/debug.log — иначе утонете в мусоре от других модулей.

Как отличить медленное чтение XML от медленной записи в MySQL

Самый частый вопрос при разборе: тормозит парсинг XML или запись в базу? Отличить просто, если смотреть на два независимых признака одновременно.

Первый — загрузка CPU и диска на сервере. Чтение XML — это в основном CPU и последовательное чтение файла. Если в момент провисания процессор загружен, а диск почти не пишет — вы в фазе парсинга. Если наоборот: CPU простаивает, а диск и iostat показывают активную запись — вы в фазе вставки и индексации.

Второй — состояние MySQL. Подключитесь к базе и посмотрите SHOW PROCESSLIST в момент провисания. Долгий запрос на INSERT или ALTER TABLE — это запись. Пустой список процессов при активном PHP-скрипте — это парсинг или подготовка данных в памяти.

Удобнее всего совместить оба признака с логом фаз из предыдущего раздела: тогда вы видите не только «где стоим», но и «на чём именно». Заодно проверьте параметр read_size в вызове ReadXMLToDatabase() — он определяет, сколько байт считывается за одну операцию, и слишком маленькое значение само по себе растягивает фазу чтения.

Что показывает Xdebug в BitrixVM и когда его включать

В BitrixVM Xdebug присутствует из коробки, но по умолчанию отключён. Включать его стоит точечно — на время разбора, а не на постоянку. Профилировщик Xdebug даёт то, чего не даёт лог фаз: он показывает, какая именно функция внутри фазы съедает время. Это полезно, когда фаза чтения XML внезапно занимает не минуту, а двадцать — и непонятно, почему.

Порядок такой: включаете Xdebug под root, ставите режим профилирования, прогоняете один обмен и снимаете профиль. Дальше смотрите не весь отчёт, а верхушку — самые тяжёлые вызовы. Часто там оказывается не парсинг, а подготовка данных или работа с кешем.

Для регулярного мониторинга Xdebug не нужен — хватает лога фаз. Профилировщик включайте как разовый инструмент, когда лог показал аномалию и надо копнуть глубже.

Фаза Что делает Где смотреть Типичный симптом
Очистка таблиц Готовит таблицы к импорту Лог фаз, SHOW PROCESSLIST Долгий DELETE на старте
Чтение XML Парсит файл обмена CPU, read_size Процессор загружен, диск молчит
Индексация Строит индексы по данным iostat, MySQL CPU простаивает, диск пишет
Метаданные и группы Импортирует структуру каталога Лог фаз Много мелких запросов подряд
Товары и свойства Пишет элементы и значения свойств MySQL, размер b_iblock_element_prop_s* Провисание на широких свойствах
Цены и остатки Обновляет торговые предложения Лог фаз, MySQL Медленный UPDATE по большой таблице

Медленный импорт 1С Битрикс: read_size и пошаговый импорт

Разница принципиальная. read_size работает внутри одной операции чтения и упирается в память и диск. Пошаговый импорт работает снаружи, на уровне сессии обмена, и упирается в лимиты веб-сервера. Когда к нам приходят с жалобой «импорт идёт часами», первым делом мы смотрим, какой именно из двух сценариев у клиента: файл читается долго, но сессия завершается, или сессия обрывается на середине с ошибкой 504. От этого зависит, какой параметр вообще имеет смысл трогать.

Как read_size влияет на скорость и на память

По документации read_size определяет количество байт, считываемых за одну операцию. Большие значения повышают производительность, но и потребление памяти растёт — причём нелинейно, потому что прочитанный кусок ещё нужно разобрать и разложить по таблицам. По умолчанию метод вызывается с read_size = 1024, и на крупных XML-файлах это превращается в тысячи мелких чтений.

Увеличение имеет смысл, когда профилирование показало: узкое место — именно фаза чтения файла, а не индексация и не запись товаров. Если тормозит MySQL на вставках, read_size ничего не даст: вы просто быстрее дойдёте до того же медленного места. Мы обычно поднимаем значение ступенями — 4096, 8192, 16384 — и после каждого шага смотрим memory_limit и потребление процесса. Резкий скачок до мегабайтов почти гарантированно роняет PHP-FPM.

Когда включать CATALOG_IMPORT_STEP_SIZE и как это спасает от request_terminate_timeout

Если в логах PHP-FPM фиксируется ошибка request_terminate_timeout, а обмен обрывается после успешного старта — это признак того, что сессия импорта превышает лимит выполнения воркера. Для решения проблемы в настройках компонента следует использовать пошаговый режим: параметр File_size_limit ограничивает размер считываемой части файла, а метод CIBlockXMLFile::ReadXMLToDatabase позволяет через read_size регулировать объем данных, обрабатываемых за одну операцию, чтобы уложиться в ограничения сервера.

пошаговость — это не ускорение, а выживание. Импорт может идти столько же по суммарному времени, но он перестанет падать. Зато появляется побочный эффект: между шагами освобождается память, и обмен перестаёт упираться в memory_limit. Мы включаем этот режим, когда каталог большой и обмен стабильно не доживает до конца, а увеличивать таймауты в php-fpm.conf нельзя или бессмысленно.

PHP
<?php
// Скрипт-обёртка для ручного запуска импорта с явными параметрами.
// Запускать из CLI или из агента, не из публичного эндпоинта.
require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';

use Bitrix\Main\Loader;

Loader::includeModule('iblock');
Loader::includeModule('catalog');

$filePath = $_SERVER['DOCUMENT_ROOT'] . '/upload/1c_catalog/import.xml'; // замените на ваш путь
$fp = fopen($filePath, 'rb');

if (!$fp) {
    fwrite(STDERR, "Не удалось открыть {$filePath}\n");
    exit(1);
}

$xmlFile = new CIBlockXMLFile();
$NS = [];                 // состояние импорта между вызовами
$timeLimit = 20;          // секунд на один вызов, чтобы не упереться в таймаут
$readSize = 8192;         // байт за операцию — поднимайте постепенно

$finished = false;
while (!$finished) {
    $finished = $xmlFile->ReadXMLToDatabase($fp, $NS, $timeLimit, $readSize);
}

fclose($fp);
echo "Импорт завершён\n";

Здесь ключевое — timeLimit и readSize передаются явно, а не берутся из настроек по умолчанию. В цикле метод возвращает true только когда файл дочитан до конца, поэтому обёртка сама управляет порциями и не зависит от таймаута веб-сервера.

Но есть случаи, когда ни read_size, ни шаги не помогут — потому что проблема не в чтении и не в таймауте:

  • Медленный диск или сетевое хранилище: XML читается, но физически не успевает отдаваться.
  • Отсутствие индексов в таблицах каталога: каждая вставка товара сканирует таблицу целиком.
  • Конкурирующие процессы: cron запускает обмен поверх ещё не завершившегося предыдущего.
  • Тяжёлые обработчики на событиях каталога: OnAfterIBlockElementUpdate и подобные тормозят каждую запись.

Грабля: выставить read_size сразу в мегабайтах «чтобы ускорить». На слабом сервере это приводит к OOM и падению PHP-FPM — причём не всегда сразу, а под нагрузкой, когда рядом работает ещё один процесс. Метод читает блок в память целиком перед разбором, поэтому значение read_size напрямую умножается на количество параллельных обменов. Поднимайте постепенно, следите за memory_limit и реальным потреблением процесса, а не ставьте 10 МБ «на глаз».

События обмена 1С Битрикс: какое за что отвечает

После логгера и профилирования из предыдущих секций становится видно: «вот здесь провисает». Дальше упираешься в вопрос — каким хуком цепляться за импорт, чтобы получить нужную точку? В модуле catalog есть два штатных события: OnBeforeCatalogImport1C и OnSuccessCatalogImport1C. Оба описаны в документации, оба принимают arParams (параметры подключения компонента обмена) и ABS_FILE_NAME (полный путь к текущему XML-файлу). Первое срабатывает до старта импорта, второе — после того, как обмен одним XML-файлом закончен. Этого достаточно, чтобы обернуть сессию в таймер, посчитать длительность по каждому файлу и понять, на каком шаге вы теряете часы.

Кроме официальной пары, в сообществе обсуждают недокументированное OnCompleteCatalogImport1C — по сигнатуре оно похоже на OnSuccessCatalogImport1C. Разберём его ниже: как диагностический инструмент оно иногда полезно, но в продакшене опираться на него нельзя.

Событие Когда стреляет Аргументы Официальное Опора в проде
OnBeforeCatalogImport1C Перед началом импорта каталога arParams, ABS_FILE_NAME Да Да
OnSuccessCatalogImport1C После окончания обмена одним XML-файлом arParams, ABS_FILE_NAME Да Да
OnCompleteCatalogImport1C По завершении импорта (по данным сообщества) Аналогичны OnSuccessCatalogImport1C Нет Нет

Разделение простое. Под замер берите официальную пару OnBeforeCatalogImport1C / OnSuccessCatalogImport1C: первое даёт точку «старт», второе — «финиш» по конкретному файлу. Вместе они позволяют накопить статистику по фазам без вмешательства в сам импорт. Регистрируйте обработчики через EventManager в init.php, пишите метки в свой лог или таблицу — получите честную картину длительности, не трогая логику обмена.

Под пост-обработку (пересчёт индексов, сброс кеша, отправка уведомлений, переиндексация поиска) берите OnSuccessCatalogImport1C: он гарантированно сработал после того, как файл разобран и данные легли в базу.

Ставить тяжёлую логику на OnBeforeCatalogImport1C смысла нет — данные ещё не пришли.

Недокументированное OnCompleteCatalogImport1C заманчиво тем, что якобы ловит самый финал сессии, а не отдельного файла. Но у него нет описания в документации, нет гарантий по имени и сигнатуре между версиями модуля, и поведение может измениться в любом обновлении без предупреждения. Для отладки — можно временно повесить и посмотреть. Для критичной бизнес-логики (списание остатков, синхронизация цен, триггеры на склад) — нет.

Ускорить обмен 1С Битрикс: пять причин торможения и что с каждой делать

Разбор обмена — это всегда раскопки по слоям. Причины медленного импорта живут ровно в пяти местах, и по нашему опыту они почти никогда не встречаются поодиночке. Сначала разбор XML: сколько байт за операцию читает CIBlockXMLFile::ReadXMLToDatabase и как часто дёргается парсер. Потом запись в MySQL: сколько строк за раз уходит в b_iblock_element и как ведут себя свойства. Третий слой — индексация: пересборка индексов по b_iblock_element_prop_s* и поисковых таблиц, которая идёт после загрузки. Четвёртый — инфраструктура: cron, PHP-FPM, диск, лимиты воркеров. Пятый — пользовательские обработчики на событиях импорта, которые синхронно висят в основном потоке и делают что-то своё.

Задача «ускорить обмен» на практике почти всегда означает «найти, какой из пяти слоёв даёт основную задержку, и снять её». Мы разбираем это по шагам именно потому, что попытка ускорить всё сразу приводит к тому, что меняют настройки, а картина не двигается — узкое место осталось нетронутым. Ниже — пять причин, которые встречаются чаще всего, и что с каждой делать.

  • Маленький read_size — XML читается мелкими порциями, парсер и БД дёргаются слишком часто.
  • Нет индексов на b_iblock_element_prop_s* — каждый апдейт свойства превращается в full scan.
  • Cron каждую минуту с наложением процессов — новый импорт стартует, пока предыдущий ещё не закончился.
  • Тяжёлые хендлеры на событиях импорта — пост-обработка выполняется синхронно внутри основного потока.
  • request_terminate_timeout и отсутствие пошагового импорта — воркер убивается по таймауту, обмен начинается заново.
Причина Как проявляется в логе Как проверить Что делать
Маленький read_size Долгие паузы между пакетами чтения, ровный «полочный» график Логгер на событие импорта + замер длительности чтения Поднять read_size в вызове ReadXMLToDatabase, следить за памятью
Нет индексов на prop-таблицах Время растёт нелинейно с ростом каталога SHOW INDEX FROM b_iblock_element_prop_s<ID> Добавить индексы, пересобрать статистику
Cron каждую минуту В логе несколько стартов подряд, обмены накладываются ps aux | grep 1c, лог крона Интервал больше максимальной длительности, flock-лок
Тяжёлые хендлеры Импорт «замер» на конкретном этапе, CPU в потолок Профилирование по фазам, замер хендлеров Вынести в очередь, обрабатывать асинхронно
Таймаут и отсутствие шагов Обрыв на середине, повторный старт с нуля Лог PHP-FPM, request_terminate_timeout Пошаговый импорт через параметр read_size метода ReadXMLToDatabase

Причину надо выбирать по данным логгера, а не по интуиции. Самая частая ошибка — сразу лезть в read_size, потому что про него все пишут, и не проверить, совпадает ли симптом с вашими таймингами. Если в логе видно, что импорт стартует три раза за ночь и каждый раз идёт с нуля — это не XML-разбор, это cron и таймаут. Если время растёт пропорционально числу SKU — это индексы. Если обмен «замер» ровно на шаге импорта свойств или после него — смотрите хендлеры.

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

Оптимизация обмена 1С: что делать с индексами MySQL

Распространённая ошибка: искать причину медленного обмена в XML, в размере файла, в скорости канала между 1С и сайтом. А по факту на профиле, который мы снимали в секции выше, видно другое — основное время съедает запись товаров и цен в MySQL, а не разбор XML. Каждый товар из выгрузки — это UPDATE или INSERT в b_iblock_element, апдейт значений свойств в таблицах b_iblock_element_prop_s* и пересчёт в b_catalog_price. На каталоге в десятки тысяч SKU это миллионы мелких транзакций. Нет индекса по нужным колонкам — каждая такая запись превращается в full scan.

Когда к нам приходят с жалобой «обмен идёт всю ночь», первым делом мы смотрим не на XML-файл, а на план выполнения запросов и на состав индексов в таблицах каталога. Именно здесь прячется та самая узкая точка, которую профилирование по фазам только нащупывает. Индексы — это тот рычаг, который реально ускоряет обмен без правки самого модуля, и он же — самый опасный, если делать его не вовремя.

Ниже — какие индексы нужны, как их проверить по slow query log и почему создавать их в разгар импорта категорически нельзя.

Какие индексы реально нужны на b_iblock_element_prop_s*

Таблицы b_iblock_element_prop_s*- это «широкие» таблицы значений свойств, где колонки соответствуют ID свойств инфоблока. Импорт из 1С постоянно дёргает их на запись, а витрина и компоненты - на чтение. Критичны два индекса: первичный ключ поIBLOCK_ELEMENT_ID (обычно уже есть) и составной индекс по тем свойствам, по которым идёт фильтрация в каталоге.

Что именно индексировать - подскажет не догадка, а лог медленных запросов. Но есть и типовые случаи, которые мы правим почти в каждом втором проекте:

  • по IBLOCK_ELEMENT_ID - если он не первичный ключ, запись свойств при импорте сканирует всю таблицу;
  • по часто фильтруемым свойствам (бренд, размер, цвет) - составной индекс из 2-3 колонок;
  • по b_catalog_price - составной (PRODUCT_ID, CATALOG_GROUP_ID), без него выборка цены в цикле импорта читает таблицу целиком.

Не ставьте индекс «на всё»: каждая лишняя колонка в индексе замедляет запись, а во время импорта запись - это 90% нагрузки. Индексируем только то, что реально участвует в WHERE и JOIN.

Как читать slow query log во время импорта

Slow query log - самый честный источник. Включаете его на время обмена, потом смотрите, какие запросы висят дольше порога. Синтаксис включения зависит от версии MySQL, но общий смысл один: задать long_query_time и указать файл лога.

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

Обратите внимание на Rows_examined в строке лога: если там десятки тысяч строк на запрос, который выбирает одну цену товара, - индекс отсутствует или не используется. Сравните это число с Rows_sent: разрыв в тысячи раз - верный признак full scan.

SQL
-- Смотрим план выборки товара с ценой (типичный запрос витрины и импорта)
EXPLAIN
SELECT p.PRODUCT_ID,
       p.PRICE,
       p.CURRENCY
FROM b_catalog_price p
WHERE p.PRODUCT_ID = 12345
    AND p.CATALOG_GROUP_ID = 1;

-- Если type = ALL и rows большое — индекса нет, создаём составной

ALTER TABLE b_catalog_price ADD INDEX ix_price_product_group (PRODUCT_ID, CATALOG_GROUP_ID);

-- Повторный EXPLAIN должен показать type = ref и rows = 1
EXPLAIN
SELECT p.PRODUCT_ID,
       p.PRICE,
       p.CURRENCY
FROM b_catalog_price p
WHERE p.PRODUCT_ID = 12345
    AND p.CATALOG_GROUP_ID = 1;

Не создавайте индексы во время идущего импорта. ALTER TABLE в MySQL берёт блокировку на таблицу - обмен встанет колом, а на больших таблицах операция может тянуться часами. Делайте это только в окно между сессиями обмена: остановите импорт, дайте текущим транзакциям закрыться, создайте индекс, проверьте EXPLAIN и только потом запускайте обмен снова. Если окна нет - используйте pt-online-schema-change или gh-ost, но не ALTER в лоб.

Узкое место обмен 1С: как проверить, что вы его нашли

Самая коварная ситуация в разборе обмена - когда правку внесли, замеры сняли, а непонятно, стало ли лучше. Или стало, но непонятно почему. Мы регулярно видим, как администратор меняет read_size, чистит лишние индексы и переводит обмен на cron - всё за один вечер, - а потом на вопрос «что из этого помогло» пожимает плечами. Через месяц импорт снова начинает провисать, и разбор приходится начинать с нуля: базы для сравнения нет.

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

Чтобы это увидеть, нужна дисциплина замера: одна правка - один прогон - сравнение по тем же файлам.

Ниже - четыре шага, которые мы прогоняем сами и советуем клиентам.

  1. Зафиксировать базовые тайминги по всем файлам за 2–3 ночи. Логгер на OnBeforeCatalogImport1C и OnSuccessCatalogImport1C (мы разбирали его выше) пишет имя файла и время старта/финиша в отдельный лог. Две-три ночи нужны, чтобы отсечь разовый выброс - например, ночь с бэкапом или с параллельным полнотекстовым переиндексированием. Сохраните лог как эталон и не трогайте его до конца эксперимента.
  2. Применить ровно одну правку. Одну - значит одну. Увеличили read_size в ReadXMLToDatabase - и всё, больше ничего в эту ночь не меняем. Не «заодно поправим cron», не «и индекс пересоберём», не «и CATALOG_IMPORT_STEP_SIZE подкрутим». Каждая лишняя правка убивает эксперимент: вы уже не отличите причину от следствия.
  3. Сравнить дельты по тем же файлам, а не среднее по всем. Средний тайминг по каталогу врёт: один тяжёлый файл с ценами и остатками тянет его вниз, а вы смотрите на «среднее по больнице». Берите конкретные файлы из эталонного лога и смотрите дельту между событиями по каждому из них - OnSuccess минус OnBefore. Если правка сработала, дельта просядет именно на тех файлах, где живёт причина.
  4. Дельта не изменилась - откатить правку и перейти к следующей причине. Не оставляйте «на всякий случай»: параметр, который ничего не ускорил, но увеличил потребление памяти, - это чистая потеря. Откатили, вернулись к базовому логу, взяли следующую гипотезу из списка причин. Так вы методично отсекаете шум и оставляете только то, что реально двигает тайминги.

Самые частые ошибки, которые ломают импорт 1С в Битрикс

Разговор про ускорение обмена мы начали с замеров и профилирования. Но у части проектов проблема не в скорости, а в том, что импорт вообще не доезжает до конца. Он падает на середине, зависает на одном файле, оставляет каталог в половинчатом состоянии - часть товаров обновилась, часть нет, а цены и остатки разъехались с реальностью. Такие сбои мы разбираем чаще, чем «медленно, но стабильно»: медленный обмен хотя бы предсказуем, а сломанный ломает витрину и заказы.

Причина обычно не в самом XML и не в канале между 1С и сайтом. Основные грабли живут на стороне приёмника: в настройках PHP-FPM по таймауту, в транзакциях MySQL, в неправильно выставленном read_size, в отсутствии пошагового режима при большом файле. И почти всегда рядом лежит вторая проблема - нет логгера, поэтому непонятно, на каком именно файле обмен оборвался.

Ниже собрали грабли, которые встречаем регулярно, и короткий чеклист, что сделать перед тем, как катить правки в прод.

Грабля Причина Как заметить Как лечить
Обмен обрывается на середине request_terminate_timeout в PHP-FPM Последний файл в логе - один и тот же Включить пошаговый импорт, поднять лимит
Каталог в половинчатом состоянии Фатальная ошибка внутри транзакции Часть товаров есть, часть пропала Логгер на события обмена, разбор файла
Память заканчивается на большом XML Завышенный read_size Fatal error про memory_limit Снизить read_size в ReadXMLToDatabase
Наложение двух обменов Cron раз в минуту без блокировки Паразитная нагрузка, дубли в логах Блокировка процесса или увеличенный интервал

Перед выкаткой любой правки в обмен держим под рукой короткий чеклист. Он экономит часы откатов.

  • Свежий бэкап БД: импорт пишет в b_xml_tree и таблицы каталога, откатить точечно сложно.
  • Окно между сессиями обмена - правку вносим, когда 1С гарантированно не передаёт файл.
  • Логгер уже пишет: события OnBeforeCatalogImport1C и OnSuccessCatalogImport1C фиксируют старт и финал каждого файла.
  • Откат подготовлен: старые настройки read_size и PHP-FPM сохранены рядом, чтобы вернуть одной строкой.

Короткие ответы на частые вопросы про обмен 1С и Битрикс

Пока мы разбирали замеры, профилирование и индексы, часть вопросов осталась за кадром - они не тянут на отдельную секцию, но всплывают в переписке регулярно. Три темы, которые чаще всего приходится объяснять после того, как основные рычаги уже перебраны: что делать с dbconn.php, куда вешать недокументированные события модуля catalog и как аккуратно включить Xdebug, чтобы он не съел весь бюджет времени импорта. По официальной документации с версии главного модуля 20.900.0 параметры соединения с БД из dbconn.php не читаются - настройки живут в .settings.php. При этом вторичные источники до сих пор советуют прописывать туда константы вроде BX_CATALOG_IMPORT_1C_PRESERVE для отладки: это работает как практика сообщества, но не как норма. То же с событиями - OnSuccessCatalogImport1C документирован, а OnCompleteCatalogImport1C известен в основном по разборам на профильных форумах. Ниже - короткие ответы на то, что не влезло в предыдущие секции.

Можно ли замерить импорт без правки ядра?

Да, и это предпочтительный путь. Ничего в /bitrix/modules/ не трогаем - вешаем обработчики на OnBeforeCatalogImport1C и OnSuccessCatalogImport1C через EventManager в init.php или в собственном модуле и пишем тайминги в файл или в лог. Ядро остаётся чистым, обновления не ломают замер.

Чем OnSuccessCatalogImport1C отличается от OnCompleteCatalogImport1C?

OnSuccessCatalogImport1C документирован и стреляет после окончания обмена одним XML-файлом, принимая arParams и ABS_FILE_NAME. OnCompleteCatalogImport1C в документации не описан, аргументы у него аналогичные - по разборам в сообществе он срабатывает в конце полного цикла импорта. Если нужен гарантированный контракт - опирайтесь на первый.

Почему увеличение read_size не всегда ускоряет импорт?

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

Что делать, если PHP-FPM убивает процесс по request_terminate_timeout?

Разбить импорт на шаги: включить пошаговый режим через CATALOG_IMPORT_STEP_SIZE, чтобы каждый HTTP-запрос укладывался в лимит. Параллельно поднять сам таймаут в пуле FPM - но это костыль, а не решение. Правильнее уменьшить объём работы на один вызов, чем ждать, что процесс доживёт.

Как понять, что виноват не обмен, а индексы в MySQL?

Смотрим EXPLAIN по тяжёлым запросам импорта и SHOW PROCESSLIST в момент провисания. Если фаза чтения XML проходит быстро, а время уходит на UPDATE/INSERT с полным сканированием - дело в индексах, а не в 1С и не в канале. Об этом подробно в секции про оптимизацию индексов.

Читайте также: Обмен с сайтом 1С Битрикс: пошаговая настройка узла и импорта, События 1С в Bitrix: как менять данные при импорте - рецепты под реальные задачи.

Подробнее об услуге: интеграция 1С с сайтом и CRM.

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

Можно ли замерять время импорта не через OnBeforeCatalogImport1C, а через OnBeforeCatalogImport1C с параметром?

Нет, у OnBeforeCatalogImport1C нет параметров - он вызывается один раз в начале импорта каталога. Для пошаговых замеров используйте OnSuccessCatalogImport1C в связке с $_SESSION или своим статическим счётчиком шагов.

Что делать, если OnSuccessCatalogImport1C не срабатывает при пошаговом импорте?

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

Чем отличается read_size от step_import в настройках обмена 1С?

read_size задаёт размер порции XML, которую модуль читает за один проход парсера, а step_import ограничивает число элементов каталога, обрабатываемых за один шаг импорта. Уменьшение read_size снижает пиковую память, а step_import - время одного запроса к БД.

А если у меня импорт тормозит только на этапе торговых предложений, а каталог летает?

Смотрите события OnBeforeOfferImport1C и OnSuccessOfferImport1C отдельно - узкое место обычно в привязке предложений к товарам через CML2_LINK. Проверьте индексы на свойствах CML2_LINK и CML2_BASE_UNIT в таблице b_iblock_element_property.

Можно ли ускорить обмен, отключив пересчёт цен и остатков на каждом шаге?

Да, в настройках обмена снимите галочки «Пересчитывать цены» и «Обновлять остатки» на время импорта, а пересчёт запустите отдельным агентом после завершения. Это убирает лишние UPDATE по b_catalog_price и b_catalog_store_product на каждой итерации.

Что делать, если после оптимизации индексов MySQL импорт всё равно упирается в 100% CPU?

Проверьте, не блокирует ли импорт транзакция с длинным lock wait - включите slow_query_log с long_query_time=1 и посмотрите EXPLAIN на самых частых запросах. Часто виноват не сам импорт, а параллельный агент или cron, который держит блокировку на b_iblock_element.

Читайте также: События в Битрикс: где искать, как подписаться в init.php и не сломать импорт 1С.

Читайте также: Битрикс тормозит: диагностика узких мест и оптимизация производительности.

#Bitrix #PHP #SQL #MySQL #Cron
автор · Backend / SRE Engineer
Артем Колячек

Backend-разработчик и SRE в студии Paradigma. Занимается тем, что у других проектов обычно обнаруживается за неделю до запуска: переездом Bitrix-проектов между серверами, починкой кешей после миграций, выстраиванием pipeline для контейнеров, мониторингом под нагрузкой.

До Paradigma — 7 лет в backend (PHP + Postgres) и в operations (Linux, Docker, Coolify, restic-бэкапы). Любит когда логи разговаривают полным синтаксисом ошибки, а не «что-то пошло не так».

На блоге пишет ровно про те ситуации с которыми сам разбирался руками: какой Bitrix-апдейт сломал кеш и как откатить, почему Docker-сеть не находит контейнер после рекрейта, что делать когда asyncpg ругается на event loop.