Inžinerijos dienoraštis - Prexy: itin greitos paieškos ir pakeitimo taisyklės
Sveiki atvykę į mūsų pirmąjį inžinerinį tinklaraštį. Jis gali būti šiek tiek techniškesnis, nei esate įpratę iš kitų tinklaraščių, tačiau pasistengėme, kad jis būtų suprantamas visiems. Šiame straipsnyje kalbėsime apie " Prexy"- naują " Clonable programinę įrangą, naudojamą pakeitimo taisyklėms taikyti.
Pagrindinė informacija
Būdami " Clonable naudotojai, galbūt jau esate susipažinę su pakeitimo taisyklės funkcija. Šias paieškos ir pakeitimo taisykles galite naudoti teksto ar kodo dalims pakeisti savo pačių pasirinktu variantu. Tai galite naudoti, pavyzdžiui, norėdami pakeisti API raktą arba analizės ID, kad galėtumėte sukurti skirtingą analizę savo originalioje svetainėje ir išverstuose klonuose. Viduje tos pačios taisyklės taip pat naudojamos originalaus domeno pavadinimui pakeisti į klono domeno pavadinimą ir daugeliui kitų dalykų.
Nuo pat pirmosios " Clonable versijos ši funkcija niekada nebuvo atnaujinta, o tai savaime nestebina, nes ji puikiai atliko savo darbą. Nepaisant to, buvo galima patobulinti tiek našumo, tiek patogumo naudotojui požiūriu. Kartkartėmis mes, Clonable, imamės ilgai nenaudotos savo produkto dalies ir pradedame ją tobulinti. Pavyzdžiui, praėjusiais metais iš pagrindų pertvarkėme visą savo duomenų infrastruktūrą, kad ji taptų greitesnė, labiau keičiamo dydžio ir atsparesnė gedimams, o prieš tai taip pat iš pagrindų pertvarkėme URL vertimo funkciją. Šį ketvirtį atėjo pakeitimo taisyklių eilė.
Senoji situacija ir apribojimai
Pakeitimo taisyklės " Clonable sistemoje taikomos tik paskutiniame etape, prieš pat siunčiant atsakymą atgal klientui. Todėl pirmojoje " Clonable versijoje nuspręsta tai įgyvendinti žiniatinklio serveryje "NGINX", naudojant "OpenResty" sukurtą modulį replace-filter-nginx-module. Modulis naudoja sregex kaip srautinio regex variklį, kurį taip pat sukūrė "OpenResty". Šiuo atveju srautinio perdavimo aspektas yra svarbus, nes nenorime kiekvieno atsakymo visiškai įkelti į atmintį. Jei taip darytume, dėl kelių didelių atsakymų NGINX gali sutrikti arba sugesti dėl to, kad nepakanka atminties. Antroji svarbi sregex savybė yra ta, kad ji gali lygiagrečiai apdoroti kelias eilutes. Taip yra todėl, kad kiekvienas klonas jau turi tam tikras numatytąsias taisykles, o kartais, be to, kai kurias specialiai jam pridėtas taisykles. Jei jų nebūtų galima apdoroti lygiagrečiai, vis tiek tektų buferizuoti visą atsakymą, nes po pirmosios taisyklės turėtume vėl jį sugretinti su antrąja taisykle.
Tačiau praėjusiais metais vis dažniau susidurdavome su keistomis našumo problemomis. Šios problemos dažnai būdavo trumpalaikės, tačiau kraštutiniais atvejais puslapio atsako laikas galėjo pailgėti keliomis sekundėmis. Po vieno tokio šuolio puslapis dažnai vėl įkeldavo normaliai, todėl problemą buvo sunku atkurti. Norėdami toliau šalinti trūkumus, į visus atsakymus įtraukėme antraštę Clonable-Timings, kuri leido apytiksliai nustatyti, kuris vertimo proceso etapas užima tiek daug laiko. Tai parodė, kad beveik visais atvejais aukštesnės pakopos serveris reagavo greitai, o vertimas taip pat vyko gana sklandžiai. Tačiau tarp vertimo užbaigimo ir visiško užklausos įvykdymo buvo tarpas, ir tai mums leido suprasti, kad tai gali būti susiję su pakeitimo taisyklėmis.
NGINX lygiagretumo modelis
Tačiau tai dar nedavė mums atsakymo, kodėl šie vėlavimai pasireikšdavo tik retkarčiais ir kodėl taip atsitiko, kai serveriai nebuvo apkrauti net iki pusės savo pajėgumo. Kad geriau suprastume šį reiškinį, turime šiek tiek giliau pažvelgti į tai, kaip NGINX apdoroja darbo krūvius.
NGINX turi darbininkų architektūrą su vienu pagrindiniu procesu ir keliais darbininkų procesais. Ryšiai paskirstomi tarp darbininkų, kad vienu metu būtų galima apdoroti kelias užklausas. Tačiau ši konfigūracija turi ir trūkumų: kai darbininkas užimtas apdorojant užklausą, kitos užklausos, taip pat priskirtos tam pačiam darbininkui, turi laukti. Dėl to viena didelė užklausa gali sukelti kelių užklausų vėlavimą, net jei kiti darbuotojai neturi ką veikti. Šis poveikis gerai matomas toliau pateiktame paveikslėlyje. Kuriant šį paveikslėlį atliekamas streso testas per 10 skirtingų jungčių. Tuomet paveikslėlyje aiškiai matyti, kad trys darbininkai yra užimti, o ketvirtasis beveik nieko nedaro.

Laikas "Prexy
Aiškiai matydami silpnąją vietą, sukūrėme planą, kaip ją pagerinti. Šį projektą pavadinome "Prexy" - "RegEx" ir "Proxy" junginiu. Galutiniam rezultatui buvo keliami 3 pagrindiniai reikalavimai:
Jis turėtų būti tinkamas pakeisti dabartinę sąranką. Pakeitimo taisyklės turėtų būti taikomos vienodai, kad nesugadintumėte esamų sąrankų.
Pakeitimo taisyklės turi būti srautinės, kad sprendimas nenaudotų per daug atminties.
Naujasis sprendimas turėtų būti greitesnis už dabartinį.
Pirmiausia turėjome ieškoti galingo daugiasrautinio paleidimo režimo, kuris galėtų efektyviai apdoroti užklausas. Apsvarsčius kelias galimybes, pasirinkimas nusileido programavimo kalbos "Rust" paleidimo sistemai " Tokyo". Rust yra žinoma tuo, kad leidžia rašyti greitas programas be saugos rizikos, kuri kyla dėl kitų žemo lygio kalbų, pavyzdžiui, C++. Tokyo runtime yra lanksti asinchroninė paleidimo sistema, sukurta tinklinėms programoms. Viena iš svarbiausių mums savybių yra ta, kad ji yra darbo vagystė. Tai reiškia, kad, skirtingai nei NGINX, užklausa nėra pririšta prie vieno darbuotojo, bet nieko neveikiantis darbuotojas gali "pavogti" darbą iš kito darbuotojo. Taip galima geriau išnaudoti mūsų serverių išteklius.
Pirmasis prototipas
Pasirinkę pagrindines technologijas, nusprendėme sukurti pradinį prototipą, kad apytiksliai įvertintume, kiek greičio padidėjimo šis projektas mums suteiktų ir ar apskritai verta jį įgyvendinti. Kaip pradinį regex variklio (dalies, kuri iš tikrųjų apdoroja ir taiko taisykles) įgyvendinimą panaudojome standartinę regex biblioteką (arba "Rust" terminologijoje vadinamą "crate"). Šis variklis, kaip ir sregex, nėra grįžtamasis, o tai reiškia mažesnę ReDoS problemos riziką. ReDoS atveju reguliarioji išraiška susiduria su tokia įvestimi, kad laikas, reikalingas įvesties įvertinimui, didėja eksponentiškai. Tai gali būti pražūtinga našumui ir dėl to "cloudflare" patyrė didelį sutrikimą. Taigi mums svarbu, kad mūsų naudojamas variklis būtų be grįžtamojo ryšio.
Naudodami šį regex variklį sukūrėme pradinį prototipą. Šiame prototipe buvo naudojamos kietai užkoduotos taisyklės, o regeksų variklis visą atsakymą talpino į buferį, užuot jį transliavęs, tačiau to pakako pradiniam našumo bandymui.
Naudodami šį regex variklį sukūrėme pradinį prototipą. Šiame prototipe buvo naudojamos kietai užkoduotos taisyklės, o regeksų variklis visą atsakymą talpino į buferį, užuot jį transliavęs, tačiau to pakako pradiniam našumo bandymui. Mūsų bandymų sąranką sudarė dvi virtualios mašinos, kurių vienoje veikė sąranka, kurioje galėjo veikti ir senasis, ir naujasis sprendimas. Taip galėjome lengvai palyginti abu metodus. Kitame serveryje veikė NGINX, kuris buvo naudojamas kaip kilmės serveris ir pateikė bandomąjį failą. Tame pačiame serveryje taip pat veikė wrk įrankis, kurį naudojome apkrovai generuoti. Tai nėra visiškai optimalu, nes gryniems duomenims geriau paleisti apkrovos generatorių atskiroje virtualioje mašinoje, tačiau šioje virtualioje mašinoje buvo pakankamai išteklių, kad NGINX ir wrk netrukdytų vienas kitam.
Pirmieji bandymų rezultatai buvo visiškai aiškūs: "Prexy" maždaug 22 kartus greičiau apdorojo vieną užklausą (žr. toliau pateiktą paveikslėlį). Tai suteikė projektui galutinę žalią šviesą, nes esant tokiam dideliam skirtumui buvo pakankamai erdvės absorbuoti tam tikrus našumo sumažėjimus, kurie gali atsirasti įgyvendinant funkcionalumą.

Po pradinių bandymų įgyvendinome keletą akivaizdžių optimizavimų, pavyzdžiui, sukompiluotų reguliariųjų išraiškų talpinimą į spartinančiąją atmintį. Taip pat įgyvendinome efektyvesnį pakartotinio ryšio su aukštesniuoju serveriu panaudojimo būdą. Tai padėjo pasiekti maždaug 20 % spartos padidėjimą. Tuomet abu sprendimus išbandėme ir didžiausios apkrovos sąlygomis, siųsdami į serverį kuo daugiau užklausų. Čia skirtumas taip pat buvo aiškiai matomas, o "Prexy" pasiekė maždaug 25 kartus didesnį pralaidumą nei senasis sprendimas.

Srautinės regex variklis
Vienas iš šio projekto reikalavimų buvo, kad naudojamas regex variklis būtų srautinis. Prototipo regex create toks nėra, todėl šioje srityje reikėjo atlikti papildomą darbą. Tačiau paaiškėjo, kad regex crate yra labai optimizuotas, todėl nusprendėme vis dėlto imti šią realizaciją kaip srautinio variklio pagrindą. Srautiniu būdu perduodant duomenis per regex variklį, svarbu nepamiršti kelių dalykų. Pirma, jūs nežinote, kokie duomenys dar ateis. Todėl turėtumėte pradėti dirbti su daliniais atitikmenimis: atitikmenimis, kurie dar nėra baigti, bet jau atitinka 1 ar daugiau simbolių. Ieškodami visiško atitikmens, turite patikrinti, ar nėra persidengiančių dalinių atitikmenų, nes jie taip pat gali tapti atitikmenimis (o persidengimo atveju ilgesnis atitikmuo laimi prieš trumpesnį). Taigi galite pradėti tvarkyti atitikmenį tik tada, jei paaiškėja, kad visi šiuo metu esantys daliniai atitikmenys nėra visiškas atitikmuo.
Tolesnis optimizavimas
Kaip ir tikėtasi, regex variklio pertvarkymas turėjo neigiamos įtakos "Prexy" našumui. Tačiau, palyginti su senuoju sprendimu, vis dar buvo daug rezervo, o pridėję papildomų optimizavimų, galiausiai vėl pasiekėme dar aukštesnį rezultatą nei prieš regex variklio pertvarkymą. Viena iš mūsų taikytų optimizacijų buvo prisiminti, ar taisyklę taikyti konkrečiam failui. Statinis turtas, pavyzdžiui, CSS ir JS failai, beveik niekada nesikeičia, todėl beprasmiška kiekvieną kartą vykdyti taisyklę, jei ji niekada nesutampa su konkrečiu failu. Alternatyva tam buvo pakeitimų rezultatus talpinti į talpyklą, tačiau šios strategijos trūkumas yra tas, kad visiems failams saugoti reikia daug atminties. Prisimindami, kurias taisykles taikyti, saugodami informaciją bitų žemėlapio pavidalu, kiekvienam failui prireiks tik kelių baitų.
Be to, visapusiškai išnaudojame regex variklio optimizavimo privalumus, pavyzdžiui, nelaikome užfiksuotų grupių, kai jos nenaudojamos pakeitimui. Todėl regex varikliui tereikia įsiminti viso atitikmens pradžią ir pabaigą, todėl sutaupoma darbo. Taip pat išjungėme įvardytas gaudymo grupes, nes srautiniame variante tai buvo gana sudėtinga, o kadangi senasis sprendimas to taip pat nepalaikė, tai nebuvo būtinai reikalinga. Visi šie optimizavimai galiausiai užtikrino tokį našumą:

Galutinis rezultatas
Iš esmės išbandę "Prexy", kad įsitikintume, jog jo elgsena iš tiesų tokia pati kaip ir senojo sprendimo, pradėjome palaipsniui jį diegti. Per maždaug savaitę "Prexy" aktyvavome kiekvienam klonui. Atliekant stebėseną, dažnai buvo lengva pastebėti aktyvavimo laiką. Neretai buvo galima pastebėti maždaug 50 % sumažėjusį atsako laiką. Nedideli maždaug 10 % skirtumai buvo pastebėti ir su kitais klientais, kurie turėjo labai mažai pakeitimo taisyklių. Mūsų pačių svetainė tapo maždaug 30 % greitesnė (~50 ms).
Įdiegus "Prexy" visoje mūsų infrastruktūroje pastebimi šie skirtumai.
33 % mažiau operatyviosios atminties
20 % mažesnis procesoriaus suvartojimas
10-50 % greitesni atsakymai
Apskritai galime teigti, kad "Prexy" buvo sėkmingas projektas. Pakeitę pasenusį "NGINX" modulį, dabar galime naudoti naujesnius ir efektyvesnius metodus, kurie visiems mūsų klientams suteikia aiškią ir išmatuojamą naudą. Ateityje ir toliau tobulinsime Clonable, tiek diegdami naujas funkcijas, tiek tobulindami esamą funkcionalumą.
Ačiū, kad perskaitėte šį inžinerijos tinklaraštį. Praneškite mums, jei norėtumėte dažniau gauti tokių techninių įžvalgų apie mūsų produktą. Susidomėjote šiuo projektu? Tuomet peržvelkite mūsų laisvų darbo vietų puslapį :)