E-mailová kampaň pro 1 000 000 příjemců

E-mailová kampaň

Black Friday je důležitý okamžit pro většinu eshopů. Jeden z našich zákazníků se na nás obrátil s požadavek na asistenci při rozeslání něco přes 1 000 000 emailů.

Nejdříve jsme si mysleli, že se tomu věnovat nebudeme, protože podobné služby nabízí velké množství společností. Navíc už v tomto oboru mají výrazně více zkušeností než my. Chtěli jsme zákazníkovi jen s nejlepším vědomím a svědomím doporučit nějakou z exisujících služeb. Při hledání vhodné služby jsme ale narazili na složité ceníky, kde je mnoho omezení a ve výsledku by kampaň stála už docela zajímavé peníze.

Změnili jsme tedy pohled na věc a projekt přijali jako technologickou výzvu.

Podmínky

Zákazník má šikovné vývojáře, takže jsme si rozdělili role - my se postaráme o servery pro doručování pošty a zákazník si zařídí instalaci systému na rozesílku emailů a postará se o samotnou rozesílku.

Ze strany zákazíka padla volba na phplist jako systém pro správu kampaně.

Z naší strany jsme zvolili jako MTA postfix v kombinaci s opendkim. Celkem jsme se rozhodli použít 250 SMTP serverů.

Instalace serverů

Protože se jednalo o jednorázovou akci, rozhodli jsme se situaci nekomplikovat a šli jsme nejjednodušší možnou cestou, která nás napadla.

Řešení jsme postavili na jednom serveru s kontejnerovou virtualizací LXC v kombinaci se souborovým systémem BTRFS.

Příprava kontejnerů

Samotná instalace serverů byla velmi podobná tomu, co se běžně dělá v docker prostředích. Na začátku jsme si vytvořili jeden server, který sloužil jako šablona. Tento server jsme pro replikovatelnost celého procesu instalovali scriptem, takže se šablona dala kdykoliv zahodit a sestavit znovu.

Následně jsme tuto šablonu naklonovali 250x a při startu jsme jen podle předem připraveného seznamu upravili hostname (resp. mailname) a IP adresu. V tento moment přichází na řadu BTRFS, které nám s tímto krokem výrazně ušetřilo čas a IO operace. Díky snapshotům bylo vytváření jednotlivých instancí ze šablony okamžité a na disku každá instance zabírala jen místo se změnami proti šabloně.

root@lxc1:/# df -h /var/lib/lxc
Filesystem      Size  Used Avail Use% Mounted on
/dev/vdb        100G  1.9G   97G   2% /var/lib/lxc

Co se paměťové náročnosti týče, tak všech 250 instancí potřebovalo v idle režimu jen 4,66GB paměti i se samotným hostitelským systémem.

Síťování

Bohužel jsme stále v době, kdy všechny servery příjmají poštu přes IPv4, ale jen některé zároveň i přes IPv6. Jednotlivým kontejnerům jsme přiřazovali jen veřejné /24 IPv4 adresy a IPv6 jsme zjednodušení vynechali (už se těšíme na dobu, kdy to budeme moc udělat obráceně).

Výsledek - přehled kontejnerů

root@lxc1:/# lxc-ls -f
NAME        STATE   AUTOSTART GROUPS IPV4            IPV6
aaron       RUNNING 1         -      xxx.yyy.zzz.113 -
abigail     RUNNING 1         -      xxx.yyy.zzz.192 -
adam        RUNNING 1         -      xxx.yyy.zzz.121 -
alan        RUNNING 1         -      xxx.yyy.zzz.179 -
albert      RUNNING 1         -      xxx.yyy.zzz.165 -
alexander   RUNNING 1         -      xxx.yyy.zzz.103 -
alexis      RUNNING 1         -      xxx.yyy.zzz.204 -
alice       RUNNING 1         -      xxx.yyy.zzz.156 -
...

Chytrý load balancing

Všechny instance byly nainstalované a z naší strany bylo vše připraveno pro napojení na phplist a spuštění prvních rozesílek. V tento moment jsme zjistili, že phplist je omezen pouze na jeden SMTP server.

Jednoduché řešení v tento okamžik bylo použít haproxy s round-robin balancingem, který by se postaral o rovnoměrné rozložení zpráv na všechny servery.

My jsme si zde ale definovali ještě jeden požadavek - chtěli jsme se vyhnout rate-limitu ze strany serverů příchozí pošty. Udělali jsme si rychlou analýzu příjemců emailů a distribuce poskytovatelů emailových služeb byla dostatečně pestrá, aby tento požadavek měl smysl řešit

Bylo tedy potřeba implementovat chytrý loadbalancing tak, aby se na jednom serveru nesešly všechny emaily pro gmail, ale aby byl rovnoměrně rozložen mezi všechny servery. K docílení tohoto požadavku jsme použili původně webový server nginx. V současné době už rozumí emailovým protokolům a dá se použít i jako obecná TCP nebo UDP proxy.

Nginx konfigurace

Samotná nginx konfigurace vypadala následovně:

mail {
    auth_http  localhost/index.php;

    proxy_pass_error_message on;

    server {
        listen     25;
        protocol   smtp;
        proxy      on;
        smtp_auth  none;
    }
}

Zde je důležité nastavení smtp_auth none;, které nám zajistí, že náš backend dostane hlavičku Auth-SMTP-To, která obsahuje příjemce zprávy.

Výběr backend serveru

V tento moment nám nginx pro každý odeslaný email vytvořil HTTP požadavek na localhost/index.php, kde mu PHP script odpověděl pomocí následujících hlaviček, který backend server má použít:

header('Auth-Status: OK');
header('Auth-Server: xxx.yyy.zzz.nnn');
header('Auth-Port: 25');

Pro každý email jsme se tedy pomocí proměnné $_SERVER["HTTP_AUTH_SMTP_TO"] podívali, kam se bude doručovat. Zjistili jsme si poskytovatele emailových služeb a pomocí databáze v redisu implementovali pro každého poskytovatele vlastní round-robin index.

Abychom předešli situaci, kdy první server bude rozesílat všem unikátním poskytovatelům, tak jsme ještě implementovali mechanismus, který průběžně měnil první server, takže se emaily rovnoměrně rozložily mezi všechny servery.

Závěr

Cílem bylo s co nejmenším množstvím investované energie dosáhnout co nejlepšího výsledku a to se nám povedlo. Časově jsme se vešli do 4 MD na celý projekt, včetně monitoringu a podpory při rozesílce. Nejnáročnější byl následný monitoring, sbírání logů a vyhodnocování, jestli se emaily doručují v pořádku.

Při rozesílce se nám povedlo dostat se na blacklist, protože zákazník byl příliš horlivý s dávkováním. Díky dobrému monitoringu se nám ale povedlo problém podchytit a napravit včas.

Výsledná distribuce emailů mezi servery byla dobrá a servery byly téměř rovnoměrně vyvážené.