Handige architecturale patronen

Hé Habr!

In het licht van de huidige gebeurtenissen als gevolg van het coronavirus beginnen een aantal internetdiensten een grotere belasting te krijgen. Bijvoorbeeld, Een van de Britse winkelketens stopte simpelweg met zijn online bestelsite., omdat er niet genoeg capaciteit was. En het is niet altijd mogelijk om een ​​server te versnellen door simpelweg krachtigere apparatuur toe te voegen, maar klantverzoeken moeten worden verwerkt (anders gaan ze naar concurrenten).

In dit artikel zal ik het kort hebben over populaire praktijken waarmee u een snelle en fouttolerante service kunt creëren. Uit de mogelijke ontwikkelingsschema's heb ik echter alleen de schema's geselecteerd die momenteel beschikbaar zijn makkelijk te gebruiken. Voor elk item heb je kant-en-klare bibliotheken, of je hebt de mogelijkheid om het probleem op te lossen met behulp van een cloudplatform.

Horizontale schaling

Het eenvoudigste en meest bekende punt. Conventioneel zijn de twee meest voorkomende belastingverdelingsschema's horizontaal en verticaal schalen. In een perm-zaak u laat services parallel draaien, waardoor de belasting onderling wordt verdeeld. In de tweede u bestelt krachtigere servers of optimaliseert de code.

Ik neem bijvoorbeeld abstracte opslag van cloudbestanden, dat wil zeggen een analoog van OwnCloud, OneDrive, enzovoort.

Hieronder staat een standaardafbeelding van zo'n schakeling, maar deze demonstreert alleen de complexiteit van het systeem. We moeten tenslotte de services op de een of andere manier synchroniseren. Wat gebeurt er als de gebruiker een bestand opslaat vanaf de tablet en dit vervolgens vanaf de telefoon wil bekijken?

Handige architecturale patronen
Het verschil tussen de benaderingen: bij verticaal schalen zijn we klaar om de kracht van knooppunten te vergroten, en bij horizontaal schalen zijn we klaar om nieuwe knooppunten toe te voegen om de belasting te verdelen.

CQRS

Segregatie van verantwoordelijkheden voor opdrachtquery's Een nogal belangrijk patroon, omdat het verschillende clients niet alleen in staat stelt verbinding te maken met verschillende diensten, maar ook dezelfde gebeurtenisstromen te ontvangen. De voordelen ervan zijn niet zo voor de hand liggend voor een eenvoudige toepassing, maar voor een drukke dienst is het uiterst belangrijk (en eenvoudig). De essentie ervan: inkomende en uitgaande datastromen mogen elkaar niet kruisen. Dat wil zeggen dat u geen verzoek kunt verzenden en een antwoord kunt verwachten; in plaats daarvan verzendt u een verzoek naar dienst A, maar ontvangt u een antwoord van dienst B.

De eerste bonus van deze aanpak is de mogelijkheid om de verbinding te verbreken (in de brede zin van het woord) terwijl een lang verzoek wordt uitgevoerd. Laten we bijvoorbeeld een min of meer standaardreeks nemen:

  1. De client heeft een verzoek naar de server verzonden.
  2. De server heeft een lange verwerkingstijd gestart.
  3. De server reageerde met het resultaat op de client.

Laten we ons voorstellen dat in punt 2 de verbinding werd verbroken (of dat het netwerk opnieuw werd verbonden, of dat de gebruiker naar een andere pagina ging en de verbinding verbrak). In dit geval zal het voor de server moeilijk zijn om een ​​antwoord naar de gebruiker te sturen met informatie over wat er precies is verwerkt. Bij gebruik van CQRS zal de volgorde iets anders zijn:

  1. De klant heeft zich geabonneerd op updates.
  2. De client heeft een verzoek naar de server verzonden.
  3. De server antwoordde “verzoek geaccepteerd.”
  4. De server reageerde met het resultaat via het kanaal vanaf punt “1”.

Handige architecturale patronen

Zoals u kunt zien, is het schema iets ingewikkelder. Bovendien ontbreekt hier de intuïtieve request-response-benadering. Zoals u kunt zien, leidt een verbindingsonderbreking tijdens het verwerken van een verzoek echter niet tot een fout. Bovendien, als de gebruiker daadwerkelijk vanaf meerdere apparaten (bijvoorbeeld vanaf een mobiele telefoon en vanaf een tablet) met de dienst is verbonden, kunt u ervoor zorgen dat de reactie op beide apparaten komt.

Interessant is dat de code voor het verwerken van inkomende berichten hetzelfde wordt (niet 100%), zowel voor gebeurtenissen die door de cliënt zelf zijn beïnvloed, als voor andere gebeurtenissen, inclusief die van andere cliënten.

In werkelijkheid krijgen we echter een extra bonus vanwege het feit dat unidirectionele stroom in een functionele stijl kan worden afgehandeld (met behulp van RX en dergelijke). En dit is al een serieus pluspunt, omdat de applicatie in essentie volledig reactief kan worden gemaakt, en ook met behulp van een functionele aanpak. Voor vetprogramma's kan dit de ontwikkelings- en ondersteuningsmiddelen aanzienlijk besparen.

Als we deze aanpak combineren met horizontaal schalen, krijgen we als bonus de mogelijkheid om verzoeken naar de ene server te sturen en reacties van een andere te ontvangen. Zo kan de klant de dienst kiezen die voor hem handig is, en het systeem binnenin kan de gebeurtenissen nog steeds correct verwerken.

Evenement sourcing

Zoals u weet, is een van de belangrijkste kenmerken van een gedistribueerd systeem de afwezigheid van een gemeenschappelijke tijd, een gemeenschappelijk kritisch gedeelte. Voor één proces kun je een synchronisatie doen (op dezelfde mutexen), waarbij je zeker weet dat niemand anders deze code uitvoert. Dit is echter gevaarlijk voor een gedistribueerd systeem, omdat het overhead nodig heeft en ook alle schoonheid van schaalvergroting tenietdoet - alle componenten zullen nog steeds op één systeem wachten.

Vanaf hier krijgen we een belangrijk feit: een snel gedistribueerd systeem kan niet worden gesynchroniseerd, omdat we dan de prestaties zullen verminderen. Aan de andere kant hebben we vaak een zekere consistentie tussen componenten nodig. En hiervoor kun je de aanpak gebruiken uiteindelijke consistentie, waarbij gegarandeerd wordt dat als er gedurende een bepaalde periode na de laatste update (“uiteindelijk”) geen gegevenswijzigingen plaatsvinden, alle zoekopdrachten de laatst bijgewerkte waarde retourneren.

Het is belangrijk om te begrijpen dat het voor klassieke databases vrij vaak wordt gebruikt sterke consistentie, waarbij elk knooppunt dezelfde informatie heeft (dit wordt vaak bereikt in het geval dat de transactie pas als tot stand wordt beschouwd nadat de tweede server heeft gereageerd). Er zijn hier enkele versoepelingen vanwege de isolatieniveaus, maar het algemene idee blijft hetzelfde: je kunt in een volledig geharmoniseerde wereld leven.

Laten we echter terugkeren naar de oorspronkelijke taak. Als een deel van het systeem kan worden gebouwd uiteindelijke consistentie, dan kunnen we het volgende diagram construeren.

Handige architecturale patronen

Belangrijke kenmerken van deze aanpak:

  • Elk binnenkomend verzoek wordt in één wachtrij geplaatst.
  • Tijdens het verwerken van een aanvraag kan de dienst ook taken in andere wachtrijen plaatsen.
  • Elke binnenkomende gebeurtenis heeft een ID (die nodig is voor deduplicatie).
  • De wachtrij werkt ideologisch volgens het ‘alleen toevoegen’-schema. Je kunt er geen elementen uit verwijderen of opnieuw rangschikken.
  • De wachtrij werkt volgens het FIFO-schema (sorry voor de tautologie). Als u een parallelle uitvoering moet uitvoeren, moet u op een gegeven moment objecten naar verschillende wachtrijen verplaatsen.

Ik wil u eraan herinneren dat we het geval van online bestandsopslag overwegen. In dit geval zal het systeem er ongeveer zo uitzien:

Handige architecturale patronen

Het is belangrijk dat de services in het diagram niet noodzakelijkerwijs een aparte server betekenen. Zelfs het proces kan hetzelfde zijn. Iets anders is belangrijk: ideologisch gezien zijn deze zaken zo gescheiden dat horizontale schaling gemakkelijk kan worden toegepast.

En voor twee gebruikers ziet het diagram er als volgt uit (diensten bedoeld voor verschillende gebruikers worden in verschillende kleuren aangegeven):

Handige architecturale patronen

Bonussen van een dergelijke combinatie:

  • Informatieverwerkingsdiensten zijn gescheiden. Ook de wachtrijen zijn gescheiden. Als we de systeemdoorvoer moeten vergroten, hoeven we alleen maar meer services op meer servers te lanceren.
  • Wanneer wij informatie ontvangen van een gebruiker, hoeven wij niet te wachten totdat de gegevens volledig zijn opgeslagen. Integendeel, we hoeven alleen maar ‘oké’ te antwoorden en dan geleidelijk aan aan de slag te gaan. Tegelijkertijd strijkt de wachtrij pieken glad, omdat het toevoegen van een nieuw object snel gebeurt en de gebruiker niet hoeft te wachten op een volledige doorloop van de hele cyclus.
  • Als voorbeeld heb ik een deduplicatieservice toegevoegd die identieke bestanden probeert samen te voegen. Als het in 1% van de gevallen langdurig werkt, zal de klant er nauwelijks iets van merken (zie hierboven), wat een groot pluspunt is, aangezien we niet langer XNUMX% snelheid en betrouwbaarheid hoeven te zijn.

De nadelen zijn echter meteen zichtbaar:

  • Ons systeem heeft zijn strikte consistentie verloren. Dit betekent dat als u zich bijvoorbeeld op verschillende services abonneert, u in theorie een andere status kunt krijgen (aangezien een van de services mogelijk geen tijd heeft om een ​​melding van de interne wachtrij te ontvangen). Een ander gevolg is dat het systeem nu geen gemeenschappelijke tijd meer heeft. Dat wil zeggen dat het bijvoorbeeld onmogelijk is om alle gebeurtenissen eenvoudigweg op aankomsttijd te sorteren, omdat de klokken tussen servers mogelijk niet synchroon zijn (bovendien is dezelfde tijd op twee servers een utopie).
  • Geen enkele gebeurtenis kan nu eenvoudigweg worden teruggedraaid (zoals met een database wel het geval zou kunnen zijn). In plaats daarvan moet u een nieuwe gebeurtenis toevoegen − compensatie gebeurtenis, waardoor de laatste status in de vereiste wordt gewijzigd. Als voorbeeld uit een soortgelijk gebied: zonder de geschiedenis te herschrijven (wat in sommige gevallen slecht is), kun je een commit in git niet terugdraaien, maar je kunt wel een speciale terugdraaien commit, wat in wezen gewoon de oude staat retourneert. Zowel de foutieve commit als de rollback zullen echter in de geschiedenis blijven bestaan.
  • Het dataschema kan van release tot release veranderen, maar oude evenementen kunnen niet meer worden bijgewerkt naar de nieuwe standaard (aangezien evenementen in principe niet kunnen worden gewijzigd).

Zoals je ziet werkt Event Sourcing goed samen met CQRS. Bovendien is het implementeren van een systeem met efficiënte en handige wachtrijen, maar zonder datastromen te scheiden, op zichzelf al lastig, omdat je synchronisatiepunten zult moeten toevoegen die het hele positieve effect van de wachtrijen zullen neutraliseren. Als u beide benaderingen tegelijk toepast, is het noodzakelijk om de programmacode enigszins aan te passen. In ons geval komt het antwoord bij het verzenden van een bestand naar de server alleen “ok”, wat alleen betekent dat “de bewerking voor het toevoegen van het bestand is opgeslagen.” Formeel betekent dit niet dat de gegevens al beschikbaar zijn op andere apparaten (de deduplicatieservice kan bijvoorbeeld de index opnieuw opbouwen). Na enige tijd ontvangt de klant echter een melding in de stijl van ‘bestand X is opgeslagen’.

Als resultaat:

  • Het aantal statussen voor het verzenden van bestanden neemt toe: in plaats van het klassieke ‘bestand verzonden’ krijgen we er twee: ‘het bestand is toegevoegd aan de wachtrij op de server’ en ‘het bestand is opgeslagen in de opslag’. Dit laatste betekent dat andere apparaten het bestand al kunnen ontvangen (gecorrigeerd voor het feit dat de wachtrijen op verschillende snelheden werken).
  • Omdat de indieningsinformatie nu via verschillende kanalen binnenkomt, moeten we oplossingen bedenken om de verwerkingsstatus van het bestand te achterhalen. Als gevolg hiervan: in tegenstelling tot de klassieke request-response kan de client opnieuw worden opgestart tijdens het verwerken van het bestand, maar de status van deze verwerking zelf zal correct zijn. Bovendien werkt dit item in wezen out-of-the-box. Het gevolg is dat we nu toleranter zijn ten aanzien van mislukkingen.

sharding

Zoals hierboven beschreven ontbreekt het bij eventsourcingsystemen aan strikte consistentie. Dit betekent dat we meerdere opslagplaatsen kunnen gebruiken zonder enige synchronisatie daartussen. Als we ons probleem benaderen, kunnen we:

  • Scheid bestanden op type. Foto's/video's kunnen bijvoorbeeld worden gedecodeerd en er kan een efficiënter formaat worden geselecteerd.
  • Aparte rekeningen per land. Vanwege veel wetten kan dit vereist zijn, maar dit architectuurschema biedt automatisch een dergelijke mogelijkheid

Handige architecturale patronen

Als je data van de ene opslag naar de andere wilt overbrengen, zijn standaardmiddelen niet meer voldoende. Helaas moet u in dit geval de wachtrij stoppen, de migratie uitvoeren en deze vervolgens starten. In het algemeen kunnen gegevens niet ‘on-the-fly’ worden overgedragen. Als de gebeurteniswachtrij echter volledig is opgeslagen en u over momentopnamen beschikt van eerdere opslagstatussen, kunnen we de gebeurtenissen als volgt afspelen:

  • In Gebeurtenisbron heeft elke gebeurtenis zijn eigen identificatie (idealiter niet-aflopend). Dit betekent dat we een veld aan de opslag kunnen toevoegen: de ID van het laatst verwerkte element.
  • We dupliceren de wachtrij zodat alle gebeurtenissen kunnen worden verwerkt voor verschillende onafhankelijke opslagplaatsen (de eerste is degene waarin de gegevens al zijn opgeslagen en de tweede is nieuw, maar nog steeds leeg). De tweede wachtrij wordt uiteraard nog niet verwerkt.
  • We lanceren de tweede wachtrij (dat wil zeggen, we beginnen gebeurtenissen opnieuw af te spelen).
  • Wanneer de nieuwe wachtrij relatief leeg is (dat wil zeggen dat het gemiddelde tijdsverschil tussen het toevoegen van een element en het ophalen ervan acceptabel is), kunt u beginnen met het overschakelen van lezers naar de nieuwe opslag.

Zoals u kunt zien, hadden we geen strikte consistentie in ons systeem, en dat hebben we nog steeds niet. Er is alleen sprake van uiteindelijke consistentie, dat wil zeggen een garantie dat gebeurtenissen in dezelfde volgorde worden verwerkt (maar mogelijk met verschillende vertragingen). En hiermee kunnen we relatief eenvoudig gegevens overdragen zonder het systeem te stoppen naar de andere kant van de wereld.

Als we ons voorbeeld over online opslag van bestanden voortzetten, levert een dergelijke architectuur ons al een aantal bonussen op:

  • We kunnen objecten op een dynamische manier dichter bij gebruikers brengen. Zo kunt u de kwaliteit van de dienstverlening verbeteren.
  • Sommige gegevens kunnen wij binnen bedrijven opslaan. Enterprise-gebruikers eisen bijvoorbeeld vaak dat hun gegevens worden opgeslagen in gecontroleerde datacenters (om datalekken te voorkomen). Via sharding kunnen wij dit eenvoudig ondersteunen. En de taak is nog eenvoudiger als de klant een compatibele cloud heeft (bijvoorbeeld Azure zelf gehost).
  • En het belangrijkste is dat we dit niet hoeven te doen. Om te beginnen zouden we immers al blij zijn met één opslag voor alle accounts (om snel aan de slag te kunnen). En het belangrijkste kenmerk van dit systeem is dat het, hoewel het uitbreidbaar is, in de beginfase vrij eenvoudig is. Je hoeft alleen niet meteen code te schrijven die werkt met een miljoen afzonderlijke, onafhankelijke wachtrijen, enz. Indien nodig kan dit in de toekomst worden gedaan.

Hosting van statische inhoud

Dit punt lijkt misschien heel voor de hand liggend, maar het is nog steeds nodig voor een min of meer standaard geladen applicatie. De essentie is simpel: alle statische inhoud wordt niet gedistribueerd vanaf dezelfde server waarop de applicatie zich bevindt, maar vanaf speciale servers die specifiek voor deze taak zijn bedoeld. Als gevolg hiervan worden deze bewerkingen sneller uitgevoerd (voorwaardelijke nginx serveert bestanden sneller en goedkoper dan een Java-server). Plus CDN-architectuur (Content Delivery Network) stelt ons in staat onze bestanden dichter bij de eindgebruikers te lokaliseren, wat een positief effect heeft op het gemak van het werken met de dienst.

Het eenvoudigste en meest standaardvoorbeeld van statische inhoud is een set scripts en afbeeldingen voor een website. Bij hen is alles eenvoudig: ze zijn van tevoren bekend, waarna het archief wordt geüpload naar CDN-servers, vanwaar ze worden gedistribueerd naar eindgebruikers.

In werkelijkheid kun je voor statische inhoud echter een aanpak gebruiken die enigszins lijkt op de lambda-architectuur. Laten we terugkeren naar onze taak (online bestandsopslag), waarbij we bestanden naar gebruikers moeten distribueren. De eenvoudigste oplossing is om een ​​dienst te creëren die voor elk gebruikersverzoek alle noodzakelijke controles uitvoert (autorisatie, enz.) en vervolgens het bestand rechtstreeks uit onze opslag downloadt. Het grootste nadeel van deze aanpak is dat statische inhoud (en een bestand met een bepaalde revisie is in feite statische inhoud) wordt gedistribueerd door dezelfde server die de bedrijfslogica bevat. In plaats daarvan kunt u het volgende diagram maken:

  • De server biedt een download-URL. Het kan de vorm file_id + key hebben, waarbij key een mini-digitale handtekening is die recht geeft op toegang tot de bron voor de komende XNUMX uur.
  • Het bestand wordt gedistribueerd door eenvoudige nginx met de volgende opties:
    • Caching van inhoud. Omdat deze dienst zich op een aparte server kan bevinden, hebben we onszelf een reserve voor de toekomst gelaten met de mogelijkheid om de nieuwste gedownloade bestanden op schijf op te slaan.
    • Controle van de sleutel op het moment dat de verbinding tot stand wordt gebracht
  • Optioneel: streaming contentverwerking. Als we bijvoorbeeld alle bestanden in de service comprimeren, kunnen we het uitpakken rechtstreeks in deze module doen. Het gevolg is dat IO-operaties worden uitgevoerd waar ze thuishoren. Een archiveringshulpmiddel in Java zal gemakkelijk veel extra geheugen toewijzen, maar het herschrijven van een service met bedrijfslogica in Rust/C++-conditionals kan ook ineffectief zijn. In ons geval worden verschillende processen (of zelfs services) gebruikt, en daarom kunnen we bedrijfslogica en IO-operaties behoorlijk effectief scheiden.

Handige architecturale patronen

Dit schema lijkt niet erg op het distribueren van statische inhoud (aangezien we niet het hele statische pakket ergens uploaden), maar in werkelijkheid houdt deze aanpak zich juist bezig met het distribueren van onveranderlijke gegevens. Bovendien kan dit schema worden gegeneraliseerd naar andere gevallen waarin de inhoud niet eenvoudigweg statisch is, maar kan worden weergegeven als een reeks onveranderlijke en niet-verwijderbare blokken (hoewel ze kunnen worden toegevoegd).

Nog een voorbeeld (ter verduidelijking): als je met Jenkins/TeamCity hebt gewerkt, dan weet je dat beide oplossingen in Java zijn geschreven. Beiden zijn een Java-proces dat zowel build-orkestratie als contentbeheer afhandelt. In het bijzonder hebben ze allebei taken als “een bestand/map overbrengen van de server.” Als voorbeeld: het uitgeven van artefacten, het overbrengen van broncode (wanneer de agent de code niet rechtstreeks uit de repository downloadt, maar de server het voor hem doet), toegang tot logs. Al deze taken verschillen in hun IO-belasting. Dat wil zeggen, het blijkt dat de server die verantwoordelijk is voor de complexe bedrijfslogica tegelijkertijd in staat moet zijn om grote gegevensstromen effectief door zichzelf heen te duwen. En wat het meest interessant is, is dat een dergelijke operatie volgens exact hetzelfde schema naar dezelfde nginx kan worden gedelegeerd (behalve dat de datasleutel aan het verzoek moet worden toegevoegd).

Als we echter terugkeren naar ons systeem, krijgen we een soortgelijk diagram:

Handige architecturale patronen

Zoals u kunt zien, is het systeem radicaal complexer geworden. Nu is het niet alleen een miniproces dat bestanden lokaal opslaat. Wat nu nodig is, is niet de eenvoudigste ondersteuning, API-versiebeheer, enz. Daarom is het het beste om, nadat alle diagrammen zijn getekend, in detail te evalueren of uitbreidbaarheid de kosten waard is. Als je het systeem echter wilt kunnen uitbreiden (en met nog meer gebruikers wilt werken), dan zul je voor vergelijkbare oplossingen moeten gaan. Maar als gevolg daarvan is het systeem architectonisch klaar voor verhoogde belasting (bijna elk onderdeel kan worden gekloond voor horizontale schaalvergroting). Het systeem kan worden bijgewerkt zonder het te stoppen (sommige handelingen worden alleen iets vertraagd).

Zoals ik aan het begin al zei, beginnen een aantal internetdiensten nu een grotere belasting te krijgen. En sommigen van hen begonnen gewoon niet meer correct te werken. In feite faalden de systemen precies op het moment dat het bedrijf geld moest verdienen. Dat wil zeggen, in plaats van uitgestelde levering, in plaats van klanten voor te stellen “plan uw levering voor de komende maanden”, zei het systeem eenvoudigweg “ga naar uw concurrenten.” In feite is dit de prijs van een lage productiviteit: verliezen zullen zich juist voordoen wanneer de winsten het hoogst zijn.

Conclusie

Al deze benaderingen waren al eerder bekend. Dezelfde VK gebruikt al lang het idee van Static Content Hosting om afbeeldingen weer te geven. Veel online games gebruiken het Sharding-schema om spelers in regio's te verdelen of spellocaties te scheiden (als de wereld zelf één is). De Event Sourcing-aanpak wordt actief gebruikt in e-mail. De meeste handelsapplicaties waarbij voortdurend gegevens worden ontvangen, zijn feitelijk gebouwd op een CQRS-aanpak om de ontvangen gegevens te kunnen filteren. Welnu, horizontaal schalen wordt al geruime tijd in veel diensten gebruikt.

Het allerbelangrijkste is echter dat al deze patronen zeer eenvoudig toepasbaar zijn geworden in moderne toepassingen (als ze passend zijn, natuurlijk). Clouds bieden meteen Sharding en horizontale schaling, wat veel eenvoudiger is dan zelf verschillende dedicated servers in verschillende datacenters bestellen. CQRS is veel eenvoudiger geworden, alleen al door de ontwikkeling van bibliotheken zoals RX. Ongeveer tien jaar geleden kon een zeldzame website dit ondersteunen. Event Sourcing is bovendien ontzettend eenvoudig op te zetten dankzij kant-en-klare containers met Apache Kafka. 10 jaar geleden zou dit een innovatie zijn geweest, nu is het gemeengoed. Hetzelfde geldt voor Static Content Hosting: dankzij handigere technologieën (waaronder het feit dat er gedetailleerde documentatie en een grote database met antwoorden is) is deze aanpak nog eenvoudiger geworden.

Hierdoor is de implementatie van een aantal tamelijk complexe architectuurpatronen nu veel eenvoudiger geworden, waardoor het beter is om dit vooraf nader te bekijken. Als in een tien jaar oude applicatie een van de bovenstaande oplossingen werd verlaten vanwege de hoge kosten van implementatie en werking, kunt u nu, in een nieuwe applicatie of na refactoring, een service creëren die architectonisch al zowel uitbreidbaar zal zijn ( qua prestaties) en klaar voor nieuwe verzoeken van klanten (bijvoorbeeld om persoonsgegevens te lokaliseren).

En het allerbelangrijkste: gebruik deze benaderingen alstublieft niet als u een eenvoudige toepassing heeft. Ja, ze zijn mooi en interessant, maar voor een site met een piekbezoek van 100 mensen kun je vaak rondkomen met een klassieke monoliet (tenminste aan de buitenkant, alles binnenin kan in modules worden verdeeld, enz.).

Bron: www.habr.com

Voeg een reactie