Tekninen blogi – Prexy: huippunopeat etsi ja korvaa -säännöt
Tervetuloa ensimmäiseen tekniseen blogiimme. Se on ehkä hieman teknisempi kuin mihin olet tottunut muista blogeista, mutta olemme tehneet parhaamme, jotta se olisi ymmärrettävää kaikille. Tässä artikkelissa puhumme Prexystä, uudesta Clonable -ohjelmistosta, jota käytetään korvaussääntöjen soveltamiseen.
Tausta
Clonable -käyttäjänä olet ehkä jo perehtynyt korvaussääntöjen toimintoon. Näitä etsi ja korvaa -sääntöjä voit käyttää tekstin tai koodin osien korvaamiseen itse valitsemallasi vaihtoehdolla. Käytä tätä esimerkiksi API-avaimen tai analytiikka-tunnuksen korvaamiseen, jotta voit tehdä erillisiä analyyseja alkuperäiselle sivustollesi ja käännetyille klooneillesi. Sisäisesti näitä samoja sääntöjä käytetään myös alkuperäisen verkkotunnuksen korvaamiseen kloonin verkkotunnuksella sekä muihin tarkoituksiin.
Clonable n ensimmäisestä versiosta lähtien tätä toimintoa ei ole koskaan muutettu, mikä ei sinänsä ole ihme, sillä se toimi erinomaisesti. Silti parannuksia oli mahdollista tehdä sekä suorituskyvyn että käyttäjäystävällisyyden osalta. Ajoittain otamme Clonable sta osan tuotteestamme, johon ei ole pitkään aikaan käytetty aikaa, ja ryhdymme parantamaan sitä. Esimerkiksi viime vuonna uudistimme koko tietoinfrastruktuurimme, jotta se olisi nopeampi, skaalautuvampi ja vikasietoisempi, ja rakensimme tätä varten myös URL-muunnosominaisuuden kokonaan uudelleen alusta alkaen. Tänä vuosineljänneksenä oli korvaussääntöjen vuoro.
Aikaisempi tilanne ja rajoitukset
Clonable -palvelussa korvaussäännöt sovelletaan vasta aivan viimeisessä vaiheessa, juuri ennen vastauksen lähettämistä asiakkaalle. Clonable-palvelun ensimmäisessä versiossa päätettiin siksi toteuttaa tämä NGINX-verkkopalvelimessa käyttämällä replace-filter-nginx-moduulia, jonka on kehittänyt OpenResty. Moduuli käyttää sregexiä suoratoistavana säännöllisten lausekkeiden moottorina, joka on myös OpenRestyn kehittämä. Suoratoistava ominaisuus on tässä tapauksessa tärkeä, koska emme halua ladata jokaista vastausta kokonaan muistiin. Jos tekisimme näin, muutamat suuret vastaukset saattaisivat aiheuttaa NGINX:n kaatumisen tai jumittumisen, koska käytettävissä ei olisi riittävästi muistia. Toinen tärkeä sregexin ominaisuus on, että se pystyy käsittelemään useita sääntöjä rinnakkain. Jokaisella kloonilla on nimittäin jo joukko vakiosääntöjä ja lisäksi joskus joitakin nimenomaan kyseiselle kloonille lisättyjä sääntöjä. Jos näitä ei voitaisi käsitellä samanaikaisesti, meidän pitäisi silti puskuroida koko vastaus, koska ensimmäisen säännön jälkeen meidän on pystyttävä käymään se läpi uudelleen toisen säännön osalta.
Viime vuoden aikana kohtasimme kuitenkin yhä useammin outoja suorituskykyongelmia. Nämä ongelmat olivat usein lyhytaikaisia, mutta saattoivat äärimmäisissä tapauksissa pidentää sivun vasteaikaa sekunneilla. Yhden tällaisen piikin jälkeen sivu latautui usein taas normaalisti, minkä vuoksi ongelmaa oli vaikea toistaa. Jotta voisimme tutkia asiaa tarkemmin, lisäsimme Clonable-Timings-otsikon kaikkiin vastauksiin, minkä ansiosta pystyimme suurin piirtein määrittämään, mikä vaihe käännösprosessissa vei niin paljon aikaa. Tästä kävi ilmi, että lähes kaikissa tapauksissa ylävirta reagoi nopeasti ja että myös käännös sujui melko sujuvasti. Käännöksen valmistumisen ja pyynnön täydellisen loppuun saattamisen välillä oli kuitenkin viive, mikä antoi meille vihjeen siitä, että asia saattoi liittyä korvaussääntöihin.
NGINX:n samanaikaisuusmalli
Tämä ei kuitenkaan vielä antanut vastausta kysymykseen, miksi nämä viiveet ilmenivät vain satunnaisesti ja miksi näin tapahtui, vaikka palvelimien kuormitus ei ollut edes puolet niiden kapasiteetista. Jotta voimme ymmärtää tätä ilmiötä paremmin, meidän on tarkasteltava hieman syvällisemmin, miten NGINX käsittelee työkuormia.
NGINX:llä on työntekijäarkkitehtuuri, jossa on yksi pääprosessi ja useita työntekijäprosesseja. Yhteydet jaetaan workerprosesseille, jotta useita pyyntöjä voidaan käsitellä samanaikaisesti. Tällä kokoonpanolla on kuitenkin myös haittapuoli: kun yksi workerprosessi on kiireinen pyynnön käsittelyn parissa, muut samalle workerprosessille osoitetut pyynnöt joutuvat odottamaan. Tämä voi johtaa siihen, että yksi raskas pyyntö aiheuttaa viivettä useissa pyynnöissä, vaikka muilla workerprosesseilla ei olisi mitään tekemistä. Tämä ilmiö näkyy selvästi alla olevassa kuvassa. Kuvan luomisen aikana suoritettiin kuormitustesti kymmenellä eri yhteydellä. Kuvasta näkyy selvästi, että kolme työprosessia on kiireisiä, kun taas neljäs ei tee juuri mitään.

Aika Prexylle
Kun pullonkaula oli selvästi näkyvissä, laadimme suunnitelman tilanteen parantamiseksi. Projektille annettiin nimi Prexy, joka on yhdistelmä sanoista RegEx ja Proxy. Lopputulokselle asetettiin kolme päävaatimusta:
Sen on toimittava suora korvike nykyiselle järjestelmällesi. Sovella korvaussääntöjä samalla tavalla, jotta välttäisit ongelmat olemassa olevissa järjestelmissäsi.
Sovella korvaussääntöjä suoratoistona, jotta ratkaisu ei kuluta liikaa muistia.
Uuden ratkaisun on oltava nykyistä ratkaisua nopeampi.
Ensimmäiseksi meidän piti etsiä tehokas monisäikeinen ajoympäristö, joka pystyy käsittelemään pyyntöjä tehokkaasti. Harkittuamme useita vaihtoehtoja valinta osui Rust-ohjelmointikielen Tokio-ajoympäristöön. Rust tunnetaan siitä, että sen avulla voi kirjoittaa nopeita ohjelmia ilman muiden matalan tason kielten, kuten C++:n, turvallisuusriskejä. Tokio-ajoympäristö on joustava, asynkroninen ajoympäristö, joka on suunniteltu verkkosovelluksille. Yksi meille tärkeimmistä ominaisuuksista on se, että se tukee work-stealing-toimintoa. Tämä tarkoittaa, että toisin kuin NGINX:ssä, pyyntö ei ole sidottu yhteen työntekijään, vaan työntekijä, jolla ei ole tekemistä, voi ”varastaa” työtä toiselta työntekijältä. Tällä tavoin palvelimiemme resursseja voidaan hyödyntää paremmin.
Ensimmäinen prototyyppi
Valittuamme perustana käytettävät teknologiat päätimme rakentaa ensimmäisen prototyypin arvioidaksemme, kuinka paljon nopeusetuja tämä projekti meille suunnilleen toisi ja oliko se ylipäätään vaivan arvoista. Ensimmäisenä toteutuksena regex-moottorille (osa, joka tosiasiallisesti käsittelee ja soveltaa sääntöjä) käytimme standardia regex-kirjastoa (tai cratea, kuten niitä Rust-terminologiassa kutsutaan). Tämä moottori on, aivan kuten sregex, non-backtracking-tyyppinen, mikä tarkoittaa, että ReDoS-ongelman riski on pienempi. ReDoS-tilanteessa säännöllinen lauseke joutuu käsittelemään sellaista syötettä, jonka arviointiin kuluva aika kasvaa eksponentiaalisesti. Tämä voi olla katastrofaalista suorituskyvyn kannalta, ja se on aiheuttanut Cloudflare-palvelussa merkittävän häiriön. Siksi meille on tärkeää, että käyttämämme moottori on non-backtracking-tyyppinen.
Rakensimme tämän regex-moottorin avulla ensimmäisen prototyypin. Prototyyppi käytti kiinteästi koodattuja sääntöjä, ja regex-moottori puskuroi koko vastauksen sen sijaan, että olisi suoratoistanut sitä, mutta tämä riitti ensimmäiseen suorituskykytestiin.
Tämän regex-moottorin avulla rakensimme ensimmäisen prototyypin. Prototyyppi käytti kiinteästi koodattuja sääntöjä, ja regex-moottori puskuroi koko vastauksen sen sijaan, että olisi suoratoistanut sitä, mutta tämä riitti ensimmäiseen suorituskykytestiin. Testijärjestelmämme koostui kahdesta virtuaalikoneesta (VM). Yhdellä virtuaalikoneella oli ympäristö, jossa sekä vanha että uusi ratkaisu pystyivät toimimaan. Näin pystyimme helposti vertaamaan molempia menetelmiä keskenään. Toisella palvelimella pyöri NGINX, joka toimi alkuperäispalvelimena ja tarjoili testitiedostoa. Samalla palvelimella pyöri myös wrk-työkalu, jolla loimme kuormitusta. Tämä ei ole täysin optimaalinen ratkaisu, sillä puhtaiden tietojen saamiseksi kuormitusgeneraattori tulisi mieluiten ajaa erillisellä virtuaalikoneella, mutta tällä virtuaalikoneella oli riittävästi resursseja, jotta NGINX ja wrk eivät häirinneet toisiaan.
Ensimmäiset testitulokset olivat kiistattomat: Prexy käsitteli yhden pyynnön noin 22 kertaa nopeammin (katso kuva alla). Tämä antoi projektille lopullisen vihreän valon, sillä näin suurella erolla oli riittävästi liikkumavaraa sietää mahdollisia suorituskyvyn heikkenemisiä, joita saattaisi ilmetä toiminnallisuuden viimeistelyn yhteydessä.

Ensimmäisen testin jälkeen teimme joitakin ilmeisiä optimointeja, kuten kompiloitujen säännöllisten lausekkeiden välimuistiin tallentamisen. Lisäksi otimme käyttöön tehokkaamman tavan käyttää uudelleen yhteyksiä ylävirtaan. Tällä saavutimme noin 20 %:n nopeusparannuksen. Testasimme molempia ratkaisuja myös huippukuormituksella lähettämällä palvelimelle mahdollisimman paljon pyyntöjä. Myös tässä eroa oli selvästi nähtävissä, ja Prexy saavutti noin 25-kertaisen läpimenon verrattuna vanhaan ratkaisuun.

Streamingregexmoottori
Yksi tämän projektin vaatimuksista oli, että käytettävä regex-moottori on suoratoistava. Prototyypin regex-koodi ei ole sellainen, joten tässä suhteessa tarvittiin vielä lisätyötä. Regex-koodi osoittautui kuitenkin erittäin optimoiduksi, joten päätimme käyttää tätä toteutusta suoratoistomoottorimme perustana. Kun dataa suoratoistetaan regex-moottorin läpi, on tärkeää pitää silmällä muutamia seikkoja. Ensinnäkin et tiedä, mitä dataa on vielä tulossa. Käytä osittaisia osumia: osumia, jotka eivät ole vielä täydellisiä, mutta joissa on jo yksi tai useampi merkki. Kun löydät täydellisen osuman, tarkista, ettei ole päällekkäisiä osittaisia osumia, koska ne voivat lopulta myös muodostaa osumia (ja pidempi osuma voittaa lyhyemmän osuman päällekkäisyyden tapauksessa). Voit siis aloittaa vastaavuuden käsittelyn vasta, kun kaikki sillä hetkellä olemassa olevat osittaiset vastaavuudet osoittautuvat olevan muita kuin täydellisiä vastaavuuksia.
Optimoi lisäksi.
Regexmoottorin uudistaminen vaikutti odotetusti negatiivisesti Prexyn suorituskykyyn. Verrattuna vanhaan ratkaisuun suorituskykyä oli kuitenkin vielä riittävästi jäljellä, ja lisäämällä uusia optimointeja pääsimme lopulta jopa parempaan tulokseen kuin ennen regexmoottorin uudistamista. Yksi toteuttamistamme optimoinneista oli se, että muistimme, onko tiettyä sääntöä sovellettava tiettyyn tiedostoon. Staattisetresurssit, kuten CSS- ja JS-tiedostot, eivät muutu lähes koskaan, joten on turhaa suorittaa sääntöä joka kerta, vaikka se ei koskaan osu kyseiseen tiedostoon. Vaihtoehtona oli tallentaa korvaustulokset välimuistiin, mutta tämän strategian haittana on, että kaikkien tiedostojen tallentaminen vie paljon muistia. Muistamalla, mitkä säännöt on sovellettava, tarvitsemme vain muutaman tavun tiedostoa kohti, kun tallennamme tiedot bittikarttana.
Lisäksi hyödynnämme regexmoottorin optimointeja parhaalla mahdollisella tavalla esimerkiksi jättämällä capturingryhmät huomioimatta, kun niitä ei käytetä korvaamisessa. Tämän ansiosta regexmoottorin tarvitsee muistaa vain koko vastaavuuden alku ja loppu, mikä säästää työtä. Olemme myös poistaneet nimetyt capturingryhmät käytöstä, koska ne olivat varsin monimutkaisia suoratoistoversiossa ja koska vanha ratkaisu ei tukenut niitä, ne eivät olleet välttämättömiä. Kaikki nämä optimoinnit johtivat lopulta seuraaviin suorituskykyarvoihin:

Lopputulos
Testattuamme Prexyä perusteellisesti varmistaaksemme, että sen toiminta oli todellakin sama kuin vanhassa ratkaisussa, aloitimme vaiheittaisen käyttöönoton. Noin viikon aikana aktivoimme Prexyn jokaiselle kloonille. Seurannassamme aktivointiajankohta näkyi usein selvästi. Noin 50 prosentin lyheneminen vasteajoissa ei ollut harvinaista. Myös muilla asiakkailla, joilla oli vain vähän korvaussääntöjä, havaittiin pienempiä, noin 10 prosentin eroja. Oma verkkosivustomme nopeutui noin 30 prosenttia (~50 ms).
Prexyn käyttöönoton jälkeen havaitsemme seuraavia eroja kokonaisinfrastruktuurissamme.
33 % vähemmän RAM-muistia
20 % vähemmän CPU-kulutusta
10–50 % nopeammat vastaukset
Kaiken kaikkiaan voimme siis todeta, että Prexy oli menestyksekäs projekti. Korvaamalla vanhentuneen NGINX-moduulin voimme nyt hyödyntää uudempia ja tehokkaampia tekniikoita, jotka tuovat selvästi mitattavissa olevan eron kaikille asiakkaillemme. Jatkamme myös tulevaisuudessa Clonable-palvelun kehittämistä sekä uusien ominaisuuksien lisäämisen että olemassa olevien toimintojen parantamisen osalta.
Kiitos, että luit tämän teknologian blogin. Kerro meille, jos haluaisit saada useammin tällaisia teknisiä kurkistuksia tuotteeseemme. Oletko innostunut tästä projektista? Tutustu sitten myös avoimien työpaikkojen sivuillemme :)
Aloita tänään ilmaiseksi Clonable-palvelun avulla
Tutustu siihen, miten voit hallita kansainvälisiä verkkosivustoja keskitetysti yhdeltä alustalta.