Карантин mailcow: Deadlock 1213 при обучении на спаме
АйТи Фреш
Linux, Docker и DevOps

Карантин mailcow выдаёт Deadlock 1213 при обучении на спаме, хотя healthcheck зелёный

Автор: , директор ООО «АйТи-Фреш» · · ~15 мин чтения
Иллюстрация: две параллельные операции в карантине mailcow сталкиваются за одни и те же записи, вызывая deadlock, хотя сервис остаётся здоровым
Deadlock — это конфликт двух транзакций за одну блокировку, а не отказ сервиса

Если в карантине mailcow массовая операция «Toggle all → Learn as spam and delete» иногда обрывается ошибкой PDOException SQLSTATE[40001]: Deadlock found, а watchdog в это же время показывает MySQL/MariaDB здоровым на 100 %, — это не повод паниковать и разворачивать бэкап. Разбираю, что означает код 1213 в MariaDB, почему healthcheck и deadlock не противоречат друг другу, разбираю известный баг-репорт по этой ошибке и показываю, как посмотреть подробности конкретной взаимоблокировки через InnoDB.

Симптом: массовая очистка карантина падает с Deadlock, а watchdog зелёный

Ситуация чаще всего повторяется так: администратор открывает карантин в панели корпоративной почты на mailcow, выделяет через «Toggle all» разом десятки или сотни писем, помеченных как спам, и нажимает «Learn as spam and delete», чтобы одновременно обучить Rspamd и очистить карантин. Операция иногда падает с ошибкой вида PDOException: SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock, при этом часть писем успевает удалиться, а часть — нет, и непонятно, что теперь в каком состоянии.

Первая реакция — проверить состояние базы через watchdog: в mailcow это отдельный контейнер, который постоянно следит за здоровьем сервисов и пишет статус в лог и в UI. И тут появляется второй, более пугающий симптом — watchdog в этот же момент показывает MySQL/MariaDB полностью здоровым, без единого предупреждения. Возникает ощущение, что ошибка какая-то «из ниоткуда»: мониторинг молчит, а операция в интерфейсе падает.

Почему healthcheck зелёный, а Deadlock всё равно происходит

Формальное противоречие снимается, если понимать, что именно проверяет watchdog и что означает код 1213. Watchdog в mailcow проверяет доступность и общую работоспособность сервиса — отвечает ли MariaDB на соединение, принимает ли базовые запросы, не упал ли процесс. Deadlock — это не поломка сервиса и не недоступность базы, а штатный, предусмотренный механизм InnoDB: когда две транзакции одновременно пытаются заблокировать одни и те же строки в разном порядке и упираются друг в друга, InnoDB детектирует взаимную блокировку и принудительно откатывает одну из транзакций с ошибкой 1213, чтобы вторая могла продолжить работу. Сервис при этом полностью жив и здоров — просто одна из двух параллельных операций не смогла завершиться с первой попытки. Это принципиально другая ситуация, чем повреждение InnoDB после аварии сервера — там данные действительно требуют восстановления, а не просто повторной попытки той же операции.

Применительно к карантину mailcow это означает: при массовом «Toggle all → Learn as spam and delete» на десятках писем PHP-код панели (functions.quarantine.inc.php) удаляет записи из таблицы quarantine пачкой, а с этой же таблицей в фоне работают другие участники: скрипт уведомлений о карантине quarantine_notify.py в контейнере dovecot-mailcow, который по расписанию читает таблицу, и очистка старых записей сверх лимита на ящик. В обсуждении issue #6361 разработчик mailcow прямо предположил, что таблицу блокирует именно запрос скрипта уведомлений, а участники нашли медленные запросы: выборку по SHA2(CONCAT(id, qid), 256), которая пересчитывает хэш для всех строк, и DELETE … NOT IN (…) при очистке. Чем больше записей в карантине, тем дольше эти транзакции держат блокировки и тем выше шанс, что массовое удаление из панели упрётся в них, — тогда MariaDB честно откатывает одну из сторон. Это ожидаемое поведение реляционной базы под конкурентной нагрузкой, а не признак деградации сервиса — именно поэтому watchdog, который проверяет доступность, а не факт единичного отката транзакции, остаётся зелёным. Похожий механизм есть и у серийных сбоев транзакций в PostgreSQL — там ошибка называется иначе, но суть та же: конкурирующие транзакции, а не поломка базы.

Схема, объясняющая разницу между проверкой здоровья MySQL через watchdog и штатным механизмом deadlock в InnoDB
Watchdog и deadlock проверяют разные вещи — зелёный статус не противоречит ошибке 1213

Известный баг-репорт: issue #6361 на mailcow 2025-02

Эта ошибка задокументирована в официальном трекере mailcow-dockerized как issue #6361: пользователь на версии mailcow 2025-02 получал ровно SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock именно при действии «Toggle all → Learn as spam and delete» в карантине. Ошибка воспроизводилась в файле /web/inc/functions.quarantine.inc.php — это тот самый код, который обрабатывает массовое действие над отмеченными письмами. Окружение автора репорта: Debian Bullseye, 64 ГБ RAM, 12 ядер, Docker 28.0.1, docker-compose 2.33.1, без ограничений SELinux/AppArmor — то есть не редкое, экзотическое окружение, а достаточно типичный сервер.

Issue помечен метками bug и unconfirmed, а в октябре 2025 года закрыт автоматически за неактивностью — без исправления в коде. Разработчик не смог воспроизвести ошибку у себя, зато в обсуждении нашлась закономерность: у всех пострадавших большие таблицы карантина — 20–30 тысяч записей на сервер. Участники сообщали, что ошибка ловится примерно в 3–5 % операций и не ушла после обновления до 2025-03b, а увеличение innodb_buffer_pool_size со штатных 24M (так он задан в data/conf/mysql/my.cnf mailcow) до 4G сделало её редкой, но не убрало совсем. Предложенный сообществом PR #6556 с отдельной колонкой qhash вместо пересчёта SHA2 закрыт без слияния. Автор репорта отметил важную практическую деталь: ошибка не блокирует операцию полностью — повторный запуск того же самого действия «Learn as spam and delete» в одну-две попытки обычно завершает удаление успешно. Это соответствует природе deadlock: откатывается конкретная транзакция в конкретный момент конкуренции за блокировку, а не сама возможность выполнить операцию.

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

Совпадение по времени с обновлением MariaDB 10.5 → 10.11

Релиз mailcow 2025-02, на котором зафиксирован баг-репорт, — тот же релиз, в котором вышло крупное обновление базы данных: MariaDB обновилась с 10.5 до 10.11 (LTS-ветка). В официальном анонсе релиза это обновление выделено отдельно и с двумя жёсткими предупреждениями: вручную менять версию MariaDB в docker-compose.yml нельзя — это может привести к повреждению базы, если код не адаптирован под нужную версию, а откат обратно на 10.5 после миграции на 10.11 в принципе невозможен. Формально issue #6361 напрямую не связывает Deadlock с этим обновлением, но совпадение версии релиза наводит на мысль, что стоит проверить именно это направление, если проблема появилась не сразу, а после апдейта.

Практический вывод для администратора: если Deadlock 1213 в карантине появился сразу после планового обновления mailcow, стоит зафиксировать в заметках, что это могло совпасть с переходом на MariaDB 10.11, и не пытаться откатить версию базы вручную — согласно предупреждению самого проекта, обратного пути после миграции нет, а ручное редактирование docker-compose.yml без понимания, что именно поменялось в схеме и движке, рискует данными куда сильнее, чем единичный deadlock при массовой операции.

Между мажорными версиями движка InnoDB нередко меняются внутренние детали планирования блокировок, даже если внешне таблицы и код приложения не изменились ни на строку, — это достаточно распространённая причина, почему поведение под конкурентной нагрузкой «вдруг» становится заметнее именно после апдейта СУБД, а не после изменений в самом mailcow. Я не стал бы тратить время на доказательство точной причинно-следственной связи между конкретной строчкой в changelog MariaDB и конкретным deadlock в карантине — для практической работы администратора достаточно знать сам факт совпадения по времени и относиться к нему как к поводу для наблюдения, а не как к точно установленной причине.

Как посмотреть детали конкретной взаимоблокировки через InnoDB

Если хочется не просто принять факт deadlock, а понять, какие именно транзакции столкнулись, в MariaDB есть штатная диагностика — команда SHOW ENGINE INNODB STATUS, выполненная сразу после ошибки (её данные не хранятся долго, старая информация о взаимоблокировке перезаписывается новой). Подключиться к базе mailcow из каталога mailcow-dockerized можно так (для SHOW ENGINE INNODB STATUS и SET GLOBAL нужны права, которых у пользователя DBUSER может не быть, поэтому я захожу под root с паролем DBROOT из mailcow.conf):

source mailcow.conf
docker compose exec mysql-mailcow mysql -uroot -p${DBROOT} ${DBNAME}

а внутри выполнить:

SHOW ENGINE INNODB STATUS\G

В выводе нужен раздел LATEST DETECTED DEADLOCK — согласно документации MariaDB, он показывается только если взаимоблокировка действительно была зафиксирована, и содержит участвовавшие транзакции, выполнявшиеся операторы, какие блокировки были удержаны и какие запрошены каждой стороной, а также какая именно транзакция была откачена для разрешения конфликта.

Проблема в том, что раздел LATEST DETECTED DEADLOCK хранит только самую последнюю взаимоблокировку — если между падением операции в панели и вашим подключением к базе произошёл ещё один deadlock (в том числе не связанный с карантином), нужные детали будут уже перезаписаны. Для системной диагностики, а не разовой проверки, в MariaDB есть системная переменная innodb_print_all_deadlocks — по умолчанию она выключена (0/OFF), а включённая заставляет сервер писать полную информацию о каждой обнаруженной взаимоблокировке в лог ошибок MariaDB, а не только хранить последнюю в памяти. Переменная динамическая, так что включить её можно без перезапуска: SET GLOBAL innodb_print_all_deadlocks=1; — и дальше искать deadlock не через SHOW ENGINE INNODB STATUS вручную после каждой жалобы, а в логе контейнера docker compose logs mysql-mailcow. Учтите две вещи: после перезапуска контейнера значение вернётся к 0 (для постоянного включения строку innodb_print_all_deadlocks = 1 добавляют в data/conf/mysql/my.cnf), а в штатном my.cnf mailcow стоит log-warnings = 0 — после включения сразу проверьте, что записи о взаимоблокировках действительно появляются в логе в вашей версии.

Пошаговая схема диагностики ошибки Deadlock 1213 в карантине mailcow через InnoDB status и логи MariaDB
От единичной ошибки до системного наблюдения — два разных уровня диагностики одного и того же deadlock

Кейс «Дебютный код»: восемь рабочих мест, но карантин рос быстрее, чем чистился

Клиент — компьютерная школа «Дебютный код», всего 8 рабочих мест, у которой mailcow развёрнут с нуля силами приглашённого администратора ещё до нашего подключения к проекту, но с активной рекламной рассылкой для родителей учеников, из-за которой в карантин ежедневно попадало по 30–60 писем, помеченных Rspamd как вероятный спам. Предыдущий администратор поднял лимит хранения карантина на ящик до максимума «чтобы ничего не терялось», и к моменту нашего подключения в таблице quarantine лежало около 9 тысяч записей — при штатных 24M буферного пула InnoDB. Администратор школы — по совместительству один из преподавателей — раз в несколько дней заходил и чистил карантин через «Toggle all → Learn as spam and delete» одним махом на все накопившиеся письма. Именно на этих массовых операциях, а не на единичном удалении одного письма, стабильно раз в одну-две недели появлялась ошибка Deadlock 1213, и часть писем оставалась висеть в карантине, вызывая у администратора устойчивое ощущение, что «карантин иногда глючит».

Разбор занял немного времени, потому что ситуация точно совпадала с описанием issue #6361: версия mailcow у клиента была обновлена как раз до 2025-02 незадолго до появления жалоб. Проверили здоровье сервиса через watchdog — зелёный, как и ожидалось; выполнили SHOW ENGINE INNODB STATUS сразу после воспроизведения ошибки и увидели в LATEST DETECTED DEADLOCK две конкурирующие транзакции на таблице карантина с пересекающимися блокировками строк — то есть подтвердили, что это классический конкурентный deadlock, а не повреждение данных. Решение было практическим, а не «патчем»: администратору дали инструкцию при ошибке просто повторить «Learn as spam and delete» ещё раз — по опыту issue и по собственной проверке, повтор почти всегда проходит без ошибки — и дополнительно предложили чистить карантин меньшими порциями по 10–15 писем, а не одним махом на полсотни, чтобы снизить саму вероятность конкуренции за блокировки. Вторым шагом вернули разумный лимит хранения карантина на ящик — таблица за две недели ужалась до полутора тысяч записей — и подняли innodb_buffer_pool_size в data/conf/mysql/my.cnf с 24M до 512M: памяти на сервере хватало с запасом. За следующие два месяца ошибка всплыла один раз и прошла со второго нажатия.

Deadlock 1213 в карантине mailcow — это штатный механизм InnoDB, а не признак поломки: сервис здоров, а откатывается только одна из двух конкурирующих транзакций. Решение почти всегда — повторить операцию, а не восстанавливать базу.

Чек-лист: что делать при Deadlock 1213 в карантине mailcow

Первое — не паниковать и не запускать восстановление из бэкапа: единичный deadlock не повреждает базу, это встроенный механизм разрешения конкуренции за блокировки в InnoDB. Второе — проверить статус MySQL/MariaDB через watchdog: если он зелёный, это ожидаемо и не противоречит факту deadlock, сервис жив, просто одна транзакция была откачена. Третье — повторить то же самое действие «Learn as spam and delete» ещё раз: по опыту issue #6361 и практике на клиентских инсталляциях, одна-две повторные попытки почти всегда завершаются успешно.

Если ошибка повторяется регулярно на одном и том же типе массовой операции, стоит подтвердить диагноз через SHOW ENGINE INNODB STATUS сразу после воспроизведения — найти раздел LATEST DETECTED DEADLOCK и убедиться, что участвуют именно ожидаемые таблицы карантина, а не что-то постороннее. Для систематического наблюдения имеет смысл временно включить innodb_print_all_deadlocks=1 и смотреть лог docker compose logs mysql-mailcow за период жалоб. И последнее — если Deadlock появился сразу после обновления mailcow, зафиксировать это в заметках на будущее: репорт по этой проблеме привязан к релизу 2025-02, который одновременно поднял MariaDB с 10.5 до 10.11, и это совпадение стоит иметь в виду при похожих жалобах после апдейта.

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

Что означает ошибка SQLSTATE[40001] Deadlock found в карантине mailcow?

Это штатный механизм InnoDB: когда две транзакции одновременно блокируют одни и те же строки в разном порядке, MariaDB детектирует взаимоблокировку и откатывает одну из транзакций с кодом 1213, чтобы вторая могла продолжить работу. Данные при этом не повреждаются.

Почему watchdog показывает MySQL здоровым, если возникла ошибка Deadlock?

Watchdog проверяет доступность и общую работоспособность сервиса, а не факт единичного отката транзакции. Deadlock — не сбой сервиса, а нормальная реакция на конкуренцию за блокировки, поэтому healthcheck остаётся зелёным.

Это известный баг mailcow?

Есть баг-репорт issue #6361 в трекере mailcow-dockerized на версии 2025-02, с тем же симптомом при действии Toggle all → Learn as spam and delete. Issue помечен как unconfirmed и в октябре 2025 года закрыт за неактивностью без исправления; в обсуждении ошибку связывают с большими таблицами карантина (20–30 тысяч записей).

Что делать сразу после ошибки Deadlock 1213 в карантине?

Повторить то же действие ещё раз. По опыту issue #6361 и практике, одна-две повторные попытки массового Learn as spam and delete почти всегда завершаются успешно — восстанавливать базу из бэкапа не требуется.

Как посмотреть подробности конкретной взаимоблокировки в MariaDB?

Сразу после ошибки выполнить SHOW ENGINE INNODB STATUS\G и найти раздел LATEST DETECTED DEADLOCK — там перечислены участвовавшие транзакции, выполнявшиеся операторы, удержанные и запрошенные блокировки. Раздел хранит только самую последнюю взаимоблокировку, старая информация перезаписывается.

Связан ли Deadlock 1213 с обновлением MariaDB 10.5 → 10.11 в релизе 2025-02?

Прямая связь не подтверждена: автор заметил ошибку после этого обновления, но разработчик не смог её воспроизвести, а участники обсуждения связали её скорее с размером таблицы карантина. Откатывать версию базы вручную нельзя — согласно анонсу mailcow, после миграции на 10.11 обратного пути на 10.5 нет.

Столкнулись с похожей задачей? Обращайтесь — решим

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

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

📞 +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи