zurück zum Artikel

Verteilte Systeme mit Etcd in der Praxis

Architektur/Methoden
Verteilte Systeme mit Etcd in der Praxis

Etcd ist ein in Go geschriebener, hierarchischer, verteilter Key-Value-Store. Als Bestandteil der CoreOs-Familie ist er Open Source. Anspruch der Entwickler war, die Verwaltung von verteilten Anwendungen überschaubar und gleichzeitig robust zu machen.

Verteilte Systeme sind heute eine Selbstverständlichkeit. Themen wie Big Data und Internet of Things (IoT) verstärken diesen Trend. Klassische Verteilungen, bei denen Frontend und Datenbank oft sogar auf derselben Maschine liefen, gehören der Vergangenheit an. An ihre Stelle sind Cloud-Systeme getreten, die das Frontend so nah wie möglich beim Kunden platzieren. Die einzelne Datenbank ist einem flexibel skalierenden Cluster gewichen. Hinzu kommen Erkenntnisse zum Thema Microservices [1], wie sie James Lewis und Martin Fowler beschrieben haben. Bei allen Vorzügen haben verteilte Systeme auch ihre Schattenseiten.

Verteilte Systeme bringen eine Reihe von Herausforderungen mit sich. Netzwerke können ausfallen und bringen stets Verzögerungen mit sich. Der Datendurchsatz ist endlich und kostet Geld. Durch den ständigen Austausch über das Netz entstehen zudem zusätzliche Herausforderungen an die Sicherheit beim Transport der Daten. Auch kann sich die Netzwerktopologie ohne Vorwarnung ändern.

Die Aufteilung von Anwendungen in immer kleinere Bestandteil stellt eine weitere Herausforderung dar. Der Betreiber muss im Blick behalten, wo sich die einzelnen Bestandteile des Systems befinden, wer sie koordiniert und woher sie ihre Konfiguration beziehen.

Das Ziel ist eine Möglichkeit, änderbare Informationen zentral an einem ausfallsicheren, fehlertoleranten und möglichst konsistenten Ort abzulegen. Ein vielversprechender Lösungsansatz für diese Probleme heißt Etcd [2].

Etcd erreichte im Januar dieses Jahres mit Version 2.0 ein Major Release. Laut den Entwicklern wurde besonderer Wert auf folgende Eigenschaften gelegt:

Geburtsstunde des Clusters

Um den praktischen Teil des Artikels nachzuvollziehen, braucht es eine Docker-Installation, cURL und eine Shell. Der einfachste Weg zu einem Etcd-Cluster führt über Docker. Das passende Image gibt es unter der folgenden URL:

docker pull quay.io/coreos/etcd:v2.0.12

Zur Vereinfachung der nächsten Schritte empfiehlt es sich, eine Umgebungsvariable mit der zu verwendenden IP-Adresse anzulegen. (Boot2Docker-Verwender finden sie in der Umgebungsvariable DOCKER_HOST oder erhalten es mit dem Befehl boot2docker ip):

export DOCKER_HOST_IP=192.168.59.103

Folgendes Kommando startet das erste Mitglied des Clusters, das dadurch zum Leader wird (siehe Kasten zum Thema Konsens):

docker run -d -p 5001:5001 -p 7001:7001 --name node1 \
quay.io/coreos/etcd:v2.0.12 -name node1 \
-advertise-client-urls http://${DOCKER_HOST_IP}:7001 \
-listen-client-urls http://0.0.0.0:7001 \
-initial-advertise-peer-urls http://${DOCKER_HOST_IP}:5001 \
-listen-peer-urls http://0.0.0.0:5001 \
-initial-cluster-token etcd-cluster-1 \
-initial-cluster node1=http://${DOCKER_HOST_IP}:5001, \
node2=http://${DOCKER_HOST_IP}:5002, \
node3=http://${DOCKER_HOST_IP}:5003

Neben den üblichen Docker-Parametern gibt es hier auch ein paar Etcd-spezifische:

Den ersten Follower für den frischen Leader startet folgendes Kommando:

docker run -d -p 5002:5002 -p 7002:7002 --name node2 \
quay.io/coreos/etcd:v2.0.12 -name node2 \
-advertise-client-urls http://${DOCKER_HOST_IP}:7002 \
-listen-client-urls http://0.0.0.0:7002 \
-initial-advertise-peer-urls http://${DOCKER_HOST_IP}:5002 \
-listen-peer-urls http://0.0.0.0:5002 \
-initial-cluster-token etcd-cluster-1 \
-initial-cluster node1=http://${DOCKER_HOST_IP}:5001, \
node2=http://${DOCKER_HOST_IP}:5002, \
node3=http://${DOCKER_HOST_IP}:5003

Im Vergleich zum ersten Befehl ist der Parameter peers hinzugekommen. Er enthält eine kommaseparierte Liste, die mindestens eines der anderen Cluster-Mitglieder enthalten sollte. Zwei weitere Follower runden das Beispiel ab:

docker run -d -p 5003:5003 -p 7003:7003 --name node3 \
quay.io/coreos/etcd:v2.0.12 -name node3 \
-advertise-client-urls http://${DOCKER_HOST_IP}:7003 \
-listen-client-urls http://0.0.0.0:7003 \
-initial-advertise-peer-urls http://${DOCKER_HOST_IP}:5003 \
-listen-peer-urls http://0.0.0.0:5003 \
-initial-cluster-token etcd-cluster-1 \
-initial-cluster node1=http://${DOCKER_HOST_IP}:5001, \
node2=http://${DOCKER_HOST_IP}:5002, \
node3=http://${DOCKER_HOST_IP}:5003

Folgendes Kommando überprüft, ob alle Instanzen korrekt gestartet sind:

curl -L http://${DOCKER_HOST_IP}:7001/version

Die Ereignismeldung etcd 2.0.12 bedeutet, dass der erste 3-Node-Cluster steht.

Ein Blick unter die Haube auf das Datenmodell

Werte in Etcd können entweder einfache Key-Value- oder über einen Key identifizierte Verzeichnis-Knoten sein. Jeder Knoten kann somit eindeutig über seinen Pfad identifiziert werden, beispielsweise mit /Verzeichnis1/Verzeichnis2/Key1.

Für die weitere Arbeit stehen folgende Funktionen zur Verfügung:

Im Folgenden werden die wichtigsten Funktionen vorgestellt.

Key-Value-Knoten

Spielereien mit Key-Value-Knoten

Generell ist bei der Interaktion mit Etcd darauf zu achten, dem cURL-Kommando immer den Parameter -L mitzugeben. Dieser sorgt dafür, dass der Befehl eventuellen Redirects folgt, von denen Etcd exzessiven Gebrauch macht.

Folgendes Kommando erstellt einen simplen Knoten:

curl -L http://${DOCKER_HOST_IP}:7001/v2/keys/forever \
-XPUT -d value="I am here forever"

Als Antwort sollte folgendes JSON-Dokument zurückgegeben werden:

{
"action":"set",
"node":{
"key":"/forever",
"value":"I am here forever",
"modifiedIndex":7,
"createdIndex":7
}
}

Der Parameter action steht für die durchgeführte Operation. Das in node abgelegte JSON-Objekt enthält alle Informationen rund um den Knoten. Neben key und value finden sich hier der modifiedIndex, der für jede Veränderung durch ein update hochgezählt wird, und der createIndex, der für jedes Erzeugen des Knotens erhöht wird.

Dessen Existenz überprüft folgendes Kommando:

curl -L http://${DOCKER_HOST_IP}:7001/v2/keys/forever

Erfolg meldet das Resultat

{
"action":"get",
"node":{
"key":"/forever",
"value":"I am here forever2",
"modifiedIndex":7,
"createdIndex":7
}
}

Der nun folgende Befehl erzeugt durch Verwenden des ttl-Flags einen Knoten mit einer Lebenszeit von fünf Sekunden:

curl -L http://${DOCKER_HOST_IP}:7001/v2/keys/temporary \
-XPUT -d value="I will be gone soon" -d ttl=5

Seinen Zustand überprüft folgendes Kommando:

curl -L http://${DOCKER_HOST_IP}:7001/v2/keys/temporary

Falls zwischen den beiden Befehlen mehr als fünf Sekunden verstreichen, hat der Knoten seine Lebensdauer überschritten, was zu folgender Meldung führt:

{
"errorCode":100,
"message":"Key not found",
"cause":"/temporary",
"index":9
}

Eine wichtige Eigenschaft von TTL-Knoten ist die Möglichkeit, ihn über einen Refresh mit der Ergänzung -d prevExist=true am Leben zu halten:

curl -L http://${DOCKER_HOST_IP}:7001/v2/keys/temporary \
-XPUT -d value="I will be gone soon" -d ttl=5 -d prevExist=true

Wenn dieses Kommando vor dem Erreichen der TTL ausgeführt wird, bleibt der Knoten erhalten.

Spielereien mit Verzeichnis-Knoten

Um einen hierarchischen Graphen aufzubauen, wird zunächst ein Verzeichnisknoten benötigt, der mit dem dir-Flag als true entsteht:

curl -L http://${DOCKER_HOST_IP}:7001/v2/keys/queue -XPUT -d dir=true

Als Ergebnis liefert Etcd folgendes Dokument:

{
"action":"set",
"node":{
"key":"/myDir",
"dir":true,
"modifiedIndex":11,
"createdIndex":11
}
}

Innerhalb eines solchen Verzeichnisses haben weitere Knoten oder Verzeichnisse Platz. Praktischerweise bietet Etcd die Möglichkeit, Knoten automatisch zu nummerieren und dadurch sortiert abzulegen. Durch das Ausführen eines HTTP-POSTs, der nur einen value setzt, wird ein solcher Schlüssel automatisch vergeben:

curl http://${DOCKER_HOST_IP}:7001/v2/keys/queue \
-XPOST -d value=Job1
curl http://${DOCKER_HOST_IP}:7001/v2/keys/queue \ 
-XPOST -d value=Job2

Das Ergebnis dieser Kommandos lässt sich wieder mit einem einfachen HTTP-GET überprüfen:

curl http://${DOCKER_HOST_IP}:7001/v2/keys/queue

Das Antwort-JSON sollte in etwa wie folgt aussehen:

{
"action":"get",
"node":{
"key":"/queue",
"dir":true,
"nodes":[
{
"key":"/queue/12",
"value":"Task1",
"modifiedIndex":12,
"createdIndex":12
},
{
"key":"/queue/17",
"value":"Task2",
"modifiedIndex":17,
"createdIndex":17
}],
"modifiedIndex":11,
"createdIndex":11
}
}

So lassen sich auf einfache Art und Weise Work-Queues erzeugen.

Compare-and-Swap

Vergleiche und tausche!: Compare-and-Swap/Compare-and-Delete

Neben dem simplen Ändern des Wertes eines Knotens via HTTP-PUT bietet Etcd noch eine wichtige weitere Variante. Jeder Knoten kann durch eine sogenannte CAS-Operation (Compare-and-Swap) verändert werden. Dabei wird vor dem Schreiben überprüft, ob der Wert des Knotens einem erwarteten Wert entspricht. Diese Funktion wird vor allem für das gesamte Cluster umspannende Locks benötigt.

Dazu wird zunächst wieder ein Testknoten erstellt:

curl -L http://${DOCKER_HOST_IP}:7001/v2/keys/swapme \
-XPUT -d value="foo"

Falls der alte Wert bar ist, verändert der nächste Befehl den Wert des Knotens auf newvalue. Zur Überprüfung dient das GET-Kommando prevValue=bar.

curl http://${DOCKER_HOST_IP}:7001/v2/keys/swapme?prevValue=bar \
-XPUT -d value=newvalue

Da der vorherige Befehl den Knoten auf den Wert foo gesetzt hat, wird die Schreiboperation mit folgendem Fehler quittiert:

{
"cause": "[bar != foo]",
"errorCode": 101,
"index": 6,
"message": "Compare failed"
}

Steht der Wert der von prevValue auf foo, ist die Schreiboperation erfolgreich:

curl http://${DOCKER_HOST_IP}:7001/v2/keys/swapme?prevValue=foo \
-XPUT -d value=bar

Bedingte HTTP-DELETE-Anfragen funktionieren analog.
Die folgende Tabelle zeigt ein paar interessante cURL-Kommandos:

Kommando Auswirkung
curl -s ‘http://${DOCKER_HOST_IP}:7001/v2/keys/services/myService?recursive=true&sorted=true' Gibt den Verzeichnisinhalt rekursiv sortiert aus.
curl http://${DOCKER_HOST_IP}:7001/v2/keys/myKey?wait=true Der Aufruf wartet, bis sich der Schlüssel ändert
curl http://${DOCKER_HOST_IP}:7001/v2/keys/myKey -XDELETE Löschen eines Schlüssels
curl http://${DOCKER_HOST_IP}:7001/v2/keys/myKey?prevExist=false -XPUT -d value=newValue Setzen eines Schlüssels unter der Bedingung, dass der vorherige Schlüssel nicht existiert
curl http://${DOCKER_HOST_IP}:7001/v2/keys/myKey?prevValue=oldValue -XDELETE Löschen eines Schlüssels unter der Bedingung, dass der vorherige Schlüssel den Wert oldValue hat
curl http://${DOCKER_HOST_IP}:7001/_hiddenKey -XPUT -d value="You do not see me" Setzt einen versteckten Schlüssel

Stresstest mit ein wenig Sabotage

Ausfälle sind in einem verteilten System eher die Regel als die Ausnahme und selbst einzelne können katastrophale Folgen haben.

A distributed system is one in which the failure of a computer you didn't even know existed can render your own computer unusable. (Leslie Lamport)

Etcd muss somit auf Knotenausfälle vorbereitet sein. Aus dem verwendeten RAFT-Protokoll (siehe Box) ergeben sich folgende Eigenschaften für einen Etcd-Cluster:

Zum Testen des doppelten Bodens dienen im Folgenden ein paar Sabotageakte. Zunächst bekommt der oben erstellte, aus drei Knoten bestehende Cluster einen neuen Schlüssel:

curl -L http://${DOCKER_HOST_IP}:7001/v2/keys/key1 \
-XPUT -d value="I am a key"
{"action":"set","node":{"key":"/key1","value":"I am a key", 
"modifiedIndex":7,"createdIndex":7}}
curl http://${DOCKER_HOST_IP}:7001/v2/keys/key1
{"action":"get","node":{"key":"/key1","value":"I am a key",
"modifiedIndex":7,"createdIndex":7}}

Das docker-kill-Kommando fährt einen Knoten ohne Umschweife herunter. Der Befehl docker stop würde dagegen ein reguläres Herunterfahren bewirken, was für den Härtetest ungeeignet ist:

docker kill -s 9 node3

Das Cluster sollte nach wie vor erreichbar sein:

curl http://${DOCKER_HOST_IP}:7001/v2/keys/key1
{"action":"get","node":{"key":"/key1","value":"I am a key", 
"modifiedIndex":7, "createdIndex":7}}
curl -L http://${DOCKER_HOST_IP}:7001/v2/keys/key2 \
-XPUT -d value="I am another key"
{"action":"set","node":{"key":"/key2","value":"I am another key",
"modifiedIndex":9,"createdIndex":9}}

Nun muss auch der zweite Knoten dran glauben:

docker kill -s 9 node2

Ab jetzt scheitern Schreiboperationen, da der verbliebene einzelne Knoten keinen Leader wählen kann. Entsprechende Requests werden mit dem HTTP-Code 500 beantwortet:

curl -L http://${DOCKER_HOST_IP}:7001/v2/keys/key3 \
-XPUT -d value= "I am another key"
{"message": "Internal Server Error"}
curl http://${DOCKER_HOST_IP}:7001/v2/keys/key2
{"action":"get","node":{"key":"/key2","value":"I am another key",
"modifiedIndex":9,"createdIndex":9}}
curl http://${DOCKER_HOST_IP}:7001/v2/keys/key3
{"errorCode":100,"message":"Key not found","cause":"/key3","index":9}

Nach dem Neustart eines der unsanft beendeten Knoten kehrt der Cluster in einen beschreibbaren Zustand zurück.

Java-API & Fazit

Erweiterte Möglichkeiten mit der Java-API

Eine vernünftige HTTP-Client-Bibliothek genügt bei Etcd für den Einstieg. Mit ihr lassen sich die Funktionsweise und Operationen schnell auf der Kommandozeile testen. Spätestens wenn die Funktionen in eigene Bibliotheken verpackt werden sollen, ist es Zeit, auf die Arbeit anderer Leute zurückzugreifen.

Es gibt eine ganze Reihe verschiedener Etcd-Treiber für die verschiedenen Plattformen. Exemplarisch gibt es hier einen Blick auf die Java-VM-Varianten. Dabei liegt der Fokus vor allem auf der Aktualität, der Unterstützung asynchroner Operationen und der Abbildung der vollständigen Etcd-REST-API.

Unter diesen Gesichtspunkten stechen vor allem etcd4j [3] und der Boon etcd Client [4] aus der Masse heraus. Beide werden aktiv gepflegt, bieten alle Funktionen der REST-API und unterstützen sowohl synchrone als auch asynchrone Operationen.

Die folgende Tabelle zeigt ein paar Code-Beispiele:

Operation Etcd4j Etcd-Client
Client erzeugen EtcdClient client = new EtcdClient(
URI.create(
"http://192.168.59.103:7001/"));
Etcd client = ClientBuilder.builder().hosts(
URI.create("http://192.168.59.103:7001")).
createClient();

Put synchron client.put(key, value).send().get(); client.set(key, value);
Put asynchron client.put(key, value).send()
.addListener(resp -> {..});
client.set(response -> {...}, "forever" +
key, value);
Delete synchron client.put(key, value).delete().get(); client.delete(key);
Delete asynchron client.delete(key, value).delete().get()
.addListener(resp -> {..});

client.delete(response -> {...}key);
Auf Änderung warten client.get(key).waitForChange().send()
.addListener(response -> {...});
client.wait(response -> {...}, key);

Boon hat im direkten Vergleich die Nase knapp vorn. Die API ist nicht nur kompakter, sondern fühlt sich beim Umgang mit asynchronen Operationen mehr nach dem aktuellen Java 8 an als Etcd4j. Diese Bewertung ist jedoch recht subjektiv. Wer selbst vergleichen möchte, kann auf das Git-Verzeichnis [5] zum Artikel zugreifen. In dem Repository finden sich einige Beispiele.

Fazit

Etcd besticht durch seine soliden Konzepte, allen voran die Einfachheit und Zuverlässigkeit. Die gut strukturierte REST-API erlaubt einen extrem schnellen Einstieg, den manches Konkurrenzprodukt vermissen lässt. Gleichzeitig senkt sie auch die Hürde zur Integration in eigene Software mittels .NET, Java oder C/C++.

Die einfache Verwendung ist allerdings nicht unbedingt das wichtigste Argument für einen verteilten Key-Value-Store. Wichtiger ist, dass sich im Fehlerfall das Verhalten überschaubar nachvollziehen lässt, wie die aufgeführten Beispiele belegen.

Etcd hat eine niedrige Einstiegshürde und verspricht schnelle Ergebnisse. So erhält der Administrator eine Lösung für verteilte Systeme ohne viel Aufwand. Wer einen überschaubaren Key-Value-Store aufbauen möchte, sollte einen Blick auf Etcd werfen. (rme [6])

Conrad Pöpke
ist Senior IT-Consultant bei der codecentric AG und sieht sich in der Rolle eines "Coding-Software-Architekts", Entwicklers und alles anderem, was für die erfolgreiche Durchführung eines Projektes von Nöten ist. Sein Fokus liegt dabei sowohl auf klassischen Java-Projekten im Enterprise-Umfeld als auch modernen Ansätzen wie Microservice-Architekturen im Kontext von Lean Enterprises.

Jochen Mader
ist Lead IT Consultant bei der codecentric AG, wo er in den Bereichen Big Data und Agile Software Factory tätig ist. Neben seiner Haupttätigkeit ist er regelmäßiger Autor von Fachartikeln und auf verschiedenen Konferenzen als Speaker anzutreffen.

Exkurs

Exkurs: Ein kurzer Ausflug in den Konsens

Konsensprotokolle sind hoch komplex, aber zum Verständnis von Key-Value-Stores unvermeidlich. Das Ziel eines verteilten Key-Value-Stores ist, dass Schreiboperationen innerhalb eines bestimmten Zeitintervalls auf allen Knoten des Clusters durchgeführt werden und einen konsistenten Zustand hinterlassen. Gleichzeitig müssen sie den Wegfall eines oder mehrerer Knoten verkraften und auch im Fehlerfall ein deterministisches Verhalten zeigen.

Es gibt zwei Typen von Knoten, die für die Algorithmen zum Einsatz kommen: Der Koordinator, der auch Leader genannt wird, ist zuständig für die Zuweisung von Schreiboperation im Cluster. Die Slave-Knoten führen Schreiboperationen unter der Koordination des Leaders durch.

Die wohl bekannteste Variante eines Konsens-Algorithmus ist der 2-Phase-Commit: In der ersten Phase, die Commit-Request heißt, informiert der Koordinator die Knoten über die anstehende Operation. Er wartet, bis alle Knoten ihre Bereitschaft zur Durchführung signalisiert haben, und startet dann die zweite, die Commit Phase.

Der Koordinator wartet dann auf die Antworten seiner Slaves. Antwortet auch nur ein einzelner Teilnehmer mit abort, sendet der Koordinator rollback an alle Knoten, um wieder einen konsistenten Zustand herzustellen. Erhält er dagegen von allen ein acknowledge, schickt er commit an die Teilnehmer und sorgt dafür, dass der neue konsistente Zustand in alle Knoten persistiert wird. Problematisch wird diese Vorgehensweise beim Ausfall eines Teilnehmers oder dem Verlust einzelner Nachrichten auf dem Transportweg. Beispielsweise stellt sich die Frage, was passiert, wenn einzelne Knoten die commit-Nachricht nicht erhalten. Wie soll sich das System erholen?

Mit den Erfahrungen aus 2-Phase-Commit-basierten Systemen wurde eine ganze Reihe neuer Algorithmen entwickelt. Sie sollten besser mit Ausfällen umgehen können und eine möglichst geringe Zahl an Nachrichten zum Herstellen eines konsistenten Zustands benötigen.

RAFT-Protokoll

Bis vor etwa zwei Jahren stellte die Familie der Paxos-Protokolle den Höhepunkt dieser Entwicklung dar. Allerdings erwarben sie sich schnell den Ruf, schwer verständlich und umsetzbar zu sein. Vor allem die Analyse von Fehlverhalten stellte Anwender vor einige Probleme.

Im Jahr 2013 wurde ein Paper zu einem neuen Konsens-Protokoll mit dem Namen RAFT [7] veröffentlicht. Sein Ziel war es, ein einfach zu verstehendes Protokoll mit ähnlicher Ausfallsicherheit und Performance wie Paxos zu entwickeln. Und dabei sollte Etcd den Konsens bilden.

Zum besseren Verständnis von Etcd ist ein Blick auf RAFT unverzichtbar. Glücklicherweise gibt es bereits eine ausgezeichnete visuelle [8] Erklärung zu dem Thema.


URL dieses Artikels:
http://www.heise.de/-2845358

Links in diesem Artikel:
[1] http://martinfowler.com/articles/microservices.html
[2] https://github.com/coreos/etcd
[3] https://github.com/jurmous/etcd4j
[4] https://github.com/boonproject/boon/tree/master/etcd
[5] https://github.com/codecentric/etcd-artikel-heise
[6] mailto:rme@ct.de
[7] http://thesecretlivesofdata.com/raft
[8] http://thesecretlivesofdata.com/raft/