Verteilte Systeme mit Etcd in der Praxis

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.