Elementi costitutivi di applicazioni distribuite. Seconda approssimazione

annuncio

Colleghi, a metà estate ho intenzione di pubblicare un'altra serie di articoli sulla progettazione dei sistemi di coda: "The VTrade Experiment" - un tentativo di scrivere una struttura per i sistemi di scambio. La serie esaminerà la teoria e la pratica della costruzione di una borsa, di un'asta e di un negozio. Alla fine dell’articolo ti invito a votare gli argomenti che più ti interessano.

Elementi costitutivi di applicazioni distribuite. Seconda approssimazione

Questo è l'articolo finale della serie sulle applicazioni reattive distribuite in Erlang/Elixir. IN primo articolo si possono trovare i fondamenti teorici dell’architettura reattiva. Secondo articolo illustra i modelli e i meccanismi di base per la costruzione di tali sistemi.

Oggi solleveremo le questioni relative allo sviluppo del codice base e dei progetti in generale.

Organizzazione dei servizi

Nella vita reale, quando si sviluppa un servizio, spesso è necessario combinare diversi modelli di interazione in un unico controller. Ad esempio, il servizio utenti, che risolve il problema della gestione dei profili utente del progetto, deve rispondere alle richieste req-resp e segnalare gli aggiornamenti del profilo tramite pub-sub. Questo caso è abbastanza semplice: dietro la messaggistica c'è un controller che implementa la logica del servizio e pubblica gli aggiornamenti.

La situazione diventa più complicata quando dobbiamo implementare un servizio distribuito tollerante ai guasti. Immaginiamo che i requisiti per gli utenti siano cambiati:

  1. ora il servizio dovrebbe elaborare le richieste su 5 nodi del cluster,
  2. essere in grado di eseguire attività di elaborazione in background,
  3. ed essere anche in grado di gestire dinamicamente gli elenchi di abbonamenti per gli aggiornamenti del profilo.

osservazione: Non consideriamo la questione dell'archiviazione coerente e della replica dei dati. Supponiamo che questi problemi siano stati risolti in precedenza e che il sistema disponga già di un livello di archiviazione affidabile e scalabile e che i gestori dispongano di meccanismi per interagire con esso.

La descrizione formale del servizio agli utenti è diventata più complicata. Dal punto di vista del programmatore, le modifiche sono minime a causa dell'uso della messaggistica. Per soddisfare il primo requisito dobbiamo configurare il bilanciamento nel punto di scambio req-resp.

La necessità di elaborare attività in background si verifica frequentemente. Negli utenti, ciò potrebbe significare il controllo dei documenti utente, l'elaborazione di contenuti multimediali scaricati o la sincronizzazione dei dati con i social media. reti. Questi compiti devono essere in qualche modo distribuiti all'interno del cluster e il progresso dell'esecuzione deve essere monitorato. Pertanto, abbiamo due opzioni di soluzione: utilizzare il modello di distribuzione delle attività dell'articolo precedente o, se non è adatto, scrivere un pianificatore di attività personalizzato che gestirà il pool di processori nel modo di cui abbiamo bisogno.

Il punto 3 richiede l'estensione del modello pub-sub. E per l'implementazione, dopo aver creato un punto di scambio pub-sub, dobbiamo avviare ulteriormente il controller di questo punto all'interno del nostro servizio. Pertanto, è come se stessimo spostando la logica per l'elaborazione delle iscrizioni e delle cancellazioni dal livello di messaggistica all'implementazione degli utenti.

Di conseguenza, la scomposizione del problema ha mostrato che per soddisfare i requisiti è necessario avviare 5 istanze del servizio su nodi diversi e creare un'entità aggiuntiva: un controller pub-sub responsabile dell'abbonamento.
Per eseguire 5 gestori, non è necessario modificare il codice di servizio. L'unica azione aggiuntiva è l'impostazione di regole di bilanciamento nel punto di scambio, di cui parleremo più avanti.
C'è anche un'ulteriore complessità: il controller pub-sub e l'utilità di pianificazione delle attività personalizzate devono funzionare in un'unica copia. Ancora una volta, il servizio di messaggistica, essendo fondamentale, deve fornire un meccanismo per la selezione di un leader.

Selezione del leader

Nei sistemi distribuiti, l'elezione del leader è la procedura per nominare un singolo processo responsabile della pianificazione dell'elaborazione distribuita di alcuni carichi.

Nei sistemi non inclini alla centralizzazione vengono utilizzati algoritmi universali e basati sul consenso, come paxos o raft.
Poiché la messaggistica è un intermediario e un elemento centrale, conosce tutti i controllori del servizio: i leader candidati. La messaggistica può nominare un leader senza votare.

Dopo l'avvio e la connessione al punto di scambio, tutti i servizi ricevono un messaggio di sistema #'$leader'{exchange = ?EXCHANGE, pid = LeaderPid, servers = Servers}. Se LeaderPid partite con pid processo in corso, ne viene nominato il capofila e la lista Servers include tutti i nodi e i relativi parametri.
Nel momento in cui ne appare uno nuovo e un nodo del cluster funzionante viene disconnesso, tutti i controller del servizio ricevono #'$slave_up'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} и #'$slave_down'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} rispettivamente.

In questo modo, tutti i componenti sono consapevoli di tutti i cambiamenti e il cluster ha la garanzia di avere un leader in qualsiasi momento.

intermediari

Per implementare processi complessi di elaborazione distribuita, così come in problemi di ottimizzazione di un'architettura esistente, è conveniente utilizzare intermediari.
Per non modificare il codice del servizio e risolvere, ad esempio, problemi di elaborazione aggiuntiva, instradamento o registrazione dei messaggi, è possibile abilitare prima del servizio un gestore proxy, che eseguirà tutto il lavoro aggiuntivo.

Un classico esempio di ottimizzazione pub-sub è un'applicazione distribuita con un core aziendale che genera eventi di aggiornamento, come variazioni di prezzo nel mercato, e un livello di accesso: server N che forniscono un'API websocket per i client web.
Se decidi frontalmente, il servizio clienti si presenterà così:

  • il cliente stabilisce connessioni con la piattaforma. Sul lato del server che termina il traffico viene avviato un processo per servire questa connessione.
  • Nel contesto del processo di servizio si verificano l'autorizzazione e la sottoscrizione agli aggiornamenti. Il processo chiama il metodo di sottoscrizione per gli argomenti.
  • Una volta che un evento viene generato nel kernel, viene consegnato ai processi che servono le connessioni.

Immaginiamo di avere 50000 iscritti all'argomento "notizie". Gli abbonati sono distribuiti uniformemente su 5 server. Di conseguenza, ogni aggiornamento, arrivando al punto di scambio, verrà replicato 50000 volte: 10000 volte su ciascun server, a seconda del numero di abbonati su di esso. Non è uno schema molto efficace, vero?
Per migliorare la situazione introduciamo un proxy che abbia lo stesso nome del punto di scambio. Il registrar dei nomi globale deve essere in grado di restituire il processo più vicino per nome, questo è importante.

Lanciamo questo proxy sui server del livello di accesso e tutti i nostri processi che servono l'API websocket si iscriveranno ad esso e non al punto di scambio pub-sub originale nel kernel. Il proxy si iscrive al core solo nel caso di una sottoscrizione univoca e replica il messaggio in arrivo a tutti i suoi abbonati.
Di conseguenza, verranno inviati 5 messaggi tra il kernel e i server di accesso, invece di 50000.

Routing e bilanciamento

Richiesta-Resp

Nell'attuale implementazione della messaggistica, esistono 7 strategie di distribuzione delle richieste:

  • default. La richiesta viene inviata a tutti i controllori.
  • round-robin. Le richieste vengono enumerate e distribuite ciclicamente tra i controller.
  • consensus. I controllori che prestano servizio si dividono in leader e schiavi. Le richieste vengono inviate solo al leader.
  • consensus & round-robin. Il gruppo ha un leader, ma le richieste vengono distribuite tra tutti i membri.
  • sticky. La funzione hash viene calcolata e assegnata a un gestore specifico. Le richieste successive con questa firma vanno allo stesso gestore.
  • sticky-fun. Quando si inizializza il punto di scambio, la funzione di calcolo dell'hash per sticky bilanciamento.
  • fun. Simile a Sticky-Fun, solo tu puoi inoltre reindirizzarlo, rifiutarlo o pre-elaborarlo.

La strategia di distribuzione viene impostata al momento dell'inizializzazione del punto di scambio.

Oltre al bilanciamento, la messaggistica consente di taggare le entità. Diamo un'occhiata ai tipi di tag nel sistema:

  • Etichetta di connessione. Permette di capire attraverso quale connessione sono arrivati ​​gli eventi. Utilizzato quando un processo del controller si connette allo stesso punto di scambio, ma con chiavi di instradamento diverse.
  • Etichetta di servizio. Consente di combinare gestori in gruppi per un servizio ed espandere le capacità di instradamento e bilanciamento. Per il modello req-resp, il routing è lineare. Inviamo una richiesta al punto di scambio, quindi la trasmette al servizio. Ma se dobbiamo dividere i gestori in gruppi logici, la suddivisione viene effettuata utilizzando i tag. Quando si specifica un tag, la richiesta verrà inviata a un gruppo specifico di controller.
  • Richiedi etichetta. Consente di distinguere tra le risposte. Poiché il nostro sistema è asincrono, per elaborare le risposte del servizio dobbiamo essere in grado di specificare un RequestTag quando inviamo una richiesta. Da esso potremo comprendere la risposta a quale richiesta ci è arrivata.

Pub-sub

Per i pub-sub tutto è un po’ più semplice. Abbiamo un punto di scambio in cui vengono pubblicati i messaggi. Il punto di scambio distribuisce i messaggi tra gli abbonati che hanno sottoscritto le chiavi di instradamento di cui hanno bisogno (si può dire che questo è analogo ai topic).

Scalabilità e tolleranza agli errori

La scalabilità del sistema nel suo complesso dipende dal grado di scalabilità degli strati e dei componenti del sistema:

  • I servizi vengono ridimensionati aggiungendo ulteriori nodi al cluster con gestori per questo servizio. Durante il funzionamento di prova è possibile scegliere la politica di bilanciamento ottimale.
  • Il servizio di messaggistica stesso all'interno di un cluster separato viene generalmente ridimensionato spostando punti di scambio particolarmente caricati su nodi del cluster separati o aggiungendo processi proxy ad aree particolarmente caricate del cluster.
  • La scalabilità dell'intero sistema come caratteristica dipende dalla flessibilità dell'architettura e dalla capacità di combinare i singoli cluster in un'entità logica comune.

Il successo di un progetto dipende spesso dalla semplicità e dalla velocità di scalabilità. La messaggistica nella sua versione attuale cresce insieme all'applicazione. Anche se ci manca un cluster di 50-60 macchine possiamo ricorrere alla federazione. Sfortunatamente, il tema della federazione va oltre lo scopo di questo articolo.

Prenotazione

Analizzando il bilanciamento del carico, abbiamo già discusso della ridondanza dei controller di servizio. Tuttavia, anche la messaggistica deve essere riservata. In caso di guasto di un nodo o di una macchina, la messaggistica dovrebbe essere ripristinata automaticamente e nel più breve tempo possibile.

Nei miei progetti utilizzo nodi aggiuntivi che raccolgono il carico in caso di caduta. Erlang dispone di un'implementazione in modalità distribuita standard per le applicazioni OTP. La modalità distribuita esegue il ripristino in caso di errore avviando l'applicazione non riuscita su un altro nodo avviato in precedenza. Il processo è trasparente; dopo un errore, l'applicazione si sposta automaticamente al nodo di failover. Puoi leggere ulteriori informazioni su questa funzionalità qui.

Производительность

Proviamo a confrontare almeno approssimativamente le prestazioni di RabbitMQ e della nostra messaggistica personalizzata.
ho trovato risultati ufficiali test di conigliomq dal team openstack.

Al paragrafo 6.14.1.2.1.2.2. Il documento originale mostra il risultato dell'RPC CAST:
Elementi costitutivi di applicazioni distribuite. Seconda approssimazione

Non effettueremo in anticipo alcuna impostazione aggiuntiva al kernel del sistema operativo o alla VM Erlang. Condizioni per il test:

  • erl opta: +A1 +sbtu.
  • Il test all'interno di un singolo nodo erlang viene eseguito su un laptop con un vecchio i7 in versione mobile.
  • I test cluster vengono eseguiti su server con rete 10G.
  • Il codice viene eseguito in contenitori docker. Rete in modalità NAT.

Codice di prova:

req_resp_bench(_) ->
  W = perftest:comprehensive(10000,
    fun() ->
      messaging:request(?EXCHANGE, default, ping, self()),
      receive
        #'$msg'{message = pong} -> ok
      after 5000 ->
        throw(timeout)
      end
    end
  ),
  true = lists:any(fun(E) -> E >= 30000 end, W),
  ok.

Script 1: Il test viene eseguito su un laptop con una vecchia versione mobile i7. Il test, la messaggistica e il servizio vengono eseguiti su un nodo in un contenitore Docker:

Sequential 10000 cycles in ~0 seconds (26987 cycles/s)
Sequential 20000 cycles in ~1 seconds (26915 cycles/s)
Sequential 100000 cycles in ~4 seconds (26957 cycles/s)
Parallel 2 100000 cycles in ~2 seconds (44240 cycles/s)
Parallel 4 100000 cycles in ~2 seconds (53459 cycles/s)
Parallel 10 100000 cycles in ~2 seconds (52283 cycles/s)
Parallel 100 100000 cycles in ~3 seconds (49317 cycles/s)

Script 2: 3 nodi in esecuzione su macchine diverse sotto docker (NAT).

Sequential 10000 cycles in ~1 seconds (8684 cycles/s)
Sequential 20000 cycles in ~2 seconds (8424 cycles/s)
Sequential 100000 cycles in ~12 seconds (8655 cycles/s)
Parallel 2 100000 cycles in ~7 seconds (15160 cycles/s)
Parallel 4 100000 cycles in ~5 seconds (19133 cycles/s)
Parallel 10 100000 cycles in ~4 seconds (24399 cycles/s)
Parallel 100 100000 cycles in ~3 seconds (34517 cycles/s)

In tutti i casi, l'utilizzo della CPU non ha superato il 250%

Risultati di

Spero che questo ciclo non assomigli a una discarica mentale e che la mia esperienza sarà di reale beneficio sia per i ricercatori di sistemi distribuiti che per i professionisti che sono all'inizio della costruzione di architetture distribuite per i loro sistemi aziendali e stanno guardando Erlang/Elixir con interesse , ma dubito che ne valga la pena...

foto @chuttersnap

Solo gli utenti registrati possono partecipare al sondaggio. AccediPer favore.

Quali argomenti dovrei trattare in modo più dettagliato come parte della serie VTrade Experiment?

  • Teoria: Mercati, ordini e loro tempistica: DAY, GTD, GTC, IOC, FOK, MOO, MOC, LOO, LOC

  • Libro degli ordini. Teoria e pratica della realizzazione di un libro con raggruppamenti

  • Visualizzazione del trading: Tick, barre, risoluzioni. Come conservare e come incollare

  • Back office. Pianificazione e sviluppo. Monitoraggio dei dipendenti e indagini sugli incidenti

  • API. Scopriamo quali interfacce sono necessarie e come implementarle

  • Archiviazione delle informazioni: PostgreSQL, Timescale, Tarantool nei sistemi di trading

  • Reattività nei sistemi di trading

  • Altro. Scrivo nei commenti

6 utenti hanno votato. 4 utenti si sono astenuti.

Fonte: habr.com

Aggiungi un commento