Habr sta cambiando il mondo. Da oltre un anno gestiamo il nostro blog. Circa sei mesi fa abbiamo ricevuto un feedback piuttosto logico dai lettori di Habr: «Dodo, voi dite sempre che avete il vostro sistema. Ma che tipo di sistema è? E a cosa serve per una catena di pizzerie?».
Ci siamo seduti, abbiamo riflettuto e capito che avevate ragione. Cerchiamo di spiegare tutto in modo semplice, ma ciò risulta fragmentario e non c'è una descrizione completa del sistema. Così è iniziato un lungo percorso di raccolta di informazioni, ricerca di autori e scrittura di una serie di articoli su Dodo IS. Andiamo!
Ringraziamenti: grazie per condividere il vostro feedback con noi. Grazie a esso, siamo finalmente riusciti a descrivere il sistema, abbiamo creato un technoradar e presto pubblicheremo una descrizione completa dei nostri processi. Senza di voi saremmo stati fermi per altri 5 anni.

La serie di articoli «Cosa è Dodo IS?» parlerà di:
- Il monolite iniziale in Dodo IS (2011-2015). (In corso...)
- Il percorso del back office: database separati e bus. (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...)
Se sei interessato a scoprire qualcos'altro, scrivi nei commenti.
Opinione sull'esposizione cronologica da parte dell'autore
Regolarmente tengo incontri per i nuovi dipendenti sul tema «Architettura del sistema». Da noi si chiama «Introduzione all'Architettura di Dodo IS» ed è parte del processo di onboarding per i nuovi sviluppatori. Raccontando in un modo o nell'altro della nostra architettura e delle sue peculiarità, ho sviluppato un approccio storico per la descrizione.
Tradizionalmente, guardiamo al sistema come un insieme di componenti (tecnici o di livello più elevato), moduli aziendali che interagiscono tra loro per raggiungere un determinato obiettivo. E se per la progettazione questo punto di vista è giustificato, per la descrizione e comprensione non è del tutto adeguato. Le ragioni sono diverse:
- La realtà è diversa da quella scritta su carta. Non tutto ciò che era previsto si realizza. E a noi interessa come sia effettivamente il tutto e come funzioni.
- Esposizione sequenziale delle informazioni. In sostanza, è possibile procedere cronologicamente dall'inizio fino allo stato attuale.
- Dal semplice al complesso. Non è universale, ma nel nostro caso funziona proprio così. Da approcci più semplici, l'architettura è passata a modelli più complessi. Spesso attraverso la complicazione si risolvevano problemi di velocità di implementazione e stabilità, così come decine di altre caratteristiche della lista dei requisiti non funzionali ( ben raccontato il contrasto tra complessità e altri requisiti).
Nel 2011, l'architettura di Dodo IS appariva così:

Entro il 2020 si è leggermente complicata e ha assunto questa forma:

Come è avvenuta questa evoluzione? A cosa servono le diverse parti del sistema? Quali soluzioni architettoniche sono state adottate e perché? Approfondiremo in questa serie di articoli.
I primi problemi del 2016: perché i servizi devono uscire dal monolite
I primi articoli del ciclo riguarderanno i servizi che si sono staccati per primi dal monolite. Per introdurvi nel contesto, parlerò dei problemi che avevamo nel sistema all'inizio del 2016 e di cosa abbiamo dovuto fare per separare i servizi.
Un'unica base MySql, in cui tutti le applicazioni esistenti al momento in Dodo IS scrivevano le loro registrazioni. Le conseguenze furono le seguenti:
- Carico elevato (85% delle richieste erano per letture).
- Il database si espandeva. Di conseguenza, il suo costo e la sua manutenzione diventavano un problema.
- Unico punto di fallimento. Se un'applicazione che scriveva nel database iniziava a farlo improvvisamente in modo più intensivo, le altre applicazioni ne sentivano gli effetti.
- Inefficienza nella memorizzazione e nelle richieste. Spesso i dati erano conservati in una certa struttura, che era comoda per alcuni scenari, ma non si adattava ad altri. Gli indici acceleravano alcune operazioni, ma potevano rallentare altre.
- Alcuni problemi sono stati alleviati con cache e read-replicas del database fatti in fretta (ne parlerò in un articolo separato), ma questi hanno solo permesso di guadagnare tempo e non risolvevano il problema in modo fondamentale.
Il problema era la presenza stessa del monolite. Le conseguenze furono le seguenti:
- Rilasci unici e rari.
- Difficoltà nella collaborazione di un gran numero di persone.
- Impossibilità di introdurre nuove tecnologie, nuovi framework e librerie.
Problemi con il database e il monolite sono stati descritti più volte, ad esempio, nel contesto dei crash all'inizio del 2018 (, e ), quindi non mi soffermerò ulteriormente. Dirò solo che volevamo offrire una maggiore flessibilità nello sviluppo dei servizi. Questo riguardava soprattutto quelli che erano i più carichi e fondamentali in tutto il sistema: Auth e Tracker.
Il percorso del back office: database separati e bus
Navigazione nel capitolo
Schema del monolite del 2016
Di fronte a voi ci sono i principali blocchi del monolite Dodo IS del 2016, e qui sotto la spiegazione delle loro principali funzioni.

Cassa Consegna. Gestione dei corrieri, rilascio degli ordini ai corrieri.
Centro Contatto. Accettazione degli ordini tramite operatore.
Sito. I nostri siti (dodopizza.ru, dodopizza.co.uk, dodopizza.by ecc.).
Auth. Servizio di autorizzazione e autenticazione per il back office.
Tracker. Tracker degli ordini in cucina. Servizio di segnalazione degli stati di preparazione durante la preparazione dell'ordine.
Cassa Ristorante. Accettazione degli ordini nel ristorante, interfacce per il cassiere.
Export. Esportazione dei rapporti in 1C per la contabilità.
Notifiche e bolle. Comandi vocali in cucina (ad esempio, «È arrivata una nuova pizza») + stampa delle bolle per i corrieri.
Manager Turno. Interfacce per il lavoro del manager di turno: elenco degli ordini, grafici di produttività, assegnazione del personale per il turno.
Manager Ufficio. Interfacce per il lavoro dei franchisor e del manager: assunzione del personale, rapporti sull'attività della pizzeria.
Tabellone Ristorante. Visualizzazione del menu sui televisori nelle pizzerie.
Admin. Impostazioni specifiche per la pizzeria: menu, prezzi, gestione, codici promozionali, offerte, banner per il sito, ecc.
Area Personale Dipendente. Grafici di lavoro dei dipendenti, informazioni sui dipendenti.
Tabellone Motivazione Cucina. Schermo separato, appeso in cucina, che mostra la velocità di lavoro dei pizzaioli.
Comunicazione. Invio di sms e email.
FileStorage. Servizio proprietario per la ricezione e il rilascio di file statici.
I primi tentativi di risolvere i problemi ci hanno aiutato, ma sono stati solo una pausa temporanea. Non sono diventati soluzioni sistemiche, quindi era chiaro che era necessario fare qualcosa con i database. Ad esempio, separare il database comune in più database specializzati.
Iniziamo a ridurre il carico del monolite: separazione di Auth e Tracker
I principali servizi che all'epoca registravano e leggevano di più dal database:
- Auth. Servizio di autorizzazione e autenticazione per il back office.
- Tracker. Tracker degli ordini in cucina. Servizio di segnalazione degli stati di preparazione durante la preparazione dell'ordine.
Cosa fa Auth
Auth è il servizio attraverso il quale gli utenti accedono al back office (nella parte cliente c'è un accesso separato e indipendente). Viene anche utilizzato per verificare che siano presenti i diritti di accesso necessari e che questi diritti non siano cambiati dall'ultimo accesso. Attraverso di esso avviene anche l'accesso dei dispositivi nelle pizzerie.
Ad esempio, vogliamo aprire sul televisore appeso in sala un display con gli stati degli ordini pronti. Quindi apriamo auth.dodopizza.ru, selezioniamo "Accesso come dispositivo", appare un codice che può essere inserito in una pagina speciale sul computer del manager di turno, specificando il tipo di dispositivo. Il televisore passerà automaticamente all'interfaccia della propria pizzeria e inizierà a visualizzare i nomi dei clienti i cui ordini sono pronti.

Da dove proviene il carico?
Ogni utente autenticato del back office, per ogni richiesta, accede al database, estrae l'utente dalla tabella utenti tramite una query SQL e controlla se ha i necessari accessi e diritti per quella pagina.
Ogni dispositivo fa lo stesso, verificando il proprio ruolo e i propri accessi con la tabella dei dispositivi. Un gran numero di richieste al master database porta al suo sovraccarico e al consumo di risorse del database generale per queste operazioni.
Riduciamo il carico da Auth
Auth ha un dominio isolato, cioè i dati sugli utenti, login o dispositivi vengono inviati al servizio (futuro) e lì rimangono. Se qualcuno ne avrà bisogno, andrà a quel servizio per i dati.
C'ERA. Il processo di lavoro inizialmente era il seguente:

Voglio chiarire un po' come funzionava:
- Una richiesta esterna arriva al backend (lì c'è Asp.Net MVC), portando con sé un cookie di sessione, che viene utilizzato per ottenere i dati di sessione da Redis(1). Qui c'è o meno informazione sugli accessi, e se c'è, l'accesso al controller è aperto (3,4), altrimenti no.
- Se non ci sono accessi, è necessario superare la procedura di autorizzazione. Qui, per semplificare, viene mostrata come parte del percorso nello stesso attributo, anche se si tratta di un passaggio alla pagina di login. In caso di scenario positivo, riceveremo una sessione correttamente compilata e passeremo al Backoffice Controller.
- Se i dati sono presenti, è necessario verificarne l'attualità nel database utente. È cambiato il suo ruolo? Potrebbe non aver accesso ora a quella pagina. In questo caso, dopo aver ricevuto la sessione (1), è necessario accedere direttamente al database e verificare gli accessi dell'utente tramite il livello logico di autenticazione (2). Successivamente, si passa o alla pagina di login o al controllo del controller. È un sistema piuttosto semplice, ma non del tutto standard.
- Se tutte le procedure sono state completate, proseguiamo nella logica nei controller e nei metodi.
I dati degli utenti sono separati da tutte le altre informazioni, vengono memorizzati in una tabella separata denominata membership, le funzioni del layer logico AuthService possono benissimo diventare metodi API. I confini del dominio sono definiti in modo chiaro: utenti, i loro ruoli, informazioni sugli accessi, concessione e revoca degli accessi. Tutto sembra poter essere estratto in un servizio separato.
È SUCCESSO. E così abbiamo fatto:

Questo approccio ha alcune problematiche. Ad esempio, chiamare un metodo all'interno di un processo non è la stessa cosa che chiamare un servizio esterno via http. La latenza, l'affidabilità, la manutenibilità e la trasparenza dell'operazione sono completamente diverse. Di queste problematiche ha parlato in dettaglio Andrey Morevsky nella sua conferenza .
Il servizio di autenticazione e il servizio dispositivi vengono utilizzati per il back office, ovvero per servizi e interfacce utilizzati nella produzione. L'autenticazione per i servizi client (come il sito web o l'app mobile) avviene separatamente senza utilizzare Auth. La separazione ha richiesto circa un anno, e ora ci stiamo nuovamente occupando di questo tema, migrando il sistema a nuovi servizi di autenticazione (con protocolli standard).
Perché la separazione è durata così a lungo?
Lungo il percorso ci sono stati numerosi problemi che hanno rallentato:
- Volevamo trasferire i dati sugli utenti, dispositivi e autenticazione da database nazionali a uno solo. Per questo abbiamo dovuto trasferire tutte le tabelle e cambiare l'uso dell'identificatore int in un identificatore globale UUID (questo codice è stato recentemente rielaborato e progetto open-source ). La memorizzazione dei dati sugli utenti (poiché si tratta di informazioni personali) ha delle limitazioni e, per alcuni paesi, devono essere archiviati separatamente. Ma l'identificatore globale dell'utente deve esserci.
- Molte tabelle nel database contengono informazioni sul audit dell'utente che ha effettuato l'operazione. Questo ha richiesto un meccanismo aggiuntivo per garantire la consistenza.
- Dopo la creazione dei servizi API, c'è stato un lungo e graduale periodo di migrazione a un altro sistema. Le transizioni dovevano avvenire senza interruzioni per gli utenti e richiedevano intervento manuale.
Schema di registrazione del dispositivo nella pizzeria:

Architettura generale dopo la separazione del servizio Auth e Devices:

Nota. Nel 2020 stiamo lavorando a una nuova versione di Auth, basata sullo standard di autorizzazione OAuth 2.0. Questo standard è piuttosto complesso, ma sarà utile per lo sviluppo del servizio di autenticazione end-to-end. Nell'articolo «» abbiamo cercato di spiegare il standard in modo semplice e chiaro, in modo che tu possa risparmiare tempo nella sua comprensione.
Cosa fa Tracker
Ora parliamo del secondo dei servizi ad alto carico. Il tracker svolge un ruolo duplice:
- Da un lato, il suo compito è mostrare ai dipendenti in cucina quali ordini sono attualmente in lavorazione e quali ingredienti devono essere preparati.
- Dall'altro lato, deve digitalizzare tutti i processi in cucina.

Quando un nuovo prodotto (ad esempio, una pizza) appare in un ordine, viene inviato alla stazione del tracker 'Stesura'. In questa stazione c'è un pizzaiolo che prende la pasta della dimensione necessaria e la stende, quindi segna sul tablet del tracker che ha completato il suo compito e passa il disco di pasta alla stazione successiva - 'Farcitura'.
Qui, un altro pizzaiolo farcisce la pizza, quindi segna sul tablet che ha completato il suo compito e mette la pizza nel forno (questa è anche una stazione separata, che deve essere segnata sul tablet). Questo sistema è stato utilizzato fin dall'inizio in Dodo e dall'esistenza di Dodo IS. Permette di tracciare e digitalizzare completamente tutte le operazioni. Inoltre, il tracker fornisce indicazioni su come preparare prodotti specifici, guida ogni tipo di prodotto attraverso le proprie schede di produzione, conserva il tempo di preparazione ottimale del prodotto e monitora tutte le operazioni relative al prodotto.
Ecco come appare lo schermo del tablet alla stazione del tracker 'Stesura'
Da dove proviene il carico?
In ogni pizzeria ci sono circa cinque tablet con un tracker. Nel 2016 avevamo più di 100 pizzerie (ora sono oltre 600). Ognuno di questi tablet fa una richiesta al backend ogni 10 secondi e recupera dati dalla tabella degli ordini (associazione con il cliente e l'indirizzo), dal contenuto dell'ordine (associazione con il prodotto e indicazione della quantità), dalla tabella di monitoraggio delle motivazioni (dove viene registrato il tempo di pressione). Quando il pizzaiolo preme sul prodotto nel tracker, vengono aggiornate le registrazioni in tutte queste tabelle. La tabella degli ordini è comune, in essa vengono effettuate contemporaneamente inserimenti quando viene accettato un ordine, aggiornamenti da altre parti del sistema e molte letture, ad esempio, sul televisore appeso nella pizzeria che mostra agli clienti gli ordini pronti.
Durante il periodo di gestione dei carichi, quando tutto e tutti venivano memorizzati nella cache e trasferiti su una replica asincrona del database, queste operazioni con il tracker continuavano a passare al database principale. Non deve esserci alcun ritardo, i dati devono essere aggiornati, la desincronizzazione non è accettabile.
Inoltre, l'assenza di tabelle e indici propri non consentiva di scrivere query più specifiche, mirate al proprio utilizzo. Ad esempio, per il tracker potrebbe essere efficace avere un indice sulla pizzeria nella tabella degli ordini. Recuperiamo sempre dal database del tracker gli ordini per la pizzeria. Tuttavia, per l'accettazione dell'ordine non è così importante a quale pizzeria appartiene, è più importante quale cliente ha effettuato quest'ordine. Pertanto, è necessario un indice sul cliente. Inoltre, per il tracker nella tabella degli ordini non è necessario memorizzare l'id della ricevuta stampata o le promozioni bonus associate all'ordine. Queste informazioni non interessano il nostro servizio di tracker. Nella base monolitica generale, le tabelle potevano essere solo una compromissione tra tutti gli utenti. Questo era uno dei problemi iniziali.
C'ERA. Inizialmente, l'architettura era la seguente:

Anche dopo la separazione in processi distinti, la maggior parte del codice rimaneva comune per diversi servizi. Tutto ciò che era al di sotto dei controller era unico e viveva in un unico repository. Venivano utilizzati metodi comuni per servizi, repository, un database comune in cui si trovavano tabelle comuni.
Riduciamo il carico da Tracker
Il problema principale con il tracker è che i dati devono essere sincronizzati tra diverse basi. Questa è anche la sua principale differenza dalla separazione del servizio Auth, dato che un ordine e il suo stato possono cambiare e devono essere visualizzati in diversi servizi.
Accettiamo ordini al Cassa del Ristorante (questo è un servizio), che vengono salvati nel database con lo stato "Accettato". Successivamente devono passare al tracker, dove cambiano ancora diverse volte il loro stato: da "Cucina" a "Imballato". Durante questo processo, possono esserci influenze esterne dall'interfaccia di Cassa o dal Manager di turno. Riporterò nella tabella gli stati dell'ordine con le loro descrizioni:

Lo schema di cambiamento degli stati dell'ordine è il seguente:

Gli stati cambiano tra sistemi diversi. Qui il tracker non è un sistema finale in cui si chiudono i dati. Abbiamo visto diversi approcci possibili per la separazione in questo caso:
- Concentriamo tutte le azioni dell'ordine in un solo servizio. Nel nostro caso, questa opzione richiede un servizio troppo grande per la gestione degli ordini. Se ci fossimo fermati a questo, avremmo creato un secondo monolite. Non avremmo risolto i problemi.
- Un sistema effettua una chiamata a un altro. La seconda opzione è già più interessente. Ma con essa sono possibili catene di chiamate (), la connessione tra i componenti è maggiore e gestirla è più complesso.
- Organizziamo eventi, e ogni servizio comunica con un altro tramite questi eventi. Alla fine è stata scelta proprio la terza opzione, secondo cui tutti i servizi iniziano a scambiarsi eventi tra loro.
Il fatto che abbiamo scelto la terza opzione significava che il tracker avrebbe avuto il proprio database, e ad ogni modifica dell'ordine avrebbe inviato un evento al riguardo, a cui si iscrivono gli altri servizi e che finisce anche nel master database. Per questo avevamo bisogno di un servizio che garantisse la consegna dei messaggi tra i servizi.
A quel tempo nel nostro stack avevamo già RabbitMQ, da qui la soluzione finale di utilizzare esso come broker di messaggi. Nello schema è mostrato il passaggio dell'ordine dalla Cassa del Ristorante attraverso il Tracker, dove cambia i suoi stati e la sua visualizzazione nell'interfaccia Ordini del manager. È DIVENTATO:

Il percorso dell'ordine passo dopo passo
Il percorso dell'ordine inizia in uno dei servizi fonte dell'ordine. Qui è la Cassa del Ristorante:
- Alla Cassa l'ordine è pronto per essere inviato al tracker. Viene sollevato un evento a cui il tracker è iscritto.
- Il tracker, accettando l'ordine, lo salva nel proprio database, generando l'evento «OrdineAccettatoDalTracker» e inviandolo a RMQ.
- Nel bus degli eventi, ci sono già diversi gestori iscritti all'ordine. Per noi è importante quello che esegue la sincronizzazione con il database monolitico.
- Il gestore riceve l'evento, estrae i dati significativi per lui: nel nostro caso, questo è lo stato dell'ordine «AccettatoDalTracker» e aggiorna la sua entità d'ordine nel database principale.
Se a qualcuno serve l'ordine proprio dalla tabella monolitica degli ordini, può leggerlo anche da lì. Questo è necessario, ad esempio, per l'interfaccia Ordini nel Gestore dei Turni:

Anche tutti gli altri servizi possono iscriversi agli eventi dell'ordine dal tracker, per utilizzarli a loro favore.
Se dopo un po' l'ordine viene preso in carico, il suo stato viene inizialmente modificato nel proprio database (il database del Tracker), e poi viene immediatamente generato l'evento «OrdineInLavoro». Anche questo entra in RMQ, da dove si sincronizza nel database monolitico e viene consegnato ad altri servizi. Lungo questo percorso possono verificarsi vari problemi, a proposito dei quali si può consultare la relazione di Zhenya Peshkov .
L'architettura finale dopo le modifiche in Auth e Tracker

Per fare un bilancio intermedio: inizialmente avevo l'idea di racchiudere una storia di nove anni del sistema Dodo IS in un solo articolo. Volevo raccontare rapidamente e semplicemente le fasi dell'evoluzione. Tuttavia, una volta seduto sui materiali, ho capito che tutto è molto più complesso e interessante di quanto sembri.
Riflettendo sull'utilità (o assenza di essa) di un tale materiale, sono giunto alla conclusione che lo sviluppo continuo non è possibile senza cronache complete degli eventi, retrospettive dettagliate e analisi delle proprie decisioni passate.
Spero che sia stato utile e interessante conoscere il nostro percorso. Ora sono a un bivio, decidendo quale parte del sistema Dodo IS descrivere nel prossimo articolo: scrivete nei commenti o votate.
Solo gli utenti registrati possono partecipare al sondaggio. , per favore.
Quale parte di Dodo IS vi piacerebbe conoscere nel prossimo articolo?
24,1%Primo monolitico in Dodo IS (2011-2015)
24,1%Prime problematiche e le loro soluzioni (2015-2016)
20,7%Il percorso della parte client: facciata sopra il database (2016-2017)
36,2%Storia dei veri microservizi (2018-2019)
44,8%Taglio completo del monolite e stabilizzazione dell'architettura26
29,3%Sui futuri piani di sviluppo del sistema17
19,0%Non voglio sapere nulla di Dodo IS11
Hanno votato 58 utenti. Si sono astenuti 6 utenti.
Fonte: habr.com
