Inženýrský blog - Prexy: superrychlá pravidla pro vyhledávání a nahrazování

Vítejte na našem prvním inženýrském blogu. Je možná trochu techničtější, než jste zvyklí z jiných blogů, ale snažili jsme se, aby byl srozumitelný pro každého. V tomto článku budeme hovořit o Prexy, novém softwaru v rámci Clonable, který slouží k aplikaci substitučních pravidel.

Pozadí

Jako uživatelé Clonable již možná znáte funkci pravidel nahrazování. Pomocí těchto pravidel pro vyhledávání a nahrazování můžete vyměnit části textu nebo kódu za vlastní vybranou variantu. Můžete tak například nahradit klíč API nebo ID analytiky, abyste mohli vytvořit odlišnou analytiku pro původní web a přeložené klony. Interně se stejná pravidla používají také k nahrazení názvu původní domény názvem domény vašeho klonu a k řadě dalších věcí.

Od první verze Clonable nebyla tato funkce nikdy aktualizována, což samo o sobě není překvapivé, protože svou práci plnila dobře. Přesto bylo možné provést vylepšení, a to jak z hlediska výkonu, tak z hlediska uživatelské přívětivosti. Jednou za čas vezmeme Clonable kus našeho produktu, který se dlouho nepoužíval, a začneme ho vylepšovat. V loňském roce jsme například přepracovali celou naši datovou infrastrukturu, aby byla rychlejší, škálovatelnější a odolnější vůči chybám, a předtím jsme také od základu přestavěli funkci překladu adres URL. V tomto čtvrtletí přišla řada na pravidla substituce.

Stará situace a omezení

Pravidla nahrazování se v Clonable uplatňují až v poslední fázi, těsně před odesláním odpovědi zpět klientovi. V první verzi Clonable jsme se proto rozhodli implementovat je v rámci webového serveru NGINX pomocí modulu replace-filter-nginx-module, který vytvořila společnost OpenResty. Modul používá jako svůj streamovací regexový engine sregex, rovněž vytvořený společností OpenResty. Aspekt streamování je v tomto případě důležitý, protože nechceme každou odpověď načítat celou do paměti. Pokud bychom tak učinili, mohlo by množství velkých odpovědí způsobit pád nebo zhroucení systému NGINX, protože by nebyl k dispozici dostatek paměti. Druhou důležitou vlastností sregexu je, že dokáže paralelně zpracovávat více řádků. Je to proto, že každý klon již má některá výchozí pravidla a někdy navíc některá pravidla přidaná speciálně pro daný klon. Pokud by je nebylo možné zpracovávat paralelně, museli bychom celou odpověď stále ukládat do vyrovnávací paměti, protože bychom ji museli být schopni po prvním pravidle znovu porovnat pro druhé pravidlo.

V loňském roce jsme se však stále častěji setkávali s podivnými výkonnostními problémy. Tyto problémy byly často krátkodobé, ale v extrémních případech mohly prodloužit dobu odezvy stránky o několik sekund. Po jednom takovém výkyvu se stránka často opět normálně načetla, takže bylo obtížné problém reprodukovat. Abychom mohli tento problém dále ladit, přidali jsme ke všem odpovědím hlavičku Clonable-Timings, která nám umožnila přibližně určit, který krok procesu překladu zabral tolik času. To odhalilo, že téměř ve všech případech reagoval upstream rychle a překlad byl také poměrně plynulý. Mezi dokončením překladu a úplným dokončením požadavku však byla mezera, což nám napovědělo, že by to mohlo mít co do činění s pravidly nahrazování.

Souběžný model NGINX

To nám však ještě nedalo odpověď na otázku, proč k těmto zpožděním docházelo jen sporadicky a proč se tak dělo, když servery nebyly vytíženy ani na polovinu své kapacity. Abychom tento jev lépe pochopili, musíme se podívat trochu hlouběji na to, jak NGINX zpracovává pracovní zátěž.

NGINX má architekturu worker s jedním hlavním procesem a několika pracovními procesy. Připojení jsou rozdělena mezi pracovní procesy, aby bylo možné zpracovávat více požadavků současně. Toto uspořádání má však také nevýhodu: když je worker zaneprázdněn zpracováním požadavku, ostatní požadavky, které jsou rovněž přiřazeny stejnému workeru, musí čekat. To může způsobit, že jeden náročný požadavek způsobí zpoždění více požadavků, i když ostatní pracovníci nemají nic na práci. Tento efekt je dobře vidět na obrázku níže. Během vytváření tohoto obrázku probíhá zátěžový test přes 10 různých připojení. Na obrázku je pak jasně vidět, že tři pracovníci jsou vytíženi a čtvrtý nedělá téměř nic.

Čas pro Prexy

Když bylo úzké místo jasně viditelné, vytvořili jsme plán na jeho zlepšení. Tento projekt jsme pojmenovali Prexy, což je spojení slov RegEx a Proxy. Na konečný výsledek byly kladeny 3 hlavní požadavky:

  1. Měl by být drop-in náhradou za stávající nastavení. Pravidla pro nahrazení by měla být použita stejným způsobem, aby nedošlo k rozbití věcí ve stávajících nastaveních.

  2. Pravidla substituce musí být streamována, aby řešení nevyužívalo příliš mnoho paměti.

  3. Nové řešení by mělo být rychlejší než stávající.

Nejprve jsme museli hledat výkonný vícevláknový runtime, který by dokázal efektivně zpracovávat požadavky. Po zvážení několika možností padla volba na runtime Tokyo pro programovací jazyk Rust. Jazyk Rust je známý tím, že umožňuje psát rychlé programy bez bezpečnostních rizik jiných nízkoúrovňových jazyků, jako je například C++. Runtime Tokyo je flexibilní asynchronní runtime vytvořený pro síťové aplikace. Jednou z pro nás nejdůležitějších vlastností je skutečnost, že je práce kradená. To znamená, že na rozdíl od NGINX není požadavek vázán na jednoho pracovníka, ale pracovník, který nemá co dělat, může "ukrást" práci jinému pracovníkovi. Tímto způsobem lze lépe využít prostředky našich serverů.

První prototyp

Po výběru základních technologií jsme se rozhodli vytvořit počáteční prototyp, abychom zhruba odhadli, jak velký nárůst rychlosti nám tento projekt přinese a zda se vůbec vyplatí. Jako počáteční implementaci pro regex engine (část, která skutečně zpracovává a aplikuje pravidla) jsme použili standardní regex knihovnu (nebo crate, jak se jim říká v terminologii Rust). Tento engine je stejně jako sregex non-backtracking, což znamená menší riziko problému s ReDoS. V případě ReDoS čelí regulární výraz takovému vstupu, že čas potřebný k jeho vyhodnocení exponenciálně roste. To může mít katastrofální dopad na výkon a způsobilo to velký výpadek služby cloudflare. Proto je pro nás důležité, aby použitý engine nebyl backtrackingový.

Pomocí tohoto regexového enginu jsme vytvořili počáteční prototyp. Tento prototyp používal pevně zakódovaná pravidla a regexový engine celou odpověď ukládal do vyrovnávací paměti namísto streamování, ale pro počáteční test výkonu to stačilo.

Pomocí tohoto regexového enginu jsme vytvořili počáteční prototyp. Tento prototyp používal pevně zakódovaná pravidla a regexový engine celou odpověď ukládal do vyrovnávací paměti namísto streamování, ale pro počáteční test výkonu to stačilo. Naše testovací sestava se skládala ze dvou virtuálních počítačů, přičemž na jednom virtuálním počítači byla spuštěna sestava, na které mohlo běžet jak staré, tak nové řešení. Tímto způsobem jsme mohli snadno porovnávat obě metody. Na druhém serveru běžel server NGINX, který sloužil jako server původu a obsluhoval testovací soubor. Na stejném serveru běžel také nástroj wrk, který jsme použili ke generování zátěže. To není úplně optimální, protože pro čistá data je lepší spustit generátor zátěže na samostatném virtuálním počítači, ale tento virtuální počítač měl dostatek prostředků, takže se nástroje NGINX a wrk navzájem nerušily.

První výsledky testů byly naprosto jasné: Prexy byl při zpracování jednoho požadavku přibližně 22krát rychlejší (viz obrázek níže). To dalo projektu definitivní zelenou, protože při tak velkém rozdílu byl dostatečný prostor pro absorbování některých výkonnostních poklesů, které by mohly nastat s doplněním funkčnosti.

Po počátečním testování jsme implementovali několik zjevných optimalizací, například ukládání zkompilovaných regulárních výrazů do mezipaměti. Implementovali jsme také efektivnější způsob opakovaného použití připojení k upstreamu. Díky tomu jsme dosáhli zvýšení rychlosti přibližně o 20 %. Poté jsme obě řešení testovali také při špičkovém zatížení, kdy jsme na server posílali co nejvíce požadavků. I zde byl rozdíl jasně patrný a Prexy dosáhlo asi 25x vyšší propustnosti než staré řešení.

Streaming regex engine

Jedním z požadavků tohoto projektu bylo, aby použitý regex engine byl streamovací. Vytváření regexů v prototypu není, takže v této oblasti bylo nutné provést další práci. Ukázalo se však, že regex crate je vysoce optimalizovaný, takže jsme se rozhodli vzít tuto implementaci jako základ pro náš streamovací engine, i když. Při streamování dat prostřednictvím regexového enginu je třeba mít na paměti několik věcí. Za prvé, nevíte, jaká data ještě přijdou. Měli byste proto začít pracovat s částečnými shodami: se shodami, které ještě nejsou úplné, ale již odpovídají 1 nebo více znakům. Při hledání úplné shody pak musíte zkontrolovat, zda se nepřekrývají částečné shody, protože ty mohou být nakonec také shodami (a v případě překrývání vítězí delší shoda nad kratší). Shodu tedy můžete začít zpracovávat pouze tehdy, pokud se ukáže, že všechny aktuálně nalezené částečné shody nejsou úplnou shodou.

Další optimalizace

Jak se dalo očekávat, přestavba regexového enginu měla na výkon Prexy negativní dopad. Oproti starému řešení však stále zůstávala velká rezerva a přidáním dalších optimalizací jsme se dostali opět na ještě vyšší úroveň než před přestavbou regexového enginu. Jednou z optimalizací, kterou jsme použili, bylo zapamatování si, zda se má pravidlo použít na konkrétní soubor. Statické prostředky, jako jsou soubory CSS a JS, se téměř nikdy nemění, a proto je zbytečné pokaždé provádět pravidlo, když se nikdy neshoduje s tímto konkrétním souborem. Alternativou k tomu bylo ukládání výsledků nahrazení do mezipaměti, ale nevýhodou této strategie je, že ukládání všech souborů zabere hodně paměti. Tím, že si pamatujeme, která pravidla se mají použít, potřebujeme jen několik bajtů na soubor, protože informace ukládáme jako bitovou mapu.

Kromě toho plně využíváme optimalizace v rámci regexového enginu, například tím, že neuchováváme zachycující skupiny, pokud nejsou při nahrazování použity. Díky tomu si regex engine musí pamatovat pouze začátek a konec celé shody, což šetří práci. Vypnuli jsme také pojmenované zachycující skupiny, protože to bylo v proudové variantě poměrně složité, a protože to staré řešení také nepodporovalo, nebylo to nezbytně nutné. Všechny tyto optimalizace nakonec zajistily následující výkon:

Konečný výsledek

Po rozsáhlém testování služby Prexy, abychom se ujistili, že její chování je skutečně stejné jako u starého řešení, jsme ji začali postupně zavádět. V průběhu zhruba jednoho týdne jsme aktivovali Prexy pro každý klon. Při našem monitorování bylo často snadno zjistitelné načasování aktivace. Nezřídka bylo vidět snížení doby odezvy o přibližně 50 %. Drobné rozdíly kolem 10 % byly pozorovány i u dalších zákazníků, kteří měli velmi málo pravidel pro záměnu. Naše vlastní webové stránky se zrychlily přibližně o 30 % (~50 ms).

V celé naší infrastruktuře vidíme po zavedení Prexy následující rozdíly.

  • O 33 % nižší spotřeba paměti RAM

  • O 20 % nižší spotřeba procesoru

  • O 10-50 % rychlejší odezvy

Celkově tedy můžeme říci, že Prexy byl úspěšný projekt. Díky nahrazení zastaralého modulu NGINX můžeme nyní používat novější a efektivnější techniky, které mají pro všechny naše zákazníky jasně měřitelný přínos. V budoucnu budeme Clonable nadále vylepšovat, a to jak z hlediska nových funkcí, tak z hlediska vylepšování stávajících funkcí.


Děkujeme, že jste si přečetli tento inženýrský blog. Dejte nám vědět, pokud byste chtěli mít podobné technické informace o našem produktu častěji. Nadchl vás tento projekt? Pak se podívejte na naši stránku s volnými pracovními místy :)

Začněte ještě dnes zdarma s Clonable

Zjistěte, jak centrálně spravovat mezinárodní webové stránky z jedné platformy.

Používáme soubory cookie ke zlepšení vašeho uživatelského zážitku, analýze návštěvnosti webových stránek a zdokonalení našich služeb.

Přečtěte si naše zásady používání souborů cookie