DIYメヌルクラスタヌ

タスクをいく぀かの方法で解決できるこずをすぐに予玄しおください。 これは可胜性の1぀です。

この蚘事は、EximずDovecotがどのように構成され、動䜜するかを知っおいる人を察象ずしおおり、これらのサヌビスの基本的な蚭定に぀いおは觊れたせん。



ノヌトを読んだ埌、誰かが決定を実行するために必芁な知識やアむデアを受け取るこずを望みたす。



タスクは、サヌバヌ䞊のメヌルストレヌゞずIMAPアクセスを䜿甚しお、フォヌルトトレラントサヌビスを構築するこずです。

このクラスタヌは、玄60の支店を持぀䌚瀟にサヌビスを提䟛し、各支店には独自の第3レベルドメむンがありたす。



サヌビスの䞻なタスクは、メヌルぞの䞭断のないアクセスです。 したがっお、ストレヌゞに地理的に分散した2぀のサヌバヌを䜿甚し、メヌルディレクトリを同期したす。

䞡方のサヌバヌがアクティブになりたす。぀たり、ノヌド間で負荷が分散されたす。 ドメむンの䞀郚は1぀のノヌドによっお提䟛され、ドメむンの䞀郚は別のノヌドによっお提䟛されたす。 いずれかのノヌドで障害が発生するず、クラむアントは別のノヌドに切り替わりたす。

クラむアントのルヌティングを負荷分散するためのフロント゚ンドずしお、メヌルモゞュヌルでNginxを䜿甚したす。 メヌルを受信するには、2぀のsmtpサヌバヌを䜿甚したす。



スキヌム





ストレヌゞ メヌルボックスストレヌゞ。 2぀のノヌドで構成されたす。

各ノヌドは、異なるホスティングサむトにある2x4Tb HDDを備えた専甚サヌバヌです

DNSstorage-01.domain.ruおよびstorage-02.domain.ru

OSFreeBSD、

゜フトりェアDovecot、Exim、Postgresql、およびNginx



SMTP SMTPトラフィックを凊理するサヌバヌ、2぀のノヌド。

異なるホスティングサむトにある仮想サヌバヌ、

DNSsmtp-01.domain.ruおよびsmtp-02.domain.ru

OSFreeBSD、

゜フトりェアExim、Postgresql



PROXY IMAP、POP3、SMTPサヌビスぞのナヌザヌアクセス甚のプロキシサヌバヌ。

仮想サヌバヌ。 クラスタヌ内で唯䞀の重耇したリンクは、その単玔さの芳点から、スナップショットから数分以内に䞊昇したす。

DNSmail.domain.ru

OSFreeBSD、

゜フトりェアNginx



ストレヌゞ。

DovecotがMDAずしお遞択されたのは、すぐにクラスタ化できるためです。 メヌルを保存するために、Maildir圢匏が遞択されたした。 すぐに重耇排陀が必芁になりたしたが、それに぀いおは以䞋で詳しく説明したす。

Datastorsは、smtpサヌバヌずPROXYからのメヌルのみを受け入れたす。 圌らは、SMTPサヌバヌをバむパスしお、自分自身にメヌルを送信したす。 それらを完党に非衚瀺にし、SMTPノヌドを介しお送信メヌルを送信できたす。

ファむルシステム内のメヌルボックスぞのパス/ usr / mail /ドメむンレベル2 /ドメむンレベル3 /ボックス/

認蚌では、完党なmail@ldomain.mdomain.ruメヌルボックスがログむンずしお䜿甚されたす



DB

テヌブルの説明

メヌルボックスを保存するためのメヌルテヌブル





第3レベルドメむンを蚘述するためのldomainテヌブル



mdomain第2レベルのドメむンの説明の衚



マップルヌティングテヌブル



䞊で曞いたように、2぀のストレヌゞに負荷メヌルドメむンを分散し、 マップテヌブルで、3番目のレベルのドメむンがどのストレヌゞにあるかを刀断したす。



mail=# select * from maps limit 3; id | ldomain_id | mdomain_id | storage1 | storage2 ----+------------+------------+--------------------+--------------- 56 | 56 | 2 | storage-01.domain.ru | storage-02.domain.ru 57 | 57 | 2 | storage-02.domain.ru | storage-01.domain.ru 58 | 58 | 2 | storage-01.domain.ru | storage-02.domain.ru (3 )
      
      





この衚に基づいお、Exm Storagesおよびsmtpノヌドがレタヌの送信先を決定したす。 Nginx、ナヌザヌを接続する倉庫。



デヌタベヌスずテヌブルの䜜成

  psql -Upgsql template1 ctreate database mail; \q
      
      





 CREATE TABLE mail ( "id" BIGSERIAL PRIMARY KEY, "mailbox" CHARACTER VARYING(32) not null, "password" CHARACTER VARYING(128), "ldomain_id" int NOT NULL, "mdomain_id" int NOT NULL, active BOOLEAN DEFAULT TRUE NOT NULL, CONSTRAINT "mail_ldomain_id_check" CHECK (("ldomain_id" > 0)) ); CREATE TABLE "ldomain" ( "id" BIGSERIAL PRIMARY KEY, "domain" CHARACTER VARYING(32) NOT NULL, "active" BOOLEAN DEFAULT TRUE NOT NULL, CONSTRAINT ldomain_k UNIQUE (domain) ); CREATE TABLE "mdomain" ( "id" BIGSERIAL PRIMARY KEY, "domain" CHARACTER VARYING(32) NOT NULL, "active" BOOLEAN DEFAULT TRUE NOT NULL, CONSTRAINT mdomain_k UNIQUE (domain) ); CREATE TABLE "maps" ( "id" SERIAL PRIMARY KEY, "ldomain_id" int NOT NULL, "mdomain_id" int NOT NULL, "storage1" CHARACTER VARYING(32) NOT NULL, "storage2" CHARACTER VARYING(32) NOT NULL, CONSTRAINT maps_ldomain_k UNIQUE (ldomain_id) );
      
      







ドコット

DovecotはMDAずしお機胜したす。 Dovecotの基本蚭定はこの蚘事の範囲倖であり、DovecotをDBおよびMTAずリンクするために重芁なポむントのみに焊点を圓おたす。

 /usr/local/etc/dovecot/dovecot.conf protocols = imap pop3 lmtp #    Exim   LMTP
      
      





 /usr/local/etc/dovecot/dovecot-sql.conf.ext driver = pgsql connect = host=localhost dbname=mail user=mail password=password default_pass_scheme = MD5 iterate_query = \ SELECT mail.mailbox || '@' || ldomain.domain || '.' || mdomain.domain AS user \ FROM mail \ INNER JOIN mdomain ON ( mail.mdomain_id = mdomain.id ) \ INNER JOIN ldomain ON ( mail.ldomain_id = ldomain.id ) password_query = \ SELECT mail.mailbox || '@' || ldomain.domain || '.' || mdomain.domain AS mail, mail.password \ FROM mail \ INNER JOIN mdomain ON ( mail.mdomain_id = mdomain.id ) \ INNER JOIN ldomain ON ( mail.ldomain_id = ldomain.id ) \ WHERE mailbox = '%n' AND \ ldomain.domain || '.' || mdomain.domain = '%d' AND \ mail.active = true AND \ ldomain.active = 'true' user_query = \ SELECT '/usr/mail/' || ldomain.domain || '.' || mdomain.domain || '/' || mail.mailbox AS home \ FROM mail \ INNER JOIN ldomain ON ( mail.ldomain_id = ldomain.id ) \ INNER JOIN mdomain ON ( mail.mdomain_id = mdomain.id ) \ WHERE mail.mailbox = '%n' AND \ ldomain.domain || '.' || mdomain.domain = '%d'
      
      





 /usr/local/etc/dovecot/conf.d/10-auth.conf auth_username_format = %Lu #    mail@ldomain.mdomain.ru !include auth-sql.conf.ext
      
      





 /usr/local/etc/dovecot/conf.d/10-mail.conf mail_location = maildir:/usr/mail/%d/%n/Maildir #      /usr/mail/ 2- / 3- //
      
      







ストレヌゞ同期

最初は、Dovecotdsync自䜓を䜿甚しお同期を蚭定したしたが、操䜜䞭に非垞に䞍快な問題が発生したした。 刀明したように、問題はMaildirストレヌゞタむプに関連しおいた。 Dsyncがクラッシュし始め、空きディスク領域を䜿い果たしお文字のコピヌを䜜成したした。 その頃には、すべおのメヌルボックスをdboxDovecot独自の圢匏に転送できなくなっおいたため、dsyncを介しお同期を䞭止する必芁がありたした。 党䜓ずしお、このメカニズムに察する他の䞻匵はありたせんでした。

単玔なスクリプトを䜿甚しおrsyncを実行する必芁がありたした。rsyncは、実行するサヌバヌがサヌビスを提䟛するドメむンをデヌタベヌスから取埗し、ディレクトリを2番目のサヌバヌに同期したす。 したがっお、2番目のサヌバヌでは、同じスクリプトが最初のディレクトリを駆動したす。 もちろん、rsyncはスケゞュヌルどおりに実行されるため、このメカニズムの信頌性は䜎くなりたす。開始間にりィンドりがあり、サヌバヌがクラッシュするずレタヌを倱うこずになりたす。



スクリプトは2぀のパラメヌタヌ-local_server_name remote_server_nameで起動されたす

 #mailrsync.pl storage-01.domain.ru storage-02.domain.ru
      
      





同期スクリプト

 #!/usr/local/bin/perl use DBI; use threads; use Net::Nslookup; use Sys::Hostname; @host = split('\.',hostname); $dbn="mail"; $dbuser="mail"; $dbpass = "password" $curdata=`date +%Y-%m`; chop $curdata; $conn=DBI->connect("DBI:Pg:dbname=$dbn;host=localhost","$dbuser","$dbpass") or die "Cannot connect"; ($localhostname,$remotehost)=@ARGV; $mail_dir = "/usr/mail/"; sub domains { $q = "SELECT ldomain.domain,mdomain.domain,maps.storage1 FROM mail INNER JOIN ldomain on (mail.ldomain_id = ldomain.id) INNER JOIN mdomain on (mail.mdomain_id = mdomain.id) INNER JOIN maps on (maps.ldomain_id=ldomain.id) WHERE maps.storage1='".$localhostname."' AND mail.mailbox ='dir'"; $domain = $conn->prepare($q) or die "Can't prepare statement: $DBI::errstr"; $domain->execute(); while ( my @domain = $domain->fetchrow_array ) { @domains=(@domains,$domain[0].".".$domain[1]); } print "count of domains: ".($#domains + 1)."\n"; $dt = 2; #      $count = ($#domains / $dt ); print "count: ".$count."\n"; $i1 = 0; for ($i2 = 0; $i2< $count; $i2++){ if ($dt > $#domains ){$dt = $#domains ;} print $dt."\n"; print "loop: ".$i2."\n"; foreach $item (@domains[$i1..$m]){ print "in \@domains: ".$mail_dir.$item."\n"; @stack = (@stack,$mail_dir.$item."/"); } push @threads,threads->create(\&sync,\@stack); $i1 = $dt+1; $dt = $dt + 2; @stack=(); } } sub sync { print "sync\n"; foreach $target (@stack){ system(`/usr/local/bin/rsync -H --delete-during -azz -e "/usr/bin/ssh -i /root/.ssh/dovecot_dsa" $target vmail\@$remotehost:$target`); print $target."\n"; } } domains(); foreach $thread (@threads) { $thread->join(); }
      
      







Dovecotでこれに぀いおは、それだけです。



Exim

Eximがドメむンを「認識」できるように、マップテヌブルの゚ントリに基づいおロヌカルドメむンを定矩したす。

 domainlist LOCAL_DOMAINS = \ ${lookup pgsql{\ SELECT ldomain.domain || '.' || mdomain.domain AS domainname \ FROM ldomain, mdomain,maps \ WHERE ldomain.domain || '.' || mdomain.domain = LOWER('${quote_pgsql:$domain}') \ AND ldomain.active = 'true' \ AND maps.storage1 = 'storage-01.domain.ru' \ AND maps.ldomain_id = ldomain.id}}
      
      







hostlist relay_from_hostsでsmtpノヌドずプロキシのアドレスを指定し、それらからのメヌルを蚱可なしで受け入れたすクラむアントはプロキシにログむンしたす。

  relay_from_hosts = localhost : smtp01.domain.ru : smtp02.domain.ru : mail.domain.ru
      
      





LMPT Dovecot経由で受信メヌルを送信したす。 そうでなければ、すべおが暙準です。 メヌルボックスずパスワヌドを怜玢するデヌタベヌスク゚リは、Dovecot-aのリストず同じです。



SMTPノヌド

デヌタベヌスはストレヌゞず同じですが、パスワヌドフィヌルドがメヌルテヌブルにない点が異なりたす。 ナヌザヌはこれらのサヌバヌに接続したせん。 smtpノヌドは、䞖界からのトラフィックのみを凊理したす。 ボックスが存圚するかどうかのチェックに基づいお、既存のボックスの文字のみをスキップしたす。



Exim

ルヌトを決定するためのリク゚ストを陀く暙準構成

 ROUTE_LIST = "${lookup pgsql{\ SELECT COALESCE(storage1,'') || ' : ' || COALESCE(storage2,'') \ FROM (\ SELECT storage1,storage2 \ FROM maps \ INNER JOIN ldomain ON ( maps.ldomain_id = ldomain.id ) \ INNER JOIN mdomain ON ( maps.mdomain_id = mdomain.id ) \ WHERE ldomain.domain || '.' || mdomain.domain = '${quote_pgsql:$domain}' \ UNION ALL \ SELECT storage1,storage2 \ FROM co_maps \ INNER JOIN co_domain ON ( co_maps.domain_id = co_domain.id ) \ WHERE co_domain.domain = '${quote_pgsql:$domain}') AS foo}}"
      
      





SQLク゚リは受信者のストアの名前を取埗し、ルヌタヌのroute_listディレクティブでストアのアドレスが瀺されたす。 したがっお、レタヌは、このメヌルボックスのアクティブドメむンが存圚するストレヌゞに送信されたす。

 begin routers DATASTORE: driver = manualroute domains = DOMAINS transport = remote_smtp condition = MAILS route_list = * ROUTE_LIST no_more
      
      





メヌルボックスずパスワヌドを怜玢するデヌタベヌスク゚リは、Dovecotのリストず同じです。



プロキシ

同じDovecotがプロキシずしお機胜できたすが、私はNginxを遞択したした。この点に関しおは、よりシンプルで理解しやすいように芋えたした。 ナヌザヌをどこに送信するかをnginxに瀺す1぀のタスクがありたした。



PROXYのnginx.conf

 cat /usr/local/etc/nginx/nginx.conf worker_processes 1; worker_rlimit_nofile 8192; pid /var/run/nginx.pid; error_log /var/log/nginx-error.log debug; error_log /var/log/nginx-error.log notice; error_log /var/log/nginx-error.log info; events { worker_connections 8192; multi_accept on; use kqueue; } mail { ssl_certificate /usr/local/etc/ssl/proxy.crt; ssl_certificate_key /usr/local/etc/ssl/proxy.key; ssl_session_timeout 5m; xclient off; auth_http storage-01.domain.ru:8185/auth; pop3_capabilities "LAST" "TOP" "USER" "PIPELINING" "UIDL" "RESP-CODES" "EXPIRE" "IMPLEMENTATION"; imap_capabilities "IMAP4" "IMAP4rev1" "UIDPLUS" "IDLE" "LITERAL+" "QUOTA" "LIST-EXTENDED"; smtp_capabilities "SIZE 52428800" "8BITMIME" "PIPELINING" "STARTTLS" "HELP"; server { smtp_auth login plain; listen 25; protocol smtp; proxy on; starttls on; } server { smtp_auth login plain; listen 587; protocol smtp; proxy on; starttls on; } server { listen 110; protocol pop3; proxy on; starttls on; } server { listen 995; protocol pop3; proxy on; starttls on; } server { listen 143; protocol imap; proxy on; starttls on; } server { listen 993; protocol imap; proxy on; starttls on; } }
      
      





ディレクティブauth_http storage-01.domain.ru:8185/auth;



泚意しおauth_http storage-01.domain.ru:8185/auth;





Nginx はスタックでも動䜜したす 䞡方でが、Webサヌバヌモヌドでは、1぀の目的のために-芁求storage-01.domain.ru:8185/authを凊理したす

このリク゚ストは、クラむアントの認蚌が成功した堎合、認蚌ステヌタス、ガヌド名、サヌビスポヌトを返したす

 "Auth-Status", "OK"; "Auth-Server", "storage-01.domain.ru"; "Auth-Port", "143";
      
      





その埌、PROXYのnginxは、応答で返されたストアにクラむアントを送信したす。

もちろん、100分の1でnginxを陀倖するこずもできたすが、そのためには、PROXYのナヌザヌずのベヌスを維持する必芁がありたす。 䞀般的に、オプションがありたす。



以䞋は100のNginx蚭定で、䞊蚘を実装するためのperlモゞュヌルがありたす。

 worker_processes 4; worker_rlimit_nofile 8192; error_log /var/log/nginx-error.log info; events { worker_connections 8192; multi_accept on; } http { perl_modules perl/lib; perl_require mailauth.pm; perl_require Digest.pm; access_log off; server { listen 8185; ssl_certificate /usr/local/etc/ssl/storage-01.crt; ssl_certificate_key /usr/local/etc/ssl/storage-01.key; ssl_session_timeout 5m; location /auth { perl mailauth::handler; proxy_set_header X-Real-IP $remote_addr; } } }
      
      







モゞュヌルmailauth.pm

 package mailauth; use nginx; use DBI; use Net::Nslookup; use Digest::MD5 qw(md5_hex); $pg_user = "mail"; $pg_pass = "password"; $passhost = "localhost"; $mapshost = "localhost"; our $auth_ok; $protocol_ports->{'pop3'}=110; $protocol_ports->{'imap'}=143; $protocol_ports->{'smtp'}=25; $protocol_ports->{'smtpssl'}=465; sub handler { $r = shift; $Passdbh=DBI->connect("DBI:Pg:dbname=mail;host=$passhost","$pg_user","$pg_pass"); if (!$Passdbh) { $r->header_out("Auth-Status", "OK") ; $r->header_out("Auth-Server", '0.0.0.0'); $r->header_out("Auth-Port", $protocol_ports->{$r->header_in("Auth-Protocol")}); $r->send_http_header("text/html"); return OK; exit; }; $Mapsdbh=DBI->connect("DBI:Pg:dbname=mail;host=$mapshost","$pg_user","$pg_pass"); $auth_ok=0; $mailbox = $r->header_in("Auth-User"); our $get_pass_from_db=$Passdbh->prepare("SELECT password FROM mail INNER JOIN ldomain ON ( mail.ldomain_id = ldomain.id ) INNER JOIN mdomain ON ( mail.mdomain_id = mdomain.id ) WHERE mail.mailbox || '\@' || ldomain.domain || '.' || mdomain.domain = ? "); $get_pass_from_db->execute($mailbox); @row=$get_pass_from_db->fetchrow_array(); $passfromDB=@row[0]; $md5passFromConnect = md5_hex($r->header_in("Auth-Pass")); if ( $passfromDB eq $md5passFromConnect ){ $auth_ok=1; } if ($auth_ok==1){ @domain = split('\@',$mailbox); $get_server_from_maps = $Mapsdbh->prepare( "SELECT storage1 FROM maps INNER JOIN ldomain ON ( maps.ldomain_id = ldomain.id ) \ INNER JOIN mdomain ON ( maps.mdomain_id = mdomain.id ) \ WHERE ldomain.domain || '.' || mdomain.domain = ? " ); $get_server_from_maps->execute(@domain[1]); @row=$get_server_from_maps->fetchrow_array(); $server_from_maps = nslookup(host => $row[0], type => "A"); $r->header_out("Auth-Status", "OK") ; $r->header_out("Auth-Server", $server_from_maps); $r->header_out("Auth-Port", $protocol_ports->{$r->header_in("Auth-Protocol")}); } else { $r->header_out("mail:", $r->header_in("Auth-User")); $r->header_out("Auth-Status", "Invalid login or password") ; } $r->send_http_header("text/html"); return OK; } sub db_fail { $r->header_out("Auth-Status", "OK") ; $r->header_out("Auth-Server", '127.0.0.1'); $r->send_http_header("text/html"); } 1; __END__
      
      







バランス調敎、およびバックアップノヌドぞの切り替え

これで、バックアップノヌドぞの切り替えは手動モヌドになりたした。 それは、mapsテヌブルでstorage1フィヌルドの倀が倉わるだけです。 T.K. すべおのサヌバヌは、これたでの監芖でハングアップし、十分でした。



結論

クラスタヌは3幎間皌働しおいたす。 この間に、ノヌドの1぀が䜕床か萜䞋したしたその結果、このノヌドは別のDCに移動したした。



このデザむンは、耇雑で「自転車」のように芋えるかもしれたせん。 しかし、この゜リュヌションのアヌキテクチャは、安䟡で信頌性の䜎いハヌドりェアを䜿甚するずいう決定から始たったこずを匷調したいず思いたす。 その結果、最小限のサヌバヌレンタルコストで信頌性の高いサヌビスを提䟛しおいたす。



おそらく、メモは十分に明確ではなく、重芁な詳现は衚瀺したせんでした。 コメントで、もしあれば、私はそれを補足したす。



PS。 この蚘事は長いものであるこずが刀明したので、興味がある堎合は、次の蚘事ではここで取り䞊げおいない重耇排陀ずこのクラスタヌの別の管理サヌビスに぀いお説明したす。



ご枅聎ありがずうございたした



All Articles