Modulares Router-Design für Vert.x-Mikroservices

By Gerald Mücke | September 15, 2017

Modulares Router-Design für Vert.x-Mikroservices

Beim Entwickeln von Mikroservices mit dem Vert.x-Framework bin ich mehr als einmal über die Frage gestolpert, wie man Vertikel organisiert und ein modulares Design erreicht. Vert.x ist unvoreingenommen und ermöglicht verschiedene Wege, dies zu erreichen. In diesem Artikel möchte ich zwei Optionen für den Aufbau modularer Dienste diskutieren.

In Vert.x werden die Endpunkte eines Mikroservices als Routen eines Routers definiert, der an einen HTTP-Server gebunden ist. Ein HTTP-Server kann einen TCP-Port teilen, was es ermöglicht, mehrere Instanzen eines Vertikels zu betreiben, die auf demselben Port lauschen. Ein Router wird jedoch nicht geteilt. Wenn also als Handler auf einem HTTP-Server eingestellt, der auf einem gemeinsamen Port lauscht, erhält nur der erste Router Anfragen, während die anderen nichts erhalten. Daher werden beim Bereitstellen von Vertikeln mit unterschiedlichen Routern auf demselben Port nicht alle Endpunkte verfügbar gemacht. In meiner Forschung stellte ich fest, dass dies kein ungewöhnliches Problem ist (siehe hier,hier oder hier).

Es gibt zwei Optionen, dieses Problem zu lösen

  • ein einzelner Vertikel, der verschiedene Subrouter einbindet, die von verschiedenen Klassen bereitgestellt werden
  • das Teilen einer einzelnen Router-Instanz über verschiedene Vertikel hinweg

Einzelner Vertikel

Die Grundidee ist, dass nur ein einzelner Vertikel den HTTP-Server und einen Router erstellt. Endpunkte der verschiedenen Dienste werden hinzugefügt, indem Sub-Router gemäß der Vert.x-Dokumentation eingebunden werden. Die Sub-Router werden von Klassen definiert, die die Endpunkte festlegen, welche von verschiedenen Teams gewartet werden können. Um eine dynamische Zusammensetzung von Diensten zu erreichen, kann der ServiceLoader-Mechanismus verwendet werden, sodass zusätzliche Dienste automatisch eingebunden werden, wenn sie im Klassenpfad vorhanden sind. Dennoch ist es nicht möglich, zusätzliche Funktionalität zur Laufzeit bereitzustellen.

Zuerst definieren wir eine Schnittstelle, die Dienste implementieren müssen. Die Schnittstelle definiert den Einhängepunkt für den Subrouter und den eigentlichen Router für die Endpunkte

public interface ServiceEndpoint {
String mountPoint();
Router router(Vertx vertx);
}

Weiterhin erstellen wir eine Implementierung für das ServiceEndpoint

public class OneService implements ServiceEndpoint {

@Override
public String mountPoint() {
return "/1";
}

@Override
public Router router(Vertx vertx) {
Router router = Router.router(vertx);
router.get("/one").handler(ctx -> ctx.response().end("One OK"));
return router;
}
}

In der META-INF/services erstellen wir eine Datei mit dem voll qualifizierten Namen der ServiceEndpoint-Schnittstelle, z.B. io.devcon5.vertx.examples.ServiceEndpoint, die den voll qualifizierten Namen einer oder mehrerer implementierender Klassen enthält, z.B. io.devcon5.vertx.examples.OneService. Diese Dienste können im selben oder in anderen Jars definiert sein, der ServiceLoader-Mechanismus kann alle Implementierungsklassen sammeln. Wir laden alle Implementierungen während der Initialisierung des HTTP-Servers und binden ihre Router an den zentralen Router.

public class ServerVerticle extends AbstractVerticle {

@Override
public void start(final Future<Void> startFuture) throws Exception {

    //Erstellen eines ServiceLoaders für die ServiceEndpoints
    ServiceLoader<ServiceEndpoint> loader = ServiceLoader.load(ServiceEndpoint.class);

    //Über alle Endpunkte iterieren und alle ihre Endpunkte an einen einzelnen Router anhängen
    Router main = StreamSupport.stream(loader.spliterator(), false)
                               .collect(() -> Router.router(vertx), //der Haupt-Router
                                        (r, s) -> r.mountSubRouter(s.mountPoint(), s.router(vertx)),
                                        (r1, r2) -> {});

    //den Haupt-Router an den HTTP-Server binden
    vertx.createHttpServer().requestHandler(main::accept).listen(8080, res -> {
      if (res.succeeded()) {
        startFuture.complete();
      } else {
        startFuture.fail(res.cause());
      }
    });
}
}

Jetzt können Sie Dienste dynamisch mit einem einzigen Server zusammenstellen. Der ServerVerticle kann mehrfach bereitgestellt werden, um Skalierbarkeit zu erreichen. Diese Lösung ist wahrscheinlich “vertxier” als die gemeinsame Router- Lösung, da sie nicht auf das Teilen einer Instanz zwischen Threads angewiesen ist, aber die Service-Endpunkte können nicht als separate Vertikel verwendet und bereitgestellt werden.

Das vollständige Beispiel finden Sie auf GitHub

Geteilter Router

Die Kernidee ist, einen einzelnen Haupt-Router zu verwenden, der zwischen Vertikeln geteilt wird, um ihre Sub-Router daran zu binden. Die RouterImpl-Klasse von Vert.x ist thread-sicher, daher sollte es kein Problem sein, die Instanz zwischen verschiedenen Vertikeln zu teilen, die möglicherweise auf verschiedenen Threads laufen.

Wir benötigen eine Wrapper- oder Erweiterung für den Router, um ihn Shareable zu machen. Der Router kann dann zwischen Vertikeln geteilt werden, aber nicht über ein Cluster hinaus. Wir definieren auch eine Methode, um diesen teilbaren Router zu erstellen, die sicherstellt, dass nur ein Router erstellt wird, selbst wenn mehrere Vertikel versuchen, einen neuen Router zu erstellen.

public class ShareableRouter extends RouterImpl implements Shareable {
public static Router router(Vertx vertx) {
return (Router) vertx.sharedData()
.getLocalMap("router")
.computeIfAbsent("main", n -> new ShareableRouter(vertx));
}

ShareableRouter(final Vertx vertx) {
super(vertx);
}
}

Jetzt kann jeder Vertikel, der eine eigenständige Menge von Endpunkten definiert, einschließlich eines HttpServers, ihre Router als Sub-Router an diesen gemeinsamen Router anhängen oder direkt Routen definieren - was nicht empfohlen wird, da es zu potenziellen Endpunkt-Kollisionen führen kann.

public class HttpOneVerticle extends AbstractVerticle {

@Override
public void start(final Future<Void> startFuture) throws Exception {

    //Erstellen eines Routers, der die Endpunkte des Dienstes definiert
    final Router router = Router.router(vertx);
    router.get("/one").handler(ctx -> ctx.response().end("OK one"));

    //den Router als Subrouter an den geteilten Router anhängen
    final Router main = ShareableRouter.router(vertx).mountSubRouter("/1", router);

    vertx.createHttpServer().requestHandler(main::accept).listen(8080, res -> {
      if(res.succeeded()){
        startFuture.complete();
      } else {
        startFuture.fail(res.cause());
      }
    });
}
}

Obwohl dieser Ansatz etwas weniger “vertxy” ist als der andere, ist er gültig, da Vert.x unvoreingenommen ist, wie Tim Fox feststellte. Dieser Ansatz hat den Vorteil, dass jeder Dienst eigenständig betrieben werden kann und möglicherweise dynamisch zur Laufzeit bereitgestellt oder abgebaut wird.

Das vollständige Beispiel finden Sie auf GitHub

comments powered by Disqus