Ein Kommentar

Von Eureka zu Consul: Service-Registrator mit nodeJS

Die Service-Registry ist der zentrale Bestandteil einer Microservice-Architektur. Eine Alternative zu Eureka aus dem Netflix-Stack ist die Verwendung von Hashicorp’s Consul. In diesem Blog-Eintrag zeigen wir eine alternative Implementierung von Gliderlab’s Registrator in NodeJS.

Die Implementierung zeigt die Benutzung zweier im DevOps-Umfeld wesentlicher APIs: der Docker Events und den Consul-Servicekatalog.

Grundlagen: Selbstregistrierung vs. Service-Discovery

Ein möglicher Ansatz, eine Service-Registry um einen neuen Dienst zu ergänzen, ist die Verwendung der Selbstregistrierung: dabei beinhaltet jeder Microservice den Code, um sich bei einer ganz bestimmten Service-Registry anzumelden. Beispielsweise bringt Spring Cloud die Eureka-Registry mit. Beim Start trägt sich jeder Microservice selbst in die Registry ein und sendet von sich aus einen Heartbeat in bestimmten Intervallen.

Beim Service-Discovery-Ansatz weiß der Microservice nichts von einer Registry. Bei Docker-Umgebungen ist es daher üblich, sich an die Events der Docker-Engine anzuflanschen und dort beim Start eines neuen Containers nachzusehen was Sache ist. Diese Aufgabe erfüllt ein Registrator – also ein Infrastruktur-Baustein, der auch ein Microservice sein kann.

In der Consort Academy zeigen wir verschiedene Microservice-Prinzipien mit ihren jeweiligen Vor- und Nachteilen auf. Dazu gehört auch, dass wir Code bereitstellen, der für eine eigene Plattform verwendet werden kann. Die folgende Implementierung ist eines dieser Code-Beispiele, die im Sinne des Microservice-Gedankens extrem schlank gehalten sind.

Implementierung: Der Plan

Der Registrator ist eine aktive Komponente, der als Integrationsmechanismus zwischen Docker und Consul fungiert:

  1. Docker startet einen neuen Container nachdem dieser aus der CI-Pipeline gepurzelt ist.
  2. Nach dem erfolgreichen Start schreibt Docker mehrere Events (z. B. Netzwerk- und Volume-Ereignisse) in die Streaming API, auf die sich der Registrator abonniert hat.
  3. Der Registrator untersucht die Docker-Events und stellt (z. B. anhand der Container-Labels) fest, ob es sich tatsächlich um einen Microservice handelt. Wenn der Container gestartet ist, heißt das aber noch lange nicht, dass der enthaltene Microservice auch schon bereit ist: deshalb wartet der Registrator, bis der health-Endpoint (Actuator im Spring Sprachgebrauch) abfragbar ist.
  4. Der Registrator trägt den Service per API in Consul ein und gibt dabei auch die URL des health-Endpoints mit.
  5. Nun kann Consul von sich aus in bestimmten Intervallen feststellen, ob der Service noch verfügbar ist und diesen ggf. austragen.

Implementierung: Die Umsetzung

Natürlich gibt es für die meisten DevOps-Problemstellungen passende npm-Packages, die die Entwicklung deutlich beschleunigen:

var consul = require('consul')({ host: 'consul', promisify: true });  

var dockerConnection = { socketPath: '/var/run/docker.sock' }
var Docker = require('dockerode');
var docker = new Docker(dockerConnection);

Da wir davon ausgehen, dass der Registrator innerhalb der Docker-Infrastruktur steht, sprechen wir Consul über den DNS-Namen an und verwenden für die Docker-API nicht Http, sondern docker.sock.

Außerdem konfigurieren wir das Consul-Package so, dass alle Aufrufe als Promises ummantelt werden. Das macht den späteren Code übersichtlicher.

Von Docker empfangen wir verschiedene Events, von denen uns nur die Container interessieren (Type == „container). Dabei sieht der Event als JSON-Objekt folgendermaßen aus:

{ 
"status":"start",
"id":"70c311dfd08d464649ab60eac690097c10dad3cead3a575d46e3da765552bb23",
"from":"bitnami/rabbitmq:3.6.7-r0",
"Type":"container",
"Action":"start",
"Actor":{"ID":"70c311dfd08d464649ab60eac690097c10dad3cead3a575d46e3da765552bb23",
         "Attributes":{ "category":"base",
                        "image":"bitnami/rabbitmq:3.6.7-r0",
                        "name":"rabbitmq",
                        "pinned":"3.6.7-r0"}},
"time":1495380895,
"timeNano":1495380895382431130
}

Neben der rein Event-basierten Synchronisation der Consul-Registry ist es auch noch notwendig für Recovery-Szenarien periodisch im Registrator einen Sync durchzuführen:

function syncRegistryWithContainerList() { 

    // a) get status of service registry 
    consul.catalog.service.list(function(err, consulServices) {
         ...
        docker.listContainers({all: true}, function(err, containers) {

              /* returns list of objects

                { Id: '9115274a595cf4465e190f3ee159dc5145eb9d92570c76417893a7256219a578',
                  Names: [ '/kpi-adapter-java' ],
                  Image: 'consortit-docker-cme-local.jfrog.io/kpi-adapter-java:latest',
                  ImageID: 'sha256:c5c7f370c7e62bead7c6e6219f1985e3e4102419382126ea0b72a7eb0c5277cc',
                  Command: '/run.sh',
                  Created: 1495466872,
                  Ports: [ [Object], [Object] ],
                  Labels: {},
                  State: 'running',
                  Status: 'Up 17 hours',
                  HostConfig: { NetworkMode: 'default' },
                  NetworkSettings: { Networks: [Object] },
                  Mounts: [] }

              */

Die obige Funktion wird alle 10 Sekunden aufgerufen und gleicht den aktuellen Stand der Registry (consulServices) mit der Docker-Instanz (containers) ab. Dabei ist im Snippet auch zu sehen, welche Informationen Docker bei listContainers als JSON zurückliefert.

Implementierung: Die Details

Es genügt nicht, einen Microservice zu registrieren – damit mehrere Hauptversionen parallel betrieben werden können, sollte die Major-Version des semantischen Versionierungsschemas mit im Namen enthalten sein: z. B. my-service-v1. Damit lassen sich auch ältere Clients, die die API eines Microservice benutzen, über längere Zeit unterstützen.

Ebenfalls zu überlegen ist, ob als Host (Address) die IP oder ein DNS-Name an Consul übermittelt wird. In Docker Swarm-Umgebungen ist es eine gute Idee, den im internen Docker-Netzwerk erzeugten DNS-Namen zu verwenden, damit das Round-Robin-Verfahren des Swarms genutzt werden kann.

nodeJS Quellcode

Das nodeJS-Projekt registrator-nodejs finden Sie demnächst in unserem GibHub-Verzeichnis.

Ein Kommentar

  1. Albert

    Eine viel einfachere Variante Service Discovery zu betreiben, wäre UDP Broadcasting/Multicasting zu arbeiten. Dies setzt voraus, dass eine Single-Host Docker-Container Lösung eingesetzt wird.
    Da die wenigsten Unternehmen oder Startups eine Infrastruktur wie Netflix, Spotify, etc. im Einsatz haben ist dieser Ansatz leichtgewichtiger, da weniger bzw. keine Abhängigkeiten im Spiel sind und man mit einfachen Werkzeugen zum Ziel kommt.

    Die UDP Service Discovery kann man um Docker Container Monitoring erweitern. Damit ist gemeint, dass man erst dann auf UDP Pakete eines bestimmten Topics anfängt zu hören, wenn der Container gestartet wurde, der als UDP-Sender fungiert.

    Alles NPM Module, die dafür benötigt werden, sind: udp-service-discovery und node-docker-monitor.

Kommentar schreiben

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.