Verteilte Systeme mit Etcd in der Praxis
Seite 3: 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:
- Ein Cluster sollte aus einer ungeraden Anzahl von Mitgliedern bestehen, um bei einer Teilung immer eine Gruppe mit der Mehrheit zu erhalten.
- Innerhalb eines Clusters handeln die Mitglieder untereinander aus, wer der aktuelle Leader ist.
- Der Ausfall eines Leaders führt dazu, dass die anderen Knoten einen neuen Leader wählen.
- Sinkt die Zahl der Mitglieder unter die notwendige Mindestmenge zur Entscheidungsfähigkeit, sind nur noch Leseoperationen möglich.
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.