L'évolution de l'architecture du système de négociation et de compensation de la Bourse de Moscou. Partie 2

L'évolution de l'architecture du système de négociation et de compensation de la Bourse de Moscou. Partie 2

Il s'agit de la suite d'une longue histoire sur notre chemin épineux vers la création d'un système puissant et à forte charge qui assure le fonctionnement de l'Exchange. La première partie est ici : habr.com/en/post/444300

Erreur mystérieuse

Après de nombreux tests, le système de trading et de compensation mis à jour a été mis en service et nous avons rencontré un bug sur lequel nous pourrions écrire une histoire policière et mystique.

Peu de temps après le lancement sur le serveur principal, l'une des transactions a été traitée avec une erreur. Cependant, tout allait bien sur le serveur de sauvegarde. Il s'est avéré qu'une simple opération mathématique de calcul de l'exposant sur le serveur principal a donné un résultat négatif par rapport à l'argument réel ! Nous avons poursuivi nos recherches et dans le registre SSE2, nous avons trouvé une différence d'un bit, responsable de l'arrondi lorsque l'on travaille avec des nombres à virgule flottante.

Nous avons écrit un utilitaire de test simple pour calculer l'exposant avec le bit d'arrondi défini. Il s'est avéré que dans la version de RedHat Linux que nous avons utilisée, il y avait un bug dans le travail avec la fonction mathématique lors de l'insertion du bit malheureux. Nous l'avons signalé à RedHat, après un certain temps, nous avons reçu un correctif de leur part et l'avons déployé. L'erreur ne s'est plus produite, mais on ne sait pas exactement d'où vient ce bit ? La fonction en était responsable fesetround du langage C. Nous avons soigneusement analysé notre code à la recherche de l'erreur supposée : nous avons vérifié toutes les situations possibles ; examiné toutes les fonctions utilisant l'arrondi ; essayé de reproduire une session ayant échoué ; utilisé différents compilateurs avec différentes options ; Des analyses statiques et dynamiques ont été utilisées.

La cause de l'erreur n'a pas pu être trouvée.

Ensuite, ils ont commencé à vérifier le matériel : ils ont effectué des tests de charge des processeurs ; vérifié la RAM ; Nous avons même effectué des tests pour le scénario très improbable d’une erreur multi-bits dans une cellule. En vain.

En fin de compte, nous avons opté pour une théorie issue du monde de la physique des hautes énergies : une particule de haute énergie a volé dans notre centre de données, a percé la paroi du boîtier, a heurté le processeur et a provoqué le blocage du loquet de déclenchement. Cette théorie absurde était appelée le « neutrino ». Si l'on est loin de la physique des particules : les neutrinos n'interagissent quasiment pas avec le monde extérieur, et ne sont certainement pas capables d'affecter le fonctionnement du processeur.

Comme il n'a pas été possible de trouver la cause de la panne, le serveur « incriminé » a été mis hors service au cas où.

Après un certain temps, nous avons commencé à améliorer le système de sauvegarde à chaud : nous avons introduit ce que l'on appelle les « réserves chaudes » (chaud) - des répliques asynchrones. Ils recevaient un flux de transactions qui pouvaient être localisés dans différents centres de données, mais les warms n'interagissaient pas activement avec d'autres serveurs.

L'évolution de l'architecture du système de négociation et de compensation de la Bourse de Moscou. Partie 2

Pourquoi cela a-t-il été fait ? Si le serveur de sauvegarde tombe en panne, la nouvelle sauvegarde est liée au serveur principal. Autrement dit, après une panne, le système ne reste pas sur un serveur principal jusqu'à la fin de la séance de négociation.

Et lorsque la nouvelle version du système a été testée et mise en service, l’erreur d’arrondi s’est à nouveau produite. De plus, avec l'augmentation du nombre de serveurs chauds, l'erreur a commencé à apparaître plus souvent. Dans le même temps, le vendeur n’avait rien à démontrer, puisqu’il n’existait aucune preuve concrète.

Lors de l'analyse suivante de la situation, une théorie est apparue selon laquelle le problème pourrait être lié au système d'exploitation. Nous avons écrit un programme simple qui appelle une fonction dans une boucle sans fin fesetround, se souvient de l'état actuel et le vérifie pendant le sommeil, et cela se fait dans de nombreux threads concurrents. Après avoir sélectionné les paramètres de veille et le nombre de threads, nous avons commencé à reproduire systématiquement l'échec du bit après environ 5 minutes d'exécution de l'utilitaire. Cependant, le support Red Hat n'a pas pu le reproduire. Les tests de nos autres serveurs ont montré que seuls ceux dotés de certains processeurs sont sensibles à l'erreur. Dans le même temps, le passage à un nouveau noyau a résolu le problème. Au final, nous avons simplement remplacé le système d’exploitation, et la véritable cause du bug reste floue.

Et du coup l’année dernière un article a été publié sur Habré »Comment j'ai trouvé un bug dans les processeurs Intel Skylake" La situation qui y est décrite était très similaire à la nôtre, mais l'auteur a poussé l'enquête plus loin et a avancé une théorie selon laquelle l'erreur résidait dans le microcode. Et lorsque les noyaux Linux sont mis à jour, les fabricants mettent également à jour le microcode.

Développement ultérieur du système

Bien que nous ayons éliminé l'erreur, cette histoire nous a obligé à reconsidérer l'architecture du système. Après tout, nous n’étions pas protégés contre la répétition de tels bugs.

Les principes suivants ont constitué la base des prochaines améliorations du système de réservation :

  • Vous ne pouvez faire confiance à personne. Les serveurs peuvent ne pas fonctionner correctement.
  • Réservation majoritaire.
  • Assurer le consensus. Comme un complément logique à la réserve majoritaire.
  • Des doubles échecs sont possibles.
  • Vitalité. Le nouveau système de secours automatique ne devrait pas être pire que le précédent. Les échanges devraient se dérouler sans interruption jusqu'au dernier serveur.
  • Légère augmentation de la latence. Tout temps d'arrêt entraîne d'énormes pertes financières.
  • Interaction réseau minimale pour maintenir la latence aussi faible que possible.
  • Sélection d'un nouveau serveur maître en quelques secondes.

Aucune des solutions disponibles sur le marché ne nous convenait, et le protocole Raft en était encore à ses balbutiements, nous avons donc créé notre propre solution.

L'évolution de l'architecture du système de négociation et de compensation de la Bourse de Moscou. Partie 2

La mise en réseau

En plus du système de réservation, nous avons commencé à moderniser l'interaction réseau. Le sous-système d'E/S était composé de nombreux processus, qui avaient le plus grand impact sur la gigue et la latence. Avec des centaines de processus gérant les connexions TCP, nous étions obligés de basculer constamment entre eux, et à l'échelle de la microseconde, c'est une opération plutôt longue. Mais le pire, c'est que lorsqu'un processus recevait un paquet à traiter, il l'envoyait vers une file d'attente SystemV, puis attendait un événement provenant d'une autre file d'attente SystemV. Cependant, lorsqu'il y a un grand nombre de nœuds, l'arrivée d'un nouveau paquet TCP dans un processus et la réception de données dans la file d'attente dans un autre représentent deux événements concurrents pour le système d'exploitation. Dans ce cas, s'il n'y a pas de processeurs physiques disponibles pour les deux tâches, l'une sera traitée et la seconde sera placée dans une file d'attente. Il est impossible de prédire les conséquences.

Dans de telles situations, un contrôle dynamique de la priorité des processus peut être utilisé, mais cela nécessitera l'utilisation d'appels système gourmands en ressources. En conséquence, nous sommes passés à un seul thread utilisant epoll classique, ce qui a considérablement augmenté la vitesse et réduit le temps de traitement des transactions. Nous avons également supprimé les processus de communication réseau séparés et la communication via SystemV, réduit considérablement le nombre d'appels système et commencé à contrôler les priorités des opérations. Sur le seul sous-système d'E/S, il a été possible d'économiser environ 8 à 17 microsecondes, selon le scénario. Ce schéma à thread unique a été utilisé sans changement depuis lors ; un seul thread epoll avec une marge suffit pour gérer toutes les connexions.

Transaction en cours

La charge croissante de notre système a nécessité la mise à niveau de presque tous ses composants. Mais malheureusement, la stagnation de la croissance des vitesses d'horloge des processeurs ces dernières années n'a plus permis de faire évoluer les processus de front. Par conséquent, nous avons décidé de diviser le processus Engine en trois niveaux, le plus utilisé étant le système de vérification des risques, qui évalue la disponibilité des fonds sur les comptes et crée les transactions elles-mêmes. Mais l'argent peut être dans différentes devises, et il a fallu déterminer sur quelle base le traitement des demandes devait être réparti.

La solution logique est de le diviser par devise : un serveur négocie en dollars, un autre en livres sterling et un troisième en euros. Mais si, avec un tel schéma, deux transactions sont envoyées pour acheter des devises différentes, alors le problème de la désynchronisation du portefeuille se posera. Mais la synchronisation est difficile et coûteuse. Par conséquent, il serait correct de fragmenter séparément par portefeuilles et séparément par instruments. Soit dit en passant, la plupart des bourses occidentales n'ont pas pour tâche de contrôler les risques avec autant d'acuité que nous, c'est pourquoi cela se fait le plus souvent hors ligne. Nous devions mettre en œuvre une vérification en ligne.

Expliquons avec un exemple. Un trader souhaite acheter 30$, et la demande va jusqu'à la validation de la transaction : nous vérifions si ce trader est autorisé à ce mode de trading et s'il dispose des droits nécessaires. Si tout est en ordre, la demande est transmise au système de vérification des risques, c'est-à-dire vérifier la suffisance des fonds pour conclure une transaction. Il y a une note indiquant que le montant requis est actuellement bloqué. La demande est ensuite transmise au système commercial, qui approuve ou désapprouve la transaction. Disons que la transaction est approuvée - alors le système de vérification des risques indique que l'argent est débloqué et que les roubles se transforment en dollars.

En général, le système de contrôle des risques contient des algorithmes complexes et effectue un grand nombre de calculs très gourmands en ressources, et ne vérifie pas simplement le « solde du compte », comme cela peut paraître à première vue.

Lorsque nous avons commencé à diviser le processus Engine en niveaux, nous avons rencontré un problème : le code disponible à cette époque utilisait activement le même ensemble de données aux étapes de validation et de vérification, ce qui nécessitait de réécrire toute la base de code. En conséquence, nous avons emprunté une technique de traitement des instructions aux processeurs modernes : chacune d'elles est divisée en petites étapes et plusieurs actions sont effectuées en parallèle dans un cycle.

L'évolution de l'architecture du système de négociation et de compensation de la Bourse de Moscou. Partie 2

Après une petite adaptation du code, nous avons créé un pipeline pour le traitement parallèle des transactions, dans lequel la transaction était divisée en 4 étapes du pipeline : interaction réseau, validation, exécution et publication du résultat.

L'évolution de l'architecture du système de négociation et de compensation de la Bourse de Moscou. Partie 2

Regardons un exemple. Nous disposons de deux systèmes de traitement, en série et en parallèle. La première transaction arrive et est envoyée pour validation dans les deux systèmes. La deuxième transaction arrive immédiatement : dans un système parallèle, elle est immédiatement mise en œuvre, et dans un système séquentiel, elle est mise dans une file d'attente en attendant que la première transaction passe par l'étape de traitement en cours. Autrement dit, le principal avantage du traitement par pipeline est que nous traitons la file d'attente des transactions plus rapidement.

C'est ainsi que nous avons créé le système ASTS+.

Certes, tout ne se passe pas non plus aussi bien avec les convoyeurs. Disons que nous avons une transaction qui affecte les tableaux de données dans une transaction voisine ; c'est une situation typique pour un échange. Une telle transaction ne peut pas être exécutée dans un pipeline car elle peut affecter d’autres. Cette situation est appelée risque de données, et ces transactions sont simplement traitées séparément : lorsque les transactions « rapides » dans la file d'attente sont épuisées, le pipeline s'arrête, le système traite la transaction « lente », puis redémarre le pipeline. Heureusement, la proportion de ces transactions dans le flux global est très faible, de sorte que le pipeline s'arrête si rarement que cela n'affecte pas les performances globales.

L'évolution de l'architecture du système de négociation et de compensation de la Bourse de Moscou. Partie 2

Ensuite, nous avons commencé à résoudre le problème de la synchronisation de trois threads d'exécution. Le résultat fut un système basé sur un tampon en anneau avec des cellules de taille fixe. Dans ce système, tout est soumis à la vitesse de traitement ; les données ne sont pas copiées.

  • Tous les paquets réseau entrants entrent dans la phase d'allocation.
  • Nous les plaçons dans un tableau et les marquons comme disponibles pour l'étape n°1.
  • La deuxième transaction est arrivée, elle est à nouveau disponible pour l'étape n°1.
  • Le premier thread de traitement voit les transactions disponibles, les traite et les déplace vers l'étape suivante du deuxième thread de traitement.
  • Il traite ensuite la première transaction et marque la cellule correspondante deleted — il est désormais disponible pour une nouvelle utilisation.

La file d'attente entière est traitée de cette manière.

L'évolution de l'architecture du système de négociation et de compensation de la Bourse de Moscou. Partie 2

Le traitement de chaque étape prend des unités ou des dizaines de microsecondes. Et si nous utilisons des schémas de synchronisation standard du système d'exploitation, nous perdrons alors plus de temps sur la synchronisation elle-même. C'est pourquoi nous avons commencé à utiliser spinlock. Cependant, c'est une très mauvaise forme dans un système en temps réel, et RedHat ne recommande strictement pas de le faire, nous appliquons donc un spinlock pendant 100 ms, puis passons en mode sémaphore pour éliminer la possibilité d'un blocage.

En conséquence, nous avons atteint une performance d'environ 8 millions de transactions par seconde. Et littéralement deux mois plus tard, article à propos de LMAX Disruptor, nous avons vu une description d'un circuit avec la même fonctionnalité.

L'évolution de l'architecture du système de négociation et de compensation de la Bourse de Moscou. Partie 2

Il pourrait désormais y avoir plusieurs threads d'exécution en même temps. Toutes les transactions ont été traitées une par une, dans l'ordre de leur réception. En conséquence, les performances maximales sont passées de 18 50 à XNUMX XNUMX transactions par seconde.

Système de gestion des risques de change

Il n'y a pas de limite à la perfection et nous avons rapidement recommencé à nous moderniser : dans le cadre d'ASTS+, nous avons commencé à transformer les systèmes de gestion des risques et d'opérations de règlement en composants autonomes. Nous avons développé une architecture moderne et flexible et un nouveau modèle de risque hiérarchique, et avons essayé d'utiliser la classe autant que possible. fixed_point au lieu de double.

Mais un problème s'est immédiatement posé : comment synchroniser toute la logique métier qui fonctionne depuis de nombreuses années et la transférer vers le nouveau système ? En conséquence, la première version du prototype du nouveau système a dû être abandonnée. La deuxième version, qui est actuellement en production, est basée sur le même code, qui fonctionne à la fois dans la partie trading et dans la partie risque. Lors du développement, la chose la plus difficile à faire était de fusionner git entre deux versions. Notre collègue Evgeniy Mazurenok effectuait cette opération chaque semaine et à chaque fois il jurait très longtemps.

Lors de la sélection d'un nouveau système, nous avons immédiatement dû résoudre le problème de l'interaction. Lors du choix d'un bus de données, il était nécessaire de garantir une gigue stable et une latence minimale. Le réseau InfiniBand RDMA était le mieux adapté pour cela : le temps de traitement moyen est 4 fois inférieur à celui des réseaux Ethernet 10 G. Mais ce qui nous a vraiment captivés, c’est la différence entre les centiles – 99 et 99,9.

Bien sûr, InfiniBand a ses défis. Premièrement, une API différente - des ibverbs au lieu de sockets. Deuxièmement, il n’existe pratiquement aucune solution de messagerie open source largement disponible. Nous avons essayé de créer notre propre prototype, mais cela s'est avéré très difficile, nous avons donc choisi une solution commerciale - Confinity Low Latency Messaging (anciennement IBM MQ LLM).

Ensuite, la tâche de diviser correctement le système de risque s'est posée. Si vous supprimez simplement le Risk Engine et ne créez pas de nœud intermédiaire, les transactions provenant de deux sources peuvent être mélangées.

L'évolution de l'architecture du système de négociation et de compensation de la Bourse de Moscou. Partie 2

Les solutions dites Ultra Low Latency disposent d'un mode de réorganisation : les transactions provenant de deux sources peuvent être organisées dans l'ordre requis à la réception ; ceci est mis en œuvre à l'aide d'un canal séparé pour l'échange d'informations sur la commande. Mais nous n'utilisons pas encore ce mode : il complique l'ensemble du processus, et dans un certain nombre de solutions, il n'est pas du tout pris en charge. De plus, chaque transaction devrait se voir attribuer des horodatages correspondants, et dans notre schéma, ce mécanisme est très difficile à mettre en œuvre correctement. Par conséquent, nous avons utilisé le schéma classique avec un courtier de messages, c'est-à-dire avec un répartiteur qui distribue les messages entre le Risk Engine.

Le deuxième problème était lié à l'accès client : s'il existe plusieurs Risk Gateways, le client doit se connecter à chacune d'elles, ce qui nécessitera des modifications de la couche client. Nous voulions nous éloigner de cela à ce stade, c'est pourquoi la conception actuelle de Risk Gateway traite l'intégralité du flux de données. Cela limite considérablement le débit maximum, mais simplifie grandement l'intégration du système.

Reproduction

Notre système ne doit pas avoir un seul point de défaillance, c'est-à-dire que tous les composants doivent être dupliqués, y compris le courtier de messages. Nous avons résolu ce problème en utilisant le système CLLM : il contient un cluster RCMS dans lequel deux répartiteurs peuvent travailler en mode maître-esclave, et en cas de panne de l'un, le système passe automatiquement à l'autre.

Travailler avec un centre de données de sauvegarde

InfiniBand est optimisé pour fonctionner en tant que réseau local, c'est-à-dire pour connecter des équipements montés en rack, et un réseau InfiniBand ne peut pas être posé entre deux centres de données géographiquement répartis. Par conséquent, nous avons implémenté un pont/répartiteur, qui se connecte au stockage des messages via des réseaux Ethernet classiques et relaie toutes les transactions vers un deuxième réseau IB. Lorsque nous devons migrer depuis un centre de données, nous pouvons choisir avec quel centre de données travailler maintenant.

Les résultats de

Tout ce qui précède n’a pas été fait en une seule fois ; il a fallu plusieurs itérations pour développer une nouvelle architecture. Nous avons créé le prototype en un mois, mais il a fallu plus de deux ans pour le mettre en état de marche. Nous avons essayé d'atteindre le meilleur compromis entre l'augmentation du temps de traitement des transactions et l'augmentation de la fiabilité du système.

Le système ayant été fortement mis à jour, nous avons mis en œuvre la récupération de données à partir de deux sources indépendantes. Si la banque de messages ne fonctionne pas correctement pour une raison quelconque, vous pouvez extraire le journal des transactions d'une deuxième source, à partir du Risk Engine. Ce principe est observé dans tout le système.

Entre autres choses, nous avons pu préserver l'API client afin que ni les courtiers ni personne d'autre n'aient besoin de retravailler en profondeur la nouvelle architecture. Nous avons dû modifier certaines interfaces, mais il n'a pas été nécessaire d'apporter des modifications significatives au modèle opérationnel.

Nous avons appelé la version actuelle de notre plateforme Rebus - comme abréviation des deux innovations les plus notables de l'architecture, Risk Engine et BUS.

L'évolution de l'architecture du système de négociation et de compensation de la Bourse de Moscou. Partie 2

Au départ, nous voulions allouer uniquement la partie compensation, mais le résultat a été un énorme système distribué. Les clients peuvent désormais interagir avec Trade Gateway, Clearing Gateway ou les deux.

Ce que nous avons finalement réalisé :

L'évolution de l'architecture du système de négociation et de compensation de la Bourse de Moscou. Partie 2

Réduction du niveau de latence. Avec un petit volume de transactions, le système fonctionne de la même manière que la version précédente, mais peut en même temps supporter une charge beaucoup plus élevée.

Les performances maximales sont passées de 50 180 à XNUMX XNUMX transactions par seconde. Une nouvelle augmentation est entravée par le seul flux de correspondance des commandes.

Il existe deux manières d'améliorer davantage : paralléliser la correspondance et changer la façon dont il fonctionne avec Gateway. Désormais, toutes les passerelles fonctionnent selon un schéma de réplication qui, sous une telle charge, cesse de fonctionner normalement.

Enfin, je peux donner quelques conseils à ceux qui finalisent les systèmes d'entreprise :

  • Préparez-vous au pire à tout moment. Les problèmes surviennent toujours de manière inattendue.
  • Il est généralement impossible de refaire rapidement une architecture. Surtout si vous devez obtenir une fiabilité maximale sur plusieurs indicateurs. Plus il y a de nœuds, plus les ressources nécessaires au support sont importantes.
  • Toutes les solutions personnalisées et propriétaires nécessiteront des ressources supplémentaires pour la recherche, le support et la maintenance.
  • Ne tardez pas à résoudre les problèmes de fiabilité du système et de récupération après une panne ; tenez-en compte dès la phase de conception initiale.

Source: habr.com

Ajouter un commentaire