Container, Mikrodienste und Service-Meshes

Im Internet eine Menge Artikel o in Service-Meshes (Service Mesh), und das ist noch eins. Hurra! Aber warum? Weil ich meine Meinung dazu äußern möchte, dass es besser gewesen wäre, wenn Service-Meshes vor 10 Jahren aufgetaucht wären, vor der Einführung von Container-Plattformen wie Docker und Kubernetes. Ich behaupte nicht, dass meine Sichtweise besser oder schlechter ist als die anderer, aber da Service-Meshes ziemlich komplexe Gebilde sind, hilft die Vielfalt der Perspektiven, sie besser zu verstehen.

Ich werde über die Plattform dotCloud sprechen, die auf mehr als hundert Mikrodiensten basierte und Tausende von Anwendungen in Containern unterstützte. Ich werde die Probleme erläutern, mit denen wir bei ihrer Entwicklung und Einführung konfrontiert waren, und wie Service-Meshes helfen könnten (oder nicht).

Die Geschichte von dotCloud

Ich habe bereits über die Geschichte von dotCloud und die Architekturwahl für diese Plattform geschrieben, aber nur wenig über die Netzwerkschicht berichtet. Wenn Sie nicht in die Lektüre des vorherigen Artikels über dotCloud eintauchen möchten, hier die Kurzfassung: Es ist eine Platform-as-a-Service (PaaS), die es Kunden ermöglicht, eine Vielzahl von Anwendungen (Java, PHP, Python...) auszuführen, mit Unterstützung für eine breite Palette von Datenservices (MongoDB, MySQL, Redis...) und einem Arbeitsablauf wie bei Heroku: Sie laden Ihren Code auf die Plattform hoch, sie erstellt Container-Images und setzt sie ein.

Ich werde erläutern, wie der Verkehr auf die dotCloud-Plattform geleitet wurde. Nicht weil es besonders großartig war (obwohl das System für seine Zeit ziemlich gut funktionierte!), sondern vor allem, weil moderne Werkzeuge ein solches Design in kurzer Zeit von einem kleinen Team umsetzen lassen, wenn es einen Weg braucht, um den Verkehr zwischen vielen Mikrodiensten oder vielen Anwendungen zu routen. So können die Optionen miteinander verglichen werden: Was passiert, wenn man alles selbst entwickelt oder ein bestehendes Service-Mesh nutzt? Die Standardwahl: selbst machen oder kaufen.

Verkehrssteuerung für gehostete Anwendungen

Anwendungen auf dotCloud können HTTP- und TCP-Endpunkte bereitstellen.

HTTP-Endpunkte werden dynamisch zur Konfiguration des Cluster-Load-Balancers hinzugefügt Hipache. Das ähnelt dem, was heute die Ressourcen Ingress in Kubernetes und einem Load Balancer wie Traefik.

tun. Kunden verbinden sich über die entsprechenden Domains mit den HTTP-Endpunkten, vorausgesetzt, der Domainname verweist auf die Load Balancer von dotCloud. Nichts Besonderes.

TCP-Endpunkte sind mit der Portnummer verbunden, die dann über Umgebungsvariablen an alle Container dieses Stacks weitergegeben wird.

Kunden können sich mit den TCP-Endpunkten verbinden, indem sie den entsprechenden Hostnamen verwenden (etwas wie gateway-X.dotcloud.com) und die Portnummer.

Dieser Hostname wird auf das Cluster von „nats“ Servern aufgelöst (hat nichts mit NATS) zu tun, die eingehende TCP-Verbindungen an den richtigen Container (oder im Falle von Diensten mit Lastverteilung an die richtigen Container) weiterleiten.

Wenn Sie mit Kubernetes vertraut sind, erinnert Sie das wahrscheinlich an Dienste NodePort.

Auf der Plattform dotCloud gab es kein Äquivalent zu Diensten ClusterIP: Der Zugang zu Diensten erfolgte der Einfachheit halber sowohl von innen als auch von außen auf der Plattform gleich.

Alles war recht einfach organisiert: Die ursprünglichen Implementierungen der HTTP- und TCP-Routing-Netzwerke umfassten wahrscheinlich nur ein paar Hundert Zeilen Python. Einfache (ich würde sagen, naive) Algorithmen, die mit dem Wachstum der Plattform und dem Eintreffen zusätzlicher Anforderungen weiterentwickelt wurden.

Umfassendes Refactoring des vorhandenen Codes war nicht erforderlich. Insbesondere 12-Faktor-Anwendungen können die Adresse direkt verwenden, die über Umgebungsvariablen abgerufen wurde.

Wie unterscheidet sich das von modernen Service-Meshes?

Begrenzte Observability. Wir hatten überhaupt keine Metriken für das TCP-Routing. Was das HTTP-Routing betrifft, so gab es in späteren Versionen detaillierte HTTP-Metriken mit Fehlercodes und Reaktionszeiten, aber moderne Service-Meshes gehen noch weiter und bieten Integrationen mit Metrik-Sammlungssystemen wie Prometheus.

Observability ist nicht nur aus operationeller Sicht wichtig (um bei der Problemlösung zu helfen), sondern auch beim Rollout neuer Funktionen. Es geht um ein sicheres Blue-Green Deployment und Canary Deployment.

Routing-Effizienz ist ebenfalls eingeschränkt. Im Routing-Netzwerk von dotCloud musste der gesamte Datenverkehr durch einen Cluster dedizierter Routing-Knoten geleitet werden. Das bedeutete potenzielles Überqueren mehrerer AZ-Grenzen (Availability Zones) und eine signifikante Erhöhung der Latenz. Ich erinnere mich, wie ich Probleme mit dem Code behoben habe, der über hundert SQL-Abfragen pro Seite gemacht hat und für jede Abfrage eine neue Verbindung zum SQL-Server geöffnet hat. Bei lokalem Start lädt die Seite sofort, aber in dotCloud dauert es mehrere Sekunden, weil jeder TCP-Verbindung (und der nachfolgenden SQL-Abfrage) Dutzende von Millisekunden benötigen. In diesem speziellen Fall wurden die Probleme durch dauerhafte Verbindungen gelöst.

Moderne Service-Meshes kommen besser mit solchen Problemen zurecht. Zunächst prüfen sie, dass die Verbindungen geroutet werden in der Quelle. Der logische Fluss bleibt derselbe: Klient → Mesh → Dienst, aber jetzt arbeitet das Mesh lokal und nicht auf entfernten Knoten, wodurch die Verbindung Klient → Mesh lokal und sehr schnell ist (Mikrosekunden statt Millisekunden).

Moderne Service-Meshes implementieren auch intelligentere Lastverteilungsalgorithmen. Indem sie die Verfügbarkeit der Backends überwachen, können sie mehr Verkehr auf schnellere Backends leiten, was die Gesamteffizienz steigert.

Sicherheit ist ebenfalls besser. Das Routing-Netzwerk von dotCloud arbeitete vollständig auf EC2 Classic und verschlüsselte den Datenverkehr nicht (aus der Annahme heraus, dass, wenn jemand einen Sniffer auf den EC2-Datenverkehr gesetzt hat, bereits große Probleme bestehen). Moderne Service-Meshes schützen unseren gesamten Verkehr transparent, beispielsweise durch gegenseitige TLS-Authentifizierung und anschließende Verschlüsselung.

Traffic Routing für Plattformdienste

Gut, wir haben den Datenverkehr zwischen Anwendungen besprochen, aber wie sieht es mit der Plattform von dotCloud selbst aus?

Die Plattform selbst bestand aus etwa einhundert Mikrodiensten, die für verschiedene Funktionen verantwortlich waren. Einige nahmen Anfragen von anderen entgegen, während einige Hintergrundarbeiter waren, die sich mit anderen Diensten verbanden, aber keine Verbindungen selbst entgegennahmen. In jedem Fall muss jeder Dienst die Endpunkte kennen, zu denen eine Verbindung hergestellt werden muss.

Viele hochrangige Dienste können das oben beschriebene Routing-Netzwerk nutzen. Tatsächlich wurden viele der mehr als hundert Mikroservices von dotCloud als normale Anwendungen auf der Plattform dotCloud bereitgestellt. Aber eine kleine Anzahl von Low-Level-Diensten (insbesondere diejenigen, die dieses Routing-Netzwerk implementieren) benötigte etwas Einfacheres mit weniger Abhängigkeiten (da sie nicht von sich selbst abhängen konnten – das alte Problem von Huhn und Ei).

Diese Low-Level-Dienste, die von großer Bedeutung sind, wurden bereitgestellt, indem Container direkt auf mehreren Schlüssel-Knoten gestartet wurden. Dabei wurden keine Standarddienste der Plattform genutzt: Komponist, Scheduler und Runner. Wenn man es mit modernen Container-Plattformen vergleicht, ähnelt es dem Start der Steuerungsplane von docker run direkt auf den Knoten, statt die Aufgabe an Kubernetes zu delegieren. Das ähnelt ziemlich dem Konzept von statischen Modulen (Pods),das von kubeadm oder bootkube bei der Bootsequenz eines autonomen Clusters verwendet wird.

Diese Dienste wurden auf einfache und brutale Weise exponiert: In einer YAML-Datei wurden ihre Namen und Adressen aufgelistet; jeder Client musste eine Kopie dieser YAML-Datei für die Bereitstellung nehmen.

Einerseits ist dies äußerst zuverlässig, da es keine Unterstützung für externe Key-Value-Speicher wie Zookeeper erfordert (denken Sie daran, zu dieser Zeit gab es noch nicht etcd oder Consul). Andererseits machte dies die Migration von Diensten schwierig. Jedes Mal, wenn ein Dienst migriert wurde, mussten alle Clients die aktualisierte YAML-Datei erhalten (und potenziell neu gestartet werden). Das ist nicht sehr praktisch!

Im Laufe der Zeit begannen wir, ein neues Schema zu implementieren, bei dem sich jeder Client mit einem lokalen Proxy-Server verband. Statt einer Adresse und eines Ports genügte es, nur die Portnummer des Dienstes zu kennen und über localhosteine Verbindung herzustellen. Der lokale Proxy-Server verarbeitet diese Verbindung und leitet sie an den tatsächlichen Server weiter. Jetzt muss beim Umzug des Backends auf eine andere Maschine oder beim Skalieren anstelle der Aktualisierung aller Clients nur noch diese lokalen Proxys aktualisiert werden; ein Neustart ist nicht mehr erforderlich.

(Es war auch geplant, den Datenverkehr in TLS-Verbindungen zu kapseln und einen weiteren Proxy-Server auf der Empfangsseite einzurichten sowie TLS-Zertifikate ohne Beteiligung des Empfangsdienstes, der nur für den Empfang von Verbindungen konfiguriert ist, zu überprüfen localhost. Dazu später).

Das ähnelt sehr SmartStack von Airbnb, aber der wesentliche Unterschied besteht darin, dass SmartStack implementiert und in der Produktion ausgerollt wurde, während das interne Routing-System von dotCloud in die Schublade gelegt wurde, als dotCloud in Docker umgewandelt wurde.

Ich persönlich betrachte SmartStack als einen Vorläufer solcher Systeme wie Istio, Linkerd und Consul Connect, weil sie alle einem ähnlichen Muster folgen:

  • Einen Proxy auf jedem Knoten ausführen.
  • Clients verbinden sich mit dem Proxy.
  • Die Management-Ebene aktualisiert die Konfiguration des Proxy-Servers, wenn sich die Backends ändern.
  • … Gewinn!

Moderne Implementierung von Service-Mesh

Wenn wir heute ein ähnliches Netz einrichten müssten, könnten wir ähnliche Prinzipien verwenden. Zum Beispiel eine interne DNS-Zone einrichten, die Dienstnamen den Adressen im Raum zuordnet 127.0.0.0/8. Dann HAProxy auf jedem Knoten des Clusters starten, Verbindungen an jede Dienstadresse (in diesem Subnetz 127.0.0.0/8) entgegennehmen und die Last auf die entsprechenden Backends umleiten/verteilen. Die HAProxy-Konfiguration kann confd, verwaltet werden, wodurch Backend-Informationen in etcd oder Consul gespeichert und die aktualisierte Konfiguration bei Bedarf automatisch an HAProxy gepusht wird.

So funktioniert ungefähr Istio! Aber mit einigen Unterschieden:

  • Verwendet Envoy Proxy anstatt HAProxy.
  • Speichert die Backend-Konfiguration über die Kubernetes API anstelle von etcd oder Consul.
  • Diensten werden Adressen im internen Subnetz zugewiesen (Adressen Kubernetes ClusterIP) anstelle von 127.0.0.0/8.
  • Hat eine zusätzliche Komponente (Citadel) zum Hinzufügen der gegenseitigen TLS-Authentifizierung zwischen Client und Servern.
  • Unterstützt neue Funktionen wie Circuit Breaking, verteilte Tracing, Canary-Deployments und mehr.

Lassen Sie uns einige Unterschiede kurz betrachten.

Envoy Proxy

Der Envoy Proxy wurde von Lyft geschrieben [einem Konkurrenten von Uber im Taximarkt – Anm. d. Übers.]. Er ähnelt in vielerlei Hinsicht anderen Proxies (z.B. HAProxy, Nginx, Traefik …), aber Lyft hat ihren eigenen entwickelt, weil ihnen Funktionen fehlten, die in anderen Proxies nicht vorhanden waren, und es sinnvoller erschien, einen neuen zu erstellen, als den bestehenden zu erweitern.

Envoy kann eigenständig verwendet werden. Wenn ich einen bestimmten Dienst habe, der sich mit anderen Diensten verbinden soll, kann ich ihn so konfigurieren, dass er sich mit Envoy verbindet, und anschließend Envoy dynamisch konfigurieren und neu konfigurieren, um die Standorte anderer Dienste zu erfassen, während ich viele hervorragende zusätzliche Funktionen wie Observabilität erhalte. Anstatt eine benutzerdefinierte Client-Bibliothek oder eine Implementierung im Code für die Aufrufverfolgung zu verwenden, leiten wir den Datenverkehr zu Envoy, der für uns Metriken sammelt.

Aber Envoy kann auch als Datenebene (data plane) für das Service Mesh fungieren. Das bedeutet, dass für dieses Service Mesh Envoy als Kontroll-Plane (control plane) konfiguriert wird.

Kontroll-Plane

In der Kontroll-Plane stützt sich Istio auf die Kubernetes API. Das unterscheidet sich nicht wesentlich von der Verwendung von confd, das auf etcd oder Consul angewiesen ist, um eine Menge von Schlüsseln im Datenspeicher zu durchsuchen. Istio durchsucht über die Kubernetes API eine Menge von Kubernetes-Ressourcen.

In der Zwischenzeit: persönlich fand ich diese Beschreibung der Kubernetes API, die besagt:

Der Kubernetes API-Server ist ein 'dummer Server', der Speicherung, Versionsverwaltung, Validierung, Aktualisierung und Semantik von API-Ressourcen bietet.

Istio wurde entwickelt, um mit Kubernetes zu arbeiten; wenn Sie es jedoch außerhalb von Kubernetes verwenden möchten, müssen Sie eine Instanz des Kubernetes API-Servers (und einen unterstützenden Dienst von etcd) ausführen.

Service-Adressen

Istio verlässt sich auf ClusterIP-Adressen, die Kubernetes zuweist, sodass Istio-Dienste eine interne Adresse erhalten (nicht im Bereich 127.0.0.0/8).

Datenverkehr an die ClusterIP-Adresse für einen bestimmten Dienst im Kubernetes-Cluster ohne Istio wird von kube-proxy erfasst und an die Backendseite dieses Proxys weitergeleitet. Wenn Sie an technischen Einzelheiten interessiert sind, installiert kube-proxy iptables-Regeln (oder IPVS-Load-Balancer, je nachdem, wie es konfiguriert wurde), um die Ziel-IP-Adressen für Verbindungen zu ändern, die an die ClusterIP-Adresse gerichtet sind.

Nach der Installation von Istio im Kubernetes-Cluster ändert sich nichts, bis es ausdrücklich für den jeweiligen Verbraucher oder sogar den gesamten Namespace aktiviert wird, indem der Sidecar in die benutzerdefinierten Pods eingeführt wird. Dieser Container führt eine Instanz von Envoy aus und richtet eine Reihe von iptables-Regeln ein, um den Datenverkehr, der an andere Dienste gesendet wird, abzufangen und diesen Datenverkehr an Envoy umzuleiten.

Bei der Integration mit Kubernetes DNS bedeutet dies, dass unser Code über den Servicenamen verbunden werden kann und alles "einfach funktioniert". Mit anderen Worten, unser Code sendet Anfragen vom Typ http://api/v1/users/4242, dann API wird die Anfrage auf 10.97.105.48, iptables-Regeln fangen Verbindungen von 10.97.105.48 ab und leiten sie an den lokalen Envoy-Proxy um, der diese Anfrage an das tatsächliche API-Backend weiterleitet. Puh!

Zusätzliche Feinheiten

Istio bietet auch End-to-End-Verschlüsselung und Authentifizierung über mTLS (Mutual TLS). Dafür ist ein Komponenten verantwortlich, das Citadel.

Es gibt auch eine Komponente Mixer, die Envoy anfordern kann, um jede eine Anfrage zu erhalten, um eine spezielle Entscheidung über diese Anfrage basierend auf verschiedenen Faktoren wie Header, Backend-Last usw. zu treffen... (Keine Sorge: Es gibt viele Möglichkeiten, um sicherzustellen, dass Mixer funktioniert, und selbst wenn es ausfällt, funktioniert Envoy weiterhin normal als Proxy).

Und natürlich haben wir die Übersichtlichkeit erwähnt: Envoy sammelt eine riesige Menge an Metriken und ermöglicht dabei verteiltes Tracing. In einer Microservices-Architektur, wenn eine API-Anfrage durch die Microservices A, B, C und D gehen muss, wird beim Eintritt in das System das verteilte Tracing der Anfrage eine eindeutige ID hinzufügen und diese ID durch die Unteranfragen an all diese Microservices speichern, was es ermöglicht, alle zugehörigen Aufrufe, deren Verzögerungen usw. zu protokollieren.

Entwickeln oder kaufen

Istio hat den Ruf, ein komplexes System zu sein. Im Gegensatz dazu ist der Aufbau des Routing-Netzwerks, das ich zu Beginn dieses Beitrags beschrieben habe, relativ einfach mit den bestehenden Tools. Macht es also Sinn, stattdessen ein eigenes Service Mesh zu erstellen?

Wenn wir bescheidene Anforderungen haben (keine Übersichtlichkeit, keine Circuit Breaker und andere Feinheiten), kommen Gedanken an die Entwicklung eines eigenen Tools auf. Aber wenn wir Kubernetes verwenden, könnte es sogar nicht notwendig sein, da Kubernetes bereits grundlegende Tools für die Dienstentdeckung und Lastverteilung bereitstellt.

Aber wenn wir avancierte Anforderungen haben, erscheint der "Kauf" eines Service Mesh als viel bessere Option. (Es ist nicht immer tatsächlich ein "Kauf", da Istio Open Source ist, aber wir müssen trotzdem Ingenieurzeit investieren, um es zu verstehen, bereitzustellen und zu verwalten.)

Was wählen: Istio, Linkerd oder Consul Connect?

Bisher haben wir nur über Istio gesprochen, aber das ist nicht die einzige Service-Mesh. Eine beliebte Alternative ist Linkerd, und es gibt noch Consul Connect.

Was auswählen?

Ehrlich gesagt, weiß ich es nicht. Im Moment halte ich mich nicht für kompetent genug, um diese Frage zu beantworten. Es gibt einige interessante Artikel Vergleiche dieser Werkzeuge und sogar Benchmarks.

Ein vielversprechender Ansatz ist, ein Werkzeug wie SuperGloozu verwenden. Es implementiert eine Abstraktionsschicht zur Vereinfachung und Vereinheitlichung der von Service-Meshen bereitgestellten APIs. Anstatt die spezifischen (und meiner Meinung nach relativ komplexen) APIs verschiedener Service-Meshen zu studieren, können wir einfachere Konstrukte von SuperGloo verwenden – und problemlos von einem zum anderen wechseln, als hätten wir ein Zwischenformat für die Konfiguration, das HTTP-Interfaces und Backends beschreibt und in der Lage ist, die tatsächliche Konfiguration für Nginx, HAProxy, Traefik, Apache… zu generieren.

Ich habe ein wenig mit Istio und SuperGloo experimentiert und möchte im nächsten Artikel zeigen, wie man Istio oder Linkerd in einen bestehenden Cluster mit SuperGloo integriert, und wie gut letzteres dabei abschneidet, sprich, es ermöglicht, von einem Service-Mesh zu einem anderen zu wechseln, ohne Konfigurationen umschreiben zu müssen.

Quelle: habr.com

60GB SSD 8Gb DDR4