De evolutie van de architectuur van het handels- en clearingsysteem van de Moscow Exchange. Deel 2

De evolutie van de architectuur van het handels- en clearingsysteem van de Moscow Exchange. Deel 2

Dit is een voortzetting van een lang verhaal over ons netelige pad naar het creëren van een krachtig systeem met hoge belasting dat de werking van de Exchange verzekert. Het eerste deel staat hier: habr.com/en/post/444300

Mysterieuze fout

Na talloze tests werd het bijgewerkte handels- en clearingsysteem in gebruik genomen en kwamen we een bug tegen waarover we een detective-mystiek verhaal konden schrijven.

Kort na het opstarten op de hoofdserver werd een van de transacties met een fout verwerkt. Op de back-upserver was alles echter in orde. Het bleek dat een eenvoudige wiskundige bewerking van het berekenen van de exponent op de hoofdserver een negatief resultaat opleverde van het echte argument! We vervolgden ons onderzoek en in het SSE2-register vonden we een verschil in één bit, dat verantwoordelijk is voor de afronding bij het werken met drijvende-kommagetallen.

We hebben een eenvoudig testhulpprogramma geschreven om de exponent te berekenen met de afrondingsbit ingesteld. Het bleek dat er in de versie van RedHat Linux die we gebruikten een fout zat in het werken met de wiskundige functie toen het noodlottige bit werd ingevoegd. Wij hebben dit gemeld bij RedHat, na een tijdje hebben wij een patch van hen ontvangen en deze uitgerold. De fout trad niet meer op, maar het was onduidelijk waar dit bit überhaupt vandaan kwam? De functie was er verantwoordelijk voor fesetround uit de taal C. We hebben onze code zorgvuldig geanalyseerd op zoek naar de vermeende fout: we hebben alle mogelijke situaties gecontroleerd; gekeken naar alle functies die gebruik maakten van afronding; geprobeerd een mislukte sessie te reproduceren; gebruikte verschillende compilers met verschillende opties; Er werd gebruik gemaakt van statische en dynamische analyses.

De oorzaak van de fout kon niet worden gevonden.

Vervolgens begonnen ze de hardware te controleren: ze voerden belastingtests uit van de processors; controleerde het RAM-geheugen; We hebben zelfs tests uitgevoerd voor het zeer onwaarschijnlijke scenario van een meerbitsfout in één cel. Het mocht niet baten.

Uiteindelijk kwamen we uit op een theorie uit de wereld van de hoge-energiefysica: een hoogenergetisch deeltje vloog ons datacenter binnen, doorboorde de muur van de behuizing, raakte de processor en zorgde ervoor dat de trekkergrendel precies in dat stukje bleef steken. Deze absurde theorie werd het ‘neutrino’ genoemd. Als je ver van de deeltjesfysica afstaat: neutrino's hebben vrijwel geen interactie met de buitenwereld en kunnen zeker geen invloed uitoefenen op de werking van de processor.

Omdat het niet mogelijk was de oorzaak van de storing te achterhalen, werd de “overtredende” server voor de zekerheid buiten gebruik gesteld.

Na enige tijd begonnen we het hot backup-systeem te verbeteren: we introduceerden zogenaamde "warme reserves" (warm) - asynchrone replica's. Ze ontvingen een stroom transacties die zich in verschillende datacenters konden bevinden, maar de Warms communiceerden niet actief met andere servers.

De evolutie van de architectuur van het handels- en clearingsysteem van de Moscow Exchange. Deel 2

Waarom werd dit gedaan? Als de back-upserver uitvalt, wordt de warme verbinding met de hoofdserver de nieuwe back-up. Dat wil zeggen dat het systeem na een storing niet bij één hoofdserver blijft tot het einde van de handelssessie.

En toen de nieuwe versie van het systeem werd getest en in gebruik werd genomen, trad de afrondingsbitfout opnieuw op. Bovendien begon de fout met de toename van het aantal warme servers vaker voor te komen. Tegelijkertijd had de verkoper niets aan te tonen, aangezien er geen concreet bewijs was.

Tijdens de volgende analyse van de situatie ontstond er een theorie dat het probleem verband zou kunnen houden met het besturingssysteem. We hebben een eenvoudig programma geschreven dat een functie in een eindeloze lus aanroept fesetround, onthoudt de huidige status en controleert deze tijdens de slaapstand, en dit gebeurt in veel concurrerende threads. Nadat we de parameters voor de slaapstand en het aantal threads hadden geselecteerd, begonnen we de bitfout consequent te reproduceren na ongeveer 5 minuten nadat we het hulpprogramma hadden uitgevoerd. Red Hat-ondersteuning kon het echter niet reproduceren. Uit tests van onze andere servers is gebleken dat alleen servers met bepaalde processors gevoelig zijn voor de fout. Tegelijkertijd loste het overschakelen naar een nieuwe kernel het probleem op. Uiteindelijk hebben we eenvoudigweg het besturingssysteem vervangen en bleef de ware oorzaak van de bug onduidelijk.

En plotseling verscheen er vorig jaar een artikel over Habré “Hoe ik een bug vond in Intel Skylake-processors" De daarin beschreven situatie leek sterk op de onze, maar de auteur ging verder met het onderzoek en bracht een theorie naar voren dat de fout in de microcode zat. En wanneer Linux-kernels worden bijgewerkt, werken fabrikanten ook de microcode bij.

Verdere ontwikkeling van het systeem

Hoewel we de fout hebben verholpen, dwong dit verhaal ons om de systeemarchitectuur te heroverwegen. We waren tenslotte niet beschermd tegen de herhaling van dergelijke bugs.

De volgende principes vormden de basis voor de volgende verbeteringen aan het reserveringssysteem:

  • Je kunt niemand vertrouwen. Servers werken mogelijk niet goed.
  • Voorbehoud van meerderheid.
  • Zorgen voor consensus. Als logische aanvulling op meerderheidsvoorbehoud.
  • Dubbele mislukkingen zijn mogelijk.
  • Vitaliteit. Het nieuwe hot standby-schema zou niet slechter moeten zijn dan het vorige. De handel moet ononderbroken doorgaan tot de laatste server.
  • Lichte toename van de latentie. Elke stilstand brengt enorme financiële verliezen met zich mee.
  • Minimale netwerkinteractie om de latentie zo laag mogelijk te houden.
  • Binnen enkele seconden een nieuwe masterserver selecteren.

Geen van de op de markt beschikbare oplossingen beviel ons, en het Raft-protocol stond nog in de kinderschoenen, dus creëerden we onze eigen oplossing.

De evolutie van de architectuur van het handels- en clearingsysteem van de Moscow Exchange. Deel 2

Netwerken

Naast het reserveringssysteem zijn we begonnen met het moderniseren van de netwerkinteractie. Het I/O-subsysteem bestond uit veel processen, die de grootste impact hadden op jitter en latentie. Omdat honderden processen TCP-verbindingen afhandelden, waren we gedwongen voortdurend tussen deze processen te schakelen, en op microsecondenschaal is dit een nogal tijdrovende operatie. Maar het ergste is dat wanneer een proces een pakket ontving voor verwerking, het dit naar de ene SystemV-wachtrij stuurde en vervolgens wachtte op een gebeurtenis uit een andere SystemV-wachtrij. Wanneer er echter een groot aantal knooppunten zijn, vertegenwoordigen de aankomst van een nieuw TCP-pakket in het ene proces en de ontvangst van gegevens in de wachtrij in een ander proces twee concurrerende gebeurtenissen voor het besturingssysteem. Als er in dit geval geen fysieke processors beschikbaar zijn voor beide taken, wordt er één verwerkt en wordt de tweede in een wachtrij geplaatst. Het is onmogelijk om de gevolgen te voorspellen.

In dergelijke situaties kan dynamische procesprioriteitscontrole worden gebruikt, maar hiervoor zijn systeemaanroepen nodig die veel hulpbronnen vergen. Als gevolg hiervan zijn we overgestapt op één thread met behulp van klassieke epoll, dit verhoogde de snelheid aanzienlijk en verkortte de verwerkingstijd van transacties. We hebben ook afzonderlijke netwerkcommunicatieprocessen en communicatie via SystemV afgeschaft, het aantal systeemoproepen aanzienlijk verminderd en de prioriteiten van de activiteiten onder controle gekregen. Alleen al op het I/O-subsysteem was het mogelijk om ongeveer 8-17 microseconden te besparen, afhankelijk van het scenario. Dit single-threaded schema is sindsdien onveranderd gebruikt; één epoll-thread met een marge is voldoende om alle verbindingen te bedienen.

Transactieverwerking

De toenemende belasting van ons systeem vereiste een upgrade van bijna alle componenten. Maar helaas heeft de stagnatie in de groei van de kloksnelheden van processors de afgelopen jaren het niet langer mogelijk gemaakt om processen frontaal op te schalen. Daarom hebben we besloten om het Engine-proces in drie niveaus te verdelen, waarbij het drukste niveau het risicocontrolesysteem is, dat de beschikbaarheid van geld op rekeningen evalueert en de transacties zelf creëert. Maar geld kan in verschillende valuta's zijn, en het was noodzakelijk om uit te zoeken op welke basis de verwerking van verzoeken verdeeld moest worden.

De logische oplossing is om het te verdelen naar valuta: de ene server handelt in dollars, een andere in ponden en een derde in euro's. Maar als met een dergelijk schema twee transacties worden verzonden om verschillende valuta te kopen, zal het probleem van desynchronisatie van de portemonnee ontstaan. Maar synchronisatie is moeilijk en duur. Daarom zou het juist zijn om afzonderlijk per portemonnee en afzonderlijk per instrumenten te delen. Overigens hebben de meeste westerse beurzen niet de taak om risico’s zo acuut te controleren als wij, dus gebeurt dit meestal offline. We moesten online verificatie implementeren.

Laten we het uitleggen met een voorbeeld. Een handelaar wil €30 kopen en het verzoek gaat naar transactievalidatie: we controleren of deze handelaar toegang heeft tot deze handelsmodus en of hij over de benodigde rechten beschikt. Als alles in orde is, gaat het verzoek naar het risicoverificatiesysteem, d.w.z. om te controleren of er voldoende middelen zijn om een ​​transactie af te ronden. Er staat vermeld dat het benodigde bedrag momenteel geblokkeerd is. Het verzoek wordt vervolgens doorgestuurd naar het handelssysteem, dat de transactie goedkeurt of afkeurt. Laten we zeggen dat de transactie is goedgekeurd, waarna het risicoverificatiesysteem aangeeft dat het geld is gedeblokkeerd en de roebels in dollars veranderen.

Over het algemeen bevat het risicocontrolesysteem complexe algoritmen en voert het een groot aantal zeer resource-intensieve berekeningen uit, en controleert het niet simpelweg het ‘rekeningsaldo’, zoals het op het eerste gezicht lijkt.

Toen we het Engine-proces in niveaus begonnen te verdelen, kwamen we een probleem tegen: de code die op dat moment beschikbaar was, gebruikte actief dezelfde reeks gegevens tijdens de validatie- en verificatiefasen, waardoor de hele codebasis moest worden herschreven. Als gevolg hiervan hebben we een techniek voor het verwerken van instructies van moderne processors geleend: elk ervan is verdeeld in kleine fasen en verschillende acties worden parallel in één cyclus uitgevoerd.

De evolutie van de architectuur van het handels- en clearingsysteem van de Moscow Exchange. Deel 2

Na een kleine aanpassing van de code creëerden we een pijplijn voor parallelle transactieverwerking, waarbij de transactie werd opgedeeld in 4 fasen van de pijplijn: netwerkinteractie, validatie, uitvoering en publicatie van het resultaat

De evolutie van de architectuur van het handels- en clearingsysteem van de Moscow Exchange. Deel 2

Laten we eens kijken naar een voorbeeld. We hebben twee verwerkingssystemen, serieel en parallel. De eerste transactie arriveert en wordt ter validatie in beide systemen verzonden. De tweede transactie arriveert onmiddellijk: in een parallel systeem wordt deze onmiddellijk aan het werk gezet, en in een sequentieel systeem wordt deze in een wachtrij geplaatst, wachtend tot de eerste transactie de huidige verwerkingsfase heeft doorlopen. Dat wil zeggen: het belangrijkste voordeel van pijplijnverwerking is dat we de transactiewachtrij sneller verwerken.

Zo kwamen we op het ASTS+ systeem.

Toegegeven, ook met transportbanden verloopt niet alles zo soepel. Laten we zeggen dat we een transactie hebben die data-arrays in een aangrenzende transactie beïnvloedt; dit is een typische situatie voor een uitwisseling. Een dergelijke transactie kan niet in een pijplijn worden uitgevoerd, omdat deze gevolgen voor anderen kan hebben. Deze situatie wordt data-gevaar genoemd en dergelijke transacties worden eenvoudigweg afzonderlijk verwerkt: wanneer de ‘snelle’ transacties in de wachtrij opraken, stopt de pijplijn, verwerkt het systeem de ‘langzame’ transactie en start de pijplijn vervolgens opnieuw. Gelukkig is het aandeel van dergelijke transacties in de totale stroom zeer klein, zodat de pijplijn zo zelden stopt dat dit geen invloed heeft op de algehele prestaties.

De evolutie van de architectuur van het handels- en clearingsysteem van de Moscow Exchange. Deel 2

Toen begonnen we het probleem van het synchroniseren van drie uitvoeringsdraden op te lossen. Het resultaat was een systeem gebaseerd op een ringbuffer met cellen van vaste grootte. In dit systeem is alles afhankelijk van de verwerkingssnelheid; gegevens worden niet gekopieerd.

  • Alle binnenkomende netwerkpakketten komen in de toewijzingsfase.
  • We plaatsen ze in een array en markeren ze als beschikbaar voor fase #1.
  • De tweede transactie is gearriveerd, deze is weer beschikbaar voor fase nr. 1.
  • De eerste verwerkingsthread ziet de beschikbare transacties, verwerkt deze en verplaatst ze naar de volgende fase van de tweede verwerkingsthread.
  • Vervolgens verwerkt het de eerste transactie en markeert het de corresponderende cel deleted — het is nu beschikbaar voor nieuw gebruik.

Op deze manier wordt de gehele wachtrij verwerkt.

De evolutie van de architectuur van het handels- en clearingsysteem van de Moscow Exchange. Deel 2

De verwerking van elke fase duurt eenheden of tientallen microseconden. En als we standaard OS-synchronisatieschema's gebruiken, zullen we meer tijd verliezen aan de synchronisatie zelf. Daarom zijn we spinlock gaan gebruiken. Dit is echter een zeer slechte vorm in een real-time systeem, en RedHat raadt dit ten strengste af, dus passen we een spinlock toe gedurende 100 ms, en schakelen dan over naar de semafoormodus om de mogelijkheid van een impasse te elimineren.

Hierdoor behaalden we een prestatie van ongeveer 8 miljoen transacties per seconde. En letterlijk twee maanden later статье over LMAX Disruptor zagen we een beschrijving van een circuit met dezelfde functionaliteit.

De evolutie van de architectuur van het handels- en clearingsysteem van de Moscow Exchange. Deel 2

Nu kunnen er in één fase meerdere uitvoeringsdraden zijn. Alle transacties werden één voor één verwerkt, in de volgorde waarin ze werden ontvangen. Als gevolg hiervan namen de piekprestaties toe van 18 naar 50 transacties per seconde.

Beursrisicobeheersysteem

Er zijn geen grenzen aan perfectie, en al snel zijn we opnieuw begonnen met moderniseren: binnen het raamwerk van ASTS+ zijn we begonnen met het verplaatsen van systemen voor risicobeheer en afwikkelingsoperaties naar autonome componenten. We ontwikkelden een flexibele moderne architectuur en een nieuw hiërarchisch risicomodel, en probeerden waar mogelijk de klasse te gebruiken fixed_point in plaats van double.

Maar er ontstond meteen een probleem: hoe konden we alle bedrijfslogica die al jaren werkte, synchroniseren en overbrengen naar het nieuwe systeem? Als gevolg hiervan moest de eerste versie van het prototype van het nieuwe systeem worden verlaten. De tweede versie, die momenteel in productie is, is gebaseerd op dezelfde code, die zowel in het handels- als in het risicogedeelte werkt. Tijdens de ontwikkeling was het moeilijkste om te doen het samenvoegen van twee versies. Onze collega Evgeniy Mazurenok voerde deze operatie elke week uit en elke keer vloekte hij heel lang.

Bij de keuze voor een nieuw systeem moesten we meteen het probleem van de interactie oplossen. Bij het kiezen van een databus was het noodzakelijk om stabiele jitter en minimale latentie te garanderen. Het InfiniBand RDMA-netwerk was hiervoor het meest geschikt: de gemiddelde verwerkingstijd is 4 keer korter dan in 10G Ethernet-netwerken. Maar wat ons echt boeide was het verschil in percentielen: 99 en 99,9.

Natuurlijk heeft InfiniBand zijn uitdagingen. Ten eerste een andere API: ibverbs in plaats van sockets. Ten tweede zijn er vrijwel geen algemeen beschikbare opensource-berichtenoplossingen. We probeerden ons eigen prototype te maken, maar dat bleek erg moeilijk, dus kozen we voor een commerciële oplossing: Confinity Low Latency Messaging (voorheen IBM MQ LLM).

Toen ontstond de taak om het risicosysteem goed te verdelen. Als u eenvoudigweg de Risk Engine verwijdert en geen tussenknooppunt maakt, kunnen transacties uit twee bronnen worden gemengd.

De evolutie van de architectuur van het handels- en clearingsysteem van de Moscow Exchange. Deel 2

De zogenaamde Ultra Low Latency-oplossingen kennen een nabestelmodus: transacties uit twee bronnen kunnen bij ontvangst in de gewenste volgorde worden gerangschikt; dit gebeurt via een apart kanaal voor het uitwisselen van informatie over de bestelling. Maar deze modus gebruiken we nog niet: het compliceert het hele proces en wordt bij een aantal oplossingen helemaal niet ondersteund. Bovendien zou aan elke transactie een overeenkomstige tijdstempel moeten worden toegewezen, en in ons systeem is dit mechanisme zeer moeilijk correct te implementeren. Daarom hebben we het klassieke schema gebruikt met een berichtenmakelaar, dat wil zeggen met een coördinator die berichten verspreidt tussen de Risk Engine.

Het tweede probleem had te maken met clienttoegang: als er meerdere Risk Gateways zijn, moet de client verbinding maken met elk van deze, en dit vereist wijzigingen in de clientlaag. We wilden hier in dit stadium vanaf komen, dus verwerkt het huidige Risk Gateway-ontwerp de volledige datastroom. Dit beperkt de maximale doorvoer aanzienlijk, maar vereenvoudigt de systeemintegratie aanzienlijk.

Duplicatie

Ons systeem mag geen enkel storingspunt hebben, dat wil zeggen dat alle componenten moeten worden gedupliceerd, inclusief de berichtenmakelaar. Dit probleem hebben we opgelost met het CLLM-systeem: het bevat een RCMS-cluster waarin twee coördinatoren in master-slave-modus kunnen werken, en als er één uitvalt, schakelt het systeem automatisch over naar de andere.

Werken met een back-up datacenter

InfiniBand is geoptimaliseerd voor gebruik als lokaal netwerk, dat wil zeggen voor het verbinden van in een rek gemonteerde apparatuur, en een InfiniBand-netwerk kan niet tussen twee geografisch verspreide datacenters worden gelegd. Daarom hebben we een bridge/dispatcher geïmplementeerd, die via reguliere Ethernet-netwerken verbinding maakt met de berichtenopslag en alle transacties doorstuurt naar een tweede IB-netwerk. Wanneer we vanuit een datacenter moeten migreren, kunnen we kiezen met welk datacenter we nu gaan werken.

Resultaten van

Al het bovenstaande werd niet in één keer gedaan; er waren verschillende iteraties nodig om een ​​nieuwe architectuur te ontwikkelen. We creëerden het prototype in een maand, maar het duurde meer dan twee jaar om het werkend te krijgen. We hebben geprobeerd het beste compromis te bereiken tussen het vergroten van de transactieverwerkingstijd en het vergroten van de systeembetrouwbaarheid.

Omdat het systeem zwaar werd bijgewerkt, hebben we gegevensherstel uit twee onafhankelijke bronnen geïmplementeerd. Als het berichtenarchief om de een of andere reden niet correct functioneert, kunt u het transactielogboek van een tweede bron halen: van de Risk Engine. Dit principe wordt in het hele systeem nageleefd.

We konden onder andere de client-API behouden, zodat noch makelaars, noch iemand anders aanzienlijke aanpassingen aan de nieuwe architectuur nodig zouden hebben. We moesten een aantal interfaces wijzigen, maar het was niet nodig om significante wijzigingen aan het bedieningsmodel aan te brengen.

De huidige versie van ons platform noemden we Rebus – als afkorting voor de twee meest opvallende innovaties in de architectuur, Risk Engine en BUS.

De evolutie van de architectuur van het handels- en clearingsysteem van de Moscow Exchange. Deel 2

Aanvankelijk wilden we alleen het verrekeningsgedeelte toewijzen, maar het resultaat was een enorm gedistribueerd systeem. Klanten kunnen nu communiceren met de Trade Gateway, de Clearing Gateway of beide.

Wat we uiteindelijk hebben bereikt:

De evolutie van de architectuur van het handels- en clearingsysteem van de Moscow Exchange. Deel 2

Het latentieniveau verlaagd. Bij een klein transactievolume werkt het systeem hetzelfde als de vorige versie, maar is het tegelijkertijd bestand tegen een veel hogere belasting.

De piekprestaties stegen van 50 naar 180 transacties per seconde. Een verdere stijging wordt belemmerd door de enige stroom ordermatching.

Er zijn twee manieren voor verdere verbetering: het parallelliseren van matching en het veranderen van de manier waarop het met Gateway werkt. Nu werken alle gateways volgens een replicatieschema, dat onder een dergelijke belasting niet meer normaal functioneert.

Ten slotte kan ik wat advies geven aan degenen die bezig zijn met het finaliseren van bedrijfssystemen:

  • Wees te allen tijde voorbereid op het ergste. Problemen ontstaan ​​altijd onverwacht.
  • Het is meestal onmogelijk om architectuur snel opnieuw te maken. Vooral als u maximale betrouwbaarheid op meerdere indicatoren wilt bereiken. Hoe meer knooppunten, hoe meer middelen er nodig zijn voor ondersteuning.
  • Voor alle op maat gemaakte en bedrijfseigen oplossingen zijn extra middelen nodig voor onderzoek, ondersteuning en onderhoud.
  • Stel het oplossen van problemen met de systeembetrouwbaarheid en het herstel na storingen niet uit; houd er al in de eerste ontwerpfase rekening mee.

Bron: www.habr.com

Voeg een reactie