Utveckling av arkitekturen för handels- och clearingsystemet för Moskvabörsen. Del 2

Utveckling av arkitekturen för handels- och clearingsystemet för Moskvabörsen. Del 2

Detta är en fortsättning på en lång historia om vår svåra väg till att skapa ett kraftfullt, högbelastningssystem som säkerställer driften av Exchange. Den första delen är här: habr.com/en/post/444300

Mystiskt fel

Efter många tester togs det uppdaterade handels- och clearingsystemet i drift, och vi stötte på en bugg som vi kunde skriva en detektiv-mystisk historia om.

Kort efter lanseringen på huvudservern behandlades en av transaktionerna med ett fel. Allt var dock bra på backupservern. Det visade sig att en enkel matematisk operation att beräkna exponenten på huvudservern gav ett negativt resultat från det verkliga argumentet! Vi fortsatte vår forskning och i SSE2-registret hittade vi en skillnad på en bit, som är ansvarig för avrundning när man arbetar med flyttal.

Vi skrev ett enkelt testverktyg för att beräkna exponenten med uppsättningen avrundningsbitar. Det visade sig att i den version av RedHat Linux som vi använde fanns det en bugg i arbetet med den matematiska funktionen när den olyckliga biten sattes in. Vi rapporterade detta till RedHat, efter ett tag fick vi en patch från dem och rullade ut den. Felet uppstod inte längre, men det var oklart var denna bit ens kom ifrån? Funktionen ansvarade för det fesetround från språket C. Vi analyserade vår kod noggrant i jakten på det förmodade felet: vi kontrollerade alla möjliga situationer; tittade på alla funktioner som använde avrundning; försökte återskapa en misslyckad session; använde olika kompilatorer med olika alternativ; Statisk och dynamisk analys användes.

Orsaken till felet kunde inte hittas.

Sedan började de kontrollera hårdvaran: de utförde belastningstester av processorerna; kollade RAM; Vi körde till och med tester för det mycket osannolika scenariot med ett flerbitsfel i en cell. Förgäves.

Till slut bestämde vi oss för en teori från högenergifysikens värld: någon högenergipartikel flög in i vårt datacenter, genomborrade höljets vägg, träffade processorn och fick avtryckarspärren att fastna i just den biten. Denna absurda teori kallades "neutrino". Om du är långt ifrån partikelfysik: neutriner interagerar nästan inte med omvärlden och kan definitivt inte påverka processorns funktion.

Eftersom det inte var möjligt att hitta orsaken till felet togs den "kränkande" servern ur drift för säkerhets skull.

Efter en tid började vi förbättra det heta backup-systemet: vi introducerade så kallade "varma reserver" (varma) - asynkrona repliker. De fick en ström av transaktioner som kunde placeras i olika datacenter, men warms interagerade inte aktivt med andra servrar.

Utveckling av arkitekturen för handels- och clearingsystemet för Moskvabörsen. Del 2

Varför gjordes detta? Om backupservern misslyckas, blir varm bunden till huvudservern den nya backupen. Det vill säga, efter ett misslyckande förblir systemet inte med en huvudserver förrän i slutet av handelssessionen.

Och när den nya versionen av systemet testades och togs i drift uppstod avrundningsbitfelet igen. Dessutom, med ökningen av antalet varma servrar, började felet dyka upp oftare. Samtidigt hade säljaren inget att visa, eftersom det inte fanns några konkreta bevis.

Vid nästa analys av situationen uppstod en teori om att problemet kunde relateras till operativsystemet. Vi skrev ett enkelt program som anropar en funktion i en oändlig loop fesetround, kommer ihåg det aktuella tillståndet och kontrollerar det genom sömn, och detta görs i många konkurrerande trådar. Efter att ha valt parametrarna för sömn och antalet trådar, började vi konsekvent reproducera bitfelet efter cirka 5 minuters körning av verktyget. Red Hat-stödet kunde dock inte reproducera det. Tester av våra andra servrar har visat att endast de med vissa processorer är mottagliga för felet. Samtidigt löste problemet att byta till en ny kärna. Till slut bytte vi helt enkelt ut operativsystemet och den verkliga orsaken till felet förblev oklart.

Och plötsligt förra året publicerades en artikel på Habré ”Hur jag hittade en bugg i Intel Skylake-processorer" Situationen som beskrivs i den var väldigt lik vår, men författaren tog utredningen vidare och lade fram en teori om att felet låg i mikrokoden. Och när Linux-kärnor uppdateras uppdaterar tillverkarna också mikrokoden.

Vidareutveckling av systemet

Även om vi blev av med felet, tvingade den här historien oss att ompröva systemarkitekturen. När allt kommer omkring var vi inte skyddade från upprepning av sådana buggar.

Följande principer låg till grund för nästa förbättringar av bokningssystemet:

  • Du kan inte lita på någon. Servrarna kanske inte fungerar korrekt.
  • Majoritetsreservation.
  • Att säkerställa konsensus. Som ett logiskt tillägg till majoritetsreservation.
  • Dubbla misslyckanden är möjliga.
  • Vitalitet. Det nya hot standby-schemat bör inte vara sämre än det tidigare. Handeln bör fortsätta oavbrutet fram till den sista servern.
  • Lätt ökad latens. Alla stillestånd innebär stora ekonomiska förluster.
  • Minimal nätverksinteraktion för att hålla latensen så låg som möjligt.
  • Att välja en ny huvudserver på några sekunder.

Ingen av lösningarna på marknaden passade oss, och Raft-protokollet var fortfarande i sin linda, så vi skapade vår egen lösning.

Utveckling av arkitekturen för handels- och clearingsystemet för Moskvabörsen. Del 2

Nätverk

Utöver bokningssystemet började vi modernisera nätverksinteraktionen. I/O-delsystemet bestod av många processer, som hade den värsta inverkan på jitter och latens. Med hundratals processer som hanterade TCP-anslutningar var vi tvungna att ständigt växla mellan dem, och i mikrosekundsskala är detta en ganska tidskrävande operation. Men det värsta är att när en process tog emot ett paket för bearbetning skickade den det till en SystemV-kö och väntade sedan på en händelse från en annan SystemV-kö. Men när det finns ett stort antal noder representerar ankomsten av ett nytt TCP-paket i en process och mottagandet av data i kön i en annan två konkurrerande händelser för operativsystemet. I det här fallet, om det inte finns några fysiska processorer tillgängliga för båda uppgifterna, kommer den ena att bearbetas och den andra placeras i en väntande kö. Det är omöjligt att förutse konsekvenserna.

I sådana situationer kan dynamisk processprioritetskontroll användas, men detta kommer att kräva användning av resurskrävande systemanrop. Som ett resultat bytte vi till en tråd med klassisk epoll, detta ökade hastigheten avsevärt och minskade transaktionsbehandlingstiden. Vi blev också av med separata nätverkskommunikationsprocesser och kommunikation genom SystemV, minskade antalet systemsamtal avsevärt och började styra verksamhetens prioriteringar. Enbart på I/O-delsystemet var det möjligt att spara cirka 8-17 mikrosekunder, beroende på scenariot. Detta entrådiga schema har använts oförändrat sedan dess; en epoll-tråd med marginal räcker för att betjäna alla anslutningar.

Transaktionshantering

Den växande belastningen på vårt system krävde uppgradering av nästan alla dess komponenter. Men tyvärr har stagnation i tillväxten av processorklockhastigheter de senaste åren inte längre gjort det möjligt att skala processer direkt. Därför bestämde vi oss för att dela upp Engine-processen i tre nivåer, där den mest trafikerade av dem är riskkontrollsystemet, som utvärderar tillgången på medel på konton och skapar själva transaktionerna. Men pengar kan vara i olika valutor, och det var nödvändigt att ta reda på på vilken grund behandlingen av förfrågningar skulle delas upp.

Den logiska lösningen är att dela upp den efter valuta: en server handlas i dollar, en annan i pund och en tredje i euro. Men om, med ett sådant system, två transaktioner skickas för att köpa olika valutor, kommer problemet med plånboksdesynkronisering att uppstå. Men synkronisering är svårt och dyrt. Därför skulle det vara korrekt att skära separat med plånböcker och separat med instrument. De flesta västerländska börser har förresten inte till uppgift att kontrollera risker lika akut som vi, så oftast görs detta offline. Vi behövde implementera onlineverifiering.

Låt oss förklara med ett exempel. En handlare vill köpa $30, och begäran går till transaktionsvalidering: vi kontrollerar om denna handlare har tillåtelse till detta handelsläge och om han har de nödvändiga rättigheterna. Om allt är i sin ordning går förfrågan till riskverifieringssystemet, d.v.s. för att kontrollera om det finns tillräckligt med medel för att slutföra en transaktion. Det finns en notering att det erforderliga beloppet för närvarande är spärrat. Begäran skickas sedan vidare till handelssystemet, som godkänner eller underkänner transaktionen. Låt oss säga att transaktionen är godkänd - då markerar riskverifieringssystemet att pengarna är avblockerade och rubeln förvandlas till dollar.

I allmänhet innehåller riskkontrollsystemet komplexa algoritmer och utför en stor mängd mycket resurskrävande beräkningar, och kontrollerar inte bara "kontosaldot", som det kan tyckas vid första anblicken.

När vi började dela upp motorprocessen i nivåer stötte vi på ett problem: koden som var tillgänglig vid den tiden använde aktivt samma datauppsättning vid validerings- och verifieringsstadierna, vilket krävde att hela kodbasen skrevs om. Som ett resultat lånade vi en teknik för att bearbeta instruktioner från moderna processorer: var och en av dem är uppdelad i små steg och flera åtgärder utförs parallellt i en cykel.

Utveckling av arkitekturen för handels- och clearingsystemet för Moskvabörsen. Del 2

Efter en liten anpassning av koden skapade vi en pipeline för parallell transaktionsbearbetning, där transaktionen delades upp i 4 steg av pipelinen: nätverksinteraktion, validering, exekvering och publicering av resultatet

Utveckling av arkitekturen för handels- och clearingsystemet för Moskvabörsen. Del 2

Låt oss titta på ett exempel. Vi har två bearbetningssystem, seriella och parallella. Den första transaktionen anländer och skickas för validering i båda systemen. Den andra transaktionen anländer omedelbart: i ett parallellt system tas den omedelbart till arbete, och i ett sekventiellt system ställs den i en kö i väntan på att den första transaktionen ska gå igenom det aktuella bearbetningsstadiet. Det vill säga den största fördelen med pipelinebehandling är att vi bearbetar transaktionskön snabbare.

Så här kom vi fram till ASTS+-systemet.

Det är sant att allt inte är så smidigt med transportörer heller. Låt oss säga att vi har en transaktion som påverkar datamatriser i en angränsande transaktion; detta är en typisk situation för ett utbyte. En sådan transaktion kan inte utföras i en pipeline eftersom den kan påverka andra. Denna situation kallas datarisk, och sådana transaktioner behandlas helt enkelt separat: när de "snabba" transaktionerna i kön tar slut stoppas pipelinen, systemet bearbetar den "långsamma" transaktionen och startar sedan pipelinen igen. Lyckligtvis är andelen sådana transaktioner i det totala flödet mycket liten, så pipelinen stannar så sällan att den inte påverkar den totala prestandan.

Utveckling av arkitekturen för handels- och clearingsystemet för Moskvabörsen. Del 2

Sedan började vi lösa problemet med att synkronisera tre exekveringstrådar. Resultatet blev ett system baserat på en ringbuffert med celler av fast storlek. I detta system är allt föremål för bearbetningshastighet, data kopieras inte.

  • Alla inkommande nätverkspaket går in i allokeringsstadiet.
  • Vi placerar dem i en array och markerar dem som tillgängliga för steg #1.
  • Den andra transaktionen har kommit, den är återigen tillgänglig för steg nr 1.
  • Den första bearbetningstråden ser de tillgängliga transaktionerna, bearbetar dem och flyttar dem till nästa steg i den andra bearbetningstråden.
  • Den bearbetar sedan den första transaktionen och flaggar motsvarande cell deleted — den är nu tillgänglig för ny användning.

Hela kön bearbetas på detta sätt.

Utveckling av arkitekturen för handels- och clearingsystemet för Moskvabörsen. Del 2

Bearbetningen av varje steg tar enheter eller tiotals mikrosekunder. Och om vi använder standard OS-synkroniseringsscheman, kommer vi att förlora mer tid på själva synkroniseringen. Det var därför vi började använda spinlock. Detta är dock mycket dålig form i ett realtidssystem, och RedHat rekommenderar strängt taget inte att göra detta, så vi tillämpar ett spinlock i 100 ms, och byter sedan till semaforläge för att eliminera möjligheten till ett dödläge.

Som ett resultat uppnådde vi en prestation på cirka 8 miljoner transaktioner per sekund. Och bokstavligen två månader senare artikeln om LMAX Disruptor såg vi en beskrivning av en krets med samma funktionalitet.

Utveckling av arkitekturen för handels- och clearingsystemet för Moskvabörsen. Del 2

Nu kan det finnas flera trådar av avrättning i ett skede. Alla transaktioner behandlades en efter en, i den ordning de mottogs. Som ett resultat ökade toppprestanda från 18 tusen till 50 tusen transaktioner per sekund.

Riskhanteringssystem för utbyte

Det finns ingen gräns för perfektion, och snart började vi modernisera igen: inom ramen för ASTS+ började vi flytta riskhanterings- och avvecklingssystem till autonoma komponenter. Vi utvecklade en flexibel modern arkitektur och en ny hierarkisk riskmodell och försökte använda klassen där det var möjligt fixed_point istället för double.

Men ett problem uppstod omedelbart: hur man synkroniserar all affärslogik som har fungerat i många år och överför den till det nya systemet? Som ett resultat måste den första versionen av prototypen av det nya systemet överges. Den andra versionen, som för närvarande arbetar i produktion, bygger på samma kod, som fungerar i både trading- och riskdelen. Under utvecklingen var det svåraste att göra git merge mellan två versioner. Vår kollega Evgeniy Mazurenok utförde denna operation varje vecka och varje gång förbannade han väldigt länge.

När vi valde ett nytt system var vi omedelbart tvungna att lösa problemet med interaktion. När man valde en databuss var det nödvändigt att säkerställa stabilt jitter och minimal latens. InfiniBand RDMA-nätverket var bäst lämpat för detta: den genomsnittliga bearbetningstiden är 4 gånger mindre än i 10 G Ethernet-nätverk. Men det som verkligen fängslade oss var skillnaden i percentiler – 99 och 99,9.

Självklart har InfiniBand sina utmaningar. För det första, ett annat API - ibverbs istället för sockets. För det andra finns det nästan inga allmänt tillgängliga meddelandelösningar med öppen källkod. Vi försökte göra en egen prototyp, men det visade sig vara väldigt svårt, så vi valde en kommersiell lösning - Confinity Low Latency Messaging (tidigare IBM MQ LLM).

Då uppstod uppgiften att korrekt dela upp risksystemet. Om du helt enkelt tar bort Risk Engine och inte skapar en mellannod, kan transaktioner från två källor blandas.

Utveckling av arkitekturen för handels- och clearingsystemet för Moskvabörsen. Del 2

De så kallade Ultra Low Latency-lösningarna har ett ombeställningsläge: transaktioner från två källor kan ordnas i önskad ordning vid mottagandet, detta implementeras med hjälp av en separat kanal för utbyte av information om beställningen. Men vi använder ännu inte det här läget: det komplicerar hela processen, och i ett antal lösningar stöds det inte alls. Dessutom skulle varje transaktion behöva tilldelas motsvarande tidsstämplar, och i vårt schema är denna mekanism mycket svår att implementera korrekt. Därför använde vi det klassiska schemat med en meddelandemäklare, det vill säga med en avsändare som distribuerar meddelanden mellan Risk Engine.

Det andra problemet var relaterat till klientåtkomst: om det finns flera Risk Gateways måste klienten ansluta till var och en av dem, och detta kommer att kräva ändringar i klientlagret. Vi ville komma bort från detta i det här skedet, så den nuvarande Risk Gateway-designen bearbetar hela dataströmmen. Detta begränsar den maximala genomströmningen avsevärt, men förenklar systemintegrationen avsevärt.

Duplicering

Vårt system ska inte ha en enda felpunkt, det vill säga alla komponenter måste dupliceras, inklusive meddelandeförmedlaren. Vi löste detta problem med CLLM-systemet: det innehåller ett RCMS-kluster där två dispatchers kan arbeta i master-slave-läge, och när den ena misslyckas växlar systemet automatiskt till den andra.

Arbeta med ett backup-datacenter

InfiniBand är optimerat för drift som ett lokalt nätverk, det vill säga för att ansluta rackmonterad utrustning, och ett InfiniBand-nätverk kan inte läggas mellan två geografiskt fördelade datacenter. Därför implementerade vi en brygga/dispatcher, som ansluter till meddelandelagringen via vanliga Ethernet-nätverk och vidarebefordrar alla transaktioner till ett andra IB-nätverk. När vi behöver migrera från ett datacenter kan vi välja vilket datacenter vi ska arbeta med nu.

Resultat av

Allt ovanstående gjordes inte på en gång, det tog flera iterationer att utveckla en ny arkitektur. Vi skapade prototypen på en månad, men det tog mer än två år att få den i fungerande skick. Vi försökte uppnå den bästa kompromissen mellan att öka transaktionsbehandlingstiden och öka systemets tillförlitlighet.

Eftersom systemet var kraftigt uppdaterat implementerade vi dataåterställning från två oberoende källor. Om meddelandearkivet inte fungerar korrekt av någon anledning kan du ta transaktionsloggen från en andra källa - från Risk Engine. Denna princip observeras i hela systemet.

Vi kunde bland annat bevara klientens API så att varken mäklare eller någon annan skulle kräva betydande omarbetning av den nya arkitekturen. Vi var tvungna att ändra några gränssnitt, men det behövdes inte göra några större förändringar i driftsmodellen.

Vi kallade den nuvarande versionen av vår plattform för Rebus – som en förkortning för de två mest märkbara innovationerna inom arkitekturen, Risk Engine och BUS.

Utveckling av arkitekturen för handels- och clearingsystemet för Moskvabörsen. Del 2

Från början ville vi bara tilldela clearingdelen, men resultatet blev ett enormt distribuerat system. Kunder kan nu interagera med antingen Trade Gateway, Clearing Gateway eller båda.

Vad vi till slut uppnådde:

Utveckling av arkitekturen för handels- och clearingsystemet för Moskvabörsen. Del 2

Minskade latensnivån. Med en liten volym transaktioner fungerar systemet på samma sätt som den tidigare versionen, men tål samtidigt en mycket högre belastning.

Toppprestanda ökade från 50 tusen till 180 tusen transaktioner per sekund. En ytterligare ökning hämmas av den enda strömmen av ordermatchning.

Det finns två sätt att förbättra ytterligare: parallellisering av matchning och att ändra hur det fungerar med Gateway. Nu fungerar alla Gateways enligt ett replikeringsschema, som under en sådan belastning upphör att fungera normalt.

Slutligen kan jag ge några råd till dem som håller på att färdigställa företagssystem:

  • Var beredd på det värsta hela tiden. Problem uppstår alltid oväntat.
  • Det är vanligtvis omöjligt att snabbt göra om arkitektur. Speciellt om du behöver uppnå maximal tillförlitlighet över flera indikatorer. Ju fler noder, desto mer resurser behövs för support.
  • Alla anpassade och egenutvecklade lösningar kommer att kräva ytterligare resurser för forskning, support och underhåll.
  • Skjut inte upp problem med systemtillförlitlighet och återställning efter fel, ta hänsyn till dem i det inledande designskedet.

Källa: will.com

Lägg en kommentar