Zum Inhalt springen

1 - Datenmodell

Verfahren zur Datenspeicherung in etcd

etcd ist darauf ausgelegt, selten aktualisierte Daten zuverlässig zu speichern und zuverlässige Watch-Abfragen bereitzustellen. etcd macht frühere Versionen von Schlüssel-Wert-Paaren zugänglich, um kostengünstige Snapshots und die Beobachtung historischer Ereignisse zu ermöglichen („Zeitreiseabfragen“). Ein persistentes Datenmodell mit Mehrversions-Nebenläufigkeitskontrolle eignet sich gut für diese Anwendungsfälle.

etcd speichert Daten in einem persistenten Schlüssel-Wert-Speicher mit mehreren Versionen. Dieser bewahrt die vorherige Version eines Schlüssel-Wert-Paars auf, wenn dessen Wert durch neue Daten ersetzt wird. Der Schlüssel-Wert-Speicher ist praktisch unveränderlich: Seine Operationen aktualisieren die Struktur nicht an Ort und Stelle, sondern erzeugen stets eine neue, aktualisierte Struktur. Alle früheren Versionen von Schlüsseln bleiben nach einer Änderung zugänglich und können beobachtet werden. Damit der Datenspeicher nicht unbegrenzt wächst und alte Versionen dauerhaft vorhält, kann er kompaktiert werden, um die ältesten Versionen ersetzter Daten zu entfernen.

Logische Sicht

Die logische Sicht auf den Speicher ist ein flacher binärer Schlüsselraum. Ein lexikalisch sortierter Index über Schlüssel aus Bytefolgen ermöglicht kostengünstige Bereichsabfragen.

Der Schlüsselraum verwaltet mehrere Revisionen. Beim Anlegen des Speichers ist die erste Revision 1. Jede atomare ändernde Operation erzeugt eine neue Revision des Schlüsselraums; eine Transaktionsoperation kann beispielsweise mehrere Operationen enthalten. Alle Daten früherer Revisionen bleiben unverändert. Alte Schlüsselversionen sind über frühere Revisionen weiterhin zugänglich. Auch die Revisionen sind indiziert, sodass Watcher effizient Revisionsbereiche durchlaufen können. Wird der Speicher zur Platzersparnis kompaktiert, werden die Revisionen vor der Kompaktierungsrevision entfernt. Revisionen steigen während der gesamten Lebensdauer eines Clusters monoton an.

Das Leben eines Schlüssels umfasst eine Generation von der Erstellung bis zur Löschung. Jeder Schlüssel kann eine oder mehrere Generationen haben. Das Anlegen eines Schlüssels erhöht seine Version, beginnend bei 1, falls der Schlüssel in der aktuellen Revision nicht existiert. Das Löschen erzeugt einen Tombstone für den Schlüssel und beendet dessen aktuelle Generation, indem seine Version auf 0 zurückgesetzt wird. Jede Änderung eines Schlüssels erhöht seine Version. Versionen steigen daher innerhalb einer Schlüsselgeneration monoton an. Bei einer Kompaktierung werden alle Generationen entfernt, die vor der Kompaktierungsrevision endeten. Auch alle vor dieser Revision gesetzten Werte außer dem jeweils neuesten werden entfernt.

Physische Sicht

etcd speichert die physischen Daten als Schlüssel-Wert-Paare in einem persistenten B+-Baum . Aus Effizienzgründen enthält jede Revision des Speicherzustands nur die Differenz zur vorherigen Revision. Einer einzelnen Revision können mehrere Schlüssel im Baum entsprechen.

Der Schlüssel eines Schlüssel-Wert-Paars ist ein 3-Tupel (major, sub, type). Major ist die Speicherrevision, die den Schlüssel enthält. Sub unterscheidet Schlüssel innerhalb derselben Revision. Type ist ein optionales Suffix für besondere Werte, etwa t, wenn der Wert einen Tombstone enthält. Der Wert des Schlüssel-Wert-Paars enthält die Änderung gegenüber der vorherigen Revision, also eine Differenz zu ihr. Der B+-Baum ist lexikalisch nach den Bytes der Schlüssel sortiert. Bereichssuchen über Revisionsdifferenzen sind schnell; so lassen sich Änderungen zwischen zwei bestimmten Revisionen rasch ermitteln. Die Kompaktierung entfernt veraltete Schlüssel-Wert-Paare.

etcd hält außerdem einen sekundären B-Baum -Index im Arbeitsspeicher, um Bereichsabfragen über Schlüssel zu beschleunigen. Die Schlüssel in diesem Index sind die für Benutzer sichtbaren Schlüssel des Speichers. Der Wert ist ein Zeiger auf die Änderung im persistenten B+-Baum. Die Kompaktierung entfernt ungültig gewordene Zeiger.

etcd ermittelt also die Revisionsinformationen aus dem B-Baum und verwendet anschließend die Revision als Schlüssel, um den Wert aus dem B+-Baum abzurufen (siehe unten).

![MVCC-Datenmodell](/docs/etcd/learning/img/data-model-figure-01.png)

2 - Design des etcd-Clients

Architekturentscheidungen des Clients und Details ihrer Implementierung

Design des etcd-Clients

Gyuho Lee (github.com/gyuho, Amazon Web Services, Inc.), Joe Betz (github.com/jpbetz, Google Inc.)

Einleitung

Der etcd-Server hat seine Robustheit in jahrelangen Tests mit gezielt ausgelösten Fehlern bewiesen. Die meiste komplexe Anwendungslogik übernimmt bereits der etcd-Server mit seinen Datenspeichern. Beispielsweise bleibt die Clustermitgliedschaft für Clients transparent, da die Raft-Schicht Vorschläge an den Leader weiterleitet. Auch wenn die Serverkomponenten korrekt arbeiten, erfordert ihr Zusammenspiel mit dem Client weitere komplexe Protokolle, um Korrektheit und Hochverfügbarkeit auch bei Fehlern zu gewährleisten. Idealerweise bietet der etcd-Server eine einzige logische Clustersicht auf viele physische Rechner, während der Client automatisches Failover zwischen Replikaten implementiert. Dieses Dokument beschreibt die Architekturentscheidungen des Clients und die Details ihrer Implementierung.

Glossar

clientv3: Offizieller Go-Client von etcd für die etcd-v3-API.

clientv3-grpc1.0: Offizielle Client-Implementierung mit grpc-go v1.0.x , die in der neuesten Version von etcd v3.1 verwendet wird.

clientv3-grpc1.7: Offizielle Client-Implementierung mit grpc-go v1.7.x , die in den neuesten Versionen von etcd v3.2 und v3.3 verwendet wird.

clientv3-grpc1.23: Offizielle Client-Implementierung mit grpc-go v1.23.x , die in der neuesten Version von etcd v3.4 verwendet wird.

Load-Balancer: Lastverteiler des etcd-Clients, der Wiederholungs- und Failover-Mechanismen implementiert. Der etcd-Client sollte die Last automatisch auf mehrere Endpunkte verteilen.

Endpunkte: Eine Liste von etcd-Serverendpunkten, mit denen sich Clients verbinden können. Üblicherweise sind dies 3 oder 5 Client-URLs eines etcd-Clusters.

Fest zugeordneter Endpunkt: Bei mehreren konfigurierten Endpunkten wählt der Client-Load-Balancer in Versionen <= v3.3 nur einen Endpunkt für eine TCP-Verbindung aus, um die Gesamtzahl offener Verbindungen zum etcd-Cluster gering zu halten. In v3.4 wechselt der Load-Balancer für jede Anfrage im Round-Robin-Verfahren zwischen den fest zugeordneten Endpunkten und verteilt die Last dadurch gleichmäßiger.

Client-Verbindung: Eine über gRPC Dial aufgebaute TCP-Verbindung zu einem etcd-Server.

Unterverbindung: Die gRPC-Schnittstelle SubConn. Jede Unterverbindung enthält eine Adressliste. Der Load-Balancer erstellt aus einer Liste aufgelöster Adressen eine SubConn. Eine gRPC ClientConn kann mehreren SubConn entsprechen, etwa wenn example.com auf 10.10.10.1 und 10.10.10.2 zweier Unterverbindungen aufgelöst wird. Der Load-Balancer von etcd v3.4 verwendet einen internen Resolver, um für jeden Endpunkt eine Unterverbindung aufzubauen.

Vorübergehender Verbindungsabbruch: Wenn der gRPC-Server einen Statusfehler vom Typ code Unavailable zurückgibt.

Anforderungen an den Client

Korrektheit. Bei Serverfehlern können Anfragen fehlschlagen. Konsistenzgarantien werden jedoch nie verletzt: Eigenschaften der globalen Reihenfolge, kein Schreiben beschädigter Daten, At-most-once-Semantik für ändernde Operationen, keine Beobachtung unvollständiger Ereignisse durch Watches usw.

Fortschrittsgarantie. Server können ausfallen oder kurzzeitig die Verbindung verlieren. Clients sollten in beiden Fällen Fortschritte machen. Sie sollten beim Warten auf die Rückkehr eines offline gegangenen Servers niemals dauerhaft blockieren , sofern dies nicht ausdrücklich konfiguriert ist. Idealerweise erkennen Clients nicht verfügbare Server mit HTTP/2-Pings und wechseln mit verständlichen Fehlermeldungen zu anderen Knoten.

Effizienz. Clients sollten mit minimalem Ressourcenverbrauch wirksam arbeiten: Nach einem Endpunktwechsel sollten vorherige TCP-Verbindungen geordnet geschlossen werden. Der Failover-Mechanismus sollte das nächste geeignete Replikat für eine Verbindung bestimmen, ohne Ressourcen durch wiederholte Versuche an ausgefallenen Knoten zu verschwenden.

Portierbarkeit. Der offizielle Client sollte klar dokumentiert sein, und seine Implementierung sollte sich auf Anbindungen anderer Sprachen übertragen lassen. Die Fehlerbehandlung sollte über die Sprachanbindungen hinweg konsistent sein. Da etcd vollständig auf gRPC setzt, sollte sich die Implementierung eng an den langfristigen Entwurfszielen von gRPC orientieren. Beispielsweise sollte eine austauschbare Wiederholungsrichtlinie mit gRPC-Wiederholungen kompatibel sein. Upgrades zwischen zwei Client-Versionen sollten den Betrieb nicht unterbrechen.

Client-Überblick

Der etcd-Client implementiert die folgenden Komponenten:

  • einen Load-Balancer, der gRPC-Verbindungen zu einem etcd-Cluster aufbaut,
  • einen API-Client, der RPCs an einen etcd-Server sendet, und
  • eine Fehlerbehandlung, die entscheidet, ob eine fehlgeschlagene Anfrage wiederholt oder der Endpunkt gewechselt wird.

Sprachen können sich darin unterscheiden, wie die erste Verbindung aufgebaut wird, etwa durch TLS-Konfiguration, wie Protocol-Buffer-Nachrichten kodiert und an den Server gesendet werden, wie Streaming-RPCs verarbeitet werden usw. Die vom etcd-Server zurückgegebenen Fehler sind jedoch dieselben. Entsprechend sollten auch Fehlerbehandlung und Wiederholungsrichtlinie gleich sein.

Beispielsweise kann der etcd-Server "rpc error: code = Unavailable desc = etcdserver: request timed out" zurückgeben, einen vorübergehenden Fehler, bei dem Wiederholungen erwartet werden. Oder er gibt rpc error: code = InvalidArgument desc = etcdserver: key is not provided zurück. Dies bedeutet, dass die Anfrage ungültig war und nicht wiederholt werden sollte. Der Go-Client kann Fehler mit google.golang.org/grpc/status.FromError auswerten, der Java-Client mit io.grpc.Status.fromThrowable.

clientv3-grpc1.0: Überblick über den Load-Balancer

Bei mehreren konfigurierten etcd-Endpunkten hält clientv3-grpc1.0 mehrere TCP-Verbindungen offen. Anschließend wählt er eine Adresse und sendet darüber alle Client-Anfragen. Die fest zugeordnete Adresse bleibt erhalten, bis das Client-Objekt geschlossen wird (siehe Abbildung 1). Wenn der Client einen Fehler erhält, wählt er zufällig eine andere Adresse und versucht es erneut.

client-balancer-figure-01.png

clientv3-grpc1.0: Einschränkungen des Load-Balancers

Die mehreren TCP-Verbindungen von clientv3-grpc1.0 können ein schnelleres Failover des Load-Balancers ermöglichen, benötigen aber mehr Ressourcen. Der Load-Balancer kennt weder den Zustand der Knoten noch ihre Clustermitgliedschaft. Daher kann er an einem ausgefallenen oder durch eine Netzwerkpartition abgetrennten Knoten hängen bleiben.

clientv3-grpc1.7: Überblick über den Load-Balancer

clientv3-grpc1.7 hält nur eine TCP-Verbindung zu einem ausgewählten etcd-Server offen. Bei mehreren angegebenen Clusterendpunkten versucht ein Client zunächst, sich mit allen zu verbinden. Sobald eine Verbindung steht, ordnet der Load-Balancer die Adresse fest zu und schließt die anderen Verbindungen (siehe Abbildung 2). Die Adresse soll zugeordnet bleiben, bis das Client-Objekt geschlossen wird. Ein Fehler des Servers oder des Client-Netzwerks wird an die Fehlerbehandlung des Clients weitergeleitet (siehe Abbildung 3).

client-balancer-figure-02.pngclient-balancer-figure-03.png

Die Fehlerbehandlung des Clients erhält einen Fehler vom gRPC-Server und entscheidet anhand von Fehlercode und Meldung, ob dieselbe Anfrage am selben Endpunkt wiederholt oder zu einer anderen Adresse gewechselt wird (siehe Abbildung 4 und Abbildung 5).

client-balancer-figure-04.pngclient-balancer-figure-05.png

Streaming-RPCs wie Watch und KeepAlive werden häufig ohne Zeitlimit angefordert. Stattdessen kann der Client regelmäßig HTTP/2-Pings senden, um den Zustand eines fest zugeordneten Endpunkts zu prüfen. Antwortet der Server nicht auf den Ping, wechselt der Load-Balancer zu anderen Endpunkten (siehe Abbildung 6).

client-balancer-figure-06.png

clientv3-grpc1.7: Einschränkungen des Load-Balancers

Der Load-Balancer von clientv3-grpc1.7 sendet HTTP/2-Keepalives, um Verbindungsabbrüche bei Streaming-Anfragen zu erkennen. Dieser einfache Ping-Mechanismus für gRPC-Server berücksichtigt keine Clustermitgliedschaft und kann daher Netzwerkpartitionen nicht erkennen. Da ein abgetrennter gRPC-Server weiterhin auf Client-Pings antworten kann, kann der Load-Balancer an diesem Knoten hängen bleiben. Idealerweise erkennt der Keepalive-Ping die Partition und löst vor Ablauf des Anfragezeitlimits einen Endpunktwechsel aus (siehe etcd#8673 und Abbildung 7).

client-balancer-figure-07.png

Der Load-Balancer von clientv3-grpc1.7 führt eine Liste nicht funktionsfähiger Endpunkte. Adressen mit abgebrochener Verbindung werden dieser Liste hinzugefügt und gelten bis zum Ende einer Wartezeit als nicht verfügbar. Diese Wartezeit ist fest an das Verbindungsaufbau-Zeitlimit mit einem Standardwert von 5 Sekunden gekoppelt. Der Load-Balancer kann Endpunkte fälschlich als nicht funktionsfähig einstufen. Beispielsweise kann Endpunkt A unmittelbar nach Aufnahme in die Sperrliste zurückkehren, bleibt aber dennoch für die nächsten 5 Sekunden unbenutzbar (siehe Abbildung 8).

clientv3-grpc1.0 hatte dieselben oben beschriebenen Probleme.

client-balancer-figure-08.png

Das Upstream-Projekt gRPC Go war bereits auf die neue Load-Balancer-Schnittstelle umgestiegen. So verwendet die zugrunde liegende Implementierung von clientv3-grpc1.7 den neuen gRPC-Load-Balancer und versucht dabei, das Verhalten des alten beizubehalten. Obwohl die Kompatibilität recht gut erhalten blieb, war der etcd-Client dennoch von subtilen inkompatiblen Änderungen betroffen . Zudem empfiehlt der gRPC-Maintainer, sich nicht auf die alte Load-Balancer-Schnittstelle zu stützen . Für bessere Unterstützung durch Upstream ist es generell am besten, mit den neuesten gRPC-Versionen Schritt zu halten. Neue Funktionen wie Wiederholungsrichtlinien werden möglicherweise nicht auf den gRPC-1.7-Zweig zurückportiert. Daher müssen sowohl etcd-Server als auch -Client auf die neuesten gRPC-Versionen umsteigen.

clientv3-grpc1.23: Überblick über den Load-Balancer

clientv3-grpc1.7 ist so eng an die alte gRPC-Schnittstelle gekoppelt, dass jedes Upgrade der gRPC-Abhängigkeit das Client-Verhalten beeinträchtigte. Der Großteil der Entwicklungs- und Fehlersucharbeit floss in die Behebung dieser Verhaltensänderungen. Dadurch wurde die Implementierung übermäßig komplex und beruhte auf falschen Annahmen über Serververbindungen.

Das Hauptziel von clientv3-grpc1.23 ist eine einfachere Failover-Logik des Load-Balancers. Statt eine möglicherweise veraltete Liste nicht funktionsfähiger Endpunkte zu führen, wechselt er bei jedem Verbindungsabbruch zum aktuellen Endpunkt im Round-Robin-Verfahren zum nächsten. Er trifft keine Annahmen über den Endpunktzustand. Eine komplexe Zustandsverfolgung ist daher nicht mehr nötig (siehe Abbildung 8 und die vorangehenden Erläuterungen). Ein Upgrade auf clientv3-grpc1.23 sollte problemlos sein: Alle Änderungen waren intern und bewahrten die vollständige Abwärtskompatibilität.

Bei mehreren Endpunkten erstellt clientv3-grpc1.23 intern mehrere Unterverbindungen, eine pro Endpunkt. clientv3-grpc1.7 erstellt dagegen nur eine Verbindung zu einem fest zugeordneten Endpunkt (siehe Abbildung 9). In einem Cluster mit 5 Knoten benötigt der Load-Balancer von clientv3-grpc1.23 beispielsweise 5 TCP-Verbindungen, während clientv3-grpc1.7 nur eine benötigt. Durch den erhaltenen TCP-Verbindungspool kann clientv3-grpc1.23 mehr Ressourcen verbrauchen, bietet aber eine flexiblere Lastverteilung mit besserer Failover-Leistung. Standardmäßig wird Round Robin verwendet; andere Verfahren wie „Power of Two“ oder die Auswahl des Leaders lassen sich leicht ergänzen. clientv3-grpc1.23 verwendet eine gRPC-Resolver-Gruppe und implementiert eine Auswahlrichtlinie für den Load-Balancer, um die komplexe Lastverteilung an Upstream-gRPC zu delegieren. clientv3-grpc1.7 verwaltet dagegen jede gRPC-Verbindung und das Failover selbst, was die Implementierung verkompliziert. clientv3-grpc1.23 implementiert Wiederholungen in der gRPC-Interceptor-Kette, die interne gRPC-Fehler automatisch behandelt und fortgeschrittene Wiederholungsrichtlinien wie Backoff ermöglicht. clientv3-grpc1.7 wertet gRPC-Fehler für Wiederholungen dagegen selbst aus.

client-balancer-figure-09.png

clientv3-grpc1.23: Einschränkungen des Load-Balancers

Verbesserungen sind durch Zwischenspeichern des Zustands jedes Endpunkts möglich. Beispielsweise kann der Load-Balancer vorab jeden Server anpingen, um eine Liste funktionsfähiger Kandidaten zu führen und diese Informationen beim Round-Robin-Verfahren zu nutzen. Oder er kann nach einem Verbindungsabbruch funktionsfähige Endpunkte bevorzugen. Dies kann die Implementierung verkomplizieren und deshalb in späteren Versionen behandelt werden.

Der clientseitige Keepalive-Ping berücksichtigt weiterhin keine Netzwerkpartitionen. Streaming-Anfragen können an einem abgetrennten Knoten hängen bleiben. Um die Clustermitgliedschaft zu berücksichtigen, muss ein erweiterter Health-Check-Dienst implementiert werden (Einzelheiten unter etcd#8673 ).

client-balancer-figure-07.png

Derzeit wird die Wiederholungslogik manuell als Interceptor umgesetzt. Dies kann durch offizielle gRPC-Wiederholungen vereinfacht werden.

3 - Design der Learner in etcd

Häufige Schwierigkeiten bei der Neukonfiguration der Clustermitgliedschaft entschärfen

Learner in etcd

Gyuho Lee (github.com/gyuho, Amazon Web Services, Inc.), Joe Betz (github.com/jpbetz, Google Inc.)

Hintergrund

Die Neukonfiguration der Clustermitgliedschaft zählt zu den größten Herausforderungen im Betrieb. Im Folgenden werden häufige Schwierigkeiten betrachtet.

1. Ein neues Clustermitglied überlastet den Leader

Ein neu beigetretenes etcd-Mitglied startet ohne Daten und fordert daher zusätzliche Aktualisierungen vom Leader an, bis es dessen Logstand erreicht hat. Dadurch kann das Netzwerk des Leaders leichter überlastet werden, sodass seine Heartbeats an Follower blockiert oder verworfen werden. In diesem Fall kann bei einem Follower das Wahlzeitlimit ablaufen und eine neue Leader-Wahl auslösen. Ein Cluster mit einem neuen Mitglied ist somit anfälliger für Leader-Wahlen. Sowohl die Wahl als auch die anschließende Übertragung der Aktualisierungen an das neue Mitglied können Zeiträume verursachen, in denen der Cluster nicht verfügbar ist (siehe Abbildung 1).

server-learner-figure-01

2. Szenarien mit Netzwerkpartitionen

Was passiert bei einer Netzwerkpartition? Das hängt von der Partition des Leaders ab. Verfügt der Leader weiterhin über ein aktives Quorum, arbeitet der Cluster weiter (siehe Abbildung 2).

server-learner-figure-02

2.1 Isolation des Leaders

Was passiert, wenn der Leader vom Rest des Clusters isoliert wird? Der Leader überwacht den Fortschritt jedes Followers. Verliert er die Verbindung zum Quorum, kehrt er in die Follower-Rolle zurück. Dies beeinträchtigt die Clusterverfügbarkeit (siehe Abbildung 3).

server-learner-figure-03

Wird einem Cluster mit 3 Knoten ein neuer Knoten hinzugefügt, steigt die Clustergröße auf 4 und die Quorumgröße auf 3. Was passiert, wenn nach dem Beitritt eines neuen Knotens eine Netzwerkpartition auftritt? Das hängt davon ab, in welcher Partition sich das neue Mitglied anschließend befindet.

2.2 Clusteraufteilung 3+1

Befindet sich der neue Knoten in derselben Partition wie der Leader, verfügt dieser weiterhin über ein aktives Quorum von 3. Es findet keine Leader-Wahl statt, und die Clusterverfügbarkeit bleibt unbeeinträchtigt (siehe Abbildung 4).

server-learner-figure-04

2.3 Clusteraufteilung 2+2

Wird der Cluster in 2 und 2 Knoten aufgeteilt, erreicht keine Partition das Quorum von 3. In diesem Fall findet eine Leader-Wahl statt (siehe Abbildung 5).

server-learner-figure-05

2.4 Quorum verloren

Was passiert, wenn zuerst eine Netzwerkpartition auftritt und danach ein neues Mitglied hinzugefügt wird? In einem partitionierten Cluster mit 3 Knoten ist bereits ein Follower abgetrennt. Wird ein neues Mitglied hinzugefügt, steigt das Quorum von 2 auf 3. Der Cluster hat nun nur 2 aktive von 4 Knoten, verliert dadurch sein Quorum und beginnt eine neue Leader-Wahl (siehe Abbildung 6).

server-learner-figure-06

Da das Hinzufügen eines Mitglieds die Quorumgröße ändern kann, wird beim Ersetzen eines nicht funktionsfähigen Knotens stets empfohlen, zuerst „member remove“ auszuführen.

Das Hinzufügen eines neuen Mitglieds zu einem Cluster mit 1 Knoten erhöht die Quorumgröße auf 2. Sobald der bisherige Leader feststellt, dass das Quorum nicht aktiv ist, löst dies unmittelbar eine Leader-Wahl aus. Der Grund: „member add“ ist ein Vorgang mit 2 Schritten, bei dem der Benutzer zuerst den Befehl „member add“ ausführen und anschließend den Prozess des neuen Knotens starten muss (siehe Abbildung 7).

server-learner-figure-07

3. Fehlkonfigurationen des Clusters

Noch problematischer ist ein falsch konfiguriertes hinzugefügtes Mitglied. Die Neukonfiguration der Mitgliedschaft besteht aus zwei Schritten: „etcdctl member add“ und dem Start eines etcd-Serverprozesses mit der angegebenen Peer-URL. Der Befehl „member add“ wird also unabhängig von der URL angewendet, selbst wenn diese ungültig ist. Werden im ersten Schritt ungültige URLs verwendet, kann der zweite Schritt den neuen etcd-Prozess nicht einmal starten. Sobald der Cluster sein Quorum verliert, lässt sich die Mitgliedschaftsänderung nicht mehr rückgängig machen (siehe Abbildung 8).

server-learner-figure-08

Dasselbe gilt für einen Cluster mit mehreren Knoten. Beispielsweise hat der Cluster zwei nicht verfügbare Mitglieder, eines ausgefallen und eines falsch konfiguriert, sowie zwei aktive Mitglieder. Er benötigt nun aber mindestens 3 Stimmen, um die Clustermitgliedschaft zu ändern (siehe Abbildung 9).

server-learner-figure-09

Wie gezeigt, kann eine einfache Fehlkonfiguration den gesamten Cluster funktionsunfähig machen. In diesem Fall muss ein Betreiber den Cluster manuell mit der Option etcd --force-new-cluster neu erstellen. Da etcd für Kubernetes zu einem betriebskritischen Dienst geworden ist, können selbst kürzeste Ausfälle erhebliche Auswirkungen auf Benutzer haben. Wie lassen sich solche etcd-Operationen vereinfachen? Besonders entscheidend für die Clusterverfügbarkeit ist die Leader-Wahl: Lässt sich die Neukonfiguration der Mitgliedschaft weniger störend gestalten, indem die Quorumgröße unverändert bleibt? Kann ein neuer Knoten zunächst inaktiv bleiben und nur die nötigsten Aktualisierungen vom Leader anfordern, bis er aufgeholt hat? Lassen sich Fehlkonfigurationen der Mitgliedschaft immer rückgängig machen und sicherer behandeln, sodass ein falscher Befehl zum Hinzufügen eines Mitglieds niemals den Cluster ausfallen lässt? Sollte ein Benutzer beim Hinzufügen eines Mitglieds die Netzwerktopologie berücksichtigen müssen? Kann die API zum Hinzufügen von Mitgliedern unabhängig von der Lage der Knoten und bestehenden Netzwerkpartitionen funktionieren?

Raft-Learner

Um die im vorigen Abschnitt beschriebenen Verfügbarkeitslücken zu verringern, führt Raft §4.2.1 den neuen Knotenzustand „Learner“ ein. Ein solcher Knoten tritt dem Cluster als nicht stimmberechtigtes Mitglied bei, bis er den Logstand des Leaders erreicht hat.

Funktionen in v3.4

Ein Betreiber sollte möglichst wenig Aufwand haben, um einen neuen Learner-Knoten hinzuzufügen. Der Befehl member add --learner fügt einen Learner hinzu, der als nicht stimmberechtigtes Mitglied beitritt, aber dennoch alle Daten vom Leader empfängt (siehe Abbildung 10).

server-learner-figure-10

Hat ein Learner den Fortschritt des Leaders eingeholt, kann er über die API member promote zu einem stimmberechtigten Mitglied hochgestuft werden und zählt dann zum Quorum (siehe Abbildung 11).

server-learner-figure-11

Der etcd-Server validiert die Hochstufungsanfrage, um einen sicheren Betrieb zu gewährleisten. Erst wenn sein Log den Stand des Leaders erreicht hat, kann ein Learner zu einem stimmberechtigten Mitglied hochgestuft werden (siehe Abbildung 12).

server-learner-figure-12

Bis zur Hochstufung dient ein Learner nur als Standby-Knoten: Die Führungsrolle kann ihm nicht übertragen werden. Ein Learner lehnt Lese- und Schreibanfragen von Clients ab; der Client-Load-Balancer sollte keine Anfragen an ihn weiterleiten. Daher muss ein Learner keine Read-Index-Anfragen an den Leader senden. Diese Einschränkung vereinfacht die erste Learner-Implementierung in v3.4 (siehe Abbildung 13).

server-learner-figure-13

Außerdem begrenzt etcd die Gesamtzahl der Learner in einem Cluster, um eine Überlastung des Leaders durch Logreplikation zu vermeiden. Ein Learner stuft sich niemals selbst hoch. etcd liefert Statusinformationen über Learner und Sicherheitsprüfungen; die endgültige Entscheidung über eine Hochstufung muss jedoch der Clusterbetreiber treffen.

Vorgeschlagene Funktionen für künftige Versionen

Learner als einzigen Anfangszustand und Standard festlegen: Wird der Anfangszustand neuer Mitglieder auf Learner gesetzt, erhöht dies die Sicherheit der Mitgliedschaftsneukonfiguration erheblich, da Learner die Quorumgröße nicht verändern. Fehlkonfigurationen lassen sich dann immer rückgängig machen, ohne das Quorum zu verlieren.

Die Hochstufung zum stimmberechtigten Mitglied vollständig automatisieren: Sobald ein Learner den Logstand des Leaders erreicht hat, kann der Cluster ihn automatisch hochstufen. Der Benutzer legt bestimmte von etcd benötigte Schwellenwerte fest. Sind die Anforderungen erfüllt, stuft sich der Learner selbst zu einem stimmberechtigten Mitglied hoch. Aus Benutzersicht würde „member add“ wie bisher funktionieren, aber dank der Learner-Funktion mehr Sicherheit bieten.

Learner als Standby-Knoten für Failover verwenden: Ein Learner tritt als Standby-Knoten bei und wird automatisch hochgestuft, wenn die Clusterverfügbarkeit beeinträchtigt ist.

Learner nur für Lesezugriffe verwenden: Ein Learner kann als ausschließlich lesender Knoten dienen, der nie hochgestuft wird. In einem Modus mit schwacher Konsistenz empfängt er nur Daten vom Leader und verarbeitet keine Schreibvorgänge. Lokale Lesezugriffe ohne Konsensaufwand würden die Last des Leaders erheblich verringern, können aber veraltete Daten liefern. In einem Modus mit starker Konsistenz fordert der Learner einen Leseindex vom Leader an, um die neuesten Daten bereitzustellen, lehnt Schreibvorgänge aber weiterhin ab.

Learner und Mirror Maker im Vergleich

etcd implementiert „mirror maker“ mit der Watch-API, um Schlüsselerstellungen und -aktualisierungen fortlaufend an einen separaten Cluster weiterzuleiten. Nach Abschluss der anfänglichen Synchronisierung verursacht die Spiegelung gewöhnlich nur geringe zusätzliche Latenz. Learner und Spiegelung überschneiden sich darin, dass beide vorhandene Daten für reine Lesezugriffe replizieren können. Die Spiegelung garantiert jedoch keine Linearisierbarkeit. Bei Netzwerkunterbrechungen können frühere Schlüssel-Wert-Paare bereits verworfen worden sein, und von Clients wird erwartet, die Watch-Antworten auf korrekte Reihenfolge zu prüfen. Eine Spiegelung bietet daher keine Reihenfolgegarantie. Spiegelung eignet sich für minimale Latenz, etwa zwischen Rechenzentren, auf Kosten der Konsistenz. Learner eignen sich, um alle historischen Daten und deren Reihenfolge zu erhalten.

Anhang: Learner-Implementierung in v3.4

Den Knotentyp “Learner” in der API “MemberAdd” verfügbar machen.

Der etcd-Client ergänzt die API „MemberAdd“ um eine Option für Learner-Knoten. Der Server-Handler von etcd wendet einen Mitgliedschaftsänderungseintrag vom Typ pb.ConfChangeAddLearnerNode an. Nach Anwendung des Befehls tritt ein Server mit der Option etcd --initial-cluster-state=existing dem Cluster bei. Dieser Learner-Knoten ist weder stimmberechtigt noch zählt er zum Quorum.

Der etcd-Server darf die Führungsrolle keinem Learner übertragen, da dieser noch zurückliegen kann und nicht zum Quorum zählt. Der etcd-Server begrenzt die Anzahl der Learner pro Cluster auf einen: Je mehr Learner vorhanden sind, desto mehr Daten muss der Leader verteilen. Clients können mit einem Learner-Knoten kommunizieren, dieser lehnt jedoch alle Anfragen außer serialisierbaren Lesezugriffen und der API für den Mitgliedsstatus ab. Das vereinfacht die erste Implementierung. Künftig kann ein Learner zu einem ausschließlich lesenden Server erweitert werden, der Clusterdaten fortlaufend spiegelt. Der Client-Load-Balancer muss eine Hilfsfunktion bereitstellen, um den Endpunkt eines Learner-Knotens auszuschließen. Andernfalls können Anfragen an den Learner fehlschlagen. Der Client-Aufruf zur Mitgliedersynchronisierung sollte den Knotentyp Learner berücksichtigen, ebenso der Aufruf zur Aktualisierung der Client-Endpunkte.

Die Antworten von MemberList und MemberStatus sollten angeben, welcher Knoten ein Learner ist.

Die API “MemberPromote” hinzufügen.

Raft-intern stuft ein zweiter MemberAdd-Aufruf für den Learner-Knoten diesen zu einem stimmberechtigten Mitglied hoch. Der Leader verwaltet den Fortschritt jedes Followers und Learners. Hat der Learner die Verarbeitung seiner Snapshot-Nachricht noch nicht abgeschlossen, wird die Hochstufungsanfrage abgelehnt. Sie wird genau dann angenommen, wenn der Learner-Knoten funktionsfähig ist und mit dem Leader synchron ist oder die Differenz innerhalb des Schwellenwerts liegt. Beispielsweise muss die Zahl der an den Learner zu replizierenden Einträge unter 1/10 der Snapshot-Anzahl liegen; damit ist es weniger wahrscheinlich, dass der Leader selbst nach der Hochstufung einen Snapshot an den Learner senden müsste. Diese gesamte Logik ist im Paket etcdserver fest codiert und nicht konfigurierbar.

Referenzen

4 - Authentifizierungsdesign von etcd v3

Authentifizierung in etcd v3

Warum das Authentifizierungssystem von v2 nicht wiederverwenden?

Das v3-Protokoll verwendet gRPC als Transport anstelle einer RESTful-Schnittstelle wie in v2. Das neue Protokoll bietet die Gelegenheit, den Entwurf von v2 weiterzuentwickeln und zu verbessern. Beispielsweise authentifiziert v3 pro Verbindung, während die langsamere Authentifizierung in v2 pro Anfrage erfolgt. Zudem erschwert die Semantik der v2-Authentifizierung in der Praxis Überlegungen zur Konsistenz, wie die nächsten Abschnitte erläutern. Für v3 gibt es eine klar definierte Beschreibung und Implementierung des Authentifizierungsmechanismus, die die Schwächen des v2-Systems behebt.

Funktionale Anforderungen

  • Authentifizierung pro Verbindung statt pro Anfrage
    • Auf Benutzer-ID und Passwort basierende Authentifizierung für die gRPC-API
    • Die Authentifizierung muss nach Änderungen der Authentifizierungsrichtlinie erneuert werden
  • Die Funktionalität sollte so einfach und nützlich sein wie in v2
    • v3 bietet einen flachen Schlüsselraum statt der Verzeichnisstruktur von v2. Berechtigungen werden durch Intervallabgleich geprüft.
  • Sie sollte stärkere Konsistenzgarantien als die v2-Authentifizierung bieten

Wesentliche erforderliche Änderungen

  • Ein Client muss vor dem Senden authentifizierter Anfragen eine eigene Verbindung ausschließlich zur Authentifizierung aufbauen
  • Berechtigungsinformationen (Benutzer-ID und autorisierte Revision) zu den Raft-Befehlen (etcdserverpb.InternalRaftRequest) hinzufügen
  • Die Berechtigungen jeder Anfrage werden in der Zustandsmaschinenschicht statt in der API-Schicht geprüft

Konsistenz der Berechtigungsmetadaten

Die Authentifizierungsmetadaten sollten wie andere Daten in etcd im Speicher abgelegt und verwaltet werden, den das Raft-Protokoll von etcd kontrolliert. Das ist erforderlich, um Verfügbarkeit und Konsistenz des gesamten etcd-Clusters zu erhalten. Wenn das Lesen oder Schreiben der Metadaten, etwa von Berechtigungsinformationen, die Zustimmung jedes Knotens und damit mehr als ein Quorum benötigt, kann der Ausfall eines einzelnen Knotens den gesamten Cluster anhalten. Müssen alle Knoten gleichzeitig zustimmen, lässt sich die Prüfung gewöhnlicher Lese- und Schreibanfragen nicht abschließen, sobald ein Clustermitglied ausfällt, selbst wenn noch ein Quorum verfügbar ist. Dieses Einstimmigkeitsverfahren verschlechtert letztlich die Clusterverfügbarkeit. Der auf einem Quorum basierende Raft-Konsens sollte ausreichen, da die Übereinstimmung aus einer konsistenten Reihenfolge folgt.

Der Authentifizierungsmechanismus des etcd-v2-Protokolls hat eine problematische Eigenschaft: Die Metadatenkonsistenz sollte wie oben beschrieben funktionieren, tut es aber nicht. Jede Berechtigungsprüfung wird von dem etcd-Mitglied durchgeführt, das die Client-Anfrage empfängt (server/etcdserver/api/v2http/client.go), einschließlich Follower-Mitgliedern. Die Prüfung kann daher auf veralteten Metadaten beruhen.

Aufgrund dieser möglichen Veraltung wird die Authentifizierungskonfiguration nicht zwingend wirksam, sobald Betreiber etcdctl ausführen. Es lässt sich daher nicht feststellen, wie lange veraltete Metadaten noch gelten. In der Praxis wird die Konfigurationsänderung unmittelbar nach der Befehlsausführung wirksam. Unter hoher Last kann der inkonsistente Zustand jedoch länger anhalten und für Benutzer und Entwickler unerwartete Situationen verursachen. Dafür ist eine Umgehungslösung wie diese erforderlich.

Inkonsistente Berechtigungen gefährden linearisierte Anfragen

Ein inkonsistenter Authentifizierungszustand ist bei Schreibvorgängen am schwerwiegendsten. Selbst wenn ein Betreiber einem Benutzer das Schreiben verbietet, kann ein Schreibvorgang erfolgreich abgeschlossen werden, sofern seine Reihenfolge nur gegenüber dem Schlüssel-Wert-Speicher, nicht aber gegenüber dem Authentifizierungssystem festgelegt ist. Ohne eine Reihenfolge über Authentifizierungsspeicher und Schlüssel-Wert-Speicher hinweg ist das System für Angriffe mit veralteten Berechtigungen anfällig.

Deshalb sollte die Berechtigungsprüfung in die Zustandsmaschine von etcd aufgenommen werden. Jede Zustandsmaschine sollte Anfragen in der Apply-Phase anhand ihrer Berechtigungsinformationen prüfen. Die Authentifizierungsinformationen dürfen dabei nicht veraltet sein.

Entwurf und Implementierung

Authentifizierung

Zunächst muss ein Client eine gRPC-Verbindung ausschließlich zur Authentifizierung seiner Benutzer-ID und seines Passworts aufbauen. Ein etcd-Server antwortet bei Erfolg mit einem Authentifizierungstoken, bei Fehlschlag mit einem Fehler. Der Client kann das Token verwenden, um sich bei API-Anfragen gegenüber etcd auszuweisen.

Die Client-Verbindung, über die das Authentifizierungstoken angefordert wurde, wird üblicherweise verworfen; sie kann die Anmeldedaten des neuen Tokens nicht übertragen. gRPC bietet nämlich keine Möglichkeit, nach dem Verbindungsaufbau durch grpc.Dial() Anmeldedaten pro RPC hinzuzufügen. Ein Client kann einer Verbindung daher kein Token zuweisen, das er über diese Verbindung erhalten hat. Zur Verwendung des Tokens benötigt er eine neue Verbindung.

Hinweise zur Implementierung des RPC Authenticate()

Der RPC Authenticate() erzeugt anhand eines angegebenen Benutzernamens und Passworts ein Authentifizierungstoken. etcd verwendet das Go-Paket bcrypt, um ein konfiguriertes Passwort zu speichern und ein angegebenes Passwort dagegen zu prüfen. Die Passwortprüfung von bcrypt ist absichtlich rechenaufwendig und dauert auf einem gewöhnlichen x64-Server fast 100ms. Diese Prüfung in der Apply-Phase der Zustandsmaschine würde daher Leistungsprobleme verursachen: Der gesamte etcd-Cluster könnte nur knapp 10 Authenticate()-Anfragen pro Sekunde verarbeiten.

Aus Leistungsgründen prüft der v3-Authentifizierungsmechanismus Passwörter in der API-Schicht von etcd, wo dies außerhalb von Raft parallelisiert werden kann. Dadurch können jedoch Berechtigungslücken zwischen Prüf- und Verwendungszeitpunkt entstehen (Time-of-check/Time-of-use, TOCTOU):

  1. Client A sendet eine Anfrage Authenticate()
  2. Die API-Schicht führt die Passwortprüfung von Authenticate() aus
  3. Ein anderer Client B sendet eine Anfrage ChangePassword(), die der Server abschließt
  4. Die Zustandsmaschinenschicht ermittelt die Revisionsnummer für Authenticate() von A
  5. Der Server meldet A Erfolg
  6. A ist nun mit einem veralteten Passwort authentifiziert

Um diese Situation zu vermeiden, führt die API-Schicht eine Validierung der Versionsnummer anhand der Revisionsnummer des Authentifizierungsspeichers durch. Während der Passwortprüfung speichert sie dessen Revisionsnummer. Nach erfolgreicher Prüfung vergleicht sie die gespeicherte mit der neuesten Revisionsnummer. Unterscheiden sich die Nummern, wurden die Authentifizierungsmetadaten zwischenzeitlich aktualisiert, und die Prüfung wird wiederholt. Dieser Mechanismus verhindert eine erfolgreiche Prüfung anhand eines veralteten Passworts.

Ein Token in der API-Schicht auflösen

Nach der Authentifizierung mit Authenticate() kann ein Client wie ohne Authentifizierung eine gRPC-Verbindung aufbauen. Zusätzlich zum bestehenden Initialisierungsablauf muss er das Token der neu erstellten Verbindung zuordnen. grpc.WithPerRPCCredentials() bietet die dafür erforderliche Funktion.

Jede authentifizierte Client-Anfrage enthält ein Token. Serverseitig lässt es sich mit grpc.metadata.FromIncomingContext() abrufen. Der Server kann daraus ermitteln, wer die Anfrage stellt und wann der Benutzer autorisiert wurde. Die API-Schicht trägt diese Informationen in den Header (etcdserverpb.RequestHeader.Username und etcdserverpb.RequestHeader.AuthRevision) eines Raft-Logeintrags (etcdserverpb.InternalRaftRequest) ein.

Berechtigungen in der Zustandsmaschine prüfen

Die Authentifizierungsinformationen in etcdserverpb.RequestHeader werden in der Apply-Phase der Zustandsmaschine geprüft. Dabei wird anhand der neuesten Revision des Authentifizierungsspeichers geprüft, ob der Benutzer Zugriff auf die angeforderten Schlüssel hat.

Zwei Token-Typen: simple und JWT

Es gibt zwei Token-Typen: simple und JWT. Das simple-Token ist nicht für den Produktivbetrieb vorgesehen. Diese Token sind nicht kryptografisch signiert, und die Server müssen die Zuordnung zwischen Token und Benutzern als Zustand verwalten. Sie sind für Entwicklungstests gedacht. Für produktive Bereitstellungen sollten JWT-Token verwendet werden, da sie kryptografisch signiert und geprüft werden. Aus Implementierungssicht ist JWT zustandslos. Ein JWT-Token kann Metadaten wie Benutzernamen und Revision enthalten, sodass Server die Zuordnung zwischen Token und Metadaten nicht speichern müssen.

Warnung

Für simple-Token ist das Problem #18437 bekannt. Auf etcd-Servern werden Token in der API-Schicht aufgelöst, und simple-Token sind zustandsbehaftet. Der Vorgang ist nicht durch eine linearisierbare Prüfung geschützt. Daher kann ein etcd-Mitglied die vorherige Authentifizierungsanfrage noch nicht vollständig verarbeitet haben, wenn die nächste eintrifft. In diesem Fall kann es dem Client den Fehler “invalid auth token” zurückgeben. Auf einem Knoten mit guten Netzwerkbedingungen tritt dieses Problem gewöhnlich selten auf; bei hoher Latenz ist es jedoch möglich. Als Umgehungslösung können Anwendungen bei diesem Fehler einen Wiederholungsmechanismus einsetzen.

JWT-Token direkt setzen

Zusätzlich zum Standardablauf über den RPC Authenticate() unterstützt etcd das direkte Setzen von JWT-Token auf Client-Ebene. Dadurch können Anwendungen den gesamten Lebenszyklus von JWT-Token außerhalb von etcd verwalten, einschließlich Erzeugung, Validierung und Rotation.

Anwendungsfall und Ablauf

Dieser Ansatz ist sinnvoll, wenn:

  • Ein separates Token-Verwaltungssystem außerhalb von etcd die Erzeugung und den Lebenszyklus der JWT-Token übernimmt
  • Anwendungen bereits signierte JWT-Token über einen externen Mechanismus erhalten, etwa Umgebungsvariablen oder einen Konfigurationsdienst
  • Der Token-Lebenszyklus vollständig von der Client-Anwendung statt durch die automatische Token-Erzeugung von etcd verwaltet werden muss

Der typische Ablauf ist:

  1. Eine externe Instanz außerhalb von etcd erzeugt ein signiertes JWT-Token, das den Benutzernamen und weitere Claims enthält
  2. Die Anwendung erhält das signierte Token und konfiguriert damit den etcd-Client
  3. Der Client sendet das JWT-Token direkt mit den Anfragen, ohne Authenticate() aufzurufen
  4. Der etcd-Server prüft die Token-Signatur mit seinem konfigurierten öffentlichen Schlüssel und gewährt Zugriff anhand des Benutzernamens im Token
  5. Vor Ablauf des Tokens ruft die Anwendung ein neues Token von der externen Instanz ab
  6. Die Anwendung erstellt einen neuen Client mit dem aktualisierten Token; Token-Aktualisierungen erfordern eine Neuerstellung des Clients

Unterschiede zur Standardauthentifizierung

Beim Standardablauf mit Authenticate():

  • Der Client ruft Authenticate() mit Benutzername und Passwort auf
  • etcd erzeugt ein Token und gibt es zurück
  • Der Client verwendet dieses Token automatisch für nachfolgende Anfragen
  • Zum Erneuern des Tokens muss Authenticate() erneut aufgerufen werden

Beim direkten Setzen von JWT-Token:

  • Der Client wird mit einem bereits signierten JWT-Token initialisiert
  • Der Client ruft Authenticate() nicht auf
  • Das Token wird direkt in allen Anfragen verwendet
  • Die Client-Anwendung ist dafür verantwortlich, vor Ablauf neue Token zu beschaffen und den Lebenszyklus des Clients zu verwalten

AuthStatus ohne gültiges Token

Um Anwendungen mit eigener JWT-Token-Verwaltung zu unterstützen, ermöglicht der RPC AuthStatus Clients festzustellen, ob die Authentifizierung aktiviert ist, und die aktuelle authRevision abzurufen. Das ist für Wiederherstellungsszenarien wichtig, in denen ein Token abgelaufen ist und der Client die neueste Revision benötigt, um von seinem externen Token-Anbieter ein neues gültiges Token zu erhalten.

Ohne diese Möglichkeit könnte ein abgelaufenes Token verhindern, dass ein Client die aktuelle authRevision ermittelt. Dies könnte zu einer Blockade führen, in der kein neues Token erzeugt werden kann.

Unterschiede zwischen KVS-Modellen und Dateisystemmodellen

etcd v3 ist ein KVS und kein Dateisystem. Daher können Benutzern Berechtigungen für einen exakten Schlüsselnamen oder einen Schlüsselbereich wie ["start key", "end key") erteilt werden. Berechtigungen für nicht vorhandene Schlüssel sind somit möglich. Benutzer sollten auf unbeabsichtigt erteilte Berechtigungen achten. In dateisystemähnlichen Systemen wie Chubby oder ZooKeeper kann eine inode-ähnliche Datenstruktur die Berechtigungsinformationen enthalten. Dort können keine Berechtigungen für nicht vorhandene Schlüssel erteilt werden, abgesehen vom Fall der Sticky Bits.

Anders als dateisystemähnliche Systeme erfordert das Modell von etcd v3 mehrere Metadatenabfragen. Im ungünstigsten Fall entspricht der Suchaufwand der Summe aller Schlüssel und Intervalle, für die der Benutzer Berechtigungen hat. Dieser Aufwand lässt sich nicht vermeiden, weil sich der flache Schlüsselraum von v3 grundlegend vom Unix-Dateisystemmodell unterscheidet, bei dem jeder inode Berechtigungsmetadaten enthält. In der Praxis ist der Aufwand kein ernstes Problem, da die Metadaten klein genug sind, um vom Caching zu profitieren.

5 - etcd-API

Überblick über den grundlegenden Entwurf der etcd-API

Dieses Dokument gibt einen Überblick über den grundlegenden Entwurf der etcd-v3-APIs. Diese sind nicht mit der etcd-v2-API zu verwechseln, die seit etcd v3.5 veraltet ist. Der Überblick ist keineswegs vollständig. Er konzentriert sich auf die Grundideen, die zum Verständnis von etcd nötig sind, ohne durch seltenere API-Aufrufe abzulenken. Alle etcd-APIs sind in gRPC-Diensten definiert, die die vom etcd-Server verstandenen Remote Procedure Calls (RPCs) nach Funktionen gliedern. Eine vollständige Liste aller etcd-RPCs ist in Markdown in der gRPC-API-Übersicht dokumentiert.

gRPC-Dienste

Jede API-Anfrage an einen etcd-Server ist ein gRPC-Remote-Procedure-Call. Die RPCs von etcd werden nach ihrer Funktion in Dienste eingeteilt.

Für die Arbeit mit dem Schlüsselraum von etcd sind folgende Dienste wichtig:

  • KV - Erstellt, aktualisiert, liest und löscht Schlüssel-Wert-Paare.
  • Watch - Überwacht Änderungen an Schlüsseln.
  • Lease - Grundoperationen zum Verarbeiten von Keepalive-Nachrichten der Clients.

Den Cluster selbst verwalten folgende Dienste:

  • Auth - Rollenbasierter Authentifizierungsmechanismus zur Authentifizierung von Benutzern.
  • Cluster - Liefert Mitgliedschaftsinformationen und Konfigurationsfunktionen.
  • Maintenance - Erstellt Snapshots für die Wiederherstellung, defragmentiert den Speicher und liefert Statusinformationen pro Mitglied.

Anfragen und Antworten

Alle RPCs in etcd folgen demselben Format. Jeder RPC hat eine Funktion Name, die NameRequest als Argument entgegennimmt und NameResponse als Antwort zurückgibt. Beispielsweise lautet die Beschreibung des RPC Range:

service KV {
  Range(RangeRequest) returns (RangeResponse)
  ...
}

Antwortheader

Alle Antworten der etcd-API enthalten einen Antwortheader mit Clustermetadaten zur Antwort:

message ResponseHeader {
  uint64 cluster_id = 1;
  uint64 member_id = 2;
  int64 revision = 3;
  uint64 raft_term = 4;
}
  • Cluster_ID - Die ID des Clusters, der die Antwort erzeugt.
  • Member_ID - Die ID des Mitglieds, das die Antwort erzeugt.
  • Revision - Die Revision des Schlüssel-Wert-Speichers beim Erzeugen der Antwort.
  • Raft_Term - Der Raft-Term des Mitglieds beim Erzeugen der Antwort.

Eine Anwendung kann das Feld Cluster_ID oder Member_ID lesen, um sicherzustellen, dass sie mit dem beabsichtigten Cluster beziehungsweise Mitglied kommuniziert.

Anwendungen können über das Feld Revision die neueste Revision des Schlüssel-Wert-Speichers ermitteln. Das ist besonders nützlich, wenn sie eine historische Revision für eine Zeitreiseabfrage angeben und die zum Anfragezeitpunkt neueste Revision kennen möchten.

Mit Raft_Term können Anwendungen erkennen, wann der Cluster eine neue Leader-Wahl abgeschlossen hat.

Schlüssel-Wert-API

Die Schlüssel-Wert-API bearbeitet die in etcd gespeicherten Schlüssel-Wert-Paare. Der Großteil der Anfragen an etcd sind üblicherweise Schlüssel-Wert-Anfragen.

Grundoperationen des Systems

Schlüssel-Wert-Paar

Ein Schlüssel-Wert-Paar ist die kleinste Einheit, die die Schlüssel-Wert-API bearbeiten kann. Jedes Paar hat mehrere Felder, die im Protobuf-Format definiert sind:

message KeyValue {
  bytes key = 1;
  int64 create_revision = 2;
  int64 mod_revision = 3;
  int64 version = 4;
  bytes value = 5;
  int64 lease = 6;
}
  • Key - Schlüssel als Bytes. Ein leerer Schlüssel ist nicht zulässig.
  • Value - Wert als Bytes.
  • Version - Version ist die Version des Schlüssels. Eine Löschung setzt sie auf null zurück, und jede Änderung des Schlüssels erhöht sie.
  • Create_Revision - Revision der letzten Erstellung des Schlüssels.
  • Mod_Revision - Revision der letzten Änderung des Schlüssels.
  • Lease - Die ID der dem Schlüssel zugeordneten Lease. Ist lease 0, ist dem Schlüssel keine Lease zugeordnet.

Zusätzlich zu Schlüssel und Wert fügt etcd der Schlüsselnachricht Revisionsmetadaten hinzu. Diese Informationen ordnen Schlüssel nach Erstellungs- und Änderungszeitpunkt, was für die Nebenläufigkeitskontrolle bei verteilter Synchronisierung nützlich ist. Die verteilten gemeinsamen Sperren des etcd-Clients verwenden die Erstellungsrevision, um auf den Besitz einer Sperre zu warten. Entsprechend dient die Änderungsrevision dazu, Konflikte in der Lesemenge von Software Transactional Memory zu erkennen und auf Aktualisierungen bei der Leader-Wahl zu warten.

Revisionen

etcd verwaltet einen clusterweiten 64-Bit-Zähler, die Speicherrevision, der bei jeder Änderung des Schlüsselraums erhöht wird. Die Revision dient als globale logische Uhr und ordnet alle Speicheraktualisierungen sequenziell. Die durch eine neue Revision repräsentierte Änderung ist inkrementell: Einer Revision sind die Daten zugeordnet, die den Speicher geändert haben. Intern bedeutet eine neue Revision, die Änderungen mit der erhöhten Revision als Schlüssel in den B+-Baum des Backends zu schreiben.

Besonders nützlich sind Revisionen im Backend mit Mehrversions-Nebenläufigkeitskontrolle von etcd. Das MVCC-Modell erlaubt eine Sicht auf den Schlüssel-Wert-Speicher aus früheren Revisionen, da historische Schlüsselrevisionen erhalten bleiben. Clusteradministratoren können die Aufbewahrungsrichtlinie für diesen Verlauf zur gezielten Speicherverwaltung konfigurieren. Üblicherweise verwirft etcd alte Schlüsselrevisionen zeitgesteuert. Ein typischer etcd-Cluster bewahrt ersetzte Schlüsseldaten mehrere Stunden auf. Das ermöglicht auch die zuverlässige Behandlung langer Client-Verbindungsunterbrechungen statt nur kurzer Netzwerkausfälle: Watcher setzen einfach ab der letzten beobachteten historischen Revision fort. Ebenso können Leseanfragen mit einer Revision versehen werden, um Schlüssel aus der Sicht des Schlüsselraums zu dem Zeitpunkt zurückzugeben, an dem diese Revision bestätigt wurde.

Schlüsselbereiche

Das Datenmodell von etcd indiziert alle Schlüssel in einem flachen binären Schlüsselraum. Es unterscheidet sich damit von anderen Schlüssel-Wert-Speichern, die Schlüssel hierarchisch in Verzeichnissen organisieren. Schlüssel werden über Schlüsselintervalle [a, b) statt nach Verzeichnissen aufgelistet.

Diese Intervalle werden in etcd häufig als Bereiche bezeichnet. Bereichsoperationen sind leistungsfähiger als Verzeichnisoperationen. Wie ein hierarchischer Speicher unterstützen Intervalle einzelne Schlüsselabfragen über [a, a+1), etwa [‘a’, ‘a\x00’) zum Abrufen von ‘a’, und Verzeichnisabfragen durch Kodierung der Schlüssel nach Verzeichnistiefe. Darüber hinaus können Intervalle auch Präfixe abbilden: Das Intervall ['a', 'b') ruft beispielsweise alle Schlüssel mit dem Präfix ‘a’ ab.

Vereinbarungsgemäß werden Bereiche einer Anfrage durch die Felder key und range_end angegeben. Das Feld key ist der erste Schlüssel des Bereichs und sollte nicht leer sein. range_end ist der Schlüssel nach dem letzten Schlüssel des Bereichs. Ist range_end nicht angegeben oder leer, enthält der Bereich nur das Argument key. Ist range_end gleich key plus eins, etwa “aa”+1 == “ab” oder “a\xff”+1 == “b”, umfasst der Bereich alle Schlüssel mit dem Präfix key. Sind key und range_end beide ‘\0’, umfasst der Bereich alle Schlüssel. Ist range_end ‘\0’, umfasst der Bereich alle Schlüssel größer oder gleich dem Argument key.

Range

Schlüssel werden mit dem API-Aufruf Range aus dem Schlüssel-Wert-Speicher abgerufen. Er nimmt eine RangeRequest entgegen:

message RangeRequest {
  enum SortOrder {
	NONE = 0; // default, no sorting
	ASCEND = 1; // lowest target value first
	DESCEND = 2; // highest target value first
  }
  enum SortTarget {
	KEY = 0;
	VERSION = 1;
	CREATE = 2;
	MOD = 3;
	VALUE = 4;
  }

  bytes key = 1;
  bytes range_end = 2;
  int64 limit = 3;
  int64 revision = 4;
  SortOrder sort_order = 5;
  SortTarget sort_target = 6;
  bool serializable = 7;
  bool keys_only = 8;
  bool count_only = 9;
  int64 min_mod_revision = 10;
  int64 max_mod_revision = 11;
  int64 min_create_revision = 12;
  int64 max_create_revision = 13;
}
  • Key, Range_End - Der abzurufende Schlüsselbereich.
  • Limit - Die maximale Anzahl der für die Anfrage zurückgegebenen Schlüssel. Der Wert 0 bedeutet keine Begrenzung.
  • Revision - Der für den Bereich zu verwendende Zeitpunkt im Schlüssel-Wert-Speicher. Ist revision kleiner oder gleich null, bezieht sich der Bereich auf den neuesten Stand. Wurde die Revision kompaktiert, wird ErrCompacted zurückgegeben.
  • Sort_Order - Die Reihenfolge für sortierte Anfragen.
  • Sort_Target - Das Schlüssel-Wert-Feld, nach dem sortiert wird.
  • Serializable - Legt fest, dass die Bereichsanfrage serialisierbare, auf das Mitglied beschränkte Lesezugriffe verwendet. Standardmäßig ist Range linearisierbar und spiegelt den aktuellen Konsens des Clusters wider. Für bessere Leistung und Verfügbarkeit auf Kosten möglicherweise veralteter Leseergebnisse wird eine serialisierbare Bereichsanfrage lokal bearbeitet, ohne einen Konsens mit anderen Clusterknoten herstellen zu müssen.
  • Keys_Only - Gibt nur die Schlüssel, nicht die Werte zurück.
  • Count_Only - Gibt nur die Anzahl der Schlüssel im Bereich zurück.
  • Min_Mod_Revision - Die Untergrenze für Änderungsrevisionen von Schlüsseln; kleinere Änderungsrevisionen werden herausgefiltert.
  • Max_Mod_Revision - Die Obergrenze für Änderungsrevisionen von Schlüsseln; größere Änderungsrevisionen werden herausgefiltert.
  • Min_Create_Revision - Die Untergrenze für Erstellungsrevisionen von Schlüsseln; kleinere Erstellungsrevisionen werden herausgefiltert.
  • Max_Create_Revision - Die Obergrenze für Erstellungsrevisionen von Schlüsseln; größere Erstellungsrevisionen werden herausgefiltert.

Der Client erhält vom Aufruf Range eine Nachricht RangeResponse:

message RangeResponse {
  ResponseHeader header = 1;
  repeated mvccpb.KeyValue kvs = 2;
  bool more = 3;
  int64 count = 4;
}
  • Kvs - Die Liste der Schlüssel-Wert-Paare, die der Bereichsanfrage entsprechen. Wenn Count_Only gesetzt ist, ist Kvs leer.
  • More - Gibt bei gesetztem limit an, ob im angeforderten Bereich weitere Schlüssel zurückzugeben sind.
  • Count - Die Gesamtzahl der Schlüssel, die die Bereichsanfrage erfüllen.

Für große Schlüsselbereiche, deren vollständige Antwort nicht zwischengespeichert werden soll, siehe RangeStream .

RangeStream

RangeStream liefert dieselbe Ergebnismenge wie Range, der Server teilt die Antwort jedoch in eine Folge von Blöcken auf und überträgt sie als Stream an den Client. So muss keine Seite große Bereiche vollständig im Arbeitsspeicher zwischenspeichern. RangeStream akzeptiert dieselbe RangeRequest wie Range.

Der Client erhält vom Aufruf RangeStream einen Stream von RangeStreamResponse-Nachrichten:

message RangeStreamResponse {
  RangeResponse range_response = 1;
}

Belegung der Felder über die Blöcke hinweg:

  • Kvs - Jeder Block enthält einen disjunkten Teil des Ergebnisses. Werden die kvs aller Blöcke in ihrer Empfangsreihenfolge aneinandergehängt, ergibt sich dieselbe Schlüsselmenge wie bei einem einzelnen Range-Aufruf.
  • Header, More, Count - Werden nur im letzten Block gesetzt, und nur wenn der Stream ohne Fehler endet. In früheren Blöcken behalten diese Felder ihre Nullwerte. Wird proto.Merge auf die range_response jedes Blocks angewendet, entsteht eine RangeResponse, die der Antwort von Range entspricht.

Endet der Stream mit einem Fehler, enthält kein Block einen gültigen Wert für header, more oder count.

Alle Blöcke des Streams werden anhand derselben Revision bereitgestellt. Ist Revision in der Anfrage nicht gesetzt, erfasst der Server beim Start des Streams die neueste bestätigte Revision und verwendet sie für den restlichen Stream weiter.

RangeStream unterstützt weder benutzerdefinierte Sortierreihenfolgen noch Revisionsfilter (min_mod_revision, max_mod_revision, min_create_revision, max_create_revision). Anfragen, die eines davon verwenden, liefern Unimplemented. Auch der gRPC-Proxy von etcd unterstützt RangeStream nicht.

Es gibt zwei übliche Arten, einen RangeStream zu verarbeiten:

  1. Jeden Block unabhängig verarbeiten. Geeignet für Szenarien mit hohen Leistungsanforderungen, in denen der Client Schlüssel beim Eintreffen dekodieren und verarbeiten möchte, statt zuerst das gesamte Ergebnis zu sammeln. Der Client durchläuft die Blöcke und verarbeitet die jeweiligen kvs. Nachdem der Stream fehlerfrei beendet wurde, liest er header, more oder count aus dem letzten Block.
  2. Eine einzelne Antwort zusammensetzen. Geeignet, wenn der Client ein Ergebnis wie bei einem unären Range-Aufruf benötigt. Er führt die range_response jedes Blocks zu einer RangeResponse zusammen, etwa mit proto.Merge. Das Ergebnis enthält die vollständigen kvs sowie header, more und count aus dem letzten Block. Der Go-Client bietet dafür die Hilfsfunktion clientv3.GetStreamToGetResponse.

Put

Schlüssel werden mit dem Aufruf Put im Schlüssel-Wert-Speicher gespeichert. Er nimmt eine PutRequest entgegen:

message PutRequest {
  bytes key = 1;
  bytes value = 2;
  int64 lease = 3;
  bool prev_kv = 4;
  bool ignore_value = 5;
  bool ignore_lease = 6;
}
  • Key - Der Name des Schlüssels, der in den Schlüssel-Wert-Speicher geschrieben werden soll.
  • Value - Der Wert als Bytes, der dem Schlüssel im Schlüssel-Wert-Speicher zugeordnet werden soll.
  • Lease - Die Lease-ID, die dem Schlüssel im Schlüssel-Wert-Speicher zugeordnet werden soll. Der Lease-Wert 0 bedeutet keine Lease.
  • Prev_Kv - Wenn gesetzt, enthält die Antwort die Daten des Schlüssel-Wert-Paars vor der Aktualisierung durch diese Put-Anfrage.
  • Ignore_Value - Wenn gesetzt, wird der Schlüssel aktualisiert, ohne seinen aktuellen Wert zu ändern. Existiert der Schlüssel nicht, wird ein Fehler zurückgegeben.
  • Ignore_Lease - Wenn gesetzt, wird der Schlüssel aktualisiert, ohne seine aktuelle Lease zu ändern. Existiert der Schlüssel nicht, wird ein Fehler zurückgegeben.

Der Client erhält vom Aufruf Put eine Nachricht PutResponse:

message PutResponse {
  ResponseHeader header = 1;
  mvccpb.KeyValue prev_kv = 2;
}
  • Prev_Kv - Das durch Put überschriebene Schlüssel-Wert-Paar, wenn Prev_Kv in der PutRequest gesetzt war.

Schlüsselbereich löschen

Schlüsselbereiche werden mit dem Aufruf DeleteRange gelöscht. Er nimmt eine DeleteRangeRequest entgegen:

message DeleteRangeRequest {
  bytes key = 1;
  bytes range_end = 2;
  bool prev_kv = 3;
}
  • Key, Range_End - Der zu löschende Schlüsselbereich.
  • Prev_Kv - Wenn gesetzt, wird der Inhalt der gelöschten Schlüssel-Wert-Paare zurückgegeben.

Der Client erhält vom Aufruf DeleteRange eine Nachricht DeleteRangeResponse:

message DeleteRangeResponse {
  ResponseHeader header = 1;
  int64 deleted = 2;
  repeated mvccpb.KeyValue prev_kvs = 3;
}
  • Deleted - Anzahl der gelöschten Schlüssel.
  • Prev_Kv - Eine Liste aller durch die Operation DeleteRange gelöschten Schlüssel-Wert-Paare.

Transaktion

Eine Transaktion ist ein atomares If/Then/Else-Konstrukt über dem Schlüssel-Wert-Speicher. Sie bietet eine Grundoperation, die Anfragen zu atomaren Blöcken, also then/else, zusammenfasst und deren Ausführung anhand des Speicherinhalts bedingt, also if. Transaktionen können Schlüssel vor unbeabsichtigten gleichzeitigen Aktualisierungen schützen, Compare-and-swap-Operationen bilden und als Grundlage einer übergeordneten Nebenläufigkeitskontrolle dienen.

Eine Transaktion kann mehrere Anfragen atomar in einer einzigen Anfrage verarbeiten. Bei Änderungen des Schlüssel-Wert-Speichers bedeutet dies, dass die Speicherrevision für die Transaktion nur einmal erhöht wird und alle erzeugten Ereignisse dieselbe Revision haben. Derselbe Schlüssel darf innerhalb einer einzelnen Transaktion jedoch nicht mehrfach geändert werden.

Alle Transaktionen werden durch eine Konjunktion von Vergleichen abgesichert, ähnlich einer If-Anweisung. Jeder Vergleich prüft einen einzelnen Schlüssel im Speicher. Er kann prüfen, ob ein Wert fehlt oder vorhanden ist, ihn mit einem angegebenen Wert vergleichen oder die Revision beziehungsweise Version eines Schlüssels prüfen. Zwei verschiedene Vergleiche können denselben oder unterschiedliche Schlüssel betreffen. Alle Vergleiche werden atomar angewendet. Sind alle wahr, gilt die Transaktion als erfolgreich, und etcd wendet ihren then- beziehungsweise success-Anfrageblock an. Andernfalls gilt sie als fehlgeschlagen, und der else- beziehungsweise failure-Anfrageblock wird angewendet.

Jeder Vergleich wird als Nachricht Compare kodiert:

message Compare {
  enum CompareResult {
    EQUAL = 0;
    GREATER = 1;
    LESS = 2;
    NOT_EQUAL = 3;
  }
  enum CompareTarget {
    VERSION = 0;
    CREATE = 1;
    MOD = 2;
    VALUE= 3;
  }
  CompareResult result = 1;
  // target is the key-value field to inspect for the comparison.
  CompareTarget target = 2;
  // key is the subject key for the comparison operation.
  bytes key = 3;
  oneof target_union {
    int64 version = 4;
    int64 create_revision = 5;
    int64 mod_revision = 6;
    bytes value = 7;
  }
}
  • Result - Die Art der logischen Vergleichsoperation, etwa gleich oder kleiner als.
  • Target - Das zu vergleichende Schlüssel-Wert-Feld: Version, Erstellungsrevision, Änderungsrevision oder Wert des Schlüssels.
  • Key - Der Schlüssel für den Vergleich.
  • Target_Union - Die vom Benutzer angegebenen Daten für den Vergleich.

Nach dem Verarbeiten des Vergleichsblocks wendet die Transaktion einen Anfrageblock an. Ein Block ist eine Liste von RequestOp-Nachrichten:

message RequestOp {
  // request is a union of request types accepted by a transaction.
  oneof request {
    RangeRequest request_range = 1;
    PutRequest request_put = 2;
    DeleteRangeRequest request_delete_range = 3;
  }
}
  • Request_Range - Eine RangeRequest.
  • Request_Put - Eine PutRequest. Die Schlüssel müssen eindeutig sein. Sie darf keine Schlüssel mit anderen Put- oder Delete-Anfragen gemeinsam haben.
  • Request_Delete_Range - Eine DeleteRangeRequest. Sie darf keine Schlüssel mit Put- oder Delete-Anfragen gemeinsam haben.

Zusammengesetzt wird eine Transaktion mit dem API-Aufruf Txn ausgeführt, der eine TxnRequest entgegennimmt:

message TxnRequest {
  repeated Compare compare = 1;
  repeated RequestOp success = 2;
  repeated RequestOp failure = 3;
}
  • Compare - Eine Liste von Prädikaten, die eine Konjunktion von Bedingungen zur Absicherung der Transaktion repräsentiert.
  • Success - Eine Liste von Anfragen, die verarbeitet werden, wenn alle Vergleiche true ergeben.
  • Failure - Eine Liste von Anfragen, die verarbeitet werden, wenn ein Vergleich false ergibt.

Der Client erhält vom Aufruf Txn eine Nachricht TxnResponse:

message TxnResponse {
  ResponseHeader header = 1;
  bool succeeded = 2;
  repeated ResponseOp responses = 3;
}
  • Succeeded - Gibt an, ob Compare true oder false ergab.
  • Responses - Eine Liste von Antworten auf die Ergebnisse der Anwendung des Blocks Success, wenn succeeded true ist, beziehungsweise Failure, wenn succeeded false ist.

Die Liste Responses entspricht den Ergebnissen der angewendeten Liste RequestOp. Jede Antwort ist als ResponseOp kodiert:

message ResponseOp {
  oneof response {
    RangeResponse response_range = 1;
    PutResponse response_put = 2;
    DeleteRangeResponse response_delete_range = 3;
  }
}

Der in jeder inneren Antwort enthaltene ResponseHeader sollte in keiner Weise ausgewertet werden. Wenn Clients die neueste Revision benötigen, sollten sie immer den obersten ResponseHeader in TxnResponse prüfen.

Watch-API

Die API Watch bietet eine ereignisbasierte Schnittstelle zur asynchronen Überwachung von Schlüsseländerungen. Ein etcd-Watch wartet auf Änderungen, indem er ab einer angegebenen aktuellen oder historischen Revision fortlaufend beobachtet und Schlüsselaktualisierungen an den Client zurückstreamt.

Ereignisse

Jede Änderung an jedem Schlüssel wird durch Event-Nachrichten dargestellt. Eine Event-Nachricht enthält sowohl die Daten als auch die Art der Aktualisierung:

message Event {
  enum EventType {
    PUT = 0;
    DELETE = 1;
  }
  EventType type = 1;
  KeyValue kv = 2;
  KeyValue prev_kv = 3;
}
  • Type - Die Art des Ereignisses. PUT zeigt an, dass neue Daten unter dem Schlüssel gespeichert wurden. DELETE zeigt an, dass der Schlüssel gelöscht wurde.
  • KV - Der dem Ereignis zugeordnete KeyValue. Ein PUT-Ereignis enthält das aktuelle kv-Paar. Ein PUT-Ereignis mit kv.Version=1 zeigt die Erstellung eines Schlüssels an. Ein DELETE-Ereignis enthält den gelöschten Schlüssel, dessen Änderungsrevision auf die Revision der Löschung gesetzt ist.
  • Prev_KV - Das Schlüssel-Wert-Paar aus der Revision unmittelbar vor dem Ereignis. Um Bandbreite zu sparen, wird es nur gesetzt, wenn der Watch dies ausdrücklich aktiviert hat.

Watch-Streams

Watches sind lang laufende Anfragen und übertragen Ereignisdaten über gRPC-Streams. Ein Watch-Stream ist bidirektional: Der Client schreibt in den Stream, um Watches einzurichten, und liest daraus, um Watch-Ereignisse zu empfangen. Ein einzelner Stream kann viele verschiedene Watches multiplexen, indem er Ereignisse mit einer Kennung pro Watch versieht. Dieses Multiplexing verringert den Speicherbedarf und den Verbindungsaufwand im eigentlichen etcd-Cluster.

Garantien für Watch-Ereignisse sind unter Garantien der etcd-API beschrieben.

Ein Client erstellt einen Watch, indem er eine WatchCreateRequest über einen von Watch zurückgegebenen Stream sendet:

message WatchCreateRequest {
  bytes key = 1;
  bytes range_end = 2;
  int64 start_revision = 3;
  bool progress_notify = 4;

  enum FilterType {
    NOPUT = 0;
    NODELETE = 1;
  }
  repeated FilterType filters = 5;
  bool prev_kv = 6;
}
  • Key, Range_End - Der zu beobachtende Schlüsselbereich.
  • Start_Revision - Eine optionale Revision, ab der einschließlich beobachtet wird. Ist sie nicht angegeben, werden Ereignisse nach der Revision im Antwortheader der Watch-Erstellung gestreamt. Der gesamte verfügbare Ereignisverlauf kann ab der letzten Kompaktierungsrevision beobachtet werden.
  • Progress_Notify - Wenn gesetzt und keine aktuellen Ereignisse vorliegen, erhält der Watch regelmäßig eine WatchResponse ohne Ereignisse. Das ist nützlich, wenn Clients einen unterbrochenen Watcher ab einer kürzlich bekannt gewordenen Revision wiederherstellen möchten. Der etcd-Server entscheidet anhand der aktuellen Serverlast, wie häufig er Benachrichtigungen sendet.
  • Filters - Eine Liste von Ereignistypen, die serverseitig herausgefiltert werden sollen.
  • Prev_Kv - Wenn gesetzt, erhält der Watch die Schlüssel-Wert-Daten vor dem Ereignis. So lässt sich erkennen, welche Daten überschrieben wurden.

Als Antwort auf eine WatchCreateRequest oder bei einem neuen Ereignis für einen eingerichteten Watch erhält der Client eine WatchResponse:

message WatchResponse {
  ResponseHeader header = 1;
  int64 watch_id = 2;
  bool created = 3;
  bool canceled = 4;
  int64 compact_revision = 5;

  repeated mvccpb.Event events = 11;
}
  • Watch_ID - Die ID des Watches, zu dem die Antwort gehört.
  • Created - Wird auf true gesetzt, wenn die Antwort zu einer Anfrage zum Erstellen eines Watches gehört. Der Client sollte die ID speichern und die Ereignisse für den Watch im Stream erwarten. Alle an den erstellten Watcher gesendeten Ereignisse haben dieselbe watch_id.
  • Canceled - Wird auf true gesetzt, wenn die Antwort zu einer Anfrage zum Abbrechen eines Watches gehört. An den abgebrochenen Watcher werden keine weiteren Ereignisse gesendet.
  • Compact_Revision - Wird auf die kleinste in etcd verfügbare historische Revision gesetzt, wenn ein Watcher versucht, eine kompaktierte Revision zu beobachten. Dies geschieht beim Erstellen eines Watchers an einer kompaktierten Revision oder wenn er den Fortschritt des Schlüssel-Wert-Speichers nicht einholen kann. Der Watcher wird abgebrochen; das Erstellen neuer Watches mit derselben start_revision schlägt fehl.
  • Events - Eine geordnete Liste neuer Ereignisse für die angegebene Watch-ID.

Wenn der Client keine weiteren Ereignisse für einen Watch empfangen möchte, sendet er eine WatchCancelRequest:

message WatchCancelRequest {
   int64 watch_id = 1;
}
  • Watch_ID - Die ID des abzubrechenden Watches, damit keine weiteren Ereignisse übertragen werden.

Lease-API

Leases sind ein Mechanismus zur Erkennung, ob Clients noch aktiv sind. Der Cluster gewährt Leases mit einer Lebensdauer. Eine Lease läuft ab, wenn der etcd-Cluster innerhalb eines angegebenen TTL-Zeitraums kein keepAlive empfängt.

Um Leases mit dem Schlüssel-Wert-Speicher zu verknüpfen, kann jeder Schlüssel höchstens einer Lease zugeordnet werden. Läuft eine Lease ab oder wird sie widerrufen, werden alle ihr zugeordneten Schlüssel gelöscht. Jeder abgelaufene Schlüssel erzeugt ein Löschereignis im Ereignisverlauf.

Leases erhalten

Leases werden mit dem API-Aufruf LeaseGrant angefordert. Er nimmt eine LeaseGrantRequest entgegen:

message LeaseGrantRequest {
  int64 TTL = 1;
  int64 ID = 2;
}
  • TTL - Die als Richtwert angegebene Lebensdauer in Sekunden.
  • ID - Die angeforderte ID der Lease. Ist ID auf 0 gesetzt, wählt etcd eine ID aus.

Der Client erhält vom Aufruf LeaseGrant eine LeaseGrantResponse:

message LeaseGrantResponse {
  ResponseHeader header = 1;
  int64 ID = 2;
  int64 TTL = 3;
}
  • ID - Die Lease-ID der gewährten Lease.
  • TTL - Die vom Server gewählte Lebensdauer der Lease in Sekunden.
message LeaseRevokeRequest {
  int64 ID = 1;
}
  • ID - Die Lease-ID der zu widerrufenden Lease. Beim Widerruf werden alle zugeordneten Schlüssel gelöscht.

Keepalives

Leases werden über einen bidirektionalen Stream erneuert, der mit dem API-Aufruf LeaseKeepAlive erstellt wird. Möchte der Client eine Lease erneuern, sendet er eine LeaseKeepAliveRequest über den Stream:

message LeaseKeepAliveRequest {
  int64 ID = 1;
}
  • ID - Die Lease-ID der am Leben zu haltenden Lease.

Der Keepalive-Stream antwortet mit einer LeaseKeepAliveResponse:

message LeaseKeepAliveResponse {
  ResponseHeader header = 1;
  int64 ID = 2;
  int64 TTL = 3;
}
  • ID - Die Lease, die mit einer neuen TTL erneuert wurde.
  • TTL - Die neue verbleibende Lebensdauer der Lease in Sekunden.

6 - Dateien der persistenten Speicherung in etcd

Referenz der persistenten Speicherformate und Dateien

Dieses Dokument erläutert das Format der persistenten Speicherung in etcd: Benennung, Inhalt und Werkzeuge, mit denen Entwickler die Dateien untersuchen können. Künftig sollte es um Änderungen am Speichermodell erweitert werden. Es richtet sich an etcd-Entwickler und soll sie bei der Datenwiederherstellung unterstützen.

Voraussetzungen

Die folgenden Artikel liefern hilfreiche Hintergrundinformationen zu diesem Dokument:

Überblick

Langlebige Dateien

Dateiname Zweck
./member/snap/db
bbolt B+-Baum, der alle angewendeten Daten, Informationen zur Mitgliedschaft und Autorisierung sowie Metadaten speichert. Er kennt den zuletzt angewendeten WAL-Index ("consistent_index").
./member/snap/0000000000000002-0000000000049425.snap
./member/snap/0000000000000002-0000000000061ace.snap

Regelmäßige Snapshots des alten v2-Speichers, mit:

  • grundlegenden Mitgliedschaftsinformationen
  • etcd-Version

Seit etcd v3 ist dieser Inhalt gegenüber den /snap/db-Dateien redundant.

Regelmäßig (30s) werden diese Dateien bereinigt, wobei die letzten --max-snapshots=5 erhalten bleiben.

/member/snap/000000000007a178.snap.db

Ein vollständiger heruntergeladener bbolt-Snapshot vom etcd-Leader, falls das Replikat zu weit zurücklag.

Enthält dieselbe Art von Daten wie die Datei (./member/snap/db).

Die Datei wird in 2 Fällen verwendet:

  • Als Antwort auf die Aufforderung des Leaders zur Wiederherstellung aus dem Snapshot.
  • Beim Serverstart, wenn der letzte Snapshot, eine .snap.db-Datei, gefunden wird und einen neueren Index als consistent_index in der aktuellen Datei snap.db hat.
Hinweis: Regelmäßige Snapshots auf den einzelnen Replikaten werden nur als *.snap-Datei erzeugt, nicht als snap.db-Datei. Daher ist nicht garantiert, dass der neueste Snapshot im WAL eine *.snap.db-Datei besitzt. In diesem Fall wird jedoch erwartet, dass das Backend snap/db neuer ist als der Snapshot.

Die Datei wird nach Abschluss der Wiederherstellung nicht gelöscht (der gesamte Inhalt wurde dann in ./member/snap/db übernommen). Regelmäßig (30s) werden die Dateien bereinigt. Auch hier bleiben --max-snapshots=5 erhalten. Da diese Dateien O(GBs) groß sein können, besteht die Gefahr, dass der Datenträgerspeicher ausgeht.

./member/wal/000000000000000f-00000000000b38c7.wal
./member/wal/000000000000000e-00000000000a7fe3.wal
./member/wal/000000000000000d-000000000009c70c.wal

Write-Ahead-Logs von Raft, mit aktuellen von Raft akzeptierten Transaktionen, regelmäßigen Snapshots oder CRC-Einträgen.

Die neuesten --max-wals=5 Dateien bleiben erhalten. Jede dieser Dateien ist ~64*10^6 Bytes groß. Bei Überschreiten dieser fest codierten Größe wird eine neue Datei begonnen. Die Dateien können deshalb etwas größer werden; die vorab zugewiesene Datei 0.tmp schützt daher nicht vollständig vor einer Überschreitung des verfügbaren Datenträgerspeichers.

Bei zu seltenen Snapshots können mehr als --max-wals=5 Dateien vorhanden sein, da Sperren auf Dateisystemebene sie vor vorzeitiger Löschung schützen.

./member/wal/0.tmp (or .../1.tmp)
Vorab zugewiesener Speicherplatz für die nächste Write-Ahead-Log-Datei. Verhindert, dass Raft wegen fehlender WAL-Kapazität blockiert, ohne einen Alarm auslösen zu können.

Temporäre Dateien

Während der internen Verarbeitung von etcd können mehrere kurzlebige Dateien auftreten:

Datei Zweck
./member/snap/0000000000000002-000000000007a178.snap.broken

Snapshot-Dateien werden in „broken“ umbenannt, wenn sie nicht geladen werden können.

Der Versuch, die neueste Datei zu laden, erfolgt, wenn etcd gestartet wird.

Oder während der backup/migrate-Befehle von etcdctl.

./member/snap/tmp071677638 (random suffix)

Temporäre bbolt-Datei, die auf Replikaten als Antwort auf die msgSnap-Anfrage des Leaders erstellt wird, also auf dessen Aufforderung, den Speicher aus dem angegebenen Snapshot wiederherzustellen.

Nach erfolgreichem vollständigem Abruf des Inhalts wird die Datei umbenannt in: /member/snap/[SNAPSHOT-INDEX].snap.db. Stürzt der Server während des Herunterladens ab oder wird er beendet, bleiben die Dateien auf dem Datenträger und werden niemals automatisch bereinigt. Sie können erheblichen Umfang haben (GBs).

Siehe etcd/issues/12837. In etcd 3.5 behoben.

/member/snap/db.tmp.071677638 (random suffix)

Eine temporäre Datei, die während der Defragmentierung eine Kopie des Backend-Inhalts (/member/snap/db) enthält. Nach erfolgreichem Abschluss wird sie in /member/snap/db umbenannt und ersetzt das ursprüngliche Backend.

Beim Start des etcd-Servers werden diese Dateien bereinigt.

bbolt-B+-Baum: member/snap/db

Diese Datei enthält die Hauptdaten von etcd, angewendet bis zu einer bestimmten Position im Raft-Log (siehe consistent_index ).

Physischer Aufbau

Der Speicher von „better bolt“ ist physisch als B+-Baum organisiert. Die physischen Seiten des B-Baums werden niemals an Ort und Stelle geändert1. Stattdessen wird ihr Inhalt auf eine neue Seite kopiert, die aus der Liste freier Seiten stammt. Die alte Seite wird dieser Liste hinzugefügt, sobald keine offene Transaktion mehr auf sie zugreifen könnte. Dadurch sieht eine offene RO-Transaktion einen konsistenten historischen Speicherzustand. Die RW-Transaktion ist exklusiv und blockiert alle anderen RW-Transaktionen.
Große Werte werden auf mehreren zusammenhängenden Seiten gespeichert. Die Wiederverwendung von Seiten zusammen mit der nötigen Zuweisung zusammenhängender Seitenbereiche unterschiedlicher Größe kann zu einer zunehmenden Fragmentierung des bbolt-Speichers führen.

Die bbolt-Datei schrumpft niemals von selbst. Nur bei der Defragmentierung kann sie in eine neue Datei mit einem Puffer freier Seiten am Ende und verkleinerter Dateigröße umgeschrieben werden.

Logischer Aufbau

Der bbolt-Speicher ist in Buckets unterteilt. In jedem Bucket werden Schlüssel (Paare vom Typ byte[]->value byte[]) in lexikografischer Reihenfolge gespeichert. Die folgende Liste zeigt die von etcd verwendeten Buckets mit Stand Version 3.5 und ihre Schlüssel.

Bucket Schlüssel Beispielwert Beschreibung
alarm rpcpb.Alarm: {MemberID, Alarm: NONE|NOSPACE|CORRUPT} nil Zeigt an, dass bei einem Mitglied Probleme erkannt wurden.
auth "authRevision" "" (leer) oder BigEndian.PutUint64

Jede Änderung an Rollen oder Benutzern erhöht dieses Feld beim Bestätigen der Transaktion.

Der Wert dient ausschließlich zur optimistischen Sperrung während des Autorisierungsvorgangs.

authRoles [roleName] als Zeichenfolge authpb.Role serialisiert
authUsers [userName] als Zeichenfolge authpb.User serialisiert
cluster "clusterVersion" "3.5.0" (string) Minor-Version der durch Konsens vereinbarten gemeinsamen Speicherversion.
"downgrade" JSON:
{
  "target-version": "3.4.0"
  "enabled": true/false
}

Speichert die Absicht dauerhaft, die durch die neueste Anfrage Downgrade RPC konfiguriert wurde.

Seit v3.5

key

[revisionId], kodiert mit bytesToRev{main,sub}

Löschungen von Schlüssel-Wert-Paaren werden mit einem abschließenden 't' als „Tombstone“ serialisiert

mvccpb.KeyValue als serialisiertes Proto (key, create_rev, mod_rev, version, value, lease id)
lease leasepb.Lease als serialisiertes Proto (ID, TTL, RemainingTTL)

Hinweis: LeaseCheckpoint verlängert nur RemainingTTL. TTL stammt weiterhin vom ursprünglichen Grant.

Hinweis2: TTLs werden in Sekunden ab einem nicht definierten „jetzt“ gespeichert. Ein ständig abstürzender und neu startender Server gibt keine Leases frei!!!

members [memberId] hexadezimal als Zeichenfolge: "8e9e05c52164694d" Als JSON-Zeichenfolge serialisierte Member-Struktur:
{
  "id":10276657743932975437,
  "peerURLs":[
  "http://localhost:2380"],
  "name":"default",
  "clientURLs": ["http://localhost:2379"]
}
Durch Konsens vereinbarte Informationen zur Clustermitgliedschaft.
members_removed [memberId] hexadezimal als Zeichenfolge: "8e9e05c52164694d" []byte("removed")

IDs aller entfernten Mitglieder. Damit wird geprüft, dass ein entferntes Mitglied niemals unter derselben ID erneut hinzugefügt wird.

Das Feld wird derzeit (3.4) aus dem V2-Speicher und niemals aus V3 gelesen. Siehe https://github.com/etcd-io/etcd/pull/12820

meta "consistent_index" uint64-Bytes (BigEndian) Repräsentiert den Offset des letzten auf den bolt-DB-Speicher angewendeten WAL-Eintrags.
"scheduledCompactRev" bytesToRev{main,sub} kodiert. (16 Bytes) Dient zur erneuten Initialisierung der Kompaktierung, wenn nach einer Kompaktierungsanfrage ein Absturz aufgetreten ist.
"finishedCompactRev" bytesToRev{main,sub} kodiert. (16 Bytes) Revision, bis zu der der Speicher zuletzt erfolgreich kompaktiert wurde (https://github.com/etcd-io/etcd/blob/ae7862e8bc8007eb396099db4e0e04ac026c8df5/server/mvcc/kvstore_compaction.go#L54)
"confState" Seit etcd 3.5
"term" Seit etcd 3.5
"storage-version"

Werkzeuge

bbolt

bbolt bietet ein Befehlszeilenwerkzeug zum Untersuchen des Dateiinhalts.

Anwendungsbeispiele:

Alle Buckets einer bbolt-Datei auflisten:
% go run go.etcd.io/bbolt/cmd/bbolt buckets ./default.etcd/member/snap/db
Ein bestimmtes Schlüssel-Wert-Paar lesen:
% go run go.etcd.io/bbolt/cmd/bbolt get ./default.etcd/member/snap/db cluster clusterVersion

etcd-dump-db

etcd-dump-db kann den Inhalt des etcd-v3-Backends (bbolt) auflisten.

% go run go.etcd.io/etcd/v3/tools/etcd-dump-db  list-bucket default.etcd
alarm
auth
...

Weitere Beispiele: https://github.com/etcd-io/etcd/tree/master/tools/etcd-dump-db

WAL: Write-Ahead-Log

Das Write-Ahead-Log ist ein persistenter Raft-Speicher für Vorschläge. Zuerst speichert der Leader den Vorschlag in seinem Log und repliziert ihn anschließend gleichzeitig über das Raft-Protokoll an die Follower. Jeder Follower speichert den Vorschlag dauerhaft in seinem WAL, bevor er dem Leader die Replikation bestätigt.

Das in etcd verwendete WAL unterscheidet sich in 2 Punkten vom kanonischen Raft-Modell:

  • Es speichert nicht nur indizierte Einträge dauerhaft, sondern auch kleine Raft-Snapshots und HardState. So lässt sich der gesamte Raft-Zustand eines Mitglieds allein aus dem WAL wiederherstellen.
  • Es wird nur angehängt. Einträge werden nicht an Ort und Stelle überschrieben; ein später in der Datei angehängter Eintrag mit demselben Index ersetzt den vorherigen logisch.

Dateinamen

Die WAL-Dateien werden nach folgendem Muster benannt:

"%016x-%016x.wal", seq, index

Beispiel: ./member/wal/0000000000000010-00000000000bf1e6.wal

Die Dateinamen enthalten also hexadezimal kodiert:

  • Die fortlaufende Nummer der WAL-Datei
  • Den Index des ersten Eintrags oder Snapshots in der Datei. Insbesondere enthält die erste Datei „0000000000000000-0000000000000000.wal“ den anfänglichen Snapshot-Eintrag mit index=0.

Physischer Inhalt

Die WAL-Datei enthält eine Folge von „Frames “. Jeder Frame enthält:

  1. Eine in LittleEndian 2 kodierte uint64-Zahl mit der Länge des serialisierten walpb.Record (3).
  2. Auffüllung: Eine Anzahl von 0-Bytes, sodass die Größe des gesamten Frames ausgerichtet ist (mod 8)
  3. Serialisierte walpb.Record -Daten:
    1. type - Als int kodierte Aufzählung, die die Interpretation des folgenden data-Felds bestimmt
    2. data - Je nach type gewöhnlich ein serialisiertes Proto
    3. crc - CRC-32-Prüfsumme aller zusammengefassten „data“-Felder ohne type in allen Logeinträgen dieses Replikats seit Erstellung des WAL. CRC berücksichtigt ALLE Einträge, auch wenn sie nicht durch Raft bestätigt wurden.

Eine neue Datei beginnt, sobald die aktuelle Datei 64*10^6 Bytes überschreitet.

Logischer Inhalt

Auf logischer Ebene enthalten Write-Ahead-Log-Dateien:

  • Raftpb.Entry: Aktuelle vom Raft-Leader replizierte Vorschläge. Einige gelten als bestätigt, andere können noch logisch überschrieben werden.
  • Raftpb.HardState(term,commit,vote): Regelmäßige, sehr häufige Informationen über den Index eines bestätigten Logeintrags, der also auf die Mehrheit der Server repliziert wurde. Er wird garantiert nicht mehr geändert oder überschrieben und kann auf die Backends v2 und v3 angewendet werden. Enthalten sind außerdem ein „term“ als Hinweis auf wahlbezogene Änderungen sowie eine Stimme: das Mitglied, für das das aktuelle Replikat im aktuellen Term gestimmt hat.
  • walpb.Snapshot(term, index): Regelmäßige Snapshots des Raft-Zustands ohne DB-Inhalt, nur mit Snapshot-Logindex und Raft-Term
    • Der Inhalt des V2-Speichers liegt in separaten *.store-Dateien.
    • Der Inhalt des V3-Speichers wird in der bbolt-Datei verwaltet und bildet einen impliziten Snapshot, sobald die Einträge dort angewendet sind.
  • Ein crc32-Prüfsummeneintrag am Anfang jeder Datei, um die CRC-Prüfung für den Rest der Datei fortzusetzen.
  • etcdserverpb.Metadata(node_id, cluster_id) - Identifiziert den Cluster und das Replikat, für die das Log steht.

Jede WAL-Datei besteht in dieser Reihenfolge aus:

  1. CRC-32-Frame mit der fortlaufenden crc aller vorherigen Dateien, 0 für die erste Datei.

  2. Metadaten-Frame mit Cluster- und Replikat-IDs

  3. Nur für die erste WAL-Datei:

    • Leerer Snapshot-Frame (Index:0, Term: 0). Er stellt die Invariante sicher, dass allen Einträgen ein Snapshot vorausgeht.

    Für spätere WAL-Dateien (ab der 2.):

    • HardState-Frame.
  4. Mischung aus Entry-, HardState- und Snapshot-Einträgen

Das WAL kann mehrere Einträge mit demselben Index enthalten. Dies kann in den in Abbildung 7. der Raft-Veröffentlichung beschriebenen Fällen auftreten. Da an das etcd-WAL nur angehängt wird, werden Einträge durch Anhängen eines neuen Eintrags mit demselben Index überschrieben.

Insbesondere ersetzt die Leselogik alte Einträge durch neuere Einträge . Daher kann nur die jeweils letzte Version von Einträgen mit entry.index <= HardState.commit als endgültig gelten. Einträge mit index > HardState.commit können sich noch ändern.

Es wird erwartet, dass die Terms im WAL monoton sind.

Für die Indizes im WAL wird Folgendes erwartet:

  1. Sie beginnen bei einem Snapshot.
  2. Nach diesem Snapshot steigen sie fortlaufend, solange sie im selben Term bleiben.
  3. Bei einem Term-Wechsel kann der Index sinken, jedoch nur auf einen neuen Wert oberhalb des neuesten HardState.commit.
  4. Ein neuer Snapshot kann mit jedem Index >= HardState.commit auftreten und beginnt eine neue Indexfolge.
Dateien der persistenten Speicherung in etcd

Werkzeuge

etcd-dump-logs

WAL-Dateien von etcd können mit dem Werkzeug etcd-dump-logs gelesen werden:

% go install go.etcd.io/etcd/v3/tools/etcd-dump-logs@latest

% go run go.etcd.io/etcd/v3/tools/etcd-dump-logs --start-index=0 aname.etcd

Zu beachten:

  • Das Werkzeug zeigt nur Entries und nicht alle WAL-Einträge wie Snapshots und HardStates in den WAL-Dateien.
  • Es berücksichtigt automatisch das Überschreiben von Einträgen. Wurde ein Eintrag durch einen neueren mit demselben Index überschrieben, wird nur der endgültige Wert ausgegeben.
  • Es gibt auch unbestätigte Einträge vom Ende des LOG aus, ohne Informationen zu HardState.commitIndex. Daher ist nicht erkennbar, ob diese Einträge endgültig sind.

Snapshots des V2-Speichers: member/snap/{term}-{index}.snap

Dateinamen:

member/snap/{term}-{index}.snap

Die Dateinamen werden hier mit ("%016x-%016x.snap") erzeugt und verwenden 2 hexadezimal kodierte Bestandteile:

  • term -> Raft-Term, also der Zeitraum zwischen Wahlen, zum Zeitpunkt der Snapshot-Erstellung
  • index -> Index des zuletzt angewendeten Vorschlags zum Zeitpunkt der Snapshot-Erstellung

Erstellung

Die *.snap-Dateien werden von der Methode Snapshotter.SaveSnap erstellt.

Es gibt 2 Auslöser für die Erstellung dieser Dateien:

  • Ungefähr alle –snapshot-count=(standardmäßig 10,000 in v3.7.0) angewendeten Vorschläge wird eine neue Datei erstellt. Dies ist nur ein Näherungswert, da Vorschläge gebündelt eintreffen können, ein Snapshot erst am Ende eines solchen Pakets geprüft und der Snapshot-Vorgang schließlich asynchron eingeplant wird. Der Optionsname –snapshot-count ist recht irreführend, da er die Differenz der Indexwerte zwischen dem letzten Snapshot und dem zuletzt angewendeten Vorschlag steuert.
  • Raft fordert das Replikat auf, sich aus einem Snapshot wiederherzustellen. Während das Replikat den Snapshot über eine msgSnap-Nachricht empfängt, hält es ihn auch als kleinen Checkpoint im WAL fest. Damit ist garantiert, dass am Ende des WAL stets ein gültiger Snapshot gefolgt von Einträgen steht. Mögliche Lücken im WAL werden so vermieden.

Derzeit entsprechen die Dateien ungefähr3 im Verhältnis 1-1 den Snapshot-Einträgen im WAL. Mit der Stilllegung des v2-Speichers wird erwartet, dass diese Dateien überhaupt nicht mehr geschrieben werden, optional ab 3.5.x und verpflichtend ab 3.6.x.

Inhalt

Die Datei enthält das serialisierte snapdb.snapshot-Proto (uint32 crc, bytes data),

das im Feld ‘data’ einen Raftpb.Snapshot enthält:

(bytes data, SnapshotMetadata{index, term, conf } metadata),

Die verschachtelten Daten enthalten schließlich den JSON-serialisierten Inhalt des v2-Speichers .

Insbesondere enthalten sind:

  • Term
  • Index
  • Mitgliedschaftsdaten:
    • /0/members/8e9e05c52164694d/attributes -> {"name":"default","clientURLs":["http://localhost:2379"]}
    • /0/members/8e9e05c52164694d/RaftAttributes -> "{"peerURLs":["http://localhost:2380"]}"
  • Speicherversion: /0/version-> 3.5.0

Werkzeuge

protoc

Der folgende Befehl zeigt den Dateiinhalt an, wenn er im Stammverzeichnis von etcd ausgeführt wird:

cat default.etcd/member/snap/0000000000000002-0000000000049425.snap |
  protoc --decode=snappb.snapshot \
    server/etcdserver/api/snap/snappb/snap.proto \
    -I $(go list -f '{{.Dir}}' github.com/gogo/protobuf/proto)/.. \
    -I . \
    -I $(go list -m -f '{{.Dir}}' github.com/gogo/protobuf)/protobuf

Ebenso lässt sich das Feld ‘data’ extrahieren und als Raftpb.Snapshot dekodieren.

Beispiel für JSON-serialisierten v2-Speicherinhalt in *.snap-Dateien von etcd 3.4:

{
  "Root":{
    "Path":"/",
    "CreatedIndex":0,
    "ModifiedIndex":0,
    "ExpireTime":"0001-01-01T00:00:00Z",
    "Value":"",
    "Children":{
      "0":{
        "Path":"/0",
        "CreatedIndex":0,
        "ModifiedIndex":0,
        "ExpireTime":"0001-01-01T00:00:00Z",
        "Value":"",
        "Children":{
          "members":{
            "Path":"/0/members",
            "CreatedIndex":1,
            "ModifiedIndex":1,
            "ExpireTime":"0001-01-01T00:00:00Z",
            "Value":"",
            "Children":{
              "8e9e05c52164694d":{
                "Path":"/0/members/8e9e05c52164694d",
                "CreatedIndex":1,
                "ModifiedIndex":1,
                "ExpireTime":"0001-01-01T00:00:00Z",
                "Value":"",
                "Children":{
                  "attributes":{
                    "Path":"/0/members/8e9e05c52164694d/attributes",
                    "CreatedIndex":2,
                    "ModifiedIndex":2,
                    "ExpireTime":"0001-01-01T00:00:00Z",
                    "Value":"{\"name\":\"default\",\"clientURLs\":[\"http://localhost:2379\"]}",
                    "Children":null
                  },
                  "RaftAttributes":{
                    "Path":"/0/members/8e9e05c52164694d/RaftAttributes",
                    "CreatedIndex":1,
                    "ModifiedIndex":1,
                    "ExpireTime":"0001-01-01T00:00:00Z",
                    "Value":"{\"peerURLs\":[\"http://localhost:2380\"]}",
                    "Children":null
                  }
                }
              }
            }
          },
          "version":{
            "Path":"/0/version",
            "CreatedIndex":3,
            "ModifiedIndex":3,
            "ExpireTime":"0001-01-01T00:00:00Z",
            "Value":"3.5.0",
            "Children":null
          }
        }
      },
      "1":{
        "Path":"/1",
        "CreatedIndex":0,
        "ModifiedIndex":0,
        "ExpireTime":"0001-01-01T00:00:00Z",
        "Value":"",
        "Children":{


        }
      }
    }
  },
  "WatcherHub":{
    "EventHistory":{
      "Queue":{
        "Events":[
          {
            "action":"create",
            "node":{
              "key":"/0/members/8e9e05c52164694d/RaftAttributes",
              "value":"{\"peerURLs\":[\"http://localhost:2380\"]}",
              "modifiedIndex":1,
              "createdIndex":1
            }
          },
          {
            "action":"set",
            "node":{
              "key":"/0/members/8e9e05c52164694d/attributes",
              "value":"{\"name\":\"default\",\"clientURLs\":[\"http://localhost:2379\"]}",
              "modifiedIndex":2,
              "createdIndex":2
            }
          },
          {
            "action":"set",
            "node":{
              "key":"/0/version",
              "value":"3.5.0",
              "modifiedIndex":3,
              "createdIndex":3
            }
          }
        ]
      }
    }
  }
}

Änderungen

Dieser Abschnitt ist für die Beschreibung von Dateiformatänderungen zwischen etcd-Versionen vorgesehen.


  1. Die Metadatenseiten am Anfang der bbolt-Datei werden an Ort und Stelle geändert. ↩︎

  2. Inkonsistent, da die meisten uint-Werte in Big Endian geschrieben werden ↩︎

  3. Der anfängliche Snapshot mit index:0 am Anfang des WAL ist keiner *.snap-Datei zugeordnet. Zudem können alte *.snap-Dateien oder WAL-Dateien bereinigt werden. ↩︎

7 - Garantien der etcd-API

API-Garantien von etcd

etcd ist ein konsistenter und dauerhafter Schlüssel-Wert-Speicher. Der Zugriff erfolgt über gRPC-Dienste . etcd bietet die stärksten Konsistenz- und Dauerhaftigkeitsgarantien für ein verteiltes System. Diese Spezifikation führt die API-Garantien von etcd auf.

Betrachtete APIs

Die KV-API ermöglicht das direkte Lesen und Bearbeiten des Schlüssel-Wert-Speichers. Mit der Watch-API lassen sich Änderungen am Schlüssel-Wert-Speicher abonnieren. Mit der Lease-API lässt sich einem Schlüssel eine Lebensdauer zuweisen.

Die KV- und Watch-APIs erlauben den Zugriff nicht nur auf die neuesten Schlüsselversionen, sondern auch auf frühere Versionen innerhalb eines zusammenhängenden Verlaufsfensters, das durch eine Kompaktierungsoperation begrenzt wird.

Ein Aufruf der KV-API wirkt unmittelbar, während die Watch-API mit einer nicht begrenzten Verzögerung zurückkehrt. In einem korrekt arbeitenden etcd-Cluster sollten Watch-Ereignisse etwa 10ms nach ihrem Auftreten sichtbar werden. Es gibt jedoch keine Obergrenze; in gestörten Clustern kommen Ereignisse möglicherweise nie an.

KV-APIs

etcd gewährleistet Dauerhaftigkeit und strikte Serialisierbarkeit für alle KV-API-Aufrufe. Dies sind die stärksten Isolationsgarantien verteilter transaktionaler Datenbanksysteme.

Dauerhaftigkeit

Alle abgeschlossenen Operationen sind dauerhaft. Auch alle zugänglichen Daten sind dauerhaft gespeichert. Ein Lesezugriff gibt niemals Daten zurück, die noch nicht dauerhaft gespeichert wurden.

Strikte Serialisierbarkeit

Operationen des KV-Dienstes sind atomar und erfolgen in einer totalen Ordnung, die ihrer Echtzeitreihenfolge entspricht. Diese totale Ordnung ergibt sich aus der Revision . Weitere Informationen zur strikten Serialisierbarkeit .

Bei Transaktionen ohne verschachtelte TXNs entspricht die Ausführungsreihenfolge garantiert jener in der Operationsliste. Dadurch sind die GET-Antworten innerhalb der Transaktion stabil. Bei Transaktionen mit verschachtelten TXNs ist die Ausführungsreihenfolge nicht festgelegt.

Strikte Serialisierbarkeit impliziert weitere, schwächere Garantien, die möglicherweise leichter verständlich sind:

Atomarität

Alle API-Anfragen sind atomar: Eine Operation wird entweder vollständig oder gar nicht abgeschlossen. Bei Watch-Anfragen befinden sich alle von einer Operation erzeugten Ereignisse in einer Watch-Antwort. Ein Watch beobachtet niemals nur einen Teil der Ereignisse einer einzelnen Operation.

Linearisierbarkeit

Aus Sicht des Clients bietet Linearisierbarkeit nützliche Eigenschaften, die das Nachvollziehen des Verhaltens erleichtern. Die folgende klare Beschreibung stammt aus der Originalveröffentlichung : Linearisierbarkeit erweckt den Eindruck, dass jede von nebenläufigen Prozessen ausgeführte Operation zu einem Zeitpunkt zwischen ihrem Aufruf und ihrer Antwort augenblicklich wirksam wird.

Angenommen, ein Client schließt einen Schreibvorgang zum Zeitpunkt 1 (t1) ab. Ein Client, der zu t2 mit t2 > t1 einen Lesevorgang startet, sollte einen Wert erhalten, der mindestens so aktuell ist wie der zuvor zu t1 abgeschlossene Schreibvorgang. Der Lesevorgang wird möglicherweise aber erst zu t3 abgeschlossen. Linearisierbarkeit garantiert, dass er den neuesten Wert zu einem Zeitpunkt zwischen seinem Beginn zu t2 und seinem Abschluss zu t3 zurückgibt. Er berücksichtigt alle vor seinem Beginn abgeschlossenen Schreibvorgänge. Ein gleichzeitiger Schreibvorgang zwischen t2 und t3 kann berücksichtigt werden oder auch nicht; der zurückgegebene Wert muss beim Abschluss des Lesevorgangs nicht der neueste sein.

etcd gewährleistet standardmäßig Linearisierbarkeit für alle anderen Operationen. Sie hat jedoch ihren Preis, da linearisierte Anfragen den Raft-Konsensprozess durchlaufen müssen. Für geringere Latenzen und höheren Durchsatz bei Leseanfragen können Clients deren Konsistenzmodus auf serializable setzen. Dabei können gegenüber dem Quorum veraltete Daten gelesen werden, doch der Leistungsnachteil linearisierter Zugriffe durch ihre Abhängigkeit vom laufenden Konsens entfällt.

Watch-APIs

Watches bieten folgende Garantien für Ereignisse:

  • Geordnet - Ereignisse sind nach Revision geordnet. Ein Ereignis erscheint niemals in einem Watch, wenn es zeitlich vor einem bereits ausgegebenen Ereignis liegt. Bei Transaktionen ohne verschachtelte TXNs entspricht die Reihenfolge der erzeugten Ereignisse garantiert der Reihenfolge in der Operationsliste. Bei Transaktionen mit verschachtelten TXNs ist sie nicht festgelegt.
  • Eindeutig - Ein Ereignis erscheint niemals zweimal in einem Watch.
  • Zuverlässig - Innerhalb des verfügbaren Verlaufsfensters geht niemals eine Teilfolge einer Ereignisfolge verloren. Sind Ereignisse zeitlich als a < b < c geordnet und empfängt der Watch a und c, empfängt er garantiert auch b, sofern b im verfügbaren Verlaufsfenster liegt.
  • Atomar - Eine Ereignisliste umfasst garantiert vollständige Revisionen. Aktualisierungen mehrerer Schlüssel in derselben Revision werden nicht auf mehrere Ereignislisten aufgeteilt.
  • Fortsetzbar - Ein unterbrochener Watch kann durch einen neuen Watch fortgesetzt werden, der nach der letzten vor der Unterbrechung in einem Watch-Ereignis empfangenen Revision beginnt, sofern diese Revision noch im Verlaufsfenster liegt.
  • Mit Fortschrittsmarken - Fortschrittsbenachrichtigungen garantieren, dass alle Ereignisse bis zu einer Revision bereits ausgeliefert wurden.

etcd gewährleistet keine Linearisierbarkeit für Watch-Operationen. Von Benutzern wird erwartet, die Revision von Watch-Ereignissen zu prüfen, um ihre korrekte Reihenfolge gegenüber anderen Operationen sicherzustellen.

Lease-APIs

etcd bietet einen Lease-Mechanismus . Der wichtigste Anwendungsfall einer Lease ist die Implementierung verteilter Koordinationsmechanismen wie verteilter Sperren. Der Mechanismus selbst ist einfach: Eine Lease kann mit der Grant-API erstellt, mit der Put-API einem Schlüssel zugeordnet und mit der Revoke-API widerrufen werden. Sie läuft nach ihrer anhand der realen Uhrzeit gemessenen Lebensdauer (TTL) ab. Um korrekte verteilte Koordinationsmechanismen zu implementieren, müssen Benutzer jedoch die wesentlichen Eigenschaften der APIs und ihrer Verwendung kennen.

etcd-spezifische Definitionen

Abgeschlossene Operation

Eine etcd-Operation gilt als abgeschlossen, wenn sie durch Konsens bestätigt und damit von der Speicherengine von etcd „ausgeführt“, also dauerhaft gespeichert wurde. Der Client weiß, dass eine Operation abgeschlossen ist, sobald er eine Antwort vom etcd-Server erhält. Bei einer Zeitüberschreitung oder einer Netzwerkunterbrechung zwischen Client und etcd-Mitglied kann ihr Status für den Client jedoch ungewiss sein. etcd kann Operationen auch während einer Leader-Wahl abbrechen. In diesem Fall sendet etcd keine abort-Antworten auf ausstehende Client-Anfragen.

Revision

Einer etcd-Operation, die den Schlüssel-Wert-Speicher ändert, wird genau eine steigende Revision zugewiesen. Eine Transaktionsoperation kann den Speicher mehrfach ändern, erhält aber nur eine Revision. Das Revisionsattribut eines durch die Operation geänderten Schlüssel-Wert-Paars hat denselben Wert wie die Revision der Operation. Die Revision kann als logische Uhr des Schlüssel-Wert-Speichers dienen. Ein Paar mit einer größeren Revision wurde nach einem Paar mit einer kleineren Revision geändert. Zwei Schlüssel-Wert-Paare mit derselben Revision wurden durch eine Operation „gleichzeitig“ geändert.

8 - etcd im Vergleich mit anderen Schlüssel-Wert-Speichern

Geschichte und Verwendung von etcd im Vergleich mit anderen Werkzeugen

Der Name „etcd“ entstand aus zwei Ideen: dem Unix-Verzeichnis “/etc” und verteilten Systemen, auf Englisch „distributed systems“. Im Verzeichnis “/etc” liegen Konfigurationsdaten eines einzelnen Systems, während etcd Konfigurationsinformationen für große verteilte Systeme speichert. Aus „/etc“ und dem Anfangsbuchstaben von „distributed“ wird somit „etcd“.

etcd ist als allgemeine Grundlage für große verteilte Systeme ausgelegt. Diese Systeme tolerieren keinen Split-Brain-Betrieb und nehmen dafür Einschränkungen der Verfügbarkeit in Kauf. etcd speichert Metadaten konsistent und fehlertolerant. Ein etcd-Cluster soll einen Schlüssel-Wert-Speicher mit erstklassiger Stabilität, Zuverlässigkeit, Skalierbarkeit und Leistung bieten.

Verteilte Systeme verwenden etcd als konsistenten Schlüssel-Wert-Speicher für Konfigurationsverwaltung, Diensterkennung und die Koordination verteilter Arbeit. Viele Organisationen nutzen etcd für Produktivsysteme wie Container-Scheduler, Dienste zur Diensterkennung und verteilte Datenspeicherung. Häufige verteilte Entwurfsmuster mit etcd sind Leader-Wahlen , verteilte Sperren und die Überwachung der Erreichbarkeit von Rechnern.

Anwendungsfälle

  • Container Linux von CoreOS: Anwendungen auf Container Linux erhalten automatische Linux-Kernel-Updates ohne Ausfallzeit. Container Linux koordiniert Updates mit locksmith . Locksmith implementiert über etcd ein verteiltes Semaphor, das sicherstellt, dass zu jedem Zeitpunkt nur ein Teil des Clusters neu startet.
  • Kubernetes speichert Konfigurationsdaten für Diensterkennung und Clusterverwaltung in etcd. Die Konsistenz von etcd ist für die korrekte Einplanung und den Betrieb von Diensten entscheidend. Der Kubernetes-API-Server speichert den Clusterzustand dauerhaft in etcd. Er verwendet die Watch-API von etcd, um den Cluster zu überwachen und kritische Konfigurationsänderungen auszurollen.

Vergleichstabelle

Vielleicht erscheint etcd bereits passend. Wie bei jeder Technologieentscheidung ist jedoch Vorsicht geboten. Diese Dokumentation wurde vom etcd-Team verfasst. Obwohl ein unvoreingenommener Vergleich von Technik und Funktionen das Ziel ist, begünstigen Fachkenntnisse und Vorlieben der Autoren offensichtlich etcd. Nur nach Anweisung verwenden.

Die folgende Tabelle bietet einen schnellen Überblick über die Unterschiede zwischen etcd und seinen beliebtesten Alternativen. Weitere Erläuterungen und Einzelheiten zu jeder Spalte folgen in den Abschnitten nach der Tabelle.

etcd ZooKeeper Consul NewSQL (Cloud Spanner, CockroachDB, TiDB)
Grundoperationen für Nebenläufigkeit Lock-RPCs , Election-RPCs , Sperren per Befehlszeile , Wahlen per Befehlszeile , Rezepte in Go Externe Curator-Rezepte in Java Native Sperr-API Selten , wenn überhaupt
Linearisierbare Lesezugriffe Ja Nein Ja Manchmal
Mehrversions-Nebenläufigkeitskontrolle Ja Nein Nein Manchmal
Transaktionen Feldvergleiche, Lesen, Schreiben Versionsprüfungen, Schreiben Feldvergleich, Sperren, Lesen, Schreiben Im SQL-Stil
Änderungsbenachrichtigungen Historische und aktuelle Schlüsselintervalle Aktuelle Schlüssel und Verzeichnisse Aktuelle Schlüssel und Präfixe Trigger (manchmal)
Benutzerberechtigungen Rollenbasiert ACLs ACLs Unterschiedlich (GRANT pro Tabelle, Rollen pro Datenbank)
API für HTTP/JSON Ja Nein Ja Selten
Neukonfiguration der Mitgliedschaft Ja >3.5.0 Ja Ja
Maximal zuverlässig nutzbare Datenbankgröße Mehrere Gigabyte Hunderte Megabyte (manchmal mehrere Gigabyte) Hunderte MB Terabyte und mehr
Minimale Latenz zur Linearisierung von Lesezugriffen Netzwerk-RTT Keine Linearisierung von Lesezugriffen RTT + fsync Uhrzeitbarrieren (Atomuhr, NTP)

ZooKeeper

ZooKeeper löst dasselbe Problem wie etcd: die Koordination verteilter Systeme und die Speicherung von Metadaten. etcd kann jedoch auf Entwicklungs- und Betriebserfahrungen mit dem Entwurf und der Implementierung von ZooKeeper zurückgreifen. Diese Erfahrungen haben den Entwurf von etcd geprägt und dabei geholfen, große Systeme wie Kubernetes zu unterstützen. Zu den Verbesserungen von etcd gegenüber ZooKeeper gehören:

  • Dynamische Neukonfiguration der Clustermitgliedschaft
  • Stabile Lese- und Schreibzugriffe unter hoher Last
  • Ein Datenmodell mit Mehrversions-Nebenläufigkeitskontrolle
  • Zuverlässige Schlüsselüberwachung, die niemals unbemerkt Ereignisse verwirft
  • Lease-Grundoperationen, die Verbindungen von Sitzungen entkoppeln
  • APIs für sichere verteilte gemeinsame Sperren

Darüber hinaus unterstützt etcd unmittelbar zahlreiche Sprachen und Frameworks. ZooKeeper verwendet sein eigenes, ausschließlich dort eingesetztes Jute-RPC-Protokoll, das die unterstützten Sprachanbindungen begrenzt. Das Client-Protokoll von etcd basiert dagegen auf gRPC , einem verbreiteten RPC-Framework mit Anbindungen für Go, C++, Java und weitere Sprachen. gRPC lässt sich außerdem als JSON über HTTP serialisieren, sodass auch allgemeine Befehlszeilenwerkzeuge wie curl damit kommunizieren können. Dank dieser Auswahl lassen sich Systeme mit ihren nativen Werkzeugen auf etcd aufbauen, statt sie mit einem einzigen festen Technologiesatz um etcd herum zu entwickeln.

Unter Berücksichtigung von Funktionen, Unterstützung und Stabilität sind neue Anwendungen, die ZooKeeper als konsistenten Schlüssel-Wert-Speicher einsetzen wollen, gut beraten, stattdessen etcd zu wählen.

Consul

Consul ist ein umfassendes Framework zur Diensterkennung. Es bietet integrierte Health Checks, Ausfallerkennung und DNS-Dienste. Außerdem stellt Consul einen Schlüssel-Wert-Speicher mit RESTful-HTTP-APIs bereit. Beim Stand von Consul 1.0 skaliert dieser Speicher bei Schlüssel-Wert-Operationen weniger gut als Systeme wie etcd oder ZooKeeper. Systeme mit Millionen Schlüsseln leiden unter hohen Latenzen und Speicherdruck. Der Schlüssel-Wert-API fehlen insbesondere Mehrversionsschlüssel, bedingte Transaktionen und zuverlässige Streaming-Watches.

etcd und Consul lösen unterschiedliche Probleme. Für einen verteilten konsistenten Schlüssel-Wert-Speicher ist etcd die bessere Wahl. Für eine umfassende Diensterkennung im Cluster reichen die Funktionen von etcd nicht aus; dafür eignen sich Kubernetes, Consul oder SmartStack.

NewSQL (Cloud Spanner, CockroachDB, TiDB)

Sowohl etcd als auch NewSQL-Datenbanken wie Cockroach , TiDB und Google Spanner bieten starke Datenkonsistenzgarantien und Hochverfügbarkeit. Die deutlich unterschiedlichen Systementwurfsparameter führen jedoch zu deutlich unterschiedlichen Client-APIs und Leistungseigenschaften.

NewSQL-Datenbanken sind für horizontale Skalierung über Rechenzentren hinweg ausgelegt. Sie verteilen Daten üblicherweise auf mehrere konsistente Replikationsgruppen, sogenannte Shards, die weit auseinanderliegen können, und speichern Datenmengen im Terabyte-Bereich und darüber. Für verteilte Koordination eignet sich diese Skalierung schlecht: Das Warten auf Uhren verursacht hohe Latenzen, und Aktualisierungen werden mit weitgehend lokalen Abhängigkeitsgraphen erwartet. Die Daten sind in Tabellen organisiert und lassen sich mit SQL-artigen Abfragen mit umfangreicherer Semantik als bei etcd abfragen. Dafür steigt jedoch die Komplexität der Verarbeitung sowie der Planung und Optimierung von Abfragen.

Für Metadatenspeicherung oder die Koordination verteilter Anwendungen eignet sich etcd. Sollen mehr als einige GB Daten gespeichert werden oder sind vollständige SQL-Abfragen erforderlich, eignet sich eine NewSQL-Datenbank.

etcd für Metadaten verwenden

etcd repliziert alle Daten innerhalb einer einzigen konsistenten Replikationsgruppe. Für bis zu einige GB Daten mit konsistenter Reihenfolge ist dies der effizienteste Ansatz. Jede Änderung des Clusterzustands, die mehrere Schlüssel betreffen kann, erhält aus einem monoton steigenden Zähler eine global eindeutige ID, in etcd Revision genannt. Anhand dieser ID lässt sich die Reihenfolge bestimmen. Da es nur eine Replikationsgruppe gibt, muss die Änderungsanfrage zur Bestätigung nur das Raft-Protokoll durchlaufen. Durch die Begrenzung des Konsenses auf eine Replikationsgruppe erreicht etcd verteilte Konsistenz mit einem einfachen Protokoll bei geringer Latenz und hohem Durchsatz.

Die Replikation von etcd lässt sich mangels Daten-Sharding nicht horizontal skalieren. NewSQL-Datenbanken verteilen Daten dagegen üblicherweise auf mehrere konsistente Replikationsgruppen und speichern Datenmengen im Terabyte-Bereich und darüber. Um jeder Änderung eine global eindeutige, steigende ID zuzuweisen, muss jede Anfrage jedoch ein zusätzliches Koordinationsprotokoll zwischen den Replikationsgruppen durchlaufen. Dieser zusätzliche Schritt kann Konflikte um die globale ID verursachen und geordnete Anfragen zur Wiederholung zwingen. Das Ergebnis ist ein komplexerer Ansatz mit üblicherweise schlechterer Leistung als etcd bei strikter Reihenfolge.

Wenn eine Anwendung vor allem Metadaten oder deren Reihenfolge verarbeitet, etwa zur Prozesskoordination, eignet sich etcd. Benötigt sie einen großen Datenspeicher über mehrere Rechenzentren hinweg und ist nicht stark auf eine strikte globale Reihenfolge angewiesen, eignet sich eine NewSQL-Datenbank.

etcd für verteilte Koordination verwenden

etcd bietet direkt Grundoperationen für verteilte Koordination, darunter Ereignis-Watches, Leases, Wahlen und verteilte gemeinsame Sperren. Bei letzteren müssen Benutzer allerdings nicht offensichtliche Eigenschaften beachten, die unten erläutert werden. Diese Grundoperationen werden von den etcd-Entwicklern gepflegt und unterstützt. Sie externen Bibliotheken zu überlassen, würde die Verantwortung für die Entwicklung grundlegender verteilter Software abgeben und das System im Wesentlichen unvollständig lassen. NewSQL-Datenbanken erwarten üblicherweise, dass Drittanbieter solche Grundoperationen entwickeln. Ebenso verfügt ZooKeeper bekanntlich über eine separate, unabhängige Bibliothek mit Koordinationsrezepten. Consul bietet eine native Sperr-API, weist aber sogar entschuldigend darauf hin, dass sie „keine absolut sichere Methode “ sei.

Theoretisch lassen sich diese Grundoperationen auf jedem Speichersystem mit starker Konsistenz aufbauen. Die Algorithmen sind jedoch oft subtil: Schnell entsteht ein scheinbar funktionierender Sperralgorithmus, der plötzlich an gleichzeitig aufwachenden Wartenden, dem Thundering-Herd-Problem, oder Zeitabweichungen scheitert. Außerdem hängen weitere von etcd unterstützte Grundoperationen wie transaktionaler Speicher vom MVCC-Datenmodell ab; starke Konsistenz allein reicht nicht aus.

Für verteilte Koordination kann die Wahl von etcd Betriebsprobleme vermeiden helfen und Entwicklungsaufwand sparen.

Hinweise zur Verwendung von Sperren und Leases

etcd bietet Sperr-APIs , die auf dem Lease-Mechanismus und dessen Implementierung in etcd basieren. Die Grundidee: Ein Server gewährt einem anfragenden Client ein Token namens Lease und ordnet ihm eine TTL zu. Stellt der Server fest, dass mehr Zeit als die TTL vergangen ist, widerruft er die Lease. Solange der Client eine nicht widerrufene Lease hält, kann er beanspruchen, den Zugriff auf eine zugeordnete Ressource zu besitzen. Bei etcd ist diese Ressource ein Schlüssel im Schlüsselraum. Auf diesem Prinzip beruhen die Sperr-APIs von etcd. Für sich allein gewährleisten sie jedoch keinen gegenseitigen Ausschluss. Die Bezeichnung „lock“ hat historische Gründe . Wie unten beschrieben, können die Sperr-APIs den gegenseitigen Ausschluss jedoch optimieren.

Der wichtigste Aspekt des Lease-Mechanismus ist, dass die TTL als physisches Zeitintervall definiert ist. Server und Client messen den Zeitablauf jeweils mit ihrer eigenen Uhr. Daher kann der Server die Lease bereits widerrufen haben, während der Client weiterhin behauptet, sie zu besitzen.

Wie gewährleistet der Lease-Mechanismus dann den gegenseitigen Ausschluss der Sperren? Tatsächlich gewährleistet er selbst keinen gegenseitigen Ausschluss. Der Besitz einer Lease garantiert nicht, dass ihr Besitzer eine Sperre auf der Ressource hält.

Wenn etcd-Sperren den gegenseitigen Zugriff auf Schlüssel von etcd selbst steuern, wird der gegenseitige Ausschluss durch die Validierung von Versionsnummern implementiert. In anderen Systemen wie Consul wird dies manchmal Compare and Swap genannt. In etcd-RPCs wie Put oder Txn lassen sich erforderliche Bedingungen für Revisionsnummer und Lease-ID der Operationen angeben. Sind diese nicht erfüllt, kann die Operation fehlschlagen. Mit diesem Mechanismus stellt etcd verteilte Sperren für Clients bereit. Ein Client weiß also, dass er die Sperre eines Schlüssels erwirbt, wenn der etcd-Cluster seine Anfragen erfolgreich abschließt.

Die Literatur zu verteilten Sperren beschreibt ähnliche Entwürfe:

Warum bieten etcd und andere Systeme Leases, wenn sie gegenseitigen Ausschluss durch Versionsnummernvalidierung gewährleisten? Leases dienen als Optimierung, um die Anzahl abgebrochener Anfragen zu verringern.

Schlüssel in etcd lassen sich dank Leases und Versionsnummernvalidierung effizient sperren. Müssen Benutzer Ressourcen außerhalb von etcd schützen, müssen diese wie etcd-Schlüssel eine Versionsnummernvalidierung und konsistente Replikate bereitstellen. Die Sperrfunktion von etcd allein kann externe Ressourcen nicht schützen.

9 - Glossar

Begriffe in der etcd-Dokumentation, der Befehlszeile und dem Quellcode

Dieses Dokument definiert die verschiedenen Begriffe, die in der etcd-Dokumentation, der Befehlszeile und dem Quellcode verwendet werden.

Alarm

Der etcd-Server löst einen Alarm aus, wenn ein Eingriff des Betreibers erforderlich ist, damit der Cluster zuverlässig bleibt.

Authentifizierung

Die Authentifizierung verwaltet die Zugriffsberechtigungen von Benutzern auf etcd-Ressourcen.

Client

Ein Client verbindet sich mit dem etcd-Cluster, um Dienstanfragen zu stellen, etwa zum Abrufen von Schlüssel-Wert-Paaren, zum Schreiben von Daten oder zum Beobachten von Änderungen.

Cluster

Ein Cluster besteht aus mehreren Mitgliedern.

Der Knoten in jedem Mitglied folgt dem Raft-Konsensprotokoll, um Logs zu replizieren. Der Cluster empfängt Vorschläge von Mitgliedern, bestätigt sie und wendet sie auf den lokalen Speicher an.

Kompaktierung

Die Kompaktierung verwirft den gesamten etcd-Ereignisverlauf und die ersetzten Schlüssel vor einer bestimmten Revision. Sie dient dazu, Speicherplatz in der Backend-Datenbank von etcd zurückzugewinnen.

Wahl

Der etcd-Cluster hält im Rahmen des Raft-Konsensprotokolls Wahlen unter seinen Mitgliedern ab, um einen Leader zu bestimmen.

Endpunkt

Eine URL, die auf einen etcd-Dienst oder eine etcd-Ressource verweist.

Schlüssel

Ein benutzerdefinierter Bezeichner zum Speichern und Abrufen benutzerdefinierter Werte in etcd.

Schlüsselbereich

Eine Schlüsselmenge, die entweder einen einzelnen Schlüssel, ein lexikalisches Intervall für alle x mit a <= x < b oder alle Schlüssel größer als ein bestimmter Schlüssel enthält.

Schlüsselraum

Die Menge aller Schlüssel in einem etcd-Cluster.

Lease

Eine kurzlebige, verlängerbare Vereinbarung, die bei ihrem Ablauf die ihr zugeordneten Schlüssel löscht.

Mitglied

Ein logischer etcd-Server, der am Betrieb eines etcd-Clusters beteiligt ist.

Änderungsrevision

Die erste Revision, die den letzten Schreibvorgang für einen bestimmten Schlüssel enthält.

Peer

Ein Peer ist ein anderes Mitglied desselben Clusters.

Vorschlag

Ein Vorschlag ist eine Anfrage, etwa eine Schreibanfrage oder eine Anfrage zur Konfigurationsänderung, die das Raft-Protokoll durchlaufen muss.

Quorum

Die Anzahl aktiver Mitglieder, die für einen Konsens zur Änderung des Clusterzustands benötigt wird. etcd benötigt eine Mehrheit der Mitglieder, um ein Quorum zu erreichen.

Revision

Ein clusterweiter 64-Bit-Zähler, der bei 1 beginnt und bei jeder Änderung des Schlüsselraums erhöht wird.

Rolle

Eine Einheit von Berechtigungen für eine Menge von Schlüsselbereichen, die zur Zugriffskontrolle einer Menge von Benutzern zugewiesen werden kann.

Snapshot

Ein Backup des etcd-Clusterzustands zu einem bestimmten Zeitpunkt.

Speicher

Der physische Speicher, in dem der Schlüsselraum des Clusters abgelegt ist.

Term

Ein Term ist eine monoton steigende Ganzzahl, die im Raft-Algorithmus jeder Leader-Wahl zugeordnet wird. Pro Term kann es nur einen gewählten Leader geben. Bei einem Leader-Wechsel wird der Term erhöht.

Transaktion

Eine atomar ausgeführte Menge von Operationen. Alle in einer Transaktion geänderten Schlüssel haben dieselbe Änderungsrevision.

Schlüsselversion

Die Anzahl der Schreibvorgänge für einen Schlüssel seit seiner Erstellung, beginnend bei 1. Die Version eines nicht vorhandenen oder gelöschten Schlüssels ist 0.

Watcher

Ein Client öffnet einen Watcher, um Änderungen in einem bestimmten Schlüsselbereich zu beobachten.