Es muss nicht immer Spring Boot sein Schlanke Microservices mit Pistache

Autor / Redakteur: Eduardo Hahn * / Stephan Augsten

Die verbreitetste Implementierung von Microservices, Spring Boot in der Java Virtual Machine (JVM), ist unumstritten. Doch was hat es mit dem Trend, Java-Applikationen als Microservices umzusetzen, auf sich? Dieser Beitrag zeigt das auf und vergleicht JVM mit plausiblen Alternativen. Der Fokus liegt hier auf Pistache.

Firmen zum Thema

Pistache ist ein in modernem C++ geschriebenes Web-Framework, das insbesondere auf Performance ausgelegt ist.
Pistache ist ein in modernem C++ geschriebenes Web-Framework, das insbesondere auf Performance ausgelegt ist.
(Bild: HOerwin56 / Pixabay )

Der in den vergangenen Jahren entstandene Trend, Java-Applikationen für das Internet als Microservices umzusetzen oder bestehende Monolithen in solche zu migrieren, wird in letzter Zeit immer häufiger hinterfragt. Dafür gibt es eine Vielzahl von Gründen.

Hierzu zählen beispielsweise die in Anspruch genommenen Hardware-Ressourcen oder die resultierende Performance. Interessant sind nebst den Antwortzeiten auch die Startzeit der Applikation und deren Ressourcenverbrauch. Dabei liegt der Fokus auf dem Betrieb orchestrierter Container in einer Cloud.

Die Kommodität für den Entwickler bei der Erstellung der Software sowie die damit verbundene „Time to Market“-Geschwindigkeit stehen dem ökologischen und ökonomischen Fußabdruck des Betriebs gegenüber. Nicht zu unterschätzen ist, dass auch bei der Software-Entwicklung ein Beitrag zum nachhaltigen Umgang mit der Natur und ihren Ressourcen geleistet werden kann.

Ursprung von Java, Portabilität heute und State of the Art

Die Programmiersprache Java wurde einst mit dem Fokus auf Portabilität entwickelt. Der Marketing-Slogan „Write once, run anywhere“ von Sun Microsystems aus dem Jahre 1995 verdeutlicht dies. Diese Portabilität ist in der Cloud nicht mehr notwendig, da der Container das Betriebssystem und die Prozessorarchitektur vorgibt.

Die Kompilierung zu portablem ByteCode für die JVM erzeugt einen unnötig gewordenen Mehraufwand. Mittels GraalVM können native Binaries erzeugt werden, was dieses Problem zur Laufzeit entschärft. Was noch übrig bleibt, sind die langen Build-Zeiten und die Auswirkungen der Garbage Collection. Diese letzte Hürde lässt sich nicht so leicht lösen, da das Konzept der automatischen Speicherverwaltung stark mit der Sprache verbunden ist. Die meisten Java-Entwickler würden darauf aber kaum verzichten wollen.

Will man den Optimierungsgedanken jedoch weiterverfolgen, bietet sich eine Implementierung der Anwendung in einer Sprache wie C++ an. Die Garbage Collection und die Virtual Machine entfallen hier komplett. Die Verantwortung der Speicherverwaltung liegt dann beim Entwickler. Dank einer modernen Herangehensweise und den Möglichkeiten, welche aktuelle C++ Standards bieten, ist dies jedoch unproblematisch.

Und jetzt? Verschiedene Möglichkeiten im Vergleich

Es gibt zahlreiche, vielversprechende Bibliotheken, die in Frage kommen und getestet wurden. Das C++ REST SDK von Microsoft schien zunächst eine interessante Option zu sein, da die Bibliothek unter der MIT-Lizenz herausgegeben wird und in vielen Linux-Distributionen über zahlreiche, offzielle Linux-Repositories bezogen werden kann.

Wie sich jedoch herausgestellt hat, weist die Bibliothek eine teils problematische Performance bei der JSON-Generierung auf. Es existieren dazu unabhängige Benchmarks, welche dies deutlich aufzeigen. Der Benchmark zeigt, dass im Vergleich zwischen C++ REST SDK, restbed und Pistache, letztere Bibliothek die besten Ergebnisse aufweist. Andere, hier nicht geprüfte Alternativen wären Beast, Drogon, oat++ oder das teils kostenpflichtige POCO.

Pistache wird gegenwärtig aktiv gepflegt und lässt sich entweder aus den Quellen bauen oder im Falle von Ubuntu über ein vom Entwickler-Team gepflegtes Repository beziehen. Die Bibliothek setzt auf RapidJSON, welche derzeit eine der performantesten Implementierungen ist. Das ist entscheidend, da bei REST-Schnittstellen JSON das verbreitetste Austauschformat ist.

Pistache: Hello World!

Mit wenigen Zeilen Code ist die erste Applikation rasch geschrieben:

#include <pistache/endpoint .h>
using namespace Pistache;
class HelloHandler : public Http::Handler {
public:
   HTTP_PROTOTYPE(HelloHandler)
   void onRequest(const Http::Request& request, Http::ResponseWriter response) override {
      response.send(Http::Code::Ok, "Hello from Pistache");
   }
};
int main() {
   Http::listenAndServe<HelloHandler>(Address (Ipv4::any(), Port (8084)), Http::Endpoint::options ());
}

In der Subklasse der Http::Handler-Klasse muss die onRequest-Methode implementiert werden. Einfache Handler wie oben können das Makro HTTP_PROTOTYPE einsetzen, welches sich um die Definition der abstrakten (pure virtual) clone-Methode kümmert. In der main-Methode (dem Einstiegspunkt eines jeden C++-Programms) wird listenAndServe aufgerufen und Address – welche hier den Port beinhaltet – sowie Option – das beispielsweise die Anzahl Threads oder Größenbeschränkungen für Anfragen und Antworten beinhalten könnte – mitgegeben.

Es empfiehlt sich, das Projekt mit CMake aufzusetzen. Ein entsprechendes Beispiel dafür und sämtliche im Dokument erwähnte Codes können im Repository eingesehen werden. Der Test im Browser mit der URL localhost:8084 zeigt nach Kompilation und Ausführung das Ergebnis:

Hello from Pistache

Docker

Damit die Applikation in der Cloud orchestriert verwendet werden kann, ist ein Docker-Image die Grundlage. Basierend auf dem schlanken Alpine Linux kann das Image so aussehen:

FROM alpine:latest
RUN apk -U upgrade && apk add --no-cache git clang g++ rapidjson-dev libstdc++ meson && \
   git clone https://github.com/pistacheio/pistache.git && \
   cd pistache && \
   meson setup build && \
   meson install -C build && \
   cd - && rm -r pistache && \
   apk del --no-cache --purge g++ clang meson
COPY . pistache
RUN apk add --no-cache --purge g++ clang cmake make && \
   export CXX=clang++ && export CC=clang && \
   cd pistache && mkdir build && cd build && \
   cmake .. -DCMAKE_BUILD_TYPE=Release && \
   make && \
   mv pistachedemo /usr/bin/pistachedemo && \
   cd ../.. && rm -r pistache && \
   apk del --no-cache --purge g++ clang cmake make
CMD ["pistachedemo"]
EXPOSE 8084 8084

Pistache wird mit dem Build-System Meson gebaut und installiert. Es wurde sichergestellt, dass die beiden RUN-Abschnitte die nicht mehr verwendeten Abhängigkeiten wieder freigeben, um das resultierende Image so klein wie möglich zu gestalten.

Vergleich mit anderen Technologien

Gegenwärtig sind Spring Native und Quarkus in der GraalVM sowie die Implementierung in der Programmiersprache Go zusammen mit dem GIN-Framework plausible Alternativen zu der wohl verbreitetsten Implementierung von Microservices mit Spring Boot in der JVM. Um einen objektiven Vergleich der verschiedenen Varianten erstellen zu können, wurde jeweils ein Endpunkt erstellt, welcher Primzahlen bis zu einem bestimmten Limit berechnet und die Anzahl der Resultate als JSON zurückgibt. Dafür wurde das Sieb von Atkin als Berechnungsgrundlage verwendet. Sämtlicher Code ist im erwähnten Repository zu finden.

Build

Beim generierten Dockerfile führt Pistache mit rund 20 MB für das gesamte Image.
Beim generierten Dockerfile führt Pistache mit rund 20 MB für das gesamte Image.
(Bild: adesso Schweiz AG)

Beim Vergleich der Build-Zeiten gewinnt ganz klar Go, der ungefähr in einer Sekunde baut. Gefolgt von der Umsetzung in C++. Das Erstellen der ausführbaren Dateien aus dem Java-Code ist sehr zeit- und arbeitsspeicherintensiv. Während des Build-Prozesses, der mehrere Minuten dauert, werden über 6 Gigabyte (GB) RAM vom Host in Anspruch genommen. Im CI/CD-Umfeld und einem bestimmten Volumen an Projekten, könnte dies ein ernstzunehmendes Problem darstellen.

Performance

Bei der Startzeit haben Pistache und Go die Nase vorn.
Bei der Startzeit haben Pistache und Go die Nase vorn.
(Bild: adesso Schweiz AG)

Mit der Startzeit wird die Zeit vom Starten der Applikation bis zum Verarbeiten des ersten Requests gemessen. Sowohl Pistache als auch Go starten in weniger als einer Millisekunde. Quarkus und Spring Native benötigen etwas mehr Zeit, liefern jedoch deutlich bessere Ergebnisse als das Starten der SpringBoot-Applikation in der JVM.

Den zeitliche Verlauf der Response Time gemessen in einem JMeter-Test.
Den zeitliche Verlauf der Response Time gemessen in einem JMeter-Test.
(Bild: adesso Schweiz AG)

Bei den Antwortzeiten bleibt Pistache ungeschlagen. Gegenüber Go antwortet Pistache im Test in der Hälfte der Zeit. Zur Laufzeit verhält sich die SpringBoot-Applikation, was die Performance angeht, nicht schlecht. In der vorngestellten Grafik zeigt sich, dass die Ergebnisse besser werden, je länger die Applikation betrieben wird. Leider sehen die Ergebnisse bei den GraalVM-basierten Microservices weniger gut aus.

Ressourcen

Maschinennähe zahlt sich bei der Ressourcennutzung aus.
Maschinennähe zahlt sich bei der Ressourcennutzung aus.
(Bild: adesso Schweiz AG)

Die C++-Implementierung geht wie erwartet besonders schonend mit den Ressourcen um und hebt sich deutlich von den verglichenen Java-Umsetzungen und der Go-Applikation ab.

OpenAPI

Von offizieller Seite gibt es Unterstützung für Pistache im Swagger-Codegen-Tool, welches auch direkt in SwaggerHub ausgeführt werden kann. Dazu gibt es das Profil pistache-server, um den Server-Stub zu erzeugen. Der generierte Code enthält Templates, welche dann implementiert werden müssen, wie das bei allen anderen Sprachen und Frameworks der Fall ist.

Ebenso ist es möglich, die Definition aus bestehendem Code zu generieren. Dafür wird die Definition Klasse verwendet und damit ist die Request-Beschreibung sichergestellt. Die Beschreibung der DTOs, welche zurückgeliefert werden, gestaltet sich allerdings schwieriger. Wer aus dem Java-Umfeld gewohnt ist, dass die entsprechenden Klassen einfach mit Annotationen versehen werden können, wird hier eine große Enttäuschung erleben.

Mangels Reflection bei C++ ist dies leider nicht so einfach möglich. Es ist dennoch empfehlenswert, den Microservice auf der Definition aufzubauen. Es werden der Pfad, der MIME-Type und entsprechende Beschreibungen wie nachfolgend gezeigt definiert. Zudem wird über die bind-Methode die effektive Implementierung angebunden. Im nachfolgenden Beispiel ist die Initialisierung in eine Methode ausgelagert worden:

void createDescription() {
   description.route (description.get("/"))
      .bind(&Server::hello)
      .produces(MIME(Text, Plain))
      .response(Http::Code::Ok, "HelloWorld!");
   description.route(description.get("/primes"), "Calculate primes")
      .bind(&Server::primes)
      .produces(MIME(Application, Json))
      .parameter<Rest::Type::Integer>("limit", "Limit of the calculation")
      .response(Http::Code::Ok, "Total and list of primes");
}

Im gezippten Beispiel sind es die Methoden hello und primes, in welchen die Request-Abwicklung stattfindet. Es wird darin schließlich die send-Methode vom ResponseWriter aufgerufen. Die gezeigte Definition wird einer Router-Instanz der initFromDescription übergeben. Daraus kann dann der Handler für den HttpEndpoint gesetzt, bevor dieser gestartet wird. Letzterer muss zuvor initialisiert worden sein.

void start() {
   router.initFromDescription(description);
   httpEndpoint.setHandler(router.handler());
   httpEndpoint.serveThreaded();
}

Fazit

Schonender Umgang mit Ressourcen und hohe Performance müssen keine Gegensätze sein, wie im Vergleich deutlich aufgezeigt werden konnte. Soll ein Microservice effizient und sparsam umgesetzt werden, ist Pistache ganz klar eine sehr interessante Option, mit der auch ein bescheidener Beitrag zum Schutz der Umwelt geleistet werden kann.

Gerade für kleine Anwendungen mit geringer Komplexität ist die Technologie besonders geeignet. Erwähnenswert ist zudem, dass Promises/A+ für die Verarbeitung für JavaScript Promises von Pistache unterstützt werden.

Nebst der erschwerten Generierung von umfangreicher API-Dokumentation, muss jedoch ein weiterer Schwachpunkt erwähnt werden: Es handelt sich dabei um die bestehende Dokumentation von Pistache, die stark zu wünschen übriglässt. Bis auf wenige Implementierungsbeispiele ist zum heutigen Zeitpunkt kaum etwas vorhanden.

Eduardo Hahn Paredes
Eduardo Hahn Paredes
(Bild: adesso Schweiz AG)

Fairerweise muss aber auch berücksichtigt werden, dass der 1.0-Meilenstein der Bibliothek noch nicht erreicht wurde. Gemäß README im Github-Repository von Pistache ist der produktive Einsatz als Server jedoch bereits möglich. Im Client-Teil, welcher dieser Artikel nicht behandelt, scheint es noch Probleme zu geben. Man darf also gespannt sein, was man in Zukunft noch von Pistache hören wird.

* Eduardo Hahn Paredes ist Team Leader und Backend-Entwickler bei der adesso Schweiz AG. Er hat 20 Jahre IT-Erfahrung in zahlreichen Technologien. In den vergangenen Jahren lag sein Fokus hauptsächlich auf Spring Boot und OpenShift.

(ID:47717050)