Evoluzione dell'architettura del sistema di negoziazione e compensazione della Borsa di Mosca. Parte 2

Evoluzione dell'architettura del sistema di negoziazione e compensazione della Borsa di Mosca. Parte 2

Questa è la continuazione di una lunga storia sul nostro spinoso percorso verso la creazione di un sistema potente e ad alto carico che garantisca il funzionamento dell'Exchange. La prima parte è qui: habr.com/en/post/444300

Errore misterioso

Dopo numerosi test, il sistema aggiornato di trading e compensazione è stato messo in funzione e abbiamo riscontrato un bug sul quale potremmo scrivere una storia mistica e poliziesca.

Poco dopo l'avvio sul server principale, una delle transazioni è stata elaborata con un errore. Tuttavia, tutto andava bene sul server di backup. Si è scoperto che una semplice operazione matematica di calcolo dell'esponente sul server principale ha dato un risultato negativo rispetto all'argomento reale! Abbiamo continuato la nostra ricerca e nel registro SSE2 abbiamo trovato una differenza in un bit, che è responsabile dell'arrotondamento quando si lavora con numeri in virgola mobile.

Abbiamo scritto una semplice utility di test per calcolare l'esponente con il bit di arrotondamento impostato. Si è scoperto che nella versione di RedHat Linux da noi utilizzata si verificava un bug nel funzionamento della funzione matematica quando veniva inserito il bit sfortunato. Abbiamo segnalato questo a RedHat, dopo un po' abbiamo ricevuto una patch da loro e l'abbiamo implementata. L'errore non si è più verificato, ma non era chiaro da dove provenisse questo bit? La funzione ne era responsabile fesetround dal linguaggio C. Abbiamo analizzato attentamente il nostro codice alla ricerca del presunto errore: abbiamo verificato tutte le possibili situazioni; esaminato tutte le funzioni che utilizzavano l'arrotondamento; ho provato a riprodurre una sessione fallita; utilizzato diversi compilatori con diverse opzioni; Sono state utilizzate analisi statiche e dinamiche.

Impossibile trovare la causa dell'errore.

Poi hanno iniziato a controllare l'hardware: hanno effettuato test di carico dei processori; controllato la RAM; Abbiamo anche eseguito test per lo scenario molto improbabile di un errore multi-bit in una cella. Inutilmente.

Alla fine, ci siamo basati su una teoria presa dal mondo della fisica delle alte energie: alcune particelle ad alta energia sono volate nel nostro data center, hanno perforato la parete del case, hanno colpito il processore e hanno fatto sì che il grilletto si bloccasse in quel preciso punto. Questa teoria assurda fu chiamata “neutrino”. Se siete lontani dalla fisica delle particelle: i neutrini quasi non interagiscono con il mondo esterno, e di certo non sono in grado di influenzare il funzionamento del processore.

Poiché non è stato possibile individuare la causa del guasto, per ogni evenienza il server "incriminato" è stato sospeso dal funzionamento.

Dopo un po ', abbiamo iniziato a migliorare il sistema di backup a caldo: abbiamo introdotto le cosiddette "riserve calde" (calde) - repliche asincrone. Hanno ricevuto un flusso di transazioni che potrebbero essere localizzate in diversi data center, ma Warms non ha interagito attivamente con altri server.

Evoluzione dell'architettura del sistema di negoziazione e compensazione della Borsa di Mosca. Parte 2

Perché è stato fatto questo? Se il server di backup fallisce, il server collegato a caldo diventa il nuovo backup. Cioè, dopo un guasto, il sistema non rimane con un server principale fino alla fine della sessione di negoziazione.

E quando la nuova versione del sistema è stata testata e messa in funzione, si è verificato nuovamente l'errore del bit di arrotondamento. Inoltre, con l'aumento del numero di server caldi, l'errore ha cominciato a comparire più spesso. Allo stesso tempo, il venditore non aveva nulla da dimostrare, poiché non esistevano prove concrete.

Durante la successiva analisi della situazione, è emersa la teoria secondo cui il problema potrebbe essere correlato al sistema operativo. Abbiamo scritto un semplice programma che richiama una funzione in un ciclo infinito fesetround, ricorda lo stato corrente e lo controlla durante la sospensione, e questo viene fatto in molti thread concorrenti. Dopo aver selezionato i parametri per la sospensione e il numero di thread, abbiamo iniziato a riprodurre costantemente il guasto del bit dopo circa 5 minuti di esecuzione dell'utilità. Tuttavia, il supporto Red Hat non è stato in grado di riprodurlo. I test sugli altri nostri server hanno dimostrato che solo quelli con determinati processori sono suscettibili all'errore. Allo stesso tempo, il passaggio a un nuovo kernel ha risolto il problema. Alla fine abbiamo semplicemente sostituito il sistema operativo e la vera causa del bug non è stata chiara.

E all’improvviso l’anno scorso è uscito un articolo su Habré”Come ho trovato un bug nei processori Intel Skylake" La situazione descritta era molto simile alla nostra, ma l'autore ha approfondito l'indagine e ha avanzato la teoria secondo cui l'errore era nel microcodice. E quando i kernel Linux vengono aggiornati, i produttori aggiornano anche il microcodice.

Ulteriore sviluppo del sistema

Sebbene ci siamo sbarazzati dell'errore, questa storia ci ha costretto a riconsiderare l'architettura del sistema. Dopotutto, non eravamo protetti dal ripetersi di tali bug.

I seguenti principi hanno costituito la base per i successivi miglioramenti al sistema di prenotazione:

  • Non puoi fidarti di nessuno. I server potrebbero non funzionare correttamente.
  • Riserva di maggioranza.
  • Garantire il consenso. Come logica aggiunta alla riserva di maggioranza.
  • Sono possibili doppi fallimenti.
  • Vitalità. Il nuovo schema hot standby non dovrebbe essere peggiore del precedente. Il trading dovrebbe procedere ininterrottamente fino all'ultimo server.
  • Leggero aumento della latenza. Qualsiasi tempo di inattività comporta enormi perdite finanziarie.
  • Interazione di rete minima per mantenere la latenza quanto più bassa possibile.
  • Selezione di un nuovo server master in pochi secondi.

Nessuna delle soluzioni disponibili sul mercato era adatta a noi e il protocollo Raft era ancora agli inizi, quindi abbiamo creato la nostra soluzione.

Evoluzione dell'architettura del sistema di negoziazione e compensazione della Borsa di Mosca. Parte 2

Rete

Oltre al sistema di prenotazione, abbiamo iniziato a modernizzare l'interazione della rete. Il sottosistema I/O era costituito da molti processi, che avevano l'impatto peggiore su jitter e latenza. Con centinaia di processi che gestiscono connessioni TCP, eravamo costretti a passare costantemente da uno all'altro e, su scala di microsecondi, questa è un'operazione piuttosto dispendiosa in termini di tempo. Ma la parte peggiore è che quando un processo riceve un pacchetto da elaborare, lo invia a una coda SystemV e quindi attende un evento da un'altra coda SystemV. Tuttavia, quando è presente un numero elevato di nodi, l'arrivo di un nuovo pacchetto TCP in un processo e la ricezione dei dati in coda in un altro rappresentano due eventi concorrenti per il sistema operativo. In questo caso, se non sono disponibili processori fisici per entrambe le attività, uno verrà elaborato e il secondo verrà inserito in una coda di attesa. È impossibile prevederne le conseguenze.

In tali situazioni, è possibile utilizzare il controllo dinamico della priorità del processo, ma ciò richiederà l'uso di chiamate di sistema ad alta intensità di risorse. Di conseguenza, siamo passati a un thread utilizzando il classico epoll, ciò ha notevolmente aumentato la velocità e ridotto i tempi di elaborazione delle transazioni. Ci siamo anche sbarazzati dei processi di comunicazione di rete separati e della comunicazione tramite SystemV, abbiamo ridotto significativamente il numero di chiamate di sistema e abbiamo iniziato a controllare le priorità delle operazioni. Solo sul sottosistema I/O è stato possibile risparmiare, a seconda dello scenario, circa 8-17 microsecondi. Questo schema a thread singolo è stato utilizzato senza modifiche da allora; un thread epoll con un margine è sufficiente per servire tutte le connessioni.

Transazione in corso

Il carico crescente sul nostro sistema ha richiesto l'aggiornamento di quasi tutti i suoi componenti. Ma, sfortunatamente, la stagnazione nella crescita della velocità di clock dei processori negli ultimi anni non ha più consentito di scalare i processi in modo diretto. Pertanto, abbiamo deciso di dividere il processo Engine in tre livelli, di cui il più impegnativo è il sistema di controllo del rischio, che valuta la disponibilità di fondi nei conti e crea le transazioni stesse. Ma il denaro può essere in valute diverse ed era necessario capire su quale base suddividere l'elaborazione delle richieste.

La soluzione logica è dividerlo per valuta: un server scambia in dollari, un altro in sterline e un terzo in euro. Ma se con tale schema vengono inviate due transazioni per l'acquisto di valute diverse, sorgerà il problema della desincronizzazione del portafoglio. Ma la sincronizzazione è difficile e costosa. Pertanto, sarebbe corretto suddividere separatamente per portafogli e separatamente per strumenti. A proposito, la maggior parte degli scambi occidentali non ha il compito di controllare i rischi in modo così accurato come lo facciamo noi, quindi molto spesso questo viene fatto offline. Avevamo bisogno di implementare la verifica online.

Spieghiamo con un esempio. Un trader vuole acquistare 30 dollari e viene inviata la richiesta per convalidare la transazione: controlliamo se questo trader è autorizzato a questa modalità di trading e se possiede i diritti necessari. Se tutto è in ordine, la richiesta va al sistema di verifica dei rischi, ovvero per verificare la sufficienza dei fondi per concludere una transazione. C'è una nota che l'importo richiesto è attualmente bloccato. La richiesta viene quindi inoltrata al sistema di negoziazione, che approva o disapprova la transazione. Supponiamo che la transazione sia approvata, quindi il sistema di verifica del rischio segnala che il denaro è sbloccato e i rubli si trasformano in dollari.

In generale, il sistema di controllo del rischio contiene algoritmi complessi ed esegue una grande quantità di calcoli ad alta intensità di risorse e non controlla semplicemente il “saldo del conto”, come potrebbe sembrare a prima vista.

Quando abbiamo iniziato a dividere il processo Engine in livelli, abbiamo riscontrato un problema: il codice disponibile in quel momento utilizzava attivamente lo stesso array di dati nelle fasi di validazione e verifica, il che richiedeva la riscrittura dell'intera codebase. Di conseguenza, abbiamo preso in prestito una tecnica per elaborare le istruzioni dai moderni processori: ognuna di esse è divisa in piccole fasi e diverse azioni vengono eseguite in parallelo in un ciclo.

Evoluzione dell'architettura del sistema di negoziazione e compensazione della Borsa di Mosca. Parte 2

Dopo un piccolo adattamento del codice, abbiamo creato una pipeline per l'elaborazione parallela delle transazioni, in cui la transazione è stata divisa in 4 fasi della pipeline: interazione di rete, validazione, esecuzione e pubblicazione del risultato

Evoluzione dell'architettura del sistema di negoziazione e compensazione della Borsa di Mosca. Parte 2

Diamo un'occhiata a un esempio. Disponiamo di due sistemi di elaborazione, seriale e parallelo. La prima transazione arriva e viene inviata per la convalida in entrambi i sistemi. La seconda transazione arriva immediatamente: in un sistema parallelo viene immediatamente messa in funzione, mentre in un sistema sequenziale viene messa in coda in attesa che la prima transazione passi attraverso la fase di elaborazione corrente. Cioè, il vantaggio principale dell'elaborazione della pipeline è che elaboriamo la coda delle transazioni più velocemente.

È così che abbiamo ideato il sistema ASTS+.

È vero, anche con i trasportatori non tutto è così fluido. Diciamo che abbiamo una transazione che influenza gli array di dati in una transazione vicina; questa è una situazione tipica per uno scambio. Una transazione di questo tipo non può essere eseguita in una pipeline perché potrebbe influenzare altri. Questa situazione è chiamata data Hazard e tali transazioni vengono semplicemente elaborate separatamente: quando le transazioni “veloci” nella coda si esauriscono, la pipeline si ferma, il sistema elabora la transazione “lenta” e quindi riavvia la pipeline. Fortunatamente, la percentuale di tali transazioni nel flusso complessivo è molto piccola, quindi la pipeline si interrompe così raramente da non influire sulle prestazioni complessive.

Evoluzione dell'architettura del sistema di negoziazione e compensazione della Borsa di Mosca. Parte 2

Quindi abbiamo iniziato a risolvere il problema della sincronizzazione di tre thread di esecuzione. Il risultato è stato un sistema basato su un buffer ad anello con celle di dimensione fissa. In questo sistema tutto è soggetto alla velocità di elaborazione; i dati non vengono copiati.

  • Tutti i pacchetti di rete in entrata entrano nella fase di allocazione.
  • Li inseriamo in un array e li contrassegniamo come disponibili per la fase n. 1.
  • La seconda transazione è arrivata, è nuovamente disponibile per la fase n. 1.
  • Il primo thread di elaborazione vede le transazioni disponibili, le elabora e le sposta alla fase successiva del secondo thread di elaborazione.
  • Quindi elabora la prima transazione e contrassegna la cella corrispondente deleted - è ora disponibile per un nuovo utilizzo.

L'intera coda viene elaborata in questo modo.

Evoluzione dell'architettura del sistema di negoziazione e compensazione della Borsa di Mosca. Parte 2

L'elaborazione di ciascuna fase richiede unità o decine di microsecondi. E se utilizziamo schemi di sincronizzazione del sistema operativo standard, perderemo più tempo nella sincronizzazione stessa. Ecco perché abbiamo iniziato a utilizzare lo spinlock. Tuttavia, questa è una pessima forma in un sistema in tempo reale e RedHat sconsiglia vivamente di farlo, quindi applichiamo uno spinlock per 100 ms, quindi passiamo alla modalità semaforo per eliminare la possibilità di un deadlock.

Di conseguenza, abbiamo raggiunto una performance di circa 8 milioni di transazioni al secondo. E letteralmente due mesi dopo Articolo riguardo a LMAX Disruptor abbiamo visto la descrizione di un circuito con la stessa funzionalità.

Evoluzione dell'architettura del sistema di negoziazione e compensazione della Borsa di Mosca. Parte 2

Ora potrebbero esserci diversi thread di esecuzione in una fase. Tutte le transazioni sono state elaborate una per una, nell'ordine in cui sono state ricevute. Di conseguenza, le prestazioni di picco sono aumentate da 18mila a 50mila transazioni al secondo.

Sistema di gestione del rischio di cambio

Non c'è limite alla perfezione e presto abbiamo ricominciato la modernizzazione: nell'ambito di ASTS+, abbiamo iniziato a spostare i sistemi di gestione del rischio e delle operazioni di regolamento in componenti autonomi. Abbiamo sviluppato un'architettura moderna e flessibile e un nuovo modello di rischio gerarchico e abbiamo cercato di utilizzare la classe ove possibile fixed_point invece di double.

Ma subito è sorto un problema: come sincronizzare tutta la logica aziendale che funziona da tanti anni e trasferirla nel nuovo sistema? Di conseguenza, la prima versione del prototipo del nuovo sistema ha dovuto essere abbandonata. La seconda versione, attualmente in produzione, si basa sullo stesso codice, che funziona sia nella parte commerciale che in quella di rischio. Durante lo sviluppo, la cosa più difficile da fare è stata git merge tra due versioni. Il nostro collega Evgeniy Mazurenok ha eseguito questa operazione ogni settimana e ogni volta ha imprecato a lungo.

Quando abbiamo scelto un nuovo sistema, abbiamo dovuto risolvere immediatamente il problema dell'interazione. Quando si sceglie un bus dati, è necessario garantire un jitter stabile e una latenza minima. La rete InfiniBand RDMA era la più adatta a questo scopo: il tempo di elaborazione medio è 4 volte inferiore rispetto alle reti Ethernet 10 G. Ma ciò che ci ha davvero affascinato è stata la differenza nei percentili: 99 e 99,9.

Naturalmente, InfiniBand ha le sue sfide. Innanzitutto, un'API diversa: ibverbs invece dei socket. In secondo luogo, non esistono quasi soluzioni di messaggistica open source ampiamente disponibili. Abbiamo provato a realizzare il nostro prototipo, ma si è rivelato molto difficile, quindi abbiamo scelto una soluzione commerciale: Confinity Low Latency Messaging (ex IBM MQ LLM).

Poi è sorto il compito di ripartire adeguatamente il sistema di rischio. Se rimuovi semplicemente il Risk Engine e non crei un nodo intermedio, le transazioni da due fonti possono essere miste.

Evoluzione dell'architettura del sistema di negoziazione e compensazione della Borsa di Mosca. Parte 2

Le cosiddette soluzioni Ultra Low Latency hanno una modalità di riordino: le transazioni da due fonti possono essere organizzate nell'ordine richiesto al momento della ricezione; ciò viene implementato utilizzando un canale separato per lo scambio di informazioni sull'ordine. Ma non utilizziamo ancora questa modalità: complica l'intero processo e in alcune soluzioni non è affatto supportata. Inoltre, a ciascuna transazione dovrebbero essere assegnati timestamp corrispondenti e nel nostro schema questo meccanismo è molto difficile da implementare correttamente. Abbiamo quindi utilizzato lo schema classico con un message broker, cioè con un dispatcher che distribuisce i messaggi tra i Risk Engine.

Il secondo problema riguardava l’accesso del client: se sono presenti più Risk Gateway, il client dovrà connettersi a ciascuno di essi e ciò richiederà modifiche al livello client. Volevamo evitare questo problema in questa fase, quindi l'attuale progettazione del Risk Gateway elabora l'intero flusso di dati. Ciò limita notevolmente il throughput massimo, ma semplifica notevolmente l'integrazione del sistema.

duplicazione

Il nostro sistema non dovrebbe avere un singolo punto di errore, ovvero tutti i componenti devono essere duplicati, incluso il broker di messaggi. Abbiamo risolto questo problema utilizzando il sistema CLLM: contiene un cluster RCMS in cui due dispatcher possono lavorare in modalità master-slave e quando uno fallisce, il sistema passa automaticamente all'altro.

Lavorare con un data center di backup

InfiniBand è ottimizzato per il funzionamento come rete locale, ovvero per il collegamento di apparecchiature montate su rack, e una rete InfiniBand non può essere posata tra due data center distribuiti geograficamente. Pertanto, abbiamo implementato un bridge/dispatcher che si collega all'archivio dei messaggi tramite normali reti Ethernet e inoltra tutte le transazioni a una seconda rete IB. Quando dobbiamo migrare da un data center, possiamo scegliere con quale data center lavorare adesso.

Risultati di

Tutto quanto sopra non è stato fatto in una volta; ci sono volute diverse iterazioni per sviluppare una nuova architettura. Abbiamo creato il prototipo in un mese, ma ci sono voluti più di due anni per renderlo funzionante. Abbiamo cercato di raggiungere il miglior compromesso tra l'aumento del tempo di elaborazione delle transazioni e l'aumento dell'affidabilità del sistema.

Poiché il sistema è stato ampiamente aggiornato, abbiamo implementato il recupero dei dati da due fonti indipendenti. Se per qualche motivo l'archivio dei messaggi non funziona correttamente, è possibile acquisire il registro delle transazioni da una seconda fonte, ovvero dal Risk Engine. Questo principio è rispettato in tutto il sistema.

Tra le altre cose, siamo riusciti a preservare l'API client in modo che né i broker né nessun altro richiedessero una rielaborazione significativa per la nuova architettura. Abbiamo dovuto modificare alcune interfacce, ma non è stato necessario apportare modifiche significative al modello operativo.

Abbiamo chiamato l'attuale versione della nostra piattaforma Rebus, come abbreviazione delle due innovazioni più evidenti nell'architettura, Risk Engine e BUS.

Evoluzione dell'architettura del sistema di negoziazione e compensazione della Borsa di Mosca. Parte 2

Inizialmente volevamo destinare solo la parte di compensazione, ma il risultato è stato un enorme sistema distribuito. I clienti ora possono interagire con il Trade Gateway, il Clearing Gateway o entrambi.

Cosa abbiamo ottenuto alla fine:

Evoluzione dell'architettura del sistema di negoziazione e compensazione della Borsa di Mosca. Parte 2

Ridotto il livello di latenza. Con un volume di transazioni ridotto, il sistema funziona come la versione precedente, ma allo stesso tempo può sopportare un carico molto maggiore.

Le prestazioni di picco sono aumentate da 50mila a 180mila transazioni al secondo. Un ulteriore aumento è ostacolato dal solo flusso di abbinamento degli ordini.

Esistono due modi per migliorare ulteriormente: parallelizzare la corrispondenza e modificare il modo in cui funziona con Gateway. Ora tutti i gateway funzionano secondo uno schema di replica che, sotto tale carico, cessa di funzionare normalmente.

Infine, posso dare qualche consiglio a chi sta finalizzando i sistemi aziendali:

  • Preparati al peggio in ogni momento. I problemi sorgono sempre inaspettatamente.
  • Di solito è impossibile rifare rapidamente l’architettura. Soprattutto se è necessario ottenere la massima affidabilità su più indicatori. Più nodi, più risorse necessarie per il supporto.
  • Tutte le soluzioni personalizzate e proprietarie richiederanno risorse aggiuntive per la ricerca, il supporto e la manutenzione.
  • Non rimandare la risoluzione dei problemi di affidabilità e ripristino del sistema dopo i guasti; tenerne conto nella fase iniziale di progettazione.

Fonte: habr.com

Aggiungi un commento