Обучение байесовского фильтра rspamd: ручное обучение, IMAPSieve, autolearn и общий корпус на несколько нод
Байес в rspamd — это не галочка в конфиге, а живой корпус в Redis, который умирает от дисбаланса. Разбираем три режима обучения, объясняем, почему autolearn — не ИИ, и показываем, как заставить несколько mail-нод делить один статистический корпус вместо трёх рассинхронизированных.
EvilMail Team21 июля 2026 г.13 мин чтения
Проблема: почему BAYES_SPAM молчит или врёт
Симптом всегда один и тот же. Открываешь rspamc stat, видишь Statfile: BAYES_SPAM learned 40 — и удивляешься, почему очевидный спам проходит с чистым score, а иногда наоборот BAYES_SPAM прилетает на нормальную рассылку от банка. Символ либо не срабатывает вообще, либо бьёт по ham.
Диагноз почти всегда сводится к двум вещам. Первое: корпус пуст или несбалансирован — min_learns не набран, и rspamd сознательно отказывается использовать статистику, которой не доверяет. Второе, и это классика для тех, кто вырос из одной ноды: на каждом сервере свой локальный Redis, и статистика размазана по трём инстансам, каждый из которых видел треть писем.
Отсюда тезис, который стоит усвоить раз и навсегда: байес в rspamd — это состояние в Redis, а не строчка в конфиге. Им надо управлять как данными — бэкапить, балансировать, централизовать и иногда сбрасывать. Конфиг лишь описывает, куда эти данные складывать и когда им доверять.
Как устроен байес: токены, Redis, siphash
Обучение байеса rspamd: ручное обучение, autolearn и общий корпус на несколько серверов — EvilMail Blog
Когда письмо попадает в classifier, rspamd режет его на токены — это не только слова из тела, но и мета-токены: пары слов, элементы заголовков, признаки структуры. Каждый токен прогоняется через siphash, и по полученному хешу в Redis инкрементируются два счётчика — сколько раз этот токен встречался в спаме и сколько в ham. Оценка нового письма — это перемножение вероятностей по его токенам. Никакой магии, чистая статистика Байеса.
Важное следствие: корпус непортируем в открытом виде. Токены хешированы siphash, поэтому нельзя взять текстовый дамп «слов спама» и перенести. Перенос корпуса = перенос keyspace Redis, и точка.
Backend — только Redis. sqlite-бэкенд формально ещё существует, но он мёртв: не масштабируется, не шарится между нодами, тихо деградирует. Если видите в чужом конфиге backend = "sqlite3" — это легаси, которое нужно мигрировать.
Ещё одно принципиальное разделение — глобальный классификатор против per-user. При users_enabled = true rspamd держит отдельный корпус на каждого получателя. Для корпоративной почты с активными пользователями это может иметь смысл. Для shared-хостинга, а тем более для temp-mail, как у нас на evilmail.pro, это катастрофа: тысячи одноразовых адресов, каждый со своим пустым корпусом, который никогда не наберёт min_learns. Здесь нужен один глобальный корпус — users_enabled = false.
И не путайте байес с fuzzy. Fuzzy-storage хранит нечёткие хеши целых писем или их частей (rspamc fuzzy_add) и ловит массовые рассылки по совпадению. Байес — это статистика отдельных токенов. Разные подсистемы, разные Redis-ключи, разные задачи. Их часто настраивают вместе, но путать их в голове — прямой путь к неправильной диагностике.
Базовый конфиг classifier-bayes.conf
Начинаем с рабочего /etc/rspamd/local.d/classifier-bayes.conf. Автообучение на старте сознательно выключено — сначала наберём чистый корпус руками, потом решим про autolearn.
ini
classifier "bayes" {
tokenizer {
name = "osb";
}
backend = "redis";
servers = "127.0.0.1:6379";
new_schema = true;
expire = 8640000; # 100 дней жизни токена
min_tokens = 11; # письма короче — не учим
min_learns = 200; # порог доверия к корпусу
per_language = true;
users_enabled = false; # глобальный корпус — критично для shared/temp-mail
autolearn = false; # включим осознанно позже
statfile {
symbol = "BAYES_HAM";
spam = false;
}
statfile {
symbol = "BAYES_SPAM";
spam = true;
}
cache {
backend = "redis";
}
}
Контроллеру нужен пароль, иначе rspamc learn_* по сети не пройдёт. В /etc/rspamd/local.d/worker-controller.inc:
Никогда не кладите пароль в открытом виде — сгенерируйте хеш командой rspamadm pw. После правок обязательно rspamadm configtest, и только потом systemctl reload rspamd. Configtest ловит большинство опечаток до того, как они уронят воркер.
Ручное обучение: bulk-прогон существующего корпуса
Самый быстрый способ поднять байес с нуля — прогнать уже отсортированную почту. У вас наверняка есть Maildir'ы, где пользователи месяцами таскали спам в .Junk, а нормальную почту держали в инбоксе. Это готовый размеченный датасет.
bash
CTRL="127.0.0.1:11334"
PW="ваш_controller_password"
# SPAM: всё, что лежит в папках Junk
find /var/mail/vhosts/*/*/Maildir/.Junk/cur -type f -print0 \
| xargs -0 -n 50 rspamc -h "$CTRL" -P "$PW" learn_spam
# HAM: инбоксы (осторожно — там не должно быть спама!)
find /var/mail/vhosts/*/*/Maildir/cur -type f -print0 \
| xargs -0 -n 50 rspamc -h "$CTRL" -P "$PW" learn_ham
Ключевое правило, которое ломает больше корпусов, чем любая ошибка конфига — баланс. Гоните примерно равное число ham и spam. Если скормить 10 000 ham и 300 spam, классификатор решит, что почти всё на свете — ham, и BAYES_SPAM замолчит. При автообучении за перекос отвечает check_balance = true — rspamd откажется доучивать вырвавшийся вперёд класс, но при ручном rspamc learn_* баланс целиком на вашей совести.
Следите за прогрессом через rspamc stat. Пока по обоим классам не набрано min_learns (у нас 200), BAYES не будет влиять на итоговый score вообще — это защита от преждевременных выводов на трёх письмах. Практический ориентир: сотни learn каждого класса, чтобы фильтр ожил, и комфортно — от 1000/1000. Дальше растёт точность, но с убывающей отдачей.
Обратная связь пользователя через IMAPSieve
Bulk-прогон даёт стартовый корпус, но он стухнет: спам мутирует каждую неделю. Самый ценный источник свежих learn — живой сигнал от человека, который сам перетащил письмо в Junk или вытащил обратно. Это ловится через Dovecot imap_sieve.
В /etc/dovecot/conf.d/90-sieve.conf (плагин imap_sieve):
plugin {
sieve_plugins = sieve_imapsieve sieve_extprograms
sieve_pipe_bin_dir = /etc/dovecot/sieve/bin
# письмо скопировали В Junk → это спам
imapsieve_mailbox1_name = Junk
imapsieve_mailbox1_causes = COPY
imapsieve_mailbox1_before = file:/etc/dovecot/sieve/learn-spam.sieve
# письмо вытащили ИЗ Junk → это ham
imapsieve_mailbox2_name = *
imapsieve_mailbox2_from = Junk
imapsieve_mailbox2_causes = COPY
imapsieve_mailbox2_before = file:/etc/dovecot/sieve/learn-ham.sieve
}
Сам learn-spam.sieve просто пайпит письмо в обёртку:
Почему это лучше autolearn: обучаетесь на решении человека, а не на собственном пороге. Пользователь, отправивший письмо в спам, дал вам разметку, которую невозможно получить эвристикой. Это единственный режим, который стоит держать включённым всегда.
autolearn: когда включать и как не отравить корпус
Развеем главный миф. autolearn — это не самообучающийся ИИ. Это тупой порог: «если итоговый score письма выше X — считать его спамом и доучиться на нём; если ниже Y — считать ham». Всё. Никакого интеллекта, только сравнение чисел.
И отсюда встроенная опасность — петля обратной связи. Если поставить пороги мягко, классификатор начнёт учиться на собственных решениях. Пограничное письмо получило score 6.5 из-за случайного совпадения → autolearn записал его как спам → следующее похожее письмо теперь чуть спамнее → и так по кругу, пока корпус не отравлен и не тащит в спам легитимную почту.
Правило простое: ham-порог должен быть глубоко отрицательным, spam — уверенно высоким. Учите на автомате только то, в чём система и так не сомневается. Уже ham_threshold = -0.5 — на грани риска; всё, что мягче нуля, начинает захватывать пограничные письма. На многих продах autolearn держат либо очень узким, либо выключенным вовсе, полагаясь на IMAPSieve — потому что человеческий сигнал не создаёт петлю, а порог создаёт.
Общая статистика на несколько серверов
Вот ради чего всё затевалось. Три rspamd-ноды за балансировщиком, каждая со своим локальным Redis — это три разных байеса, каждый видел треть трафика, и ни один не набрал уверенный корпус. Пользователь пометил спам на ноде 1 — ноды 2 и 3 об этом не узнают.
Решение — вынести Bayes-Redis в один центральный инстанс и на всех нодах в classifier-bayes.conf указать его:
ini
backend = "redis";
servers = "10.0.0.5:6379";
Для HA не держите один Redis точкой отказа. Разделите запись и чтение:
Два предостережения из практики. Первое: байес читается из Redis на каждом письме. Централизованный инстанс обязан жить в той же low-latency сети (< ~1 мс RTT). Вынесете Redis в другой ДЦ — получите просадку времени сканирования на каждом сообщении, и балансировщик начнёт копить очередь. Второе: fuzzy-storage централизуйте тем же приёмом. Нет смысла шарить байес, если fuzzy у каждой ноды свой.
Эксплуатация: бэкап, сброс, диагностика
rspamc stat — ваша главная приборная панель. Смотрите revision (счётчик learn) по BAYES_SPAM и BAYES_HAM, общее число learn и объём токенов в каждом статфайле. Если revision одного класса сильно обгоняет другой — корпус перекошен, доучите отстающий.
Бэкап байеса — это бэкап Redis, а не экспорт через rspamc. Никакого текстового дампа корпуса не существует (siphash, помните). Настройте RDB-снапшоты того инстанса, где живут ключи BAYES_*, и кладите их в крон:
bash
redis-cli -h 10.0.0.5 SAVE
# затем скопировать dump.rdb в бэкап-хранилище
Отравленный корпус лечится сбросом. Удаляете байесовы ключи и переучиваете с чистого размеченного датасета:
Ещё чище — заранее выделить байесу отдельный номер Redis-DB (параметр dbname в настройках бэкенда) и сбрасывать его одним FLUSHDB, не рискуя зацепить ключи fuzzy, ratelimit или greylist.
Отладка одного письма — rspamc -v покажет, какие символы навесились и с каким весом, включая присвоенный байесом класс и вклад в score. Когда пользователь жалуется «моё письмо ушло в спам», начинаете именно отсюда: прогоняете исходник через rspamc и смотрите, BAYES_SPAM ли виноват или что-то другое.
Чек-лист запуска
Redis централизован — все ноды пишут в один master, читают из реплик; инстанс в low-latency сети (< 1 мс).
Классификатор глобальный — users_enabled = false для shared/temp-mail.
min_learns осознан — знаете, при каком объёме байес начнёт влиять на score (по умолчанию 200 на класс).
Стартовый bulk сбалансирован — примерно равное число ham и spam; при ручном learn_* баланс держите вы, check_balance спасает только autolearn.
IMAPSieve подключён — живой сигнал от пользователей в обе стороны (в Junk и из Junk).
Пороги autolearn консервативны — spam ≥ 6.0, ham ≤ -0.5, или autolearn выключен.
`rspamc stat` в мониторинге — следите за revision обоих классов и объёмом корпуса, а не только за uptime.
Бэкап Redis в кроне — RDB-снапшот keyspace байеса, плюс fuzzy-storage тем же приёмом.
Байес перестаёт быть чёрным ящиком в ту минуту, когда вы начинаете относиться к нему как к данным: сбалансированный корпус, один Redis на весь кластер, человеческая обратная связь как основной источник learn и консервативный autolearn на подхвате. Всё остальное — уже тюнинг цифр под свой трафик.