Éléments de base des applications distribuées. Deuxième approximation

annonce

Chers collègues, au milieu de l'été, je prévois de publier une autre série d'articles sur la conception de systèmes de file d'attente : « L'expérience VTrade » - une tentative d'écrire un cadre pour les systèmes de trading. La série examinera la théorie et la pratique de la création d'une bourse, d'une vente aux enchères et d'un magasin. A la fin de l'article, je vous invite à voter pour les sujets qui vous intéressent le plus.

Éléments de base des applications distribuées. Deuxième approximation

Ceci est le dernier article de la série sur les applications réactives distribuées dans Erlang/Elixir. DANS premier article vous pouvez y retrouver les fondements théoriques de l’architecture réactive. Deuxième article illustre les modèles et mécanismes de base pour la construction de tels systèmes.

Aujourd'hui, nous aborderons les questions de développement de la base de code et des projets en général.

Organisation des prestations

Dans la vraie vie, lors du développement d'un service, vous devez souvent combiner plusieurs modèles d'interaction dans un seul contrôleur. Par exemple, le service utilisateurs, qui résout le problème de la gestion des profils utilisateur du projet, doit répondre aux requêtes req-resp et signaler les mises à jour des profils via pub-sub. Ce cas est assez simple : derrière la messagerie se trouve un contrôleur qui implémente la logique du service et publie les mises à jour.

La situation se complique lorsque nous devons implémenter un service distribué tolérant aux pannes. Imaginons que les exigences des utilisateurs aient changé :

  1. maintenant le service devrait traiter les requêtes sur 5 nœuds de cluster,
  2. être capable d'effectuer des tâches de traitement en arrière-plan,
  3. et également être capable de gérer dynamiquement les listes d'abonnements pour les mises à jour de profil.

Remarque: Nous ne prenons pas en compte la question du stockage cohérent et de la réplication des données. Supposons que ces problèmes ont été résolus plus tôt et que le système dispose déjà d'une couche de stockage fiable et évolutive, et que les gestionnaires disposent de mécanismes pour interagir avec elle.

La description formelle du service utilisateurs est devenue plus compliquée. Du point de vue du programmeur, les changements sont minimes en raison de l'utilisation de la messagerie. Pour satisfaire la première exigence, nous devons configurer l’équilibrage au point d’échange req-resp.

La nécessité de traiter des tâches en arrière-plan se produit fréquemment. Chez les utilisateurs, il peut s'agir de vérifier les documents utilisateur, de traiter les fichiers multimédias téléchargés ou de synchroniser les données avec les médias sociaux. réseaux. Ces tâches doivent être réparties d'une manière ou d'une autre au sein du cluster et la progression de l'exécution doit être surveillée. Par conséquent, nous avons deux options de solution : soit utiliser le modèle de répartition des tâches de l'article précédent, soit, s'il ne convient pas, écrire un planificateur de tâches personnalisé qui gérera le pool de processeurs de la manière dont nous avons besoin.

Le point 3 nécessite l'extension de modèle pub-sub. Et pour la mise en œuvre, après avoir créé un point d'échange pub-sub, nous devons en plus lancer le contrôleur de ce point au sein de notre service. Ainsi, tout se passe comme si nous déplacions la logique de traitement des abonnements et des désabonnements de la couche messagerie vers la mise en œuvre des utilisateurs.

En conséquence, la décomposition du problème a montré que pour répondre aux exigences, nous devons lancer 5 instances du service sur différents nœuds et créer une entité supplémentaire - un contrôleur pub-sub, responsable de l'abonnement.
Pour exécuter 5 gestionnaires, vous n'avez pas besoin de modifier le code de service. La seule action supplémentaire est la mise en place de règles d'équilibrage au point d'échange, dont nous parlerons un peu plus loin.
Il existe également une complexité supplémentaire : le contrôleur pub-sub et le planificateur de tâches personnalisé doivent fonctionner en un seul exemplaire. Encore une fois, le service de messagerie, en tant que service fondamental, doit fournir un mécanisme de sélection d'un leader.

Choix du chef

Dans les systèmes distribués, l'élection du leader est la procédure de nomination d'un processus unique chargé de planifier le traitement distribué d'une certaine charge.

Dans les systèmes peu enclins à la centralisation, des algorithmes universels et consensuels, tels que paxos ou raft, sont utilisés.
Puisque la messagerie est un intermédiaire et un élément central, elle connaît tous les contrôleurs de service – les candidats leaders. La messagerie peut nommer un leader sans voter.

Après avoir démarré et connecté au point d'échange, tous les services reçoivent un message système #'$leader'{exchange = ?EXCHANGE, pid = LeaderPid, servers = Servers}. Si LeaderPid correspond à pid processus en cours, il est nommé leader, et la liste Servers inclut tous les nœuds et leurs paramètres.
Au moment où un nouveau nœud apparaît et qu'un nœud de cluster fonctionnel est déconnecté, tous les contrôleurs de service reçoivent #'$slave_up'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} и #'$slave_down'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} respectivement.

De cette façon, tous les composants sont conscients de tous les changements et le cluster est assuré d'avoir un leader à tout moment.

Посредники

Pour mettre en œuvre des processus de traitement distribué complexes, ainsi que dans les problèmes d'optimisation d'une architecture existante, il est pratique d'utiliser des intermédiaires.
Afin de ne pas modifier le code du service et de résoudre, par exemple, des problèmes de traitement supplémentaire, de routage ou de journalisation des messages, vous pouvez activer un gestionnaire proxy avant le service, qui effectuera tout le travail supplémentaire.

Un exemple classique d'optimisation pub-sub est une application distribuée avec un cœur de métier qui génère des événements de mise à jour, tels que des changements de prix sur le marché, et une couche d'accès - N serveurs qui fournissent une API websocket pour les clients Web.
Si vous décidez de front, alors le service client ressemble à ceci :

  • le client établit des connexions avec la plateforme. Du côté du serveur qui termine le trafic, un processus est lancé pour gérer cette connexion.
  • Dans le cadre du processus de service, l'autorisation et l'abonnement aux mises à jour ont lieu. Le processus appelle la méthode d'abonnement pour les sujets.
  • Une fois qu'un événement est généré dans le noyau, il est transmis aux processus gérant les connexions.

Imaginons que nous ayons 50000 5 abonnés au sujet « actualité ». Les abonnés sont répartis uniformément sur 50000 serveurs. De ce fait, chaque mise à jour arrivant au point d'échange sera répliquée 10000 XNUMX fois : XNUMX XNUMX fois sur chaque serveur, en fonction du nombre d'abonnés sur celui-ci. Ce n’est pas un programme très efficace, n’est-ce pas ?
Pour améliorer la situation, introduisons un proxy qui porte le même nom que le point d'échange. Le registraire global de noms doit être capable de renvoyer le processus le plus proche par son nom, c'est important.

Lançons ce proxy sur les serveurs de couche d'accès, et tous nos processus servant l'API websocket s'y abonneront, et non au point d'échange pub-sub d'origine dans le noyau. Proxy s'abonne au core uniquement dans le cas d'un abonnement unique et réplique le message entrant à tous ses abonnés.
De ce fait, 5 messages seront envoyés entre le noyau et les serveurs d'accès, au lieu de 50000 XNUMX.

Routage et équilibrage

Req-Resp

Dans l'implémentation actuelle de la messagerie, il existe 7 stratégies de distribution de requêtes :

  • default. La demande est envoyée à tous les contrôleurs.
  • round-robin. Les requêtes sont énumérées et distribuées cycliquement entre les contrôleurs.
  • consensus. Les contrôleurs qui servent le service sont divisés en chefs et esclaves. Les demandes sont envoyées uniquement au leader.
  • consensus & round-robin. Le groupe a un leader, mais les demandes sont réparties entre tous les membres.
  • sticky. La fonction de hachage est calculée et attribuée à un gestionnaire spécifique. Les requêtes suivantes avec cette signature sont adressées au même gestionnaire.
  • sticky-fun. Lors de l'initialisation du point d'échange, la fonction de calcul de hachage pour sticky équilibrage.
  • fun. Semblable à sticky-fun, vous seul pouvez le rediriger, le rejeter ou le pré-traiter.

La stratégie de distribution est définie lors de l'initialisation du point d'échange.

En plus de l'équilibrage, la messagerie vous permet de marquer des entités. Examinons les types de balises dans le système :

  • Balise de connexion. Permet de comprendre par quelle connexion les événements sont survenus. Utilisé lorsqu'un processus contrôleur se connecte au même point d'échange, mais avec des clés de routage différentes.
  • Numéro de service. Vous permet de combiner des gestionnaires en groupes pour un seul service et d'étendre les capacités de routage et d'équilibrage. Pour le modèle req-resp, le routage est linéaire. Nous envoyons une demande au point d'échange, puis celui-ci la transmet au service. Mais si nous devons diviser les gestionnaires en groupes logiques, la division se fait à l'aide de balises. Lors de la spécification d'une balise, la demande sera envoyée à un groupe spécifique de contrôleurs.
  • Balise de demande. Vous permet de distinguer les réponses. Notre système étant asynchrone, pour traiter les réponses du service, nous devons pouvoir spécifier un RequestTag lors de l'envoi d'une requête. De là, nous pourrons comprendre la réponse à laquelle la demande nous est parvenue.

Pub-sous

Pour le pub-sub, tout est un peu plus simple. Nous disposons d'un point d'échange sur lequel les messages sont publiés. Le point d'échange distribue les messages entre les abonnés ayant souscrit aux clés de routage dont ils ont besoin (on peut dire que c'est analogue aux sujets).

Évolutivité et tolérance aux pannes

L'évolutivité du système dans son ensemble dépend du degré d'évolutivité des couches et des composants du système :

  • Les services sont mis à l'échelle en ajoutant des nœuds supplémentaires au cluster avec des gestionnaires pour ce service. Pendant l'opération d'essai, vous pouvez choisir la politique d'équilibrage optimale.
  • Le service de messagerie lui-même au sein d'un cluster distinct est généralement mis à l'échelle soit en déplaçant des points d'échange particulièrement chargés vers des nœuds de cluster séparés, soit en ajoutant des processus proxy à des zones particulièrement chargées du cluster.
  • L'évolutivité de l'ensemble du système en tant que caractéristique dépend de la flexibilité de l'architecture et de la capacité de combiner des clusters individuels en une entité logique commune.

Le succès d’un projet dépend souvent de la simplicité et de la rapidité de sa mise à l’échelle. La messagerie dans sa version actuelle évolue avec l'application. Même s’il nous manque un cluster de 50 à 60 machines, nous pouvons recourir à la fédération. Malheureusement, le sujet de la fédération dépasse le cadre de cet article.

Réservation

Lors de l'analyse de l'équilibrage de charge, nous avons déjà évoqué la redondance des contrôleurs de service. Cependant, la messagerie doit également être réservée. En cas de panne d'un nœud ou d'une machine, la messagerie devrait récupérer automatiquement et dans les plus brefs délais.

Dans mes projets, j'utilise des nœuds supplémentaires qui reprennent la charge en cas de chute. Erlang dispose d'une implémentation standard en mode distribué pour les applications OTP. Le mode distribué effectue une récupération en cas d'échec en lançant l'application défaillante sur un autre nœud précédemment lancé. Le processus est transparent : après une panne, l'application se déplace automatiquement vers le nœud de basculement. Vous pouvez en savoir plus sur cette fonctionnalité ici.

Performance

Essayons de comparer au moins approximativement les performances de RabbitMQ et de notre messagerie personnalisée.
j'ai trouvé résultats officiels Tests RabbitMQ de l'équipe OpenStack.

Au paragraphe 6.14.1.2.1.2.2. Le document original montre le résultat du RPC CAST :
Éléments de base des applications distribuées. Deuxième approximation

Nous n'effectuerons aucun paramètre supplémentaire sur le noyau du système d'exploitation ou la VM erlang à l'avance. Conditions de test :

  • erl opte : +A1 +sbtu.
  • Le test au sein d'un seul nœud erlang est exécuté sur un ordinateur portable avec un ancien i7 en version mobile.
  • Les tests de cluster sont effectués sur des serveurs dotés d'un réseau 10G.
  • Le code s'exécute dans des conteneurs Docker. Réseau en mode NAT.

Code d'essai :

req_resp_bench(_) ->
  W = perftest:comprehensive(10000,
    fun() ->
      messaging:request(?EXCHANGE, default, ping, self()),
      receive
        #'$msg'{message = pong} -> ok
      after 5000 ->
        throw(timeout)
      end
    end
  ),
  true = lists:any(fun(E) -> E >= 30000 end, W),
  ok.

Script 1: Le test est exécuté sur un ordinateur portable doté d’une ancienne version mobile i7. Le test, la messagerie et le service sont exécutés sur un nœud dans un conteneur Docker :

Sequential 10000 cycles in ~0 seconds (26987 cycles/s)
Sequential 20000 cycles in ~1 seconds (26915 cycles/s)
Sequential 100000 cycles in ~4 seconds (26957 cycles/s)
Parallel 2 100000 cycles in ~2 seconds (44240 cycles/s)
Parallel 4 100000 cycles in ~2 seconds (53459 cycles/s)
Parallel 10 100000 cycles in ~2 seconds (52283 cycles/s)
Parallel 100 100000 cycles in ~3 seconds (49317 cycles/s)

Script 2: 3 nœuds fonctionnant sur des machines différentes sous docker (NAT).

Sequential 10000 cycles in ~1 seconds (8684 cycles/s)
Sequential 20000 cycles in ~2 seconds (8424 cycles/s)
Sequential 100000 cycles in ~12 seconds (8655 cycles/s)
Parallel 2 100000 cycles in ~7 seconds (15160 cycles/s)
Parallel 4 100000 cycles in ~5 seconds (19133 cycles/s)
Parallel 10 100000 cycles in ~4 seconds (24399 cycles/s)
Parallel 100 100000 cycles in ~3 seconds (34517 cycles/s)

Dans tous les cas, l'utilisation du processeur n'a pas dépassé 250 %

Les résultats de

J'espère que ce cycle ne ressemble pas à une décharge mentale et que mon expérience sera réellement bénéfique à la fois aux chercheurs en systèmes distribués et aux praticiens qui en sont au tout début à la construction d'architectures distribuées pour leurs systèmes d'entreprise et qui regardent Erlang/Elixir avec intérêt. , mais j'ai des doutes, est-ce que ça vaut le coup...

photo @chuttersnap

Seuls les utilisateurs enregistrés peuvent participer à l'enquête. se connecters'il te plait.

Quels sujets devrais-je aborder plus en détail dans le cadre de la série VTrade Experiment ?

  • Théorie : Marchés, ordres et leur timing : DAY, GTD, GTC, IOC, FOK, MOO, MOC, LOO, LOC

  • Carnet de commandes. Théorie et pratique de la mise en œuvre d'un livre avec des regroupements

  • Visualisation du trading : ticks, barres, résolutions. Comment stocker et comment coller

  • Back-office. Planification et développement. Surveillance des employés et enquête sur les incidents

  • API. Voyons quelles interfaces sont nécessaires et comment les implémenter

  • Stockage des informations : PostgreSQL, Timescale, Tarantool dans les systèmes de trading

  • Réactivité dans les systèmes de trading

  • Autre. j'écrirai dans les commentaires

6 utilisateurs ont voté. 4 utilisateurs se sont abstenus.

Source: habr.com

Ajouter un commentaire