Čau Habr!
Ņemot vērā pašreizējos notikumus koronavīrusa dēļ, vairāki interneta pakalpojumi ir sākuši piedzīvot palielinātu slodzi. Piemēram, , jo nebija pietiekamas jaudas. Un ne vienmēr ir iespējams paātrināt serveri, vienkārši pievienojot jaudīgāku aparatūru, taču klientu pieprasījumi ir jāapstrādā (citādi tie nonāks pie konkurentiem).
Šajā rakstā es īsumā apspriedīšu populāras prakses, kas palīdzēs jums izveidot ātru un pret kļūmēm izturīgu pakalpojumu. Tomēr no iespējamām izstrādes shēmām esmu izvēlējies tikai tās, kas pašlaik tiek izmantotas. viegli lietojamsKatram vienumam ir vai nu gatavas bibliotēkas, vai arī problēmu var atrisināt, izmantojot mākoņplatformu.
Horizontālā mērogošana
Šis ir vienkāršākais un vispazīstamākais punkts. Vispārīgi runājot, divas visizplatītākās slodzes sadales shēmas ir horizontālā un vertikālā mērogošana. Jūs ļaujat pakalpojumiem darboties paralēli, tādējādi sadalot slodzi starp tiem. Jūs pasūtāt jaudīgākus serverus vai optimizējat kodu.
Kā piemēru ņemšu abstraktu mākoņa failu krātuvi, tas ir, kādu OwnCloud, OneDrive utt. analogu.
Tipiska šādas shēmas diagramma ir redzama zemāk, taču tā tikai parāda sistēmas sarežģītību. Galu galā mums kaut kā ir jāsinhronizē pakalpojumi. Kas notiek, ja lietotājs saglabā failu no planšetdatora un pēc tam vēlas to skatīt tālrunī?

Atšķirība starp pieejām ir tāda, ka vertikālajā mērogošanā mēs esam gatavi palielināt mezglu jaudu, savukārt horizontālajā mērogošanā mēs esam gatavi pievienot jaunus mezglus, lai sadalītu slodzi.
CQRS
Šis ir diezgan svarīgs modelis, jo tas ļauj dažādiem klientiem ne tikai izveidot savienojumu ar dažādiem pakalpojumiem, bet arī saņemt vienas un tās pašas notikumu plūsmas. Tā priekšrocības nav tik acīmredzamas vienkāršai lietojumprogrammai, bet tas ir ārkārtīgi svarīgi (un vienkārši) noslogotam pakalpojumam. Tā būtība ir tāda, ka ienākošajām un izejošajām datu plūsmām nevajadzētu krustoties. Tas ir, jūs nevarat nosūtīt pieprasījumu un sagaidīt atbildi; tā vietā jūs nosūtāt pieprasījumu pakalpojumam A, bet saņemat atbildi no pakalpojuma B.
Šīs pieejas pirmā priekšrocība ir spēja pārtraukt savienojumus (vārda plašā nozīmē) gara vaicājuma laikā. Piemēram, ņemsim vairāk vai mazāk standarta secību:
- Klients nosūtīja pieprasījumu serverim.
- Serveris sāka ilgu apstrādi.
- Serveris atbildēja klientam ar rezultātu.
Iedomāsimies, ka 2. darbībā savienojums tika pārtraukts (vai nu tīkla savienojums tika atjaunots, vai arī lietotājs pārgāja uz citu lapu, pārtraucot savienojumu). Šādā gadījumā serverim būtu grūti nosūtīt lietotājam atbildi, informējot viņu par to, kas tieši tika apstrādāts. Izmantojot CQRS, secība nedaudz atšķirtos:
- Klients ir abonējis atjauninājumus.
- Klients nosūtīja pieprasījumu serverim.
- Serveris atbildēja ar tekstu "pieprasījums pieņemts".
- Serveris atbildēja ar rezultātu, izmantojot kanālu no punkta "1".

Kā redzat, shēma ir nedaudz sarežģītāka. Turklāt šeit nav intuitīva pieprasījuma-atbildes pieejas. Tomēr, kā redzat, savienojuma pārtraukšana pieprasījuma apstrādes laikā neradīs kļūdu. Turklāt, ja lietotājs faktiski ir izveidojis savienojumu ar pakalpojumu no vairākām ierīcēm (piemēram, mobilā tālruņa un planšetdatora), ir iespējams noorganizēt atbildes nosūtīšanu uz abām ierīcēm.
Interesanti, ka ienākošo ziņojumu apstrādes kods kļūst vienāds (nevis 100%) gan klienta paša ietekmētiem notikumiem, gan citiem notikumiem, tostarp no citiem klientiem.
Tomēr patiesībā mēs iegūstam papildu priekšrocības no tā, ka vienvirziena plūsmu var apstrādāt funkcionāli (izmantojot RX un līdzīgas metodes). Tā ir būtiska priekšrocība, jo lietojumprogrammu būtībā var padarīt pilnībā reaktīvu, pat izmantojot funkcionālu pieeju. Liela mēroga programmām tas var ievērojami samazināt izstrādes un atbalsta resursus.
Šīs pieejas apvienošana ar horizontālo mērogošanu sniedz papildu priekšrocību, jo pieprasījumus var nosūtīt uz vienu serveri un saņemt atbildes no cita. Tas ļauj klientam izvēlēties sev vēlamo pakalpojumu, kamēr pamatā esošā sistēma joprojām var pareizi apstrādāt notikumus.
Pasākumu piegāde
Kā zināms, viena no izkliedētas sistēmas galvenajām iezīmēm ir kopīga laika vai kopīgas kritiskās sadaļas neesamība. Vienam procesam var ieviest sinhronizāciju (izmantojot savstarpējas izpildes (mutex)), nodrošinot, ka neviens cits neizpilda to pašu kodu. Tomēr izkliedētai sistēmai tas ir bīstami, jo tas prasa papildu resursus un ir pretrunā ar mērogojamības mērķi — visi komponenti joprojām gaidīs vienu un to pašu.
Tas noved mūs pie svarīga fakta: ātri izkliedētu sistēmu nevar sinhronizēt, jo tas samazinātu veiktspēju. No otras puses, mums bieži ir nepieciešama zināma saskaņotības pakāpe starp komponentiem. Un šim nolūkam mēs varam izmantot pieeju ar , kur tiek garantēts, ka, ja noteiktā laika periodā pēc pēdējās atjaunināšanas ("galu galā") dati netiek mainīti, visi vaicājumi atgriezīs pēdējo atjaunināto vērtību.
Ir svarīgi saprast, ka klasiskajām datubāzēm to diezgan bieži izmanto , kur katram mezglam ir viena un tā pati informācija (tas bieži tiek panākts, ja darījums tiek uzskatīts par izveidotu tikai pēc otrā servera atbildes). Šeit ir dažas atvieglojumi izolācijas līmeņu dēļ, taču vispārējā ideja paliek nemainīga — jūs varat dzīvot pilnīgi konsekventā pasaulē.
Bet atgriezīsimies pie sākotnējās problēmas. Ja daļu sistēmas var uzbūvēt ar , tad var konstruēt šādu diagrammu.

Šīs pieejas svarīgākās iezīmes:
- Katrs ienākošais pieprasījums tiek ievietots vienā rindā.
- Apstrādājot pieprasījumu, pakalpojums var ievietot uzdevumus arī citās rindās.
- Katram ienākošajam notikumam ir ID (kas ir nepieciešams deduplikācijai).
- Rinda darbojas pēc principa "tikai pievienot". Elementus nevar noņemt vai mainīt to secību.
- Rinda darbojas pēc FIFO (pirmais iekšā, pirmais ārā) principa. Ja nepieciešama paralēla izpilde, objekti jāpārvieto uz dažādām rindām vienā posmā.
Atgādināšu, ka mēs apsveram tiešsaistes failu glabāšanu. Šajā gadījumā sistēma izskatītos apmēram šādi:

Ir svarīgi atzīmēt, ka diagrammā redzamie pakalpojumi ne vienmēr apzīmē atsevišķus serverus. Tie var pat koplietot vienu un to pašu procesu. Svarīgi ir tas, lai šīs lietas būtu ideoloģiski atdalītas tādā veidā, lai horizontālu mērogošanu varētu viegli ieviest.
Un diviem lietotājiem diagramma izskatīsies šādi (pakalpojumi, kas paredzēti dažādiem lietotājiem, ir atzīmēti dažādās krāsās):

Bonusi no šādas kombinācijas:
- Informācijas apstrādes pakalpojumi ir atdalīti. Arī rindas ir atdalītas. Ja mums ir jāpalielina sistēmas caurlaidspēja, mums vienkārši ir jāuzsāk vairāk pakalpojumu uz vairāk serveriem.
- Kad saņemam informāciju no lietotāja, mums nav jāgaida, kamēr dati ir pilnībā saglabāti. Tā vietā mēs vienkārši atbildam ar "Labi" un pēc tam pakāpeniski sākam apstrādi. Rinda arī izlīdzina slodzes maksimumu, jo jauna objekta pievienošana notiek ātri, un lietotājam nav jāgaida, kamēr viss cikls ir pabeigts.
- Kā piemēru esmu pievienojis deduplikācijas pakalpojumu, kas mēģina apvienot identiskus failus. Ja 1% gadījumu tas aizņem ilgu laiku, klients to tikpat kā nepamanīs (skatīt iepriekš), kas ir liels pluss, jo mums vairs nav nepieciešams 100% ātrums un uzticamība.
Tomēr trūkumi ir uzreiz pamanāmi:
- Mūsu sistēmai vairs nav stingras konsekvences. Tas nozīmē, ka, piemēram, ja abonējat dažādus pakalpojumus, teorētiski varētu saņemt dažādus stāvokļus (jo vienam no pakalpojumiem, iespējams, nav laika saņemt paziņojumu no iekšējās rindas). Vēl viena sekas ir tā, ka sistēmai vairs nav kopīga laika. Tas nozīmē, piemēram, ka nav iespējams kārtot visus notikumus vienkārši pēc ierašanās laika, jo pulksteņi starp serveriem var nebūt sinhronizēti (patiesībā viens un tas pats laiks divos serveros ir utopija).
- Nevienu notikumu vairs nevar vienkārši atsaukt (kā tas būtu ar datubāzi). Tā vietā ir jāpievieno jauns notikums — , kas mainīs pēdējo stāvokli uz vēlamo. Kā piemērs no līdzīgas jomas: bez vēstures pārrakstīšanas (kas dažos gadījumos ir slikti), Git nevar atsaukt izmaiņu (commit), bet var izveidot īpašu , kas būtībā vienkārši atgriež veco stāvokli. Tomēr gan kļūdainā pievienošana, gan atcelšana tiks saglabāta vēsturē.
- Datu shēma var mainīties no vienas izlaiduma uz otru, taču vecos notikumus vairs nevarēs atjaunināt atbilstoši jaunajam standartam (jo notikumus principā nevar mainīt).
Kā redzat, notikumu avoti labi darbojas ar CQRS. Turklāt sistēmas ieviešana ar efektīvām un ērtām rindām, neatdalot datu plūsmas, ir principiāli sarežģīta, jo tas prasītu sinhronizācijas punktu pievienošanu, kas neitralizētu rindu pozitīvo ietekmi. Izmantojot abas pieejas vienlaikus, ir nepieciešamas nelielas korekcijas programmas kodā. Mūsu gadījumā, nosūtot failu uz serveri, atbilde atgriež tikai "OK", kas vienkārši nozīmē "faila pievienošanas darbība ir saglabāta". Tehniski tas nenozīmē, ka dati jau ir pieejami citās ierīcēs (piemēram, deduplikācijas pakalpojums varētu atjaunot indeksu). Tomēr pēc kāda laika klients saņems paziņojumu "Fails X ir saglabāts".
Rezultātā:
- Failu sūtīšanas statusu skaits pieaug: klasiskā "fails nosūtīts" vietā tagad redzam divus: "fails pievienots servera rindai" un "fails saglabāts krātuvē". Pēdējais nozīmē, ka citas ierīces tagad var sākt saņemt failu (ar atrunu, ka rindas darbojas ar atšķirīgu ātrumu).
- Tā kā iesniegšanas informācija tagad tiek saņemta, izmantojot dažādus kanālus, mums ir jāizstrādā risinājumi, lai iegūtu faila apstrādes statusu. Tā rezultātā, atšķirībā no klasiskās pieprasījuma-atbildes metodes, klientu var restartēt faila apstrādes laikā, bet apstrādes statuss paliks pareizs. Turklāt šī funkcija būtībā darbojas uzreiz pēc instalēšanas. Tā rezultātā mēs tagad esam tolerantāki pret kļūmēm.
Sharding
Kā aprakstīts iepriekš, sistēmām ar notikumu avotu trūkst stingras konsekvences. Tas nozīmē, ka mēs varam izmantot vairākas atmiņas ierīces bez jebkādas sinhronizācijas starp tām. Pieejot mūsu uzdevumam, mēs varam:
- Atdaliet failus pēc veida. Piemēram, attēlus/video var dekodēt un izvēlēties efektīvāku formātu.
- Atsevišķi konti pa valstīm. Daudzi likumi to var pieprasīt, taču šī arhitektūra to atļauj automātiski.

Ja vēlaties migrēt datus no vienas krātuves uz citu, standarta rīki nederēs. Diemžēl šajā gadījumā jums ir jāaptur rinda, jāveic migrācija un pēc tam tā jārestartē. Parasti datus nevar migrēt "uzreiz". Tomēr, ja notikumu rinda ir saglabāta pilnībā un jums ir iepriekšējo krātuves stāvokļu momentuzņēmumi, varat atkārtot notikumus šādi:
- Notikumu avotā (Event Source) katram notikumam ir savs identifikators (ideālā gadījumā nedilstošs). Tas nozīmē, ka krātuvei varam pievienot lauku — pēdējā apstrādātā elementa ID.
- Mēs dublējam rindu, lai visus notikumus varētu apstrādāt vairākās neatkarīgās krātuves vietās (pirmā ir tā, kurā pašlaik tiek glabāti dati, bet otrā ir jauna, bet pašlaik tukša). Otrā rinda, protams, pašlaik netiek apstrādāta.
- Mēs sākam otro rindu (tas ir, mēs sākam notikumu atskaņošanu).
- Kad jaunā rinda ir relatīvi tukša (t. i., vidējā laika starpība starp elementa pievienošanu un tā izgūšanu ir pieņemama), lasītāji var sākt pārslēgties uz jauno krātuvi.
Kā redzat, mūsu sistēmai nekad nav bijusi un joprojām nav stingras konsekvences. Pastāv tikai eventuāla konsekvence, kas garantē, ka notikumi tiek apstrādāti vienā secībā (lai gan, iespējams, ar atšķirīgu latentumu). Izmantojot to, mēs varam relatīvi viegli pārsūtīt datus uz otru pasaules malu, neapturot sistēmu.
Tādējādi, turpinot mūsu tiešsaistes failu glabāšanas piemēru, šī arhitektūra jau sniedz mums vairākus bonusus:
- Mēs varam dinamiski pārvietot objektus tuvāk lietotājiem, tādējādi uzlabojot pakalpojumu kvalitāti.
- Dažus datus varam glabāt uzņēmumu iekšienē. Piemēram, uzņēmumu lietotājiem bieži ir nepieciešams, lai viņu dati tiktu glabāti kontrolētos datu centros (lai novērstu datu noplūdes). Ar shardingu mēs to varam viegli atbalstīt. Šis uzdevums tiek vēl vairāk vienkāršots, ja klientam ir saderīgs mākonis (piemēram, ).
- Un pats galvenais, mums tas nav jādara. Galu galā, sākumā viena krātuve visiem kontiem būtu pilnīgi piemērota (lai ātrāk sāktu). Un šīs sistēmas galvenā iezīme ir tā, ka, lai gan tā ir paplašināma, sākumā tā ir diezgan vienkārša. Jums vienkārši nav uzreiz jāraksta kods, kas apstrādā miljonu atsevišķu neatkarīgu rindu utt. Ja nepieciešams, mēs to varam izdarīt vēlāk.
Statiskā satura mitināšana
Šis punkts var šķist pilnīgi acīmredzams, taču tas joprojām ir būtisks vairāk vai mazāk standarta, augstas noslodzes lietojumprogrammai. Tā būtība ir vienkārša: viss statiskais saturs netiek pasniegts no tā paša servera, kurā atrodas lietojumprogramma, bet gan no īpašiem serveriem, kas īpaši paredzēti šim uzdevumam. Tā rezultātā šīs darbības tiek veiktas ātrāk (piemēram, Nginx apkalpo failus ātrāk un ar zemākām izmaksām nekā Java serveris). Turklāt pastāv CDN arhitektūra () ļauj mums novietot failus tuvāk gala lietotājiem, kas pozitīvi ietekmē pakalpojuma lietošanas ērtumu.
Vienkāršākais un standarta statiskā satura piemērs ir tīmekļa vietnes skriptu un attēlu kolekcija. Tas ir vienkārši — tie ir zināmi iepriekš, un arhīvs pēc tam tiek augšupielādēts CDN serveros, no kurienes tas tiek piegādāts gala lietotājiem.
Tomēr praksē statiskajam saturam var izmantot pieeju, kas ir nedaudz līdzīga lambda arhitektūrai. Atgriezīsimies pie mūsu uzdevuma (tiešsaistes failu glabāšanas), kurā mums ir jāizplata faili lietotājiem. Vienkāršākais tiešais risinājums ir izveidot pakalpojumu, kas veic visas nepieciešamās pārbaudes (autorizāciju utt.) katram lietotāja pieprasījumam un pēc tam lejupielādē failu tieši no mūsu krātuves. Šīs pieejas galvenais trūkums ir tas, ka statisko saturu (un fails ar noteiktu versiju būtībā ir statisks saturs) apkalpo tas pats serveris, kas satur biznesa loģiku. Tā vietā mēs varam izmantot šādu shēmu:
- Serveris nodrošina lejupielādes URL. Tas var būt formātā file_id + key, kur key ir miniatūrs digitālais paraksts, kas piešķir piekļuvi resursam nākamo 24 stundu laikā.
- Failu izplatīšanu apstrādā vienkāršs nginx ar šādām opcijām:
- Satura kešatmiņa. Tā kā šo pakalpojumu var mitināt atsevišķā serverī, mēs esam nodrošinājuši nākotnes drošību, saglabājot visus nesen lejupielādētos failus diskā.
- Atslēgas pārbaude, veidojot savienojumu
- Pēc izvēles: straumēta satura apstrāde. Piemēram, ja mēs saspiežam visus pakalpojuma failus, mēs varam tos dekompresēt tieši šajā modulī. Tā rezultātā IO operācijas tiek veiktas tur, kur tām jābūt. Java arhivētājs viegli piešķirtu daudz nevajadzīgas atmiņas, taču pakalpojuma pārrakstīšana ar biznesa loģiku parastajā Rust/C++ valodā arī varētu būt neefektīva. Mūsu gadījumā mēs izmantojam dažādus procesus (vai pat pakalpojumus), kas nozīmē, ka mēs varam efektīvi atdalīt biznesa loģiku un IO operācijas.

Šī shēma īsti neatgādina statiska satura apkalpošanu (jo mēs neaugšupielādējam visu statisko pakotni kaut kur), taču patiesībā šī pieeja precīzi apkalpo nemaināmus datus. Turklāt šo shēmu var vispārināt uz citiem gadījumiem, kad saturs nav vienkārši statisks, bet gan to var attēlot kā nemaināmu un nenoņemamu bloku kopu (lai gan tos var pievienot).
Vēl viens piemērs (lai pastiprinātu): ja esat strādājis ar Jenkins vai TeamCity, jūs zināt, ka abi risinājumi ir rakstīti Java valodā. Abi ir Java procesi, kas apstrādā gan būvējuma orķestrēšanu, gan satura pārvaldību. Konkrēti, abiem ir tādi uzdevumi kā "faila/mapes pārsūtīšana no servera". Piemēri ietver artefaktu piegādi, pirmkoda pārsūtīšanu (kad aģents nelejupielādē kodu tieši no repozitorija, bet serveris to dara tā vietā) un žurnāla piekļuvi. Visiem šiem uzdevumiem ir atšķirīga IO slodze. Tas nozīmē, ka serverim, kas atbild par sarežģītu biznesa loģiku, ir jāspēj arī efektīvi pārsūtīt lielas datu plūsmas caur sevi. Un visinteresantākais ir tas, ka šo darbību var deleģēt Nginx, izmantojot tieši to pašu shēmu (izņemot to, ka datu atslēga ir jāpievieno pieprasījumam).
Tomēr, ja atgriežamies pie savas sistēmas, iegūstam līdzīgu shēmu:

Kā redzat, sistēma ir kļuvusi radikāli sarežģītāka. Tā vairs nav tikai mini process, kas lokāli glabā failus. Tagad tai ir nepieciešams sarežģīts atbalsts, API versiju kontrole utt. Tāpēc pēc visu diagrammu uzzīmēšanas vislabāk ir rūpīgi izvērtēt, vai mērogojamība ir ieguldījumu vērta. Tomēr, ja vēlaties paplašināt sistēmu (tostarp, lai apstrādātu vēl lielāku lietotāju skaitu), jums būs jāizmanto šādi risinājumi. Tomēr rezultātā sistēmas arhitektūra ir sagatavota palielinātai slodzei (praktiski katru komponentu var klonēt horizontālai mērogošanai). Sistēmu var atjaunināt, to neizslēdzot (dažas darbības vienkārši būs nedaudz lēnākas).
Kā jau minēju pašā sākumā, vairāki tiešsaistes pakalpojumi pašlaik piedzīvo palielinātu slodzi. Un daži no tiem vienkārši pārstāja darboties pareizi. Būtībā sistēmas avarēja tieši brīdī, kad uzņēmumiem vajadzētu pelnīt. Tāpēc, tā vietā, lai atliktu piegādi, tā vietā, lai piedāvātu klientiem "ieplānot piegādi nākamajiem mēnešiem", sistēma vienkārši teica: "Dodieties pie konkurentiem." Tā patiesībā ir zemas produktivitātes cena: zaudējumi rodas tieši tad, kad peļņa būtu vislielākā.
Secinājums
Visas šīs pieejas pastāv jau labu laiku. Piemēram, VK jau sen izmanto statiskā satura mitināšanas ideju attēlu apkalpošanai. Daudzas tiešsaistes spēles izmanto shardingu, lai atdalītu spēlētājus pa reģioniem vai spēļu atrašanās vietas (ja pati pasaule ir vienota). Notikumu sourcing tiek aktīvi izmantota e-pastā. Lielākā daļa tirdzniecības lietojumprogrammu, kas nepārtraukti saņem datus, faktiski ir veidotas, izmantojot CQRS pieeju ienākošo datu filtrēšanai. Un horizontālā mērogošana jau sen tiek izmantota daudzos pakalpojumos.
Tomēr vissvarīgākais ir tas, ka visus šos modeļus ir kļuvis ļoti viegli pielietot mūsdienu lietojumprogrammās (protams, ja tie ir piemēroti). Mākoņdatošana piedāvā shardingu un horizontālu mērogošanu uzreiz pēc instalēšanas, kas ir daudz vienkāršāk nekā atsevišķu dedikētu serveru pasūtīšana dažādos datu centros. CQRS ir kļuvis daudz vienkāršāks, kaut vai tikai pateicoties tādu bibliotēku kā RX attīstībai. Pirms desmit gadiem tikai dažas tīmekļa vietnes būtu spējušas to atbalstīt. Arī notikumu resursu pārvaldību (Event Sourcing) ir neticami viegli iestatīt, pateicoties gataviem Apache Kafka konteineriem. Pirms desmit gadiem tas būtu bijis inovatīvi; tagad tas ir ikdienišķs. Līdzīgi, statiskā satura mitināšanas gadījumā lietotājam draudzīgākas tehnoloģijas (tostarp detalizēta dokumentācija un liela atbilžu datubāze) ir padarījušas šo pieeju vēl vienkāršāku.
Tā rezultātā vairāku diezgan sarežģītu arhitektūras modeļu ieviešana tagad ir kļuvusi daudz vienkāršāka, kas nozīmē, ka ir vērts tos apsvērt jau laikus. Lai gan desmit gadus veca lietojumprogramma, iespējams, ir atteikusies no viena no iepriekš minētajiem risinājumiem augsto ieviešanas un ekspluatācijas izmaksu dēļ, jauna lietojumprogramma vai, iespējams, refaktorings tagad var izveidot pakalpojumu, kas arhitektoniski ir gan paplašināms (veiktspējas ziņā), gan gatavs jauniem klientu pieprasījumiem (piemēram, personas datu lokalizācijai).
Un patsvarīgākais: lūdzu, neizmantojiet šīs pieejas, ja jums ir vienkārša lietojumprogramma. Jā, tās ir skaistas un interesantas, taču vietnei ar maksimālo apmeklētāju skaitu 100 cilvēku bieži vien var pietikt ar klasisku monolītu (vismaz ārpusē; iekšēji visu var sadalīt moduļos utt.).
Avots: www.habr.com
