
In Mail.ru Group abbiamo Tarantool: è un server di applicazioni in Lua che funge anche da database (o viceversa?). È veloce e potente, ma le capacità di un singolo server non sono infinite. La scalabilità verticale non è nemmeno la soluzione finale, quindi in Tarantool sono disponibili strumenti per la scalabilità orizzontale: il modulo vshard. Questo consente di partizionare i dati su più server, ma richiederà un po' di lavoro per configurarlo e integrare la logica di business.
Buone notizie: abbiamo imparato dai nostri errori (ad esempio, , ) e abbiamo sviluppato un nuovo framework che semplifica notevolmente la soluzione di questo problema.
è un nuovo framework per lo sviluppo di sistemi distribuiti complessi. Permette di concentrarsi sulla scrittura della logica di business anziché sulla risoluzione di problemi infrastrutturali. Sotto, spiegherò come è struttura questo framework e come usarlo per scrivere servizi distribuiti.
Qual è il problema, in effetti?
Abbiamo Tarantool, abbiamo vshard — cos'altro desiderare?
Innanzitutto, si tratta di comodità. La configurazione di vshard viene impostata tramite tabelle Lua. Affinché un sistema distribuito formato da più processi Tarantool funzioni correttamente, la configurazione deve essere identica ovunque. Nessuno vuole fare tutto questo manualmente. Perciò vengono utilizzati vari script, Ansible e sistemi di distribuzione.
Cartridge gestisce autonomamente la configurazione di vshard, lo fa in base alla sua propria configurazione distribuita. In sostanza, si tratta di un semplice file YAML, una copia del quale è conservata in ogni istanza di Tarantool. La semplificazione consiste nel fatto che il framework si occupa autonomamente della sua configurazione e si assicura che sia identica ovunque.
In secondo luogo, si tratta ancora di comodità. La configurazione di vshard non ha nulla a che fare con lo sviluppo della logica aziendale e distrae solo il programmatore dal proprio lavoro. Quando discutiamo dell'architettura di un progetto, parliamo più spesso di singoli componenti e delle loro interazioni. È presto per pensare al rollout di un cluster su 3 data center.
Abbiamo affrontato questi problemi più e più volte, e a un certo punto siamo riusciti a sviluppare un approccio che semplifica il lavoro con l'applicazione durante l'intero ciclo di vita: creazione, sviluppo, testing, CI/CD e manutenzione.
Cartridge introduce il concetto di ruolo per ogni processo di Tarantool. I ruoli sono la concezione che consente agli sviluppatori di concentrarsi sulla scrittura del codice. Tutti i ruoli presenti nel progetto possono essere eseguiti su un'unica istanza di Tarantool, e questo è sufficiente per i test.
Le principali funzionalità di Tarantool Cartridge:
- orchestrazione automatizzata del cluster;
- espansione delle funzionalità dell'applicazione con nuovi ruoli;
- template per lo sviluppo e il deployment dell'applicazione;
- sharding automatico integrato;
- integrazione con il framework di testing Luatest;
- gestione del cluster tramite WebUI e API;
- strumenti per il packaging e il deployment.
Hello, World!
Non vedo l'ora di mostrare il framework, quindi rimandiamo la discussione sull'architettura e iniziamo con qualcosa di semplice. Se assumiamo che Tarantool sia già installato, rimane solo da eseguire
$ tarantoolctl rocks install cartridge-cli
$ export PATH=$PWD/.rocks/bin/:$PATHQueste due comandi installeranno gli strumenti da riga di comando e permetteranno di creare la tua prima applicazione da un template:
$ cartridge create --name myappEcco cosa otterremo:
myapp/
├── .git/
├── .gitignore
├── app/roles/custom.lua
├── deps.sh
├── init.lua
├── myapp-scm-1.rockspec
├── test
│ ├── helper
│ │ ├── integration.lua
│ │ └── unit.lua
│ ├── helper.lua
│ ├── integration/api_test.lua
│ └── unit/sample_test.lua
└── tmp/
Questo è un repository git con un'applicazione 'Hello, World!' pronta. Proviamo subito a lanciarla, installando prima le dipendenze (incluso il framework):
$ tarantoolctl rocks make
$ ./init.lua --http-port 8080Quindi, abbiamo una nodulo in esecuzione per l'applicazione shardata futura. Il curioso può subito aprire l'interfaccia web, configurare un cluster da un nodo con il mouse e godersi il risultato, ma la gioia è prematura. Per il momento, l'applicazione non sa fare nulla di utile, quindi parlerò del deploy più tardi; ora è il momento di scrivere codice.
Sviluppo di applicazioni
Immagina di progettare un progetto che deve ricevere dati, salvarli e costruire un rapporto una volta al giorno.

Iniziamo a disegnare lo schema, e mettiamo tre componenti: gateway, storage e scheduler. Procediamo con l'architettura. Poiché utilizziamo vshard come storage, aggiungiamo allo schema vshard-router e vshard-storage. Né il gateway né lo scheduler accederanno direttamente allo storage; per questo c'è il router, creato appositamente per questo scopo.

Questo schema non riflette ancora con precisione ciò che creeremo nel progetto, poiché i componenti appaiono astratti. Dobbiamo valutare come si proietterà sul vero Tarantool — raggruppiamo i nostri componenti per processi.

Non ha molto senso mantenere vshard-router e gateway su istanze separate. Perché dover attraversare la rete, se questo è già compito del router? Dovrebbero essere eseguiti all'interno di un unico processo. Dentro questo processo si devono inizializzare sia il gateway che vshard.router.cfg, affinché possano interagire localmente.
Durante la fase di progettazione, lavorare con tre componenti è stato comodo, ma io, come sviluppatore, mentre scrivo il codice, non voglio pensare al lancio di tre istanze di Tarantool. Ho bisogno di eseguire dei test e verificare che il gateway sia stato scritto correttamente. Oppure, forse, voglio dimostrare una funzionalità ai miei colleghi. Perché dovrei faticare con il deployment di tre istanze? È così che è nata l'idea dei ruoli. Un ruolo è un normale modulo Lua il cui ciclo di vita è gestito da Cartridge. In questo esempio ci sono quattro ruoli: gateway, router, storage e scheduler. In un altro progetto potrebbero essere di più. Tutti i ruoli possono essere avviati in un unico processo, ed è sufficiente.

E quando si tratta di deployment in staging o in produzione, assegneremo a ciascun processo di Tarantool il proprio set di ruoli a seconda delle capacità hardware:

Gestione della topologia
Le informazioni su dove sono avviati quali ruoli devono essere conservate da qualche parte. E quel "qualche parte" è la configurazione distribuita di cui ho già parlato. La cosa più importante in essa è la topologia del cluster. Qui sono rappresentati 3 gruppi di replica composti da 5 processi Tarantool:

Non vogliamo perdere dati, quindi trattiamo con cura le informazioni sui processi in esecuzione. Cartridge monitora la configurazione tramite un commit in due fasi. Quando intendiamo aggiornare la configurazione, verifica prima la disponibilità di tutte le istanze e la loro prontezza ad accettare la nuova configurazione. Dopodiché, nella seconda fase, viene applicata la configurazione. In questo modo, anche se un'istanza diventa temporaneamente non disponibile, non c'è motivo di preoccuparsi. La configurazione semplicemente non verrà applicata e si vedrà in anticipo un errore.
Inoltre, nella sezione topologia è specificato un parametro fondamentale, ovvero il leader di ogni gruppo di replica. Di solito, si tratta dell'istanza su cui avviene la scrittura. Le altre sono per lo più in sola lettura, anche se possono esserci delle eccezioni. A volte, sviluppatori audaci non temono conflitti e possono scrivere dati su più repliche contemporaneamente, ma ci sono alcune operazioni che, a prescindere dalle circostanze, non devono essere eseguite due volte. Per questo motivo esiste il concetto di leader.

Ciclo di vita dei ruoli
Affinché un ruolo astratto possa esistere in un'architettura di questo tipo, il framework deve gestirlo in qualche modo. Naturalmente, la gestione avviene senza riavviare il processo di Tarantool. Ci sono 4 callback per la gestione dei ruoli. Cartridge li chiamerà a seconda di ciò che è specificato nella configurazione distribuita, applicando così la configurazione a ruoli specifici.
function init()
function validate_config()
function apply_config()
function stop()
Ogni ruolo ha una funzione init. Questa viene chiamata una sola volta, sia all'attivazione del ruolo, sia al riavvio di Tarantool. Qui è possibile, ad esempio, inizializzare box.space.create, oppure il scheduler può avviare qualche fiber in background che eseguirà lavoro a determinati intervalli di tempo.
Una funzione init potrebbe non essere sufficiente. Cartridge consente ai ruoli di utilizzare la configurazione distribuita che impiega per memorizzare la topologia. Possiamo dichiarare una nuova sezione in questa configurazione e memorizzare al suo interno un frammento della configurazione aziendale. Nel mio esempio, ciò potrebbe essere uno schema di dati o impostazioni di pianificazione per il ruolo di scheduler.
Il cluster chiama validate_config e apply_config ad ogni modifica della configurazione distribuita. Quando la configurazione viene applicata tramite un commit a due fasi, il cluster verifica che ogni ruolo sia pronto ad accettare questa nuova configurazione e informa l'utente di eventuali errori, se necessario. Quando tutti concordano che la configurazione è normale, viene eseguito apply_config.
C'è anche un metodo per i ruoli stop, che serve a pulire i risultati dell'attività del ruolo. Se diciamo che il scheduler su questo server non è più necessario, può fermare i fibri che ha avviato tramite init.
I ruoli possono interagire tra loro. Siamo abituati a scrivere chiamate di funzioni in Lua, ma può capitare che in questo processo non ci sia il ruolo di cui abbiamo bisogno. Per semplificare le chiamate di rete, utilizziamo un modulo di supporto rpc (remote procedure call), che si basa sullo standard netbox integrato in Tarantool. Questo può essere utile, ad esempio, se il tuo gateway desidera chiedere direttamente allo scheduler di effettuare il lavoro immediatamente, invece di aspettare un giorno.
Un altro punto importante è garantire la tolleranza ai guasti. Per monitorare la salute, in Cartridge viene utilizzato il protocollo SWIM. In breve, i processi si scambiano "notizie" tramite UDP: ogni processo comunica agli altri gli ultimi aggiornamenti, e questi rispondono. Se una risposta non arriva, Tarantool inizia a sospettare che qualcosa non vada, e dopo un po' annuncia la morte e informa tutti i presenti di questa novità.

Basandosi su questo protocollo, Cartridge organizza la gestione automatizzata dei guasti. Ogni processo tiene traccia del suo ambiente e, se il leader smette di rispondere, una replica può assumere il suo ruolo, mentre Cartridge configura di conseguenza i ruoli attivi.

È importante essere cauti, poiché continui passaggi da un processo all'altro possono causare conflitti nei dati durante la replica. Attivare il failover automatico senza riflessione non è raccomandabile. È fondamentale avere una chiara comprensione di ciò che sta accadendo e assicurarsi che la replica non si rompa dopo che il leader è tornato operativo e ha riottenuto il suo ruolo.
Da quanto detto, potrebbe sembrare che i ruoli siano simili ai microservizi. In un certo senso lo sono, ma come moduli all'interno dei processi di Tarantool. Tuttavia, ci sono anche alcune differenze fondamentali. Prima di tutto, tutti i ruoli del progetto devono vivere all'interno di un'unica base di codice. E tutti i processi di Tarantool devono essere avviati da una sola base di codice, per evitare sorprese come quando cerchiamo di inizializzare lo scheduler e semplicemente non esiste. Inoltre, non bisogna permettere differenze nelle versioni del codice, perché il comportamento del sistema in tale situazione è molto difficile da prevedere e debuggare.
A differenza di Docker, non possiamo semplicemente prendere l'«immagine» di un ruolo, portarla su un'altra macchina e avviarla lì. I nostri ruoli non sono così isolati come i contenitori Docker. Inoltre, non possiamo eseguire due ruoli identici su un'unica istanza. Un ruolo o esiste oppure no, in un certo senso è un singleton. Infine, all'interno di tutto il gruppo di replica, i ruoli devono essere identici, altrimenti sarebbe assurdo: i dati sono uguali, ma la configurazione è diversa.
Strumenti di deployment
Ho promesso di mostrare come Cartridge aiuti a distribuire applicazioni. Per semplificare la vita agli altri, il framework pacchetta i pacchetti RPM:
$ cartridge pack rpm myapp -- creerà per noi ./myapp-0.1.0-1.rpm
$ sudo yum install ./myapp-0.1.0-1.rpmIl pacchetto installato contiene quasi tutto il necessario: sia l'applicazione che le dipendenze Lua installate. Tarantool arriverà sul server anche come dipendenza del pacchetto RPM, e il nostro servizio sarà pronto per partire. Questo avviene tramite systemd, ma prima è necessario scrivere un po' di configurazione. Almeno, dobbiamo specificare l'URI di ogni processo. Ne bastano tre per esempio.
$ sudo tee /etc/tarantool/conf.d/demo.yml <<CONFIG
myapp.router: {"advertise_uri": "localhost:3301", "http_port": 8080}
myapp.storage_A: {"advertise_uri": "localhost:3302", "http_enabled": False}
myapp.storage_B: {"advertise_uri": "localhost:3303", "http_enabled": False}
CONFIGC'è un interessante dettaglio. Anziché specificare solo la porta del protocollo binario, indichiamo l'indirizzo pubblico del processo completo, incluso l'hostname. Questo è necessario affinché i nodi del cluster sappiano come connettersi tra loro. È una cattiva idea usare come advertise_uri l'indirizzo 0.0.0.0; deve essere un indirizzo IP esterno, non un socket di bind. Senza di esso, nulla funzionerà, quindi Cartridge non permetterà di avviare un nodo con un advertise_uri errato.
Ora che la configurazione è pronta, possiamo avviare i processi. Poiché un'unità systemd normale non consente di avviare più di un processo, le applicazioni su Cartridge impostano le cosiddette unità instantiate, che funzionano in questo modo:
$ sudo systemctl start myapp@router
$ sudo systemctl start myapp@storage_A
$ sudo systemctl start myapp@storage_BNella configurazione abbiamo specificato la porta HTTP su cui Cartridge gestisce l'interfaccia web — 8080. Andiamo a controllarla:

Abbiamo notato che i processi sono avviati, ma non ancora configurati. Il carrello non sa al momento chi deve replicarsi con chi e non può decidere autonomamente, quindi attende le nostre azioni. Le opzioni non sono molte: la vita del nuovo cluster inizia con la configurazione del primo nodo. Poi aggiungeremo gli altri al cluster, assegneremo i loro ruoli e possiamo considerare completato il deployment.
Versiamo un bicchiere della nostra bevanda preferita e rilassiamoci dopo una lunga settimana lavorativa. L'applicazione è pronta per essere utilizzata.

Risultati
E quali sono i risultati? Provate, utilizzate, lasciate un feedback e aprite ticket su GitHub.
Link
[1]
[2]
[3]
[4]
[5]
[6]
Fonte: habr.com
