Konvenaj arkitekturaj ŝablonoj

Hej Habr!

En lumo de aktualaĵoj pro koronavirus, kelkaj interretaj servoj komencis ricevi pliigitan ŝarĝon. Ekzemple, Unu el la britaj podetalaj ĉenoj simple haltigis sian interretan mendan retejon., ĉar ne estis sufiĉe da kapacito. Kaj ne ĉiam eblas akceli servilon simple aldonante pli potencajn ekipaĵojn, sed klientpetoj devas esti procesitaj (aŭ ili iros al konkurantoj).

En ĉi tiu artikolo mi mallonge parolos pri popularaj praktikoj, kiuj permesos al vi krei rapidan kaj toleran servon. Tamen, el la eblaj evoluskemoj, mi elektis nur tiujn, kiuj estas nuntempe facile uzebla. Por ĉiu objekto, vi aŭ havas pretajn bibliotekojn, aŭ vi havas la ŝancon solvi la problemon per nuba platformo.

Horizontala skalado

La plej simpla kaj plej konata punkto. Konvencie, la plej oftaj du ŝarĝaj distribukabaloj estas horizontala kaj vertikala skalo. En la unua kazo vi permesas al servoj funkcii paralele, tiel distribuante la ŝarĝon inter ili. En la dua vi mendas pli potencajn servilojn aŭ optimumigas la kodon.

Ekzemple, mi prenos abstraktan nuban dosieron stokadon, tio estas, iun analogon de OwnCloud, OneDrive, ktp.

Norma bildo de tia cirkvito estas malsupre, sed ĝi nur montras la kompleksecon de la sistemo. Post ĉio, ni devas iel sinkronigi la servojn. Kio okazas se la uzanto konservas dosieron de la tablojdo kaj tiam volas vidi ĝin de la telefono?

Konvenaj arkitekturaj ŝablonoj
La diferenco inter la aliroj: en vertikala skalo, ni pretas pliigi la potencon de nodoj, kaj en horizontala skalado, ni estas pretaj aldoni novajn nodojn por distribui la ŝarĝon.

CQRS

Komando Demanda Respondeco Apartigo Sufiĉe grava ŝablono, ĉar ĝi permesas al malsamaj klientoj ne nur konekti al malsamaj servoj, sed ankaŭ ricevi la samajn eventofluojn. Ĝiaj avantaĝoj ne estas tiel evidentaj por simpla aplikaĵo, sed ĝi estas ege grava (kaj simpla) por okupata servo. Ĝia esenco: alirantaj kaj elirantaj datumfluoj ne devus intersekciĝi. Tio estas, vi ne povas sendi peton kaj atendi respondon; anstataŭe, vi sendas peton al servo A, sed ricevas respondon de servo B.

La unua gratifiko de ĉi tiu aliro estas la kapablo rompi la ligon (en la larĝa signifo de la vorto) dum plenumado de longa peto. Ekzemple, ni prenu pli-malpli norman sinsekvon:

  1. La kliento sendis peton al la servilo.
  2. La servilo komencis longan pretigtempon.
  3. La servilo respondis al la kliento kun la rezulto.

Ni imagu, ke en la punkto 2 la konekto rompiĝis (aŭ la reto rekonektis, aŭ la uzanto iris al alia paĝo, rompante la konekton). En ĉi tiu kazo, estos malfacile por la servilo sendi respondon al la uzanto kun informoj pri kio ĝuste estis prilaborita. Uzante CQRS, la sekvenco estos iomete malsama:

  1. La kliento abonis ĝisdatigojn.
  2. La kliento sendis peton al la servilo.
  3. La servilo respondis "peto akceptita."
  4. La servilo respondis kun la rezulto per la kanalo de la punkto "1".

Konvenaj arkitekturaj ŝablonoj

Kiel vi povas vidi, la skemo estas iom pli kompleksa. Plie, la intuicia peto-responda aliro mankas ĉi tie. Tamen, kiel vi povas vidi, konekto-rompo dum prilaborado de peto ne kondukos al eraro. Krome, se fakte la uzanto estas konektita al la servo de pluraj aparatoj (ekzemple de poŝtelefono kaj de tablojdo), vi povas certigi, ke la respondo venas al ambaŭ aparatoj.

Interese, la kodo por prilabori envenantajn mesaĝojn fariĝas la sama (ne 100%) kaj por eventoj, kiuj estis influitaj de la kliento mem, kaj por aliaj eventoj, inkluzive de tiuj de aliaj klientoj.

Tamen, fakte ni ricevas plian gratifikon pro la fakto, ke unudirekta fluo povas esti pritraktata en funkcia stilo (uzante RX kaj simile). Kaj ĉi tio jam estas serioza pluso, ĉar esence la aplikaĵo povas fariĝi tute reaktiva, kaj ankaŭ uzante funkcian aliron. Por grasaj programoj, ĉi tio povas signife ŝpari evoluajn kaj subtenajn rimedojn.

Se ni kombinas ĉi tiun aliron kun horizontala skalo, tiam kiel gratifiko ni ricevas la kapablon sendi petojn al unu servilo kaj ricevi respondojn de alia. Tiel, la kliento povas elekti la servon, kiu taŭgas por li, kaj la sistemo ene ankoraŭ povos ĝuste prilabori eventojn.

Eventa Fonto

Kiel vi scias, unu el la ĉefaj trajtoj de distribuita sistemo estas la foresto de komuna tempo, komuna kritika sekcio. Por unu procezo, vi povas fari sinkronigon (sur la samaj muteksoj), ene de kiu vi estas certa, ke neniu alia plenumas ĉi tiun kodon. Tamen, ĉi tio estas danĝera por distribuita sistemo, ĉar ĝi postulos superkoston, kaj ankaŭ mortigos la tutan belecon de skalado - ĉiuj komponantoj ankoraŭ atendos unu.

De ĉi tie ni ricevas gravan fakton - rapida distribuita sistemo ne povas esti sinkronigita, ĉar tiam ni reduktos rendimenton. Aliflanke, ni ofte bezonas certan konsistencon inter komponantoj. Kaj por tio vi povas uzi la aliron kun eventuala konsistenco, kie estas garantiite ke se ne estas datumŝanĝoj dum iom da tempo post la lasta ĝisdatigo ("eventuale"), ĉiuj demandoj resendos la lastan ĝisdatigitan valoron.

Gravas kompreni, ke por klasikaj datumbazoj ĝi estas sufiĉe ofte uzata forta konsistenco, kie ĉiu nodo havas la samajn informojn (ĉi tio ofte estas atingita en la kazo kie la transakcio estas konsiderita establita nur post kiam la dua servilo respondas). Estas iuj malstreĉiĝo ĉi tie pro la izolaj niveloj, sed la ĝenerala ideo restas la sama - oni povas vivi en tute harmoniigita mondo.

Tamen ni revenu al la originala tasko. Se parto de la sistemo povas esti konstruita kun eventuala konsistenco, tiam ni povas konstrui la sekvan diagramon.

Konvenaj arkitekturaj ŝablonoj

Gravaj trajtoj de ĉi tiu aliro:

  • Ĉiu envenanta peto estas metita en unu atendovico.
  • Dum prilaborado de peto, la servo ankaŭ povas meti taskojn en aliaj atendovicoj.
  • Ĉiu envenanta evento havas identigilon (kiu estas necesa por maldupliko).
  • La vico ideologie funkcias laŭ la skemo "nur almeti". Vi ne povas forigi elementojn de ĝi aŭ rearanĝi ilin.
  • La vico funkcias laŭ la FIFO-skemo (pardonu la taŭtologion). Se vi devas fari paralelan ekzekuton, tiam en unu etapo vi devas movi objektojn al malsamaj atendovicoj.

Mi memorigu vin, ke ni pripensas la kazon de interreta dosierstokado. En ĉi tiu kazo, la sistemo aspektos kiel ĉi tio:

Konvenaj arkitekturaj ŝablonoj

Gravas, ke la servoj en la diagramo ne nepre signifas apartan servilon. Eĉ la procezo povas esti la sama. Alia afero gravas: ideologie tiuj aferoj estas apartigitaj tiel, ke horizontala skalado povas esti facile aplikata.

Kaj por du uzantoj la diagramo aspektos tiel (servoj destinitaj al malsamaj uzantoj estas indikitaj en malsamaj koloroj):

Konvenaj arkitekturaj ŝablonoj

Gratifikoj de tia kombinaĵo:

  • Servoj pri informtraktado estas apartigitaj. La vicoj ankaŭ estas apartigitaj. Se ni bezonas pliigi la sisteman trairon, tiam ni nur bezonas lanĉi pli da servoj sur pli da serviloj.
  • Kiam ni ricevas informojn de uzanto, ni ne devas atendi ĝis la datumoj estas tute konservitaj. Male, ni nur bezonas respondi "bone" kaj poste iom post iom eklabori. Samtempe, la vico glatigas pintojn, ĉar aldoni novan objekton okazas rapide, kaj la uzanto ne devas atendi kompletan trapason tra la tuta ciklo.
  • Ekzemple, mi aldonis deduplikadan servon, kiu provas kunfandi identajn dosierojn. Se ĝi funkcias dum longa tempo en 1% de kazoj, la kliento apenaŭ rimarkos ĝin (vidu supre), kio estas granda pluso, ĉar ni ne plu devas esti XNUMX% rapide kaj fidindaj.

Tamen, la malavantaĝoj estas tuj videblaj:

  • Nia sistemo perdis sian striktan konsekvencon. Ĉi tio signifas, ke se vi ekzemple abonas malsamajn servojn, tiam teorie vi povas ricevi malsaman staton (ĉar unu el la servoj eble ne havas tempon por ricevi sciigon de la interna atendovico). Kiel alia sekvo, la sistemo nun havas neniun komunan tempon. Tio estas, ke estas neeble, ekzemple, ordigi ĉiujn eventojn simple laŭ alventempo, ĉar la horloĝoj inter serviloj eble ne estas sinkronaj (cetere, la sama tempo ĉe du serviloj estas utopio).
  • Neniuj eventoj nun povas esti simple restarigitaj (kiel oni povus fari kun datumbazo). Anstataŭe, vi devas aldoni novan eventon − kompensa evento, kiu ŝanĝos la lastan staton al la postulata. Ekzemple de simila areo: sen reverki historion (kio estas malbona en iuj kazoj), vi ne povas retrorigi kommit en git, sed vi povas fari specialan rollback commit, kiu esence nur resendas la malnovan staton. Tamen, kaj la erara transdono kaj la retroiro restos en la historio.
  • La datumskemo povas ŝanĝiĝi de eldono al eldono, sed malnovaj eventoj ne plu povos esti ĝisdatigitaj al la nova normo (ĉar eventoj principe ne povas esti ŝanĝitaj).

Kiel vi povas vidi, Event Sourcing funkcias bone kun CQRS. Plie, efektivigi sistemon kun efikaj kaj oportunaj vostoj, sed sen apartigi datumfluojn, jam estas malfacila en si mem, ĉar vi devos aldoni sinkronigajn punktojn, kiuj neŭtraligos la tutan pozitivan efikon de la vostoj. Aplikante ambaŭ alirojn samtempe, necesas iomete ĝustigi la programkodon. En nia kazo, kiam oni sendas dosieron al la servilo, la respondo venas nur "bone", kio signifas nur, ke "la operacio aldoni la dosieron estis konservita." Formale, ĉi tio ne signifas, ke la datumoj jam disponeblas en aliaj aparatoj (ekzemple, la deduplika servo povas rekonstrui la indekson). Tamen, post iom da tempo, la kliento ricevos sciigon en la stilo de "dosiero X estas konservita".

Tial:

  • La nombro da dosieraj statoj pliiĝas: anstataŭ la klasika "dosiero sendita", ni ricevas du: "la dosiero estis aldonita al la atendovico sur la servilo" kaj "la dosiero estis konservita en stokado." Ĉi-lasta signifas, ke aliaj aparatoj jam povas komenci ricevi la dosieron (ĝustigita pro tio, ke la vicoj funkcias je malsamaj rapidoj).
  • Pro la fakto, ke la sendo-informoj nun venas tra malsamaj kanaloj, ni devas elpensi solvojn por ricevi la pretigan staton de la dosiero. Sekve de ĉi tio: male al la klasika peto-respondo, la kliento povas esti rekomencita dum prilaborado de la dosiero, sed la stato de ĉi tiu prilaborado mem estos ĝusta. Krome, ĉi tiu aĵo funkcias, esence, el la skatolo. Sekve: ni nun estas pli toleremaj pri malsukcesoj.

sharding

Kiel priskribite supre, okazaĵaj provizsistemoj mankas strikta konsistenco. Ĉi tio signifas, ke ni povas uzi plurajn stokaĵojn sen ajna sinkronigo inter ili. Alproksimiĝante al nia problemo, ni povas:

  • Apartigu dosierojn laŭ tipo. Ekzemple, bildoj/vidbendoj povas esti malkoditaj kaj pli efika formato povas esti elektita.
  • Apartaj kontoj laŭ lando. Pro multaj leĝoj, tio povas esti postulata, sed ĉi tiu arkitekturskemo disponigas tian ŝancon aŭtomate

Konvenaj arkitekturaj ŝablonoj

Se vi volas translokigi datumojn de unu stokado al alia, tiam normaj rimedoj ne plu sufiĉas. Bedaŭrinde, en ĉi tiu kazo, vi devas ĉesigi la atendovicon, fari la migradon, kaj poste komenci ĝin. En la ĝenerala kazo, datumoj ne povas esti transdonitaj "sur la flugo", tamen, se la eventovico estas tute konservita, kaj vi havas momentfotojn de antaŭaj stokaj statoj, tiam ni povas reludi la eventojn jene:

  • En Event Source, ĉiu evento havas sian propran identigilon (idee, ne-malkreskanta). Ĉi tio signifas, ke ni povas aldoni kampon al la stokado - la id de la lasta prilaborita elemento.
  • Ni duobligas la atendovicon, por ke ĉiuj eventoj estu prilaboritaj por pluraj sendependaj stokoj (la unua estas tiu, en kiu la datumoj jam estas konservitaj, kaj la dua estas nova, sed ankoraŭ malplena). La dua vico, kompreneble, ankoraŭ ne estas prilaborita.
  • Ni lanĉas la duan atendovicon (tio estas, ni komencas reludi eventojn).
  • Kiam la nova vico estas relative malplena (tio estas, la averaĝa tempodiferenco inter aldoni elementon kaj retrovi ĝin estas akceptebla), vi povas komenci ŝanĝi legantojn al la nova stokado.

Kiel vi povas vidi, ni ne havis, kaj ankoraŭ ne havas, striktan konsistencon en nia sistemo. Estas nur eventuala konstanteco, tio estas garantio, ke eventoj estas prilaboritaj en la sama ordo (sed eventuale kun malsamaj prokrastoj). Kaj, uzante ĉi tion, ni povas relative facile transdoni datumojn sen haltigi la sistemon al la alia flanko de la terglobo.

Tiel, daŭrigante nian ekzemplon pri interreta stokado por dosieroj, tia arkitekturo jam donas al ni kelkajn gratifikojn:

  • Ni povas movi objektojn pli proksime al uzantoj en dinamika maniero. Tiel vi povas plibonigi la kvaliton de servo.
  • Ni povas konservi iujn datumojn ene de kompanioj. Ekzemple, Enterprise-uzantoj ofte postulas, ke iliaj datumoj estu stokitaj en kontrolitaj datumcentroj (por eviti datumlikojn). Per sharding ni povas facile subteni ĉi tion. Kaj la tasko estas eĉ pli facila se la kliento havas kongruan nubon (ekzemple, Azure mem gastigita).
  • Kaj la plej grava afero estas, ke ni ne devas fari ĉi tion. Post ĉio, por komenci, ni estus sufiĉe feliĉaj kun unu stokado por ĉiuj kontoj (por eklabori rapide). Kaj la ĉefa trajto de ĉi tiu sistemo estas, ke kvankam ĝi estas ekspansiebla, en la komenca etapo ĝi estas sufiĉe simpla. Vi simple ne devas tuj skribi kodon, kiu funkcias kun miliono da apartaj sendependaj vicoj, ktp. Se necese, tio povas esti farita en la estonteco.

Statika Enhava Gastigado

Ĉi tiu punkto povas ŝajni sufiĉe evidenta, sed ĝi ankoraŭ estas necesa por pli-malpli norma ŝarĝita aplikaĵo. Ĝia esenco estas simpla: ĉiu statika enhavo estas distribuata ne de la sama servilo, kie troviĝas la aplikaĵo, sed de specialaj dediĉitaj specife al ĉi tiu tasko. Kiel rezulto, ĉi tiuj operacioj estas faritaj pli rapide (kondiĉa nginx servas dosierojn pli rapide kaj malpli multekoste ol Java-servilo). Plie CDN-arkitekturo (Reta Livera Enhavo) permesas al ni lokalizi niajn dosierojn pli proksime al finaj uzantoj, kio havas pozitivan efikon sur la oportuno labori kun la servo.

La plej simpla kaj norma ekzemplo de senmova enhavo estas aro de skriptoj kaj bildoj por retejo. Ĉio estas simpla kun ili - ili estas konataj anticipe, tiam la arkivo estas alŝutita al CDN-serviloj, de kie ili estas distribuitaj al finaj uzantoj.

Tamen, fakte, por senmova enhavo, vi povas uzi aliron iom similan al lambda arkitekturo. Ni revenu al nia tasko (rete dosierstokado), en kiu ni devas distribui dosierojn al uzantoj. La plej simpla solvo estas krei servon, kiu, por ĉiu peto de uzanto, faras ĉiujn necesajn kontrolojn (rajtigo, ktp.), kaj poste elŝutas la dosieron rekte el nia stokado. La ĉefa malavantaĝo de ĉi tiu aliro estas, ke senmova enhavo (kaj dosiero kun certa revizio estas, fakte, senmova enhavo) estas distribuita de la sama servilo kiu enhavas la komercan logikon. Anstataŭe, vi povas fari la sekvan diagramon:

  • La servilo disponigas elŝutan URL. Ĝi povas esti de la formo file_id + ŝlosilo, kie ŝlosilo estas mini-cifereca subskribo kiu donas la rajton aliri la rimedon dum la sekvaj XNUMX horoj.
  • La dosiero estas distribuita per simpla nginx kun la sekvaj opcioj:
    • Enhavo kaŝmemoro. Ĉar ĉi tiu servo povas troviĝi sur aparta servilo, ni lasis al ni rezervon por la estonteco kun la kapablo stoki ĉiujn lastajn elŝutitajn dosierojn sur disko.
    • Kontrolante la ŝlosilon en la momento de kreado de konekto
  • Laŭvola: pritraktado de enhavo en streaming. Ekzemple, se ni kunpremas ĉiujn dosierojn en la servo, tiam ni povas fari malzipon rekte en ĉi tiu modulo. Sekve: IO-operacioj estas faritaj kie ili apartenas. Arkivisto en Java facile asignos multe da kroma memoro, sed reverki servon kun komerca logiko en Rust/C++-kondicionalojn ankaŭ povas esti neefika. En nia kazo, malsamaj procezoj (aŭ eĉ servoj) estas uzataj, kaj tial ni povas sufiĉe efike apartigi komercan logikon kaj IO-operaciojn.

Konvenaj arkitekturaj ŝablonoj

Ĉi tiu skemo ne tre similas al distribuado de senmova enhavo (ĉar ni ne alŝutas la tutan senmovan pakaĵon ien), sed fakte, ĉi tiu aliro ĝuste koncernas distribuadon de neŝanĝeblaj datumoj. Krome, ĉi tiu skemo povas esti ĝeneraligita al aliaj kazoj kie la enhavo ne estas simple senmova, sed povas esti reprezentita kiel aro de neŝanĝeblaj kaj ne-forigeblaj blokoj (kvankam ili povas esti aldonitaj).

Kiel alia ekzemplo (por plifortigo): se vi laboris kun Jenkins/TeamCity, tiam vi scias, ke ambaŭ solvoj estas skribitaj en Java. Ambaŭ el ili estas Java-procezo, kiu pritraktas ambaŭ konstruan instrumentadon kaj enhavadministradon. Aparte, ili ambaŭ havas taskojn kiel "transdoni dosieron/dosierujon de la servilo." Ekzemple: eldonado de artefaktoj, translokado de fontkodo (kiam la agento ne elŝutas la kodon rekte el la deponejo, sed la servilo faras ĝin por li), aliron al ŝtipoj. Ĉiuj ĉi tiuj taskoj malsamas en sia IO-ŝarĝo. Tio estas, ĝi rezultas, ke la servilo respondeca pri kompleksa komerca logiko devas samtempe povi efike puŝi grandajn fluojn de datumoj tra si mem. Kaj kio estas plej interesa estas, ke tia operacio povas esti delegita al la sama nginx laŭ ĝuste la sama skemo (krom ke la datumŝlosilo estu aldonita al la peto).

Tamen, se ni revenas al nia sistemo, ni ricevas similan diagramon:

Konvenaj arkitekturaj ŝablonoj

Kiel vi povas vidi, la sistemo fariĝis radikale pli kompleksa. Nun ĝi ne estas nur mini-procezo, kiu stokas dosierojn loke. Nun kio necesas ne estas la plej simpla subteno, API-versia kontrolo ktp. Tial, post kiam ĉiuj diagramoj estas desegnitaj, plej bone estas detale taksi ĉu etendebleco valoras la koston. Tamen, se vi volas povi vastigi la sistemon (inkluzive labori kun eĉ pli granda nombro da uzantoj), tiam vi devos serĉi similajn solvojn. Sed, kiel rezulto, la sistemo estas arkitekture preta por pliigita ŝarĝo (preskaŭ ĉiu komponento povas esti klonita por horizontala skalo). La sistemo povas esti ĝisdatigita sen haltigi ĝin (simple iuj operacioj estos iomete malrapidigitaj).

Kiel mi diris komence, nun kelkaj Interretaj servoj komencis ricevi pliigitan ŝarĝon. Kaj kelkaj el ili simple komencis ĉesi funkcii ĝuste. Fakte, la sistemoj malsukcesis ĝuste en la momento, kiam la komerco devis gajni monon. Tio estas, anstataŭ prokrastita livero, anstataŭ sugesti al klientoj "plani vian liveron por la venontaj monatoj", la sistemo simple diris "iru al viaj konkurantoj". Fakte, ĉi tio estas la prezo de malalta produktiveco: perdoj okazos ĝuste kiam profitoj estus plej altaj.

konkludo

Ĉiuj ĉi tiuj aliroj estis konataj antaŭe. La sama VK delonge uzas la ideon de Statika Enhava Gastigado por montri bildojn. Multaj interretaj ludoj uzas la Sharding-skemon por dividi ludantojn en regionojn aŭ por apartigi ludlokojn (se la mondo mem estas tia). Event Sourcing alproksimiĝo estas aktive uzata en retpoŝto. Plej multaj komercaj aplikoj, kie datumoj konstante ricevas, estas fakte konstruitaj laŭ CQRS-aliro por povi filtri la ricevitajn datumojn. Nu, horizontala skalado estas uzata en multaj servoj dum sufiĉe longa tempo.

Tamen, plej grave, ĉiuj ĉi tiuj ŝablonoj fariĝis tre facile apliki en modernaj aplikoj (se ili taŭgas, kompreneble). Nuboj ofertas Sharding kaj horizontalan skalon tuj, kio estas multe pli facila ol ordigi malsamajn dediĉitajn servilojn en malsamaj datumcentroj mem. CQRS fariĝis multe pli facila, se nur pro la evoluo de bibliotekoj kiel ekzemple RX. Antaŭ ĉirkaŭ 10 jaroj, malofta retejo povus subteni ĉi tion. Event Sourcing ankaŭ estas nekredeble facile instalebla danke al pretaj ujoj kun Apache Kafka. Antaŭ 10 jaroj ĉi tio estus novigo, nun ĝi estas ordinara. Estas same kun Static Content Hosting: pro pli oportunaj teknologioj (inkluzive de la fakto, ke ekzistas detala dokumentaro kaj granda datumbazo de respondoj), ĉi tiu aliro fariĝis eĉ pli simpla.

Kiel rezulto, la efektivigo de kelkaj sufiĉe kompleksaj arkitekturaj ŝablonoj nun fariĝis multe pli simpla, kio signifas, ke estas pli bone rigardi ĝin antaŭe. Se en dekjara aplikaĵo unu el la supraj solvoj estis forlasita pro la alta kosto de efektivigo kaj funkciado, nun, en nova aplikaĵo, aŭ post refactoring, vi povas krei servon, kiu jam estos arkitekture ambaŭ etendebla ( laŭ rendimento) kaj preta al novaj petoj de klientoj (ekzemple por lokalizi personajn datumojn).

Kaj plej grave: bonvolu ne uzi ĉi tiujn alirojn se vi havas simplan aplikaĵon. Jes, ili estas belaj kaj interesaj, sed por retejo kun pinta vizito de 100 homoj, oni ofte povas elteni per klasika monolito (almenaŭ ekstere ĉio ene povas esti dividita en modulojn ktp.).

fonto: www.habr.com

Aldoni komenton