Geschiedenis van de Dodo IS-architectuur: een vroege monoliet

Of elk ongelukkig bedrijf met een monoliet is op zijn eigen manier ongelukkig.

De ontwikkeling van het Dodo IS-systeem begon onmiddellijk, net als bij Dodo Pizza, in 2011. Het was gebaseerd op het idee van volledige en totale digitalisering van bedrijfsprocessen, en alleen, wat toen al in 2011 voor veel vragen en scepsis zorgde. Maar al 9 jaar volgen we deze weg - met onze eigen ontwikkeling, die begon met een monoliet.

Dit artikel is een "antwoord" op de vragen "Waarom de architectuur herschrijven en zulke grootschalige en langdurige veranderingen doorvoeren?" terug naar vorig artikel "Geschiedenis van de Dodo IS-architectuur: de weg van de backoffice". Ik zal beginnen met hoe de ontwikkeling van Dodo IS begon, hoe de oorspronkelijke architectuur eruit zag, hoe nieuwe modules verschenen en door welke problemen er grootschalige wijzigingen moesten worden aangebracht.

Geschiedenis van de Dodo IS-architectuur: een vroege monoliet

Artikelreeks "Wat is Dodo IS?" vertelt over:

  1. Vroege monoliet in Dodo IS (2011-2015). (je bent hier)

  2. Het backoffice-pad: afzonderlijke bases en bus.

  3. Het pad aan de klantzijde: gevel over de sokkel (2016-2017). (Bezig…)

  4. De geschiedenis van echte microservices. (2018-2019). (Bezig…)

  5. Afgewerkt zagen van de monoliet en stabilisatie van de architectuur. (Bezig...)

Oorspronkelijke architectuur

In 2011 zag de Dodo IS-architectuur er zo uit:

Geschiedenis van de Dodo IS-architectuur: een vroege monoliet

De eerste module in de architectuur is orderacceptatie. Het bedrijfsproces was:

  • de klant belt de pizzeria;

  • de manager neemt de telefoon op;

  • neemt telefonisch een bestelling aan;

  • vult het parallel in de orderacceptatie-interface: het houdt rekening met informatie over de klant, gegevens over orderdetails, afleveradres. 

De interface van het informatiesysteem zag er ongeveer zo uit ...

Eerste versie van oktober 2011:

Iets verbeterd in januari 2012

Dodo Pizza Informatie Systeem Levering Pizza Restaurant

De middelen voor de ontwikkeling van de eerste module voor het opnemen van bestellingen waren beperkt. We moesten veel doen, snel en met een klein team. Een klein team bestaat uit 2 ontwikkelaars die de basis hebben gelegd voor het gehele toekomstige systeem.

Hun eerste beslissing bepaalde het lot van de technologiestapel:

  • Backend op ASP.NET MVC, C#-taal. De ontwikkelaars waren dotnetchiki, deze stapel was vertrouwd en prettig voor hen.

  • Frontend op Bootstrap en JQuery: gebruikersinterfaces op zelfgeschreven stijlen en scripts. 

  • MySQL-database: geen licentiekosten, eenvoudig in gebruik.

  • Servers op Windows Server, omdat .NET dan alleen onder Windows kon staan ​​(mono bespreken we niet).

Fysiek kwam dit alles tot uiting in de “dedic at the hoster”. 

Order Intake Applicatie Architectuur

Toen had iedereen het al over microservices en werd SOA 5 jaar lang gebruikt in grote projecten, zo kwam WCF uit in 2006. Maar toen kozen ze voor een betrouwbare en bewezen oplossing.

Hier is het.

Geschiedenis van de Dodo IS-architectuur: een vroege monoliet

Asp.Net MVC is Razor, dat op verzoek van een formulier of van een client een HTML-pagina met serverweergave weergeeft. Op de client geven CSS- en JS-scripts al informatie weer en voeren ze, indien nodig, AJAX-verzoeken uit via JQuery.

Verzoeken op de server komen terecht in de *Controller-klassen, waar de verwerking en het genereren van de uiteindelijke HTML-pagina in de methode plaatsvindt. Controllers doen verzoeken aan een logicalaag genaamd *Services. Elk van de diensten kwam overeen met een bepaald aspect van het bedrijf:

  • AfdelingStructuurService gaf bijvoorbeeld informatie uit over pizzeria's, over afdelingen. Een afdeling is een groep pizzeria's die wordt gerund door één franchisenemer.

  • ReceivingOrdersService heeft de bestelling geaccepteerd en berekend.

  • En SmsService stuurde sms door API-services aan te roepen om sms te verzenden.

Services verwerkte gegevens uit de database, bewaarde bedrijfslogica. Elke service had een of meer *Repositories met de juiste naam. Ze bevatten al query's naar opgeslagen procedures in de database en een laag mappers. Er zat zakelijke logica in de opslagruimtes, vooral veel in degenen die rapportagegegevens uitbrachten. ORM werd niet gebruikt, iedereen vertrouwde op handgeschreven sql. 

Er was ook een laag van het domeinmodel en gemeenschappelijke helperklassen, bijvoorbeeld de Order-klasse die de order opsloeg. Op dezelfde plaats, in de laag, was er een helper voor het converteren van de weergegeven tekst volgens de geselecteerde valuta.

Dit alles kan worden weergegeven door een dergelijk model:

Geschiedenis van de Dodo IS-architectuur: een vroege monoliet

Bestel manier

Overweeg een vereenvoudigde eerste manier om zo'n bestelling te maken.

Geschiedenis van de Dodo IS-architectuur: een vroege monoliet

Aanvankelijk was de site statisch. Er stonden prijzen op, en bovenop - een telefoonnummer en de inscriptie "Als je pizza wilt, bel dan het nummer en bestel." Om te bestellen, moeten we een eenvoudige stroom implementeren: 

  • De klant bezoekt een statische site met prijzen, selecteert producten en belt het nummer dat op de site vermeld staat.

  • De klant benoemt de producten die hij aan de bestelling wil toevoegen.

  • Geeft zijn adres en naam.

  • De telefoniste accepteert de bestelling.

  • De bestelling wordt weergegeven in de interface voor geaccepteerde bestellingen.

Het begint allemaal met het weergeven van het menu. Een ingelogde gebruiker-operator accepteert slechts één bestelling tegelijk. Daarom kan de conceptkar in zijn sessie worden opgeslagen (de sessie van de gebruiker wordt in het geheugen opgeslagen). Er is een Cart-object met producten en klantinformatie.

De klant benoemt het product, de telefoniste klikt aan + naast het product en er wordt een verzoek naar de server verzonden. Informatie over het product wordt uit de database gehaald en informatie over het product wordt aan de winkelwagen toegevoegd.

Geschiedenis van de Dodo IS-architectuur: een vroege monoliet

Noot. Ja, hier kun je het product niet uit de database halen, maar overzetten vanuit de frontend. Maar voor de duidelijkheid heb ik precies het pad uit de database laten zien. 

Voer vervolgens het adres en de naam van de klant in. 

Geschiedenis van de Dodo IS-architectuur: een vroege monoliet

Wanneer u op "Bestelling maken" klikt:

  • Het verzoek wordt verzonden naar OrderController.SaveOrder().

  • We krijgen winkelwagen van de sessie, er zijn producten in de hoeveelheid die we nodig hebben.

  • We vullen de winkelwagen aan met informatie over de klant en geven deze door aan de AddOrder-methode van de ReceivingOrderService-klasse, waar deze wordt opgeslagen in de database. 

  • De database heeft tabellen met de bestelling, de samenstelling van de bestelling, de klant en ze zijn allemaal met elkaar verbonden.

  • De interface voor het weergeven van bestellingen haalt de laatste bestellingen eruit en geeft ze weer.

Nieuwe modulen

De bestelling opnemen was belangrijk en noodzakelijk. Je kunt geen pizzabedrijf runnen als je geen bestelling hebt om te verkopen. Daarom begon het systeem functionaliteit te verwerven - ongeveer van 2012 tot 2015. Gedurende deze tijd verschenen er veel verschillende blokken van het systeem, die ik zal noemen modules, in tegenstelling tot het concept van dienst of product. 

Een module is een set functies die zijn verenigd door een gemeenschappelijk bedrijfsdoel. Tegelijkertijd bevinden ze zich fysiek in dezelfde applicatie.

Modules kunnen systeemblokken worden genoemd. Dit is bijvoorbeeld een rapportagemodule, beheerinterfaces, voedseltracker in de keuken, autorisatie. Dit zijn allemaal verschillende gebruikersinterfaces, sommige hebben zelfs verschillende visuele stijlen. Tegelijkertijd zit alles binnen de kaders van één applicatie, één lopend proces. 

Technisch zijn de modules ontworpen als Area (zo'n idee bleef zelfs binnen asp.net-kern). Er waren afzonderlijke bestanden voor de frontend, modellen en hun eigen controllerklassen. Als gevolg hiervan is het systeem getransformeerd van dit ...

Geschiedenis van de Dodo IS-architectuur: een vroege monoliet

...in dit:

Geschiedenis van de Dodo IS-architectuur: een vroege monoliet

Sommige modules worden door aparte sites geïmplementeerd (uitvoerbaar project), vanwege een volledig gescheiden functionaliteit en deels vanwege een iets aparte, meer gerichte ontwikkeling. Dit:

  • Website - eerste versie website dodopizza.ru.

  • Exporteren: rapporten uploaden van Dodo IS voor 1C. 

  • persoonlijke - persoonlijk account van de werknemer. Het is apart ontwikkeld en heeft een eigen instappunt en aparte vormgeving.

  • fs — een project voor het hosten van statica. Later zijn we ervan weggegaan en hebben we alle statica naar de Akamai CDN verplaatst. 

De rest van de blokken zaten in de BackOffice-applicatie. 

Geschiedenis van de Dodo IS-architectuur: een vroege monoliet

Naam uitleg:

  • Kassier - Restaurantkassier.

  • ShiftManager - interfaces voor de rol van "Shift Manager": operationele statistieken over de verkoop van pizzeria's, de mogelijkheid om producten op de stoplijst te plaatsen, de volgorde wijzigen.

  • OfficeManager - interfaces voor de rollen "Pizzeria Manager" en "Franchisenemer". Hier zijn verzamelde functies voor het opzetten van een pizzeria, de bonuspromoties, het ontvangen van en werken met werknemers, rapporten.

  • PublicScreens - interfaces voor tv's en tablets die in pizzeria's hangen. Tv's geven menu's, advertentie-informatie en bestelstatus weer bij levering. 

Ze gebruikten een gemeenschappelijke servicelaag, een gemeenschappelijk Dodo.Core domeinklassenblok en een gemeenschappelijke basis. Soms konden ze nog langs de overgangen naar elkaar toe leiden. Inclusief individuele sites, zoals dodopizza.ru of personal.dodopizza.ru, gingen naar algemene diensten.

Toen er nieuwe modules verschenen, probeerden we de reeds gemaakte code van services, opgeslagen procedures en tabellen in de database maximaal te hergebruiken. 

Voor een beter begrip van de schaal van de modules die in het systeem zijn gemaakt, is hier een diagram uit 2012 met ontwikkelingsplannen:

Geschiedenis van de Dodo IS-architectuur: een vroege monoliet

In 2015 stond alles op de kaart en was er nog meer in productie.

  • Orderacceptatie is uitgegroeid tot een apart blok van het Contact Center, waar de order wordt geaccepteerd door de operator.

  • In pizzeria's hingen openbare schermen met menukaarten en informatie.

  • De keuken heeft een module die automatisch het spraakbericht "Nieuwe Pizza" afspeelt wanneer er een nieuwe bestelling binnenkomt, en ook een factuur voor de koerier print. Dit vereenvoudigt de processen in de keuken aanzienlijk, waardoor werknemers niet worden afgeleid door een groot aantal eenvoudige handelingen.

  • De bezorgunit werd een aparte Delivery Checkout, waar de bestelling werd afgegeven aan de koerier die eerder de shift had aangenomen. Bij de loonberekening werd rekening gehouden met zijn werktijd. 

Tegelijkertijd verschenen van 2012 tot 2015 meer dan 10 ontwikkelaars, openden 35 pizzeria's, implementeerden het systeem in Roemenië en bereidden ze zich voor op de opening van verkooppunten in de Verenigde Staten. De ontwikkelaars namen niet meer alle taken voor hun rekening, maar werden in teams verdeeld. elk gespecialiseerd in zijn eigen deel van het systeem. 

Problemen

Onder meer vanwege de architectuur (maar niet alleen).

Chaos in de basis

Eén basis is handig. Er kan consistentie in worden bereikt, en dit gaat ten koste van tools die zijn ingebouwd in relationele databases. Het werken ermee is vertrouwd en handig, vooral als er weinig tabellen en weinig gegevens zijn.

Maar na 4 jaar ontwikkeling bleek de database ongeveer 600 tabellen te hebben, 1500 opgeslagen procedures, waarvan er vele ook logica hadden. Helaas bieden opgeslagen procedures niet veel voordeel bij het werken met MySQL. Ze worden niet in de cache opgeslagen door de basis en het opslaan van logica bemoeilijkt ontwikkeling en foutopsporing. Hergebruik van code is ook moeilijk.

Veel tabellen hadden geen geschikte indexen, ergens waren er daarentegen veel indexen, waardoor het moeilijk was om in te voegen. Het was nodig om ongeveer 20 tafels aan te passen - de transactie om een ​​bestelling aan te maken kan ongeveer 3-5 seconden duren. 

De gegevens in de tabellen waren niet altijd in de meest geschikte vorm. Ergens was het nodig om te denormaliseren. Een deel van de regelmatig ontvangen data stond in een kolom in de vorm van een XML-structuur, dit verhoogde de uitvoeringstijd, verlengde de queries en bemoeilijkte de ontwikkeling.

Aan dezelfde tafels werden zeer geproduceerd heterogene aanvragen. Vooral populaire tafels hadden het zwaar, zoals de hierboven genoemde tafel. orders of tafels pizzeria. Ze werden gebruikt om operationele interfaces in de keuken weer te geven, analyses. Een andere site nam contact met hen op (dodopizza.ru), waar op een gegeven moment ineens heel veel verzoeken konden komen. 

De gegevens zijn niet geaggregeerd en veel berekeningen vonden on the fly plaats met behulp van de basis. Dit zorgde voor onnodige berekeningen en extra belasting. 

Vaak ging de code naar de database terwijl dat niet had gekund. Ergens waren er niet genoeg bulkbewerkingen, ergens zou het nodig zijn om één aanvraag via de code over meerdere verzoeken te verspreiden om de betrouwbaarheid te versnellen en te vergroten. 

Cohesie en onduidelijkheid in code

Modules die verondersteld werden verantwoordelijk te zijn voor hun deel van het bedrijf, deden dat niet eerlijk. Sommigen van hen hadden dubbele functies voor rollen. Een lokale marketeer die verantwoordelijk is voor de marketingactiviteiten van het netwerk in zijn stad, moest bijvoorbeeld zowel de "Admin"-interface (om promoties aan te maken) als de "Office Manager"-interface (om de impact van promoties op het bedrijf te bekijken) gebruiken. Natuurlijk gebruikten beide modules binnen dezelfde service die werkte met bonuspromoties.

Services (klassen binnen één monolithisch groot project) konden elkaar bellen om hun data te verrijken.

Met de modelklassen zelf die gegevens opslaan, werk in de code werd anders uitgevoerd. Er waren ergens constructeurs waarmee het mogelijk was om verplichte velden te specificeren. Ergens gebeurde dit via openbare eigendommen. Natuurlijk was het ophalen en transformeren van gegevens uit de database gevarieerd. 

De logica zat in de controllers of in de serviceklassen. 

Dit lijken kleine problemen, maar ze hebben de ontwikkeling enorm vertraagd en de kwaliteit verminderd, wat leidde tot instabiliteit en bugs. 

De complexiteit van een grote ontwikkeling

Moeilijkheden deden zich voor in de ontwikkeling zelf. Het was nodig om verschillende blokken van het systeem te maken, en parallel. Het werd steeds moeilijker om de behoeften van elk onderdeel in één enkele code in te passen. Het was niet gemakkelijk om het eens te worden en alle componenten tegelijkertijd tevreden te stellen. Daarbij kwamen beperkingen in technologie, vooral met betrekking tot de basis en frontend. Het was noodzakelijk om jQuery te verlaten voor frameworks op hoog niveau, vooral op het gebied van klantenservice (website).

In sommige delen van het systeem zouden hiervoor geschiktere databases kunnen worden gebruikt.. Later hadden we bijvoorbeeld de use case om van Redis naar CosmosDB te verhuizen om een ​​ordermand op te slaan. 

Teams en ontwikkelaars betrokken bij hun vakgebied wilden duidelijk meer autonomie voor hun diensten, zowel qua ontwikkeling als uitrol. Conflicten samenvoegen, problemen vrijgeven. Als dit probleem voor 5 ontwikkelaars onbeduidend is, dan zou met 10, en nog meer met de geplande groei, alles serieuzer worden. En vooruit was de ontwikkeling van een mobiele applicatie (het begon in 2017, en in 2018 was het grote val). 

Verschillende onderdelen van het systeem vereisten verschillende niveaus van stabiliteit, maar vanwege de sterke connectiviteit van het systeem konden we dit niet leveren. Een fout in de ontwikkeling van een nieuwe functie in het admin-paneel zou heel goed kunnen zijn opgetreden bij het accepteren van een bestelling op de site, omdat de code gemeenschappelijk en herbruikbaar is, de database en gegevens zijn ook hetzelfde.

Binnen zo'n monolithisch-modulaire architectuur zouden deze fouten en problemen wellicht te vermijden zijn: maak een verantwoordelijkheidsverdeling, refactor zowel de code als de database, scheid de lagen duidelijk van elkaar, bewaak dagelijks de kwaliteit. Maar de gekozen architectonische oplossingen en de focus op het snel uitbreiden van de functionaliteit van het systeem leidden tot stabiliteitsproblemen.

Hoe de Power of the Mind-blog de kassa's in restaurants neerzet

Als de groei van het pizzerianetwerk (en de belasting) in hetzelfde tempo zou doorgaan, dan zouden de dalingen na een tijdje zodanig zijn dat het systeem niet zou stijgen. Dit illustreert goed de problemen waarmee we in 2015 te maken kregen, hier is zo'n verhaal. 

In de blog "Gedachte kracht” was een widget die gegevens over de omzet voor het hele jaar van het hele netwerk liet zien. De widget heeft toegang gekregen tot de openbare API van Dodo, die deze gegevens levert. Deze statistiek is momenteel beschikbaar op http://dodopizzastory.com/. De widget werd op elke pagina getoond en deed elke 20 seconden verzoeken op een timer. Het verzoek ging naar api.dodopizza.ru en verzocht om:

  • het aantal pizzeria's in het netwerk;

  • totale netwerkomzet sinds het begin van het jaar;

  • omzet voor vandaag.

Het verzoek om statistieken over inkomsten ging rechtstreeks naar de database en begon met het opvragen van gegevens over bestellingen, het verzamelen van gegevens in een oogwenk en het uitdelen van het bedrag. 

Kassa's in restaurants gingen naar dezelfde tafel met bestellingen, laadden een lijst met ontvangen bestellingen voor vandaag uit en er werden nieuwe bestellingen aan toegevoegd. Kassa's deden hun verzoeken om de 5 seconden of bij het vernieuwen van de pagina.

Het schema zag er als volgt uit:

Geschiedenis van de Dodo IS-architectuur: een vroege monoliet

Op een herfst schreef Fjodor Ovchinnikov een lang en populair artikel op zijn blog. Veel mensen kwamen naar de blog en begonnen alles aandachtig te lezen. Terwijl elk van de mensen die kwamen het artikel aan het lezen was, werkte de inkomstenwidget naar behoren en vroeg de API elke 20 seconden op.

De API riep een opgeslagen procedure aan om de som van alle bestellingen sinds het begin van het jaar te berekenen voor alle pizzeria's in de keten. De aggregatie was gebaseerd op de bestellingentabel, die erg populair is. Alle kassa's van alle openstaande restaurants gaan er op dat moment naar toe. Kassa's reageerden niet meer, bestellingen werden niet aangenomen. Ze werden ook niet geaccepteerd van de site, verschenen niet op de tracker, de shiftmanager kon ze niet zien in zijn interface. 

Dit is niet het enige verhaal. Tegen het najaar van 2015 was de belasting van het systeem elke vrijdag kritiek. Meerdere keren hebben we de openbare API uitgeschakeld en één keer moesten we zelfs de site uitschakelen, omdat niets hielp. Er was zelfs een lijst met services met een sluitingsopdracht onder zware belasting.

Vanaf nu begint onze strijd met ladingen en voor de stabilisatie van het systeem (van herfst 2015 tot herfst 2018). Toen gebeurde het"geweldige val". Verder deden zich soms ook storingen voor, sommige waren erg gevoelig, maar de algemene periode van instabiliteit kan nu als voorbij worden beschouwd.

Snelle bedrijfsgroei

Waarom kon dat niet meteen? Kijk maar eens naar de volgende grafieken.

Geschiedenis van de Dodo IS-architectuur: een vroege monoliet

Ook in 2014-2015 was er een opening in Roemenië en werd een opening in de VS voorbereid.

Het netwerk groeide zeer snel, nieuwe landen werden geopend, nieuwe formules van pizzeria's verschenen, er werd bijvoorbeeld een pizzeria geopend op de foodcourt. Dit alles vereiste veel aandacht, met name voor de uitbreiding van Dodo IS-functies. Zonder al deze functies, zonder tracking in de keuken, boekhouding van producten en verliezen in het systeem, weergave van de uitgifte van een bestelling in de food court-zaal, zouden we nauwelijks praten over de "juiste" architectuur en de "juiste" benadering van ontwikkeling nu.

Een ander obstakel voor tijdige herziening van de architectuur en algemene aandacht voor technische problemen was de crisis van 2014. Dingen als deze hebben grote gevolgen voor de groeimogelijkheden van teams, vooral voor een jong bedrijf als Dodo Pizza.

Snelle oplossingen die hielpen

Problemen hadden oplossingen nodig. Conventioneel kunnen oplossingen in 2 groepen worden verdeeld:

  • Snelle die het vuur doven en een kleine veiligheidsmarge geven en ons tijd geven om te veranderen.

  • Systemisch en dus lang. Re-engineering van een aantal modules, opdeling van een monolithische architectuur in afzonderlijke services (de meeste zijn helemaal geen micro-, maar eerder macro-services, en er is iets mee Het rapport van Andrey Morevskiy). 

De droge lijst met snelle wijzigingen is als volgt:

Schaal de basismaster op

Het eerste dat wordt gedaan om met belastingen om te gaan, is natuurlijk het vergroten van de capaciteit van de server. Dit is gedaan voor de hoofddatabase en voor webservers. Helaas kan dit maar tot een bepaalde grens, dan wordt het te duur.

Sinds 2014 zijn we overgestapt op Azure, we schreven destijds ook over dit onderwerp in het artikel “Hoe Dodo Pizza pizza bezorgt met behulp van de Microsoft Azure Cloud". Maar na een reeks verhogingen van de server voor de basis, stuitten ze op de kosten. 

Basisreplica's om te lezen

Voor de basis zijn twee replica's gemaakt:

LeesReplica voor referentieaanvragen. Het wordt gebruikt om mappen, type, stad, straat, pizzeria, producten (langzaam veranderd domein) te lezen, en in die interfaces waar een kleine vertraging acceptabel is. Er waren 2 van deze replica's, we hebben ervoor gezorgd dat ze beschikbaar waren op dezelfde manier als de meesters.

ReadReplica voor rapportaanvragen. Deze database had een lagere beschikbaarheid, maar alle rapporten gingen ernaartoe. Laat ze zware verzoeken hebben voor enorme herberekeningen van gegevens, maar ze hebben geen invloed op de hoofddatabase en operationele interfaces. 

Caches in code

Er waren nergens caches in de code (helemaal niet). Dit leidde tot extra, niet altijd noodzakelijke verzoeken aan de geladen database. Caches waren eerst zowel in het geheugen als op een externe cacheservice, dat was Redis. Alles werd door de tijd ongeldig verklaard, de instellingen werden gespecificeerd in de code.

Meerdere backend-servers

De backend van de applicatie moest ook worden geschaald om de toegenomen werklast aan te kunnen. Het was nodig om van één iis-server een cluster te maken. We hebben een nieuwe afspraak gemaakt sollicitatiesessie van memory naar RedisCache, waardoor het mogelijk werd om met round robin meerdere servers achter een simpele loadbalancer te maken. Eerst werd dezelfde Redis gebruikt als voor caches, daarna werd het opgesplitst in meerdere. 

Als gevolg hiervan is de architectuur gecompliceerder geworden ...

Geschiedenis van de Dodo IS-architectuur: een vroege monoliet

… maar een deel van de spanning werd weggenomen.

En toen was het nodig om de geladen componenten opnieuw te doen, wat we hebben gedaan. We zullen hier in het volgende deel over praten.

Bron: www.habr.com

Voeg een reactie