OpenStack LBaaS UI-Implementierung



Als ich die Benutzeroberfläche des Load Balancers für eine virtuelle private Cloud implementierte , hatte ich erhebliche Schwierigkeiten. Dies veranlasste mich, über die Rolle des Frontends nachzudenken, die ich zunächst teilen möchte. Und dann begründen Sie ihre Gedanken am Beispiel einer bestimmten Aufgabe.

Die Lösung des Problems erwies sich meiner Meinung nach als ziemlich kreativ, und ich musste in einem sehr begrenzten Rahmen danach suchen, daher denke ich, dass es interessant sein kann.

Frontend-Rolle


Ich muss sofort sagen, dass ich nicht die Wahrheit vortäusche und ein kontroverses Thema anspreche. Ich bin etwas deprimiert von der Ironie des Frontends und insbesondere des Webs als etwas Unbedeutendes. Und es ist noch bedrückender, dass dies manchmal vernünftig geschieht. Jetzt schlief die Mode bereits, aber es gab eine Zeit, in der alle mit Frameworks, Paradigmen und anderen Entitäten herumliefen und laut sagten, dass all dies überaus wichtig und überaus notwendig sei, und im Gegenzug erhielten sie eine Ironie, dass sich das Frontend mit der Ausgabe von Formen und Formen befasst Verarbeitung Klicks auf Tasten, die "auf dem Knie" erfolgen können.

Nun scheint sich alles mehr oder weniger wieder normalisiert zu haben. Niemand möchte wirklich über jede kleinere Version des nächsten Frameworks sprechen. Aufgrund des zunehmenden Bewusstseins für ihren Nutzen suchen nur wenige Menschen nach dem perfekten Werkzeug oder Ansatz. Aber auch dies beeinträchtigt beispielsweise nicht die fast ungerechtfertigte Kritik an Electron und seinen Anwendungen. Ich denke, das liegt an einem Mangel an Verständnis für die Aufgabe, die vom Frontend gelöst wird.

Das Frontend ist nicht nur ein Mittel zum Anzeigen der vom Backend bereitgestellten Informationen und nicht nur ein Mittel zum Verarbeiten von Benutzeraktionen. Das Frontend ist etwas mehr, etwas Abstraktes, und wenn Sie es einfach und klar definieren, geht die Bedeutung unweigerlich verloren.

Das Frontend befindet sich in einem „Rahmen“. Technisch gesehen liegt dies beispielsweise zwischen der vom Backend bereitgestellten API und der von den E / A-Einrichtungen bereitgestellten API. In Bezug auf Aufgaben liegt es zwischen den Aufgaben der Benutzeroberfläche, die UX löst, und den Aufgaben, die das Backend löst. Somit wird eine ziemlich enge Frontend-Spezialisierung erhalten, eine Spezialisierung der Schicht. Dies bedeutet nicht, dass Front-End-Anbieter Bereiche außerhalb ihrer Spezialisierung nicht beeinflussen können, aber in dem Moment, in dem dieser Einfluss unmöglich ist, entsteht die eigentliche Front-End-Aufgabe.

Dieses Problem kann durch einen Widerspruch ausgedrückt werden. Die Benutzeroberfläche muss nicht den Datenmodellen und dem Backend-Verhalten entsprechen. Das Verhalten und die Datenmodelle des Backends sind nicht erforderlich, um den Aufgaben der Benutzeroberfläche zu entsprechen. Und dann ist es die Aufgabe des Frontends, diesen Widerspruch zu beseitigen. Je größer die Diskrepanz zwischen den Aufgaben des Backends und der Benutzeroberfläche ist, desto wichtiger ist die Rolle des Frontends. Und um klar zu machen, wovon ich spreche, werde ich ein Beispiel geben, bei dem sich diese Diskrepanz aus irgendeinem Grund als signifikant herausgestellt hat.

Erklärung des Problems


OpenStack LBaaS ist meiner Meinung nach ein Hardware-Software-Komplex von Tools, die zum Lastausgleich zwischen Servern erforderlich sind. Für mich ist es wichtig, dass die Umsetzung von objektiven Faktoren und der physischen Darstellung abhängt. Aus diesem Grund gibt es einige Besonderheiten in der API und in der Art der Interaktion mit dieser API.

Bei der Entwicklung einer Benutzeroberfläche liegt das Hauptinteresse nicht auf den technischen Merkmalen des Backends, sondern auf seinen grundlegenden Funktionen. Die Schnittstelle wird für den Benutzer erstellt, und der Benutzer benötigt eine Schnittstelle zum Verwalten von Ausgleichsparametern, und der Benutzer muss nicht in die internen Funktionen der Backend-Implementierung eintauchen.

Das Backend wird größtenteils von der Community entwickelt, und es ist möglich, seine Entwicklung in sehr begrenzten Mengen zu beeinflussen. Eines der Hauptmerkmale für mich ist, dass die Backend-Entwickler bereit sind, die Bequemlichkeit und Einfachheit von Steuerelementen aus Gründen der Leistung zu opfern, und dies ist absolut gerechtfertigt, da es darum geht, die Last auszugleichen.

Es gibt noch einen subtileren Punkt, und ich möchte ihn sofort skizzieren und einige Fragen warnen. Es ist klar, dass auf OpenStack und ihrer API das Licht nicht konvergierte. Sie können jederzeit Ihre eigenen Tools oder eine „Ebene“ entwickeln, die mit der OpenStack-API zusammenarbeitet und eine eigene API erstellt, die für Benutzeraufgaben geeignet ist. Die Frage ist nur die Zweckmäßigkeit. Wenn anfangs verfügbare Tools es Ihnen ermöglichen, die Benutzeroberfläche wie beabsichtigt zu implementieren, ist es sinnvoll, Entitäten zu erstellen?

Die Antwort auf diese Frage ist vielfältig und für Unternehmen hängt sie von Entwicklern, ihrer Beschäftigung, ihrer Kompetenz, Verantwortungsfragen, Unterstützung usw. ab. In unserem Fall war es am zweckmäßigsten, einige der Aufgaben im Frontend zu lösen.

Funktionen von OpenStack LBaaS


Ich möchte nur die Funktionen identifizieren, die einen starken Einfluss auf das Frontend hatten. Fragen, warum diese Funktionen entstanden sind oder worauf sie sich stützen, gehen bereits über den Rahmen dieses Artikels hinaus.

Ich arbeite mit vorgefertigten Dokumentationen und muss deren Funktionen akzeptieren. Wer sich von innen für OpenStack Octavia interessiert, kann sich mit der offiziellen Dokumentation vertraut machen. Octavia ist der Name einer Reihe von Tools, mit denen die Last im OpenStack-Ökosystem ausgeglichen werden kann.

Das erste Merkmal, auf das ich während der Entwicklung gestoßen bin, ist die große Anzahl von Modellen und Beziehungen, die zum Anzeigen des Status des Balancers erforderlich sind. Die Octavia-API beschreibt 12 Modelle, für die Clientseite werden jedoch nur 7 benötigt. Diese Modelle verfügen über häufig denormalisierte Verbindungen. Das folgende Bild zeigt ein ungefähres Diagramm:



"Sieben" klingt nicht sehr beeindruckend, aber in Wirklichkeit musste ich zum Zeitpunkt des Schreibens dieses Textes 16 Datenmodelle und etwa 30 Beziehungen zwischen ihnen verwenden, um den vollen Betrieb der Benutzeroberfläche sicherzustellen. Da Octavia nur ein Balancer ist, müssen andere OpenStack-Module funktionieren. Und das alles wird nur für zwei Seiten in der Benutzeroberfläche benötigt.

Das zweite und dritte Merkmal sind asynchrone und transaktionale Octavia. Datenmodelle verfügen über ein Statusfeld , das den Status der an einem Objekt ausgeführten Operationen widerspiegelt.
StatusBeschreibung
AKTIVObjekt in gutem Zustand
GELÖSCHTObjekt gelöscht
FehlerObjekt ist beschädigt
PENDING_CREATEObjekt in der Herstellung
PENDING_UPDATEObjekt wird gerade aktualisiert
PENDING_DELETEObjekt wird gerade gelöscht
Der Vorgang des Lesens eines Objekts erfolgt synchron und unterliegt keinen Einschränkungen. Das Erstellen, Aktualisieren und Löschen von Vorgängen kann jedoch unbegrenzt lange dauern. Dies liegt genau daran, dass Datenmodelle grob gesagt eine physikalische Bedeutung haben.

Nachdem wir eine Anforderung zur Erstellung gesendet haben, können wir wissen, dass der Datensatz angezeigt wurde, wir können ihn lesen, aber bis der Erstellungsvorgang abgeschlossen ist, können wir keine anderen Vorgänge für diesen Datensatz ausführen. Ein solcher Versuch führt zu einem Fehler. Das Ändern eines Objekts kann nur gestartet werden, wenn sich das Objekt im Status AKTIV befindet . Sie können ein Objekt zum Löschen im Status AKTIV und FEHLER senden.

Diese Status können über WebSockets erfolgen, was ihre Verarbeitung erheblich erleichtert. Transaktionen sind jedoch ein viel größeres Problem. Wenn Sie Änderungen an einem Objekt vornehmen, nehmen auch alle zugehörigen Modelle an der Transaktion teil. Wenn Sie beispielsweise Änderungen an Member vornehmen, werden der zugehörige Pool , Listener und Loadbalancer blockiert. So sieht es in Bezug auf Ereignisse aus, die auf Web-Sockets empfangen werden:

  • Die ersten vier Ereignisse sind die Übertragung von Objekten in den Status PENDING_UPDATE : Das Zielfeld enthält den Modellnamen des an der Transaktion beteiligten Objekts.
  • Das fünfte Ereignis ist nur ein Duplikat (ich weiß nicht, womit es verbunden ist).
  • Die letzten vier sind eine Rückkehr zum Status AKTIV . In diesem Fall handelt es sich um eine Gewichtsänderungsoperation, die weniger als eine Sekunde dauert, manchmal aber auch viel länger.

Sie können auch im Screenshot sehen, dass die Reihenfolge der Ereignisse nicht streng sein muss. Es stellt sich also heraus, dass zum Einleiten einer Operation nicht nur der Status des Objekts selbst bekannt sein muss, sondern auch der Status aller Abhängigkeiten, die ebenfalls an der Transaktion teilnehmen.

Funktionen der Benutzeroberfläche


Stellen Sie sich nun an die Stelle eines Benutzers, der irgendwo wissen muss, dass zwischen zwei Servern ausgeglichen werden soll:

  1. Es ist erforderlich, einen Listener zu erstellen, in dem der Ausgleichsalgorithmus definiert wird.
  2. Erstellen Sie einen Pool.
  3. Weisen Sie dem Listener einen Pool zu.
  4. Fügen Sie dem Pool Links zu ausgeglichenen Ports hinzu.

Jedes Mal muss auf den Abschluss des Vorgangs gewartet werden, der von allen zuvor erstellten Objekten abhängt.

Wie eine interne Studie gezeigt hat, gibt es nach Ansicht des normalen Benutzers nur eine ungefähre Erkenntnis, dass der Balancer einen Einstiegspunkt haben muss, dass Austrittspunkte vorhanden sein müssen und die Parameter des auszuführenden Ausgleichs: Algorithmus, Gewicht und andere. Der Benutzer muss nicht wissen, was OpenStack ist.

Ich weiß nicht, wie kompliziert die Benutzeroberfläche für die Wahrnehmung sein sollte, bei der der Benutzer selbst alle technischen Funktionen des oben beschriebenen Backends befolgen muss. Für die Konsole mag dies zulässig sein, da ihre Verwendung ein hohes Maß an Eintauchen in die Technologie impliziert, aber für das Web ist eine solche Schnittstelle schrecklich.

Im Web erwartet der Benutzer, dass er ein klares und logisches Formular ausfüllt, eine Taste drückt, wartet und alles funktioniert. Vielleicht kann dies argumentiert werden, aber ich schlage vor, mich auf die Funktionen zu konzentrieren, die die Implementierung des Frontends beeinflussen.

Die Schnittstelle wurde so konzipiert, dass Operationen kaskadiert verwendet werden: Eine Aktion in der Schnittstelle kann mehrere Operationen umfassen. Die Schnittstelle bedeutet nicht, dass der Benutzer Aktionen ausführen kann, die derzeit nicht möglich sind. Die Schnittstelle geht jedoch davon aus, dass der Benutzer verstehen muss, warum dies so ist. Die Schnittstelle ist ein einzelnes Ganzes, und daher können ihre einzelnen Elemente Informationen von verschiedenen abhängigen Entitäten, einschließlich Metainformationen, verwenden.



Wenn wir berücksichtigen, dass es einige Funktionen der Benutzeroberfläche gibt, die nicht nur für den Balancer gelten, wie z. B. Schalter, Akkordeons, Registerkarten, ein Kontextmenü, und davon ausgehen, dass ihre Funktionsprinzipien zunächst klar sind, denke ich nicht an einen Benutzer, der versteht, was Lastausgleich ist Es wird sehr schwierig sein, den größten Teil der obigen Benutzeroberfläche zu lesen und davon auszugehen, wie sie verwaltet wird. Es ist jedoch nicht mehr die naheliegendste Aufgabe, hervorzuheben, welche Teile der Benutzeroberfläche hinter den Modellen von Balancer, Listener, Pool, Mitglied und anderen Entitäten verborgen sind.

Widersprüche lösen


Ich hoffe, ich konnte zeigen, dass die Funktionen des Backends nicht gut zur Benutzeroberfläche passen und dass diese Funktionen nicht immer vom Backend entfernt werden können. Außerdem passen die Funktionen der Benutzeroberfläche nicht gut in das Backend und können auch nicht immer entfernt werden, ohne die Benutzeroberfläche zu komplizieren. Jeder dieser Bereiche löst seine eigenen Probleme. Die Verantwortung des Frontends besteht darin, Probleme zu lösen, um das erforderliche Maß an Interaktion zwischen der Schnittstelle und dem Backend sicherzustellen.

In meiner Praxis eilte ich sofort mit dem Kopf in den Pool, ohne darauf zu achten oder vielmehr nicht einmal zu versuchen, die höheren Merkmale herauszufinden, aber ich hatte Glück oder die Erfahrung half (und der richtige Vektor wurde ausgewählt). Ich habe wiederholt festgestellt, dass es bei Verwendung einer API oder Bibliothek eines Drittanbieters sehr nützlich ist, sich im Voraus mit der Dokumentation vertraut zu machen: Je mehr Details, desto besser. Die Dokumentation ist oft ähnlich, die Menschen verlassen sich immer noch auf die Erfahrungen anderer Menschen, aber es gibt eine Beschreibung der Merkmale jedes einzelnen Systems und sie ist in den Details enthalten.

Wenn ich anfänglich ein paar zusätzliche Stunden damit verbracht hätte, die Dokumentation zu studieren, anstatt die erforderlichen Informationen anhand von Schlüsselwörtern herauszuholen, hätte ich über die Probleme nachgedacht, mit denen ich konfrontiert werden müsste, und dieses Wissen könnte sich von Anfang an auf die Projektarchitektur auswirken. Zurück zu gehen, um Fehler zu beseitigen, die am Anfang gemacht wurden, ist sehr demoralisierend. Und ohne einen vollständigen Kontext muss man manchmal mehrmals zurückkommen.

Optional können Sie Ihre Linie biegen und nach und nach immer mehr Code „mit einem Biss“ generieren. Je mehr dieser Codehaufen jedoch vorhanden ist, desto mehr wird er am Ende geharkt. Bei der Gestaltung der Architektur sollte man natürlich nicht zu tief eintauchen, alle möglichen und unmöglichen Optionen berücksichtigen und viel Zeit damit verbringen. Es ist wichtig, ein Gleichgewicht zu finden. Eine mehr oder weniger detaillierte Kenntnis der Dokumentation erweist sich jedoch häufig als sehr nützliche Investition, die nicht sehr viel Zeit in Anspruch nimmt.

Trotzdem wurde mir von Anfang an klar, dass es notwendig sein würde, eine Zuordnung des Backend-Status zum Client zu erstellen, wobei alle Verbindungen erhalten bleiben, nachdem ich eine große Anzahl von beteiligten Modellen gesehen hatte. Nachdem es mir gelungen war, alle erforderlichen Informationen auf dem Client mit allen Verbindungen usw. anzuzeigen, musste eine Aufgabenwarteschlange organisiert werden.

Daten werden asynchron aktualisiert, die Verfügbarkeit von Operationen wird durch eine Vielzahl von Bedingungen bestimmt, und wenn kaskadierende Operationen erforderlich sind, kann unter solchen Bedingungen auf keine Warteschlange verzichtet werden. Kurz gesagt, dies ist vielleicht die gesamte Architektur meiner Lösung: Speicher mit einer Reflexion des Backend-Status und der Task-Warteschlange.

Lösungsarchitektur


Aufgrund der unbestimmten Anzahl von Modellen und Beziehungen habe ich die Struktur des Repositorys skalierbar gemacht, indem ich eine Factory verwendet habe, die eine deklarative Beschreibung der Repository-Sammlungen zurückgibt. Die Sammlung hat einen Service, eine einfache Modellklasse mit CRUD. Es wäre möglich, eine Beschreibung der Links im Modell zu erstellen, wie dies beispielsweise in RoR oder im guten alten Backbone der Fall ist, dies würde jedoch eine große Menge an Code erfordern, die geändert werden muss. Daher liegt die Beschreibung der Beziehungen neben der Modellklasse:



Insgesamt habe ich zwei Arten von Verbindungen: eine zu eins, eine zu viele. Feedback kann auch beschrieben werden. Zusätzlich zum Typ wird die Sammlung von Abhängigkeiten angegeben, das Feld, an das die gefundene Abhängigkeit angehängt ist, und das Feld, aus dem die ID des abhängigen Objekts gelesen wird (bei einer Eins-zu-Viele-Kommunikation wird die Liste der IDs gelesen). Wenn die Kommunikationsbedingung eines Objekts komplizierter ist als einfache Verknüpfungen mit Objekten, kann im Werk die Funktion zum Testen von zwei Objekten beschrieben werden, deren Ergebnisse das Vorhandensein einer Verbindung bestimmen. Es sieht alles ein bisschen "Fahrrad" aus, aber es funktioniert ohne unnötige Abhängigkeiten und genau so, wie es sollte.

Das Repository verfügt über ein Modul zum Warten auf das Hinzufügen und Löschen einer Ressource. Im Wesentlichen verarbeitet es einmalige Ereignisse mit bedingter Überprüfung und mit einer Promis-Schnittstelle. Beim Abonnieren werden der Ereignistyp (Hinzufügen, Löschen), die Testfunktion und der Handler übergeben. Wenn ein bestimmtes Ereignis eintritt und ein positives Testergebnis vorliegt, wird der Handler ausgeführt, wonach die Verfolgung gestoppt wird. Beim synchronen Abonnieren kann ein Ereignis auftreten.

Die Verwendung eines solchen Musters ermöglichte es, beliebig komplexe Beziehungen zwischen Modellen automatisch an einer Stelle anzubringen. Diesen Ort habe ich einen Tracker genannt. Wenn Sie dem Repository ein Objekt hinzufügen, beginnt es, seine Beziehungen zu verfolgen. Mit dem Wartemodul können Sie auf Ereignisse reagieren und nach einer Verbindung zwischen dem überwachten Objekt und dem Objekt im Speicher suchen. Wenn sich das Objekt bereits im Repository befand, ruft das Wartemodul den Handler sofort auf.

Mit einem solchen Speichergerät können Sie eine beliebige Anzahl von Sammlungen und die Beziehungen zwischen ihnen beschreiben. Beim Hinzufügen und Löschen von Objekten fügt der Speicher Eigenschaften mit dem Inhalt abhängiger Objekte automatisch ein oder setzt sie zurück. Die Vorteile dieses Ansatzes bestehen darin, dass alle Beziehungen explizit beschrieben werden und von einem System überwacht und aktualisiert werden. Nachteile - in der Komplexität der Implementierung und des Debuggens.

Im Allgemeinen ist ein solches Repository eher trivial, und ich habe es selbst gemacht, da es viel schwieriger wäre, eine vorgefertigte Lösung in eine vorhandene Codebasis zu integrieren, es jedoch noch schwieriger wäre, eine Aufgabenwarteschlange an eine vorgefertigte Lösung anzuhängen.

Alle Aufgaben, wie Sammlungen, haben eine deklarative Beschreibung und werden von der Fabrik erstellt. Aufgaben können in der Beschreibung die Startbedingungen und eine Liste von Aufgaben enthalten, die nach Abschluss der aktuellen Aufgabe zur Warteschlange hinzugefügt werden müssen.


Das obige Beispiel beschreibt die Aufgabe, einen Pool zu erstellen. In den Abhängigkeiten werden der Balancer und der Listener angezeigt. Standardmäßig wird eine Überprüfung des ACTIVE- Status durchgeführt. Das Objekt des Balancers ist gesperrt, da Verarbeitungsaufgaben in der Warteschlange synchron ausgeführt werden können. Durch das Sperren können Sie Konflikte zum Zeitpunkt des Sendens der Ausführungsanforderung vermeiden, der Status hat sich jedoch nicht geändert, es wird jedoch davon ausgegangen, dass er sich ändert. Wenn der Pool als Ergebnis der Kaskade von Aufgaben erstellt wird, wird anstelle von PARENT die ID automatisch ersetzt.

Nach dem Erstellen eines Pools werden der Warteschlange Aufgaben hinzugefügt, um einen Verfügbarkeitsmonitor zu erstellen und alle Mitglieder dieses Pools zu erstellen. Die Ausgabe ist eine Struktur, die vollständig in JSON konvertiert werden kann. Dies geschieht, um die Warteschlange im Fehlerfall wiederherstellen zu können.

Die Warteschlange überwacht basierend auf der Aufgabenbeschreibung unabhängig alle Änderungen im Repository und überprüft die Bedingungen, die zum Ausführen der Aufgabe erfüllt sein müssen. Wie ich bereits sagte, werden Status über Web-Sockets gesendet, und es ist sehr einfach, die erforderlichen Ereignisse für die Warteschlange zu generieren. Bei Bedarf ist es jedoch kein Problem, einen Mechanismus zur Aktualisierung der Zeitgeberdaten anzuhängen (dies wurde ursprünglich in der Architektur festgelegt, da dies Web-Sockets waren aus verschiedenen Gründen möglicherweise nicht sehr stabil arbeiten). Nach Abschluss der Aufgabe informiert die Warteschlange das Repository automatisch über die Notwendigkeit, die Links in den angegebenen Objekten zu aktualisieren.

Fazit


Die Notwendigkeit der Skalierbarkeit hat zu einem deklarativen Ansatz geführt. Die Notwendigkeit, Modelle und die Beziehungen zwischen ihnen anzuzeigen, hat zu einem einzigen Repository geführt. Die Notwendigkeit, abhängige Objekte zu verarbeiten, hat zur Warteschlange geführt.

Die Kombination dieser Anforderungen ist möglicherweise nicht die einfachste Aufgabe in Bezug auf die Implementierung (dies ist jedoch ein separates Problem). In Bezug auf die Architektur ist die Lösung jedoch sehr einfach und ermöglicht es Ihnen, alle Widersprüche zwischen den Aufgaben des Backends und der Benutzeroberfläche zu beseitigen, deren Interaktion herzustellen und die Grundlage für andere mögliche Funktionen einer der Parteien zu legen.

Auf der Seite des Selectel- Kontrollfelds ist der Ausgleichsprozess einfach und unkompliziert. Dadurch können Servicekunden keine Ressourcen für die unabhängige Implementierung des Balancers ausgeben und gleichzeitig den Datenverkehr flexibel steuern.

Testen Sie unseren Balancer jetzt in Aktion und schreiben Sie Ihre Bewertung in die Kommentare.

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


All Articles