Почему THREADS=10 ломает бэкап 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. При выборе конкретной цифры расчёт «ядра минус два» — разумный ориентир, а не жёсткое правило: для сервера с интенсивной ночной нагрузкой (например, если через него ночью же идёт синхронизация с внешними системами) стоит закладывать больший запас.
Настоящая причина: регулярное выражение, которое боится нуля
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».
Когда починили и как узнать, что ваша версия уже безопасна
Исправление вошло в релиз 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 не обновлялся с весны — обновление откладывали, потому что предыдущий апдейт совпал с плотным учебным сезоном и администратор клиента не хотел рисковать лишний раз. Мы сначала подтвердили причину той самой командой 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, а по факту реально сделанной и читаемой копии.
- проверить регулярное выражение: grep -A1 "THREADS.*=~" helper-scripts/backup_and_restore.sh
- если видно [1-9]+ без второй цифры — обновить mailcow либо использовать THREADS от 1 до 9
- если видно [1-9][0-9]? — можно смело использовать любое значение от 1 до 99
- после изменения THREADS в 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.
Источники
- GitHub Issue #5938 — mailcow-dockerized — Точная формулировка ошибки, дата 06.07.2024, обсуждение регулярного выражения и подтверждение мержа DerLinkman 10.07.2024. https://github.com/mailcow/mailcow-dockerized/issues/5938
- GitHub Pull Request #5634 — mailcow-dockerized — Точный diff regex-проверки THREADS: ^[1-9]+$ заменено на ^[1-9][0-9]?$ в helper-scripts/backup_and_restore.sh. https://github.com/mailcow/mailcow-dockerized/pull/5634
- mailcow docs — Backup, Multithreading — Синтаксис THREADS=14 .../backup_and_restore.sh backup all и рекомендация «core count -2». https://docs.mailcow.email/backup_restore/b_n_r-backup/
- mailcow blog — Mooctober Update 2022 (2022-10) — Релиз, в котором добавлена многопоточность для backup/restore-скрипта. https://mailcow.email/posts/2022/release-2022-10/
- mailcow blog — категория Updates — Подтверждение даты релиза 2024-06b (12.07.2024) с исправлением регулярного выражения. https://mailcow.email/categories/updates/



