Blocchi fondamentali delle applicazioni distribuite. Secondo approccio

Annuncio

Colleghi, a metà estate prevedo di pubblicare un altro ciclo di articoli sulla progettazione di sistemi di assistenza di massa: “Esperimento VTrade” — un tentativo di scrivere un framework per i sistemi di trading. Il ciclo analizzerà la teoria e la pratica della costruzione di borse, aste e negozi. Alla fine dell'articolo vi invito a votare per gli argomenti che trovate più interessanti.

Blocchi fondamentali delle applicazioni distribuite. Secondo approccio

Questo è l'articolo conclusivo del ciclo sulle applicazioni reattive distribuite in Erlang/Elixir. In articolo precedente possiamo trovare le basi teoriche dell'architettura reattiva. Il secondo articolo illustra i principali modelli e meccanismi di costruzione di tali sistemi.

Oggi affronteremo le questioni relative all'evoluzione del codice e dei progetti in generale.

Organizzazione dei servizi

Nella vita reale, durante lo sviluppo di un servizio, spesso è necessario combinare diversi modelli di interazione all'interno di un unico controller. Ad esempio, il servizio users, che si occupa della gestione dei profili degli utenti del progetto, deve rispondere a richieste req-resp e comunicare aggiornamenti sui profili tramite pub-sub. Questo caso è piuttosto semplice: per il messaging c'è un unico controller che implementa la logica del servizio e pubblica aggiornamenti.

La situazione si complica quando dobbiamo implementare un servizio distribuito resistente ai guasti. Immaginiamo che i requisiti per users siano cambiati:

  1. ora il servizio deve elaborare richieste su 5 nodi del cluster,
  2. essere in grado di eseguire compiti in background,
  3. nonché gestire dinamicamente gli elenchi di iscrizione agli aggiornamenti dei profili.

Nota: Non consideriamo la questione della memorizzazione coerente e della replica dei dati. Supponiamo che queste problematiche siano state risolte in precedenza e che nel sistema esista già uno strato di archiviazione affidabile e scalabile, e che i gestori abbiano meccanismi di interazione con esso.

La descrizione formale del servizio users è diventata più complessa. Dal punto di vista del programmatore, grazie all'uso del messaging, le modifiche sono minime. Per soddisfare il primo requisito, dobbiamo configurare il bilanciamento nel punto di scambio req-resp.

La necessità di elaborare compiti in background si presenta spesso. In users possono esserci controlli sui documenti degli utenti, elaborazione di media caricati o sincronizzazione dei dati con i social network. Questi compiti devono essere distribuiti in qualche modo all'interno del cluster e monitorati nel loro espletamento. Pertanto, abbiamo due opzioni per la risoluzione: o utilizzare il modello di distribuzione dei compiti dell'articolo precedente, o, se non va bene, scrivere un pianificatore di compiti personalizzato che gestisca il pool di elaboratori nel modo necessario.

Il punto 3 richiede un'estensione del modello pub-sub. E per l'implementazione, dopo aver creato il punto di scambio pub-sub, è necessario avviare ulteriormente il controller di questo punto all'interno del nostro servizio. In questo modo, estraiamo logica di elaborazione delle iscrizioni e disiscrizioni dallo strato di messaging nella realizzazione di users.

Di conseguenza, la scomposizione del compito ha mostrato che per soddisfare i requisiti è necessario avviare 5 istanze del servizio su nodi diversi e creare un ulteriore entità: un controller pub-sub responsabile dell'iscrizione.
Per avviare 5 elaboratori non è necessario modificare il codice del servizio. L'unica azione aggiuntiva è la configurazione delle regole di bilanciamento sul punto di scambio, di cui parleremo più avanti.
È emersa anche una difficoltà aggiuntiva: il controller pub-sub e il pianificatore di compiti personalizzato devono operare in una singola istanza. Ancora una volta, il servizio messaging, come fondamentale, deve fornire un meccanismo di scelta del leader.

Scelta del leader

Nelle sistemazioni distribuite, la scelta del leader è una procedura di designazione di un unico processo responsabile della pianificazione dell'elaborazione distribuita di qualche carico.

Nei sistemi non soggetti a centralizzazione, vengono applicati algoritmi universali e algoritmi basati su consenso, ad esempio paxos o raft.
Poiché messaging è un broker e un elemento centrale, conosce tutti i controller del servizio - candidati al ruolo di leader. Messaging può designare un leader senza alcuna votazione.

Tutti i servizi, dopo l'avvio e la connessione al punto di scambio, ricevono un messaggio di sistema #'$leader'{exchange = ?EXCHANGE, pid = LeaderPid, servers = Servers}. Nel caso in cui LeaderPid corrisponde a pid del processo corrente, viene designato come leader, e l'elenco Servers include tutti i nodi e i loro parametri.
Nel momento in cui compare un nuovo nodo e si disconnette un nodo funzionante del cluster, tutti i controller del servizio ricevono #'$slave_up'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} e #'$slave_down'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} rispettivamente.

In questo modo, tutti i componenti sono a conoscenza di tutte le modifiche, e nel cluster, in ogni momento, è garantito che ci sia un solo leader.

Intermediari

Per implementare processi di elaborazione distribuiti complessi e per ottimizzare l'architettura esistente è conveniente utilizzare degli intermediari.
Per evitare di modificare il codice dei servizi e risolvere, ad esempio, compiti di elaborazione aggiuntiva, instradamento o registrazione dei messaggi, è possibile inserire un gestore proxy davanti al servizio, che eseguirà tutto il lavoro aggiuntivo.

Un esempio classico di ottimizzazione pub-sub è un'applicazione distribuita con un core aziendale che genera eventi di aggiornamento, come ad esempio il cambiamento dei prezzi sul mercato, e uno strato di accesso — N server che forniscono API websocket per i client web.
Se risolviamo la questione 'direttamente', il servizio al cliente appare nel modo seguente:

  • il cliente stabilisce connessioni con la piattaforma. Sul lato server, dove termina il traffico, viene avviato un processo che gestisce questa connessione.
  • nel contesto del processo di gestione avviene l'autenticazione e l'iscrizione agli aggiornamenti. Il processo chiama il metodo subscribe per i topic.
  • dopo la generazione di un evento nel core, esso viene recapitato ai processi che gestiscono le connessioni.

Immaginiamo di avere 50000 iscritti al topic 'news'. Gli iscritti sono distribuiti uniformemente su 5 server. Alla fine, ogni aggiornamento, arrivato al punto di scambio, verrà replicato 50000 volte: 10000 volte su ogni server, in base al numero di iscritti su di esso. Non è proprio un schema efficiente, vero?
Per migliorare la situazione, introduciamo un proxy che ha lo stesso nome del punto di scambio. Il registratore dei nomi globali deve essere in grado di restituire il processo più vicino per nome, questo è importante.

Avvieremo questo proxy sui server dello strato di accesso, e tutti i nostri processi che gestiscono l'API websocket si iscriveranno a esso, e non al punto di scambio pub-sub originale nel core. Il proxy si iscrive al core soltanto in caso di iscrizione unica e replica il messaggio ricevuto a tutti i suoi iscritti.
Alla fine, tra il core e i server di accesso verranno inoltrati 5 messaggi, invece di 50000.

Instradamento e bilanciamento

Req-Resp

Nell'attuale implementazione del messaging esistono 7 strategie per la distribuzione delle richieste:

  • default. La richiesta viene inviata a tutti i controller.
  • round-robin. Si effettua un ciclo di elaborazione e distribuzione delle richieste tra i controller.
  • consensus. I controller che gestiscono il servizio sono divisi in un leader e seguaci. Le richieste vengono inviate solo al leader.
  • consensus & round-robin. Nel gruppo c'è un leader, ma le richieste vengono distribuite tra tutti i membri.
  • sticky. Viene calcolata una funzione hash e associata a un particolare gestore. Le richieste successive con questa firma vanno a questo stesso gestore.
  • sticky-fun. Durante l'inizializzazione del punto di scambio viene fornita anche una funzione per calcolare l'hash per sticky il bilanciamento.
  • fun. È simile a sticky-fun, ma è possibile anche reindirizzarlo, rifiutarlo o pre-elaborarlo.

La strategia di distribuzione è definita durante l'inizializzazione del punto di scambio.

Oltre al bilanciamento, il messaging consente di contrassegnare le entità. Esaminiamo i tipi di tag nel sistema:

  • Tag di connessione. Permette di capire tramite quale connessione sono arrivati gli eventi. Utilizzato quando il processo del controller si collega a un punto di scambio, ma con diverse chiavi di routing.
  • Tag di servizio. Permette di raggruppare i gestori per un singolo servizio e ampliare le possibilità di routing e bilanciamento. Per il pattern req-resp, il routing è lineare. Inviamo una richiesta al punto di scambio, che poi la inoltra al servizio. Ma se dobbiamo suddividere i gestori in gruppi logici, la suddivisione avviene tramite i tag. Specificando un tag, la richiesta verrà indirizzata a un gruppo specifico di controller.
  • Tag di richiesta. Permette di distinguere le risposte. Poiché il nostro sistema è asincrono, per elaborare le risposte del servizio è necessario avere la possibilità di specificare il RequestTag durante l'invio della richiesta. Con questo possiamo capire a quale richiesta è stata data risposta.

Pub-sub

Per pub-sub è tutto un po' più semplice. Abbiamo un punto di scambio al quale vengono pubblicati i messaggi. Il punto di scambio distribuisce i messaggi tra gli abbonati, che si sono iscritti alle chiavi di routing di loro interesse (si può dire che è analogo ai temi).

Scalabilità e resilienza

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

  • I servizi si scalano aggiungendo ulteriori nodi con i gestori di questo servizio al cluster. Durante il processo di sperimentazione, è possibile scegliere la politica di bilanciamento più ottimale.
  • Il servizio di messaging, nell'ambito di un cluster separato, di solito si scala o estraendo i punti di scambio particolarmente sovraccarichi su nodi separati del cluster, oppure aggiungendo processi proxy nelle zone del cluster particolarmente sovraccariche.
  • La scalabilità dell'intero sistema come caratteristica dipende dalla flessibilità dell'architettura e dalla possibilità di unire singoli cluster in un'entità logica comune.

Il successo di un progetto dipende spesso dalla semplicità e dalla velocità di scalabilità. Il messaging nell'implementazione attuale cresce insieme all'applicazione. Anche se non abbiamo a disposizione un cluster di 50-60 macchine, possiamo ricorrere alla federazione. Purtroppo, il tema della federazione va oltre l'ambito di questo articolo.

Ridondanza

Nella discussione sul bilanciamento del carico abbiamo già parlato della riserva dei controller dei servizi. Tuttavia, anche il messaging deve essere riservato. In caso di fallimento di un nodo o di una macchina, il messaging deve ripristinarsi automaticamente e nel minor tempo possibile.

Nei miei progetti utilizzo nodi aggiuntivi che raccolgono il carico in caso di guasto. In Erlang esiste un'implementazione standard della modalità distribuita per le applicazioni OTP. La modalità distribuita effettua proprio il ripristino in caso di guasto avviando l'applicazione non funzionante su un altro nodo precedentemente avviato. Il processo è trasparente; dopo un guasto, l'applicazione si sposta automaticamente sul nodo di failover. Puoi leggere di più su questa funzionalità. qui.

Prestazioni

Proviamo a confrontare, anche se in modo approssimativo, le prestazioni di rabbitmq e del nostro messaging personalizzato.
Ho trovato risultati ufficiali dei test di rabbitmq del team openstack.

Nel punto 6.14.1.2.1.2.2 del documento originale è riportato il risultato del RPC CAST:
Blocchi fondamentali delle applicazioni distribuite. Secondo approccio

Preliminarmente, non faremo ulteriori configurazioni nel kernel del SO o nella erlang VM. Condizioni per il test:

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

Codice del test:

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.

Scenario 1: Il test viene eseguito su un laptop con un vecchio i7 mobile. Il test, il messaging e il servizio vengono eseguiti su un nodo all'interno di un контейнер Docker:

Cicli sequenziali 10000 in ~0 secondi (26987 cicli/s)
Cicli sequenziali 20000 in ~1 secondo (26915 cicli/s)
Cicli sequenziali 100000 in ~4 secondi (26957 cicli/s)
Cicli paralleli 2 100000 in ~2 secondi (44240 cicli/s)
Cicli paralleli 4 100000 in ~2 secondi (53459 cicli/s)
Cicli paralleli 10 100000 in ~2 secondi (52283 cicli/s)
Cicli paralleli 100 100000 in ~3 secondi (49317 cicli/s)

Scenario 2: 3 nodi in esecuzione su macchine diverse sotto Docker (NAT).

Cicli sequenziali 10000 in ~1 secondo (8684 cicli/s)
Cicli sequenziali 20000 in ~2 secondi (8424 cicli/s)
Cicli sequenziali 100000 in ~12 secondi (8655 cicli/s)
Cicli paralleli 2 100000 in ~7 secondi (15160 cicli/s)
Cicli paralleli 4 100000 in ~5 secondi (19133 cicli/s)
Cicli paralleli 10 100000 in ~4 secondi (24399 cicli/s)
Cicli paralleli 100 100000 in ~3 secondi (34517 cicli/s)

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

Conclusioni

Spero che questo ciclo non sembri un torrente di coscienza e che la mia esperienza porti un reale beneficio sia ai ricercatori di sistemi distribuiti che ai professionisti che sono all'inizio della loro avventura nella costruzione di architetture distribuite per i propri sistemi aziendali e guardano con interesse a Erlang/Elixir, ma dubitano se ne valga la pena...

Foto @chuttersnap

Solo gli utenti registrati possono partecipare al sondaggio. Accedi, per favore.

Quali argomenti dovrei trattare con maggiore dettaglio nell'ambito del ciclo "Esperimento VTrade"?

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

  • Libro degli ordini. Teoria e pratica dell'implementazione di un libro con raggruppamenti

  • Visualizzazione del trading: Tick, barre, risoluzioni. Come immagazzinare e come comporre

  • Back office. Pianificazione e sviluppo. Controllo del personale e investigazione di incidenti

  • API. Cerchiamo di capire quali interfacce servano e come realizzarle

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

  • Reattività nei sistemi di trading

  • Altro. Scriverò nei commenti

Hanno votato 6 utenti. 4 utenti si sono astenuti.

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