Sparti internetinės saugyklos plėtra daugiau kaip 1 mlrd. ChatGPT naudotojų aptarnauti
Kaip „Python“ kalba pritaikėme programų saugyklos platformą „Habitat“ precedento neturinčiam augimui valdyti.
Autoriai: techninio personalo nariai Jon Lee, Chaomin Yu ir Ben Ries
Kiekvienas OpenAI produktas priklauso nuo greitos ir patikimos prieigos prie duomenų – ar naudotojas jungiasi, tikrina Codex nuostatas, ar pradeda naują pokalbį ChatGPT. Prieš produktui pateikiant atsaką, kiekvienam tokiam veiksmui gali reikėti daugybės atskirų duomenų paieškų. Jei šios užklausos lėtos, lėtas atrodo ir produktas. Jei šios užklausos nepavyksta, produktas visiškai nustoja veikti.
„Habitat“ – mūsų sukurta internetinės saugyklos platforma, kad OpenAI produktai galėtų greitai ir patikimai pasiekti reikiamą informaciją. Dabar „Habitat“ apdoroja daugiau kaip 70 milijonų užklausų per sekundę ir beveik 40 geografinių regionų palaiko produktus, kuriuos kas savaitę naudoja per 1 milijardą žmonių. „Habitat“ pirmą kartą paleista GPT palaikyti per DevDay 2023 kaip paprasta kliento „Python“ biblioteka, prijungta prie vienos duomenų bazės. Šiandien tai sudėtinga paskirstytoji sistema, aptarnaujanti daugiau kaip 500 petabaitų duomenų.
01 pav. · Kas yra „Habitat“?
Internetinės saugyklos platforma
„Habitat“ – mūsų sukurta internetinės saugyklos platforma, kad OpenAI produktai galėtų greitai ir patikimai pasiekti reikiamą informaciją.
- Užklausa
- Atsakas
- Pakeitimai (CDC)
Sukurti ir eksploatuoti tokio masto infrastruktūrą nelengva, bet savaime tai nėra itin sudėtinga. Mūsų situacija buvo išskirtinė dėl precedento neturinčio tempo: turėjome plėstis, kad patenkintume stulbinamai augantį naudotojų skaičių ir produktų paklausą, tuo pat metu kurdami brandžią platformą. Sistemų inžinieriai dažnai kuria dešimteriopam mastui ir tikisi, kad jo pakaks keleriems metams, kol bus ruošiamasi kitam dešimteriopam šuoliui. Mūsų atveju pastaruosius trejus metus kasmet augome daugiau kaip dešimt kartų. Todėl „Habitat“ kūrimą ir eksploatavimą sudarė taktinių sprendimų seka: kiekvieną komponentą nagrinėjome pačiu žemiausiu lygmeniu, kad iš esamos technologijų bazės išspaustume kuo daugiau, kartu sprendėme saugyklos ir skaičiavimo pajėgumų trūkumo problemas ir taip laimėjome laiko esminėms investicijoms.
- 70 mln.+
užklausų per sekundę
- 1 mlrd.+
žmonių kas savaitę
- 500 PB+
duomenų
Augant OpenAI, kartu turėjo augti ir „Habitat“: pirmiausia tapti pakankamai patikima kritiniam produktų srautui, tada pakankamai greita pasaulio naudotojams ir galiausiai gebėti sklandžiai veikti milžinišku mastu. Šis įrašas yra pirmoji dviejų dalių serijos apie internetinės saugyklos plėtrą dalis. Šiame įraše pasakosime, kaip vystėsi „Habitat“, kodėl biblioteką pavertėme paslauga ir kaip neįprasta paslaugų technologijų kalba „Python“ parašytą paslaugą išplėtėme iki patikimo saugyklos platformos lygmens.
Būsimame įraše išsamiai aptarsime, kaip dideliu mastu užtikrinome kelių nuomininkų patikimumą, sluoksniuotą skaitymo našumo optimizavimo strategiją ir išplėtėme partnerystę su „Azure Cosmos DB“, kad patikimai patenkintume precedento neturinčią paklausą.
„Habitat“ kilo iš paprastos idėjos: produktų inžinieriams neturėtų reikėti rūpintis duomenų bazių valdymu. „Habitat“ pirmą kartą paleista GPT palaikyti per DevDay 2023 kaip nedidelė „Python“ biblioteka, sąveikavusi su pagrindiniu ChatGPT serveriu. Ji palaikė nedidelį operacijų rinkinį, kuris viduje buvo susietas su duomenų bazės programa „Azure Cosmos DB“.
Biblioteka turėjo suteikti produktų komandoms paprastą būdą saugoti ir gauti duomenis, nereikalaujant išmanyti visų vidinių aspektų. „Habitat“ atlikdavo visą būtiną darbą: nustatydavo duomenų rūšį, iš kur juos gauti arba kur siųsti, ar užklausa leidžiama, ir kita.
Produktų inžinieriams nereikia rūpintis schemos paieška, maršruto parinkimu, įgaliojimu, šifravimu, serializavimu, užklausų formavimu ir jungčių telkimu. Jiems net nereikėjo svarstyti, iš kur gaunami duomenys: iš „Azure Cosmos DB“, podėlių ar kitų saugyklų.
02 pav. · „Habitat“ paslauga
Supaprastintas „Habitat“ užklausos srautas
Atskyrę saugyklos logiką į savarankišką paslaugą, sukūrėme vieną diegimų, stebimumo ir platformos tobulinimo valdymo tašką.
- Užklausa
- Atsakas
Ši „Python“ biblioteka veikė gerai, ir OpenAI produktų inžinieriai greitai pradėjo naudoti „Habitat“, nors centralizuotai nebuvo skatinama atsisakyti savitarnos „Postgres“ ir „Azure Cosmos DB“.
Kintant produktų poreikiams, kūrėjams taip pat buvo lengva papildyti bendrą biblioteką kliento podėlio, glaudinimo ar šifravimo funkcijomis.
Iki 2025 m. vidurio „Habitat“ pasiekė kliento pusės realizacijos ribas. „Habitat“ lygmeniui sudėtingėjant ir didėjant OpenAI paslaugų skaičiui, atgalinį suderinamumą išlaikantys protokolo pakeitimai tapo nebeįmanomi.
Vienu atveju norėjome sumažinti bet kurio vieno regiono sutrikimo poveikį svarbiausiems duomenų rinkiniams, perkeldami juos į regionuose paskirstytas „Azure Cosmos DB“ paskyras. Tam reikėjo kliente įdiegti papildomą maršruto parinkimo logiką, išjungtą funkcijos žyma, išplatinti ją visiems klientams ir tada įjungti žymą.
Diegimų koordinavimas keliose dešimtyse paslaugų ir darbas su kiekviena komanda užtruko kelias dienas. Prieš įjungdami funkciją supratome, kad norime įdiegti užklausų dubliavimą, kad patikrintume skaidymo logikos teisingumą. Tam išplatinti prireikė dar poros dienų. O ištaisyti pastebėtą klaidą? Dar poros dienų. Galiausiai buvome pasirengę įjungti žymą, tačiau viena komanda dėl nesusijusių priežasčių grąžino savo paslaugą į ankstesnę versiją su klaidingu klientu ir sukėlė sutrikimą, kurio taip stengėmės išvengti.
Keičiant kliento biblioteką reikėjo sudėtingai koordinuoti dešimtis paslaugų, o šis procesas darėsi vis trapesnis, neefektyvesnis ir jautresnis eksploatavimo klaidoms. Kad būsimi diegimai mažiau išsišakotų eksploataciniu požiūriu, nusprendėme „Habitat“ paversti atskira paslauga.
Atskyrę saugyklos logiką į savarankišką paslaugą, sukūrėme vieną diegimų, stebimumo ir platformos tobulinimo valdymo tašką. Užuot valdę padrikus naujinius, galėjome centralizuotai diegti patobulinimus, iš karto naudingus kiekvienam OpenAI produktui.
Centralizuota paslauga taip pat suteikia vieną kontrolės tašką stipriausioms duomenų saugumo ir privatumo priemonėms įgyvendinti. „Habitat“ paslaugoje galime centralizuotai taikyti prieigos valdymo politiką, registruoti auditą ir riboti prieigą prie tokių bazinės saugyklos išteklių kaip „Azure Cosmos DB“. „Habitat“ atlieka itin svarbų vaidmenį saugant naudotojų duomenis ir užkertant kelią išorinių, vidinių bei agentų veikėjų neteisėtai prieigai.
Žinojome, kad mums reikia paslaugos, bet dar nenorėjome atsisakyti „Python“, nors naudojant ją paslaugai atsiranda papildomų sąnaudų. Naudojant „Python“ didelio pralaidumo paslaugai, palyginti su vietiniu bibliotekos vykdymu, išaugo tinklo delsa ir gerokai padidėjo CPU bei atminties mastelio plėtimo sąnaudos. Be to, supratome, kad šimtą kartų didesniu mastu „Python“ neefektyvumas būtų nepriimtinas, todėl galiausiai beveik neabejotinai tektų viską perrašyti.
Vis dėlto tai laikėme strateginiu techninės skolos prisiėmimu. Tuomet svarbiausia buvo ne optimizuoti sąnaudas ar išteklius, o pašalinti kliūtis produktų kūrėjams ir užtikrinti platformos stabilumą. Trumpam susitaikę su prastesniu „Python“ paslaugos našumu, galėjome spręsti aktualesnius uždavinius, įtvirtinti pagrindines API ir sukurti patikimą infrastruktūrą.
Taip pat apgalvotai tikėjomės, kad sparti mūsų programavimo modelių pažanga ateityje supaprastins techninį kelią. Tikėjomės, kad, kai reikės visiškai atsisakyti „Python“, Codex ir GPT leis sėkmingai atlikti šį perkėlimą. Galiausiai šis sprendimas pasiteisino.
Našumo požiūriu „Habitat“ vykdymas kaip „Python“ paslaugos nebuvo optimalus, bet tai buvo būtinas pasirinkimas. „Python“ leidžia dirbti greitai, tačiau tai nereiškia, kad galėjome nepaisyti atsargumo ir susitaikyti su gerokai didesne delsa. Kai vidutinė naudotojo užklausa lemia šimtus kreipinių į duomenų bazę, naudotojas pajunta būtent lėčiausio kreipinio delsą. Nustatėme, kad pagrindinis tokio masto „Python“ paslaugos iššūkis – valdyti ribinių užklausų delsą.
„Asyncio“ padeda „Python“ vienu metu vykdyti įvesties ir išvesties ribojamus darbus, tačiau neapeina „Python“ GIL ir nesuteikia CPU lygiagretumo. Be intensyvaus įvesties ir išvesties užklausų tarpinio perdavimo, „Habitat“ atlieka daug CPU imlių ir foninių užduočių: maršruto parinkimą, glaudinimą, šifravimą, kontrolinių sumų skaičiavimą, susijusių sistemų būklės tikrinimą, užklausų dubliavimą ir rezervavimą.
Kai paslaugoje tiek daug CPU imlių ir foninių užduočių, „asyncio“ planavimo delsa gali lengvai tapti pagrindine ribinių užklausų delsos priežastimi. Prieš optimizuodami pradinį paslaugos paleidimą p99 ir didesnės delsos užklausų pėdsakuose matėme, kad saugykla atsakydavo greitai, tačiau užklausos dažnai strigdavo laukdamos, kol atsakinga korutina bus vėl suplanuota atsakui išanalizuoti.
03 pav. · „asyncio“ delsos stebėjimas
Vienalaikiškumas nėra CPU lygiagretumas
„Python asyncio“ leidžia vienu metu apdoroti užklausas, tačiau CPU gijoje vienu metu vykdoma tik viena užklausa. Kai CPU tenka atlikti daug darbo, tai smarkiai padidina užklausų delsą.
Maža CPU apkrova
Trumpi „Python“ etapai; įvesties ir išvesties laukimas persidengiaDidelė CPU apkrova
Ilgi „Python“ etapai verčia parengtus atsakus lauktiNustatėme, kad OpenAI „Python“ paslaugoms, be įprastų atminties, CPU, tinklo ir disko naudojimo bei prisotinimo rodiklių, būtina stebėti „asyncio“ ciklą ir jo apkrovą, o tada atitinkamai optimizuoti.
Periodiškai planuodami fonines užduotis ir fiksuodami skirtumą tarp numatyto bei faktinio vykdymo laiko, galime realiuoju laiku empiriškai išmatuoti įvykių ciklo planavimo delsą. Esant didelei apkrovai ir daug brangių užduočių, net nedidelio vienu metu vykdomų užklausų skaičiaus vienam procesui pakanka dideliems planavimo svyravimams sukelti – iki šimtų milisekundžių, o retais atvejais ir kelių sekundžių.
Todėl kiekvienam procesui leidžiame vienu metu aptarnauti tik nedaug užklausų, o „Python“ vykdyklių procesų skaičių smarkiai didiname horizontaliai.
Pirmą kartą paleidę paslaugą, tiesiogiai profiliuodami CPU nustatėme vieną pagrindinių didelės „asyncio“ delsos ir kartu ribinių užklausų delsos priežasčių: periodinį funkcijų žymų konfigūracijų JSON analizavimą per „Statsig“ – funkcijų žymų valdymo, A/B bandymų ir kitų funkcijų įrankį.
Pagal numatytąsias nuostatas „Statsig“ kas minutę be laiko sklaidos tikrino, ar yra atnaujintų konfigūracijų, o konfigūracijoje buvo visos kiekvienos paslaugos produkcinės taisyklės. Be to, siekiant didesnio CPU naudojimo ir mažesnės delsos, buvo nuspręsta kiekviename pode vykdyti iki 8 „Python“ procesų. Dėl šių veiksnių kiekvieną minutę ateidavo akimirka, kai visi podo vykdykliai nustodavo apdoroti vykdomas užklausas ir CPU ciklus skirdavo milžiniškam konfigūracijos failui analizuoti.
CPU profiliavimu nustačius pagrindinę priežastį, sprendimas buvo paprastas: diegti mažesnę tikslinę konfigūraciją, pailginti atnaujinimo intervalą ir tokioms foninėms užduotims pridėti laiko sklaidą.
Siekiant mažos „asyncio“ delsos, taip pat būtina gerai subalansuoti užklausų apkrovą tarp serverio procesų; netinkamai sureguliuotas jungčių telkimas gali tam trukdyti.
Telkiant jungtis kliento pusėje, vienas daug vienalaikių užklausų siunčiantis kliento procesas gali užmegzti vos kelias serverio jungtis ir visą savo apkrovą nukreipti vos keliems procesams. Prieš pakoreguojant apkrovos balansavimą paslaugos naudojimas labai svyravo: kai kurie ribiniai procesai aptarnaudavo 5–10 kartų daugiau vienalaikių užklausų nei vidutiniškai.
Tai atsitiktinai pastebėjome per incidentą: nors sustabdėme dalį paslaugos perkrovusį klientą, dalies procesų veikimas dar ilgai po srauto pliūpsnio neatsigavo. Iš tiesų pastebėjome, kad jų būklė nevaldomai blogėjo: iki pat paleidimo iš naujo jie gaudavo vis daugiau užklausų. Perkrovus podą, kažkoks mechanizmas nukreipdavo į jį dar daugiau srauto. Kai kuriems komandos nariams tokio pobūdžio triktis buvo gerai pažįstama iš ankstesnio darbo: metastabilioji triktis(atsidaro naujame lange).
Įtarėme jungčių telkinį ir šį įtarimą patikrinome apribodami ilgiausią jungties pakartotinio naudojimo trukmę. Tai iš tiesų pristabdė blogėjimą ir patvirtino tyrimo kryptį. Tolesnis tyrimas parodė, kad „Python aiohttp TCPConnector“ pagal numatytąsias nuostatas jungtis pakartotinai naudoja LIFO tvarka: kitai užklausai pasirenkama vėliausiai grįžusi jungtis. Paprastai tai pagrįsta numatytoji nuostata: pakartotinai naudojant naujausias jungtis, srauto pliūpsniams sukurtos papildomos jungtys gali neveikdamos sulaukti skirtojo laiko pabaigos, todėl sumažėja jų palaikymo sąnaudos. Tačiau šiuo atveju tai sukėlė metastabiliąją triktį. Per užklausų pliūpsnį lėtesniems perkrautiems serveriams skirtos užklausos jungtis į telkinį grąžindavo vėliau, todėl vėlesnės užklausos jas rinkdavosi dažniau ir vis daugiau srauto pamažu sutelkdavo jau sunkumų patiriančiuose poduose. Pakeitus jungčių telkinį, kad jungtys būtų pakartotinai naudojamos FIFO tvarka, šis grįžtamojo ryšio ciklas nutrūko ir net sumažėjo nusistovėjusios būsenos užklausų sklaida.
04A pav. · Kliento jungčių telkimas
LIFO grąžina naują darbą lėtam procesui
Po užklausų pliūpsnio lėtesni serveriai grąžina jungtis į telkinį paskutiniai. LIFO skatina daugiau darbo sutelkti tuose pačiuose lėtesniuose serveriuose.
Pradinis pliūpsnis pasiekia A, B ir lėtesnį procesą C.
04B pav. · Kliento jungčių telkimas
FIFO nutraukia jungčių pakartotinio naudojimo grįžtamojo ryšio ciklą
Po užklausų pliūpsnio FIFO išlaiko daugiau aktyvių jungčių, bet sąžiningai subalansuoja darbus visuose serveriuose.
Pradinis pliūpsnis pasiekia A, B ir lėtesnį procesą C.
Šiandien visoje OpenAI infrastruktūroje jungtims telkti ir geresnėms, serverių apkrovą įvertinančioms balansavimo strategijoms daugiausia naudojame „Istio“ bei „Envoy“, todėl šios problemos apskritai išvengiame.
Viena optimizavimo mažai „asyncio“ delsai ir didelio „Python“ procesų skaičiaus pasekmių – daugybe jungčių labai lengva perkrauti susijusias sistemas. Šis reiškinys vadinamas „griausminga banda“.
Įprastas kasdienis diegimas, jei nesureguliuotas vykti lėtai, dėl jungčių keitimo gali sukelti didelių CPU apkrovos svyravimų. O jungčių nuotėkis, prisotinęs NAT tinklų sietuvą, gali išjungti tinklą. Tokios problemos pasitaiko ir kitose paslaugose, tačiau dėl dešimteriopai didesnio procesų skaičiaus jų sužadinimo slenkstis gerokai sumažėja. Dažnai prisotinami su tinklu susiję ištekliai, kurių klientai, vertindami vien gryną pralaidumą, nesitiki turėsiantys valdyti nusistovėjusioje būsenoje.
„Envoy“ taip pat naudojame jungtims kuo labiau sutelkti. Juo „Python“ HTTP/1 jungtis naujiname į HTTP/2, kad išnaudotume sutankinimą, tada šias jungtis telkiame ir pratęsiame jų gyvavimo trukmę. „Envoy“ taip pat suteikia vieną centrinę vietą spartos ribotuvams ir grandinės pertraukikliams įdiegti – kiekviename atskirame „Python“ procese jie būtų mažiau veiksmingi.
05 pav. · Jungčių sutelkimas
Tos pačios užklausos, mažiau jungčių
Jungčių telkimas ir HTTP/2 jungčių sutankinimas padeda sumažinti susijusių sistemų jungčių apkrovą.
Viena priežasčių, kodėl galėjome taip išplėsti „Python“, buvo ribota „Habitat“ API, užtikrinanti nuspėjamas užklausų sąnaudas. Užuot leidusi klientams kurti bet kokias SQL užklausas, galinčias nuskaityti dideles lenteles ar jungti daug lentelių, „Habitat“ pateikia paprastą NoSQL API. Ribotos API galimybės yra sąmoningas „Habitat“ architektūros kompromisas.
Siekiame optimizuoti paprastas, nuspėjamas ir pastovaus darbo užklausas. Mūsų patirtis rodo, kad tokias sistemas gerokai lengviau plėsti, o netinkamai sukonfigūruoti ar naudoti – sunku. Nenuspėjamo išsišakojimo užklausos kelia eksploatavimo pavojų: jos apsunkina izoliavimą ir apkrovos balansavimą, taip pat sukuria staigius delsos šuolius, kuriuos sunku valdyti tiek paslaugai, tiek jos klientams.
Prieš pereinant prie „Habitat“ ir „Azure Cosmos DB“, dauguma OpenAI internetinių duomenų buvo saugomi „Postgres“. Tuo metu prieš diegiant produkcinėje aplinkoje buvo lengva peržiūrėti visus užklausų ir schemų pakeitimus bei įsitikinti, kad jie veikia tinkamai ir naudoja indeksuotus duomenis. Augant komandai ir produktams, tai greitai tapo nebevaldoma ir dažnai sukeldavo sutrikimų, kai viena brangi nauja užklausa intensyviai naudojamame kelyje išjungdavo duomenų bazę.
Problema – sąnaudų disbalansas: brangiai ir sunkiai vykdomas SQL užklausas parašyti pigu ir lengva. „Habitat“ to išvengiame, o brangios užklausos kliento pusėje tampa itin akivaizdžios. Nėra neribotų užklausų, galinčių perkrauti „Habitat“, o sudėtingoms jungtims ir grafo perėjimams produktų komandos turi atlikti dalį sunkaus darbo, todėl sprendimai apskritai tampa efektyvesni.
„Habitat“ pateikia pagal klientų apibrėžtus objektų ir briaunų tipus sumodeliuotą NoSQL API, įkvėptą TAO(atsidaro naujame lange). Klientai iš anksto apibrėžia objektus, briaunas ir jų tarpusavio ryšius, bet ne kiekvieno tipo turinį. Gauti ryšiai primena grafą, tačiau pati „Habitat“ nepalaiko įprastų grafo perėjimo užklausų, išskyrus konkretaus objekto tiesioginių briaunų užklausas.
Grafą skaidome taip, kad kiekvienas objektas ir atitinkamos jo briaunos būtų tame pačiame saugyklos lygmens skaidinyje, tačiau duomenų bazės lygmeniu sąmoningai nesiekiame kartu laikyti objektų ir nuotolinių objektų, į kuriuos rodo jų briaunos. Todėl modelį lengva skaidyti horizontaliam mastelio didinimui, tačiau grafo perėjimai neefektyvūs: kiekvienam žingsniui tarp objektų gali reikėti gauti duomenis iš dviejų visiškai skirtingų, kituose regionuose esančių „Azure Cosmos DB“ paskyrų.
Sudėtingesnių užklausų turintiems klientams teikiame ir per „Rockset“ pasiekiamą autonominį antrinį „Habitat“ rodinį. Naudodami keičiamų duomenų fiksavimą (CDC), beveik realiuoju laiku srautiniu būdu perduodame internetinės saugyklos pakeitimus į izoliuotus „Rockset“ egzempliorius. Kiekviena kliento komanda pati turi plėsti savo „Rockset“ egzempliorių pagal sudėtingų užklausų poreikius.
Šis „Rockset“ parengimas sukelia klientams papildomų nepatogumų, bet šiuo metu laikome tai tinkamu kompromisu: paprastos užklausos yra numatytosios, o sudėtingų užklausų naudotojams paliekama alternatyva. Tokia architektūra izoliuoja internetinę saugyklą nuo daug skaitančių analitinių ir paieškos darbų.
Metams atidėję „Python“ sistemos perrašymą, itin spartaus augimo laikotarpiu galėjome susitelkti į skubesnius ir svarbesnius iššūkius. Platformai bręstant, augimui vis spartėjant ir šiai paslaugai pagal branduolių skaičių tapus antra didžiausia OpenAI paslauga, o pagal „Envoy“ apimtį – ketvirta, pagaliau atėjo metas atsisakyti „Python“. Piko metu „Python“ padėjo aptarnauti daugiau kaip 20 milijonų užklausų per sekundę.
2026 m. antrąjį ketvirtį vos 2 inžinieriai, padedami Codex ir GPT‑5.5, perrašė visą paslaugą „Rust“ kalba. Naujoji „Rust“ paslauga dabar apdoroja 95 % produkcinių užklausų; per kelias ateinančias savaites visiškai atsisakysime „Python“. Mūsų duomenimis, „Rust“ paslauga CPU naudoja 6 kartus, o atmintį – 15 kartų efektyviau nei „Python“ versija; jos vidutinė ir ribinė delsa taip pat gerokai mažesnė. Daugiau įžvalgų ketiname paskelbti būsimame tinklaraščio įraše.
„Python“, o dabar „Rust“, paslauga yra tik viena „Habitat“ dalis. Antroje šios serijos dalyje apie tai, kaip sparčiai išplėtėme internetinę saugyklą daugiau kaip 1 milijardui ChatGPT naudotojų aptarnauti, aptarsime saugyklos lygmenį ir tai, kaip „Habitat“ aptarnauja daugiau kaip 500 petabaitų duomenų bei per 70 milijonų užklausų per sekundę.
Jei norite dirbti su priešakinio masto OLTP sistemomis ir jus domina tokia inžinerija, peržiūrėkite laisvą darbo vietą mūsų komandoje.


