Inženirski blog - Prexy: izjemno hitra pravila za iskanje in nadomeščanje

Delite

Dobrodošli na našem prvem inženirskem blogu. Morda je nekoliko bolj tehničen, kot ste ga vajeni iz drugih blogov, vendar smo se potrudili, da bo razumljiv za vse. V tem članku bomo govorili o Prexyju, novem delu programske opreme v programu Clonable, ki se uporablja za uporabo pravil zamenjave.

Ozadje

Kot uporabnik programa Clonable morda že poznate funkcionalnost pravil za zamenjavo. Ta pravila za iskanje in nadomeščanje lahko uporabite za zamenjavo delov besedila ali kode z lastno izbrano različico. To lahko na primer uporabite za zamenjavo ključa API ali analitičnega ID, tako da lahko ustvarite različne analitične podatke za izvirno spletno mesto in prevedene klone. Ista pravila se uporabljajo tudi za zamenjavo imena izvirne domene z imenom domene klona in za številne druge stvari.

Od prve različice Clonable ta funkcionalnost ni bila nikoli posodobljena, kar samo po sebi ni presenetljivo, saj je svoje delo opravljala odlično. Kljub temu so bile možne izboljšave, tako z vidika zmogljivosti kot prijaznosti do uporabnika. Enkrat na čas Clonable vzamemo del našega izdelka, ki se že dolgo ni uporabljal, in ga začnemo izboljševati. Lani smo na primer prenovili celotno podatkovno infrastrukturo, da bi bila hitrejša, bolj skalabilna in odporna proti napakam, pred tem pa smo od začetka prenovili tudi funkcijo prevajanja URL. V tem četrtletju so bila na vrsti pravila zamenjave.

Stare razmere in omejitve

Pravila zamenjave se v Clonable uporabljajo šele v zadnji fazi, tik pred pošiljanjem odgovora nazaj odjemalcu. V prvi različici programa Clonable smo se zato odločili, da to izvedemo v spletnem strežniku NGINX z uporabo modula replace-filter-nginx-module, ki ga je ustvarilo podjetje OpenResty. Modul uporablja sregex kot pretočni mehanizem regex, ki ga je prav tako ustvaril OpenResty. V tem primeru je pomemben vidik pretakanja, saj ne želimo vsakega odgovora v celoti naložiti v pomnilnik. Če bi to storili, bi lahko več velikih odgovorov povzročilo sesutje ali okvaro sistema NGINX, ker ni na voljo dovolj pomnilnika. Druga pomembna lastnost sregexa je, da lahko vzporedno obdeluje več vrstic. To je zato, ker ima vsak klon že nekaj privzetih pravil, poleg tega pa so včasih nekatera pravila dodana posebej za klon. Če teh ne bi mogli obdelati vzporedno, bi morali še vedno v predpomnilnik shraniti celoten odgovor, saj bi ga morali po prvem pravilu za drugo pravilo ponovno ujemati.

Vendar smo se lani vse pogosteje srečevali z nenavadnimi težavami pri delovanju. Te težave so bile pogosto kratkotrajne, vendar so lahko v skrajnih primerih podaljšale odzivni čas strani za nekaj sekund. Po enem takem skoku se je stran pogosto spet normalno naložila, zato je bilo težavo težko ponoviti. Za nadaljnje odpravljanje napak smo vsem odgovorom dodali glavo Clonable-Timings, ki nam je omogočila, da smo približno določili, kateri korak postopka prevajanja je zahteval toliko časa. To je razkrilo, da se je skoraj v vseh primerih višji strežnik odzval hitro, prav tako pa je prevajanje potekalo dokaj gladko. Vendar pa je med dokončanjem prevajanja in popolnim dokončanjem zahteve obstajal razkorak, kar nam je dalo namig, da bi lahko bilo to povezano s pravili nadomeščanja.

Model sočasnosti NGINX-a

Vendar nam to še ni dalo odgovora na vprašanje, zakaj so se te zamude pojavljale le občasno in zakaj se je to zgodilo, ko strežniki niso bili obremenjeni niti do polovice svoje zmogljivosti. Da bi bolje razumeli ta pojav, se moramo nekoliko poglobiti v to, kako NGINX obdeluje delovne obremenitve.

NGINX ima arhitekturo z enim glavnim procesom in več delovnimi procesi. Povezave so porazdeljene med delavce, da lahko hkrati obravnavajo več zahtevkov. Vendar ima ta postavitev tudi pomanjkljivost: ko je delavec zaposlen z obdelavo zahteve, morajo druge zahteve, ki so prav tako dodeljene istemu delavcu, čakati. Zaradi tega lahko ena velika zahteva povzroči zamude pri več zahtevah, tudi če drugi delavci nimajo ničesar za početi. Ta učinek je dobro viden na spodnji sliki. Med ustvarjanjem te slike se stresni test izvaja v 10 različnih povezavah. Slika nato jasno pokaže, da so trije delavci zasedeni, četrti pa ne počne skoraj ničesar.

Čas za Prexy

Ker je bilo ozko grlo jasno vidno, smo izdelali načrt za njegovo izboljšanje. Ta projekt smo poimenovali Prexy, kar je združitev besed RegEx in Proxy. Za končni rezultat smo imeli tri glavne zahteve:

  1. Z njim bi bilo mogoče nadomestiti sedanjo nastavitev. Pravila zamenjave je treba uporabljati na enak način, da se izognemo razbijanju stvari v obstoječih nastavitvah.

  2. Pravila zamenjave je treba pretočno prenašati, da rešitev ne bi porabila preveč pomnilnika.

  3. Nova rešitev naj bi bila hitrejša od trenutne.

Najprej smo morali poiskati zmogljiv večnitni izvajalni čas, ki bi lahko učinkovito obdeloval zahteve. Po preučitvi več možnosti je izbira pristala na izvajalnem času Tokyo za programski jezik Rust. Rust je znan po tem, da omogoča pisanje hitrih programov brez varnostnih tveganj, ki so značilna za druge jezike nizke ravni, kot je C++. Izvedbeni čas Tokyo je prilagodljiv asinhroni izvedbeni čas, ki je namenjen omrežnim aplikacijam. Ena od najpomembnejših lastnosti za nas je dejstvo, da kradejo delo. To pomeni, da za razliko od sistema NGINX zahteva ni vezana na enega delavca, temveč lahko delavec, ki nima ničesar za početi, "ukrade" delo drugemu delavcu. Tako lahko bolje izkoristimo vire naših strežnikov.

Prvi prototip

Po izbiri osnovnih tehnologij smo se odločili vzpostaviti začetni prototip, da bi približno ocenili, kakšno povečanje hitrosti nam bo ta projekt prinesel in ali se nam sploh splača. Kot prvo implementacijo za pogon regex (del, ki dejansko obdeluje in uporablja pravila) smo uporabili standardno knjižnico regex (ali crate, kot jim pravimo v terminologiji Rust). Ta mehanizem, tako kot sregex, ni povratno sledenje, kar pomeni manjše tveganje za težave ReDoS. Pri ReDoS se regularni izraz sooča s takšnim vhodom, da se čas, potreben za ovrednotenje vhoda, eksponentno povečuje. To je lahko pogubno za zmogljivost in je družbi cloudflare povzročilo velik izpad. Zato je za nas pomembno, da motor, ki ga uporabljamo, ni povratno sledenje.

Z uporabo tega motorja regex smo zgradili začetni prototip. Ta prototip je uporabljal trdno kodirana pravila, motor regex pa je celoten odgovor shranil v medpomnilnik, namesto da bi ga pretakal, vendar je to zadoščalo za začetni preizkus zmogljivosti.

Z uporabo tega motorja regex smo zgradili začetni prototip. Ta prototip je uporabljal trdno kodirana pravila, motor regex pa je celoten odgovor shranil v medpomnilnik, namesto da bi ga pretakal, vendar je to zadoščalo za začetni preizkus zmogljivosti. Naša preskusna namestitev je bila sestavljena iz dveh virtualnih računalnikov, pri čemer je v enem virtualnem računalniku delovala namestitev, na kateri sta se lahko izvajali stara in nova rešitev. Tako smo lahko zlahka opravili primerjavo med obema metodama. V drugem strežniku je bil nameščen strežnik NGINX, ki je služil kot izvorni strežnik in je prikazoval preskusno datoteko. V istem strežniku je delovalo tudi orodje wrk, ki smo ga uporabili za ustvarjanje obremenitve. To ni povsem optimalno, saj je za čiste podatke bolje zagnati generator obremenitve v ločenem virtualnem računalniku, vendar je imel ta virtualni računalnik dovolj virov, da se NGINX in wrk med seboj nista motila.

Prvi rezultati testiranja so bili povsem jasni: Prexy je bil približno 22-krat hitrejši pri obravnavi ene zahteve (glejte spodnjo sliko). To je projektu dalo dokončno zeleno luč, saj je bilo pri tako veliki razliki dovolj prostora za absorbiranje nekaterih poslabšanj zmogljivosti, ki bi se lahko pojavila z dokončanjem funkcionalnosti.

Po začetnem testiranju smo izvedli nekaj očitnih optimizacij, kot je predpomnjenje sestavljenih regularnih izrazov. Prav tako smo uvedli učinkovitejši način ponovne uporabe povezav s povratnim strežnikom. S tem smo dosegli približno 20-odstotno povečanje hitrosti. Obe rešitvi smo nato preizkusili tudi pri največji obremenitvi, ko smo strežniku poslali čim več zahtevkov. Tudi tu je bila razlika jasno vidna in Prexy je dosegel približno 25-krat večjo prepustnost kot stara rešitev.

Streaming regex engine

Ena od zahtev tega projekta je bila, da mora biti uporabljeni pogon regex pretočni. Ustvarjalnik regexa v prototipu ni, zato je bilo na tem področju potrebno dodatno delo. Vendar se je izkazalo, da je regex crate zelo optimiziran, zato smo se odločili, da to implementacijo vendarle vzamemo za osnovo našega pretočnega motorja. Pri pretakanju podatkov prek mehanizma regex je treba upoštevati nekaj stvari. Prvič, ne veste, kateri podatki bodo še prišli. Zato morate začeti delati z delnimi ujemanji: ujemanji, ki še niso dokončana, vendar so že ujela 1 ali več znakov. Pri iskanju popolnega ujemanja morate nato preveriti, da ni prekrivajočih se delnih ujemanj, saj so lahko tudi ta na koncu ujemanja (v primeru prekrivanja pa daljše ujemanje zmaga nad krajšim). Tako lahko začnete obdelovati ujemanje le, če se izkaže, da vsa trenutna delna ujemanja niso popolno ujemanje.

Nadaljnje optimizacije

Kot je bilo pričakovati, je preoblikovanje motorja regex negativno vplivalo na zmogljivost strežnika Prexy. Vendar je bilo v primerjavi s staro rešitvijo še vedno veliko rezerve, z dodajanjem dodatnih optimizacij pa smo se ponovno znašli celo višje kot pred obnovo motorja regex. Ena od optimizacij, ki smo jo uporabili, je bila pomnjenje, ali je treba pravilo uporabiti za določeno datoteko. Statična sredstva, kot so datoteke CSS in JS, se skoraj nikoli ne spreminjajo, zato je nesmiselno vsakič izvajati pravilo, če se nikoli ne ujema z določeno datoteko. Alternativa temu je bilo shranjevanje rezultatov zamenjav v predpomnilniku, vendar je slabost te strategije, da je za shranjevanje vseh datotek potrebnega veliko pomnilnika. Če si zapomnimo, katera pravila je treba uporabiti, potrebujemo le nekaj bajtov na datoteko, saj informacije shranimo kot bitno sliko.

Poleg tega v celoti izkoriščamo optimizacije v mehanizmu regex, na primer tako, da ne hranimo skupin za zajemanje, če se ne uporabljajo pri zamenjavi. Zaradi tega si mora motor regexa zapomniti le začetek in konec celotnega ujemanja, kar prihrani delo. Onemogočili smo tudi poimenovane skupine za zajemanje, saj je bilo to v različici s pretakanjem precej zapleteno, in ker tega tudi stara rešitev ni podpirala, to ni bilo nujno potrebno. Vse te optimizacije so na koncu zagotovile naslednjo zmogljivost:

Končni rezultat

Po obsežnem testiranju Prexyja, s katerim smo se prepričali, da je njegovo obnašanje res enako kot pri stari rešitvi, smo ga začeli postopoma uvajati. V obdobju približno enega tedna smo za vsak klon aktivirali Prexy. Pri našem spremljanju je bilo pogosto enostavno opaziti čas aktivacije. Ni bilo redko opaziti približno 50-odstotnega zmanjšanja odzivnih časov. Manjše razlike v višini približno 10 % smo opazili tudi pri drugih strankah, ki so imele zelo malo pravil za zamenjavo. Naše spletno mesto je postalo za približno 30 % hitrejše (~50 ms).

V naši celotni infrastrukturi smo po uvedbi strežnika Prexy opazili naslednje razlike.

  • 33 % manjša poraba pomnilnika RAM

  • 20 % manjša poraba procesorja

  • 10-50 % hitrejši odzivi

Na splošno lahko rečemo, da je bil Prexy uspešen projekt. Z zamenjavo zastarelega modula NGINX lahko zdaj uporabljamo novejše in učinkovitejše tehnike, ki pomenijo jasno merljivo razliko za vse naše stranke. V prihodnosti bomo še naprej izboljševali program Clonable, tako v smislu novih funkcij kot tudi izboljšanja obstoječih funkcionalnosti.


Hvala za branje tega inženirskega bloga. Sporočite nam, če želite, da bi tovrstne tehnične vpoglede v naš izdelek dobivali pogosteje. Ste se navdušili nad tem projektom? Potem si oglejte našo stran o prostih delovnih mestih :)