Jak jsme (ne)úspěšně migrovali zpracování statistik návštěvnosti webů do AWS
Už nějakou dobu pracujeme ve Webnode na migraci našich služeb z on-premise řešení do cloudového řešení v AWS. Projekt je to nemalý a skrývá se v něm celá řada výzev. Jednou z nich je i odlišné zpracování statistik návštěvnosti webů našich zákazníků a zachování jejich funkčnosti po migraci do cloudu. Pro představu, jedná se o desítky miliónů projektů rozprostřených přes více než 300 serverů, kterým statistiky zpracováváme od počátku věků (těch našich). Některým i přes 10 let.
Historické okénko
On-premise řešení výpočtu statistik je od počátku řešeno již letitým nástrojem AWStats. Ten jednou denně projde access logy na každém serveru a spočítá návštěvnost všech projektů na daném serveru. Trvá to pár hodin, není to moc real-time, ale funguje to.
Je to dáno tím, že na počátku věků (těch našich), jsme jako webserver používali Apache2. Tehdy to byla pohodlná volba. Nezměnilo se to ani s pozdějším přechodem na webserver Nginx. Přece jen, měnit funkční řešení se nikomu moc nechtělo. Navíc, o jeho konfiguraci a údržbu jsme se nikdy nemuseli nijak starat, protože nám to od počátku zajišťoval smluvní partner. My o tom vždy věděli jen to, že se nám každou noc nějak "porcují logy".
Kromě toho jsme nikdy moc nechtěli unáhlenou změnou řešení ovlivnit třeba i metriky výpočtu statistik. Ono, zákazníci jsou na to docela citliví a zahltit si centrum zákaznické podpory dotazy typu "Kam mi zmizeli všichni moji návštěvníci?" také nebyl úplně náš cíl.
Když jsme se ale rozhodli přesunout z on-premise řešení do cloudu, nezbylo nám než to začít řešit. S přechodem do AWS se nám docela dost mění infrastruktura a díky tomu by on-premise řešení nefungovalo.
Cesta do cloudu začíná
Proběhlo několik meetingů, brainstormingů a náhodných setkání v kuchyňce, na kterých bylo toto téma dokola rozebíráno. Proběhla analýza dostupných řešení, padlo i několik návrhů kompletně se oprostit od aktuálního řešení a zbavit se AWStats. Nicméně, změnit systém výpočtu statistik nám nakonec přišlo jako zbytečná komplikace a nikomu se nám do toho úplně nechtělo. Výsledkem byl výběr řešení v podobě kombinace Fluent Bit + Fluentd + AWStats.
Věděli jsme tedy, co chceme použít na sběr access logů (Fluent Bit), kde je budeme chtít hromadit a porcovat (Fluentd) a čím je budeme chtít počítat pro zobrazení v grafech uživatelů (AWStats). Nikdo jsme ale vlastně nevěděli vůbec nic o tom, jak s navrženým řešením pracovat. Nicméně, hurá do toho a půl je hotovo.
Ověření konceptu na lokálním prostředí
Vznikl tedy projekt s cílem seznámit se s vybraným řešením a otestovat, že to dáme . Abychom co nejvíce omezili nechtěnou změnu výpočtu statistik, rozhodli jsme se, že nechceme moc zasahovat do konfigurace AWStats. Ideálně abychom jen převzali současné konfigurační soubory a použili je v novém řešení. První chybka.
Výsledkem PoC byla jednoduchá Docker kompozice, ve které jsme testovali celý proces zpracování access logů. Nejednalo se o nic extra složitého.
Místo webserveru jsme si napsali Pythoní skript využívající balíček Faker, kterým jsme generovali access logy stejně, jak by to dělal Nginx. Výhodou bylo, že jsme nemuseli řešit konfiguraci skutečného webserveru a generování falešného provozu na něj.
Na access logy vygenerované falešným webserverem byl pověšen Fluent Bit, který nedělal nic jiného, než že přečetl každý zápis do access logu a přeposlal jej do Fluentd. N avíc pěkně rozparsované a jako JSON. Tohle fungovalo od začátku pěkně a nečekali jsme tady žádné komplikace.
Dalším článkem řetězu pak byl Fluentd, jehož jediným účelem mělo být shrábnout logy obdržené z Fluent Bitu, roztřídit je do oddělených souborů podle projektů ke kterým náleží. A ty si následně přebere AWStats pro výpočet statistik.
To už ale začínalo vyžadovat trochu úsilí, protože ze samotného access logu není šance poznat, k jakému projektu jej přiřadit. Jeden projekt může mít více domén apod. Naštěstí Fluentd podporuje možnost napsat si vlastní plugin. A ten se může např. připojit k databázi a podle domény si vytáhnout k jakému projektu je doména přiřazena. Pluginy pro Fluentd jsou psány v Ruby, se kterým jsme neměli nikdo zkušenost, ale to přece pro programátory nemůže být překážka, že? Nebyla.
S tímto procesem jsme ale museli nutně vyřešit i to, že jsme si nechtěli zahltit SQL databázi dotazy pro každý záznam z access logu. To jsme vyřešili přidáním Redisu jako cache, do které jsme začali ukládat key=value páry v podobě doména=projekt. Vzhledem k tomu, že se jednalo jen o test, tak jsme moc neřešili nastavení samotného Redisu. Spíš vůbec. Ani jsme neřešili, jestli chceme Redis jako finální řešení pro cache. Prostě jsme jej použili, protože to šlo snadno a rychle. Druhá chybka.
Takto nám Fluentd začal porcovat access logy podle našich potřeb a ukládat je do souborové struktury rozdělené podle identifikátorů projektů, téměř shodné s aktuálně používaným nastavením AWStats a tím pádem bylo potřeba jen lehce změnit cesty v konfiguraci. O problém míň, mysleli jsme si. Ó, jak jsme byli bláhoví.
Zbývajícím článkem v řadě tedy bylo už jen prohnat roztříděné access logy samotným AWStats. V jejich FAQ je krásná informace o tom, že AWStats umí zpracovat téměř jakýkoliv formát logu. Jedinou skutečnou podmínkou je, že log musí obsahovat požadovanou informaci. Zpracovat tedy JSON, který nám teď z Fluentd přicházel místo původního Apache2 formátu, by neměl být žádný problém. Neměl by. Ale byl. A až takový, že se nám prostě nepodařilo v konfiguraci AWStats nadefinovat formát logu tak, aby to ten JSON zpracovalo. V tomhle nám pomohly možnosti Fluentd a vyřešili jsme to formátovačem na výstupu z Fluentd, kterým to převádíme z JSONu zpět na formát Apache2.
Aby to správně fungovalo, tak AWStats musí vědět, k jakému projektu patří access logy a kde je má hledat. To je definováno v konfiguračních souborech, které od počátku věků (pořád těch našich) máme definovány pro každý projekt zvlášť. Ty jen staticky leží na disku, kde si je následně AWStats vyhledá a podle cesty v nich definované zpracovává access logy. Toto jsme zachovali i pro nové řešení, jen jsme se rozhodli tyto konfigurační soubory ukládat v samostatné složce, kde budou všechny pohromadě. V původním řešení byly vždy ve složce s access logy pro daný projekt.
Pak už jsme do PoC přidali jen náhražku cronu, který se v produkčním řešení stará o spouštění AWStats. Vyřešili jsme to jednoduchým shellovým skriptem, který v nekonečné smyčce volá AWStats následovaný krátkým sleepem.
Na lokále to fungovalo krásně a všechno vypadalo zeleně. Superzeleně. Na vstupu byl access log ve formátu Apache2 a na výstupu vypočítané statistiky přístupů pro jednotlivé projekty ve formátu AWStats. Naši uživatelé by tedy neměli ve svých grafech vidět žádný rozdíl. Skvělé!
Ověření konceptu v Kubernetes
Začali jsme tedy řešit ověření funkčnosti v K8s. V prvním kole se ještě jednalo o malý testovací cluster, oddělený od produkce a umožňující jen instalaci nových projektů. Tam jsme si otestovali propojení kontejneru Fluent Bit s Nginx ve společném podu a jejich následné škálování. Neřešili jsme škálování CronJobů s AWStats. V tomto místě jsme totiž čekali další komplikace a rozhodli se řešení nechat na naše budoucí chytřejší já.
Protože v K8s není jen tak něco persistentní, museli jsme vyřešit, kam dlouhodobě ukládat konfigurace, stávající a nově vypočtené statistiky. Rozhodnutí padlo na S3 bucket, protože umožňuje přístup z obou prostředí, jak on-premise tak i EKS. Během migrace se tam mohou přesunout existující konfigurační soubory a statistiky. AWStats si odtud získá konfiguraci a vygeneruje nově vypočítané statistiky, které následně přesune do složky projektu v bucketu. Aplikace si z bucketu statistiky přečte, když je má zobrazit uživateli. To znělo skvěle.
Najednou jsme ale museli řešit, že jsme pro jeden projekt generovali klidně více souborů s access logy než jeden. S tím se moc nepočítalo v původních konfiguracích AWStats, které jsme používali, ale naštěstí to šlo vyřešit způsobem, který nabízí přímo AWStats. Místo toho, aby byl v konfiguraci AWStats uveden konkrétní soubor s access logy, uloží se tam cesta k Perlovskému skriptu doplněna cestou ke složce se všemi souvisejícími access logy. Výstupem onoho Perlovského skriptu je pak jeden soubor obsahující sloučené access logy ve správném pořadí podle času.
Tak, to bychom měli ověřený a otestovaný koncept v K8s. Třetí chybka.
První testy v AWS
Přišel čas implementace do testovacího EKS. Tam už probíhaly testy dalších částí aplikace, takže už jsme tam měli rozběhané např. pody s Nginxem odbavující provoz aplikace s testovacími projekty.
Tak trochu bezmyšlenkovitě jsme začali přejímat věci z PoC a implementovat je do testovacího prostředí. Fluent Bit jsme tedy přihodili k Nginxu do podu odbavujícího aplikaci. Rozjeli jsme tam nové pody pro další potřebné části. Jako Deployment byl v jednom podu spuštěn Fluentd + Redis. AWStats jsme se rozhodli spouštět kubernetím CronJobem. Škálování AWStats jsme opět odložili na naše budoucí chytřejší já. Tušili jsme sice, že by to tam mohlo drhnout, ale nikdo jsme neměli představu, jak moc. A mělo by to přece zvládnou minimálně to stejné, co na on-premu, ne?
Otestovali jsme, že access logy se nám zpracovávají. Statistiky návštěvnosti pro nové projekty to v grafech ukazovalo, všechno se jevilo v pohodě.
Migrace prvních produkčních projektů do AWS
Dozrál čas, naše aplikace už byla nějakou dobu v testovacím provozu na EKS clusteru v AWS a vše se jevilo OK. Padlo tedy rozhodnutí začít migrovat první projekty uživatelů do AWS. Nejsme úplně bezmozci, takže jsme se rozhodli si stanovit pár pravidel, jaké projekty budeme migrovat, abychom minimalizovali případné škody. Omezili jsme to pouze na Free projekty z trhů, kde nám moc nedochází ke konverzím na placené verze a kde ani není moc velká návštěvnost. Začali jsme malou dávkou jednoho tisíce projektů a následně se jali migrovat dál.
Jakmile jsme dosáhli hranice cca 4 tisíců projektů, zpracovávání statistik návštěvnosti totálně zkolabovalo. Tak tohle nikdo nečekal! Rozhodně ne tak brzo. Navíc, všechny předchozí testy byly prováděny na nových projektech a tak nějak se pozapomnělo na to, že je potřeba nejdříve z bucketu stáhnout existující statistiku pro aktuální měsíc. Díky této chybě zmizela statistika pro 20 dnů. Naštěstí, vhodným výběrem migrovaných projektů se jednalo o počty návštěv v řádu jednotek.
Nostra culpa, nostra maxima culpa
Nastal čas na revizi stávající řešení a hledání, kde je zakopaný pes. Ukázalo se, že jsme některé testy neměli úplně dobře podchycené. V průběhu PoC a testů nám například chyběly v konfiguraci Fluentd definice některých subdomén pro správné "porcování logů".
Během testů v PoC nám moc nefungovalo ani škálování Fluentd. Sice se v Dockeru vytvářely nové kontejnery, ale ty spíš jen hromadily chyby a reálně fungovala jen jedna instance.
Také jsme došli k prozření, že provádět hromadné operace nad tisícovkami souborů v připojeném S3 bucketu v EKS je neskutečně pomalé. Už jen procházení AWStats konfigurací pro všechny projekty trvalo neskutečně dlouho. Mít tedy miliony konfiguračních souborů pro všechny projekty, stejně jako na současném on-premise řešení, už najednou nebyla ta správná cesta.
A v neposlední řadě se nám do produkčního clusteru dostal i Redis, u kterého jsme na začátku moc neřešili jeho konfiguraci. A tím pádem jsme tak nějak zapomněli nastavit TTL pro záznamy v cache. To by nám v budoucnu mohlo způsobit problémy např. po přesunu domény k jinému projektu. Statistiky návštěvnosti by se pořád počítaly k tomu původnímu, dokud by někdo Redis cache neinvalidoval.
(Téměř) finální řešení
Najít nové řešení zabralo celý týden, včetně "pracovního" víkendu. Abychom to rozchodili, museli jsme udělat několik změn jak v infrastruktuře, tak v návrhu procesu zpracování access logů.
Fluentd sice nezpůsoboval žádné komplikace, ale zůstal opomenutý jen jako Deployment s jedním podem. Předělali jsme to na DaemonSet, díky čemuž se nám teď automaticky škáluje podle počtu nodů (běží právě jedna instance na nodu). Redis zůstal zatím stále ve stejném podu s Fluentd, ale bude jej potřeba vytáhnout do samostatné služby využívané společně všemi instancemi Fluentd.
Upravili jsme souborovou strukturu výstupu z Fluentd. Nahradili jsme dříve použitý identifikátor projektu jeho projektovou doménou. AWStats jsme překonfigurovali tak, aby nezpracovával access logy procházením konfiguračních souborů, ale naopak aby procházel všechny složky s access logy v námi definované cestě. Tím, že je nyní v názvu složky projektová doména, tak si AWStats může konfiguraci vytvořit dynamicky až v okamžiku kdy ji potřebuje. Díky tomu jsme mohli zrušit statické konfigurace AWStats pro projekty a nebudou se nám válet na S3 miliony zbytečných souborů.
Změnili jsme i samotné zpracování access logů tak, že napřed připravíme sloučený soubor access logů a až ten se předhodí ke zprocesování do AWStats. Ten Perlovský skript, dříve spouštěný procesem AWStats, se prostě spustí ještě před ním a vytvoří jeden soubor. Jakmile je pak tento soubor zpracován v AWStats, přesune se do archivu a pokud mezitím nevznikl další access log pro daný projekt (tedy pokud neproběhla během zpracování nová návštěva), tak se smaže i celá složka projektu. Tím se sníží množství složek pro zpracování v dalším běhu CronJobu.
Navíc, na začátku zpracování každé složky s access logy projektu, se do ní vloží soubor identifikující, že tato složka se právě zpracovává. Díky tomu je teď možné pustit více procesů AWStats najednou. Ano, naše budoucí chytřejší já konečně vyřešila škálování podů s AWStats. Úspěšně jsme otestovali škálování spuštěním čtyř současně bežících procesů AWStats.
Popsanými změnami se nám podařilo vyřešit stav, kdy nám zpracování access logů v EKS kolabovalo. Tím, že jsme nahradili původní statické konfigurace AWStats zpracováním složek existujících jen a pouze pro projekty, které skutečně měly nějakou návštěvu od posledního zpracování, jsme silně optimalizovali množství operací související se soubory v S3 bucketu. Touto optimalizací jsme zároveň výrazně zrychlili běh celého zpracování access logů. Z dlouhých desítek minut v případě 4 tisíc projektů, jsme se dostali na jednotky minut pro cca 150 tisíc projektů v době psaní článku .
Závěrem
Popsané řešení ještě není úplně dokončené. Zbývá nám několik věcí k dodělání. Např. zmíněné oddělení Redisu, dořešit vnitřní škálování Fluentd pomocí workerů a vytvoření nějakého alertingu, ať máme větší přehled, jestli nám to třeba zase nekolabuje. Také bychom mohli zlepšit některé věci kolem lokálního vývoje pro K8s a testování, ať se příště vyvarujeme některých zbytečných chyb.
Jak jsme psali na začátku. Byla to výzva. Výzva, ve které jsme se nevyvarovali zbytečných chyb a která nám přinesla nejednu těžkou chvilku a uvaření mozku při řešení zhrouceného zpracování statistik. Také jsme si praxí ověřili, že v cloudu je prostě spousta věcí naprosto rozdílná proti tomu, na co jsme zvyklí z on-premise řešení. Je zkrátka nutné myslet trochu jinak. Ano, ona otřepaná fráze "Think different" zde stoprocentně platí.
Na druhou stranu, nyní už víme, jak ve skutečnosti zpracování statistik funguje. Umíme nově statistiky uživatelům generovat téměř real-time a neplýtváme prostředky pro přepočty statistik u webů, kde k návštěvám nedošlo.
A navíc, koho by bavilo dělat věci, které jsou snadné, ne?
Za Webnode Hynek Lípa a Tomáš Vaverka