Sau fiecare companie nefericită cu un monolit, este nefericită în felul său.
Dezvoltarea sistemului Dodo IS a început imediat ce a început afacerea Dodo Pizza — în 2011. La baza sa a stat ideea unei digitalizări complete și totale a proceselor de afaceri, ceea ce , ceea ce provoca deja foarte multe întrebări și scepticism în 2011. Dar iată că am parcurs deja 9 ani pe acest drum — cu o dezvoltare proprie, care a început cu un monolit.
Acest articol este „răspunsul” la întrebările „De ce să refacem arhitectura și să facem astfel de schimbări masive și de durată?” din articolul anterior . Voi începe cu modul în care a început dezvoltarea Dodo IS, cum arăta arhitectura inițială, cum apăreau noi module și din cauza căror probleme a fost necesar să facem schimbări de amploare.

Seria de articole „Ce este Dodo IS?” va povesti despre:
Monolitul timpuriu în Dodo IS (2011-2015). (Ești aici)
.
Drumul părții clientului: fațada deasupra bazei (2016-2017). (În curs de realizare…)
Istoria adevăratelor microservicii. (2018-2019). (În curs de realizare…)
Finalizarea tăierii monolitului și stabilizarea arhitecturii. (În curs de realizare…)
Arhitectura inițială
În 2011, arhitectura Dodo IS arăta astfel:

Primul modul din arhitectură — primirea comenzii. Procesul de afaceri era următorul:
clientul suna la pizzerie;
managerul răspundea la telefon;
accepta comanda prin telefon;
în paralel, îl introducea în interfața de primire a comenzii: se lua în considerare informația despre client, detaliile comenzii, adresa de livrare.
Interfața sistemului informațional arăta cam așa…
Prima versiune din octombrie 2011:
Resursele pentru dezvoltarea primului modul de primire a comenzii erau limitate. Trebuia să facem multe, rapid și cu un număr mic de oameni. Numărul mic de oameni — erau 2 dezvoltatori, care au pus baza întregului sistem viitor.
Prima lor soluție a definit soarta ulterioară a tehnologiei:
Backend pe ASP.NET MVC, limbaj C#. Dezvoltatorii erau expertizați în .NET, acest stack le era familiar și plăcut.
Frontend pe Bootstrap și JQuery: interfețe utilizator pe stiluri și scripturi personalizate.
Baza de date MySQL: fără costuri de licență, ușor de utilizat.
Servere pe Windows Server, deoarece .NET putea fi folosit doar pe Windows (nu vom discuta despre Mono).
Fizic, asta se manifesta în „dedicatia la provider”.
Arhitectura aplicației pentru preluarea comenzilor
Atunci toată lumea vorbea despre microservicii, iar SOA era folosit de aproximativ 5 ani în proiecte mari, de exemplu, WCF a fost lansat în 2006. Dar atunci s-a ales o soluție sigură și testată.
Iată-l.

Asp.Net MVC este Razor, care generează la cerere o pagină HTML dintr-un formular sau de la client, cu randare pe server. Pe client, CSS și scripturile JS afișează informațiile și, dacă este necesar, efectuează solicitări AJAX prin intermediul JQuery.
Solicitările pe server ajung în clasele *Controller, unde se desfășoară procesarea și generarea paginii HTML finale. Controlerele fac solicitări la un strat de logică, numit *Services. Fiecare dintre servicii răspundea unui anumit aspect al afacerii:
De exemplu, DepartmentStructureService furniza informații despre pizzerii, despre departamente. Departamentul este un grup de pizzerii sub conducerea unui singur francizat.
ReceivingOrdersService accepta și calcula compunerea comenzii.
SmsService trimitea SMS-uri, apelând API-urile pentru trimiterea de SMS-uri.
Serviciile procesau datele din bază, păstrând logica de afaceri. Fiecare serviciu avea unul sau mai multe *Repository cu un nume corespunzător. În acestea se aflau deja solicitările către procedurile stocate din bază și stratul de mapperi. În procedurile stocate exista logica de afaceri, mai ales în cele care oferau date de raportare. ORM-ul nu era folosit, toată lumea se baza pe SQL scris manual.
Mai exista un strat de model de domeniu și clase generale de ajutor, de exemplu, clasa Order, care stoca comanda. Acolo, în strat, se afla un ajutor pentru conversia textului afișat în funcție de moneda aleasă.
Toate acestea pot fi reprezentate printr-un model:

Calea comenzii
Să analizăm călătoria simplificată inițială a creării unei astfel de comenzi.

Inițial, site-ul era static. Pe el erau prețuri, iar sus - un număr de telefon și textul „Vrei pizza - sună numărul și comandă”. Pentru a plasa o comandă, trebuie să implementăm un flux simplu:
Clienții accesează site-ul static cu prețuri, aleg produsele și sună la numărul afișat pe site.
Clienții menționează produsele pe care doresc să le adauge în comandă.
Își menționează adresa și numele.
Operatorul preia comanda.
Comanda este afișată în interfața comenzilor primite.
Totul începe cu afișarea meniului. Utilizatorul-operator autentificat poate accepta un singur comenzi la un moment dat. Prin urmare, coșul draft poate fi păstrat în sesiunea lui (sesiunea utilizatorului este stocată în memorie). Acolo se află obiectul Cart, care conține produsele și informațiile despre client.
Clientul menționează produsul, operatorul apasă pe + lângă produs, iar o cerere este trimisă serverului. Informațiile despre produs sunt extrase din baza de date și adăugate în coș.

Notă. Da, aici nu este necesar să extragem produsul din baza de date, ci putem transmite informațiile de la frontend. Dar pentru claritate, am arătat drumul din baza de date.
Apoi introducem adresa și numele clientului.

Când se apasă „Creare comandă”:
Cererea este trimisă în OrderController.SaveOrder().
Obținem Cart din sesiune, acolo se află produsele în cantitățile dorite.
Complectăm Cart cu informațiile despre client și le transmitem metodei AddOrder din clasa ReceivingOrderService, unde sunt salvate în baza de date.
Există tabele în baza de date pentru comandă, conținutul comenzii, client, și toate acestea sunt interconectate.
Interfața de afișare a comenzii preia ultimele comenzi și le afișează.
Module noi
Primirea comenzilor a fost esențială și necesară. Nu poți face afaceri de vânzare a pizzei fără un sistem de primire a comenzilor. Prin urmare, sistemul a început să devină mai complex — aproximativ între 2012 și 2015. În acest interval, au apărut multe module de sistem diferite, pe care le voi numi module, spre deosebire de conceptul de serviciu sau produs.
Un modul este un set de funcții unite de un anumit scop de afaceri. Acestea se află fizic într-o singură aplicație.
Modulele pot fi considerate blocuri ale sistemului. De exemplu, este un modul de rapoarte, interfețe de administrare, , autentificare. Toate acestea sunt diferite interfețe pentru utilizator, unele având chiar stiluri vizuale diferite. Totuși, toate sunt în cadrul unei singure aplicații, a unui singur proces funcțional.
Tehnic, modulele erau realizate ca Area (aceasta idee a rămas chiar și în ). Existau fișiere separate pentru frontend, modele, precum și clasele controller-elor. În final, sistemul s-a transformat dintr-o...

...într-o astfel de:

Unele module sunt implementate ca site-uri separate (proiect executabil), din cauza funcționalității complet diferite și parțial din cauza unei dezvoltări mai separate, mai concentrate. Acestea sunt:
Site — a site-ului dodopizza.ro.
Export: exportarea rapoartelor din Dodo IS pentru 1C.
Personal — portalul personal al angajatului. A fost dezvoltat separat, având un punct de acces și un design distinct.
fs — proiect pentru găzduirea conținutului static. Ulterior, am renunțat la el, mutând tot conținutul static pe CDN Akamai.
Celelalte module se aflau în aplicația BackOffice.

Clarificare a denumirilor:
Cashier — Casa restaurantului.
ShiftManager — interfețe pentru rolul „Manager de tură”: statistici operative despre vânzările pizzeriei, posibilitatea de a bloca produse, de a modifica comenzile.
OfficeManager — interfețe pentru rolul „Manager de pizzerie” și „Franchisor”. Aici sunt concentrate funcțiile de configurare a pizzeriei, promoțiile bonus, gestionarea angajaților, rapoartele.
PublicScreens — interfețe pentru televizoarele și tabletele montate în pizzerii. Pe televizoare sunt afișate meniul, informațiile publicitare, starea comenzii la livrare.
Au folosit un strat comun de servicii, un bloc comun de clase de domeniu Dodo.Core, dar și o bază de date comună. Uneori puteau face legături între site-uri, inclusiv site-uri separate, cum ar fi dodopizza.ro sau personal.dodopizza.ro.
Atunci când apăreau module noi, se căuta să se reutilizeze cât mai mult codul deja creat pentru servicii, proceduri stocate și tabeluri din bază.
Pentru a înțelege mai bine amploarea modulelor dezvoltate în sistem, iată o schemă din 2012 cu planurile de dezvoltare:

Până în 2015, tot ce era în schema respectivă și chiar mai mult a fost pus în producție.
Preluarea comenzilor a evoluat în un bloc separat al Centrului de Contact, unde comenzile sunt preluate de operator.
Au apărut ecrane publice cu meniuri și informații, montate în pizzerii.
În bucătărie există un modul care reproduce automat mesajul vocal „Pizza nouă” la primirea unei comenzi noi și imprimă o factură pentru curier. Acest lucru simplifică semnificativ procesele din bucătărie, permițând angajaților să nu se abată de la un număr mare de operațiuni simple.
Blocul de livrare a devenit o Casă de Livrare distinctă, unde comanda era predată curierului, care se înregistrase anterior pentru tură. Timpul său de lucru era contabilizat pentru plata salariului.
Între 2012 și 2015 au apărut mai mult de 10 dezvoltatori, s-au deschis 35 de pizzerii, s-a extins sistemul în România și s-au pregătit deschiderea unor locații în SUA. Dezvoltatorii nu mai gestionau toate sarcinile, ci erau împărțiți în echipe, fiecare specializându-se pe partea sa din sistem.
Probleme
Printre altele din cauza arhitecturii (dar nu numai).
Haos în baza de date
O singură bază de date este convenabilă. Acolo se poate obține consistență, datorită mijloacelor încorporate în bazele de date relaționale. Lucrul cu ea este familiar și convenabil, mai ales dacă există puține tabele și puține date.
Însă, în cei 4 ani de dezvoltare, în bază s-au acumulat aproximativ 600 de tabele, 1500 de proceduri stocate, în multe dintre care exista și logică. Din păcate, procedurile stocate nu aduc un avantaj semnificativ în lucrul cu MySQL. Ele nu sunt cache-uite de bază, iar includerea logici în acestea complică dezvoltarea și depanarea. Reutilizarea codului este, de asemenea, îngreunată.
Pe multe tabele nu existau indice adecvați, iar în alte părți, dimpotrivă, erau foarte multe indice, ceea ce îngreuna inserția. A fost necesar să se modifice aproximativ 20 de tabele - o tranzacție de creare a unei comenzi putea dura între 3 și 5 secunde.
Datele din tabele nu erau întotdeauna în cea mai potrivită formă. În unele locuri, era necesară denormalizarea. O parte din datele primite regulat erau într-o coloană sub formă de structură XML, ceea ce creștea timpul de execuție, lungind interogările și complicând dezvoltarea.
La aceleași tabele se aduceau foarte diverse interogări. Tabelele populare, cum ar fi tabelele menționate anterior, orders sau tabela pizzeria. Acestea erau utilizate pentru a genera interfețe operaționale în bucătărie, analize. De asemenea, site-ul () primea în orice moment un număr mare de interogări neașteptate.
Datele nu erau agregate și multe calcule se realizau pe loc prin intermediul bazei. Aceasta crea calcule suplimentare și o încărcare adițională.
Adesea, codul interoga baza atunci când nu ar fi trebuit. În unele locuri, lipseau operațiile bulk, iar în altele, ar fi fost nevoie să se disocieze o interogare în mai multe, prin cod, pentru a accelera și a crește fiabilitatea.
Conexiunea și complexitatea în cod
Modulele care ar fi trebuit să răspundă pentru propria lor parte de afaceri nu făceau acest lucru corect.. Unele dintre acestea aveau duplicări de funcții pentru roluri. De exemplu, pentru un marketer local, care este responsabil pentru activitatea de marketing a rețelei în orașul său, era necesar să utilizeze atât interfața „Admin” (pentru a crea promoții), cât și interfața „Manager de Birou” (pentru a vizualiza impactul promoțiilor asupra afacerii). Desigur, în interior, ambele module utilizau același serviciu care lucra cu promoțiile bonus.
Serviciile (clase în cadrul unui mare proiect monolitic) puteau să se invoce reciproc pentru a-și îmbogăți datele.
În sine, clasele-model care stochează datele, lucrul în cod era diferit. Acolo unde erau constructori, prin care se puteau specifica câmpurile obligatorii. Acolo unde se făcea acest lucru prin proprietăți publice. Desigur, obținerea și transformarea datelor din bază era variată.
Logica era fie în controllere, fie în clasele de servicii.
Acestea păreau probleme minore, dar încetineau semnificativ dezvoltarea și diminuau calitatea, ceea ce ducea la instabilitate și erori.
Complexitatea dezvoltării mari
Dificultățile au apărut și în dezvoltarea în sine. Era nevoie să se creeze diverse module ale sistemului, și asta pe paralel. Integrarea nevoilor fiecărui component în același cod devenea din ce în ce mai dificilă. Nu era ușor să se ajungă la un consens și să se mulțumească toate componentele simultan. La acestea se adăugau limitările tehnologice, în special în ceea ce privește baza de date și frontend-ul. Trebuia să se renunțe la JQuery în favoarea unor cadre de înalt nivel, în special în partea serviciilor client (site-ul).
În unele părți ale sistemului ar fi putut fi utilizate baze mai potrivite pentru acesta. De exemplu, ulterior am avut un precedent de trecere de la Redis la CosmosDB pentru stocarea coșului de comandă.
Echipele și dezvoltatorii, care se ocupau de domeniul lor, voiau în mod evident mai multă autonomie pentru serviciile lor, atât în ceea ce privește dezvoltarea, cât și în ceea ce privește lansarea. Conflictele la îmbinarea codului, problemele la lansări. Dacă pentru 5 dezvoltatori această problemă nu era semnificativă, pentru 10, și cu atât mai mult la creșterea planificată, totul devenea mai serios. Și înainte trebuia să aibă loc dezvoltarea aplicației mobile (care a început în 2017, iar în 2018 a fost ).
Diferitele părți ale sistemului necesitau diferite indicatori de stabilitate, dar din cauza legăturii strânse a sistemului, nu am putut asigura acest lucru. O eroare în dezvoltarea unei noi funcții în panoul de administrator ar fi putut afecta primirea comenzilor pe site, deoarece codul este comun și reutilizabil, iar baza de date și datele sunt, de asemenea, unitare.
Probabil că era posibil să evităm aceste erori și probleme în cadrul unei astfel de arhitecturi monolitice-modulare: să facem o separare a responsabilităților, să realizăm refactoring atât al codului, cât și al bazei de date, să separăm clar straturile între ele, să monitorizăm calitatea în fiecare zi. Dar soluțiile arhitecturale alese și accentul pe extinderea rapidă a funcționalității sistemului au dus la probleme de stabilitate.
Cum blogul Puterea Minții a afectat casieriile din restaurante
Dacă creșterea rețelei de pizzerii (și încărcarea) ar fi continuat în același ritm, atunci, în curând, căderile ar fi fost atât de mari încât sistemul nu s-ar mai fi ridicat. O ilustrare bună a problemelor cu care am început să ne confruntăm în 2015 este această poveste.
În blogul „” exista un widget care arăta datele legate de venituri pe an pentru întreaga rețea. Widgetul se conecta la un API public Dodo care furniza aceste date. Acum această statistică este disponibilă pe . Widgetul era afișat pe fiecare pagină și făcea cereri la fiecare 20 de secunde. Cererea era trimisă la api.dodopizza.ru și solicita:
numărul de pizzerii din rețea;
venitul total al rețelei de la începutul anului;
venitul obținut astăzi.
Cererea pentru statisticile legate de venituri mergea direct în baza de date și începea să ceară datele legate de comenzi, agregând datele în timp real și livrând suma.
Aceeași tabelă a comenzilor era folosită și de casierii din restaurante, care descărcau lista comenzilor acceptate astăzi, iar noile comenzi erau adăugate acolo. Casierii făceau cererile lor la fiecare 5 secunde sau la actualizarea paginii.
Schema arăta astfel:

Odată, toamna, Feodor Ovcihnkov a scris pe blogul său un articol lung și popular. Pe blog au venit foarte mulți oameni și au început să citească cu atenție totul. În timp ce fiecare vizitator citea articolul, widgetul legat de venituri funcționa corespunzător și solicita API la fiecare 20 de secunde.
API a apelat o procedură stocată pentru calcularea sumei tuturor comenzilor din acest an pentru toate pizzeriile din rețea. Agregarea a fost realizată pe tabela orders, care este foarte populară. Toate casele de marcat ale restaurantelor deschise în acel moment erau conectate la aceasta. Casele de marcat au încetat să mai răspundă, comenzile nu mai erau acceptate. De asemenea, comenzile nu erau acceptate de pe site, nu apăreau pe tracker, iar managerul de schimb nu putea să le vadă în interfața sa.
Aceasta nu este singura poveste. Până în toamna anului 2015, încărcătura pe sistem era critică în fiecare vineri. De câteva ori am oprit API-ul public, iar într-o ocazie, a trebuit chiar să deconectăm site-ul, pentru că nimic nu mai ajuta. A existat chiar o listă de servicii cu ordinea de deconectare în cazul unor sarcini severe.
De atunci a început lupta noastră cu încărcările și stabilizarea sistemului (din toamna anului 2015 până în toamna anului 2018). Atunci a avut loc "". De asemenea, au mai avut loc unele defecțiuni, unele destul de sensibile, dar perioada generală de instabilitate poate fi acum considerată depășită.
Creșterea rapidă a afacerii
De ce nu s-a putut „face bine totul din prima”? Este suficient să ne uităm la graficele următoare.

De asemenea, în 2014-2015 a avut loc deschiderea în România și se pregătea deschiderea în SUA.
Rețeaua a crescut foarte repede, au fost deschise noi țări, au apărut noi formate de pizzerii, de exemplu, o pizzerie la food court. Toate acestea necesitau o atenție semnificativă pentru extinderea funcțiilor Dodo IS. Fără toate aceste funcții, fără urmărirea în bucătărie, contabilizarea produselor și pierderilor în sistem, și fără afișarea comenzii în sala food court, cu greu ne-am fi gândit acum la arhitectura „corectă” și la abordarea „potrivită” în dezvoltare.
De asemenea, obstacolele pentru revizuirea la timp a arhitecturii și, în general, atenția asupra problemelor tehnice au fost criza din 2014. Astfel de lucruri afectează sever capacitățile de creștere ale echipelor, mai ales pentru o afacere tânără, așa cum era Dodo Pizza.
Soluții rapide care au ajutat
Problemele necesită soluții. În principiu, soluțiile pot fi împărțite în două grupuri:
Rapid, care sting incendiul și oferă o mică marjă de siguranță, câștigând astfel timp pentru modificări.
Sistemice și, prin urmare, tardive. Reingineria mai multor module, separarea arhitecturii monolitice în servicii separate (majoritatea dintre ele fiind mai degrabă macroservicii decât microservicii, și despre asta există ).
Lista scurtă a modificărilor rapide este următoarea:
Scale up master database
Desigur, primul lucru pe care îl facem pentru a face față sarcinilor este să creștem puterea serverului. Acest lucru a fost realizat pentru master database și pentru serverele web. Din păcate, acest lucru este posibil doar până la un anumit punct, după care devine prea costisitor.
Din 2014 am migrat în Azure, despre acest subiect am scris și atunci în articolul „”. Dar după o serie de creșteri ale serverelor pentru bază ne-am lovit de costuri.
Replica bazei pentru citire
Am creat două replici pentru bază:
ReadReplica pentru cererile de referință. Este utilizată pentru citirea referințelor, cum ar fi orașele, străzile, pizzeriile, produsele (domeniu care se schimbă lent), și în acele interfețe unde o mică întârziere este acceptabilă. Aceste replici erau 2, ne-am asigurat de disponibilitatea lor la fel ca și a masterului.
ReadReplica pentru cererile de rapoarte. Această bază avea o disponibilitate mai mică, dar toate rapoartele mergeau prin ea. Chiar dacă aveau cereri grele pentru calcule mari de date, nu influențau baza principală și interfețele operaționale.
Cache-uri în cod
Nu existau cache-uri în cod (deloc). Acest lucru ducea la cereri suplimentare, care nu erau întotdeauna necesare, în baza supraaglomerată. Cache-urile erau inițial atât în memorie, cât și pe un serviciu de cache extern, acesta fiind Redis. Totul era invalidat în funcție de timp, setările erau specificate în cod.
Mai multe servere pentru backend
Backend-ul aplicației trebuia și el scalat pentru a face față încărcărilor crescute. Era necesar să transformăm un server iis într-un cluster. Am mutat din memorie în RedisCache, ceea ce a permis implementarea mai multor servere, stând în spatele unui simplu balancer de încărcare cu round robin. Inițial s-a folosit același Redis ca pentru cache-uri, apoi s-au descompus în mai multe.
În cele din urmă, arhitectura s-a complicat...

…dar o parte din tensiune a fost eliminată.
Și apoi trebuia să refacem componentele suprasolicitate, la asta ne-am apucat. Despre asta vom vorbi în partea următoare.
Sursa: habr.com
