Ewolucja architektury systemu handlu i rozliczeń moskiewskiej giełdy. Część 2

Ewolucja architektury systemu handlu i rozliczeń moskiewskiej giełdy. Część 2

Jest to kontynuacja długiej historii o naszej ciernistej drodze do stworzenia potężnego, wysokoobciążonego systemu, który zapewni działanie Giełdy. Pierwsza część jest tutaj: habr.com/en/post/444300

Tajemniczy błąd

Po licznych testach zaktualizowany system handlu i rozliczeń został uruchomiony i napotkaliśmy błąd, o którym moglibyśmy napisać kryminalno-mistyczną historię.

Krótko po uruchomieniu na serwerze głównym jedna z transakcji została przetworzona z błędem. Jednak na serwerze kopii zapasowych wszystko było w porządku. Okazało się, że prosta operacja matematyczna obliczenia wykładnika na głównym serwerze dała wynik ujemny z rzeczywistego argumentu! Kontynuowaliśmy badania i w rejestrze SSE2 znaleźliśmy różnicę w jednym bicie, który odpowiada za zaokrąglanie podczas pracy z liczbami zmiennoprzecinkowymi.

Napisaliśmy proste narzędzie testowe do obliczania wykładnika za pomocą ustawionego bitu zaokrąglającego. Okazało się, że w wersji RedHat Linux, z której korzystaliśmy, wystąpił błąd w pracy z funkcją matematyczną po wstawieniu niefortunnego bitu. Zgłosiliśmy to firmie RedHat, po pewnym czasie otrzymaliśmy od nich łatkę i wdrożyliśmy ją. Błąd już nie występował, ale nie było jasne, skąd w ogóle pochodzi ten bit? Funkcja była za to odpowiedzialna fesetround z języka C. Dokładnie przeanalizowaliśmy nasz kod w poszukiwaniu rzekomego błędu: sprawdziliśmy wszystkie możliwe sytuacje; przyjrzał się wszystkim funkcjom korzystającym z zaokrąglania; próbował odtworzyć nieudaną sesję; użył różnych kompilatorów z różnymi opcjami; Zastosowano analizę statyczną i dynamiczną.

Nie udało się znaleźć przyczyny błędu.

Następnie rozpoczęli sprawdzanie sprzętu: przeprowadzili testy obciążeniowe procesorów; sprawdziłem pamięć RAM; Przeprowadziliśmy nawet testy dla bardzo mało prawdopodobnego scenariusza błędu wielobitowego w jednej komórce. Bez skutku.

Ostatecznie zdecydowaliśmy się na teorię ze świata fizyki wysokich energii: do naszego centrum danych wleciała cząstka o wysokiej energii, przebiła ściankę obudowy, uderzyła w procesor i spowodowała, że ​​zatrzask spustowy w tym miejscu się zaciął. Tę absurdalną teorię nazwano „neutrinem”. Jeśli jesteś daleko od fizyki cząstek elementarnych: neutrina prawie nie oddziałują ze światem zewnętrznym, a już na pewno nie są w stanie wpłynąć na pracę procesora.

Ponieważ nie udało się znaleźć przyczyny awarii, na wszelki wypadek „nieprawidłowy” serwer został wyłączony z działania.

Po pewnym czasie zaczęliśmy udoskonalać system hot Backup: wprowadziliśmy tzw. „ciepłe rezerwy” (ciepłe) – repliki asynchroniczne. Otrzymali strumień transakcji, które mogły być zlokalizowane w różnych centrach danych, ale rozgrzewki nie wchodziły w aktywną interakcję z innymi serwerami.

Ewolucja architektury systemu handlu i rozliczeń moskiewskiej giełdy. Część 2

Dlaczego to zrobiono? Jeśli serwer zapasowy ulegnie awarii, nowym zapasowym serwerem stanie się ciepło powiązany z serwerem głównym. Oznacza to, że po awarii system nie pozostaje na jednym głównym serwerze do końca sesji giełdowej.

A kiedy nowa wersja systemu została przetestowana i uruchomiona, błąd zaokrąglenia bitu pojawił się ponownie. Co więcej, wraz ze wzrostem liczby ciepłych serwerów, błąd zaczął pojawiać się częściej. Jednocześnie sprzedawca nie miał nic do pokazania, ponieważ nie było konkretnych dowodów.

Podczas kolejnej analizy sytuacji pojawiła się teoria, że ​​problem może być powiązany z systemem operacyjnym. Napisaliśmy prosty program, który wywołuje funkcję w nieskończonej pętli fesetround, zapamiętuje aktualny stan i sprawdza go poprzez sen, a dzieje się to w wielu konkurencyjnych wątkach. Po wybraniu parametrów uśpienia i liczby wątków, po około 5 minutach działania narzędzia zaczęliśmy konsekwentnie odtwarzać awarię bitu. Jednak wsparcie Red Hat nie było w stanie go odtworzyć. Testy innych naszych serwerów wykazały, że tylko te z określonymi procesorami są podatne na błędy. Jednocześnie przejście na nowe jądro rozwiązało problem. Ostatecznie po prostu wymieniliśmy system operacyjny i prawdziwa przyczyna błędu pozostała niejasna.

I nagle w zeszłym roku na Habré ukazał się artykuł „Jak znalazłem błąd w procesorach Intel Skylake" Opisana w nim sytuacja była bardzo podobna do naszej, jednak autor poszedł dalej i wysunął teorię, że błąd tkwił w mikrokodzie. A kiedy aktualizuje się jądra Linuksa, producenci aktualizują także mikrokod.

Dalszy rozwój systemu

Choć pozbyliśmy się błędu, ta historia zmusiła nas do ponownego przemyślenia architektury systemu. W końcu nie byliśmy chronieni przed powtarzaniem się takich błędów.

Poniższe zasady stały się podstawą kolejnych udoskonaleń systemu rezerwacji:

  • Nie możesz nikomu ufać. Serwery mogą nie działać prawidłowo.
  • Rezerwacja większości.
  • Zapewnienie konsensusu. Jako logiczny dodatek do rezerwacji większościowej.
  • Możliwe są podwójne awarie.
  • Witalność. Nowy schemat gorącego czuwania nie powinien być gorszy od poprzedniego. Handel powinien przebiegać nieprzerwanie aż do ostatniego serwera.
  • Nieznaczny wzrost opóźnienia. Każdy przestój wiąże się z ogromnymi stratami finansowymi.
  • Minimalna interakcja sieciowa, aby utrzymać możliwie najniższe opóźnienia.
  • Wybór nowego serwera głównego w kilka sekund.

Żadne z rozwiązań dostępnych na rynku nam nie odpowiadało, a protokół Raft był jeszcze w powijakach, dlatego stworzyliśmy własne rozwiązanie.

Ewolucja architektury systemu handlu i rozliczeń moskiewskiej giełdy. Część 2

Sieć

Oprócz systemu rezerwacji rozpoczęliśmy modernizację interakcji sieciowych. Podsystem I/O składał się z wielu procesów, które miały najgorszy wpływ na jitter i opóźnienia. Przy setkach procesów obsługujących połączenia TCP, byliśmy zmuszeni do ciągłego przełączania się między nimi, a w skali mikrosekundowej jest to operacja dość czasochłonna. Ale najgorsze jest to, że gdy proces otrzymał pakiet do przetworzenia, wysłał go do jednej kolejki SystemV, a następnie czekał na zdarzenie z innej kolejki SystemV. Jednakże w przypadku dużej liczby węzłów przybycie nowego pakietu TCP w jednym procesie i otrzymanie danych w kolejce w innym stanowią dwa konkurencyjne zdarzenia dla systemu operacyjnego. W takim przypadku, jeśli dla obu zadań nie będzie dostępnych procesorów fizycznych, jedno zostanie przetworzone, a drugie umieszczone w kolejce oczekującej. Konsekwencji nie da się przewidzieć.

W takich sytuacjach można zastosować dynamiczną kontrolę priorytetów procesów, ale będzie to wymagało użycia wywołań systemowych pochłaniających duże zasoby. W rezultacie przeszliśmy na jeden wątek przy użyciu klasycznego epoll, co znacznie zwiększyło prędkość i skróciło czas przetwarzania transakcji. Pozbyliśmy się także odrębnych procesów komunikacji sieciowej i komunikacji poprzez SystemV, znacząco ograniczyliśmy liczbę wywołań systemowych i zaczęliśmy kontrolować priorytety działań. W samym podsystemie we/wy można było zaoszczędzić około 8–17 mikrosekund, w zależności od scenariusza. Ten jednowątkowy schemat jest od tego czasu stosowany w niezmienionej formie; jeden wątek epoll z marginesem wystarczy do obsługi wszystkich połączeń.

Przetwarzanie transakcji

Rosnące obciążenie naszego systemu wymagało modernizacji niemal wszystkich jego komponentów. Niestety, stagnacja we wzroście częstotliwości taktowania procesorów w ostatnich latach nie pozwala już na bezpośrednie skalowanie procesów. Dlatego postanowiliśmy podzielić proces Engine na trzy poziomy, z których najbardziej obciążony jest system sprawdzania ryzyka, który ocenia dostępność środków na rachunkach i tworzy same transakcje. Ale pieniądze mogą być w różnych walutach i konieczne było ustalenie, na jakiej podstawie należy podzielić przetwarzanie wniosków.

Logicznym rozwiązaniem jest podzielenie tego według waluty: jeden serwer handluje w dolarach, drugi w funtach, a trzeci w euro. Ale jeśli przy takim schemacie zostaną wysłane dwie transakcje w celu zakupu różnych walut, pojawi się problem desynchronizacji portfela. Ale synchronizacja jest trudna i kosztowna. Dlatego słuszne byłoby shardowanie osobno według portfeli i osobno według instrumentów. Nawiasem mówiąc, większość zachodnich giełd nie ma za zadanie sprawdzania ryzyka tak wnikliwie jak my, więc najczęściej odbywa się to w trybie offline. Musieliśmy wdrożyć weryfikację online.

Wyjaśnijmy na przykładzie. Trader chce kupić 30 dolarów i żądanie trafia do zatwierdzenia transakcji: sprawdzamy, czy ten trader ma pozwolenie na ten tryb handlu i czy posiada niezbędne uprawnienia. Jeśli wszystko jest w porządku, zgłoszenie trafia do systemu weryfikacji ryzyka, tj. w celu sprawdzenia wystarczalności środków do zawarcia transakcji. Jest informacja, że ​​wymagana kwota jest obecnie zablokowana. Żądanie jest następnie przekazywane do systemu transakcyjnego, który zatwierdza lub odrzuca transakcję. Załóżmy, że transakcja została zatwierdzona – wówczas system weryfikacji ryzyka zaznacza, że ​​pieniądze zostały odblokowane, a ruble zamieniają się w dolary.

Ogólnie rzecz biorąc, system sprawdzania ryzyka zawiera złożone algorytmy i wykonuje dużą liczbę bardzo zasobochłonnych obliczeń, a nie po prostu sprawdza „saldo konta”, jak mogłoby się wydawać na pierwszy rzut oka.

Kiedy zaczęliśmy dzielić proces Engine na poziomy, napotkaliśmy problem: dostępny wówczas kod aktywnie wykorzystywał tę samą tablicę danych na etapach walidacji i weryfikacji, co wymagało przepisania całej bazy kodu. W rezultacie zapożyczyliśmy technikę przetwarzania instrukcji od nowoczesnych procesorów: każdy z nich jest podzielony na małe etapy i kilka działań wykonywanych jest równolegle w jednym cyklu.

Ewolucja architektury systemu handlu i rozliczeń moskiewskiej giełdy. Część 2

Po niewielkiej adaptacji kodu stworzyliśmy potok do równoległego przetwarzania transakcji, w którym transakcja została podzielona na 4 etapy potoku: interakcja sieciowa, walidacja, wykonanie i publikacja wyniku

Ewolucja architektury systemu handlu i rozliczeń moskiewskiej giełdy. Część 2

Spójrzmy na przykład. Mamy dwa systemy przetwarzania, szeregowy i równoległy. Przychodzi pierwsza transakcja, która jest wysyłana do weryfikacji w obu systemach. Druga transakcja przychodzi natychmiast: w systemie równoległym jest od razu zabierana do pracy, a w systemie sekwencyjnym umieszczana w kolejce oczekującej, aż pierwsza transakcja przejdzie przez bieżący etap przetwarzania. Oznacza to, że główną zaletą przetwarzania potokowego jest to, że szybciej przetwarzamy kolejkę transakcji.

W ten sposób powstał system ASTS+.

To prawda, że ​​\uXNUMXb\uXNUMXbz przenośnikami też nie wszystko jest tak gładkie. Załóżmy, że mamy transakcję, która wpływa na tablice danych w sąsiedniej transakcji; jest to typowa sytuacja dla wymiany. Taka transakcja nie może zostać wykonana w potoku, ponieważ może mieć wpływ na inne. Sytuację tę nazywa się zagrożeniem danych i takie transakcje są po prostu przetwarzane osobno: gdy wyczerpią się „szybkie” transakcje w kolejce, potok zatrzymuje się, system przetwarza „wolną” transakcję, a następnie ponownie uruchamia potok. Na szczęście odsetek takich transakcji w całym przepływie jest bardzo mały, dlatego potok zatrzymuje się tak rzadko, że nie ma to wpływu na ogólną wydajność.

Ewolucja architektury systemu handlu i rozliczeń moskiewskiej giełdy. Część 2

Następnie zaczęliśmy rozwiązywać problem synchronizacji trzech wątków wykonawczych. W rezultacie powstał system oparty na buforze pierścieniowym z komórkami o stałym rozmiarze. W tym systemie wszystko zależy od szybkości przetwarzania, dane nie są kopiowane.

  • Wszystkie przychodzące pakiety sieciowe wchodzą w fazę alokacji.
  • Umieszczamy je w tablicy i oznaczamy jako dostępne dla etapu nr 1.
  • Doszła druga transakcja, jest ponownie dostępna na etap nr 1.
  • Pierwszy wątek przetwarzający widzi dostępne transakcje, przetwarza je i przenosi do następnego etapu drugiego wątku przetwarzającego.
  • Następnie przetwarza pierwszą transakcję i flaguje odpowiednią komórkę deleted — jest teraz dostępny do nowego użytku.

W ten sposób przetwarzana jest cała kolejka.

Ewolucja architektury systemu handlu i rozliczeń moskiewskiej giełdy. Część 2

Przetwarzanie każdego etapu zajmuje jednostki lub dziesiątki mikrosekund. A jeśli zastosujemy standardowe schematy synchronizacji systemu operacyjnego, stracimy więcej czasu na samą synchronizację. Dlatego zaczęliśmy używać spinlocka. Jest to jednak bardzo zła forma w systemie czasu rzeczywistego i RedHat stanowczo tego nie zaleca, dlatego stosujemy blokadę na 100 ms, a następnie przełączamy się w tryb semafora, aby wyeliminować możliwość zakleszczenia.

W rezultacie osiągnęliśmy wydajność na poziomie około 8 milionów transakcji na sekundę. I dosłownie dwa miesiące później Artykuł o LMAX Disruptor widzieliśmy opis obwodu o tej samej funkcjonalności.

Ewolucja architektury systemu handlu i rozliczeń moskiewskiej giełdy. Część 2

Teraz na jednym etapie może być kilka wątków wykonania. Wszystkie transakcje były przetwarzane jedna po drugiej, w kolejności ich otrzymania. W rezultacie szczytowa wydajność wzrosła z 18 tys. do 50 tys. transakcji na sekundę.

System zarządzania ryzykiem walutowym

Perfekcja nie ma granic i wkrótce ponownie rozpoczęliśmy modernizację: w ramach ASTS+ zaczęliśmy przenosić systemy zarządzania ryzykiem i operacji rozliczeniowych do autonomicznych komponentów. Opracowaliśmy elastyczną, nowoczesną architekturę i nowy hierarchiczny model ryzyka i staraliśmy się używać tej klasy tam, gdzie to możliwe fixed_point zamiast double.

Ale od razu pojawił się problem: jak zsynchronizować całą logikę biznesową, która działa od wielu lat i przenieść ją do nowego systemu? W efekcie trzeba było porzucić pierwszą wersję prototypu nowego systemu. Druga wersja, która obecnie pracuje w produkcji, opiera się na tym samym kodzie, który działa zarówno w części handlowej, jak i ryzykownej. Podczas programowania najtrudniejszą rzeczą było git merge pomiędzy dwiema wersjami. Nasz kolega Evgeniy Mazurenok wykonywał tę operację co tydzień i za każdym razem przeklinał przez bardzo długi czas.

Wybierając nowy system, od razu musieliśmy rozwiązać problem interakcji. Przy wyborze magistrali danych należało zadbać o stabilny jitter i minimalne opóźnienia. Najlepiej nadawała się do tego sieć InfiniBand RDMA: średni czas przetwarzania jest 4 razy krótszy niż w sieciach Ethernet 10 G. Ale to, co naprawdę nas urzekło, to różnica w percentylach – 99 i 99,9.

Oczywiście InfiniBand ma swoje wyzwania. Po pierwsze inne API - ibverbs zamiast gniazd. Po drugie, prawie nie ma powszechnie dostępnych rozwiązań do przesyłania wiadomości typu open source. Próbowaliśmy stworzyć własny prototyp, ale okazało się to bardzo trudne, dlatego wybraliśmy rozwiązanie komercyjne - Confinity Low Latency Messaging (dawniej IBM MQ LLM).

Powstało wówczas zadanie odpowiedniego podziału systemu ryzyka. Jeśli po prostu usuniesz silnik ryzyka i nie utworzysz węzła pośredniego, wówczas transakcje z dwóch źródeł będą mogły być mieszane.

Ewolucja architektury systemu handlu i rozliczeń moskiewskiej giełdy. Część 2

Rozwiązania tzw. Ultra Low Latency posiadają tryb reorderingu: transakcje z dwóch źródeł można po ich otrzymaniu ułożyć w wymaganej kolejności, odbywa się to przy wykorzystaniu osobnego kanału wymiany informacji o zleceniu. Ale nie korzystamy jeszcze z tego trybu: komplikuje to cały proces, a w wielu rozwiązaniach w ogóle nie jest obsługiwany. Dodatkowo każda transakcja musiałaby mieć przypisane odpowiednie znaczniki czasu, a w naszym schemacie ten mechanizm jest bardzo trudny do prawidłowego wdrożenia. Dlatego zastosowaliśmy klasyczny schemat z brokerem komunikatów, czyli z dyspozytorem, który dystrybuuje komunikaty pomiędzy Risk Engine.

Drugi problem dotyczył dostępu klienta: jeśli istnieje kilka bramek ryzyka, klient musi połączyć się z każdą z nich, a to będzie wymagało zmian w warstwie klienta. Na tym etapie chcieliśmy od tego uciec, dlatego obecny projekt Risk Gateway przetwarza cały strumień danych. To znacznie ogranicza maksymalną przepustowość, ale znacznie upraszcza integrację systemu.

Powielanie

Nasz system nie powinien mieć pojedynczego punktu awarii, czyli wszystkie komponenty muszą być zduplikowane, łącznie z brokerem komunikatów. Rozwiązaliśmy ten problem wykorzystując system CLLM: zawiera on klaster RCMS, w którym dwóch dyspozytorów może pracować w trybie master-slave, a w przypadku awarii jednego system automatycznie przełącza się na drugiego.

Praca z zapasowym centrum danych

InfiniBand jest zoptymalizowany do pracy w sieci lokalnej, czyli do łączenia sprzętu montowanego w stojaku, a sieci InfiniBand nie można układać pomiędzy dwoma geograficznie rozproszonymi centrami danych. Dlatego zaimplementowaliśmy most/dyspozytor, który łączy się z magazynem wiadomości poprzez zwykłe sieci Ethernet i przekazuje wszystkie transakcje do drugiej sieci IB. Kiedy musimy przeprowadzić migrację z centrum danych, możemy wybrać, z którym centrum danych będziemy teraz pracować.

Wyniki

Wszystkie powyższe nie zostały wykonane od razu; opracowanie nowej architektury wymagało kilku iteracji. Prototyp stworzyliśmy w miesiąc, ale doprowadzenie go do stanu używalności zajęło ponad dwa lata. Staraliśmy się osiągnąć najlepszy kompromis pomiędzy wydłużeniem czasu przetwarzania transakcji, a zwiększeniem niezawodności systemu.

Ponieważ system był mocno aktualizowany, wdrożyliśmy odzyskiwanie danych z dwóch niezależnych źródeł. Jeśli z jakiegoś powodu magazyn wiadomości nie działa poprawnie, możesz pobrać dziennik transakcji z drugiego źródła - z silnika ryzyka. Zasada ta obowiązuje w całym systemie.

Między innymi udało nam się zachować API klienta tak, aby ani brokerzy, ani nikt inny nie wymagał znaczących przeróbek pod nową architekturę. Musieliśmy zmienić niektóre interfejsy, ale nie było potrzeby wprowadzania znaczących zmian w modelu operacyjnym.

Aktualną wersję naszej platformy nazwaliśmy Rebus – jako skrót od dwóch najbardziej zauważalnych innowacji w architekturze, Risk Engine i BUS.

Ewolucja architektury systemu handlu i rozliczeń moskiewskiej giełdy. Część 2

Początkowo chcieliśmy przydzielić tylko część rozliczeniową, ale w rezultacie powstał ogromny, rozproszony system. Klienci mogą teraz wchodzić w interakcję z Trade Gateway, Clearing Gateway lub obydwoma.

Co ostatecznie osiągnęliśmy:

Ewolucja architektury systemu handlu i rozliczeń moskiewskiej giełdy. Część 2

Zmniejszono poziom opóźnienia. Przy niewielkim wolumenie transakcji system działa tak samo jak poprzednia wersja, ale jednocześnie wytrzymuje znacznie większe obciążenie.

Maksymalna wydajność wzrosła z 50 tys. do 180 tys. transakcji na sekundę. Dalszy wzrost utrudnia jedyny strumień dopasowywania zamówień.

Istnieją dwa sposoby dalszego doskonalenia: dopasowanie równoległe i zmiana sposobu działania z Gateway. Teraz wszystkie Bramy działają według schematu replikacji, który pod takim obciążeniem przestaje normalnie działać.

Na koniec mogę dać kilka rad tym, którzy finalizują systemy korporacyjne:

  • Przez cały czas bądź przygotowany na najgorsze. Problemy zawsze pojawiają się niespodziewanie.
  • Architektury nie da się zazwyczaj szybko przerobić. Zwłaszcza jeśli chcesz osiągnąć maksymalną niezawodność w przypadku wielu wskaźników. Im więcej węzłów, tym więcej zasobów potrzebnych do wsparcia.
  • Wszystkie niestandardowe i zastrzeżone rozwiązania będą wymagały dodatkowych zasobów na badania, wsparcie i konserwację.
  • Nie odkładaj na później rozwiązywania problemów związanych z niezawodnością i odzyskiwaniem systemu po awariach; weź je pod uwagę już na wstępnym etapie projektowania.

Źródło: www.habr.com

Dodaj komentarz