Technický blog – Prexy: bleskovo rýchle pravidlá vyhľadávania a nahradzovania

Vitajte na našom prvom technickom blogu. Možno je trochu technickejší, než ste zvyknutí z iných blogov, ale snažili sme sa, aby bol zrozumiteľný pre každého. V tomto článku sa budeme venovať Prexy, novému softvéru v rámci Clonable , ktorý sa používa na uplatňovanie pravidiel nahradzovania.

Pozadie

Ak ste používateľom služby Clonable , pravdepodobne už poznáte funkciu pravidiel nahrádzania. Tieto pravidlá vyhľadávania a nahrádzania môžete použiť na výmenu častí textu alebo kódu za vlastnú variantu. Môžete to napríklad využiť na nahradenie API kľúča alebo analytického ID, aby ste mohli vytvárať rôzne analýzy pre vašu zdrojovú webovú stránku a preložené klony. Interné systémy používajú rovnaké pravidlá aj na nahradenie pôvodného názvu domény názvom domény klonu a na niekoľko ďalších operácií.

Od úplne prvej verzie Clonable sa táto funkcia nikdy neupravovala, čo samo o sebe nie je prekvapujúce, pretože svoju úlohu plnila výborne. Napriek tomu bolo možné urobiť vylepšenia, a to tak v oblasti výkonu, ako aj v oblasti užívateľskej prívetivosti. Raz za čas sa v Clonable zameriavame na časť nášho produktu, ktorej sme už dlho nevenovali pozornosť, aby sme ju vylepšili. Minulý rok sme kompletne prehodnotili našu dátovú infraštruktúru, aby bola rýchlejšia, škálovateľnejšia a odolnejšia voči chybám, a v rámci toho sme tiež od základov prebudovali funkciu prekladu URL. V tomto štvrťroku prišli na rad pravidlá nahradzovania.

Predchádzajúca situácia a obmedzenia

Pravidlá nahradzovania sa v Clonable uplatňujú až v úplne poslednej fáze, tesne pred odoslaním odpovede klientovi. V prvej verzii Clonable sme sa preto rozhodli implementovať to v rámci webového servera NGINX pomocou modulu replace-filter-nginx, ktorý vytvorila spoločnosť OpenResty. Modul používa sregex ako streamovací regulárny výrazový engine, ktorý takisto vytvorila spoločnosť OpenResty. Streamovací aspekt je v tomto prípade dôležitý, pretože nechceme načítať každú odpoveď úplne do pamäte. Ak by sme to urobili, niekoľko veľkých odpovedí by mohlo spôsobiť pád alebo zaseknutie NGINXu z dôvodu nedostatočnej pamäte. Druhou dôležitou vlastnosťou sregexu je, že dokáže spracovávať viacero pravidiel paralelne. Každý klon totiž už obsahuje niekoľko štandardných pravidiel a okrem toho niekedy aj niekoľko pravidiel, ktoré boli pridané špecificky pre daný klon. Ak by sa tieto pravidlá nedali spracovať súčasne, museli by sme aj tak vyrovnávaciu pamäť celej odpovede, pretože po prvom pravidle by sme museli prejsť odpoveďou ešte raz, aby sme mohli spracovať druhé pravidlo.

V uplynulom roku sme však čoraz častejšie narážali na podivné problémy s výkonom. Tieto problémy boli často krátkodobé, ale v extrémnych prípadoch mohli predĺžiť dobu odozvy stránky o niekoľko sekúnd. Po jednom takomto výkyve sa stránka často opäť načítala normálne, čo sťažovalo reprodukciu problému. Aby sme to mohli ďalej odlaďovať, pridali sme do všetkých odpovedí hlavičku Clonable-Timings, čo nám umožnilo zhruba určiť, ktorý krok prekladového procesu zaberal toľko času. Z toho vyplynulo, že takmer vo všetkých prípadoch upstream reagoval rýchlo a že aj preklad prebiehal pomerne plynulo. Medzi dokončením prekladu a úplným dokončením požiadavky však bola medzera, čo nám naznačilo, že by to mohlo súvisieť s pravidlami substitúcie.

Model súbežnosti NGINX

To nám však ešte nedalo odpoveď na otázku, prečo sa tieto oneskorenia vyskytovali len sporadicky a prečo sa to dialo, keď servery neboli zaťažené ani na polovicu svojej kapacity. Aby sme tento jav lepšie pochopili, musíme sa podrobnejšie pozrieť na to, ako NGINX spracováva pracovné zaťaženie.

NGINX má architektúru typu „worker“ s jedným hlavným procesom a viacerými pracovnými procesmi. Pripojenia sa rozdeľujú medzi pracovníkov, aby bolo možné spracovávať viacero požiadaviek súčasne. Táto konfigurácia má však aj nevýhodu: keď je pracovník zaneprázdnený spracovaním jednej požiadavky, ostatné požiadavky, ktoré sú tiež pridelené tomuto istému pracovníkovi, musia čakať. To môže spôsobiť, že jedna náročná požiadavka môže spôsobiť oneskorenie viacerých požiadaviek, aj keď ostatní pracovníci nemajú čo robiť. Tento efekt je dobre viditeľný na nižšie uvedenom obrázku. Počas vytvárania tohto obrázku prebiehal záťažový test na 10 rôznych pripojeniach. Na obrázku je jasne vidieť, že 3 pracovné procesy sú plne vyťažené, zatiaľ čo štvrtý takmer nič nerobí.

Čas na Prexy

Keďže sme jasne identifikovali úzke miesto, vypracovali sme plán na jeho odstránenie. Tento projekt dostal názov Prexy, čo je spojenie slov RegEx a Proxy. Na konečný výsledok boli kladené tri hlavné požiadavky:

  1. Malo by to byť priama náhrada za súčasnú konfiguráciu. Pravidlá nahradenia sa musia uplatňovať rovnakým spôsobom, aby sa zabránilo poruchám v existujúcich konfiguráciách.

  2. Pravidlá nahradzovania sa musia uplatňovať priebežne, aby sa zabránilo nadmernému využívaniu pamäte riešením.

  3. Nové riešenie musí byť rýchlejšie ako súčasné riešenie.

Najskôr sme museli hľadať výkonné viacvláknové runtime prostredie, ktoré dokáže efektívne spracovávať požiadavky. Po zvážení viacerých možností sme sa rozhodli pre Tokio runtime pre programovací jazyk Rust. Rust je známy tým, že s ním možno písať rýchle programy bez bezpečnostných rizík, ktoré sú typické pre iné nízkoúrovňové jazyky, ako je C++. Runtime Tokio je flexibilné asynchrónne prostredie vytvorené pre sieťové aplikácie. Jednou z najdôležitejších funkcií pre nás je skutočnosť, že podporuje „work-stealing“. To znamená, že na rozdiel od NGINX nie je požiadavka viazaná na jedného pracovníka, ale pracovník, ktorý nemá čo robiť, môže „ukradnúť“ prácu inému pracovníkovi. Týmto spôsobom je možné lepšie využívať zdroje našich serverov.

Prvý prototyp

Po výbere základných technológií sme sa rozhodli vytvoriť prvý prototyp, aby sme odhadli, aký približný nárast rýchlosti nám tento projekt prinesie a či sa to vôbec oplatí. Ako prvú implementáciu pre regex engine (časť, ktorá skutočne spracováva a aplikuje pravidlá) sme použili štandardnú regex knižnicu (alebo crate, ako sa v terminológii Rustu nazýva). Tento engine je, rovnako ako sregex, bez spätného sledovania (non-backtracking), čo znamená, že je menšie riziko výskytu problému typu ReDoS. Pri ReDoS sa regulárny výraz stretne s takým vstupom, že čas potrebný na jeho vyhodnotenie exponenciálne stúpa. To môže mať katastrofálny vplyv na výkon a v prípade spoločnosti Cloudflare to viedlo k závažnej poruche. Pre nás je teda dôležité, aby engine, ktorý používame, bol typu „non-backtracking“.

Pomocou tohto modulu na regulárne výrazy sme vytvorili prvý prototyp. Tento prototyp používal pevne zakódované pravidlá a modul na regulárne výrazy ukladal celú odpoveď do vyrovnávacej pamäte namiesto jej streamovania, ale na prvý test výkonu to stačilo. 

Pomocou tohto regexového modulu sme vytvorili prvý prototyp. Tento prototyp používal pevne zakódované pravidlá a regexový modul ukladal celú odpoveď do vyrovnávacej pamäte namiesto jej streamovania, ale na prvý test výkonu to stačilo. Naše testovacie prostredie pozostávalo z dvoch virtuálnych strojov (VM). Na jednom VM bežalo prostredie, v ktorom bolo možné spustiť aj staré, aj nové riešenie. Takto sme mohli ľahko porovnať obe metódy. Na druhom serveri bežal NGINX, ktorý slúžil ako originálny server a poskytoval testovací súbor. Na tom istom serveri bežal aj nástroj wrk, pomocou ktorého sme generovali zaťaženie. Nie je to úplne optimálne, keďže pre čisté údaje je najlepšie spustiť generátor zaťaženia na samostatnom virtuálnom stroji, ale tento virtuálny stroj mal dostatok zdrojov, aby si NGINX a wrk navzájom neprekážali.

Prvé výsledky testov boli jednoznačné: Prexy spracoval jednu požiadavku približne 22-krát rýchlejšie (pozrite obrázok nižšie). To projektu definitívne dalo zelenú, pretože pri takom veľkom rozdiele bol dostatočný priestor na to, aby sa zvládli prípadné poklesy výkonu, ktoré by sa mohli vyskytnúť pri dokončovaní funkcionality.

Po prvom teste sme vykonali niekoľko zrejmých optimalizácií, ako napríklad ukladanie kompilovaných regulárnych výrazov do vyrovnávacej pamäte. Okrem toho sme implementovali efektívnejší spôsob opätovného využitia pripojení k upstreamu. Tým sme dosiahli zvýšenie rýchlosti o približne 20 %. Následne sme obe riešenia otestovali aj pri špičkovom zaťažení, pričom sme na server odosielali čo najviac požiadaviek. Aj v tomto prípade bol rozdiel jasne viditeľný a Prexy dosiahol priepustnosť približne 25-krát vyššiu ako staré riešenie.

Streamingový regex engine

Jednou z požiadaviek tohto projektu bolo, aby bol používaný regex engine streamovací. Regex engine z prototypu ním nebol, takže v tejto oblasti bola potrebná dodatočná práca. Ukázalo sa však, že tento regex engine je veľmi optimalizovaný, a tak sme sa rozhodli použiť túto implementáciu ako základ pre náš streamovací engine. Pri streamovaní dát cez regex engine je dôležité mať na pamäti niekoľko vecí. Po prvé, neviete, aké údaje ešte prídu. Preto musíte pracovať s čiastočnými zhodami: zhodami, ktoré ešte nie sú kompletné, ale už sa zhodujú s 1 alebo viacerými znakmi. Pri nájdení kompletnej zhody je potom potrebné skontrolovať, či neexistujú prekrývajúce sa čiastočné zhody, pretože tie sa nakoniec môžu tiež zhodovať (a v prípade prekrývania má dlhšia zhoda prednosť pred kratšou). Zhodu teda môžete začať spracovávať až vtedy, keď sa ukáže, že žiadna z v danom momente existujúcich čiastočných zhôd nie je úplnou zhodou.

Ďalšie optimalizácie

Prebudovanie modulu regex malo, ako sa dalo očakávať, negatívny vplyv na výkon Prexy. V porovnaní so starým riešením však stále existovala dostatočná rezerva a vďaka ďalším optimalizáciám sme nakoniec dosiahli dokonca vyššiu výkonnosť ako pred prebudovaním modulu regex. Jednou z optimalizácií, ktoré sme uplatnili, bolo zapamätanie si, či sa má pravidlo uplatniť na konkrétny súbor. Statické súbory ako CSS a JS sa takmer nikdy nemenia, a preto je zbytočné zakaždým vykonávať pravidlo, keď sa na daný súbor nikdy nezhoduje. Alternatívou bolo ukladanie výsledkov nahradení do vyrovnávacej pamäte, nevýhodou tejto stratégie však je, že ukladanie všetkých súborov zaberá veľa pamäte. Zapamätaním si, ktoré pravidlá sa majú uplatniť, potrebujeme len niekoľko bajtov na súbor, keďže informácie ukladáme ako bitmapu.

Okrem toho optimálne využívame optimalizácie v rámci regexového modulu, napríklad tým, že nesledujeme zachytávacie skupiny, ak sa nepoužívajú pri nahradzovaní. Vďaka tomu si regexový modul musí pamätať len začiatok a koniec celého zhodného reťazca, čo šetrí prácu. Takisto sme deaktivovali pomenované zachytávacie skupiny, pretože to bolo v streamovanej verzii dosť zložité a keďže staré riešenie to tiež nepodporovalo, nebolo to nevyhnutne potrebné. Všetky tieto optimalizácie nakoniec viedli k nasledujúcim výkonom:

Konečný výsledok

Po dôkladnom otestovaní Prexy, aby sme sa uistili, že sa správa skutočne rovnako ako staré riešenie, sme začali s postupným zavádzaním. V priebehu približne jedného týždňa sme Prexy aktivovali pre každý klon. V našom monitorovacom systéme bol moment aktivácie často jasne viditeľný. Zníženie odozvových časov o približne 50 % nebolo zriedkavé. U iných zákazníkov, ktorí mali len málo pravidiel nahradzovania, sme zaznamenali aj menšie rozdiely v rozsahu približne 10 %. Naša vlastná webová stránka sa zrýchlila približne o 30 % (~50 ms).

Pokiaľ ide o našu celkovú infraštruktúru, po zavedení Prexy pozorujeme nasledujúce rozdiely.

  • O 33 % nižšia spotreba pamäte RAM

  • O 20 % nižšia spotreba CPU

  • O 10–50 % rýchlejšie odpovede

Celkovo teda môžeme konštatovať, že Prexy bol úspešný projekt. Nahradením zastaraného modulu NGINX môžeme teraz využívať novšie a efektívnejšie techniky, ktoré prinášajú jasne merateľný rozdiel pre všetkých našich zákazníkov. Aj v budúcnosti budeme pokračovať vo vylepšovaní Clonable, a to tak v oblasti nových funkcií, ako aj vylepšovaní existujúcich funkcií.


Ďakujeme, že ste si prečítali tento technický blog. Dajte nám vedieť, či by ste radi dostávali častejšie takéto technické pohľady do nášho produktu. Nadchlo vás toto projekt? Pozrite sa aj na našu stránku s voľnými pracovnými miestami :)

Začnite ešte dnes zadarmo s Clonable

Zistite, ako môžete centrálne spravovať medzinárodné webové stránky z jednej platformy.

Používame súbory cookie na zlepšenie vášho zážitku, analýzu návštevnosti webovej stránky a zdokonalenie našich služieb.

Prečítajte si naše zásady používania súborov cookie