#JAVAPRO #Container #Mesos

Cluster-Infrastrukturen sind heute sehr viel komplexer als noch vor 10 Jahren. Microservices, Container und Big-Data-Anwendungen stellen hohe Anforderungen an die Infrastruktur. Container allein reichen jedoch nicht aus. Mit Container-Orchestrierungs-Systemen wie DC/OS auf Basis von Mesos lassen sich Container managen, überwachen, warten, Prozesse automatisieren und ganze Cluster ausfallsicher betreiben.

Neue Microservices-Architekturen, Container und Big-Data-Anwendungen haben auch die Anforderungen und den Umgang mit der Cluster-Infrastruktur verändert. Die Anzahl und oft dynamische Skalierung dieser Services macht eine statische und manuelle Zuweisung auf einzelne Rechner im Cluster nicht praktikabel. Daher rücken Cluster- und Container-Management-Systeme in den Fokus von Entwicklern und Administratoren. Ziele hierbei sind häufig automatisches Deployment, Skalierung von Anwendungen, ein schnelleres Feedback für Entwickler sowie außerdem eine bessere Ressourcen-Auslastung im Cluster. Weiterhin ist eine höhere Fehlertoleranz und Flexibilität gegenüber Lastspitzen zu beobachten, da Container dynamisch auf Servern verteilt werden können (Abb. 1).

Verbesserung der Ressourcen-Auslastung durch Multiplexing. (Abb. 1)

Verbesserung der Ressourcen-Auslastung durch Multiplexing. (Abb. 1)

Cluster-Infrastrukturen sind heute viel komplexer

Die Welt vor 10 Jahren war, zumindest aus der Sicht der meisten Cluster-Infrastrukturen, noch einfach: Es gab häufig nur wenige verteilte Anwendungen. Daher war es zum Beispiel nicht unüblich einen dedizierten Hadoop-Cluster zu betreiben sowie einen dedizierten Cluster für eigene Web-Anwendung. Die heutige Welt sieht da schon etwas komplexer aus. Wir haben eine wachsende Anzahl an Big-Data-Anwendungen, wie zum Beispiel Apache Spark, Apache Cassandra, Apache Flink, die alle gerne eine große Anzahl an Cluster-Ressourcen für sich beanspruchen würden. Außerdem ist da noch der Trend zu Microservices, die es kleinen Teams erlauben ihren Teil des Systems als unabhängigen Service zu entwickeln. Dies führt häufig zu einer höheren Entwicklungsgeschwindigkeit, da Teams unabhängig voneinander entwickeln, testen und releasen können. Es bedeutet aber auch, dass jetzt viele kleine Microservices in der Infrastruktur deployt, skaliert und verwaltet werden müssen. Auch verstärkt dieser Trend das Problem der Abhängigkeitsverwaltung, denn häufig haben die verschiedenen Microservices unterschiedliche Abhängigkeiten, zum Beispiel unterschiedliche JRE-Versionen oder verschiedene Python-Bibliotheken.

Container allein reichen nicht aus

Um diesem Problem Herr zu werden, haben sich Container durchgesetzt, die es erlauben Anwendungen samt ihren Abhängigkeiten in ein Paket zu verpacken – dem Container. Dieser kann schnell, unabhängig und effizient deployt werden. Container Runtimes, wie zum Beispiel Docker, erlauben daher eine Anwendung auf dem eigenen Laptop zu entwickeln, diese dann in einen Container zu verpacken und den Container dann auf einer völlig anderen Test- oder Produktivumgebung zu starten. Insbesondere Java-Entwickler sollten hier den einen oder anderen Fallstrick bei der Kombination von Java und Containern beachten. Es ist zum Beispiel so, dass der maximale Speicherverbrauch vom Kernel hart begrenzt wird. Wenn ein Container zu viel Speicher verbraucht, wird er einfach vom Kernel gestoppt und nicht, wie man es als Java-Entwickler gewohnt ist, mit einem java.lang.OutOfMemoryError beendet. Da die JRE außerhalb des Heap-Memory noch weiteren Speicher benötigt, zum Beispiel für kompilieren, Thread-Verwaltung oder NIO, empfiehlt es sich die Speicherbegrenzung des Containers auf mindestens das eineinhalbfache der maximalen Heap-Größe zu setzen. Eine weitere Herausforderung besteht darin, dass die JRE viele Standardwerte wie zum Beispiel die Anzahl der Garbage-Collection-Threads abhängig von der Anzahl der Prozessoren setzt und je nach JRE-Version nicht die Umstände des Containers beachtet. Daher kann es passieren, dass eine Java-Anwendung, die einwandfrei auf einem Entwicklungs- oder Testsystem läuft, plötzlich auf einem 32-Core-Produktivsystem nur noch mit Umschalten zwischen verschiedenen Threads beschäftigt ist, weil unter falschen Annahmen operiert wird.

Die Anforderungen an einen Container sind vielseitig

Von einem einzelnen Container, der auf einem beliebigen System gestartet werden kann, ist es noch ein weiter Weg bis zu einem Produktivsystem, in dem viele Microservices produktiv betrieben und skaliert werden können. So hätten wir in Produktionsszenarien zum Beispiel gerne, dass unser Container automatisch nach Fehlern oder sogar dem gesamten Ausfall eines Rechners neu gestartet wird. Zudem müssen unsere verschiedenen Microservices, die ja jetzt in Containern auf verschiedenen Servern laufen, irgendwie miteinander kommunizieren können. Die großen Herausforderungen in diesen hochdynamischen Umgebungen sind die Platzierung von Containern, die Verwaltung von Ressourcen-Limitierung und die Verwaltung der Container. Eine Übersicht über die verschiedenen Herausforderungen gibt Tabelle 1.

 

 SchedulingRessourcenverwaltungContainerverwaltung
  • Platzierung der Container auf Server
  • Skalierung der Anzahl der Container
  • Automatisches Neustarten im Fall von Fehlern
  •  Upgrades/Downgrades (ohne den Nutzer zu beeinflussen)
  • Memory
  • CPU
  • GPU
  • Volumes
  • Ports
  • IPs
  • Labels
  • Gruppen
  • Abhängigkeiten
  • Load-Balancing
  • Service-Discovery
  • Health-Checks
Anforderungen an Container und an die Container-Orchestrierung. (Tabelle 1)

 

Container-Orchestrierungs-Systeme lösen das Problem

Hierzu kommt jetzt ein Container-Orchestrierungs-System wie zum Beispiel DC/OS ins Spiel. DC/OS steht für Datacenter Operating System und ist ein Open Source Projekt, welches um den Cluster-Manager Apache Mesos und das Container-Orchestrierungs- Tool Marathon gebaut ist. DC/OS ermöglicht das Verwalten von Containern und Anwendungen. Zudem erlaubt es DC/OS dem Benutzer, einfach Container zu starten und bietet zahlreiche Konfigurationsoptionen, wie beispielsweise Ressourcen-Limitierung, Anzahl der laufenden Instanzen, Health-Checks oder Upgrade-Strategien. Weiterhin ist es möglich zu definieren, wie die Container im Cluster verteilt werden sollten, dass Container Abhängigkeiten zueinander besitzen oder, dass Container bestimmte Netzwerkkonfigurationen beinhalten sollen. Man kann aber auch definieren, dass pro Agent z.B. nur ein Container laufen darf oder, dass alle Container auf einem Agenten mit einem bestimmten Kriterium laufen müssen.

Container-Überwachung durch Health-Checks

Weiterhin ist es möglich Health-Checks zu definieren. Ein Health-Check kann als HTTP-Endpunkt, TCP-Check oder in Form von Shell-Skripts innerhalb des Containers definiert sein. Diese Health-Checks werden periodisch ausgeführt. Sollte ein Health-Check öfter als erlaubt nicht bestanden werden, wird Marathon die Instanz erneut starten.

DC/OS besteht aus über 30 Projekten

DC/OS ist allerdings mehr als Mesos und Marathon. DC/OS ist eine Bündelung von mehr als 30 Open-Source-Projekten zu einer Distribution mit gemeinsamer Roadmap, Dokumentation, Security-Konzept, Benutzeroberfläche, Tutorials und Design. Die enthaltenen Komponenten sind aufeinander abgestimmt und stellen eine Sammlung von Best-Practices vieler Installationen dar. DC/OS wird als vorgefertigte Distribution für CentOS, RHEL und CoreOS sowie als lokale Variante und für diverse Cloud Provider bereitgestellt. Unter https://dcos.io/install/ kann eine vollständige Liste der Installationsmöglichkeiten eingesehen werden.
Mittels AWS (Amazon Webservices) Cloudformation-Templates ist es möglich, ein vollwertiges DC/OS-Cluster mit wenigen Klicks zu installieren und in Betrieb zu nehmen.

Im Dashboard laufen alle Informationen zusammen

Der Einstiegspunkt nach dem Login ist das DC/OS-Dashboard. (Abb. 2) zeigt das Dashboard und es wird eine aggregierte Ansicht des Clusters dargestellt. Es werden alle Informationen bereitgestellt um eine qualifizierte Aussage über den Zustand des Clusters zu treffen, nämlich die aggregierte Ressourcenauslastung (CPU, Arbeitsspeicher und Festplattenplatz), wie viele Anwendungen laufen und ob alle laufenden Anwendungen die konfigurierten Health-Checks bestehen. Die Information, wie viele physische oder virtuelle Server in diesem Cluster vorhanden sind, ist nur ein Detail und eigentlich auch nur interessant, wenn einer der anderen Parameter nah an der maximalen Auslastung ist.

Im DC/OS-Dashboard laufen alle Informationen zusammen. (Abb. 2)

Im DC/OS-Dashboard laufen alle Informationen zusammen. (Abb. 2)

Container per Skript erzeugen und deployen

Unter dem Services-Bereich verbirgt sich Marathon. Marathon ermöglicht es, einen einzelnen Container oder eine Gruppe von Containern mittels JSON (JavaScript Object Notation) zu beschreiben und zu deployen. (Listing 1) zeigt eine gekürzte Konfiguration, welche eine Servicelandschaft beschreibt, die aus drei Java-Anwendungen, einer JavaScript-Anwendung und einem internen Proxy besteht. Der vollständige Code ist auf GitHub veröffentlicht. Diese Konfiguration beschreibt viele der bisher vorgestellten Konzepte. So ist zum Beispiel eine Abhängigkeit vom Checkout-Service auf den Basket-Service definiert und alle beschriebenen Container haben Ressourcen-Limitierung in Bezug auf CPU, Arbeitsspeicher, Festplatte und Ports. Service-Discovery wird am Beispiel einer Virtuellen IP (VIP) gezeigt. Die Definition einer VIP hat die Publizierung eines DNS-Eintrags zur Folge, welcher über einem Load-Balancer verfügbar gemacht wird. Sollte eine Anwendung also skaliert werden, beispielhaft in (Abb. 3) gezeigt, dann werden alle Requests mittels des Load-Balancers an die laufenden Anwendungen verteilt.

Skalierung. (Abb. 3)

Skalierung. (Abb. 3)

Weiterhin werden Health-Checks und verfügbare Umgebungsvariablen definiert, so wie das Verhalten im Falle eines Upgrades, welches hier ein Rolling-Upgrade ist. Ein klassisches Blau/Grün-Upgrade wäre ebenfalls möglich. Für Anwendungen die in verschiedenen Umgebungen laufen, zum Beispiel lokal und produktiv, ist es sehr wichtig umliegende Anwendungen und sonstige Parameter extern konfigurieren zu können, damit Container auch wirklich portabel sind. In diesem Beispiel werden die Host-Namen der anderen Anwendungen per Umgebungsvariablen konfiguriert.

 

(Listing 1) – Konfiguration
{
   "id":"brewery",
   "apps":[
      {
         "id":"articleservice",
         "cpus":0.5,
         "mem":256,
         "instances":1,
         "container":{
            "type":"DOCKER",
            "docker":{
               "image":"unterstein/dcos-microservices-article",
               "network":"BRIDGE",
               "portMappings":[
                  {
                     "hostPort":0,
                     "containerPort":8081,
                     "protocol":"tcp",
                     "labels":{
                        "VIP_0":"articleservice:8081"
                     }
                  }
               ]
            }
         },
         "upgradeStrategy":{
            "minimumHealthCapacity":0.85,
            "maximumOverCapacity":0.15
         },
         "healthChecks":[
            {
               "protocol":"HTTP",
               "path":"/articles/",
               "portIndex":0,
               "timeoutSeconds":10,
               "gracePeriodSeconds":10,
               "intervalSeconds":2,
               "maxConsecutiveFailures":10
            }
         ]
      },
      {
         "id":"basketservice",
         "cpus":0.5,
         "mem":256,
         "instances":1,
         "container":{
            "type":"DOCKER",
            "docker":{
               "image":"unterstein/dcos-microservices-basket",
               "network":"BRIDGE",
               "portMappings":[
                  {
                     "hostPort":0,
                     "containerPort":8082,
                     "protocol":"tcp",
                     "labels":{
                        "VIP_0":"basketservice:8082"
                     }
                  }
               ]
            }
         },
         ...
      },
      {
         "id":"checkoutservice",
         "dependencies":[
            "/brewery/articleservice",
            "/brewery/basketservice"
         ],
         "cpus":1,
         "mem":256,
         "instances":1,
         "container":{
            "type":"DOCKER",
            "docker":{
               "image":"unterstein/dcos-microservices-checkout",
               "network":"BRIDGE",
               "portMappings":[
                  {
                     "hostPort":0,
                     "containerPort":8083,
                     "protocol":"tcp",
                     "labels":{
                        "VIP_0":"checkoutservice:8083"
                     }
                  }
               ]
            }
         },
         ...
         "env":{
            "HOST_ARTICLESERVICE":
                "articleservice.marathon.l4lb.thisdcos.directory",
            "HOST_BASKETSERVICE":
                "basketservice.marathon.l4lb.thisdcos.directory"
         }
      },
      {
         "id":"webservice",
         "cpus":0.1,
         "mem":256,
         "instances":1,
         "container":{
            "type":"DOCKER",
            "docker":{
               "image":"unterstein/dcos-microservices-web",
               "network":"BRIDGE",
               "portMappings":[
                  {
                     "hostPort":0,
                     "containerPort":80,
                     "protocol":"tcp",
                     "labels":{
                        "VIP_0":"webservice:80"
                     }
                  }
               ]
            }
         },
         ...
      },
      {
         "id":"proxy",
         "dependencies":[
            "/brewery/articleservice",
            "/brewery/basketservice",
            "/brewery/checkoutservice",
            "/brewery/webservice"
         ],
         "cpus":0.1,
         "mem":256,
         "instances":1,
         "container":{
            "type":"DOCKER",
            "docker":{
               "image":"unterstein/dcos-microservices-proxy",
               "network":"BRIDGE",
               "portMappings":[
                  {
                     "hostPort":0,
                     "containerPort":80,
                     "protocol":"tcp"
                  }
               ]
            }
         },
         "env":{
            "HOST_ARTICLESERVICE":
                "articleservice.marathon.l4lb.thisdcos.directory",
            "HOST_BASKETSERVICE":
                "basketservice.marathon.l4lb.thisdcos.directory",
            "HOST_CHECKOUTSERVICE":
                "checkoutservice.marathon.l4lb.thisdcos.directory",
            "HOST_WEBSERVICE":
                "webservice.marathon.l4lb.thisdcos.directory"
         },
         ...
      }
   ]
}

Ausfallsicherheit erfordert die Berücksichtigung zahlreicher Konzepte

Nachdem wir unsere Applikation erfolgreich gestartet haben, kommt in Produktions-Szenarien aber noch ein zentraler und wichtiger Punkt hinzu: Wie können wir den Service ohne Unterbrechung verfügbar halten und das trotz Cluster-Upgrades (inklusive DC/OS-Upgrades), neuen Anwendungs-Updates und Server-Ausfällen in unserem Cluster? Die Aufgaben, auch manchmal als Day2-Operations bezeichnet, lassen sich grob in die Kategorien Maintenance, Debugging, Metriken und Logging einteilen. Maintenance umfasst Themen die mit der Wartung der Applikationen und des eigentlichen Clusters zusammenhängen. Zum Beispiel das Deployment von neuen Versionen der Applikation oder auch ganzen Cluster Upgrades, während die Anwendung selbst für die Nutzer verfügbar bleibt. Debugging beschäftigt sich mit der Identifikation und Lösung von Problemen. Da jetzt Container auf beliebigen Servern laufen, brauchen Nutzer einen einfachen Weg diese Container unabhängig von ihrem Ausführungsort inspizieren zu können. Metriken sind wichtig um sowohl die Auslastung des Clusters zu messen, als auch um potentielle Probleme frühzeitig zu erkennen. Quellen für Metriken umfassen das System (zum Beispiel DC/OS), die Container und natürlich auch die Applikation selbst. Logging ist sowohl für das Debuggen von Problemen wichtig, als auch um später bei einem Audit feststellen zu können, wer welche Änderungen vorgenommen hat. Bei all diesen Themen ist wichtig, dass diese schon bei der Konzeption eines Systems berücksichtigt werden sollten, anstatt diese erste später hinzuzufügen.

Dr. Jörg Schad

Dr. Jörg Schad ist Distributed Systems Engineer bei Mesosphere in Hamburg und arbeitet an Apache Mesos und DC/OS. In seinem früheren Leben entwickelte er verteilte und In-Memory-Datenbanken bzw. forschte im Hadoop- und Cloud-Umfeld.

Johannes Unterstein

Johannes Unterstein organisiert die Java User Group in Kassel, lehrt an der DHBW Stuttgart und arbeitet als Distributed Applications Engineer bei Mesosphere. Früher hat er in Projekten für DAX30-Unternehmen gearbeitet und die Welt der Online-Wahlen modernisiert.

(Visited 70 times, 1 visits today)

Leave a Reply