Inženierzinātņu emuārs - Prexy: īpaši ātra meklēšana un aizstāšanas noteikumi

Dalīties

Laipni lūgti mūsu pirmajā inženierijas blogā. Tas var būt nedaudz tehniskāks, nekā jūs esat pieraduši no citiem blogiem, taču mēs esam darījuši visu iespējamo, lai tas būtu saprotams ikvienam. Šajā rakstā mēs runāsim par Prexy- jaunu Clonable programmatūras daļu, ko izmanto aizvietošanas noteikumu piemērošanai.

Pamatinformācija

Kā Clonable lietotājs jūs, iespējams, jau esat iepazinies ar aizvietošanas noteikumu funkcionalitāti. Šos meklēšanas un aizstāšanas noteikumus varat izmantot, lai teksta vai koda fragmentus apmainītu ar paša izvēlētu variantu. To var izmantot, piemēram, lai aizstātu API atslēgu vai analītikas ID, tādējādi jūs varat izveidot atšķirīgu analītiku savai oriģinālajai vietnei un saviem tulkotajiem kloniem. Iekšēji šie paši noteikumi tiek izmantoti arī jūsu oriģinālā domēna nosaukuma aizstāšanai ar jūsu klona domēna nosaukumu un daudzām citām lietām.

Kopš pirmās Clonable versijas šī funkcionalitāte nekad nav atjaunināta, kas pats par sevi nav pārsteidzoši, jo tā savu uzdevumu veica lieliski. Tomēr bija iespējami uzlabojumi gan veiktspējas, gan lietotāja ērtības ziņā. Reizi pa reizei mēs Clonable ņemam kādu sava produkta daļu, kas ilgu laiku nav izmantota, lai sāktu to uzlabot. Piemēram, pagājušajā gadā mēs pārstrādājām visu savu datu infrastruktūru, lai padarītu to ātrāku, mērogojamu un izturīgāku pret kļūmēm, un pirms tam no jauna pārbūvējām arī URL tulkošanas funkcionalitāti. Šajā ceturksnī pienāca kārta aizvietošanas noteikumiem.

Vecā situācija un ierobežojumi

Aizstāšanas noteikumi Clonable tiek piemēroti tikai pašā pēdējā posmā, tieši pirms atbildes nosūtīšanas atpakaļ klientam. Tāpēc pirmajā Clonable versijā tika izvēlēts to īstenot tīmekļa serverī NGINX, izmantojot aizstāšanas filtru-nginx-moduli, kas ir OpenResty izveidots modulis. Modulis izmanto sregex kā straumēšanas regex dzinēju, ko arī ir radījis OpenResty. Šajā gadījumā straumēšanas aspekts ir svarīgs, jo mēs nevēlamies katru atbildi pilnībā ielādēt atmiņā. Ja mēs to darītu, vairākas lielas atbildes varētu izraisīt NGINX avāriju vai darbības traucējumus, jo nav pieejams pietiekami daudz atmiņas. Otra svarīga sregex funkcija ir tā, ka tas var apstrādāt vairākas rindas paralēli. Tas ir tāpēc, ka katram klonam jau ir daži noklusējuma noteikumi un dažreiz papildus daži noteikumi, kas pievienoti speciāli šim klonam. Ja tos nevarētu apstrādāt paralēli, mums joprojām nāktos buferēt visu atbildi, jo pēc pirmā noteikuma mums būtu jāspēj to atkal saskaņot ar otro noteikumu.

Tomēr pagājušajā gadā mēs arvien biežāk saskārāmies ar dīvainām veiktspējas problēmām. Šīs problēmas bieži vien bija īslaicīgas, taču ekstremālos gadījumos tās varēja palielināt lapas atbildes laiku par sekundēm. Pēc viena šāda lēciena lapa bieži vien atkal ielādējās normāli, tādējādi problēmu bija grūti reproducēt. Lai turpinātu atkļūdošanu, mēs pievienojām Clonable-Timings galveni visām atbildēm, kas ļāva aptuveni noteikt, kurš tulkošanas procesa posms aizņēma tik daudz laika. Tas parādīja, ka gandrīz visos gadījumos augšupejošais komponents reaģēja ātri, un arī tulkošana noritēja diezgan vienmērīgi. Tomēr starp tulkojuma pabeigšanu un pieprasījuma pilnīgu pabeigšanu bija starpība, un tas deva mums mājienu, ka tas varētu būt saistīts ar aizvietošanas noteikumiem.

NGINX vienlaicības modelis

Tomēr tas mums vēl nesniedza atbildi uz jautājumu, kāpēc šie kavējumi parādījās tikai sporādiski un kāpēc tas notika, kad serveri nebija noslogoti pat līdz pusei no to jaudas. Lai labāk izprastu šo parādību, mums ir jāiedziļinās mazliet dziļāk tajā, kā NGINX apstrādā slodzi.

NGINX ir darba ņēmēju arhitektūra ar vienu galveno procesu un vairākiem darba ņēmēju procesiem. Savienojumi tiek sadalīti starp strādniekiem, lai apstrādātu vairākus pieprasījumus vienlaicīgi. Tomēr šādai konfigurācijai ir arī trūkums: kad darba ņēmējs ir aizņemts ar pieprasījuma apstrādi, citiem pieprasījumiem, kas arī ir piešķirti šim pašam darba ņēmējam, ir jāgaida. Tas var izraisīt, ka viens smags pieprasījums var aizkavēt vairākus pieprasījumus, pat ja pārējiem darbiniekiem nav nekāda darāmā darba. Šis efekts labi redzams attēlā turpmāk. Šī attēla izveides laikā stresa tests tiek veikts 10 dažādos savienojumos. Pēc tam attēlā skaidri redzams, ka trīs darbinieki ir aizņemti, bet ceturtais gandrīz neko nedara.

Laiks Prexy

Kad bija skaidri redzams vājais punkts, mēs izveidojām plānu, kā to uzlabot. Šis projekts tika nosaukts par Prexy, kas ir RegEx un Proxy apvienojums. Galarezultātam bija 3 galvenās prasības:

  1. Tam vajadzētu aizstāt pašreizējo iestatījumu. Aizvietošanas noteikumi jāpiemēro vienādi, lai izvairītos no esošo iestatījumu bojāšanas.

  2. Aizvietošanas noteikumiem jābūt straumētiem, lai risinājums neizmantotu pārāk daudz atmiņas.

  3. Jaunajam risinājumam vajadzētu būt ātrākajam par pašreizējo risinājumu.

Vispirms mums bija jāmeklē jaudīgs daudzpavedienu izpildes laiks, kas varētu efektīvi apstrādāt pieprasījumus. Pēc vairāku variantu apsvēršanas izvēle tika izdarīta uz Tokyo runtime, kas paredzēts Rust programmēšanas valodai. Rust ir pazīstama ar to, ka tā ļauj rakstīt ātras programmas bez drošības riskiem, kas raksturīgi citām zema līmeņa valodām, piemēram, C++. Tokyo runtime ir elastīgs asinhronais runtime, kas paredzēts lietojumprogrammām, kas darbojas tīklā. Viena no mums svarīgākajām īpašībām ir tā, ka tā ir darba zādzība. Tas nozīmē, ka atšķirībā no NGINX pieprasījums nav piesaistīts vienam darbiniekam, bet darbinieks, kuram nav ko darīt, var "nozagt" darbu no cita darbinieka. Tādējādi mūsu serveru resursus var labāk izmantot.

Pirmais prototips

Pēc pamattehnoloģiju izvēles mēs nolēmām izveidot sākotnējo prototipu, lai aptuveni novērtētu, cik lielu ātruma pieaugumu mums dos šis projekts un vai tas vispār ir tā vērts. Kā sākotnējo implementāciju regex dzinējam (tai daļai, kas faktiski apstrādā un piemēro noteikumus) mēs izmantojām standarta regex bibliotēku (jeb crate, kā tās sauc Rust terminoloģijā). Šis dzinējs, tāpat kā sregex, nav atgriezeniski izsekojams, kas nozīmē mazāku ReDoS problēmu risku. ReDoS gadījumā regulārā izteiksme saskaras ar tādu ievadi, ka laiks, kas nepieciešams ievadi izvērtēt, pieaug eksponenciāli. Tas var katastrofāli ietekmēt veiktspēju, un cloudflare izraisīja lielu pārtraukumu. Tāpēc mums ir svarīgi, lai mūsu izmantotais dzinējs nebūtu atgriezeniski izsekojams.

Izmantojot šo regeksas dzinēju, mēs izveidojām sākotnējo prototipu. Šajā prototipā tika izmantoti grūti kodēti noteikumi, un regeksas dzinējs visu atbildi pārsūtīja buferī, nevis straumēja, taču ar to pietika sākotnējam veiktspējas testam.

Izmantojot šo regeksas dzinēju, mēs izveidojām sākotnējo prototipu. Šajā prototipā tika izmantoti grūti kodēti noteikumi, un regeksas dzinējs visu atbildi pārsūtīja buferī, nevis straumēja, taču ar to pietika sākotnējam veiktspējas testam. Mūsu testa konfigurācija sastāvēja no diviem virtuālajiem datoriem, no kuriem vienā darbojās konfigurācija, kurā varēja darboties gan vecais, gan jaunais risinājums. Tādējādi mēs varējām viegli salīdzināt abas metodes. Otrajā serverī darbojās NGINX, kas kalpoja kā izcelsmes serveris un apkalpoja testa failu. Tajā pašā serverī darbojās arī rīks wrk, ko izmantojām slodzes ģenerēšanai. Tas nav pilnīgi optimāli, jo tīru datu gadījumā slodzes ģeneratoru labāk palaist atsevišķā VM, taču šajā VM bija pietiekami daudz resursu, lai NGINX un wrk netraucētu cits citam.

Pirmie testa rezultāti bija pilnīgi skaidri: Prexy bija aptuveni 22 reizes ātrāks, apstrādājot vienu pieprasījumu (skatiet attēlu zemāk). Tas deva projektam galīgo zaļo gaismu, jo ar tik lielu atšķirību bija pietiekami daudz vietas, lai absorbētu dažus veiktspējas samazinājumus, kas varētu rasties, padarot funkcionalitāti pilnīgu.

Pēc sākotnējās testēšanas mēs īstenojām dažas acīmredzamas optimizācijas, piemēram, kompilēto regulāro izteicienu kešēšanu. Mēs arī ieviesām efektīvāku veidu, kā atkārtoti izmantot savienojumus ar augšupvērsto serveri. Tādējādi mēs panācām aptuveni 20 % ātruma pieaugumu. Pēc tam mēs arī testējām abus risinājumus maksimālas slodzes apstākļos, nosūtot uz serveri pēc iespējas vairāk pieprasījumu. Arī šajā gadījumā atšķirība bija skaidri redzama, un Prexy sasniedza aptuveni 25 reizes lielāku caurlaides spēju nekā vecais risinājums.

Straumēšanas regeksas dzinējs

Viena no šī projekta prasībām bija, ka izmantotajam regeksdzinējam jābūt straumēšanas dzinējam. Prototipa regeksas radīšana tāda nav, tāpēc šajā jomā bija nepieciešams papildu darbs. Tomēr izrādījās, ka regex crate ir ļoti optimizēts, tāpēc mēs nolēmām šo implementāciju tomēr ņemt par pamatu mūsu straumēšanas dzinējam. Straumējot datus ar regeksas dzinēja palīdzību, ir dažas lietas, kuras ir svarīgi paturēt prātā. Pirmkārt, jūs nezināt, kādi dati vēl būs pieejami. Tāpēc jāsāk strādāt ar daļējām sakritībām: sakritībām, kas vēl nav pabeigtas, bet jau ir sakritusi 1 vai vairāk rakstzīmju. Atrodot pilnīgu sakritību, ir jāpārbauda, vai daļējas sakritības nepārklājas, jo arī tās var beigties kā sakritības (un pārklāšanās gadījumā garāka sakritība uzvar pār īsāku sakritību). Tātad jūs varat sākt apstrādāt atbilstību tikai tad, ja visas pašreizējās daļējās atbilstības izrādās, ka tās nav pilnīgas atbilstības.

Turpmāka optimizācija

Kā bija gaidāms, regeksas dzinēja pārbūve negatīvi ietekmēja Prexy veiktspēju. Tomēr, salīdzinot ar veco risinājumu, joprojām bija liela rezerve, un, pievienojot papildu optimizācijas, mēs atkal sasniedzām vēl lielāku veiktspēju nekā pirms regeksdzinēja pārbūves. Viena no optimizācijām, ko mēs veicām, bija atcerēšanās, vai noteikums jāpiemēro konkrētam datnes veidam. Tādi statiski aktīvi kā CSS un JS faili gandrīz nekad nemainās, un tāpēc ir bezjēdzīgi katru reizi izpildīt noteikumu, ja tas nekad neatbilst konkrētajam failam. Alternatīva tam bija aizvietošanas rezultātu kešēšana, taču šīs stratēģijas trūkums ir tas, ka visu failu glabāšanai ir nepieciešams daudz atmiņas. Atceroties, kurus noteikumus piemērot, mums ir nepieciešami tikai daži baiti uz katru failu, saglabājot informāciju kā bitmapi.

Turklāt mēs pilnībā izmantojam regeksas dzinēja optimizācijas priekšrocības, piemēram, nesaglabājot fiksēšanas grupas, ja tās netiek izmantotas aizstāšanā. Rezultātā regešdzinējam ir jāatceras tikai visas atbilstības sākums un beigas, kas ietaupa darbu. Mēs arī izslēdzām nosauktās uztveršanas grupas, jo straumēšanas variantā tas bija diezgan sarežģīti, un, tā kā arī vecais risinājums to neatbalstīja, tas nebija obligāti nepieciešams. Visas šīs optimizācijas galu galā nodrošināja šādu veiktspēju:

Gala rezultāts

Pēc plašas Prexy testēšanas, lai pārliecinātos, ka tā uzvedība patiešām ir tāda pati kā vecajam risinājumam, mēs sākām to pakāpeniski ieviest. Aptuveni nedēļas laikā mēs aktivizējām Prexy katram klonam. Veicot uzraudzību, bieži vien bija viegli pamanāms aktivizēšanas laiks. Atbilžu sniegšanas laika samazinājums par aptuveni 50 % nebija retums. Nelielas atšķirības aptuveni 10 % apmērā tika novērotas arī citiem klientiem, kuriem bija ļoti maz aizstāšanas noteikumu. Mūsu pašu tīmekļa vietne kļuva par aptuveni 30 % ātrāka (~ 50 ms).

Pēc Prexy ieviešanas mūsu kopējā infrastruktūrā mēs novērojam šādas atšķirības.

  • Par 33% mazāks RAM patēriņš

  • Par 20 % mazāks CPU patēriņš

  • Par 10-50 % ātrāka atbildes reakcija

Kopumā varam teikt, ka Prexy bija veiksmīgs projekts. Aizstājot novecojušo NGINX moduli, mēs tagad varam izmantot jaunākas un efektīvākas metodes, kas visiem mūsu klientiem sniedz skaidri izmērāmu ieguvumu. Nākotnē mēs turpināsim pilnveidot Clonable, gan ieviešot jaunas funkcijas, gan uzlabojot esošo funkcionalitāti.


Paldies, ka izlasījāt šo inženierijas blogu. Dariet mums zināmu, ja vēlaties biežāk saņemt šāda veida tehniskus ieskatus par mūsu produktu. Vai esat sajūsmā par šo projektu? Tad ielūkojieties mūsu vakanču lapā :)

Mēs izmantojam sīkdatnes, lai uzlabotu jūsu pieredzi, analizētu tīmekļa vietnes apmeklējumu statistiku un pilnveidotu mūsu pakalpojumus.

Lasiet mūsu sīkdatņu politiku