Apache Zookeeper

Zusammenarbeit zwischen Eclipse und der Zookeeper-Technologie

Florian Pirchner
©Shutterstock/Jeff Whyte

Zu Recht verdient dieses Apache-Projekt seinen Namen „Zookeeper“. Aufgrund seiner Architektur bildet es die Basis, um eine Vielzahl unterschiedlicher Tätigkeiten im Bereich verteilter Systeme abzubilden. Mittels eines filesystemartigen Ansatzes können höchst effiziente Datenstrukturen zur Koordination von Servern innerhalb eines Clusters entwickelt werden. Eine Bestandsaufnahme.

Dieser Artikel hat zum Ziel, Ihnen einen Idee davon zu vermitteln, was Apache Zookeeper [1] (ZK) so einzigartig macht, wie Sie es in ihr eigenes Projekt integrieren können und wo Sie weiterführende Informationen erhalten, um in Kürze selbst fundiertes Wissen über Apache Zookeeper zu besitzen. Wie bereits angemerkt, adressiert Apache Zookeeper verteilte Systeme und stellt dem Systemarchitekten ein in hohem Maße effizientes, wie auch unerlässliches Tool zur Verfügung. Als verteiltes System versteht sich in diesem Artikel ein System, das auf Basis mehrerer eigenständiger Server zusammengesetzt ist. Die Server kommunizieren untereinander und stellen sich gegenseitig Services zur Verfügung.

My-Discovery-Service als Leitfaden

Als konkretes Beispiel soll ein sehr einfacher Discovery-Service auf Basis von Zookeeper umgesetzt werden. Möchte ein Server einen Service eines bestimmten Typs wie beispielsweise MailService konsumieren, verwendet dieser den Discovery-Service, um die IP-Adresse eines geeigneten Remote-Services zu finden. Mittels dieser Adresse kann der Server anschließend das entfernte Service per Remote-Aufruf konsumieren. Hauptaugenmerk wird dabei auf den Discovery-Service basierend auf Zookeeper gelegt. Die Implementierung der Remote-Services ist nicht Teil dieses Artikels.

  1. Der Discovery-Service stellt ein Verzeichnis zur Verfügung, das jeden aktiven Server beinhaltet. Als Information je Server werden IP-Adresse und dessen zur Verfügung gestellte Services definiert.
  2. Der Discovery-Service muss hochverfügbar sein.
  3. Beim Starten eines neuen Servers soll dieser selbständig seine Services am Discovery-Service registrieren.
  4. Clients können den Discovery-Service verwenden, um alle registrierten Server abzufragen.
  5. Der Ausfall eines Servers wird durch den Discovery-Service erkannt und das Serververzeichnis aktualisiert.
  6. Clients werden über Änderungen am Discovery-Service informiert.

Diese für ein lokales und in sich geschlossenes System relativ einfache Anforderung nimmt für verteilte Systeme stark an Komplexität zu. Es muss sichergestellt werden, dass es sich beim Discovery-Service um eine hochverfügbare Komponente handelt, welche zusätzlich die Datenkonsistenz und den Lifecycle von Serverinstanzen managt.

Die Implementierung der oben definierten Anforderungen würde ohne den Einsatz eines Controllers wie Apache Zookeeper einiges an Zeit in Anspruch nehmen. Mittels Zookeeper lassen sich diese Anforderungen jedoch einfach umsetzen, zumal es darauf ausgelegt ist, exakt diese Problemstellungen zu adressieren.

Zookeeper-Server

Um eine Umsetzung des Discovery-Service verstehen zu können, benötigt es ein wenig Information über die Funktionsweise von Apache Zookeeper.

Abb. 1: Zookeeper Ensemble

Ensemble

Apache Zookeeper stellt sich dem Entwickler als ein einzelner Service dar. Im Hintergrund bildet es jedoch einen Cluster einzelner ZK-Serverinstanzen; auch „Zookeeper Ensemble“ (Abb. 1) genannt. Dieses Ensemble organisiert sich selbst und wählt stets eine Leader Node, welche für diverse Synchronisierungen und Erhaltung der Datenkonsistenz verantwortlich ist. Restliche ZK-Server werden Follower genannt. Bei Ausfall des Leaders wird innerhalb von wenigen Augenblicken ein neuer Leader aus der Menge der vorhandenen Follower gewählt.

Daten

Die ZK-Serverinstanzen stellen eine filesystemähnliche Datenstruktur zur Verfügung, in welcher Z-Nodes abgelegt werden können. Z-Nodes sind hierarchisch angeordnete Datencontainer und haben die Eigenschaft eines Folders, wobei gleichzeitig auch Daten in einer Z-Node gespeichert werden können (Abb. 2). Mittels einer Pfadangabe wie „/discovery/Server-1/“ lassen sich Z-Nodes innerhalb einer ZK-Serverinstanz adressieren. Eine wichtige Einschränkung von Zookeeper ist die Größe der Daten, die innerhalb einer Z-Node mit 1 MB begrenzt ist. Der Grund hierfür liegt darin, dass Zookeeper als Koordinator konzipiert wurde und dafür benötigte Datenmengen weit unter 1 MB liegen werden.

Abb. 2: Z-Nodes als Datencontainer

Um die unterschiedlichen Aufgabenstellungen auf Basis von Z-Nodes umsetzen zu können, werden zwei verschiedene Arten zur Verfügung gestellt:

  • Persistent: „Persistent Z-Nodes“ werden automatisch von jeder ZK-Server-Instanz gespeichert und erst dann wieder gelöscht, wenn ein Client eine dezidierte Löschanforderung sendet.
  • Ephemeral (flüchtig): „Ephemeral Z-Nodes“ sind an den Lifecycle einer Client-Session gebunden und werden automatisch gelöscht, sobald die Session beendet wird. Ob eine Session noch aktiv ist oder nicht, erkennt das System aufgrund eines Heartbeat-Protokolls. Ein ideales Mittel, um den Ausfall von Clients überwachen zu können.

Jeder ZK-Server hält sein eigenes Replikat der Daten, was Lesezugriffe sehr schnell macht. Die Versorgung der Follower mit aktuellen Daten erfolgt durch einen Broadcast des Leaders im Zuge einer Schreiboperationen (Abb. 3). Jede Schreiboperation wird immer zuerst an den Leader weitergeleitet und erhält eine eindeutige Zookeeper Transaction ID (zxid). Transaktionen sind eindeutig, atomar und geordnet. Der Leader sorgt dafür, dass eine gewisse Anzahl von Followern mit den aktuellen Daten versorgt wird. Da nicht alle Follower mit den Daten versorgt werden müssen, kann dies zur Folge haben, dass einzelne ZK-Server mit der Aktualität ihrer Daten hinter dem Leader zurückliegen werden. Somit auch die Bezeichnung Follower. ZK stellt ein API zur Verfügung, um sicherstellen zu können, dass gelesene Daten erst mit dem Leader abgeglichen werden, falls erforderlich.

Abb. 3: Schreiboperationen

Zookeeper-Client

Um Zugang zu einem Zookeeper Ensemble zu erhalten, verwendete ein Entwickler einen Zookeeper-Client (Client-API). Dieser Instanz wird eine Liste aus IP-Adressen von verfügbaren ZK-Serverinstanzen übergeben. Die eigentliche Verbindung von Client zu ZK-Server ist für den Entwickler transparent. Ebenso ist die konkrete ZK-Serverinstanz – zu welcher sich der Client verbindet – unerlässlich, zumal Zookeeper sich nach außen als ein einzelnes Service präsentiert. Sämtliche ZK-Serverinstanzen operieren homogen. Bricht die Verbindung vom Client zur aktuellen ZK-Serverinstanz ab, verbindet sich der Client automatisch zur nächsten verfügbaren Instanz.

Watcher

Ein weiteres sehr nützliches und höchst notwendiges Konzept von Zookeeper sind Watcher. Diese erlauben es, mithilfe des Client-API auf Lifecycle-Events oder Datenänderungen zu reagieren. Beispielsweise lässt sich ein Watcher definieren, der auf das Entfernen einer Z-Node reagiert und den Client mittels Callback informiert. Watcher bleiben auch erhalten, wenn die Verbindung zur aktuellen ZK-Serverinstanz abbricht und der Client innerhalb des Session-Time-outs eine neue Verbindung zu einer alternativen ZK-Serverinstanz herstellt. Abweichend zum klassischen Observer Pattern ist die Tatsache, dass Watcher nach jeder Notification erneut registriert werden müssen.

Umsetzung von „My-Discovery-Service“

Im zweiten Teil dieses Artikels betrachten wir die konkrete Umsetzung des Discovery-Service. Auf Basis der oben dargestellten Informationen und dem Anforderungskatalog wird dieser Schritt für Schritt beschrieben.

Als Nomenklatur ist festgelegt, dass es sich bei „ZK-Server“ um Instanzen von Zookeeper-Servern handelt, die den Discovery-Service umsetzen. Unter dem Wort „Server“ versteht sich in diesem Abschnitt ein „Serviceserver“, der Services zur Verfügung stellt bzw. Services konsumiert und „ZK-Server“ lediglich verwendet, um auf den Discovery-Service zugreifen zu können (Anforderung an den Discovery-Service siehe oben).

Anforderung 1: Bereitstellung eines Verzeichnisses: Dies wird mittels Z-Nodes abgebildet. Abbildung 2 zeigt die damit verbundenen Daten. Unter der Root-Z-Node (/myApp) wird eine persistente Z-Node mit dem Namen „/discovery“ angelegt. Ein Löschen erfolgt erst wieder, wenn ein Client dies per API dezidiert anfordert; was aber höchst unwahrscheinlich ist. Die Discovery Z-Node wird für jeden gestarteten Server eine weitere Ephemeral Z-Node beinhalten. Folgendes Codefragment zeigt, wie sich ein Client am Ensemble registriert und die „/discovery“ Z-Node erstellt, falls nicht vorhanden:

ZooKeeper zk = new ZooKeeper("localhost:2130", 3000, this);
// create a new persistent Z-Node if not present
if(zk.exists("/discovery", false) == null){
  zk.create("/discovery", null,
  Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}

Anforderung 2: Discovery-Service muss hochverfügbar sein. Dies ist eine Basisfunktionalität von Zookeeper. Durch das Starten mehrerer ZK-Serverinstanzen auf unterschiedlichen physikalischen Servern kann sichergestellt werden, dass ein Client immer mehrere alternative ZK-Server zur Verfügung hat und bei Ausfall eines Knotens sofort auf eine Alternative ausweichen kann.

Anforderung 3: Discovery-Service kennt neue Server automatisch. Dies wird umgesetzt, indem jeder startende Server eine Ephemeral Z-Node (Server-1, Server-2, …) unter „/discovery“ anlegt, z. B. „/discovery/Server-00001„.

Als Daten werden sowohl IP-Adresse des Servers, als auch alle exportierten Services eingetragen. Die fortlaufende Nummerierung der Server-Z-Nodes wird durch ein Built-in-Feature von Zookeeper umgesetzt. Dieses Features erlaubt es, eine „Sequence Number“ an den zu erstellenden Node Name anzuhängen und wird durch das Client-API zur Verfügung gestellt (Listing 1).

//
// register server at ensemble
//

// config data for Z-Node
String configData =   
      "@10.0.0.12:9054 n" + 
      "- MailService n" + 
      "- ReportService";
// create new Z-Node
serverPath = zk.create("/discovery/Server-", configData.getBytes(),
      Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

Anforderung 4: Abfrage aller registrierten Server. Dies wird umgesetzt, in dem alle Z-Nodes unterhalb von „/discovery“ ausgelesen werden. Listing 2 zeigt die Umsetzung.

//
// find a proper server for a service
//
List<String> children = zk.getChildren("/discovery", false);
for (String childPath : children) {
byte[] data = zk.getData("/discovery/" + childPath, false, null);
// check if data contains Service and return proper info
// ...
// ...
}

Anforderung 5: Discovery-Service erkennt Ausfall eines Servers. Auch das ist ein Built-in-Feature von Zookeeper. Jeder Server erstellt eine Ephemeral Z-Node, welche an den Lifecycle der Client-Session gebunden ist. Wird die Client-Session des Servers beendet oder läuft in einen Time-out, dann wird die erstellte Ephemeral Z-Node automatisch von allen ZK-Serverinstanzen entfernt. Zur Umsetzung von Anforderung 4 war es somit nur notwendig, eine Ephemeral Z-Node anstelle einer Persistent Z-Node zu erstellen. Folgendes Codefragment beendet die Verbindung zum Ensemble, was auch die Empheral Z-Node des Servers entfernen würde:

//
// unregister server from ensemble
//
zk.close();

Anforderung 6: Clients über Änderungen informieren. Verwendet z. B. Server 4 einen Service von Server 3, muss Server 4 somit über den Ausfall bzw. über Änderungen der Konfigurationsdaten von Server 3 informiert werden, um interne Vorkehrungen treffen zu können.

Dies kann sehr einfach mittels Watcher umgesetzt werden. Server 4 registriert einen Watcher auf die Z-Node „/discovery/Server-00003„. Wird die Z-Node entfernt – bei Ausfall von Server 3 – oder Konfigurationsdaten geändert, dann wird Server 4 darüber informiert. Ändert sich beispielsweise die IP-Adresse des Remote-Service, so kann Server 4 sein internes Handling dahingehend anpassen (Listing 3).

 //
// observe server 3 with a watcher
//
Watcher watch = new Watcher() {
  @Override
  public void process(WatchedEvent event) {
    switch (event.getType()) {
      case NodeDeleted:
      // node removed --> Server stopped
      case NodeDataChanged:
      // data changed --> Update internal config
    }
  }
};

// register watcher for path "/discovery/Server-00003"
zk.exists("/discovery/Server-00003", watch);

Eclipse-interne Verwendung

Im dritten Teil dieses Artikels soll nun auf die Verwendung von Apache Zookeeper innerhalb von Eclipse-Projekten eingegangen werden. Laut aktuellem Wissensstand [Stand: April 2013, Anm. d. Red.] verwenden zwei Eclipse-Projekte Apache Zookeeper, um verteilte Systeme zu handeln.

  • Gyrex
  • Zoo-Discovery von ECF

Gyrex

Gyrex demonstriert die Verwendung von Apache Zookeeper wirklich eindrucksvoll für unterschiedliche Aufgaben: Zum einen, um dem Cluster globale Informationen über den aktuellen Zustand einzelner Knoten, wie „freigegebene Knoten“, „Knoten online“ usw., zur Verfügung zu stellen, zum anderen, um Cloud-Preferences umsetzen zu können. Das bedeutet, dass Preferences zusätzlich zum klassischen Eclipse-Ansatz ebenfalls im Zookeeper Ensemble abgelegt und von jedem Ort sehr einfach und schnell wieder ausgelesen werden können. Schließlich verwendet Gyrex das ZK, um Informationen zwischen Knoten des Gyrex-Clusters schnell mit dem „Queue Receipt“ [2] verteilen zu können.

Zum aktuellen Zeitpunkt existiert noch das eine oder andere Detail, das bei der Verwendung von ZK als OSGi Bundle beachtet werden muss (Hierzu wurden bereits Issues bei Zookeeper erfasst). Gyrex löst diese Punkte auf eine sehr elegante Art und Weise. Des Weiteren zeigt Gyrex auf, wie Zookeeper-Server gestartet und ein einheitliches Zookeeper-Gateway implementiert werden kann.

Sollten Sie planen, Apache Zookeeper im Eclipse-Umfeld einzusetzen, ist es eine sehr gute Idee, sich die Implementierung von Gunnar Wagenknecht im Detail anzusehen: sehr schöner Code, um der Komplexität von verteilten Systemen mittels Apache Zookeeper zu begegnen. Als Einstiegspunkt empfehle ich:

org.eclipse.gyrex.cloud.internal.zk.ZooKeeperGate

ZooDiscovery von ECF

ECF implementiert die OSGi-RSA-Spezifikation [3] (Remote Service Admin [4]). RSA ist eine Erweiterung von OSGi RS (Remote Services) um einen Discovery-Mechanismus. Beim Einsatz von OSGi RS musste stets die Adresse des entfernten Service bekannt sein. RSA löst dieses Problem durch ein Discovery- und Distribution-Subsystem. Services werden automatisch zwischen Servern verteilt und von Distribution in Form von Proxies als lokale Services bereitgestellt. Hierfür verwendet ZooDiscovery [5] das Zookeeper-Projekt, um Informationen zu den exportierten Services zwischen den Nodes verteilten zu können. OSGi RSA verleitet dazu, die Spezifikation als eine Cloud-Spezifikation zu betrachten. Allerdings sollten diese Themen keinesfalls verwechselt werden. OSGi RSA definiert ausschließlich, wie Informationen von Services verteilt werden. Weiterführende Techniken für Cloud-Systeme wie Load Balancing usw. sind nicht Teil der Spezifikation. Im Zuge von RFP133 [6] wird in der OSGi-Cloud-Workinggroup bereits an einer Cloud-Spezifikation gearbeitet. Diese setzt intern wiederum auf OSGi RSA auf, allerdings sind die mit RFP133 verbundenen Konzepte wesentlich komplexer.

Weiterführende Informationen

Auf den Websites der jeweiligen Projekte findet sich eine Vielzahl von sehr hilfreichen Informationen. Zookeeper stellt eine Auswahl an „Recipes“ zur Verfügung, um anhand dieser Beispiele die Verwendung des API erlernen zu können, siehe [1], [2]. Das Buch „Hadoop – The definitive Guide“ von O’Reilly [7] widmet Zookeeper ein ganzes Kapitel. Dieses ist aus meiner Sicht die beste Beschreibung über Zookeeper, die ich bisher gelesen habe.

Allerdings sollte an dieser Stelle auch angemerkt werden, dass der professionelle Einsatz auch mit einem gewissen Overhead verbunden ist. Zookeeper stellt dem Entwickler ein sehr freies Low-Level-API zur Verfügung. Um daraus ein High-Level-API zu erzeugen, das mit sämtlichen Eventualitäten zurechtkommt, bedarf es sicher einigen Aufwands.

Hierzu existiert ein sehr interessantes Projekt namens Curator [8]. Es implementiert ein High-Level-API für zahlreiche „Zookeeper Patterns“ und stellt hilfreiche Informationen zur Verfügung. Curator steht unter der Apache-Lizenz; allerdings steht es derzeit noch nicht als OSGi Bundle zur Verfügung.

Aufmacherbild: Zoo keepers exercise elephant at the Zoo von Shutterstock / Urheberrecht: Jeff Whyte

Geschrieben von
Florian Pirchner
Florian Pirchner
Florian Pirchner ist selbstständiger Softwarearchitekt, lebt und arbeitet in Wien. Aktuell beschäftigt er sich als Project Lead in einem internationalen Team mit dem Open-Source-Projekt „lunifera.org – OSGi-Services for Business Applications“. Auf Basis von Modellabstraktionen und der Verwendung von OSGi-Spezifikationen soll ein hoch erweiterbarer und einfach zu verwendender Kernel für Businessapplikationen geschaffen werden.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.