Orchestrierung & Verwaltung

Docker Container fest im Griff: DC/OS für Java-Entwickler

Johannes Unterstein

©Shutterstock.com / Sorapop-Udomsri

In Zeiten von Docker, Big Data und Microservices wird es immer wichtiger, verteilte Anwendung sinnvoll und dynamisch auf Cluster verteilen zu können und dabei trotzdem noch den Überblick zu behalten. Daher werden Cluster-Managementsysteme wie Apache Mesos und DC/OS immer wichtiger. Dabei geht es nicht nur um die Orchestrierung von Containern, sondern auch um die Verwaltung von persistenten Daten, die fehlertolerante Auslegung der Anwendungslandschaft und die optimale Auslastung der Ressourcen im Cluster.

Schaut man sich die Ressourcenauslastung vieler aktueller Cluster an, stellt man mit Schrecken fest, dass diese sich häufig im Bereich von zehn bis fünfzehn Prozent befindet. Das ist nicht verwunderlich, da viele Anwendungen darauf ausgelegt sind, auf ganz bestimmten Knoten des Clusters zu laufen. Und natürlich müssen auch Lastspitzen abgedeckt werden. Da meist nur eine Anwendung pro Knoten läuft und ihn dadurch charakterisiert, spricht man auch von einer statischen Partitionierung des Clusters. Ein dynamisches Verteilen freier Ressourcen, auch wenn Knoten ausfallen, findet nur in seltenen Fällen statt. Häufig kann man beobachten, dass bestimmte Anwendungen Lastspitzen zu einer Tageszeit haben und andere Anwendungen wiederum zu einer anderen. Da aber beide Anwendungen separat betrachtet und freie Ressourcen nicht dynamisch verteilt werden können, müssen wir beide Anwendungen separat bezüglich ihrer Lastspitzen optimieren.

An genau diesem Punkt kommen Mesos und DC/OS ins Spiel. DC/OS, das Datacenter Operating System, ist eine Plattform für den Betrieb von Microservices, traditionellen Datenbanken sowie Big- und Fast-Data-Anwendungen. Es baut auf Mesos auf, das genutzt wird, um beispielsweise Twitter oder Apple Siri zu betreiben und dynamisch skalieren zu können (Abb. 1).

Mesos: Abstraktion weg vom Cluster

Mesos abstrahiert den Fakt, dass man mit einem Cluster arbeitet, und lässt es sich anfühlen, als würde man mit einem einzigen Server interagieren. Das Ziel dabei ist es, die im Cluster verfügbaren Ressourcen auf alle Anwendungen zu verteilen und so dynamisch auf Anforderungen reagieren zu können. Auch die Auslastung der Knoten lässt sich so erhöhen.

Abb. 1: Ressourcenauslastung traditionell vs. DC/OS

Abb. 1: Ressourcenauslastung traditionell vs. DC/OS

Mesos implementiert eine Scheduling-Architektur auf zwei Ebenen. Es verfolgt das Ziel, freie Ressourcen, wie CPU, Arbeitsspeicher (Memory) oder Festplattenspeicher (Disk), fair den verschiedenen Schedulern anzubieten, Anwendungen auf diesen Angeboten zu starten und den Zustand dieser Anwendungen zu überwachen und zu berichten. Mesos selbst besteht dabei aus zwei Arten von Knoten: Mesos Master und Mesos Agents. Die Mesos Master verwalten den Zustand des Clusters und sind rein als Managementknoten tätig, wohingegen Mesos Agents rein für die Ausführung von Anwendungen zuständig sind. Mesos Master sollten hochverfügbar ausgelegt sein. Hierbei wird der führende Master-Knoten über ein ZooKeeper Quorum bestimmt. Die anderen Master-Knoten laufen passiv. Im Fall eines Ausfalls wird ein neuer Master als der führende gewählt.

Diese Aufteilung zwischen Verteilung der Ressourcen und Entscheidung über das Scheduling ermöglicht große Flexibilität beim Betrieb von Anwendungen. Diese erlaubt es, unterschiedliche Anwendungen auf Mesos zu betreiben, da das Verhalten in bestimmten Szenarien sehr unterschiedlich sein kann. Der Scheduler entscheidet dabei nicht nur, ob mit einem Angebot eine Instanz einer Anwendung gestartet werden soll. Er wird auch informiert, wenn eine Instanz beendet wird, fehlschlägt oder unerreichbar ist. Als Beispiel für solche Scheduler seien an dieser Stelle Apache Myriad (Hadoop), Apache Spark, Apache Flink, Apache Cassandra, Apache Kafka oder Marathon genannt.

Marathon ist an der Stelle interessant, da es sich um einen generischen Scheduler zur Containerorchestrierung handelt. Marathon ermöglicht es dem Benutzer, einfach Container zu starten, und bietet zahlreiche Konfigurationsoptionen, wie Ressourcenlimitierung, Anzahl der laufenden Instanzen, Health-Checks oder Upgradestrategien. Weiterhin ist es möglich, zu definieren, wie die Container im Cluster verteilt werden sollen, dass Container Abhängigkeiten zueinander besitzen oder bestimmte Netzwerkkonfigurationen enthalten sollen. Man kann aber auch definieren, dass pro Agent z. B. nur ein Container laufen darf oder alle Container auf einem Agenten mit einem bestimmten Kriterium laufen müssen. Weiterhin ist es möglich, Health-Checks zu definieren. Ein Health-Check kann als HTTP-Endpunkt, TCP-Check oder Shell-Skript innerhalb des Containers definiert sein. Diese Health-Checks werden periodisch ausgeführt. Sollte ein Health-Check öfter als erlaubt scheitern, wird Marathon die Instanz erneut starten.

Was ist jetzt DC/OS genau?

DC/OS nutzt sowohl Mesos als Kernel als auch Marathon als Container-Scheduler. DC/OS ist allerdings mehr als Mesos und Marathon. Es ist eine Bündelung von mehr als dreißig Open-Source-Projekten mit einer gemeinsamen Distribution, Roadmap, Dokumentation, Securitykonzept, Benutzeroberfläche, Design und Tutorials. Die enthaltenen Komponenten sind aufeinander abgestimmt und bilden so eine Sammlung von Best Practices vieler Installationen. DC/OS wird als vorgefertigte Distribution für CentOS, RHEL und CoreOS bereitgestellt. Weiterhin gibt es Templates für Amazon CloudFormation und Microsoft Azure. Für die lokale Testinstallation wird eine Installation über Vagrant oder Docker angeboten. Eine vollständige Dokumentation der Installationsmöglichkeiten ist online zu finden.

In einer Welt mit vielen kollaborierenden Services ist es hilfreich, wenn die Services in einem Netzwerk miteinander kommunizieren, sich gegenseitig finden und die Last auf alle beteiligten Services verteilt wird. Daher sind in DC/OS Komponenten für DNS, Virtual IP, Port Mappings und Overlay Networks enthalten. Overlay Networks sind an dieser Stelle interessant, da in dem virtuellen Netzwerk jeder Container eine eigene IP-Adresse erhält und alle Ports exponiert. Für manche Anwendungsfälle kann es vorteilhaft sein, direkt mit Containern zu kommunizieren. In anderen Anwendungsfällen ist es besser, über einen Load Balancer mit einer Anwendung zu reden. Da alle in DC/OS enthaltenen Load Balancer Health-Checks respektieren, ist sichergestellt, dass nur Instanzen antworten, denen es auch gut geht. Das führt im Umkehrschluss dazu, dass Last von einer Instanz genommen wird, wenn es ihr nicht gut geht. Als weiteres Netzwerk- und Securityfeature wird bei Mesos Agents in DC/OS zwischen privaten und öffentlichen Agenten unterschieden. Nur öffentliche Agenten sind üblicherweise ins Internet exponiert, private nur mittels privater IPs zugänglich und hinter Firewalls verborgen. Das sorgt zum einen für Zugriffsschutz der privaten Agenten und zum anderen für eine unabhängige Skalierbarkeit zwischen privaten und öffentlichen Agenten.

DevOps Docker Camp 2017

Das neue DevOps Docker Camp – mit Erkan Yanar

Lernen Sie die Konzepte von Docker und die darauf aufbauende Infrastrukturen umfassend kennen. Bauen Sie Schritt für Schritt eine eigene Infrastruktur für und mit Docker auf!

Anwendungen mit Zustand handhaben

Da wahrscheinlich nicht alle Dienste im Cluster zustandslos sind, muss man sich in dieser stark verteilten Welt leider auch mit dem Thema persistente Daten auseinandersetzen. Selbst wenn man eine Datenbank startet und mittels einer virtuellen IP im Cluster verfügbar gemacht hat, kann man nicht sicher sein, dass es dieser laufenden Datenbankinstanz für immer gut geht und die enthaltenen Daten auch für immer verfügbar sind. Anwendungen lassen sich, je nachdem, wie tolerant sie mit persistenten Daten umgehen können, in drei Kategorien einteilen.

Zum einen gibt es zustandslose Dienste, die keine wichtigen persistenten Daten produzieren und daher keine besondere Behandlung benötigen. Ein Beispiel für diese Kategorie wäre ein Webserver. Daten werden weder einen fehlgeschlagenen Task noch einen fehlgeschlagenen Agenten überstehen. Zum anderen gibt es Anwendungen, die für verteilte Systeme entwickelt wurden und über eine eingebaute Replikation verfügen. Für diese Art lassen sich sogenannte lokale persistente Volumen definieren. Das bedeutet, dass im Fehlerfall versucht wird, den Ersatzcontainer auf genau dem gleichen Agenten mit genau dem gleichen Volumen zu starten. Die gestartete Anwendung, z. B. Elasticsearch, kann in diesem Szenario selbst entscheiden, ob die Daten weiterverwendet werden können oder schon zu alt sind und eine Replikation nötig ist. So lassen sich teure Replikationen vermeiden. Allerdings nicht, wenn der Agent fehlschlägt. Weiterhin hat diese Variante den Vorteil, dass alle Operationen auf der lokalen Festplatte ausgeführt werden und daher wesentlich performanter sind als die Lösung der folgenden Variante.

Als letzte Kategorie gibt es Anwendungen, die stark von ihren Daten abhängen, selbst aber über keine eingebaute Replikation verfügen. Klassische relationale Datenbanken, die nicht in einem Cluster laufen, sind gute Repräsentanten dieser Kategorie. Ein sinnvoller Lösungsweg wäre, diese Art der Anwendung mit einer eigenen Replikation auszustatten. Ist das allerdings nicht möglich, kann man auf ein verteiltes Dateisystem zurückgreifen. Das macht zwar schreibende Zugriffe erheblich langsamer, dafür aber werden persistente Daten sowohl einen fehlgeschlagenen Container als auch einen fehlgeschlagenen Agenten überleben.

Chuck Norris zeigt, wie es geht

Um die beschrieben Features besser verständlich zu machen, bauen wir eine Beispielanwendung, bestehend aus einer SQL-Datenbank und einer Spring-Boot-Webanwendung. Der vollständige Quelltext und weiterführende Anleitungen sind auf GitHub zu finden. Basierend auf der gleichen Demo sind in dem GitHub Repository auch Folien und eine Aufnahme des Vortrags von der letzten W-JAX verlinkt. Die Beispielanwendung dreht sich um Chuck-Norris-Witze. Die Anwendung bietet eine HTTP-Schnittstelle, um einen zufälligen Chuck-Norris-Witz aus der Datenbank zu laden, mit Metainformationen zu kombinieren und als JSON auszuliefern. Eine Antwort von GET / sieht zum Beispiel folgendermaßen aus:

{
  "hostAddress": "172.17.0.7",
  "locale": "de"
  "nodeId": "b6491fae-70b2-4c58-978e-4b8a849da22b",
  "joke": "Chuck Norris bringt das Sandmännchen ins Bett."
}

Die Anwendung verfügt über zwei weitere Endpunkte, die sich um das Thema Health-Checks drehen. GET /ping liefert im Normalfall pong zurück und wird später für Health-Checks in Marathon verwendet. PUT /ping hingegen ändert das Verhalten von GET /ping und führt dazu, dass GET /ping ein HTTP 500 zurückliefert.

Da die Anwendung mehrere Phasen in der Entwicklung durchläuft, muss sie an ihre Umgebung angepasst werden. So wird die Anwendungen in der lokalen Entwicklung anders konfiguriert als wenn sie z. B. in Docker Compose läuft oder später in Produktion in DC/OS deployt wird. Die verwendete Datenbank ist beispielsweise während der lokalen Entwicklung unter einer anderen Adresse zu erreichen als in der Produktion. Daher sollte die Anwendung die Information über die Adresse aus einer Umgebungsvariablen bekommen. Da die Anwendung mit Spring Boot implementiert ist, kann direkt von der Annotation @Value profitiert werden, die mit Standardwerten umgehen kann und Konfigurationen sowohl aus Properties-Dateien als auch aus JVM-Argumenten und aus Umgebungsvariablen auslesen kann.

Konfiguration mit Marathon

In der Marathon-Konfiguration wird die Definition einer Anwendungsgruppe gezeigt (Listing 1). Da eine Datenbank und ein Java-Service deployt werden sollen, ist es sinnvoll, dies gemeinsam zu machen und die Anwendungen in einer Gruppe zu bündeln. Mithilfe von Abhängigkeiten ist es möglich, dass der Java-Service erst deployt wird, wenn die Datenbank erfolgreich gestartet wurde und die Health-Checks passiert hat. Neben den benötigten Ressourcen (CPU, Memory, Disk) werden für beide Anwendungen Health-Checks und die zu verwendenden Container definiert. In der Containerdefinition der Datenbank wird eine virtuelle IP konfiguriert, die mittels der Umgebungsvariablen an den Java-Service übergeben wird. Der Java-Service hingegen definiert nur, dass ein wahlfreier Port auf dem Agenten über ein gebrücktes Netzwerk auf Port 8080 innerhalb des Containers zeigt.

Durch die Definition der Label beim Java-Service wird dem externen HA-Proxy mitgeteilt, dass er diese Anwendung beachten soll. Zu guter Letzt sei noch die Upgradestrategie beim Java-Service genannt. Die hier angegebene Konfiguration definiert, dass es im Fall eines Upgrades der Anwendung eine maximale Überkapazität der laufenden Instanzen von 15 Prozent geben darf, jedoch eine minimale Health-Kapazität von 85 Prozent vorhanden sein muss. Wenn beispielsweise zwanzig Instanzen laufen und ein Update eintritt, werden zunächst drei neue Instanzen gestartet. Dann wird das Passieren der Health-Checks abgewartet und erst dann werden drei alte Instanzen beendet. Dieses Spiel wiederholt sich so lange, bis alle laufenden Instanzen aktualisiert wurden. Dieses Vorgehen nennt sich rollende Upgrades. Es gibt allerdings auch Lösungen für Blau/Grün-Upgrades und Kanarienupgrades.

{
  "id":"chuck-jokes",
  "apps":[
    {
      "id":"service",
      "dependencies":[
        "/chuck-jokes/database"
      ],
      "cpus":0.25,
      "mem":1024,
      "instances":2,
      "healthChecks":[
        {
           "protocol":"HTTP",
           "path":"/health",
           "portIndex":0,
           "timeoutSeconds":10,
           "gracePeriodSeconds":10,
           "intervalSeconds":2,
           "maxConsecutiveFailures":10
        }
      ],
      "container":{
        "type":"DOCKER",
        "docker":{
          "image":"unterstein/dcos-for-java-devs-service:latest",
          "network":"BRIDGE",
          "portMappings":[
            {
              "hostPort":0,
              "containerPort":8080,
              "protocol":"tcp"
            }
          ]
        }
      },
      "env":{
        "MYSQL_URL":"jdbc:mysql://3.3.0.6:3306/chuck?user=chuck&password=norris",
      },
      "upgradeStrategy":{
        "minimumHealthCapacity":0.85,
        "maximumOverCapacity":0.15
      },
      "labels":{
        "HAPROXY_0_VHOST":"your.domain.com",
        "HAPROXY_GROUP":"external"
      }
    },
    {
      "id":"database",
      "cpus":1,
      "mem":1024,
      "instances":1,
      "container":{
        "type":"DOCKER",
        "docker":{
          "image":"unterstein/dcos-for-java-devs-database:latest",
          "network":"BRIDGE",
          "portMappings":[
            {
              "hostPort":0,
              "containerPort":3306,
              "protocol":"tcp",
              "labels":{
                "VIP_0":"3.3.0.6:3306"
              }
            }
          ]
        }
      },
      "healthChecks":[
        {
          "protocol":"TCP",
          "portIndex":0,
          "gracePeriodSeconds":300,
          "intervalSeconds":60,
          "timeoutSeconds":20,
          "maxConsecutiveFailures":3,
          "ignoreHttp1xx":false
        }
      ]
    }
  ]
}

Mehrere Wege für das Deployment

Um die Anwendungen zu deployen, gibt es verschiedene Wege. Marathon verfügt über ein HTTP-API, das das UI, das CLI und das Maven-Plug-in verwendet. Wenn man aber an die Automatisierung des Deployments denkt, z. B. im Zuge einer Continuous-Delivery-Pipeline, könnte die direkte Anbindung des HTTP-API durchaus die erste Wahl sein. Alternativ kann man mithilfe des DC/OS-Maven-Plug-ins direkt aus dem Build-System heraus ein Deployment starten. Hier sei angemerkt, dass im Zusammenspiel mit dem Docker-Maven-Plug-in folgender Befehl dazu führt, dass erst das Java-Projekt und dann das Docker Image gebaut wird, um es danach zu Docker Hub zu transferieren und es dann in DC/OS zu deployen:

mvn clean package docker:build docker:push dcos:deploy

Selbstverständlich gibt es ein CLI, das alle Features von DC/OS reflektiert und anwendungsübergreifend eine konsistente Nutzung ermöglicht. Das Gleiche gilt für das UI. Es wird versucht, das Cluster als eine Einheit darzustellen, und das spiegelt sich auf dem DC/OS-Dashboard gut wider.

Dashboard

Auf dem Dashboard werden die aggregierten Ressourcen des Clusters dargestellt. Es ist die prozentuale Auslastung von CPU, Memory und Disk zu sehen. Weiterhin kann man auf einen Blick ablesen, ob es Anwendungen gibt, welche die Health-Checks nicht passieren. Die Anzeige, wie viele Agenten im Cluster verfügbar sind, ist nur ein Detail, das zuletzt angezeigt wird. Das Dashboard ist darauf ausgelegt, Fragen zu beantworten, die ein Operator wahrscheinlich hat, wenn er nachts um drei aus dem Bett geklingelt wird, weil etwas im Cluster nicht richtig funktioniert.

Abb. 2: Das DC/OS-Dashboard ist darauf ausgelegt, auch nachts um drei noch übersichtlich zu sein

Abb. 2: Das DC/OS-Dashboard ist darauf ausgelegt, auch nachts um drei noch übersichtlich zu sein

Universe

Das Managementsystem für Pakete innerhalb von DC/OS heißt Universe. Man kann es sich wie eine Art App-Store für Cluster vorstellen. So ist es beispielsweise möglich, mit einem Klick ein Cassandra-, Spark-, neo4j- oder Kafka-Cluster zu installieren. Die hinterlegten Konfigurationen sind mit Standardwerten versehen, können allerdings auch auf die Bedürfnisse der eigenen Situation im Cluster angepasst und während der Installation angegeben werden. In der Chuck-Norris-Anwendung werden wir Marathon-lb aus dem Universe installieren. Marathon-lb ist ein HA-Proxy, der auf einem öffentlichen Agenten installiert wird und seine Proxy-Konfiguration automatisch nach den laufenden Anwendungen von Marathon anpasst. Immer wenn eine Anwendung in Marathon deploy, undeployt wird oder Health-Checks nicht mehr passiert, wird Marathon-lb seine Konfiguration anpassen.

Scaling

Genau wie beim Deployments gibt es auch verschiedene Wege, um ein Scaling auszulösen. Wenn man das CLI verwendet, kann man beispielsweise mit dem Befehl dcos marathon app update /chuck-jokes/service instances=20 die Anzahl der laufenden Java-Services auf 20 hochsetzen. Die gleiche Funktionalität steht auch in der Benutzeroberfläche zur Verfügung.

Abb. 3: Scaling über die DC/OS-Benutzeroberfläche

Abb. 3: Scaling über die DC/OS-Benutzeroberfläche

Health-Checks

Der Pfad /health in dem Chuck-Norris-Service verhält sich anders, wenn dieser Pfad zuvor mit einem HTTP PUT aufgerufen wurde. Das soll zeigen, was mit einem Service geschieht, dem es nicht mehr gut geht und daher die Health-Checks nicht mehr passieren kann. Wenn nun der Health-Check nicht mehr mit einem HTTP 20x beantwortet wird, wird der Service als unhealthy markiert und ihm werden keine Anfragen mehr zugeordnet. Sollte der Health-Check die konfigurierten zehn Fehler in Folge produzieren, wird Marathon diesen Service mit einer anderen Instanz ersetzen.

Logging

In einem verteilten System ist es wichtig, an die Logdateien der einzelnen Container zu gelangen. Vorteilhaft ist es, wenn man sich dafür nicht per SSH mit einem Server verbinden muss. Logs von konkreten laufenden Anwendungen können entweder über die Benutzeroberfläche abgerufen werden oder mittels des CLI, z. B. mit dem Befehl dcos task log /chuck-jokes/service.fb73d64c-e7dd-11e6-b707-2e51bbe38b93. Weiterhin sind die Logdateien auch auf der Festplatte der Mesos Agents zu finden. Das kann beispielsweise dazu verwendet werden, um die Logs mithilfe von Logstash in Elasticsearch zu indizieren und später über ein Kibana-Dashboard zugänglich zu machen. Da in einem Fehlerfall ein Überblick über alle Komponenten und alle laufenden Anwendungen sinnvoll ist, ist es gut, alle Logs an einem Ort zu sammeln und zentral durchsuchbar zu machen.

Lesen Sie auch: Von Linux zu Docker: Die Grundlagen der Container-Technologie

Java in Containern

Wer Java in Containern laufen lässt, hat einige Vorteile. Man kann z. B. unterschiedliche Java-Versionen in verschiedenen Containern nutzen, ohne sich über Abhängigkeiten Gedanken machen zu müssen. Es gibt aber auch einige Fallstricke. Eine Herausforderung ist, die Ressourcenlimitierung sinnvoll festzulegen. Das gilt insbesondere für Memory, da Java auch Memory über den Heap hinaus benutzen kann. Außerhalb von Containern kann man den OutOfMemoryError behandeln, und die Anwendung kann weiterlaufen. Wenn die Anwendung aber die Begrenzungen des Containers überschreitet, wird der Container gestoppt und im Fall von DC/OS automatisch neu gestartet. Ein weiterer Fallstrick sind Parameter, wie die Anzahl der Garbage-Collection-Threads, die standardmäßig auf der Anzahl der verfügbaren CPUs basieren. Ältere JDKs benutzen hierfür die Gesamtzahl der auf dem System vorhandenen CPUs und nicht die im Container verfügbaren CPUs. Das führt zu überraschenden Effekten, wenn eine Maschine 64 Kerne besitzt, dem Container allerdings nur zwei Kerne zugewiesen sind. Einen sehr guten Einblick in die Herausforderungen von Java in Containern gibt Jörg Schad, z. B. in „No one puts Spark in the container„.

Geschrieben von
Johannes Unterstein
Johannes Unterstein
Johannes organisiert die JUG in Kassel, lehrt an der DHBW Stuttgart und arbeitet als Distributed Applications Engineer bei Mesosphere. Er arbeitet an Marathon, dem Containerorchestrierungsframework von DC/OS, um Twitter-like Scaling für jedermann zu ermöglichen. Twitter: @unterstein
Kommentare

Schreibe einen Kommentar

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