Эффективная почтовая система на базе nginx+postfix+rmilter
Довольно часто возникает необходимость сделать высокоэффективную почтовую систему с возможностью масштабированияв плане увеличения mx'ов. Для этого можно воспользоваться такой связкой: на входе стоит nginx с policy, которая выполняет асинхронный резолвинг адресов и фильтрацию по rbl+regexp. На многих потоках почты, это снижает процент почты, доходящей до MTA, примерно в 2-3 раза. При этом policy выполняет балансировку между различными backend'ами на основании весов. MTA использует rmiter для выполнения различных проверок: clamav, spamd, ratelimits, greylisting, regexps. При этом, все данные greylisting'а и лимитов сохраняются в memcached, что обеспечивает очень быструю работу и возможность скалирования системы.
Итак, вначале скачиваем nginx версии 0.6.11: http://sysoev.ru/nginx/nginx-0.6.11.tar.gz Более свежие версии, к сожалению, работать не будут. Далее скачиваем libevent и применяем мой патч для поддержки TXT записей в резолвере:
libevent_txt.patch
Почему этой возможности до сих пор нет в libevent, мне не очень понятно.
После установки патченной версии libevent можно устанавливать nginx и policy.
Вначале патчим исходники nginx патчем для поддержки policy (также фиксится работа pipelining'а и включается режим работы без аутентификации). Патч лежит тут:
http://cebka.pp.ru/hg/hgwebdir.cgi/nginx-smtp-policy/file/ab33c3b8f1f1/patch-src_mail
После этого собираем nginx с поддержкой модуля mail.
Далее можно собрать policy. Скачиваем policy отсюда: http://cebka.pp.ru/hg/hgwebdir.cgi/nginx-smtp-policy/archive/tip.tar.gz
Собирается просто командой make+make install. При этом ставятся файлы nginx-smtp-policy и nginx-policy-watchdog с соответствующими стартовыми скриптами в /usr/local/etc/rc.d
Настройка policy предельно проста: в каждой строчке содержится переменная и ее значение. Строчки с # в начале считаются комментариями. Пример используемого мной конфига:
# nameserver - definition of nameserver (maybe multiply)
# special value resolv.conf is used to parse /etc/resolv.conf file
nameserver 195.19.37.129
# logfile - full path to logfile
# Default: /var/log/policy.log
logfile /var/log/policy/policy.log
# pidfile - full path to pidfile
# Default: /var/run/policy.pid
pidfile /var/log/policy/policy.pid
# backend_host - address of host that are backends for nginx (postfix address)
# Example host1:weight1,host2:weight2
# Default: 127.0.0.1
backends 127.0.0.1:1
# backend_port - port of backend
# Default: 25
backend_port 25
# listen - address of socket to listen on, maybe tcp or unix:
# tcp: listen tcp:host:port
# unix: listen unix:/path/to/sock
# Default: tcp:localhost:7070
listen tcp:localhost:7070
# loglevel - numeric value of logging verbocity
# 0 - only errors are logged
# 1 - warnings and errors are logged
# 2 - warnings, notices and errors are logged
# 3 - everything including debug messages is logged
# Default: 0
loglevel 2
# helo_regexp_file - specify path to helo regexp file and error code and error message string
# separated by ':' (maybe multiply)
# Example: helo_regexp_file /usr/local/etc/nginx-policy/helo.re:554 5.7.1:Bad address
# Default:
helo_regexp_file /usr/local/etc/postfix/maps/policy_helo_regexp:554 5.7.1:Helo rejected
# hostname_regexp_file - specify path to host regexp file and error code and error message string
# separated by ':' (maybe multiply)
# Example: hostname_regexp_file /usr/local/etc/nginx-policy/host.re:554 5.7.1:Bad address
# Default:
hostname_regexp_file /usr/local/etc/postfix/maps/policy_reverse_regexp:554 5.7.1:We do not accept mail form this dynamic pool
check_rbl xbl.spamhaus.org
check_rbl insecure-bl.rambler.ru
Файлы, которые я использую для этих регэкспов, можно найти в http://cebka.pp.ru/stuff/
Postfix ставится стандартным образом. Отдельно могу указать следующие вещи:
если мы на одном хосте пускаем policy и postfix, то в main.cf указываем
inet_interfaces = localhost
также для корректной работы rmilter нужно указать следующие опции:
milter_protocol = 4
smtpd_milters = unix:/var/run/rmilter/rmilter.sock
milter_default_action = accept
milter_mail_macros = i {auth_type} {auth_authen} {auth_author} {mail_addr} {client_addr} {client_name}
для работы XCLIENT указываем, с каких хостов он разрешен (это хост, где работает policy)
smtpd_authorized_xclient_hosts = localhost
Далее настроим nginx:
mail {
server_name host.ru;
auth_http localhost:7070/nginxauth.cgi;
smtp_auth none;
#smtp_capabilities "SIZE 28311552" PIPELINING 8BITMIME;
smtp_capabilities "SIZE 28311552" 8BITMIME;
xclient on;
smtp_helo_required on;
smtp_banner "mxi.icn.bmstu.ru ESMTP nginx\n\nSystem Info: This is a mail server at host.ru\n\n\n\nEmail contact: <abuse@host.ru>\n\n";
proxy_pass_error_message on;
server {
#listen 3525;
listen ip1:25;
listen ip2:25;
protocol smtp;
timeout  
; 300s;
}
}
Rmilter для данной системы можно поставить из портов FreeBSD - mail/rmilter
Настройка rmilter детально описано в конфиге и странице rmilter (8). Memcached для работы мильтра можно использовать любой. Для общения с memcached лучше использовать tcp. Работа в режиме udp пока нестабильна из-за кучи ошибок в реализации udp в мемкешеде.
Работа этой связки проверялась в FreeBSD 6, FreeBSD 7 и FreeBSD 8-CURRENT. Если кому-то удастся завести эту связку на других системах, шлите патчи
Также в http://cebka.pp.ru/stuff лежат примеры конфигов и используемые нами "белые" списки.
Даунгрейд FreeBSD 8 (CURRENT) до 7-STABLE
В ходе даунгрейда выплыла проблема с невозможностью установить мир. Установка затыкалась на mtree, который не мог работать с libc от семерки. Проблема решилась ручной установкой usr.bin/find и usr.sbin/mtree набрав в этих директориях make install. Также stass@ посоветовал способ с исправлением таргета bootstrap-tools из src/Makefile1.inc. Этот способ я не пробовал, но он тоже должен работать. После установки find и mtree, installworld работает нормально. Система, обновленная таким образом тоже нормально работает.
Новая версия патча для поддержки exim-xclient
По пожеланиям ряда людей добавил поддержку нового nginx (0.7.x) в реализацию XCLIENT'а для exim'а. Отличие - понимание переданного параметра LOGIN и установка authenticated_id.
patch-exim-xclient
Репозиторий некоторых софтин, которые я написал в «Рамблере»
Во время работы в "Рамблере" я написал несколько вещей, которые могут быть полезны не только у нас, но и пригодиться другим. Поэтому я решил выложить их в виде mercurial репозитария. Среди них:
- http://cebka.pp.ru/hg/rmilter/ - фильтр почты для sendmail/postfix, позволяющий делать различные проверки, в частности, spamassassin/clamav/dcc/spf, также имеются встроенные средства для грейлистинга и проверки лимитов (через memcached), также поддерживаются проверки по регэкспам. Мильтр конфигурируется при помощи конфигурационного файла, снабженного комментариями и описанного в странице man (rmilter.conf.5)
- http://cebka.pp.ru/hg/nginx-smtp-policy/ - асинхронный резолвер имен (dns resolver) для работы с nginx с патчем, изначально написанным Максом Дуниным (http://mdounin.ru/). Также nginx-smtp-policy осуществляет load balancing по различным mx'ам (согласно весам) и отсеивание нежелательной почты путем проверки RBL и мониторинг mx'ов, на которые осуществляется транспорт почты (nginx-smtp-watchdog). Данная программа предназначена для почтовых систем, обслуживающих очень большой поток почты и существенно снижает нагрузку на конечные mx'ы (патч, который есть в репозитории, работает с nginx до версии 0.6.11 включительно).
- http://cebka.pp.ru/hg/libevent/ - патчи, необходимые для работы nginx-smtp-policy. Существенно ускоряют резолвинг за счет использования вместо линейного списка DNS запросов хеш таблицу, а также патч, позволяющий резолвить TXT записи (для проверки RBL).
В настоящее время я работаю над созданием быстрой альтернативы spamassassin'у, если данный проект получится удачным, то он тоже будет открыт, скорее всего.
Использование dmalloc
Так как использовать dmalloc не так просто, то записать основные принципы на будущее не бесполезно. Итак, dmalloc линкуется статически, например, так:
.ifdef DMALLOC
CFLAGS+= -DDMALLOC -DDMALLOC_FUNC_CHECK
LIBS+= -ldmalloc
.endif
Далее, самый простой способ использовать библиотеку:
определяем функции malloc и free, чтобы иметь представление, из какой строчки кода они вызываются:
#ifdef DMALLOC
#include <dmalloc.h>
#define mymalloc(x) dmalloc_malloc(__FILE__, __LINE__, (x), DMALLOC_FUNC_MALLOC, 0, 1)
#define myrealloc(x, y) dmalloc_realloc(__FILE__, __LINE__, (x), (y), DMALLOC_FUNC_REALLOC, 0, 1)
#define myfree(x) dmalloc_free(__FILE__, __LINE__, (x), DMALLOC_FUNC_FREE)
#define mystrdup(x) dmalloc_strndup(__FILE__, __LINE__, (x), strlen((x)), 1)
#else
#define mymalloc(x) malloc((x))
#define myrealloc(x, y) realloc((x), (y))
#define myfree(x) free((x))
#define mystrdup(x) strdup((x))
#endif
Далее в коде пользуемся только этими функциями для работы с памятью.
Для инициализации библиотеки можно пользоваться переменными окружения. Но в ряде случаев это не работает (cgi, su и прочее). Поэтому я решил делать инициализацию так:
#ifdef DMALLOC
char dmalloc_log[PATH_MAX];
snprintf (dmalloc_log, sizeof (dmalloc_log), "debug=0x4f47d03,log=%s/dmalloc-%%p.log", DMALLOC_LOG_PATH);
dmalloc_debug_setup (dmalloc_log);
#endif
Лог файлы создаются автоматически, DMALLOC_LOG_PATH - это каталог, куда должна быть разрешена запись пользователю, из-под которого запускается интересующая нас программа.
Логирование неосвобожденной памяти тоже лучше выполнять самому, чтобы потом не бояться потерять информацию при падении программы где-то. На мой взгляд, лучше всего использовать марки:
в начале секции кода, где может утекать память ставим:
#ifdef DMALLOC
unsigned int mark;
mark = dmalloc_mark();
#endif
в конце секции:
#ifdef DMALLOC
dmalloc_log_changed(mark,
1 /* log unfreed pointers */,
0 /* do not log freed pointers */,
1 /* log each pnt otherwise summary */);
#endif
В логе имеем что-то вида
1212677815: 289299: Dumping Not-Freed Pointers Changed Since Mark 289178:
1212677815: 289299: memory table is empty
Или же указатели на память:
1212677788: 288383: not freed: '0x80144af88|s1' (24 bytes) from 'unknown'
1212677788: 288383: not freed: '0x80144afc8|s5' (40 bytes) from 'unknown'
1212677788: 288383: not freed: '0x80144be88|s1' (24 bytes) from 'unknown'
1212677788: 288383: not freed: '0x80144bec8|s1' (40 bytes) from 'unknown'
Можно уронить процесс в корку (kill -SEGV, например) и посмотреть эти адреса в gdb:
(gdb) x/40c 0x80144bec8
Race в коде закрытия unix сокета в FreeBSD 6.x
Выглядит race следующим образом (6.3-SMP):
48976 nginx 1202926950.760500 CALL recvfrom(0xad,0x1b2ab38,0x400,0,0,0)
48976 nginx 1202926950.760511 GIO fd 173 read 122 bytes
"HTTP/1.0 200 OK\r
Client-Host: [UNAVAILABLE]\r
Auth-Status: OK\r
Auth-Server: 10.8.2.19\r
Auth-Port: 25\r
Connection: close\r
\r
"
48976 nginx 1202926950.760521 RET recvfrom 122/0x7a
48976 nginx 1202926950.760533 CALL close(0xad)
48976 nginx 1202926950.760548 RET close -1 errno 57 Socket is not
connected
Похоже, race возникает тут:
sys/kern/uipc_socket.c:
int soclose(so)
{
...
if (so->so_state & SS_ISCONNECTED) {
if ((so->so_state & SS_ISDISCONNECTING) == 0) {
error = sodisconnect(so);
}
int sodisconnect(so)
{
...
if ((so->so_state & SS_ISCONNECTED) == 0)
return (ENOTCONN);
}
so_state для unix сокета устанавливается следующим образом:
sys/kern/uipc_usrreq.c:
static void
unp_disconnect(struct unpcb *unp)
{
struct unpcb *unp2 = unp->unp_conn;
struct socket *so;
UNP_LOCK_ASSERT();
if (unp2 == NULL)
return;
unp->unp_conn = NULL;
switch (unp->unp_socket->so_type) {
case SOCK_DGRAM:
LIST_REMOVE(unp, unp_reflink);
so = unp->unp_socket;
SOCK_LOCK(so);
so->so_state &= ~SS_ISCONNECTED;
SOCK_UNLOCK(so);
break;
case SOCK_STREAM:
soisdisconnected(unp->unp_socket);
unp2->unp_conn = NULL;
soisdisconnected(unp2->unp_socket);
break;
}
}
Насколько я понял, для sock_stream закрываются оба конца соединения.
Закрываются так:
sys/kern/uipc_socket2.c:
void
soisdisconnected(so)
register struct socket *so;
{
/*
* XXXRW: This code assumes that SOCK_LOCK(so) and
* SOCKBUF_LOCK(&so->so_rcv) are the same.
*/
SOCKBUF_LOCK(&so->so_rcv);
so->so_state &=
~(SS_ISCONNECTING|SS_ISCONNECTED|SS_ISDISCONNECTING);
so->so_state |= SS_ISDISCONNECTED;
so->so_rcv.sb_state |= SBS_CANTRCVMORE;
sorwakeup_locked(so);
SOCKBUF_LOCK(&so->so_snd);
so->so_snd.sb_state |= SBS_CANTSENDMORE;
sbdrop_locked(&so->so_snd, so->so_snd.sb_cc);
sowwakeup_locked(so);
wakeup(&so->so_timeo);
}
То есть, не исключена ситуация, когда в функции soclose у одного из
сокетов состояние SS_ISCONNECTED, а в функции sodisconnect этот флаг уже
успевает убраться. Тогда может вернуться ENOTCONN. Как вариант фикса можно попробовать убрать проверку
if ((so->so_state & SS_ISCONNECTED) == 0)
return (ENOTCONN);
из функции sodisconnect, например, возвращая в этом месте 0.
Миграция с jabberd2 на openfire
Используя плагин экспорта/импорта у openfire можно заводить
пользователей, используя достаточно простой xml код. Хотя этот плагин
работает достаточно нестабильно (xml файл, содержащий 4к пользователей
он не переварил, вываливаясь с NullPointerException), по крайней мере у
меня, но, возможно, кому-то это окажется полезным.
jabber_to_xml.pl
Memcached UDP fix
Следующий патчик исправляет ошибку в обработке udp запросов memcached'ом, которая приводила к падению его в корку при посылке пакета с неверным заголовком, а также не освобождала буфер чтения при ошибочной команде или же ненайденном ключе.
patch-memcached-udp
XCLIENT в Exim
По просьбе Андрея Зверева написал патчик, позволяющий работу команды XCLIENT в exim 4. Описание этой команды можно найти тут. По умолчанию XCLIENT запрещен для всех хостов, но его можно включить, задав опцию xclient_allow_hosts в конфигурационном файле, например:
xclient_allow_hosts = 127.0.0.1 : 192.168.1.1
Пример SMTP диалога:
Connected to localhost.
Escape character is '^]'.
220 dhcp-ng2 ESMTP Exim 4.68 Mon, 10 Dec 2007 19:26:44 +0300
XCLIENT NAME=spike.porcupine.org ADDR=168.100.189.2 HELO=blah
220 XCLIENT success
MAIL FROM:<wietse@porcupine.org>
250 OK
RCPT TO:<user@example.com>
550 relay not permitted
Memcached UDP
Хотя memcached поддерживает формально работу по udp протоколу, но пользоваться им я не рекоммендую. Более того, если мемкешеду послать udp пакет с неправильным заголовком, то он (memcached) падает в корку. Далее, даже при посылке правильных пакетов у мемкешеда через некоторое время выплывает проблема неочистки командного буфера, и при посылке очередной команды мемкешед начинает "вспоминать" куски старых команд. Сейчас я пытаюсь исправить эти баги, и если все удастся, то будет патч совместно с библиотекой работы с мемкешедом, которая умеет udp (tcp тоже, но исходной задачей было написание thread-safe библиотеки для работы с мемкешедом). При использовании udp для извлечения большого количества маленьких ключей из мемкешеда должно весьма существенно ускориться.
Данные тестирования:
UDP:
Results of memcached stress test:
Total number of connections: 10000
Number of seconds for test: 2.23
Number of successfull connections: 10000
Connections per second: 4494.25
TCP:
Results of memcached stress test:
Total number of connections: 10000
Number of seconds for test: 4.38
Number of successfull connections: 10000
Connections per second: 2280.87
При этом, использовалось 100 одновременных коннекций к серверу, каждая из которых выполняла 3 операции над своим ключом: set, get и delete.