Wie fehlertolerante Webarchitektur in der Mail.ru Cloud Solutions-Plattform implementiert wird



Hallo habr Ich bin Artyom Karamyshev, Leiter des Systemadministrationsteams bei Mail.Ru Cloud Solutions (MCS) . Im vergangenen Jahr haben wir viele neue Produkte auf den Markt gebracht. Wir wollten, dass die API-Services einfach skaliert werden können, fehlertolerant sind und für eine schnelle Erhöhung der Benutzerlast bereit sind. Unsere Plattform ist auf OpenStack implementiert, und ich möchte Ihnen sagen, welche Probleme der Fehlertoleranz von Komponenten wir schließen mussten, um ein fehlertolerantes System zu erhalten. Ich denke, das wird für diejenigen interessant sein, die auch Produkte auf OpenStack entwickeln.

Die Gesamtfehlertoleranz der Plattform besteht in der Stabilität ihrer Komponenten. Wir werden also schrittweise alle Ebenen durchlaufen, auf denen wir die Risiken entdeckt und geschlossen haben.

Eine Videoversion dieser Geschichte, deren Quelle ein Bericht auf der von ITSumma organisierten Uptime Day 4-Konferenz war, kann auf dem YouTube-Kanal der Uptime Community angesehen werden .

Fehlertoleranz der physischen Architektur


Der öffentliche Teil der MCS-Cloud befindet sich jetzt in zwei Tier III-Rechenzentren. Zwischen diesen befindet sich eine eigene dunkle Faser, die auf verschiedenen Wegen auf der physischen Schicht reserviert ist und einen Durchsatz von 200 Gbit / s aufweist. Die Stufe III bietet die erforderliche Ausfallsicherheit der physischen Infrastruktur.

Dunkle Fasern sind sowohl auf physischer als auch auf logischer Ebene reserviert. Der Kanalreservierungsprozess war iterativ, es traten Probleme auf und wir verbessern ständig die Kommunikation zwischen Rechenzentren.

Zum Beispiel hat vor nicht allzu langer Zeit, als ein Bagger in einem Brunnen neben einem der Rechenzentren arbeitete, ein Rohr gestanzt. In diesem Rohr befand sich sowohl ein optisches Haupt- als auch ein Ersatzkabel. Unser fehlertoleranter Kommunikationskanal mit dem Rechenzentrum erwies sich an einer Stelle im Bohrloch als anfällig. Dementsprechend haben wir einen Teil der Infrastruktur verloren. Wir haben Schlussfolgerungen gezogen und eine Reihe von Maßnahmen ergriffen, einschließlich der Verlegung zusätzlicher Optiken entlang eines benachbarten Brunnens.

In den Rechenzentren gibt es Präsenzpunkte von Kommunikationsanbietern, an die wir unsere Präfixe über BGP senden. Für jede Netzwerkrichtung wird die beste Metrik ausgewählt, mit der verschiedene Kunden die beste Verbindungsqualität erzielen können. Wenn die Kommunikation über einen Anbieter unterbrochen wird, bauen wir unser Routing über verfügbare Anbieter neu auf.

Bei einem Providerausfall wechseln wir automatisch zum nächsten. Im Falle eines Ausfalls eines der Rechenzentren haben wir eine Spiegelkopie unserer Dienste im zweiten Rechenzentrum, die die gesamte Last auf sich nehmen.


Ausfallsicherheit der physischen Infrastruktur

Was wir für die Fehlertoleranz auf Anwendungsebene verwenden


Unser Service basiert auf einer Reihe von Open Source-Komponenten.

ExaBGP ist ein Dienst, der eine Reihe von Funktionen mithilfe des auf BGP basierenden dynamischen Routing-Protokolls implementiert. Wir verwenden es aktiv, um unsere weißen IP-Adressen bekannt zu geben, über die Benutzer Zugriff auf die API erhalten.

HAProxy ist ein hoch geladener Balancer, mit dem Sie sehr flexible Regeln für den Datenverkehrsausgleich auf verschiedenen Ebenen des OSI-Modells konfigurieren können. Wir verwenden es, um alle Dienste auszugleichen: Datenbanken, Nachrichtenbroker, API-Dienste, Webdienste, unsere internen Projekte - alles steckt hinter HAProxy.

API-Anwendung - eine in Python geschriebene Webanwendung, mit der der Benutzer seine Infrastruktur und seinen Dienst steuert.

Worker-Anwendung (im Folgenden einfach als Worker bezeichnet) - In OpenStack-Diensten handelt es sich um einen Infrastruktur-Daemon, mit dem Sie API-Befehle in die Infrastruktur übersetzen können. Beispielsweise wird im Worker eine Festplatte erstellt, und eine Anforderung zum Erstellen befindet sich in der Anwendungs-API.

Standard OpenStack-Anwendungsarchitektur


Die meisten Dienste, die für OpenStack entwickelt wurden, versuchen, einem einzigen Paradigma zu folgen. Ein Service besteht normalerweise aus zwei Teilen: API und Workern (Backend-Executoren). In der Regel ist eine API eine Python-WSGI-Anwendung, die entweder als eigenständiger Prozess (Daemon) oder unter Verwendung eines vorgefertigten Nginx-Webservers, Apache, ausgeführt wird. Die API verarbeitet die Benutzeranforderung und leitet weitere Anweisungen an die Worker-Anwendung weiter. Die Übertragung erfolgt über einen Nachrichtenbroker, normalerweise RabbitMQ, der Rest wird schlecht unterstützt. Wenn Nachrichten an den Broker gelangen, werden sie von den Mitarbeitern verarbeitet und geben bei Bedarf eine Antwort zurück.

Dieses Paradigma impliziert isolierte häufige Fehlerquellen: RabbitMQ und die Datenbank. RabbitMQ ist jedoch innerhalb eines Dienstes isoliert und kann theoretisch für jeden Dienst individuell sein. Deshalb teilen wir bei MCS diese Dienste so weit wie möglich. Für jedes einzelne Projekt erstellen wir eine separate Datenbank, einen separaten RabbitMQ. Dieser Ansatz ist gut, da im Falle eines Unfalls an einigen gefährdeten Stellen nicht alle Dienstpausen, sondern nur ein Teil davon unterbrochen werden.

Die Anzahl der Worker-Anwendungen ist unbegrenzt, sodass die API problemlos horizontal hinter den Balancern skaliert werden kann, um die Produktivität und Fehlertoleranz zu erhöhen.

Einige Dienste erfordern eine Koordination innerhalb des Dienstes - wenn komplexe sequentielle Vorgänge zwischen APIs und Mitarbeitern auftreten. In diesem Fall wird ein einzelnes Koordinierungszentrum verwendet, ein Clustersystem wie Redis, Memcache usw., mit dem ein Mitarbeiter dem anderen mitteilen kann, dass diese Aufgabe ihm zugewiesen ist ("Bitte nicht übernehmen"). Wir verwenden etcd. In der Regel kommunizieren Mitarbeiter aktiv mit der Datenbank, schreiben und lesen dort Informationen. Als Datenbank verwenden wir Mariadb, das wir im Multimaster-Cluster haben.

Ein solcher klassischer Einzelbenutzerdienst ist in einer für OpenStack allgemein akzeptierten Weise organisiert. Es kann als geschlossenes System betrachtet werden, für das die Methoden der Skalierung und Fehlertoleranz ziemlich offensichtlich sind. Für die Fehlertoleranz der API reicht es beispielsweise aus, einen Balancer vor sie zu stellen. Die Skalierung der Arbeitnehmer wird durch die Erhöhung ihrer Zahl erreicht.

Die Schwachstellen im gesamten Schema sind RabbitMQ und MariaDB. Ihre Architektur verdient einen separaten Artikel. In diesem Artikel möchte ich mich auf die Fehlertoleranz der API konzentrieren.


Openstack-Anwendungsarchitektur Ausgleich und Ausfallsicherheit der Cloud-Plattform

HAProxy Balancer mit ExaBGP widerstandsfähig machen


Um unsere APIs skalierbar, schnell und fehlertolerant zu machen, setzen wir einen Balancer vor sie. Wir haben uns für HAProxy entschieden. Meiner Meinung nach weist es alle notwendigen Merkmale für unsere Aufgabe auf: Balancing auf mehreren OSI-Ebenen, Verwaltungsschnittstelle, Flexibilität und Skalierbarkeit, eine große Anzahl von Balancing-Methoden, Unterstützung für Sitzungstabellen.

Das erste Problem, das gelöst werden musste, war die Fehlertoleranz des Balancers selbst. Allein die Installation des Balancers führt zu einer Fehlerquelle: Der Balancer bricht ab - der Dienst wird unterbrochen. Um dies zu verhindern, haben wir HAProxy zusammen mit ExaBGP verwendet.

Mit ExaBGP können Sie einen Mechanismus zum Überprüfen des Status eines Dienstes implementieren. Wir haben diesen Mechanismus verwendet, um die Funktionalität von HAProxy zu überprüfen und bei Problemen den HAProxy-Dienst von BGP aus zu deaktivieren.

ExaBGP + HAProxy-Schema

  1. Wir installieren die erforderliche Software auf drei Servern, ExaBGP und HAProxy.
  2. Auf jedem der Server erstellen wir eine Loopback-Schnittstelle.
  3. Auf allen drei Servern weisen wir dieser Schnittstelle dieselbe weiße IP-Adresse zu.
  4. Eine weiße IP-Adresse wird im Internet über ExaBGP angekündigt.

Die Fehlertoleranz wird erreicht, indem von allen drei Servern dieselbe IP-Adresse angekündigt wird. Aus Netzwerksicht ist dieselbe Adresse aus drei verschiedenen nächsten Hoffnungen zugänglich. Der Router sieht drei identische Routen, wählt die höchste Priorität anhand seiner eigenen Metrik aus (dies ist normalerweise dieselbe Option), und der Datenverkehr wird nur an einen der Server geleitet.

Bei Problemen mit dem HAProxy-Betrieb oder einem Serverausfall beendet ExaBGP die Ankündigung der Route und der Datenverkehr wird reibungslos auf einen anderen Server umgeschaltet.

Damit haben wir die Fehlertoleranz des Balancers erreicht.


Fehlertoleranz von HAProxy-Balancern

Das Schema stellte sich als unvollkommen heraus: Wir haben gelernt, wie man HAProxy reserviert, aber nicht, wie man die Last innerhalb der Dienste verteilt. Aus diesem Grund haben wir dieses Schema ein wenig erweitert: Wir haben den Ausgleich zwischen mehreren weißen IP-Adressen vorgenommen.

DNS Based Balancing Plus BGP


Das Problem des Lastausgleichs vor unserem HAProxy blieb ungelöst. Trotzdem kann es ganz einfach gelöst werden, wie wir es zu Hause getan haben.

Um die drei Server auszugleichen, benötigen Sie 3 weiße IP-Adressen und einen guten alten DNS. Jede dieser Adressen wird auf der Loopback-Schnittstelle jedes HAProxy definiert und im Internet angekündigt.

OpenStack verwendet einen Dienstkatalog zum Verwalten von Ressourcen, wodurch die Endpunkt-API eines Dienstes festgelegt wird. In diesem Verzeichnis schreiben wir einen Domainnamen vor - public.infra.mail.ru, der über DNS mit drei verschiedenen IP-Adressen aufgelöst wird. Als Ergebnis erhalten wir einen Lastenausgleich zwischen den drei Adressen über DNS.

Da wir bei der Ankündigung von weißen IP-Adressen die Prioritäten für die Serverauswahl nicht steuern, ist dies bisher kein Ausgleich. In der Regel wird nur ein Server nach Vorrang der IP-Adresse ausgewählt, und die anderen beiden sind inaktiv, da in BGP keine Metriken angegeben sind.

Wir haben damit begonnen, Routen durch ExaBGP mit verschiedenen Metriken anzugeben. Jeder Balancer kündigt alle drei weißen IP-Adressen an, aber eine von ihnen, die Hauptadresse für diesen Balancer, wird mit einer Mindestmetrik angekündigt. Während also alle drei Balancer in Betrieb sind, fallen Anrufe an die erste IP-Adresse auf den ersten Balancer, Anrufe an die zweite an die zweite, an die dritte an die dritte.

Was passiert, wenn einer der Balancer fällt? Im Falle eines Ausfalls eines Balancers aufgrund seiner Basis wird die Adresse weiterhin von den beiden anderen angekündigt, und der Verkehr zwischen ihnen wird neu verteilt. Somit geben wir dem Benutzer über das DNS mehrere IP-Adressen gleichzeitig. Durch das Balancieren auf DNS und verschiedenen Metriken erhalten wir eine gleichmäßige Lastverteilung auf alle drei Balancer. Gleichzeitig verlieren wir nicht die Fehlertoleranz.


HAProxy Balancing basierend auf DNS + BGP

Wechselwirkung zwischen ExaBGP und HAProxy


Daher haben wir die Fehlertoleranz für den Fall implementiert, dass der Server den Server verlässt, basierend auf der Beendigung der Ankündigung von Routen. HAProxy kann jedoch auch aus anderen Gründen als einem Serverausfall getrennt werden: Verwaltungsfehler, Dienstausfälle. Wir wollen den kaputten Balancer unter der Last entfernen und in diesen Fällen brauchen wir einen anderen Mechanismus.

Aus diesem Grund haben wir zur Erweiterung des vorherigen Schemas einen Heartbeat zwischen ExaBGP und HAProxy implementiert. Dies ist eine Softwareimplementierung der Interaktion zwischen ExaBGP und HAProxy, wenn ExaBGP benutzerdefinierte Skripts verwendet, um den Status von Anwendungen zu überprüfen.

Dazu müssen Sie in der ExaBGP-Konfiguration einen Integritätsprüfer konfigurieren, der den Status von HAProxy überprüfen kann. In unserem Fall haben wir das Integritäts-Backend in HAProxy konfiguriert und von der Seite von ExaBGP aus mit einer einfachen GET-Anforderung überprüft. Wenn die Ankündigung nicht mehr erfolgt, funktioniert HAProxy höchstwahrscheinlich nicht und es ist nicht erforderlich, sie anzukündigen.


HAProxy Health Check

HAProxy Peers: Sitzungssynchronisation


Als nächstes mussten die Sitzungen synchronisiert werden. Bei der Arbeit mit verteilten Balancern ist es schwierig, die Speicherung von Informationen zu Client-Sitzungen zu organisieren. HAProxy ist jedoch einer der wenigen Balancer, die dies aufgrund der Peers-Funktionalität tun können - der Möglichkeit, Sitzungstabellen zwischen verschiedenen HAProxy-Prozessen zu übertragen.

Es gibt verschiedene Ausgleichsmethoden: Einfach, z. B. Round-Robin , und Erweitert, wenn eine Clientsitzung gespeichert wird und jedes Mal, wenn sie auf denselben Server wie zuvor gelangt. Wir wollten die zweite Option implementieren.

HAProxy verwendet Stick-Tabellen, um Client-Sitzungen für diesen Mechanismus zu speichern. Sie speichern die Quell-IP-Adresse des Clients, die ausgewählte Zieladresse (Backend) und einige Dienstinformationen. In der Regel werden Stick-Tabellen verwendet, um das Quell-IP + Ziel-IP-Paar zu speichern. Dies ist besonders nützlich für Anwendungen, die den Kontext einer Benutzersitzung nicht übertragen können, wenn sie zu einem anderen Balancer wechseln, z. B. im RoundRobin-Ausgleichsmodus.

Wenn der Stick-Tabelle beigebracht wird, sich zwischen verschiedenen HAProxy-Prozessen zu bewegen (zwischen denen das Balancing stattfindet), können unsere Balancer mit einem Pool von Stick-Tischen arbeiten. Auf diese Weise kann das Client-Netzwerk nahtlos gewechselt werden, wenn einer der Balancer ausfällt. Die Arbeit mit Client-Sitzungen wird auf denselben Backends fortgesetzt, die zuvor ausgewählt wurden.

Für einen ordnungsgemäßen Betrieb muss die Quell-IP-Adresse des Balancers aufgelöst werden, von dem aus die Sitzung eingerichtet wird. In unserem Fall ist dies eine dynamische Adresse auf der Loopback-Schnittstelle.

Der korrekte Betrieb von Peers wird nur unter bestimmten Bedingungen erreicht. Das heißt, die TCP-Zeitüberschreitungen müssen groß genug sein oder der Switch sollte schnell genug sein, damit die TCP-Sitzung keine Zeit zum Unterbrechen hat. Dies ermöglicht jedoch ein nahtloses Umschalten.

Wir bei IaaS haben einen Service, der auf der gleichen Technologie basiert. Dies ist ein Load Balancer als Service für OpenStack namens Octavia. Es basiert auf zwei HAProxy-Prozessen und umfasste ursprünglich die Unterstützung von Peers. Sie haben sich in diesem Service bewährt.

Das Bild zeigt schematisch die Bewegung von Peers-Tabellen zwischen drei HAProxy-Instanzen. Es wird eine Konfiguration vorgeschlagen, wie diese konfiguriert werden kann:


HAProxy Peers (Sitzungssynchronisation)

Wenn Sie dasselbe Schema implementieren, muss seine Arbeit sorgfältig getestet werden. Nicht die Tatsache, dass dies in 100% der Fälle auf die gleiche Weise funktioniert. Zumindest verlieren Sie jedoch keine Stick-Tabellen, wenn Sie sich die Quell-IP des Clients merken müssen.

Begrenzung der Anzahl gleichzeitiger Anforderungen vom selben Client


Alle gemeinfreien Dienste, einschließlich unserer APIs, können Lawinen von Anfragen unterliegen. Die Gründe dafür können völlig unterschiedlich sein, von Benutzerfehlern bis hin zu gezielten Angriffen. Wir sind regelmäßig DDoS an IP-Adressen. Kunden machen oft Fehler in ihren Skripten, sie machen uns zu Mini-DDoSs.

Auf die eine oder andere Weise muss zusätzlicher Schutz bereitgestellt werden. Die naheliegende Lösung besteht darin, die Anzahl der API-Anforderungen zu begrenzen und keine CPU-Zeit für die Verarbeitung böswilliger Anforderungen zu verschwenden.

Um solche Einschränkungen zu implementieren, verwenden wir Ratenlimits, die auf der Basis von HAProxy organisiert sind und dieselben Stick-Tabellen verwenden. Die Grenzwerte sind recht einfach konfiguriert und ermöglichen es Ihnen, den Benutzer durch die Anzahl der Anforderungen an die API zu begrenzen. Der Algorithmus merkt sich die Quell-IP, von der aus die Anforderungen gestellt werden, und begrenzt die Anzahl gleichzeitiger Anforderungen von einem Benutzer. Natürlich haben wir das durchschnittliche API-Lastprofil für jeden Service berechnet und den Grenzwert auf das 10-fache dieses Werts festgelegt. Bis jetzt beobachten wir die Situation weiterhin genau und halten den Finger am Puls der Zeit.

Wie sieht es in der Praxis aus? Wir haben Kunden, die ständig unsere automatischen APIs verwenden. Sie erstellen ungefähr zwei- oder dreihundert virtuelle Maschinen näher am Morgen und löschen sie näher am Abend. Erstellen Sie für OpenStack eine virtuelle Maschine, auch mit PaaS-Diensten, mindestens 1000 API-Anforderungen, da die Interaktion zwischen den Diensten auch über die API erfolgt.

Ein solches Aufgabenwerfen verursacht eine ziemlich große Last. Wir haben diese Belastung geschätzt, tägliche Spitzenwerte gesammelt, sie verzehnfacht, und dies wurde zu unserer Ratengrenze. Wir bleiben am Puls der Zeit. Wir sehen oft Bots, Scanner, die versuchen, uns anzusehen. Haben wir CGA-Skripte, die ausgeführt werden können, schneiden wir sie aktiv aus.

So aktualisieren Sie die Codebasis diskret für Benutzer


Wir implementieren auch Fehlertoleranz auf der Ebene der Codebereitstellungsprozesse. Während des Rollouts kommt es zu Abstürzen, deren Auswirkungen auf die Verfügbarkeit von Diensten können jedoch minimiert werden.

Wir aktualisieren ständig unsere Dienste und sollten sicherstellen, dass die Codebasis ohne Auswirkungen für die Benutzer aktualisiert wird. Wir haben es geschafft, dieses Problem mithilfe der Funktionen des HAProxy-Managements und der Implementierung von Graceful Shutdown in unseren Diensten zu lösen.

Um dieses Problem zu lösen, war es notwendig, eine Balancer-Steuerung und das „korrekte“ Herunterfahren von Diensten bereitzustellen:

  • Im Fall von HAProxy erfolgt die Steuerung über die Statistikdatei, die im Wesentlichen ein Socket ist und in der HAProxy-Konfiguration definiert ist. Sie können Befehle über stdio an ihn senden. Unser Hauptwerkzeug zur Konfigurationssteuerung ist jedoch ansibel und verfügt über ein integriertes Modul zur Verwaltung von HAProxy. Was wir aktiv nutzen.
  • Die meisten unserer API- und Engine-Services unterstützen ordnungsgemäße Abschalttechnologien: Beim Herunterfahren warten sie auf den Abschluss der aktuellen Aufgabe, sei es eine http-Anforderung oder eine Dienstprogrammaufgabe. Das gleiche passiert mit dem Arbeiter. Er kennt alle Aufgaben, die er erledigt, und endet, wenn er alles erfolgreich abgeschlossen hat.

Dank dieser beiden Punkte lautet der sichere Algorithmus unserer Bereitstellung wie folgt.

  1. Der Entwickler erstellt ein neues Codepaket (wir haben RPM), testet in der Entwicklungsumgebung, testet in der Phase und belässt es im Stage-Repository.
  2. Der Entwickler stellt die Aufgabe mit der detailliertesten Beschreibung der "Artefakte" auf die Bereitstellung: die Version des neuen Pakets, eine Beschreibung der neuen Funktionalität und gegebenenfalls weitere Details zur Bereitstellung.
  3. Der Systemadministrator startet das Upgrade. Startet das Ansible-Playbook, das wiederum Folgendes ausführt:
    • Es nimmt ein Paket aus dem Stage-Repository und aktualisiert damit die Paketversion im Produkt-Repository.
    • Erstellt eine Liste der Backends des aktualisierten Dienstes.
    • Deaktiviert den ersten aktualisierten Dienst in HAProxy und wartet auf das Ende seiner Prozesse. Dank des ordnungsgemäßen Herunterfahrens sind wir zuversichtlich, dass alle aktuellen Clientanforderungen erfolgreich abgeschlossen werden.
    • Nachdem die API, Worker und HAProxy vollständig gestoppt wurden, wird der Code aktualisiert.
    • Ansible startet Dienste.
    • Für jeden Dienst werden bestimmte „Stifte“ gezogen, die Unit-Tests für eine Reihe vordefinierter Schlüsseltests durchführen. Eine grundlegende Überprüfung des neuen Codes erfolgt.
    • Wenn im vorherigen Schritt keine Fehler gefunden wurden, wird das Backend aktiviert.
    • Gehe zum nächsten Backend.
  4. Nach dem Aktualisieren aller Backends werden Funktionstests gestartet. Wenn sie nicht ausreichen, prüft der Entwickler alle neuen Funktionen, die er ausgeführt hat.

Damit ist die Bereitstellung abgeschlossen.


Service-Update-Zyklus

Dieses Schema würde nicht funktionieren, wenn wir keine einzige Regel hätten. Wir unterstützen sowohl die alte als auch die neue Version im Kampf. In der Phase der Softwareentwicklung wird im Voraus festgelegt, dass selbst bei Änderungen in der Servicedatenbank der vorherige Code nicht beschädigt wird. Infolgedessen wird die Codebasis schrittweise aktualisiert.

Fazit


Ich teile meine eigenen Gedanken über die fehlertolerante WEB-Architektur und möchte noch einmal die wichtigsten Punkte hervorheben:

  • physikalische Fehlertoleranz;
  • Netzwerkfehlertoleranz (Balancer, BGP);
  • Fehlertoleranz der verwendeten und entwickelten Software.

Alles stabile Betriebszeit!

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


All Articles