Eller er hver ulykkelig virksomhed med en monolit ulykkelig på sin egen måde.
Udviklingen af Dodo IS-systemet begyndte straks som Dodo Pizza-virksomheden — i 2011. Grundidéen var at fuldstændigt og totalt digitalisere forretningsprocesserne, og , hvilket allerede dengang i 2011 vakte mange spørgsmål og skepsis. Men nu har vi i 9 år været på denne vej — med egen udvikling, der startede med en monolit.
Denne artikel er 'svaret' på spørgsmålene 'Hvorfor genopbygge arkitekturen og lave så store og langsommelige ændringer?' til den tidligere artikel . Jeg vil starte med, hvordan udviklingen af Dodo IS begyndte, hvordan den oprindelige arkitektur så ud, hvordan nye moduler blev til, og hvilke problemer der fik os til at gennemføre omfattende ændringer.

Artikelsserien «Hvad er Dodo IS?» vil fortælle om:
Den tidlige monolit i Dodo IS (2011-2015). (Du er her)
.
Vejen til klientdelen: facade over databasen (2016-2017). (I gang…)
Historien om ægte mikrotjenester. (2018-2019). (I gang…)
Den fuldstændige nedbrydning af monolitten og stabilisering af arkitekturen. (I gang…)
Den oprindelige arkitektur
I 2011 så Dodo IS-arkitekturen således ud:

Det første modul i arkitekturen er ordreaccept. Forretningsprocessen var sådan:
kunden ringer til pizzariaet;
en manager besvarer opkaldet;
modtager ordren telefonisk;
taster den samtidig ind i ordreacceptgrænsefladen: oplysninger om kunden, detaljerne om ordren, leveringsadressen tages i betragtning.
Brugergrænsefladen for informationssystemet så ud omtrent sådan her...
Første version fra oktober 2011:
Ressourcerne til udviklingen af det første ordreacceptmodul var begrænsede. Der skulle laves meget, hurtigt og med få personer. Få personer — det var 2 udviklere, der lagde fundamentet for hele det fremtidige system.
Deres første beslutning bestemte den videre skæbne for teknologistakken:
Backend på ASP.NET MVC, programmeringssprog C#. Udviklerne var .NET-programmører, og denne stak var kendt og behagelig for dem.
Frontend på Bootstrap og JQuery: brugergrænseflader med egne stilarter og scripts.
MySQL-database: uden licensomkostninger, nem at bruge.
Servere på Windows Server, fordi .NET dengang kun kunne køre på Windows (vi lader mono ligge).
Fysisk udtrykte det hele sig i "dedikationen hos hosten".
Arkitekturen for bestillingsmodtagelsesapplikationen
På det tidspunkt talte alle om mikroservices, mens SOA blev brugt i store projekter i cirka 5 år; WCF blev eksempelvis lanceret i 2006. Men dengang valgte man en pålidelig og testet løsning.
Her er den.

Asp.Net MVC er Razor, der ved anmodning fra formularen eller fra klienten genererer en HTML-side med rendering på serveren. På klienten viser CSS og JS-scripts oplysningerne og udfører, om nødvendigt, AJAX-anmodninger via JQuery.
Anmodningerne på serveren havner i klasserne *Controller, hvor metoden behandler og genererer den endelige HTML-side. Controllerne foretager anmodninger til logiklaget, kaldet *Services. Hver service svarede til et aspekt af forretningen:
For eksempel gav DepartmentStructureService oplysninger om pizzarierne, om afdelingerne. En afdeling er en gruppe pizzarier under ledelse af en franchisetager.
ReceivingOrdersService tog imod og beregnede indholdet af bestillingen.
SmsService sendte SMS'er ved at kalde API-tjenester til sending af SMS'er.
Tjenesterne behandlede data fra databasen og opbevarede forretningslogikken. Hver service havde en eller flere *Repository med det relevante navn. Her lå anmodningerne til de gemte procedurer i databasen samt mapperlaget. I de gemte procedurer var der forretningslogik, især meget i dem, der genererede rapportdata. ORM blev ikke brugt, alle stolede på håndskrevet SQL.
Der var også et lag af domænemodel og generelle hjælpeklasser, for eksempel klassen Order, der opbevarede bestillingen. Her lå også en hjælper til at omforme visningsteksten baseret på den valgte valuta.
Alt dette kan repræsenteres med denne model:

Bestillingsvejen
Lad os se på en forenklet oprindelig vej til oprettelse af en sådan bestilling.

Oprindeligt var hjemmesiden statisk. Der var priser, og øverst stod telefonnummeret samt teksten "Vil du have pizza — ring til nummeret og bestil". For at kunne bestille, skal vi implementere et simpelt flow:
Klienten går ind på den statiske hjemmeside med priserne, vælger produkter og ringer til det nummer, der står på hjemmesiden.
Klienten oplyser de produkter, han ønsker at tilføje til bestillingen.
Oplyser sin adresse og navn.
Operatøren tager imod bestillingen.
Bestillingen vises i grænsefladen for modtagne bestillinger.
Det hele starter med visningen af menuen. Den loggede bruger-operator accepterer kun én ordre ad gangen. Derfor kan draft-kurven gemmes i hans session (brugerens session opbevares i hukommelsen). Her findes et objekt Cart, hvor produkterne og informationen om kunden gemmes.
Kunden nævner produktet, og operatøren klikker på + ved siden af produktet, og der sendes en forespørgsel til serveren. Der trækkes information om produktet fra databasen og tilføjes til kurven.

Bemærk. Ja, her kan man undgå at trække produktet fra databasen og i stedet overføre det fra frontend. Men for tydelighedens skyld har jeg vist stien fra databasen.
Derefter indtastes adresse og navn på kunden.

Ved klik på „Opret ordre“:
Sender vi forespørgslen til OrderController.SaveOrder().
Vi henter Cart fra sessionen, hvor produkterne er i den ønskede mængde.
Vi supplerer Cart med oplysninger om kunden og sender den til metoden AddOrder i klassen ReceivingOrderService, hvor den gemmes i databasen.
I databasen findes der tabeller med ordren, ordresammensætningen, kunden, og de er alle forbundet.
Brugergrænsefladen til ordrevisning går ind og henter de seneste ordrer og viser dem.
Nye moduler
Ordre modtagelsen var vigtig og nødvendig. Man kan ikke drive en pizza-forretning, hvis der ikke er en modtagelse af ordrer. Derfor begyndte systemet at tilegne sig funktionalitet — cirka fra 2012 til 2015. I denne periode blev mange forskellige blokke af systemet til, som jeg vil kalde moduler, i modsætning til begrebet service eller produkt.
Et modul er en samling funktioner, der er samlet omkring et fælles forretningsmål. Samtidig befinder de sig fysisk i en og samme applikation.
Moduler kan kaldes systemblokke. For eksempel er der dette modul til rapporter, admin-grænseflader, , autorisation. Dette er alle forskellige grænseflader for brugeren, nogle har endda forskellige visuelle stilarter. Alt dette foregår inden for én applikation, én arbejdsgang.
Teknisk set blev modulerne oprettet som Area (denne idé er faktisk stadig bevaret i ). Der var separate filer til frontend, modeller samt deres egne controllerklasser. Resultatet var, at systemet transformerede fra sådan…

…til sådan:

Nogle moduler er implementeret som separate websteder (udførelsesprojekt), på grund af helt særskilt funktionalitet og delvist på grund af mere separat, mere fokuseret udvikling. Dette er:
Websted — af webstedet dodopizza.ru.
Eksportér: eksport af rapporter fra Dodo IS til 1C.
Personlig — medarbejderens personlige konto. Den er udviklet separat og har sin egen adgang og design.
fs — projekt til hosting af statisk indhold. Senere gik vi væk fra det og flyttede alt statisk indhold til CDN Akamai.
De andre blokke blev placeret i BackOffice-applikationen.

Forklaring af navnene:
Cashier — Restaurantens kasse.
ShiftManager — grænseflader til rollen 'Skiftleder': operationel statistik over pizzariaets salg, mulighed for at sætte produkter på stoplisten, ændre ordre.
OfficeManager — grænseflader til rollerne 'Pizzarialeder' og 'Franchisetager'. Her findes funktioner til konfiguration af pizzariaet, dets bonuskampagner, modtagelse og arbejde med medarbejdere, rapportering.
PublicScreens — grænseflader til tv'er og tablets, der hænger i pizzariaerne. Menuen, reklaminformation og ordrestatus vises på tv'erne ved aflevering.
De benyttede et fælles lag af tjenester, et fælles blok af domæneklasser Dodo.Core, samt en fælles database. Nogle gange kunne de desuden føre forbindelser indbyrdes, herunder separate websteder som dodopizza.ru eller personal.dodopizza.ru.
Ved introduktion af nye moduler forsøgte de at genbruge så meget af den allerede oprettede kode fra tjenester, gemte procedurer og tabeller i databasen som muligt.
For bedre forståelse af omfanget af de moduler, der er lavet i systemet, her er en skema fra 2012 med udviklingsplaner:

Ved udgangen af 2015 var alt på skemaet og endda mere i produktion.
Ordreoptagelsen udviklede sig til en separat del af Kontaktcentret, hvor ordren blev taget af en operatør.
Der kom offentligt tilgængelige skærme med menu og information, som hænger i pizzariaerne.
I køkkenet findes der en modul, der automatisk afspiller den stemmeligde besked 'Ny pizza' ved modtagelse af en ny ordre, samt printer en følgeseddel til kureren. Dette forenkler processerne i køkkenet betydeligt og lader medarbejderne undgå distraktion fra mange simple opgaver.
Leveringsblokken blev til en separat Leveringskasse, hvor ordren blev udleveret til kureren, som først havde påtaget sig en vagt. Hans arbejdstid blev taget med i beregningen af løn.
Mellem 2012 og 2015 opstod der mere end 10 udviklere, der åbnede 35 pizzeriaer, udviklede systemet til Rumænien og forberedte åbningen af steder i USA. Udviklerne beskæftigede sig ikke længere med alle opgaver, men blev delt ind i teams, som hver især specialiserede sig i deres del af systemet.
Problemer
Det skyldes delvist arkitekturen (men ikke kun).
Kaos i databasen
En database er praktisk. Den kan opnå konsistens gennem de værktøjer, der er indbygget i relationelle databaser. Det er nemt og behageligt at arbejde med, især hvis der er få tabeller og lidt data.
Men efter 4 års udvikling var der omkring 600 tabeller, 1500 gemte procedurer i databasen, mange af dem med logik. Desværre giver gemte procedurer ikke nogen særlig fordel, når man arbejder med MySQL. De bliver ikke cachet af databasen, og at have logik i dem komplicerer udviklingen og fejlfindingen. Genbrug af kode var også besværligt.
Mange tabeller havde ikke passende indekser.Mange steder var der omvendt for mange indekser, hvilket besværliggjorde indsættelse. Det var nødvendigt at modificere omkring 20 tabeller — en transaktion for at oprette en ordre kunne tage 3-5 sekunder.
Dataene i tabellerne var ikke altid i den mest hensigtsmæssige form.Nogle steder var det nødvendigt med denormalisering. Nogle af de regelmæssigt indsendte data var i en kolonne som XML-struktur, hvilket forøgede behandlingstiden, forlængede forespørgslerne og komplicerede udviklingen.
Der blev udført meget forskellige forespørgsler på de samme tabeller.Især led de populære tabeller, som den nævnte tabel orders eller tabellen pizzeria.De blev anvendt til at fremvise operationelle grænseflader i køkkenet og til analyser. Desuden blev de tilgået af hjemmesiden (), hvor der når som helst kunne komme pludselige mange forespørgsler.
Dataene var ikke aggregerede og mange beregninger fandt sted on-the-fly med databaseværktøjer. Dette skabte unødvendige beregninger og ekstra belastning.
Koden gik ofte til databasen, når den ikke behøvede det. Nogle steder manglede bulk-operationer, og nogle forespørgsler skulle opdeles i flere gennem koden for at øge hastigheden og pålideligheden.
Sammenhæng og kompleksitet i koden
Moduler, der skulle tage ansvar for deres forretningsområde, gjorde ikke dette ordentligt.. Nogle af dem havde dublering af funktioner for roller. For eksempel måtte den lokale marketingspecialist, der var ansvarlig for marketingaktiviteterne i sit område, bruge både "Admin"-grænsefladen (til at oprette kampagner) og "Office Manager"-grænsefladen (til at se kampagners indflydelse på forretningen). Selvfølgelig brugte begge moduler en fælles tjeneste, der arbejdede med bonuskampagner.
Tjenesterne (klasser inden for et stort monolitisk projekt) kunne kalde hinanden for at berige deres data.
Med selve klassemodellerne, der gemte data, arbejdede koden forskelligt. Nogle steder var der konstruktører, hvor man kunne angive obligatoriske felter. Andre steder blev dette gjort via offentlige egenskaber. Selvfølgelig var indhentning og transformation af data fra databasen forskelligartet.
Logikken lå enten i controllere eller i serviceløsninger.
Dette kan virke som små problemer, men de bremede udviklingen betydeligt og sænkede kvaliteten, hvilket førte til ustabilitet og fejl.
Kompleksitet ved større udvikling
Der opstod også vanskeligheder i selve udviklingen. Der var behov for at lave forskellige systemblokke, og det skulle gøres parallelt. At imødekomme behovene for hver komponent i en samlet kode blev stadig vanskeligere. Det var ikke nemt at nå til enighed og tilfredsstille alle komponenter samtidigt. Dertil kom begrænsninger i teknologierne, især hvad angår databasen og frontend. Man var nødt til at opgive jQuery til fordel for højniveau-rammer, især hvad angår klienttjenester (webstedet).
I nogle dele af systemet kunne der have været anvendt databaser, der bedre egner sig til dette. For eksempel havde vi senere et præcedens for at skifte fra Redis til CosmosDB til opbevaring af indkøbskurven.
De teams og udviklere, der arbejdede inden for deres område, ønskede tydeligere selvstændighed for deres tjenester, både hvad angår udviklingen og implementeringen. Konflikter ved sammenlægning, problemer ved frigivelser. Hvis dette problem ikke var væsentligt for 5 udviklere, så blev det alvorligere ved 10, især med planlagt vækst. Derudover skulle der udvikles en mobilapplikation (den startede i 2017, og i 2018 var der ).
Forskellige dele af systemet krævede forskellige stabilitetsmål, men på grund af systemets stærke sammenhæng kunne vi ikke sikre det. En fejl i udviklingen af en ny funktion i adminpanelet kunne let påvirke ordrebehandlingen på hjemmesiden, da koden er fælles og genanvendelig, og databasen og dataene også er enhed.
Det ville sandsynligvis have været muligt, selv inden for en sådan monolitisk-modulær arkitektur, at undgå disse fejl og problemer: opdele ansvarsområder, lave refaktorering af både kode og database, tydeligt adskille lagene fra hinanden og overvåge kvaliteten hver dag. Men de valgte arkitektoniske beslutninger og fokus på hurtig udvidelse af systemets funktionalitet førte til problemer med stabiliteten.
Hvordan bloggen Sila Uma satte kasserne i restauranter
Hvis væksten af kæden af pizzeriaer (og belastningen) fortsatte i samme tempo, ville faldet efter noget tid være så stort, at systemet ikke ville kunne komme op igen. En god illustration af de problemer, vi begyndte at møde i 2015, er denne historie.
I bloggen “” var der en widget, der viste data om indtægterne for hele kæden i løbet af året. Widgeten gjorde brug af Dodos offentlige API, som leverer disse data. Nu er denne statistik tilgængelig på . Widgeten blev vist på hver side og foretog anmodninger med et interval på 20 sekunder. Anmodningen blev sendt til api.dodopizza.ru og spurgte om:
antallet af pizzerier i kæden;
den samlede indtjening for kæden siden begyndelsen af året;
indtjeningen for i dag.
Anmodningen om indtjeningsstatistikken gik direkte til databasen og begyndte at anmode om data om ordrer, aggregerede dataene i realtid og returnerede summen.
De samme bestillinger kom fra kasserne i restauranterne, der downloader en liste over accepterede ordrer for i dag, og nye ordrer blev tilføjet til den. Kasserne gjorde deres anmodninger hvert 5. sekund eller ved opdatering af siden.
Diagrammet så således ud:

En gang om efteråret skrev Fyodor Ovchinnikov en lang og populær artikel i sin blog. Der kom mange mennesker til bloggen, og de begyndte at læse alt grundigt. Mens hver person, der kom, læste artiklen, fungerede widgeten med indtægterne fejlfrit og anmodede API'en hvert 20. sekund.
API kaldte en gemt procedure for at beregne summen af alle ordrer siden starten af året for alle pizzeriaer i kæden. Aggeringen fandt sted i tabellen orders, som er meget populær. Alle kasser fra de åbne restauranter på det tidspunkt gik også ind i den. Kasserne stoppede med at svare, og ordrer blev ikke accepteret. De kunne heller ikke accepteres fra hjemmesiden, og de viste sig ikke på trackeren; skifteenhedens manager kunne ikke se dem i sit interface.
Dette er ikke den eneste historie. Inden efteråret 2015 var systembelastningen kritisk hver fredag. Flere gange deaktiverede vi det offentlige API, og en gang var vi endda nødt til at slukke for hjemmesiden, fordi ingenting hjalp. Der var endda en liste over tjenester med rækkefølgen for deaktivering ved alvorlige belastninger.
Fra dette tidspunkt begynder vores kamp mod belastningerne og for stabilisering af systemet (fra efteråret 2015 til efteråret 2018). Det var netop da, at "" fandt sted. Der opstod også nogle nedbrud i fremtiden, nogle var ret følsomme, men den samlede periode med ustabilitet kan nu betragtes som overstået.
Den hurtige vækst af virksomheden
Hvorfor kunne man ikke "gøre det godt fra starten af"? Det er nok at se på de næste grafer.

Der var også åbning i Rumænien i 2014-2015, og der blev forberedt en åbning i USA.
Kæden voksede meget hurtigt, nye lande blev åbnet, der kom nye pizzeriaformater, for eksempel åbnede der et pizzeria i en food court. Alt dette krævede betydelig opmærksomhed på udvidelsen af Dodo IS's funktioner. Uden alle disse funktioner, uden tracking i køkkenet, produktregnskab og tab i systemet, samt visning af orderudlevering i food courten, ville vi næppe tale om den "rette" arkitektur og den "rigtige" tilgang til udvikling.
Også hindringer for en rettidig revision af arkitekturen og generelt for opmærksomheden på tekniske problemer var krisen i 2014. Sådanne ting rammer hårdt på vækstmulighederne for teams, især for unge virksomheder som Dodo Pizza.
Hurtige løsninger, der hjalp
Problemerne krævede løsninger. Overordnet kan løsningerne opdeles i to grupper:
Hurtige, der slukker ilden og giver en lille buffer, hvilket giver os tid til ændringer.
Systemiske og dermed lange. Reengineering af en række moduler, opdeling af den monolitiske arkitektur i separate tjenester (de fleste af dem er slet ikke mikro, men snarere makrotjenester og der er skrevet om det) ).
Den tørre liste over hurtige ændringer er som følger:
Scale up masterdatabase
Selvfølgelig er det første, der gøres for at håndtere belastninger, at øge serverens kapacitet. Det blev gjort for masterdatabasen og for webserverne. Desværre er dette kun muligt op til et vist punkt, derefter bliver det for dyrt.
Siden 2014 er vi gået over til Azure, om dette skrev vi også på det tidspunkt i artiklen «». Men efter en række serverforøgninger til databasen stødte vi på prisgrænsen.
Replikater af databasen til læsning
Der blev lavet to replikater til databasen:
ReadReplica til forespørgsler om registre. Den bruges til at læse registre som byer, gader, pizzasteder, produkter (slowly changed domain), og i de grænseflader, hvor en lille forsinkelse er acceptabel. Disse replikater var to, og vi sikrede deres tilgængelighed lige så godt som masteren.
ReadReplica til rapporteringsforespørgsler. Denne databases tilgængelighed var lavere, men den indeholdte alle rapporter. Selvom de har tunge forespørgsler om massive databehandlinger, påvirker de ikke hoveddatabasen og driftsgrænsefladerne.
Caches i koden
Der var ikke nogen caches i koden (overhovedet). Dette førte til unødvendige forespørgsler i den belastede database. Caches blev først lavet både i hukommelsen og på en ekstern cache-tjeneste, som var Redis. Alle blev invalideret over tid, og indstillingerne blev angivet i koden.
Flere servere til backend
Backend-applikationen skulle også skaleres for at kunne håndtere de øgede belastninger. Det var nødvendigt at lave et cluster ud af én IIS-server. Vi flyttede fra hukommelsen til RedisCache, hvilket gjorde det muligt at have flere servere, der stod bag en simpel belastningsbalancer med round robin. Først blev den samme Redis brugt som til caches, senere blev den opdelt på flere.
Som et resultat blev arkitekturen mere kompleks…

…men en del af spændingen kunne lette.
Og derefter skulle belastede komponenter genopbygges, og det var vi i gang med. Om dette vil vi fortælle i næste del.
Kilde: habr.com
