Oppure ogni sfortunata azienda con un monolite è infelice a modo suo.
Lo sviluppo del sistema Dodo IS è iniziato contemporaneamente all'attività di Dodo Pizza, nel 2011. Alla base c'era l'idea di una completa e totale digitalizzazione dei processi aziendali, , il che sollevava già nel 2011 molte domande e scetticismo. Ma già da 9 anni seguiamo questa strada — con uno sviluppo interno, iniziato con un monolite.
Questo articolo è una «risposta» alle domande «Perché riscrivere l'architettura e apportare tali modifiche su larga scala e di lunga durata?» all'articolo precedente . Inizierò con come è iniziato lo sviluppo di Dodo IS, come appariva l'architettura originale, come sono stati aggiunti nuovi moduli e quali problemi hanno richiesto ampie modifiche.

La serie di articoli «Cosa è Dodo IS?» parlerà di:
Il monolite iniziale in Dodo IS (2011-2015). (Sei qui)
.
Il percorso della parte clienti: facciata sopra il database (2016-2017). (In corso...)
Storia dei veri microservizi. (2018-2019). (In corso...)
Completamento della divisione del monolite e stabilizzazione dell'architettura. (In corso...)
Architettura originale
Nel 2011, l'architettura di Dodo IS appariva così:

Il primo modulo nell'architettura è la ricezione degli ordini. Il processo aziendale era il seguente:
il cliente chiama la pizzeria;
il manager risponde al telefono;
prende l'ordine per telefono;
contemporaneamente lo inserisce nell'interfaccia di ricezione ordini: si tiene conto delle informazioni sul cliente, dei dettagli dell'ordine e dell'indirizzo di consegna.
L'interfaccia del sistema informatico appariva più o meno così…
Prima versione di ottobre 2011:
Le risorse per lo sviluppo del primo modulo di ricezione ordini erano limitate. Era necessario fare molto, in fretta e con un numero ridotto di persone. Un numero ridotto significa 2 sviluppatori, che hanno posto le basi dell'intero sistema futuro.
La loro prima soluzione ha determinato il destino del stack tecnologico:
Backend su ASP.NET MVC, linguaggio C#. Gli sviluppatori erano esperti di .NET, questo stack era loro familiare e gradito.
Frontend su Bootstrap e JQuery: interfacce utente con stili e script personalizzati.
Database MySQL: senza costi per le licenze, semplice da usare.
Server su Windows Server, perché .NET all'epoca poteva funzionare solo su Windows (non parleremo di Mono).
Fisicamente si manifestava tutto in un "dedicated server presso l'hosting".
Architettura dell'applicazione per la ricezione degli ordini
Allora già tutti parlavano di microservizi, mentre l'SOA era usato da circa 5 anni nei grandi progetti, per esempio, WCF è uscito nel 2006. Ma a quel tempo è stata scelta una soluzione affidabile e consolidata.
Ecco qui.

Asp.Net MVC è Razor, che restituisce su richiesta dal modulo o dal client una pagina HTML con rendering lato server. Sul client, già CSS e script JS visualizzano le informazioni e, se necessario, eseguono richieste AJAX tramite JQuery.
Le richieste al server arrivano nelle classi *Controller, dove il metodo elabora e genera la pagina HTML finale. I controller fanno richieste al livello di logica, chiamato *Services. Ognuno dei servizi risponde a un aspetto del business:
Ad esempio, il DepartmentStructureService forniva informazioni sulle pizzerie, sui dipartimenti. Un dipartimento è un gruppo di pizzerie gestito da un solo franchisee.
ReceivingOrdersService riceveva e calcolava il contenuto dell'ordine.
E SmsService inviava SMS, richiamando API per l'invio di SMS.
I servizi elaboravano i dati dal database, conservavano la logica di business. In ogni servizio c'era uno o più *Repository con nome corrispondente. All'interno c'erano già le richieste a procedure memorizzate nel database e il livello dei mapper. Nelle stored procedure c'era la logica di business, specialmente in quelle che restituiscono dati di reportistica. Non è stato utilizzato ORM, tutti si fidavano dell'SQL scritto a mano.
C'era anche un livello di modello di dominio e classi helper comuni, ad esempio, la classe Order, che memorizzava l'ordine. Lì, nel livello, c'era un helper per la conversione del testo di visualizzazione in base alla valuta selezionata.
Tutto questo può essere rappresentato con il seguente modello:

Percorso dell'ordine
Consideriamo il percorso iniziale semplificato per la creazione di un ordine.

Inizialmente il sito era statico. Aveva prezzi e in alto c'era il numero di telefono e la scritta "Vuoi una pizza - chiama il numero e ordina". Per l'ordine dovevamo implementare un flusso semplice:
Il cliente visita il sito statico con i prezzi, sceglie i prodotti e chiama il numero indicato sul sito.
Il cliente nomina i prodotti che desidera aggiungere all'ordine.
Nomina il proprio indirizzo e il proprio nome.
L'operatore accetta l'ordine.
L'ordine viene visualizzato nell'interfaccia degli ordini ricevuti.
Tutto inizia con la visualizzazione del menu. Un utente operatori autenticato può accettare solo un ordine alla volta. Pertanto, il carrello in bozza può essere memorizzato nella sua sessione (la sessione utente è memorizzata in memoria). Lì c'è l'oggetto Cart, in cui si trovano i prodotti e le informazioni sul cliente.
Il cliente nomina il prodotto, l'operatore clicca su + accanto al prodotto, e viene inviata una richiesta al server. Vengono estratte informazioni dal database e aggiunte informazioni sul prodotto al carrello.

Nota. Sì, qui è possibile non estrarre il prodotto dal database, ma trasmetterlo dal front-end. Ma per chiarezza ho mostrato il percorso dal database.
Successivamente, inseriamo l'indirizzo e il nome del cliente.

Quando si preme "Crea ordine":
Inviamo la richiesta a OrderController.SaveOrder().
Otteniamo il Cart dalla sessione, dove si trovano i prodotti nelle quantità necessarie.
Completiamo il Cart con le informazioni sul cliente e lo passiamo al metodo AddOrder della classe ReceivingOrderService, dove viene salvato nel database.
Nel database ci sono tabelle con gli ordini, i dettagli degli ordini, i clienti e sono tutte collegate.
L'interfaccia di visualizzazione degli ordini recupera gli ordini più recenti e li visualizza.
Nuovi moduli
La ricezione degli ordini era importante e necessaria. Non si può fare business nella vendita di pizze senza un sistema di ricezione degli ordini. Pertanto, il sistema ha cominciato a arricchirsi di funzionalità — circa dal 2012 al 2015. In quel periodo sono emersi molti diversi blocchi del sistema, che chiamerò moduli, in contrapposizione al concetto di servizio o prodotto.
Un modulo è un insieme di funzioni unite da un obiettivo aziendale comune. Inoltre, fisicamente si trovano in un'applicazione.
I moduli possono essere considerati blocchi del sistema. Ad esempio, c'è il modulo report, le interfacce di amministrazione, , l'autenticazione. Questi sono tutti diversi interfacce per l'utente, alcune hanno anche stili visivi differenti. Tuttavia, restano all'interno di una stessa applicazione, uno stesso processo operativo.
Tecnicamente, i moduli erano organizzati come Area (un'idea rimasta nel ). C'erano file separati per il front-end, i modelli, così come le loro classi controller. Alla fine, il sistema si è trasformato da così...

…a così:

Alcuni moduli sono stati implementati come siti separati (progetto eseguibile), a causa di funzionalità completamente separate e in parte a causa di uno sviluppo più focalizzato e separato. Questo è:
Sito — del sito dodopizza.ru.
Export: esportazione di report da Dodo IS per 1C.
Personale — area personale per i dipendenti. Sviluppata separatamente con un proprio punto di accesso e un design unico.
fs — progetto per l'hosting di contenuti statici. In seguito abbiamo abbandonato questo approccio, migrando tutto il contenuto statico su CDN Akamai.
Gli altri blocchi erano all'interno dell'applicazione BackOffice.

Chiarimento sui nomi:
Cassa — Cassa del ristorante.
ShiftManager — interfacce per il ruolo di 'Manager di turno': statistiche operative sulle vendite della pizzeria, possibilità di inserire prodotti in una lista nera, modificare ordini.
OfficeManager — interfacce per il ruolo di 'Gestore della pizzeria' e 'Franchising'. Qui sono raccolte le funzionalità per configurare la pizzeria, le sue promozioni, gestire i dipendenti, report.
PublicScreens — interfacce per televisori e tablet, appesi nelle pizzerie. I televisori mostrano il menu, informazioni pubblicitarie e lo stato dell'ordine durante la consegna.
Hanno utilizzato uno strato comune di servizi, un blocco comune di classi di dominio Dodo.Core, oltre a un database condiviso. A volte si potevano anche fare transizioni tra loro. Compresi i servizi comuni a cui accedevano siti separati, come dodopizza.ru o personal.dodopizza.ru.
Con la comparsa di nuovi moduli, si cercava di riutilizzare al massimo il codice di servizio già creato, procedure memorizzate e tabelle nel database.
Per una migliore comprensione della scala dei moduli realizzati nel sistema, ecco uno schema del 2012 con i piani di sviluppo:

Entro il 2015, tutto nello schema e anche di più era in produzione.
La ricezione dell'ordine è diventata un blocco separato del Centro Contatti, dove l'ordine viene preso dall'operatore.
Sono comparsi schermi pubblici con menu e informazioni, appesi nelle pizzerie.
In cucina c'è un modulo che riproduce automaticamente un messaggio vocale 'Nuova pizza' all'arrivo di un nuovo ordine e stampa anche una nota per il corriere. Questo semplifica notevolmente i processi in cucina, permettendo ai dipendenti di non distrarsi in molte operazioni semplici.
Il blocco consegne è diventato una Cassa di Consegna separata, dove l'ordine veniva fornito al corriere, che si era precedentemente registrato per il turno. Si considerava il suo tempo lavorativo per il calcolo della retribuzione.
Parallelamente, dal 2012 al 2015, sono emersi più di 10 sviluppatori, sono state aperte 35 pizzerie, il sistema è stato implementato in Romania e sono state preparate aperture negli Stati Uniti. Gli sviluppatori non si occupavano più di tutti i compiti, ma erano suddivisi in team, ognuno specializzato nella propria parte del sistema.
Problemi
Tra cui l'architettura (ma non solo).
Caos nel database
Un database — è comodo. In esso si può raggiungere la coerenza, grazie agli strumenti integrati nei database relazionali. Lavorarci è familiare e conveniente, specialmente se ci sono poche tabelle e pochi dati.
Ma dopo 4 anni di sviluppo, nel database si trovavano circa 600 tabelle, 1500 procedure memorizzate, molte delle quali contenevano logica. Purtroppo, le procedure memorizzate non offrono vantaggi significativi quando si lavora con MySQL. Non vengono memorizzate nella cache dal database e contenere logica in esse complica lo sviluppo e il debug. Anche il riutilizzo del codice era complicato.
Molte tabelle non avevano indici adeguati, mentre in altri casi ce n'erano troppi, rendendo difficile l'inserimento. Era necessario modificare circa 20 tabelle: la transazione per la creazione di un ordine poteva richiedere circa 3-5 secondi.
I dati nelle tabelle non erano sempre nella forma più adatta.In alcuni casi era necessaria la denormalizzazione. Parte dei dati ricevuti regolarmente era in una colonna come struttura XML, il che aumentava i tempi di esecuzione, allungava le query e complicava lo sviluppo.
Venivano eseguite richieste molto eterogenee sulle stesse tabelle.Le tabelle popolari, come quella menzionata orders o la tabella pizzeria, ne soffrivano particolarmente. Erano utilizzate per l'output delle interfacce operative in cucina e per l'analisi. Anche il sito web (), che in qualsiasi momento poteva ricevere un numero imprevisto di richieste.
I dati non erano aggregati e molti calcoli avvenivano al volo utilizzando le risorse del database. Questo creava calcoli superflui e un carico aggiuntivo.
Spesso il codice accedeva al database quando non avrebbe dovuto. In alcuni casi mancavano operazioni bulk, in altri sarebbe stato necessario suddividere una richiesta in diverse tramite codice per accelerare e aumentare l'affidabilità.
Coesione e confusione nel codice
I moduli che avrebbero dovuto rispondere al proprio campo di attività non lo facevano in modo adeguato.. Alcuni di essi avevano funzioni duplicate per i ruoli. Ad esempio, il marketer locale, responsabile delle attività di marketing della rete nella propria città, doveva utilizzare sia l'interfaccia "Admin" (per la creazione delle promozioni) sia l'interfaccia "Office Manager" (per visualizzare l'impatto delle promozioni sul business). Ovviamente, entrambi i moduli utilizzavano un unico servizio che gestiva le promozioni.
I servizi (classi all'interno di un grande progetto monolitico) potevano chiamarsi a vicenda per arricchire i propri dati.
Con le stesse classi modello, che memorizzano i dati, il lavoro nel codice avveniva in modo diverso. In alcuni casi c'erano dei costruttori tramite i quali si potevano indicare i campi obbligatori. In altri casi, ciò avveniva tramite proprietà pubbliche. Ovviamente, l'acquisizione e la trasformazione dei dati dal database era variegata.
La logica era presente o nei controller o nelle classi dei servizi.
Questi sembravano problemi insignificanti, ma rallentavano notevolmente lo sviluppo e riducevano la qualità, portando a instabilità e bug.
La complessità di uno sviluppo su larga scala
Sono sorte difficoltà anche nello sviluppo stesso. Era necessario creare diversi blocchi del sistema, e per di più in parallelo. Era sempre più difficile riunire le esigenze di ogni componente in un unico codice. Non era semplice trovare un accordo e soddisfare tutti i componenti contemporaneamente. A questo si aggiungevano vincoli nelle tecnologie, specialmente per quanto riguardava il database e il frontend. Era necessario abbandonare JQuery in favore di framework di alto livello, soprattutto per i servizi lato cliente (sito web).
In alcune parti del sistema avrebbero potuto essere utilizzati database più adatti per questa situazione. Ad esempio, successivamente abbiamo avuto un caso di transizione da Redis a CosmosDB per la memorizzazione del carrello degli ordini.
I team e gli sviluppatori, che si occupavano della propria area, volevano chiaramente maggiore autonomia per i propri servizi, sia per quanto riguarda lo sviluppo che il rilascio. I conflitti durante il merge, i problemi nei rilasci. Se per 5 sviluppatori questo problema era insignificante, per 10, e soprattutto in previsione di una crescita pianificata, tutto sarebbe diventato più grave. E davanti a noi c'era lo sviluppo dell'app mobile (che è iniziato nel 2017, mentre nel 2018 ci fu ).
Diverse parti del sistema richiedevano diversi indicatori di stabilità., ma a causa della forte interconnessione del sistema, non siamo riusciti a garantire questo. Un errore nello sviluppo di una nuova funzionalità nell'admin poteva facilmente influire sulla ricezione degli ordini sul sito, poiché il codice è comune e riutilizzabile, così come il database e i dati.
Probabilmente, si sarebbe potuto evitare questi errori e problemi anche con un'architettura monolitica e modulare: separare le responsabilità, effettuare il refactoring sia del codice che del database, mantenere i livelli separati l'uno dall'altro, controllare la qualità ogni giorno. Ma le decisioni architettoniche scelte e il focus su un rapido ampliamento delle funzionalità del sistema hanno portato a problemi di stabilità.
Come il blog Sila della mente ha influito sulle casse dei ristoranti
Se la crescita della rete di pizzerie (e il carico) fosse continuata allo stesso ritmo, dopo un po' le cadute sarebbero state tali che il sistema non sarebbe riuscito a riprendersi. Questa storia illustra bene i problemi che abbiamo iniziato a fronteggiare nel 2015.
Nel blog “” c'era un widget che mostrava i dati sui ricavi annuali dell'intera rete. Il widget effettuava richieste al pubblico API Dodo, che fornisce questi dati. Ora queste statistiche sono disponibili su . Il widget veniva mostrato su ogni pagina e effettuava richieste a intervalli di 20 secondi. La richiesta veniva inviata a api.dodopizza.ru e richiedeva:
il numero di pizzerie nella rete;
il fatturato totale della rete dall'inizio dell'anno;
il fatturato odierno.
La richiesta delle statistiche sul fatturato veniva inviata direttamente al database, iniziando a richiedere i dati sugli ordini, aggregando i dati in tempo reale e restituendo la somma.
Nella stessa tabella degli ordini entravano le casse dei ristoranti, caricando l'elenco degli ordini ricevuti oggi, e venivano aggiunti anche nuovi ordini. Le casse effettuavano le loro richieste ogni 5 secondi o al caricamento della pagina.
Lo schema era il seguente:

Una volta in autunno, Fedor Ovchinnikov scrisse un lungo e popolare articolo sul suo blog. Al blog arrivarono molte persone che iniziarono a leggerlo attentamente. Mentre ognuno dei visitatori leggeva l'articolo, il widget dei ricavi funzionava regolarmente, effettuando richieste all'API ogni 20 secondi.
L'API ha chiamato una stored procedure per calcolare la somma di tutti gli ordini dall'inizio dell'anno in tutte le pizzerie della rete. L'aggregazione avveniva sulla tabella ordini, molto popolare. In essa confluivano tutte le casse di tutti i ristoranti aperti al momento. Le casse hanno smesso di rispondere, gli ordini non venivano accettati. Inoltre, non venivano accettati nemmeno dal sito, non comparivano nel tracker, il manager del turno non poteva vederli nella propria interfaccia.
Questa non è l'unica storia. Fino all'autunno del 2015, il carico del sistema era critico ogni venerdì. Diverse volte abbiamo disattivato l'API pubblica e una volta, abbiamo persino dovuto disattivare il sito, perché nulla sembrava funzionare. Esisteva anche un elenco di servizi con l'ordine di disattivazione in caso di carichi elevati.
Da questo momento inizia la nostra lotta con i carichi e per la stabilizzazione del sistema (dall'autunno 2015 all'autunno 2018). È proprio allora che è avvenuto il "". Anche in seguito si sono verificati occasionalmente guasti, alcuni sono stati molto significativi, ma ora possiamo considerare questo periodo di instabilità come superato.
Crescita rapida dell'azienda
Perché non si poteva "fare subito bene"? Basta guardare i grafici seguenti.

Nel 2014-2015 ci sono state aperture in Romania e si stava preparando l'apertura negli Stati Uniti.
La rete cresceva molto rapidamente, si aprivano nuovi paesi, comparivano nuovi formati di pizzerie, ad esempio, una pizzeria è stata aperta in un food court. Tutto ciò richiedeva una significativa attenzione all'espansione delle funzioni di Dodo IS. Senza tutte queste funzioni, senza tracking in cucina, gestione dei prodotti e delle perdite nel sistema, visualizzazione dell'assegnazione degli ordini nella sala del food court, è poco probabile che ora staremmo discutendo della "corretta" architettura e del "giusto" approccio allo sviluppo.
Un altro ostacolo per una revisione tempestiva dell'architettura e in generale per l'attenzione ai problemi tecnici è stata la crisi del 2014. Queste cose colpiscono duramente le possibilità di crescita dei team, specialmente per un'azienda giovane come Dodo Pizza.
Soluzioni rapide che hanno aiutato
I problemi richiedevano una soluzione. In linea di principio, le soluzioni possono essere suddivise in 2 gruppi:
Rapide, che spengono il fuoco e offrono un piccolo margine di sicurezza e ci guadagnano tempo per le modifiche.
Sistemiche e, quindi, lunghe. Reingegnerizzazione di diversi moduli, separazione dell'architettura monolitica in servizi separati (la maggior parte di essi è piuttosto macrocservizi e c'è di questo) ).
L'elenco delle modifiche rapide è il seguente:
Scalare il master del database
Certo, la prima cosa da fare per affrontare i carichi è aumentare la potenza del server. Questo è stato fatto per il master del database e per i server web. Purtroppo, ciò è possibile solo fino a un certo punto, oltre diventa troppo costoso.
Dal 2014 siamo passati a Azure, su questo tema ne abbiamo scritto anche all'epoca nell'articolo «». Ma dopo una serie di aumenti del server per il database abbiamo raggiunto un limite di costo.
Repliche del database per lettura
Abbiamo creato due repliche per il database:
ReadReplica per richieste sui dizionari. È utilizzata per leggere dizionari, come, città, strade, pizzerie, prodotti (slowly changed domain), e in quegli interface dove è accettabile un piccolo ritardo. Queste repliche erano 2, assicuravamo la loro disponibilità proprio come quella del master.
ReadReplica per richieste sui report. Questa replica aveva una disponibilità inferiore, ma da essa si estraevano tutti i report. Anche se avevano richieste pesanti su enormi rielaborazioni di dati, non influivano sul database principale e sulle interfacce operative.
Cache nel codice
Non c'erano cache nel codice da nessuna parte (in generale). Questo portava a ulteriori richieste, non sempre necessarie, nel database sotto carico. Le cache erano inizialmente sia in memoria che su un servizio di cache esterno, che era Redis. Tutto veniva invalidato nel tempo, le impostazioni erano specificate nel codice.
Diversi server per il backend
Anche il backend dell'applicazione doveva essere scalato per gestire i carichi aumentati. Era necessario trasformare un server IIS in un cluster. Abbiamo trasferito da memoria a RedisCache, il che ha permesso di avere più server dietro un semplice bilanciatore di carico con round robin. Inizialmente veniva utilizzato lo stesso Redis delle cache, poi è stato distribuito su più server.
Alla fine, l'architettura si è complicata...

…ma parte della tensione è stata alleviata.
E poi era necessario rifattorizzare i componenti sotto carico, per cui ci siamo messi al lavoro. Ne parleremo nella prossima parte.
Fonte: habr.com
