NATS ist unsere Cloud-native Message Broker der Wahl

18.08.2021 Stefan Welsch
Cloud DevOps event-driven distributed-systems cloud-native message-broker foss message-queue k8s iot edge-computing

Im Rahmen unserer Event-Driven Systems-Reihe hat Ricky im ersten Teil über Apache Kafka als der meist eingesetze Message-Broker geschrieben und warum man Kafka einsetzen sollte. Im zweiten Teil ging darum wie man ein Apache Kafka in der Cloud ausrollen kann, dies macht man, so hat es uns Raffael gezeigt, nämlich am besten mit Strimzi. Im ersten Teil haben wir uns Apache Kafka angeschaut und wofür sich Kafka besonders eignet. Im dritten Teil welcher ich euch heute präsentieren möchte, geht es darum ob es nicht eine Cloud-native Alternative zu Kafka Apache gibt und ob man diese nicht lieber auf Ihrer Cloud ausrollen sollte, da eine Cloud-native Alternative ressourceneffizienter und somit kostengünstiger sein könnte. Die Alternative gibt es und heisst NATS. Somit nehmen wir gemeinsam einen weiteren Message-Broker unter die Lupe, welches wie etwa Apache ohne eine darunterliegende JVM auskommt und damit wesentlich leichtgewichtiger ist.

NATS Server

NATS ist ein Open-Source (Apache 2.0) und Cloud-native Messaging System für adaptive Edge- und verteilte Systeme. Es wurde ursprünglich von Derek Collison in Ruby geschrieben und später dann zu Go portiert. NATS (oder NATS Messaging) ermöglicht den Austausch zwischen Applikationen und Services. Dabei werden Daten in Nachrichten verpackt, welche dann per 'Subject' und nicht per IP oder DNS Name adressiert werden. Dadurch wird der darunter liegende physische Netzwerk Layer abstrahiert. Die zu sendenden Daten werden vom Sender verschlüsselt in eine Nachricht “verpackt”. Die Nachricht kann dann von einem oder mehreren Empfängern empfangen, entschlüsselt und verarbeitet werden.

Es gibt derzeit über 40 Client API’s. Unter anderem werden die folgenden Technologien und Programmiersprachen unterstützt. Go, Java, JavaScript/TypeScript, Python, Ruby, Rust, C#, C, and NGINX.

Dabei ist zu beachten, dass NATS keine Nachrichten persistiert. Ist also ein Client zum Zeitpunkt der Nachricht nicht verfügbar, so ist diese Nachricht für ihn nicht mehr sichtbar.

NATS Streaming

Aus diesem Grund gibt es NATS Streaming. NATS Streaming implementiert Nachrichten-Persistierung und eine Nachrichten-Delivery-Garantie. So muss jeder verbundene Client beim Erhalt der Nachricht ein ACK, kurz für Acknowledge, senden. Tut er dies nicht, so wird die Nachricht nach einer bestimmten Zeit nochmals versendet. Das bedeutet, dass es sein kann, dass eine Nachricht doppelt beim Empfänger ankommt.

Ein NATS Streaming Server beinhaltet einen NATS-Server. Die Nachrichten werden vom NATS-Server weiterhin empfangen und an den Streaming-Server weitergeleitet.

Der NATS Streaming Server bietet kann zwar sicher und hochverfügbar zur Verfügung gestellt werden, allerdings gibt es einige Einschränkungen wegen der Architektur.

  • NATS Streaming ist keine 'work queue', es ist 'message log based'. Dadurch werden Nachrichten nicht durch ACKs gelöscht, sondern nur durch Limitationen.

  • Nicht horizontal skalierbar (#999)

  • Schlechte Integration mit NATS 2.0/accounts/security Konzepten. Keine Mandantenfähigkeit (#1043)

  • Clients können selbst keine Nachrichten 'pullen', Nachrichten werden nur zu ihnen 'pushed'.

  • Clients können sich nicht auf spezifische Channels registrieren (#1122)

NATS Jetstream

Im März 2021 wurde das Release 2.2.0 von NATS veröffentlicht. Mit diesem Release wurde NATS Jetstream eingeführt, wodurch viele Probleme von NATS Streaming behoben werden konnten. (siehe NATS Streaming)

Ausserdem bringt Jetstream einige Features mit. Ein sehr interessantes wollen wir uns im Detail anschauen.

Wildcards

Im Kern ist NATS dafür zuständig Nachrichten zu versenden und zu empfangen. Senden und Empfangen basiert dabei auf “Subjects”, welche Nachrichten in Streams oder Topics zuordnen. Ein Subject ist ein einfacher case-sensitiver String, welche aus alphanumerischen Zeichen und dem “.” bestehen darf.

Der Punkt hat eine weitere nützliche Funktion. Man kann hiermit eine “Subject”-Hierarchie aufbauen. Beispielsweise könnte eine Struktur folgendermassen aussehen. Dabei handelt es sich um eine logische Struktur.

com.bnova --> Alle bei b-nova
com.bnova.developer.language --> 
com.bnova.developer.language.go --> Alle internen Go Entwickler
com.bnova.developer.language.java --> Alle internen Java Entwickler
com.bnova.developer.framework.kubernetes --> Alle internen Kubernetes Entwickler
com.bnova.developer.framework.openshift --> Alle internen OpenShift Entwickler

NATS bietet uns nun zwei Wildcards, die vom Empfänger genutzt werden können, um auf mehrere Subjects zu hören.

Matching A Single Token (*)

Will ein Empfänger alle Nachrichten von den News der intern genutzten Programmiersprachen erhalten, so kann er folgendes abonnieren.

com.bnova.developer.language.*

Damit erhält er nun alle News aus der Go- und Java-Welt. Das Wildcard bedeutet jedoch nur genau ein String. Will man also alle News von Programmiersprachen und Frameworks erhalten, so kann man dieses nicht nutzen, da com.bova.developer.* nur eine weitere Ebene selektieren (also com.bnova.developer.language oder com.bnova.developer.framework) würde.

Matching Multiple Tokens (>)

Um nun mehrere Ebenen zu selektieren, gibt es einen weiteren Wildcard.

com.bnova.developer.>

Damit lassen sich nun alle Subjects unter com.bnova.developer selektieren. Es ist auch möglich alle Subjects zu abonnieren, indem man folgendes Subject nutzt. Natürlich kann man nur die lesen, auf die man auch Zugriff hat. Nur am Ende nutzbar!

>

Es ist auch möglich beide Wildcards zu mixen.

*.bnova.>

würde beispielsweise com.bnova oder auch ch.bnova selektieren.

NATS in action

Wollen wir uns nun anschauen, wie NATS in der Praxis funktioniert. Wir starten uns einen lokalen NATS Server und nutzen dafür das offizielle Docker Image. Damit wir direkt Jetstream verwenden, müssen wir als Argument noch -js angeben.

$ docker run --rm --network host -p 4222:4222 -ti nats -js                                                                                                                                                                                                                                                             10:32:52
[1] 2021/05/25 08:33:03.679062 [INF] Starting nats-server
[1] 2021/05/25 08:33:03.679139 [INF]   Version:  2.2.6
[1] 2021/05/25 08:33:03.679156 [INF]   Git:      [cf433ae]
[1] 2021/05/25 08:33:03.679186 [INF]   Name:     NDX4GCHNCTCWFTDS2RAWUFTTFXJHZV42QYVN2IVUYOY2OINCJOVNWOC7
[1] 2021/05/25 08:33:03.679276 [INF]   Node:     M0hdUjMg
[1] 2021/05/25 08:33:03.679288 [INF]   ID:       NDX4GCHNCTCWFTDS2RAWUFTTFXJHZV42QYVN2IVUYOY2OINCJOVNWOC7
[1] 2021/05/25 08:33:03.679894 [INF] Starting JetStream
[1] 2021/05/25 08:33:03.681591 [INF]     _ ___ _____ ___ _____ ___ ___   _   __  __
[1] 2021/05/25 08:33:03.681642 [INF]  _ | | __|_   _/ __|_   _| _ \ __| /_\ |  \/  |
[1] 2021/05/25 08:33:03.681658 [INF] | || | _|  | | \__ \ | | |   / _| / _ \| |\/| |
[1] 2021/05/25 08:33:03.681705 [INF]  \__/|___| |_| |___/ |_| |_|_\___/_/ \_\_|  |_|
[1] 2021/05/25 08:33:03.681721 [INF]
[1] 2021/05/25 08:33:03.681734 [INF]          https://docs.nats.io/jetstream
[1] 2021/05/25 08:33:03.681797 [INF]
[1] 2021/05/25 08:33:03.681822 [INF] ---------------- JETSTREAM ----------------
[1] 2021/05/25 08:33:03.681841 [INF]   Max Memory:      8.78 GB
[1] 2021/05/25 08:33:03.681870 [INF]   Max Storage:     29.80 GB
[1] 2021/05/25 08:33:03.681917 [INF]   Store Directory: "/tmp/nats/jetstream"
[1] 2021/05/25 08:33:03.681932 [INF] -------------------------------------------
[1] 2021/05/25 08:33:03.683881 [INF] Listening for client connections on 0.0.0.0:4222
[1] 2021/05/25 08:33:03.684340 [INF] Server is ready

Nun starten wir uns noch einen zweiten Container, in dem alle NATS Tools bereits installiert sind.

$ docker run -ti --network host natsio/nats-box                                                                                                                                                                                                                                                                                   05:24:50
Unable to find image 'natsio/nats-box:latest' locally
latest: Pulling from natsio/nats-box
ba3557a56b15: Pull complete
c31a888c6281: Pull complete
98afc89d9e8c: Pull complete
0e635ef830af: Pull complete
16d5eb463157: Pull complete
7d416da1d234: Pull complete
Digest: sha256:51f09970f8fd979bdfc8ff9b38205030384e4592de05cf52c065f9c0ff8bc5de
Status: Downloaded newer image for natsio/nats-box:latest
             _             _
 _ __   __ _| |_ ___      | |__   _____  __
| '_ \ / _` | __/ __|_____| '_ \ / _ \ \/ /
| | | | (_| | |_\__ \_____| |_) | (_) >  <
|_| |_|\__,_|\__|___/     |_.__/ \___/_/\_\

nats-box v0.5.0
6767dbda2e86:~#

Wie wir sehen können erhalten wir nun einen Prompt und können nun beginnen, mit dem Server zu interagieren.

Streams

Wollen wir uns als Erstes einen Stream anlegen. Streams definieren wie Nachrichten gespeichert und aufbewahrt werden. Streams konsumieren normale NATS Subjects. Jede Nachricht, welche in diesen Subjects gefunden wird, wird an den definierten Storage gesendet.

$ nats str add bnova
? Subjects to consume com.bnova.>
? Storage backend file
? Retention Policy Limits
? Discard Policy Old
? Stream Messages Limit -1
? Message size limit -1
? Maximum message age limit 2m
? Maximum individual message size -1
? Duplicate tracking time window 2m
? Replicas 1
Stream bnova was created

Information for Stream bnova created 2021-05-25T09:29:14Z

Configuration:

             Subjects: com.bnova.>
     Acknowledgements: true
            Retention: File - Limits
             Replicas: 1
       Discard Policy: Old
     Duplicate Window: 2m0s
     Maximum Messages: unlimited
        Maximum Bytes: unlimited
          Maximum Age: 2m0s
 Maximum Message Size: unlimited
    Maximum Consumers: unlimited

State:

             Messages: 0
                Bytes: 0 B
             FirstSeq: 0
              LastSeq: 0
     Active Consumers: 0

Schauen wir uns die Konfigurationen im Detail an

Subjects to consume com.bnova.>
? Storage backend file
? Retention Policy Limits
? Discard Policy Old
? Stream Messages Limit -1
? Message size limit -1
? Maximum message age limit 2m
? Maximum individual message size -1
? Duplicate tracking time window 2m
? Replicas 1
  1. Subjects to consume=com.bnova.>: Hier definieren wir auf welche Subjects wir hören wollen

  2. Storage Backend=file: Soll der Stream im File oder Speicher gehalten werden

  3. Retention Policy=Limits: Es sollen nur eine bestimmte Anzahl an Nachrichten gespeichert werden

  4. Discard Policy=Old: Alte Nachrichten sollen gelöscht werden, wenn Anzahl der max. Nachrichten erreicht wird

  5. Stream Messages Limit=-1: Anzahl Nachrichten im Stream (-1 unendlich)

  6. Message size limit=-1: Grösse der gesamten Nachrichten (-1 unendlich)

  7. Maximum message age limit=2m: Nachrichten werden für 2 Minuten aufbewahrt

  8. Maximum individual message size= -1: Maximale Grösse einer Nachricht (-1 unendlich)

  9. Duplicate tracking time window= 2m: Zeit in der auf Duplikate geprüft wird

  10. Replicas= 1: Anzahl der Replicas

Die erste Nachricht schreiben und lesen

Nun können wir unsere erste Nachricht in den Stream schreiben. Dazu führen wir den folgenden Befehl aus:

nats pub com.bnova.language "Programmieren ist super"
12:48:16 Published 23 bytes to "com.bnova.language"

Und schauen uns direkt an, wie unser Stream aussieht:

nats str info bnova
Information for Stream bnova created 2021-05-25T09:29:14Z

Configuration:

             Subjects: com.bnova.*
     Acknowledgements: true
            Retention: File - Limits
             Replicas: 1
       Discard Policy: Old
     Duplicate Window: 2m0s
     Maximum Messages: unlimited
        Maximum Bytes: unlimited
          Maximum Age: 2m0s
 Maximum Message Size: unlimited
    Maximum Consumers: unlimited

State:

             Messages: 1
                Bytes: 71 B
             FirstSeq: 2 @ 2021-05-25T12:48:16 UTC
              LastSeq: 2 @ 2021-05-25T12:48:16 UTC
     Active Consumers: 0

Wie wir sehen können, ist nun eine Nachricht in unserem Stream vorhanden. Diese können wir nun 2 Minuten lang abrufen, bevor diese durch die Konfiguration Maximum Age wieder gelöscht wird.

Wollen wir uns nun einen Consumer schreiben, mit dem wir die Nachricht aus dem Stream wieder lesen können.

$ nats con add
? Consumer name language-consumer
? Delivery target (empty for Pull Consumers)
? Start policy (all, new, last, 1h, msg sequence) all
? Replay policy instant
? Filter Stream by subject (blank for all) com.bnova.language
? Maximum Allowed Deliveries -1
? Maximum Acknowledgements Pending 0
? Select a Stream bnova
Information for Consumer bnova > language-consumer created 2021-05-25T12:59:51Z

Configuration:

        Durable Name: language-consumer
           Pull Mode: true
      Filter Subject: com.bnova.language
         Deliver All: true
          Ack Policy: Explicit
            Ack Wait: 30s
       Replay Policy: Instant
     Max Ack Pending: 20,000

State:

   Last Delivered Message: Consumer sequence: 0 Stream sequence: 3
     Acknowledgment floor: Consumer sequence: 0 Stream sequence: 3
         Outstanding Acks: 0 out of maximum 20000
     Redelivered Messages: 0
     Unprocessed Messages: 0

Schauen wir uns auch hier die Konfiguration wieder im Detail an:

Consumer name language-consumer
? Delivery target (empty for Pull Consumers)
? Start policy (all, new, last, 1h, msg sequence) all
? Replay policy instant
? Filter Stream by subject (blank for all) com.bnova.language
? Maximum Allowed Deliveries -1
? Maximum Acknowledgements Pending 0
? Select a Stream bnova
  1. Consumer name= language-consumer: Name des Consumers

  2. Delivery target= pull: Consumer pullt die Nachrichten beim Sender

  3. Start policy=all:Alle Nachrichten im Stream sollen gelesen werden

  4. Replay policy= instant: Consumer wird alle Nachrichten so schnell wie möglich erhalten

  5. Filter Stream by subject= com.bnova.language: Nur aus diesem Subject lesen

  6. Maximum Allowed Deliveries= -1: ???

  7. Maximum Acknowledgements Pending= 0: ???

Nun können wir mit unserem neu angelegten Consumer die Nachricht aus dem Stream lesen. Dazu müssen wir folgenden Befehl eingeben:

nats con next bnova language-consumer
[13:04:52] subj: com.bnova.language / tries: 1 / cons seq: 1 / str seq: 4 / pending: 0

Programmieren ist super

Acknowledged message

So, jetzt wissen Sie was NATS ist und wie man eine NATS-Umgebung in Ihren Kubernetes-Cluster einrichten kann. Stay tuned!

Stefan Welsch - pioneer, stuntman, mentor. As the founder of b-nova, Stefan is always looking for new and promising fields of development. He is a pragmatist through and through and therefore prefers to write articles that are as close as possible to real-world scenarios.