Vanaf nul ontwerpen van schaalbare back-infrastructuren

Tegenwoordig is er veel vraag naar het ontwerpen van een toekomstbestendig backend-platform, maar het is niet eenvoudig om je hoofd te buigen over de overweldigende informatie die er op internet beschikbaar is. Dus we zullen stap voor stap een volledig functionele schaalbare backend bouwen in deze meerdelige serie.

Ik heb een YouTube-serie gemaakt op basis van deze blogpost omdat ik zoveel verzoeken heb ontvangen. Volg mijn youtube-kanaal alphacode voor lezingen over microservices-architectuur.

Link naar de eerste reeks crashcursussen: https://www.youtube.com/playlist?list=PLZBNtT95PIW3BPNYF5pYOi4MJjg_boXCG

Bij het ontwikkelen van de eerste versie van een applicatie ondervindt u vaak geen problemen met de schaalbaarheid. Bovendien vertraagt ​​het gebruik van een gedistribueerde architectuur de ontwikkeling. Dit kan een groot probleem zijn voor startups met als grootste uitdaging het bedrijfsmodel snel te ontwikkelen en de markttijd te verkorten. Maar aangezien je hier bent, neem ik aan dat je dat al weet. Laten we er meteen in springen en rekening houden met de volgende doelen:

  1. Distribueer API-ontwikkeling: het systeem moet zodanig zijn ontworpen dat meerdere teams er tegelijkertijd aan kunnen werken en dat een enkel team geen bottleneck mag worden en dat het geen expertise over de hele applicatie nodig heeft om geoptimaliseerde eindpunten te creëren.
  2. Ondersteuning van meerdere talen: om te kunnen profiteren van opkomende technologieën moet elk functioneel onderdeel van het systeem de gewenste voorkeurstaal voor die functionaliteit kunnen ondersteunen.
  3. Minimaliseer de latentie: elke architectuur die we voorstellen, moet altijd proberen de reactietijd van de klant te minimaliseren.
  4. Minimaliseer implementatierisico's: verschillende functionele componenten van het systeem moeten afzonderlijk kunnen worden geïmplementeerd met minimale coördinatie.
  5. Minimaliseer de hardware-voetafdruk: het systeem moet proberen de hoeveelheid gebruikte hardware te optimaliseren en moet horizontaal schaalbaar zijn.

Monolithische toepassingen bouwen

Stel je voor dat je een gloednieuwe e-commerce-applicatie begon te bouwen die bedoeld was om te concurreren met Amazon. U zou beginnen met het maken van een nieuw project in het platform van uw keuze, zoals Rails, Spring Boot, Play enz. Het zou meestal een modulaire architectuur hebben die er ongeveer zo uitziet:

Figuur 1

De bovenste laag zal in het algemeen de clientaanvragen verwerken en na enkele validaties zal het de aanvraag doorsturen naar de servicelaag waar alle bedrijfslogica is geïmplementeerd. Een service maakt gebruik van verschillende adapters zoals database-toegangscomponenten in DAO-laag, berichtencomponenten, externe API's of andere services in dezelfde laag om het resultaat voor te bereiden en terug te sturen naar de controller die intern terugstuurt naar de client.

Dit soort applicatie is meestal verpakt en geïmplementeerd als een monoliet, wat betekent dat er één groot bestand is. Voor b.v. het zal een pot zijn in het geval van een laars en een zipbestand in het geval van de Rails of Node.js app. Dergelijke applicaties komen vrij vaak voor en hebben veel voordelen, ze zijn gemakkelijk te begrijpen, beheren, ontwikkelen, testen en implementeren. Je kunt ze ook schalen door er meerdere exemplaren achter een load balancer te laten draaien en het werkt redelijk goed tot een bepaald niveau.

Helaas heeft deze eenvoudige aanpak enorme beperkingen, zoals:

  • Taal- / Framework-lock: aangezien de hele applicatie is geschreven in een enkele technische stapel. Kan niet experimenteren met opkomende technologieën.
  • Moeilijk te verteren: zodra de app groot wordt, wordt het moeilijk voor een ontwikkelaar om zo'n grote codebase te begrijpen.
  • Moeilijk om API-ontwikkeling te distribueren: het wordt uiterst moeilijk om agile ontwikkeling te doen en een groot deel van de tijd van de ontwikkelaar wordt verspild aan het oplossen van conflicten.
  • Implementatie als een enkele eenheid: kan een afzonderlijke wijziging in een enkele component niet onafhankelijk implementeren. Wijzigingen worden "gegijzeld" door andere wijzigingen.
  • Ontwikkeling vertraagt: ik heb gewerkt aan een codebase met meer dan 50.000 klassen. De enorme omvang van de codebasis was voldoende om de IDE en opstarttijden te vertragen, waardoor de productiviteit te lijden had.
  • Bronnen zijn niet geoptimaliseerd: sommige modules kunnen CPU-intensieve beeldverwerkingslogica implementeren die reken-geoptimaliseerde instanties vereist en een andere module kan een in-memory database zijn en het meest geschikt voor voor geheugen geoptimaliseerde instanties. Maar we moeten een compromis sluiten over onze hardware-keuze. Het kan ook gebeuren dat een toepassingsmodule moet worden geschaald, maar dat we een volledig exemplaar van de toepassing opnieuw moeten uitvoeren omdat we een module niet afzonderlijk kunnen schalen.

Zou het niet geweldig zijn als we de toepassing in kleinere delen zouden kunnen opsplitsen en zo beheren dat deze zich als een enkele toepassing gedraagt ​​wanneer we hem uitvoeren? Ja, dat zou het zijn en dat is precies wat we hierna gaan doen!

Microservices-architectuur

Veel organisaties, zoals Amazon, Facebook, Twitter, eBay en Netflix hebben dit probleem opgelost door wat nu bekend staat als het Microservices Architecture-patroon. Het pakt dit probleem aan door het te verdelen in kleinere subproblemen aka verdeel en heers in de wereld van ontwikkelaars. Bekijk figuur 1 zorgvuldig, we snijden er verticale plakjes uit en creëren kleinere onderling verbonden diensten. Elk segment implementeert een afzonderlijke functionaliteit zoals winkelwagenbeheer, gebruikersbeheer en orderbeheer enz. Elke service kan in elke taal / elk kader worden geschreven en kan de polyglot-persistentie hebben die bij de use case past. Makkelijk toch?

Maar wacht! We wilden ook dat het zich als een enkele applicatie voor de client zou gedragen, anders zal de client moeten omgaan met alle complexiteit die bij deze architectuur hoort, zoals het verzamelen van de gegevens van verschillende services, het onderhouden van zoveel eindpunten, verhoogde chattiness van client en server, afzonderlijke authenticatie voor elke service. De afhankelijkheid van klanten van microservices maakt het moeilijk om de services ook te wijzigen. Een intuïtieve manier om dit te doen, is deze services achter een nieuwe servicelaag te verbergen en API's te bieden die op maat zijn gemaakt voor elke klant. Deze aggregator-servicelaag wordt ook API-gateway genoemd en is een gebruikelijke manier om dit probleem aan te pakken.

Op API Gateway gebaseerd microservices architectuurpatroon

Alle aanvragen van klanten gaan eerst via de API Gateway. Vervolgens stuurt het aanvragen door naar de juiste microservice. De API Gateway zal een aanvraag vaak verwerken door meerdere microservices aan te roepen en de resultaten te verzamelen. Het kan andere verantwoordelijkheden hebben, zoals authenticatie, monitoring, load balancing, caching en afhandeling van statische reacties. Omdat deze gateway client-specifieke API's biedt, vermindert het het aantal retourreizen tussen de client en de applicatie, wat de netwerklatentie vermindert en het vereenvoudigt ook de clientcode.

De functionele ontleding van de monoliet hangt af van het gebruik. Amazon gebruikt meer dan 100 microservices om een ​​enkele productpagina weer te geven, terwijl Netflix meer dan 600 microservices heeft om hun backend te beheren. De microservices in het bovenstaande diagram geven u een idee van hoe een schaalbare eCommerce-applicatie moet worden ontleed, maar een zorgvuldige observatie kan nodig zijn voordat u deze voor productie implementeert.

Er is niet zoiets als een gratis lunch. Microservices brengt een aantal complexe uitdagingen met zich mee, zoals:

  • Gedistribueerde computeruitdagingen: aangezien verschillende microservices in een gedistribueerde omgeving moeten worden uitgevoerd, moeten we voor deze fouten in gedistribueerde computergebruik zorgen. Kortom, we moeten aannemen dat het gedrag en de locaties van de componenten van ons systeem voortdurend zullen veranderen.
  • Op afstand bellen is duur: ontwikkelaars moeten kiezen en een efficiënt inter-procescommunicatiemechanisme implementeren.
  • Gedistribueerde transacties: zakelijke transacties die meerdere zakelijke entiteiten bijwerken, moeten vertrouwen op de uiteindelijke consistentie via ACID.
  • Omgaan met service onbeschikbaarheid: we moeten ons systeem ontwerpen om onbeschikbaarheid of traagheid van services af te handelen. Alles faalt de hele tijd.
  • Implementeren van functies die meerdere services omvatten.
  • Integratietesten en verandermanagement worden moeilijk.

Natuurlijk begint het manueel beheren van complexiteiten van microservices al snel uit de hand te lopen. Om een ​​geautomatiseerd en zelfherstellend gedistribueerd systeem te bouwen, hebben we de volgende functies in onze architectuur nodig.

  • Centrale configuratie: een gecentraliseerd configuratiesysteem met versiebeheer, zoiets als Zookeeper, waarvan wijzigingen dynamisch worden toegepast op actieve services.
  • Service discovery: elke actieve service moet zichzelf registreren bij een service discovery server en de server vertelt iedereen die online is. Net als een typische chat-app. We willen het eindpunt van de service niet in elkaar coderen.
  • Load balancing: client side load balancing, zodat u complexe balancing-strategieën kunt toepassen en caching, batching, fouttolerantie, service discovery kunt uitvoeren en meerdere protocollen kunt afhandelen.
  • Communicatie tussen processen: we moeten een efficiënte strategie voor communicatie tussen processen implementeren. Het kan van alles zijn zoals REST of Thrift of asynchrone, op berichten gebaseerde communicatiemechanismen zoals AMQP of STOMP. We kunnen ook efficiënte berichtindelingen gebruiken, zoals Avro of Protocol Buffers, omdat deze niet worden gebruikt om te communiceren met de buitenwereld.
  • Verificatie en beveiliging: we hebben een systeem nodig voor het identificeren van verificatievereisten voor elke bron en het afwijzen van verzoeken die niet aan deze voldoen.
  • Niet-blokkerende IO: API Gateway verwerkt aanvragen door meerdere backend-services aan te roepen en de resultaten te aggregeren. Bij sommige aanvragen, zoals een aanvraag voor productdetails, zijn de aanvragen voor backend-services onafhankelijk van elkaar. Om de responstijd te minimaliseren, moet de API Gateway tegelijkertijd onafhankelijke aanvragen uitvoeren.
  • Eventuele consistentie: we hebben een systeem nodig om zakelijke transacties af te handelen die meerdere services omvatten. Wanneer een service zijn database bijwerkt, moet deze een evenement publiceren en moet er een Message Broker zijn die garandeert dat evenementen minstens één keer worden afgeleverd bij de abonnerende services.
  • Fouttolerantie: we moeten de situatie vermijden waarbij een enkele fout overloopt in een systeemfout. API Gateway mag nooit voor onbepaalde tijd wachten op een downstream-service. Het moet fouten netjes afhandelen en waar mogelijk gedeeltelijke reacties retourneren.
  • Gedistribueerde sessies: idealiter zouden we geen status op de server moeten hebben. Applicatiestatus moet aan clientzijde worden opgeslagen. Dat is een van de belangrijke principes van een RESTful-service. Maar als je een uitzondering hebt en je kunt dit niet vermijden, zorg dan altijd voor gedistribueerde sessies. Omdat de client alleen met API Gateway communiceert, moeten we er meerdere kopieën achter een load balancer uitvoeren, omdat we niet willen dat API Gateway een bottleneck wordt. Dit betekent dat de daaropvolgende aanvragen van de klant op elk van de actieve instanties van API Gateway kunnen belanden. We moeten een manier hebben om de authenticatie-informatie te delen met verschillende instanties van API Gateway. We willen niet dat de client opnieuw authenticeert telkens wanneer zijn verzoek op een ander exemplaar van API Gateway valt.
  • Gedistribueerde caching: we moeten cachingmechanismen op meerdere niveaus hebben om clientlatentie te verminderen. Meerdere niveaus betekent gewoon dat client, API Gateway en microservices elk een betrouwbaar caching-mechanisme moeten hebben.
  • Gedetailleerde monitoring: we moeten in staat zijn om zinvolle gegevens en statistieken van elke functionele component te volgen om ons een nauwkeurig beeld van de productie te geven. Juiste alarmen moeten worden geactiveerd in het geval van uitzonderingen of hoge responstijden.
  • Dynamische routing: API Gateway moet in staat zijn om de aanvragen op intelligente wijze naar microservices te routeren als er geen specifieke toewijzing aan de gevraagde bron is. Met andere woorden, wijzigingen in API-gateway zijn niet vereist telkens wanneer een microservice een nieuw eindpunt toevoegt.
  • Auto Scaling: elk onderdeel van onze architectuur, inclusief API Gateway, moet horizontaal schaalbaar zijn en automatisch schalen indien nodig, zelfs als het in een container wordt geïmplementeerd.
  • Polyglot-ondersteuning: aangezien verschillende microservices in verschillende talen of frameworks kunnen worden geschreven, moet het systeem soepele service-aanroepen en bovengenoemde functies bieden, ongeacht de taal waarin het is geschreven.
  • Soepele implementatie: de implementatie van onze microservices moet snel, onafhankelijk en indien mogelijk geautomatiseerd zijn.
  • Platformonafhankelijk: om efficiënt gebruik te maken van hardware en onze services onafhankelijk te houden van het platform waarop deze worden ingezet, moeten we onze webservices in een container zoals de docker inzetten.
  • Logboekaggregatie: we moeten een systeem hebben dat automatisch logboeken van alle microservices op een bestandssysteem verzamelt. Deze logboeken kunnen later voor verschillende analyses worden gebruikt.

Whoa! Dit zijn veel functies om te implementeren, alleen om voor een architectuur te zorgen. Is dit het echt waard? En het antwoord is "Ja". De microservices-architectuur is in de strijd getest door bedrijven als Netflix, die alleen al ongeveer 40% van de bandbreedte van het internet in beslag neemt.

In mijn volgende bericht beschrijf ik hoe we kunnen beginnen met het ontwerpen van onze microservices. Blijf op de hoogte en volg me overal op YouTube voor updates. Vrede uit!