Implémentation de l'interface utilisateur d'OpenStack LBaaS



Lorsque j'ai implémenté l'interface utilisateur de l' équilibreur de charge pour un cloud privé virtuel, j'ai dû faire face à des difficultés importantes. Cela m'a amené à réfléchir sur le rôle du frontend, que je veux partager en premier lieu. Et puis justifier leurs pensées, en utilisant l'exemple d'une tâche spécifique.

La solution au problème s'est avérée, à mon avis, assez créative, et j'ai dû la chercher dans un cadre très limité, donc je pense que cela peut être intéressant.

Rôle frontal


Je dois dire tout de suite que je ne prétends pas la vérité et que je soulève une question controversée. Je suis quelque peu déprimé par l'ironie du front-end et du web en particulier, comme quelque chose d'insignifiant. Et il est encore plus déprimant que cela arrive parfois raisonnablement. Maintenant, la mode était déjà endormie, mais il fut un temps où tout le monde se promenait avec des cadres, des paradigmes et d'autres entités, disant haut et fort que tout cela était super important et super nécessaire, et en retour, ils ont reçu l'ironie que le front-end traite de la sortie des formulaires et traitement des clics sur les boutons, ce qui peut se faire «sur le genou».

Maintenant, il semble que tout soit plus ou moins revenu à la normale. Personne ne veut vraiment parler de chaque version mineure du prochain framework. Peu de gens recherchent l'outil ou l'approche parfaite, en raison de la prise de conscience croissante de leur utilité. Mais même cela, par exemple, n'interfère pas avec la réprimande presque déraisonnable d'Electron et ses applications. Je pense que cela est dû à un manque de compréhension de la tâche résolue par le front-end.

Le frontend n'est pas seulement un moyen d'afficher les informations fournies par le backend, et pas seulement un moyen de traiter les actions des utilisateurs. Le frontend est quelque chose de plus, quelque chose d'abstrait, et si vous lui donnez une définition simple et claire, alors le sens sera inévitablement perdu.

L'interface est dans un «cadre». Par exemple, en termes techniques, elle se situe entre l'API fournie par le backend et l'API fournie par les fonctionnalités d'E / S. En termes de tâches, c'est entre les tâches de l'interface utilisateur que UX résout et les tâches que le backend résout. Ainsi, une spécialisation frontend assez étroite est obtenue, une spécialisation de la couche. Cela ne signifie pas que les prestataires frontaux ne peuvent pas exercer d'influence sur des domaines en dehors de leur spécialisation, mais au moment où cette influence est impossible, la véritable tâche frontale se pose.

Ce problème peut s'exprimer à travers une contradiction. L'interface utilisateur n'est pas requise pour se conformer aux modèles de données et au comportement du backend. Les modèles de comportement et de données du backend ne sont pas requis pour s'adapter aux tâches de l'interface utilisateur. Et puis la tâche du front-end est d'éliminer cette contradiction. Plus l'écart entre les tâches du backend et l'interface utilisateur est important, plus le rôle du frontend est important. Et pour clarifier ce dont je parle, je vais donner un exemple où cet écart, pour une raison quelconque, s'est avéré important.

Énoncé du problème


OpenStack LBaaS, à mon avis, est un complexe matériel-logiciel d'outils nécessaires pour équilibrer la charge entre les serveurs. Il est important pour moi que sa mise en œuvre dépende de facteurs objectifs, de l'affichage physique. Pour cette raison, il y a des particularités dans l'API et dans les façons d'interagir avec cette API.

Lors du développement d'une interface utilisateur, l'intérêt principal n'est pas les caractéristiques techniques du backend, mais ses capacités fondamentales. L'interface est créée pour l'utilisateur, et l'utilisateur a besoin d'une interface pour gérer les paramètres d'équilibrage, et l'utilisateur n'a pas besoin de plonger dans les fonctionnalités internes de l'implémentation du backend.

Le backend est majoritairement développé par la communauté, et il est possible d'influencer son développement en quantités très limitées. L'une des principales caractéristiques pour moi est que les développeurs d'arrière-plan sont prêts à sacrifier la commodité et la simplicité des contrôles pour des raisons de performance, et cela est absolument justifié, car il s'agit d'équilibrer la charge.

Il y a encore un point subtil, et je veux le souligner immédiatement, en mettant en garde contre certaines questions. Il est clair que sur OpenStack et leur API, la lumière n'a pas convergé. Vous pouvez toujours développer votre propre ensemble d'outils ou une «couche» qui fonctionnera avec l'API OpenStack, produisant sa propre API qui convient aux tâches utilisateur. La seule question est l'opportunité. Si les outils initialement disponibles vous permettent d'implémenter l'interface utilisateur comme prévu, est-il judicieux de produire des entités?

La réponse à cette question est multiforme et pour les entreprises, elle reposera sur les développeurs, leur emploi, leur compétence, les questions de responsabilité, de support, etc. Dans notre cas, il était préférable de résoudre certaines des tâches sur le front-end.

Caractéristiques d'OpenStack LBaaS


Je veux identifier uniquement les fonctionnalités qui ont eu une forte influence sur l'interface. Les questions expliquant pourquoi ces fonctionnalités sont apparues ou sur lesquelles elles s'appuient dépassent déjà le cadre de cet article.

Je travaille avec une documentation toute faite et je dois accepter ses fonctionnalités. Ceux qui sont intéressés par ce qu'est OpenStack Octavia de l'intérieur peuvent se familiariser avec la documentation officielle . Octavia est le nom d'un ensemble d'outils conçus pour équilibrer la charge dans l'écosystème OpenStack.

La première caractéristique que j'ai rencontrée pendant le développement est le grand nombre de modèles et de relations nécessaires pour afficher l'état de l'équilibreur. L' API Octavia décrit 12 modèles, mais seulement 7 sont nécessaires pour le côté client. Ces modèles ont des connexions, souvent dénormalisées, l'image ci-dessous montre un schéma approximatif:



"Seven" ne semble pas très impressionnant, mais en réalité, pour assurer le bon fonctionnement de l'interface, au moment de la rédaction de ce texte, j'ai dû utiliser 16 modèles de données et environ 30 relations entre eux. Étant donné qu'Octavia n'est qu'un équilibreur, il nécessite que d'autres modules OpenStack fonctionnent. Et tout cela est nécessaire pour seulement deux pages dans l'interface utilisateur.

Les deuxième et troisième fonctionnalités sont Octavia asynchrones et transactionnelles. Les modèles de données ont un champ d' état qui reflète l'état des opérations effectuées sur un objet.
StatutLa description
ACTIFObjet en bon état
SUPPRIMÉObjet supprimé
ErreurL'objet est corrompu
PENDING_CREATEObjet en devenir
PENDING_UPDATEObjet en cours de mise à jour
PENDING_DELETEObjet en cours de suppression
L'opération de lecture d'un objet se produit de manière synchrone et n'a aucune restriction. Mais les opérations de création, de mise à jour et de suppression peuvent prendre un temps indéfini. Cela est précisément dû au fait que les modèles de données ont, en gros, une signification physique.

Après avoir envoyé une demande de création, nous pouvons savoir que l'enregistrement est apparu, nous pouvons le lire, mais tant que l'opération de création n'est pas terminée, nous ne pouvons effectuer aucune autre opération sur cet enregistrement. Une telle tentative entraînera une erreur. L'opération de modification d'un objet ne peut être lancée que lorsque l'objet est dans l'état ACTIF ; vous pouvez envoyer un objet pour suppression dans les états ACTIF et ERREUR .

Ces statuts peuvent provenir de WebSockets, ce qui facilite grandement leur traitement, mais les transactions sont un problème beaucoup plus important. Lors de la modification d'un objet, tous les modèles associés participeront également à la transaction. Par exemple, lorsque vous apportez des modifications à Member , le pool , l' écouteur et l' équilibreur de charge associés sont bloqués. Voici à quoi cela ressemble en termes d'événements reçus sur les sockets Web:

  • les quatre premiers événements sont le transfert d'objets à l'état PENDING_UPDATE : le champ cible contient le nom de modèle de l'objet participant à la transaction;
  • le cinquième événement n'est qu'un doublon (je ne sais pas à quoi il est lié);
  • les quatre derniers sont un retour à l'état ACTIF . Dans ce cas, il s'agit d'une opération de changement de poids, et cela prend moins d'une seconde, mais parfois cela prend beaucoup plus de temps.

Vous pouvez également voir dans la capture d'écran que l'ordre des événements n'a pas à être strict. Ainsi, il s'avère que pour lancer une opération, il est nécessaire de connaître non seulement le statut de l'objet lui-même, mais aussi les statuts de toutes les dépendances qui participeront également à la transaction.

Caractéristiques de l'interface utilisateur


Imaginez-vous maintenant à la place d'un utilisateur qui a besoin de savoir quelque part cela pour équilibrer entre deux serveurs:

  1. Il est nécessaire de créer un écouteur dans lequel l'algorithme d'équilibrage sera défini.
  2. Créez un pool.
  3. Attribuez un pool à l'écouteur.
  4. Ajoutez des liens vers des ports équilibrés au pool.

Chaque fois, il est nécessaire d'attendre la fin de l'opération, qui dépend de tous les objets créés précédemment.

Comme une étude interne l'a montré, de l'avis de l'utilisateur ordinaire, il n'y a qu'une prise de conscience approximative que l'équilibreur doit avoir un point d'entrée, il doit y avoir des points de sortie et les paramètres de l'équilibrage à effectuer: algorithme, poids et autres. L'utilisateur n'a pas besoin de savoir ce qu'est OpenStack.

Je ne sais pas à quel point l'interface devrait être compliquée pour la perception, où l'utilisateur lui-même doit suivre toutes les caractéristiques techniques du backend décrites ci-dessus. Pour la console, cela peut être autorisé, car son utilisation implique un niveau élevé d'immersion dans la technologie, mais pour le Web, une telle interface est horrible.

Sur le Web, l'utilisateur s'attend à remplir un formulaire clair et logique, à appuyer sur un bouton, à attendre et tout fonctionnera. Peut-être que cela peut être argumenté, mais je propose de me concentrer sur les fonctionnalités qui affectent la mise en œuvre du frontend.

L'interface a été conçue de manière à impliquer l'utilisation en cascade des opérations: une action dans l'interface peut impliquer plusieurs opérations. L'interface n'implique pas que l'utilisateur peut effectuer des actions qui ne sont actuellement pas possibles, mais l'interface suppose que l'utilisateur doit comprendre pourquoi il en est ainsi. L'interface est un tout unique et, par conséquent, ses éléments individuels peuvent utiliser des informations provenant de diverses entités dépendantes, y compris des méta-informations.



Si nous prenons en compte qu'il existe certaines fonctionnalités de l'interface qui ne sont pas uniques à l'équilibreur, telles que les commutateurs, les accordéons, les onglets, un menu contextuel et supposons que leurs principes de fonctionnement sont clairs au départ, alors je pense que pour un utilisateur qui sait ce qu'est l'équilibrage de charge, non il sera très difficile de lire la plupart de l'interface ci-dessus et de faire une hypothèse sur la façon de la gérer. Mais mettre en évidence quelles parties de l'interface sont cachées derrière les modèles de l'équilibreur, de l'écouteur, du pool, du membre et d'autres entités n'est plus la tâche la plus évidente.

Résoudre les contradictions


J'espère avoir pu montrer que les fonctionnalités du backend ne correspondent pas bien à l'interface et que ces fonctionnalités ne peuvent pas toujours être éliminées par le backend. Parallèlement à cela, les fonctionnalités de l'interface ne s'adaptent pas bien sur le backend, et ne peuvent pas toujours être éliminées sans compliquer l'interface. Chacun de ces domaines résout ses propres problèmes. La responsabilité du front-end est de résoudre les problèmes pour assurer le niveau d'interaction nécessaire entre l'interface et le back-end.

Dans ma pratique, je me suis immédiatement précipité dans la piscine avec ma tête, sans faire attention, ou plutôt sans même essayer de comprendre les caractéristiques qui sont plus élevées, mais j'ai eu de la chance ou l'expérience a aidé (et le bon vecteur a été choisi). J'ai moi-même remarqué à plusieurs reprises que lors de l'utilisation d'une API ou d'une bibliothèque tierce, il est très utile de se familiariser à l'avance avec la documentation: plus il y a de détails, mieux c'est. La documentation est souvent similaire, les gens comptent toujours sur l'expérience des autres, mais il y a une description des caractéristiques de chaque système individuel, et elle est contenue dans les détails.

Si j'avais initialement passé quelques heures supplémentaires à étudier la documentation, plutôt que d'extraire les informations nécessaires par mots clés, j'aurais pensé aux problèmes qui devraient être rencontrés, et cette connaissance pourrait avoir un impact sur l'architecture du projet dès les toutes premières étapes. Revenir en arrière pour éliminer les erreurs commises au tout début est très démoralisant. Et sans contexte complet, il faut parfois revenir plusieurs fois.

En option, vous pouvez plier votre ligne, générant progressivement de plus en plus de code «avec une bouchée», mais plus ce tas de code est, plus il sera ratissé à la fin. Lors de la conception de l'architecture, bien sûr, il ne faut pas plonger trop profondément, prendre en compte toutes les options possibles et impossibles, y consacrer beaucoup de temps, il est important de rechercher l'équilibre. Mais une connaissance plus ou moins détaillée de la documentation s'avère souvent être un investissement très utile et pas très long.

Néanmoins, dès le début, après avoir vu un grand nombre de modèles impliqués, j'ai réalisé qu'il serait nécessaire de construire un mappage de l'état du backend avec le client avec toutes les connexions préservées. Après avoir réussi à afficher toutes les informations nécessaires sur le client, avec toutes les connexions et ainsi de suite, il a été nécessaire d'organiser une file d'attente de tâches.

Les données sont mises à jour de manière asynchrone, la disponibilité des opérations est déterminée par diverses conditions et lorsque des opérations en cascade sont requises, aucune file d'attente ne peut être supprimée dans de telles conditions. Peut-être, en résumé, c'est toute l'architecture de ma solution: le stockage avec une réflexion de l'état du backend et de la file d'attente des tâches.

Architecture de la solution


En raison du nombre indéfini de modèles et de relations, j'ai mis l'évolutivité dans la structure du référentiel en le faisant à l'aide d'une fabrique qui renvoie une description déclarative des collections du référentiel. La collection a un service, une classe de modèle simple avec CRUD. Il serait possible de faire une description des liens dans le modèle, comme cela se fait, par exemple, dans RoR ou dans le bon vieux Backbone, mais cela nécessiterait une grande quantité de code à modifier. Par conséquent, la description des relations se trouve à côté de la classe modèle:



Au total, j'ai eu 2 types de connexions: un à un, un à plusieurs. La rétroaction peut également être décrite. En plus du type, la collection de dépendances est indiquée, le champ auquel la dépendance trouvée est attachée et le champ à partir duquel l'ID de l'objet dépendant est lu (en cas de communication un-à-plusieurs, la liste des ID est lue). Si la condition de communication d'un objet est plus compliquée que de simples liens vers des objets, alors en usine on peut décrire la fonction de tester deux objets, dont les résultats détermineront la présence d'une connexion. Tout cela ressemble un peu à «vélo», mais cela fonctionne sans dépendances inutiles et exactement comme il se doit.

Le référentiel dispose d'un module pour attendre l'ajout et la suppression d'une ressource, il traite essentiellement des événements ponctuels avec vérification conditionnelle et avec une interface promis. Lors de l'abonnement, le type d'événement (ajout, suppression), la fonction de test et le gestionnaire sont transmis. Lorsqu'un certain événement se produit et avec un résultat de test positif, le gestionnaire est exécuté, après quoi le suivi s'arrête. Un événement peut se produire lors de l'abonnement synchrone.

L'utilisation d'un tel modèle a permis d'apposer automatiquement des relations arbitrairement complexes entre les modèles et de le faire en un seul endroit. Cet endroit, j'ai appelé un tracker. Lors de l'ajout d'un objet au référentiel, il commence à suivre ses relations. Le module d'attente vous permet de répondre aux événements et de vérifier une connexion entre l'objet surveillé et l'objet dans le stockage. Si l'objet était déjà dans le référentiel, le module d'attente appelle immédiatement le gestionnaire.

Un tel périphérique de stockage vous permet de décrire n'importe quel nombre de collections et les relations entre elles. Lors de l'ajout et de la suppression d'objets, le magasin place ou réinitialise automatiquement les propriétés avec le contenu des objets dépendants. Les avantages de cette approche sont que toutes les relations sont décrites explicitement et qu'elles sont surveillées et mises à jour par un seul système; inconvénients - dans la complexité de la mise en œuvre et du débogage.

En général, un tel référentiel est plutôt trivial et je l'ai fait moi-même, car il serait beaucoup plus difficile d'intégrer une solution prête à l'emploi dans une base de code existante, mais il serait encore plus difficile d'attacher une file d'attente de tâches à une solution prête à l'emploi.

Toutes les tâches, comme les collections, ont une description déclarative et sont créées par l'usine. Les tâches peuvent avoir dans la description les conditions de démarrage et une liste des tâches qui devront être ajoutées à la file d'attente une fois celle-ci terminée.


L'exemple ci-dessus décrit la tâche de création d'un pool. Dans les dépendances, l'équilibreur et l'écouteur sont indiqués, par défaut, une vérification est effectuée pour l'état ACTIVE . L'objet de l'équilibreur est verrouillé, car les tâches de traitement dans la file d'attente peuvent se produire de manière synchrone, le verrouillage vous permet d'éviter les conflits au moment où la demande d'exécution a été envoyée, mais le statut n'a pas changé, mais il est supposé qu'il va changer. Au lieu de PARENT , si le pool est créé à la suite de la cascade de tâches, l' ID sera automatiquement remplacé.

Après avoir créé un pool, des tâches seront ajoutées à la file d'attente pour créer un moniteur de disponibilité et créer tous les membres de ce pool. La sortie est une structure qui peut être entièrement convertie en JSON. Ceci est fait pour pouvoir restaurer la file d'attente en cas d'échec.

La file d'attente, basée sur la description de la tâche, surveille indépendamment toutes les modifications dans le référentiel et vérifie les conditions qui doivent être remplies pour exécuter la tâche. Comme je l'ai déjà dit, les statuts proviennent de sockets Web, et il est très simple de générer les événements nécessaires pour la file d'attente, mais si nécessaire, il ne sera pas difficile d'attacher un mécanisme de mise à jour des données du minuteur (cela était initialement prévu dans l'architecture, car les sockets Web étaient pour diverses raisons peuvent ne pas fonctionner très stable). Une fois la tâche terminée, la file d'attente informe automatiquement le référentiel de la nécessité de mettre à jour les liens dans les objets spécifiés.

Conclusion


Le besoin d'évolutivité a conduit à une approche déclarative. La nécessité d'afficher les modèles et les relations entre eux a conduit à un référentiel unique. La nécessité de traiter des objets dépendants a conduit à la file d'attente.

La combinaison de ces besoins n'est peut-être pas la tâche la plus simple en termes de mise en œuvre (mais c'est une question distincte). Mais en termes d'architecture, la solution est très simple et vous permet d'éliminer toutes les contradictions entre les tâches du backend et de l'interface utilisateur, d'établir leur interaction et de jeter les bases d'autres fonctionnalités possibles de l'une des parties.

Du côté du panneau de commande Selectel , le processus d'équilibrage est simple et direct, ce qui permet aux clients du service de ne pas dépenser de ressources pour la mise en œuvre indépendante de l'équilibreur, tout en conservant la capacité de gérer le trafic de manière flexible.

Essayez notre équilibreur en action maintenant et écrivez votre avis dans les commentaires.

Source: https://habr.com/ru/post/fr455523/


All Articles