mailcow THREADS=10 ломает бэкап: чиню за минуту
АйТи Фреш
Linux, Docker и DevOps

Почему THREADS=10 ломает бэкап mailcow и как правильно выбрать число потоков

Автор: , директор ООО «АйТи-Фреш» · · ~13 мин чтения
Число потоков с нулём застревает в проверке скрипта бэкапа mailcow — метафора ошибки THREADS=10
Один символ «0» в числе потоков — и ночной бэкап mailcow не стартует вовсе.

Хочешь ускорить ночной бэкап mailcow, ставишь THREADS=10 — и скрипт вместо запуска пишет «Thread input is not a number!» и выходит с ошибкой. Дело не в количестве ядер и не в правах, а в регулярном выражении внутри backup_and_restore.sh, которое до середины 2024 года не умело работать с двузначными числами. Показываю, где баг, когда его починили и как выбирать THREADS осмысленно, а не наугад.

Кейс: «Певческая гавань» теряет ночной бэкап из-за круглого числа

Школа вокала «Певческая гавань» — 49 рабочих мест, mailcow держит переписку с учениками, договоры на аренду залов и вложения с видеозаписями концертов, которые быстро раздувают базу писем. Мы обслуживаем их корпоративную почту, и в какой-то момент штатный однопоточный бэкап стал занимать по ночам заметно больше времени, чем окно до утренней синхронизации с бухгалтерией. Администратор клиента решил ускорить процесс очевидным способом — задать многопоточность через переменную THREADS, которую как раз для этого добавили в mailcow.

Сервер под mailcow — 12 ядер, поэтому выбор пал на круглое и вроде бы безопасное число: THREADS=10, с запасом в два ядра под саму работу mailcow, как и советует документация. Но при следующем ночном запуске в cron-логе вместо отчёта о бэкапе оказалась одна строка: «Thread input is not a number!» — и скрипт завершился с кодом ошибки, ничего не скопировав. Ночь ушла без резервной копии, а утром администратор клиента был уверен, что где-то опечатался в самой цифре.

На деле опечатки не было — ошибка вообще не зависела от того, что именно вписано в THREADS в смысле здравого смысла. Она зависела от одной детали регулярного выражения в самом скрипте mailcow, которую я и разбираю ниже вместе с тем, как правильно посчитать число потоков под конкретное железо.

Откуда взялась многопоточность в бэкапе и что она на самом деле делает

Поддержка многопоточного бэкапа и восстановления появилась в релизе mailcow 2022-10, который в блоге проекта называется «Mooctober Update 2022» — помимо переработки переводов интерфейса, в нём обновили helper-скрипт backup_and_restore.sh (PR #4806), добавив многопоточное сжатие архивов при бэкапе и распаковку при восстановлении. Кстати, та самая проверка ^[1-9]+$ появилась в скрипте вместе с THREADS ещё тогда, в 2022-10, — баг прожил почти два года. Официальная документация формулирует использование предельно просто: переменная THREADS указывается перед вызовом скрипта, например THREADS=14 /opt/mailcow-dockerized/helper-scripts/backup_and_restore.sh backup all.

Рекомендация по числу потоков в документации тоже конкретная: «Please keep your core count -2 to leave enough CPU power for mailcow itself» — то есть если в системе 16 ядер, разумное значение THREADS=14, а не все 16, потому что сам mailcow (Postfix, Dovecot, Rspamd, ClamAV) должен продолжать обрабатывать почту, пока идёт бэкап. Важно понимать, что именно распараллеливает THREADS: это не число одновременно копируемых томов, а число потоков компрессора. Скрипт по-прежнему архивирует тома (vmail, crypt, redis, rspamd, postfix и остальные) по очереди через tar, а THREADS передаётся упаковщику — в версиях 2024 года это pigz -p ${THREADS}, в актуальных zstd -T${THREADS}; при восстановлении — pigz -d -p или zstd -d -T соответственно.

Ничего экзотического: чем больше потоков, тем быстрее идёт сжатие на многоядерной машине, но тем выше пиковая нагрузка на CPU в момент бэкапа, а чтение с диска остаётся одним потоком tar. При выборе конкретной цифры расчёт «ядра минус два» — разумный ориентир, а не жёсткое правило: для сервера с интенсивной ночной нагрузкой (например, если через него ночью же идёт синхронизация с внешними системами) стоит закладывать больший запас.

Сравнение регулярного выражения проверки THREADS в mailcow до и после исправления бага с нулём
Старое выражение не пропускало ни одной цифры «0» в числе потоков — отсюда и ошибка на 10, 20, 100.

Настоящая причина: регулярное выражение, которое боится нуля

6 июля 2024 года в репозитории mailcow-dockerized появился issue #5938 с точной формулировкой: «backup/restore if you use THREADS=10 you get Thread input is not a number!». Автор поймал ошибку на двух разных машинах и в разных шеллах (backup в bash, restore в fish) и сам заметил, что оболочка, похоже, ни при чём, — ошибка воспроизводилась одинаково.

Причину нашли за два дня в комментариях к issue: в скрипте backup_and_restore.sh проверка введённого значения потоков использовала регулярное выражение ^[1-9]+$ — «одна или несколько цифр от 1 до 9». На первый взгляд рабочий шаблон, но у него есть слепая зона: он проверяет каждый символ строки на принадлежность набору 1–9, а цифра 0 в этот набор не входит. Число «10» состоит из символов «1» и «0» — и вот «0» с этим выражением не совпадает никогда, в любой позиции. Поэтому НЕ проходили проверку не только 10, но и 20, 30, 100, 200 — любое число с нулём в записи. Один из участников обсуждения даже предложил не усложнять: «I think the [[:digit]] or just 0-9 is enough».

Разработчик mrclschstr в комментарии от 8 июля 2024 года указал на точное место в коде и предложил рабочую замену: ^([1-9][0-9]?)$ — первая цифра от 1 до 9, за ней опционально ещё одна цифра от 0 до 9, то есть диапазон от 1 до 99 без ведущего нуля. Самое показательное — исправление к тому моменту уже существовало: в тот же день другой участник указал на pull request #5634 «Enhanced regular expression for THREADS parameter» от torzech, открытый ещё 9 января 2024 года и полгода провисевший без внимания. 9 июля его влили в ветку staging, а 10 июля 2024 года мейнтейнер DerLinkman отписался в issue: «Merged and will be part of next update».

Точный diff из PR #5634: было `[[ "${THREADS}« =~ ^[1-9]+$ ]]`, стало `[[ »${THREADS}" =~ ^[1-9][0-9]?$ ]]` — изменение затронуло обе ветки проверки (ошибка и подтверждение) в файле helper-scripts/backup_and_restore.sh.
Дерево решений для диагностики ошибки Thread input is not a number в бэкапе mailcow
Три вопроса подряд — и понятно, баг это или что-то другое в конкретном cron-задании.

Когда починили и как узнать, что ваша версия уже безопасна

Исправление вошло в релиз 2024-06b, который вышел 12 июля 2024 года — то есть спустя три дня после мержа PR #5634, в виде отдельного bugfix-релиза поверх «Moone Update 2024» (в журнале релиза: «Improved regular expression in the backup/restore script, it should now support numbers like 10, 20, etc.»). Если ваш mailcow обновлялся хотя бы один раз после середины июля 2024-го, регулярное выражение в вашем backup_and_restore.sh уже исправлено, и THREADS=10 отработает корректно. Проверить это быстрее всего прямой командой на сервере.

Самый надёжный способ убедиться — посмотреть в сам файл скрипта, а не гадать по номеру версии: grep -A1 'THREADS.*=~' /opt/mailcow-dockerized/helper-scripts/backup_and_restore.sh. Если в выводе видно [1-9][0-9]?, значит патч применён и любое двузначное число потоков пройдёт проверку. Если видно старое [1-9]+ без второй опциональной цифры — перед обновлением всей системы стоит хотя бы обновить сам helper-scripts каталог, либо просто не указывать в THREADS числа с нулём до апдейта. Тот же принцип я применяю к любому серверу под моим сопровождением, когда выстраиваю стратегию резервного копирования Linux-серверов: не полагаться на номер версии из памяти, а проверять поведение конкретного скрипта на конкретной машине.

Отдельно стоит иметь в виду: если у вас 8 ядер и по правилу «минус два» получается THREADS=6 — однозначное число, баг вас никогда не касался, ошибка проявляется только начиная с 10 и выше. Это объясняет, почему далеко не все администраторы mailcow вообще сталкивались с этой проблемой: она бьёт по серверам с 12 и более ядрами, где расчёт числа потоков естественным образом выходит за пределы одной цифры.

Цифры кейса ускорения бэкапа mailcow в школе Певческая гавань после исправления THREADS
Временный обход через однозначное число потоков сократил бэкап почти на треть — и ни одной ошибки при этом.

Как чинили у «Певческой гавани» и что взял в регламент

На сервере клиента mailcow не обновлялся с весны — обновление откладывали, потому что предыдущий апдейт совпал с плотным учебным сезоном и администратор клиента не хотел рисковать лишний раз. Мы сначала подтвердили причину той самой командой grep по скрипту — регулярное выражение оказалось старым, без исправления из PR #5634. Дальше — штатное обновление mailcow до актуальной на тот момент версии (заведомо позже 2024-06b), после чего THREADS=10 отработал без единой ошибки и дал ещё несколько минут выигрыша относительно временного THREADS=8 — дальше упирались уже в диск, а не в сжатие.

Пока сервер оставался на старой версии, мы временно использовали однозначное число потоков — THREADS=8 при 12 ядрах, что тоже укладывалось в правило «минус два с запасом» и обходило баг просто по формату записи числа. Это рабочий обходной путь для тех, кто по каким-то причинам не готов обновляться немедленно: выбирайте однозначное число потоков (1–9), пока не убедитесь через grep, что регулярное выражение в вашем скрипте уже исправлено.

Отдельно проверили объём и время самого архива до и после: на однопоточном режиме полный бэкап (базы данных, почтовые ящики, конфиги, vmail-том) занимал около полутора часов и упирался в утреннее окно бухгалтерской синхронизации. С THREADS=8 время сократилось примерно до 50 минут — не двукратное ускорение из-за диска, который у клиента сетевой (не локальный NVMe), но разница ощутимая и достаточная, чтобы бэкап гарантированно укладывался в ночное окно с запасом.

В регламент обслуживания клиентских mailcow-серверов я добавил простое правило: после любого включения или изменения THREADS в cron-задаче бэкапа — обязательный тестовый прогон командой вручную в интерактивном режиме, а не ожидание первого ночного запуска по расписанию. Это тот случай, когда пятиминутная проверка руками экономит целую ночь без резервной копии, а заодно и нервы администратора, который утром находит в логе только сухую строку об ошибке без объяснения причины. Здесь та же логика, что и в регламенте эксплуатации mailcow, который мы применяем на всех клиентских серверах: бэкап проверяется не по факту его наличия в cron, а по факту реально сделанной и читаемой копии.

Как выбирать число потоков осознанно, а не по принципу «побольше»

Формула «ядра минус два» из документации — стартовая точка, а не догма. На сервере, где помимо mailcow ночью работают другие тяжёлые задачи (репликация базы, синхронизация с 1С, антивирусное сканирование), я закладываю больший запас — минус четыре или минус треть от общего числа ядер, в зависимости от того, что ещё крутится параллельно. Смысл в том же, что и с любой другой многопоточной операцией: THREADS ускоряет только сжатие, поэтому выигрыш есть, пока узкое место — CPU. Как только упёрлись в чтение тома или запись на медленное сетевое хранилище, дополнительные потоки компрессора ничего не дают, а только отбирают процессор у Rspamd и ClamAV.

Второй практический момент — восстановление использует ту же переменную THREADS и тот же механизм проверки, что и бэкап, включая тот же баг с регулярным выражением на старых версиях. Если вы тестируете скорость бэкапа с одним значением THREADS, а восстанавливать в аварийной ситуации планируете с другим (например, на новом сервере с иным числом ядер) — стоит хотя бы раз прогнать restore тестово, а не полагаться, что раз бэкап отработал, то и restore пройдёт так же гладко именно в момент, когда это критично. Тот же принцип я закладываю в работу с любыми серверами, не только mailcow: бэкап есть, а данных нет — типичная ситуация, когда резервная копия формально создаётся годами, но никто ни разу не проверял, что из неё реально можно восстановиться в разумное время.

Третий момент, который часто упускают именно на mailcow — многопоточность ускоряет копирование данных, но не отменяет необходимости следить за объёмом самого бэкапа. Если THREADS наконец заработал и бэкап стал укладываться в окно, это не повод перестать проверять место на диске под архивы: растущие вложения (видео, макеты, сканы) рано или поздно упрутся в лимит хранилища бэкапов быстрее, чем в лимит по времени выполнения скрипта.

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

На каких значениях THREADS воспроизводится баг с регулярным выражением?

На любом числе от 10 и выше, в записи которого встречается цифра 0 в любой позиции: 10, 20, 30, 100, 200. Однозначные числа от 1 до 9 и двузначные без нуля (11, 23, 99) баг не затрагивают, потому что старое выражение [1-9]+ их пропускало.

Как узнать номер версии mailcow, установленной на сервере?

Номер версии виден внизу веб-интерфейса администратора, а на сервере — в переменной $MAILCOW_GIT_VERSION файла data/web/inc/app_info.inc.php (её записывает update.sh). Для проверки именно этого бага надёжнее grep по самому скрипту backup_and_restore.sh.

Что если версия свежая, а ошибка всё равно повторяется?

Значит проблема не в THREADS вообще, а в чём-то другом — например, само значение переменной содержит пробел или невидимый символ из-за копипасты в cron. Проверьте прямым выводом echo "[${THREADS}]" перед вызовом скрипта.

Можно ли указать THREADS=0, чтобы отключить многопоточность полностью?

Нет, ни старое, ни новое регулярное выражение не пропускает 0 — минимальное валидное значение 1, и это же значение используется по умолчанию, если переменную не задавать вовсе (`THREADS=$(echo ${THREADS:-1})` в самом скрипте).

Влияет ли THREADS на восстановление данных так же, как на бэкап?

Да, переменная и проверка общие для обоих режимов скрипта backup_and_restore.sh — restore all с THREADS=10 на старой версии упадёт с той же ошибкой Thread input is not a number, что и backup.

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

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

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

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

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

Источники

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