Расследование: как накопленный технический долг обрушил очередь бизнес-процессов Битрикс 24
В Инитлаб мы поддерживаем крупнейшие внедрения Битрикс 24 в России: сотни одновременных пользователей, огромные базы данных и терабайты файлов, сотни бизнес-процессов. Иногда решение проблем таких внедрений превращается в увлекательное детективное расследование на стыке разработки, отладки, профилировки, администрирования баз данных. Публикуем один из таких кейсов с полезными подробностями по отладке производительности бизнес-процессов Битрикс 24.
Проблема
Клиент обратился с проблемой: CRM Bitrix 24 периодически «зависает», не получается добавлять задачи и все базовая функциональность висит. Коробочная версия (self-hosted) Битрикс 24.
Хронология проблемы*
Январь 2025. Клиент запускает автоматизацию на стадии одного из Smart-процессов CRM. Автоматизация содержит REST-активность — робот обращается к внешнему приложению и ждёт ответного колбэка. В этот период в систему поступает большой поток записей, для каждой запускается отдельный экземпляр бизнес-процесса. Внешнее приложение перестаёт отвечать — все запущенные экземпляры зависают в состоянии «Ожидание ответа от приложения». REST-активность впоследствии удаляют из шаблона, но сотни тысяч уже запущенных экземпляров продолжают висеть в базе, ожидая колбэк который никогда не придёт.
Февраль–март 2025. Зависшие экземпляры тихо накапливаются. Параллельно продолжают копиться зависшие экземпляры других автоматизаций — роботов для лидов с таймерами и задержками. Система работает штатно, старый планировщик (CBPSchedulerService) обрабатывает очередь через таблицу агентов. Проблема незаметна.
Апрель 2026 — обновление Bitrix. Разработчики Bitrix вводят новый механизм возобновления бизнес-процессов на базе Symfony Messenger. 13 апреля создаётся таблица очереди и добавляются индексы, однако из-за бага в коде механизм не работает. 28 апреля выходит исправление — баг устранён, новая очередь начинает функционировать.
28 апреля — включение нового механизма. Система обнаруживает сотни тысяч зависших бизнес-процессов и начинает генерировать для них сообщения в новую очередь. Очередь мгновенно захлёстывается накопленным за год долгом. Scheduler пытается обработать поток сообщений, но не справляется.
Май 2026 — первые дедлоки. На перегруженной таблице очереди начинают регулярно возникать дедлоки. Причина — баг в новом коде: метод сброса зависших сообщений (requeueStaleMessages) и планировщик захватывают блокировки на одну и ту же строку в обратном порядке. MySQL автоматически откатывает одну из транзакций, система продолжает работу, но дедлоки повторяются.
12 мая 2026 — расследование и устранение проблемы. Анализ дедлоков приводит к обнаружению корневой причины — сотен тысяч мёртвых экземпляров бизнес-процессов, ссылающихся на удалённые шаблоны и зависших в ожидании колбэков от несуществующих приложений. После удаления мёртвых экземпляров очередь разгружается, дедлоки прекращаются.
* изначально не было данных о том, как появилась проблема и по какой причине. Хронологию составили по итогу расследования и решения.
Расследование
Отправная точка
Отправной точкой стал лог MySQL с записью о последнем обнаруженном дедлоке. В нём фигурировали два конкурирующих UPDATE-запроса на одну таблицу: один обновлял конкретную строку по первичному ключу, второй массово сбрасывал статус зависших записей. Классическая картина взаимной блокировки — каждая транзакция держала то, что нужно другой.
Техническая причина дедлока
Порядок захвата блокировок оказался противоположным:
- Планировщик обновлял строку по первичному ключу, захватывал блокировку на PRIMARY и затем пытался обновить вторичный индекс IX_STATUS_AVAILABLE_AT.
- Метод сброса статусов шёл через вторичный индекс, захватывал его первым и затем пытался получить доступ к PRIMARY той же строки.
Оба процесса держали то, что нужно другому — классический deadlock.
Поиск кода
Следующим шагом стал поиск кода, который генерирует массовый UPDATE. Таблица b_bp_messenger_workflow_resume_message оказалась наследником базового класса из модуля main. Пройдя по цепочке наследования, удалось найти метод requeueStaleMessages() — именно он выполнял проблемный запрос. Выяснилось, что метод вызывается прямо из конструктора класса DbStorage, без advisory lock, тогда как все остальные методы этого класса lock используют. Это и создавало условие для гонки.
Масштаб проблемы
Технический анализ показал лишь симптом. Чтобы понять причину, потребовалось заглянуть в данные. Первый же запрос к таблице очереди обнаружил десятки тысяч сообщений в статусе new — они накапливались каждый день, не успевая обрабатываться. Запрос к таблице состояний бизнес-процессов показал ещё более тревожную картину: сотни тысяч экземпляров в статусе InProgress, часть из которых не менялась с января 2025 года.
Поиск виновных шаблонов
Группировка зависших экземпляров по шаблонам сразу выделила главного виновника — один шаблон давал подавляющее большинство зависших экземпляров. Второй по размеру — робот для лидов.
Чтобы понять можно ли просто удалить эти экземпляры, проверили существование связанных документов. Первая проверка дала ложный результат: джойн шёл по полю DOCUMENT_ID_INT, которое для данного типа сущностей всегда равно нулю. После исправления запроса — парсинга строкового поля DOCUMENT_ID — выяснилось, что все документы живы.
Что на самом деле ждали эти БП
Следующий вопрос: если документы существуют, почему экземпляры не двигаются? Анализ поля STATE_TITLE дал ответ — почти все экземпляры находились в состоянии «Ожидание ответа от приложения». Это REST-активность: робот обратился к внешнему приложению и ждёт колбэк.
Содержимое шаблона показало, что он полностью пуст — никаких активностей. Кто-то удалил REST-активность из шаблона, но сотни тысяч уже запущенных экземпляров остались ждать колбэк, который теперь некому отправить. Эти экземпляры никогда не завершатся.
Связь с обновлением
Анализ дат в таблице очереди показал: до 28 апреля таблица была полностью пустой. Именно в этот день вышло обновление Bitrix v26.550.300 с описанием «исправлена ошибка создания таблицы b_bp_messenger_workflow_resume_message». До этого новый механизм не работал из-за бага, старый планировщик справлялся. После исправления новая очередь заработала и моментально получила весь накопленный за год долг.
Решение
Очистка мёртвых экземпляров
Все зависшие экземпляры шаблонов-виновников ссылались на документы, которые либо были удалены, либо больше не могли получить колбэк от удалённой REST-активности. Экземпляры представляли собой чистый мусор.
Очистка выполнялась батчами по 10 000 записей, чтобы не создавать длинных транзакций и не перегружать базу. Порядок важен:
- Сначала удалялись записи из b_bp_workflow_instance — через JOIN с b_bp_workflow_state.
- Затем удалялись записи из b_bp_workflow_state.
- В последнюю очередь очищались осиротевшие сообщения из очереди — через LEFT JOIN с уже очищенной b_bp_workflow_state.
Такой порядок позволил на третьем шаге быстро найти осиротевшие сообщения: после удаления состояний JOIN сразу возвращал NULL для всех соответствующих записей очереди.
Результат
После удаления мёртвых экземпляров дедлоки прекратились. Очередь перестала захлёбываться. Оставшиеся экземпляры бизнес-процессов — это живые активные роботы с таймерами на будущие даты, они обрабатываются штатно.
Расследование провел сертифицированный эксперт Битрикс, разработчик Инитлаб — Геворг Мкртчян.