Tarantool Cartridge: sharding del backend Lua in tre righe

Tarantool Cartridge: sharding del backend Lua in tre righe

In Mail.ru Group abbiamo Tarantool, un server di applicazioni in Lua che funge anche da database (o viceversa?). È veloce e innovativo, ma le capacità di un singolo server non sono illimitate. La scalabilità verticale non è una panacea, quindi in Tarantool ci sono strumenti per la scalabilità orizzontale: il modulo vshard. [1]. Esso consente di shardare i dati su più server, ma ci vorrà del tempo per configurarlo e integrarlo con la logica aziendale.

Buone notizie: abbiamo imparato dai nostri errori (ad esempio [2], [3]) e abbiamo sviluppato un nuovo framework che semplifica notevolmente la risoluzione di questo problema.

Tarantool Cartridge è un nuovo framework per lo sviluppo di sistemi distribuiti complessi. Permette di concentrarsi sulla scrittura della logica aziendale anziché risolvere problemi infrastrutturali. Dietro le quinte, spiegherò come è strutturato questo framework e come utilizzarlo per scrivere servizi distribuiti.

Qual è, dunque, il problema?

Abbiamo un Tarantool, abbiamo vshard: cosa desiderare di più?

Prima di tutto, c'è il comfort. La configurazione di vshard viene impostata tramite tabelle Lua. Affinché un sistema distribuito composto da più processi Tarantool funzioni correttamente, la configurazione deve essere identica ovunque. Nessuno vuole fare questo a mano. Perciò si utilizzano vari script, Ansible, sistemi di deployment.

Cartridge gestisce automaticamente la configurazione di vshard, basandosi sulla sua propria configurazione distribuita.Fondamentalmente, è un semplice file YAML, una copia del quale è memorizzata in ogni istanza di Tarantool. La semplificazione consiste nel fatto che il framework stesso controlla la propria configurazione e garantisce che sia la stessa ovunque.

In secondo luogo, c'è di nuovo il comfort. La configurazione di vshard non ha nulla a che fare con la scrittura della logica aziendale e distrae solo il programmatore dal lavoro. Quando discutiamo dell'architettura di un progetto, parliamo più spesso di componenti separati e delle loro interazioni. Non è il momento di pensare a un cluster distribuito su 3 datacenter.

Abbiamo affrontato questi problemi ripetutamente e a un certo punto siamo riusciti a sviluppare un approccio che semplifica il lavoro con l'applicazione in tutto il suo ciclo di vita: creazione, sviluppo, test, CI/CD e manutenzione.

Il cartridge introduce il concetto di ruoli per ogni processo di Tarantool. I ruoli sono quel concetto che permette agli sviluppatori di concentrarsi sulla scrittura del codice. Tutti i ruoli presenti nel progetto possono essere eseguiti su un'unica istanza di Tarantool, e per i test questo sarà sufficiente.

Principali funzionalità di Tarantool Cartridge:

  • orchestrazione automatizzata del cluster;
  • espansione della funzionalità dell'applicazione tramite nuovi ruoli;
  • modello di applicazione per sviluppo e distribuzione;
  • shardizzazione automatica integrata;
  • integrazione con il framework di test Luatest;
  • gestione del cluster tramite WebUI e API;
  • strumenti per pacchettizzazione e distribuzione.

Ciao, Mondo!

Non vedo l'ora di mostrare il framework, quindi rimandiamo il discorso sull'architettura e cominciamo con qualcosa di semplice. Se presumiamo che Tarantool sia già installato, rimane solo da eseguire

$ tarantoolctl rocks install cartridge-cli
$ export PATH=$PWD/.rocks/bin/:$PATH

Queste due comandi installeranno gli strumenti da riga di comando e ci permetteranno di creare la nostra prima applicazione da un modello:

$ cartridge create --name myapp

Ecco 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 eseguirla, installando prima le dipendenze (incluso il framework stesso):

$ tarantoolctl rocks make
$ ./init.lua --http-port 8080

Quindi, abbiamo avviato un nodo dell'applicazione sharded futura. Un curioso potrebbe subito aprire l'interfaccia web, configurare il cluster da un nodo e godersi il risultato, ma per il momento non c'è motivo di rallegrarsi. L'applicazione non sa ancora fare nulla di utile, quindi parlerò del deployment poi, e ora è il momento di scrivere codice.

Sviluppo di applicazioni

Immaginate, stiamo progettando un progetto che deve ricevere dati, conservarli e generare un rapporto una volta al giorno.

Tarantool Cartridge: sharding del backend Lua in tre righe

Iniziamo a disegnare uno schema e poniamo su di esso tre componenti: gateway, storage e scheduler. Sviluppiamo ulteriormente l'architettura. Poiché utilizziamo vshard come archivio, aggiungiamo allo schema vshard-router e vshard-storage. Né il gateway né lo scheduler si collegheranno direttamente all'archivio, per questo c'è il router, creato apposta.

Tarantool Cartridge: sharding del backend Lua in tre righe

Questo schema non riflette ancora totalmente ciò che andremo a creare nel progetto, poiché i componenti appaiono astratti. È necessario esaminare ulteriormente come si proietterà su un reale Tarantool — raggruppiamo i nostri componenti per processi.

Tarantool Cartridge: sharding del backend Lua in tre righe

Non ha molto senso mantenere vshard-router e gateway su istanze separate. Perché dobbiamo attraversare la rete più volte, se è già compito del router? Devono essere eseguiti all'interno di un unico processo. Cioè, all'interno di un unico processo vengono inizializzati sia gateway che vshard.router.cfg, e lasciamo che interagiscano localmente.

Durante la fase di progettazione, lavorare con tre componenti era comodo, ma io, come sviluppatore, mentre scrivo codice, non voglio preoccuparmi di avviare tre istanze di Tarantool. Ho bisogno di eseguire i test e verificare che abbia scritto correttamente il gateway. O magari voglio dimostrare una funzionalità ai colleghi. Perché dovrei avere a che fare con il dispiegamento di tre istanze? È così che è nata la concezione dei ruoli. Un ruolo è un normale modulo Lua, il cui ciclo di vita è gestito da Cartridge. In questo esempio ce ne sono quattro: gateway, router, storage e scheduler. In un altro progetto potrebbero essercene di più. Tutti i ruoli possono essere avviati in un unico processo, e questo sarà sufficiente.

Tarantool Cartridge: sharding del backend Lua in tre righe

E quando si tratta di dispiegare in staging o in produzione, assegneremo a ciascun processo Tarantool il proprio set di ruoli in base alle capacità hardware:

Tarantool Cartridge: sharding del backend Lua in tre righe

Gestione della topologia

Le informazioni su dove sono stati avviati i vari ruoli devono essere memorizzate da qualche parte. E questo 'da qualche parte' è una configurazione distribuita, di cui ho già parlato sopra. La cosa più importante è la topologia del cluster. Qui sono rappresentati 3 gruppi di replica composti da 5 processi Tarantool:

Tarantool Cartridge: sharding del backend Lua in tre righe

Non vogliamo perdere dati, quindi trattiamo con cura le informazioni sui processi in esecuzione. Cartridge monitora la configurazione utilizzando un commit in due fasi. Non appena vogliamo aggiornare la configurazione, prima controlla 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 risulta temporaneamente non disponibile, non succede nulla di grave. La configurazione semplicemente non verrà applicata e vedrete in anticipo l'errore.

Nella sezione topologia è indicato un parametro importante, il leader di ciascun gruppo di replica. Di solito, si tratta dell'istanza su cui avviene la scrittura. Le altre istanze sono per lo più read-only, ma ci possono essere delle eccezioni. A volte, sviluppatori audaci non temono conflitti e possono scrivere dati su più repliche in parallelo, ma ci sono alcune operazioni che, in ogni caso, non dovrebbero essere eseguite due volte. A questo serve il riconoscimento del leader.

Tarantool Cartridge: sharding del backend Lua in tre righe

Vita dei ruoli

Affinché un ruolo astratto possa esistere in tale architettura, il framework deve gestirli in qualche modo. Ovviamente, la gestione avviene senza riavviare il processo Tarantool. Per la gestione dei ruoli ci sono 4 callback. Cartridge li chiamerà autonomamente in base a quanto scritto nella configurazione distribuita, applicando così la configurazione a specifici ruoli.

function init()
function validate_config()
function apply_config()
function stop()

Ogni ruolo ha una funzione join. Essa viene chiamata una sola volta, sia all'attivazione del ruolo che al riavvio di Tarantool. Qui è comodo, ad esempio, inizializzare box.space.create, o il scheduler può avviare un qualche fiber in background, che eseguirà compiti a intervalli di tempo prestabiliti.

Una sola funzione join 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 stessa configurazione e memorizzare in essa un frammento della configurazione aziendale. Nel mio esempio, si può trattare di uno schema dati o delle 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 con un commit a due fasi, il cluster verifica che ogni ruolo sia pronto ad accettare questa nuova configurazione e, se necessario, informa l'utente di eventuali errori. Quando tutti concordano che la configurazione è valida, viene eseguito apply_config.

Inoltre, i ruoli dispongono di un metodo stop, necessario per ripulire i risultati dell'attività del ruolo. Se diciamo che lo scheduler non è più necessario su questo server, può fermare i fiber che ha avviato. join.

I ruoli possono interagire tra loro. Siamo abituati a scrivere chiamate di funzione in Lua, ma può succedere che in questo processo non sia presente il ruolo di cui abbiamo bisogno. Per semplificare le comunicazioni in rete, utilizziamo un modulo ausiliario rpc (remote procedure call), costruito sulla base del netbox standard integrato in Tarantool. Questo può essere utile, ad esempio, se il tuo gateway desidera chiedere direttamente allo scheduler di eseguire un'attività immediatamente, senza dover attendere un giorno.

Un altro aspetto importante è garantire la resilienza. Per monitorare la salute, Cartridge utilizza il protocollo SWIM. [4]In breve, i processi si scambiano "voci" tra loro tramite UDP: ogni processo informa i propri vicini delle ultime novità, e questi rispondono. Se per caso non arriva una risposta, Tarantool inizia a sospettare che ci sia qualcosa che non va, e dopo un po' annuncia una morte e comunica la notizia a tutti i circostanti.

Tarantool Cartridge: sharding del backend Lua in tre righe

Basandosi su questo protocollo, Cartridge organizza la gestione automatica dei guasti. Ogni processo tiene d'occhio il proprio ambiente e se il leader smette di rispondere, una replica può assumere il suo ruolo, e Cartridge riconfigura di conseguenza i ruoli attivi.

Tarantool Cartridge: sharding del backend Lua in tre righe

Bisogna prestare attenzione, perché frequenti switch avanti e indietro possono portare a conflitti di dati durante la replica. Attivare il failover automatico a caso, ovviamente, non è consigliato. È fondamentale capire chiaramente cosa sta succedendo e avere la certezza che la replicazione non si interromperà dopo il ripristino del leader e il suo ritorno al ruolo.

Da quanto detto, può 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 diverse differenze fondamentali. Innanzitutto, tutti i ruoli del progetto devono vivere in un'unica base di codice. E tutti i processi di Tarantool devono essere avviati dalla stessa base di codice, per evitare sorprese come quando proviamo a inizializzare lo scheduler e non esiste semplicemente. Inoltre, non bisogna permettere differenze nelle versioni del codice, poiché il comportamento del sistema in tale situazione è molto difficile da prevedere e debug.

A differenza di Docker, non possiamo semplicemente prendere un 'immagine' del ruolo, trasferirla su un'altra macchina e lanciarla lì. I nostri ruoli non sono isolati come i contenitori Docker. Inoltre, non possiamo eseguire due ruoli identici su una stessa istanza. Un ruolo esiste o non esiste; in un certo senso è un singleton. E in terzo luogo, all'interno dell'intero gruppo di replica, i ruoli devono essere identici, altrimenti sarebbe ridicolo: i dati sono uguali, ma la configurazione è diversa.

Strumenti di deployment

Ho promesso di mostrare come Cartridge aiuta a deployare le applicazioni. Per semplificare la vita a tutti, il framework impacchetta i pacchetti RPM:

$ cartridge pack rpm myapp -- impacchetta per noi .\/myapp-0.1.0-1.rpm
$ sudo yum install .\/myapp-0.1.0-1.rpm

Il pacchetto installato contiene quasi tutto il necessario: sia l'applicazione che le dipendenze Lua installate. Tarantool verrà installato sul server come dipendenza del pacchetto RPM, e il nostro servizio sarà pronto per l'avvio. Questo avviene tramite systemd, ma prima è necessario scrivere un po' di configurazione. Almeno, è necessario specificare l'URI di ciascun processo. Tre per esempio sono sufficienti.

$ 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}
CONFIG

C'è un interessante dettaglio. Invece di specificare solo la porta del protocollo binario, specifichiamo l'indirizzo pubblico del processo completo, compreso l'hostname. Questo è necessario affinché i nodi del cluster sappiano come collegarsi tra loro. È una cattiva idea utilizzare come advertise_uri l'indirizzo 0.0.0.0; deve essere un indirizzo IP esterno e non l'indirizzo di binding del socket. Senza di esso, nulla funzionerà, quindi Cartridge non consentirà semplicemente di avviare un nodo con un advertise_uri errato.

Ora che la configurazione è pronta, possiamo avviare i processi. Poiché un'unità systemd standard non consente di avviare più di un processo, le applicazioni su Cartridge installano 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_B

Nella configurazione abbiamo specificato la porta HTTP sulla quale Cartridge fornisce l'interfaccia web — 8080. Andiamo a vederla:

Tarantool Cartridge: sharding del backend Lua in tre righe

Vediamo che i processi sono avviati, ma non ancora configurati. La cartuccia non sa ancora chi deve replicarsi con chi e non può prendere decisioni autonomamente, quindi aspetta le nostre azioni. E noi abbiamo poche opzioni: la vita di un nuovo cluster inizia con la configurazione del primo nodo. Poi aggiungeremo gli altri al cluster, assegneremo loro i ruoli e a quel punto si può considerare completato con successo il deployment.

Versiamo un bicchiere della nostra bevanda preferita e rilassiamoci dopo una lunga settimana di lavoro. L'applicazione è pronta per essere utilizzata.

Tarantool Cartridge: sharding del backend Lua in tre righe

Conclusioni

E quali sono i risultati? Provate, utilizzate, lasciateci un feedback e aprite ticket su GitHub.

Link

[1] Tarantool » 2.2 » Riferimento » Riferimento Rocks » Modulo vshard

[2] Come abbiamo implementato il core del business degli investimenti di Alfa-Bank basato su Tarantool

[3] Architettura di billing di nuova generazione: trasformazione con il passaggio a Tarantool

[4] SWIM — protocollo per la costruzione di un cluster

[5] GitHub — tarantool/cartridge-cli

[6] GitHub — tarantool/cartridge

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster