NATS is our cloud-native message broker of choice

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

As part of our Event-Driven Systems series, Ricky in the first part wrote about Apache Kafka as the most widely used message broker and why you should use Kafka. The second part was about how you can roll out an Apache Kafka in the cloud, this is how you do it, as Raffael showed us, namely with Strimzi. In the first part we looked at Apache Kafka and what Kafka is particularly suitable for.

The third part, which I would like to present to you today, is about whether there is not a cloud-native alternative to Kafka Apache and whether it shouldn't be rolled out on your cloud, as a cloud-native alternative could be more resource-efficient and thus more cost-effective. The alternative is there and is called NATS. So we are jointly examining another message broker, which, like Apache, does not need an underlying JVM and is therefore much lighter.

NATS server

NATS is an open source (Apache 2.0) and cloud-native messaging system for adaptive edge and distributed systems. It was originally written in Ruby by Derek Collison and later ported to Go. NATS (or NATS Messaging) enables the exchange between applications and services. In doing so, data is packed into messages, which are then addressed by 'Subject' and not by IP or DNS name. This abstracts the underlying physical network layer. The data to be sent is encrypted and “packed” into a message by the sender. The message can then be received, decrypted and processed by one or more recipients.

Nowadays, there are more than 40 client API's, like for example for Go, Java, JavaScript/TypeScript, Python, Ruby, Rust, C#, C, and NGINX.

It should be noted that NATS does not persist any messages. If a client is not available at the time of the message, this message is no longer visible to him.

NATS streaming

For this reason there is NATS Streaming. NATS Streaming implements message persistence and a message delivery guarantee. Every connected client has to send a ACK, short for Acknowledge, when receiving the message. If he does not do this, the message will be sent again after a certain time. This means that it is possible that a message arrives twice at the recipient.

A NATS Streaming Server includes a NATS server. The messages are still received by the NATS server and forwarded to the streaming server.

The NATS streaming server can be made available securely and with high availability, but there are some restrictions due to the architecture.

  • NATS streaming is not a 'work queue', it is 'message log based'. This means that messages are not deleted by ACK s, only by limitations.

  • Not horizontally scalable (#999)

  • Bad integration with NATS 2.0 / accounts / security concepts. No multi-tenancy (#1043)

  • Clients cannot pull messages themselves, messages are only pushed to them.

  • clients cannot register on specific channels (#1122)

NATS Jetstream

Release 2.2.0 of NATS was published in March 2021. With this release, NATS Jetstream was introduced, which fixed many problems with NATS Streaming (see NATS Streaming).

In addition, Jetstream has a number of features. We want to look at a very interesting one in detail.

Wildcards

At its core, NATS is responsible for sending and receiving messages. Sending and receiving are based on “subjects”, which assign messages in streams or topics. A subject is a simple case-sensitive string, which consists of alphanumeric characters and the "." may exist.

The point has another useful function. You can use this to build a “Subject” hierarchy. For example, a structure could look like this. This is a logical structure.

com.bnova --> Everyone at b-nova
com.bnova.developer.language --> 
com.bnova.developer.language.go --> All internal Go developers
com.bnova.developer.language.java --> All internal Java developers
com.bnova.developer.framework.kubernetes --> All internal Kubernetes developers
com.bnova.developer.framework.openshift --> All internal OpenShift developers

NATS now offers us two wildcards that can be used by the recipient to listen to several subjects.

Matching A Single Token (*)

If a recipient wants to receive all the news from the internally used programming languages, he can subscribe to the following.

com.bnova.developer.language.*

He now receives all the news from the Go and Java world. However, the wildcard only means exactly one string. So if you want to receive all the news about programming languages and frameworks, you cannot use this, since com.bova.developer.* would only select one additional level (so com.bnova.developer.language or com.bnova.developer.framework).

Matching Multiple Tokens (>)

There is another wildcard to select several levels.

com.bnova.developer.>

Now all subjects under com.bnova.developer can be selected. It is also possible to subscribe to all subjects using the following subject. Of course, you can only read those that you have access to. Can only be used at the end!

>

It is also possible to mix both wildcards.

*.bnova.>

would for example select com.bnova or ch.bnova.

NATS in action

Now let's see how NATS works in practice. We start a local NATS server and use the official Docker image for this. So that we can use Jetstream directly, we have to specify -js as an argument.

$ 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

Now we start a second container in which all NATS tools are already installed.

$ 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:~#

As we can see, we now receive a prompt and can now begin to interact with the server.

Streams

First, let's create a stream. Streams define how messages are saved and retained. Streams consume normal NATS subjects. Every message found in these subjects is sent to the defined storage.

$ 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

Let's look at the configurations in detail

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.>: Here we define which subjects we want to listen to

  2. Storage backend = file: Should the stream be kept in the file or memory

  3. Retention Policy = Limits: Only a certain number of messages should be saved

  4. Discard Policy = Old: Old messages should be deleted when the maximum number of messages is reached

  5. Stream Messages Limit = -1: Number of messages in the stream (-1 infinite)

  6. Message size limit = -1: size of the entire message (-1 infinite)

  7. Maximum message age limit = 2m: Messages are kept for 2 minutes

  8. Maximum individual message size = -1: Maximum size of a message (-1 infinite)

  9. Duplicate tracking time window = 2m: Time in which duplicates are checked

  10. Replicas = 1: Number of replicas

Write and read the first message

Now we can write our first message in the stream. To do this, we run the following command:

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

And look directly at what our stream looks like:

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

As we can see, there is now a message in our stream. We can now call this up for 2 minutes before it is deleted again by the Maximum Age configuration.

Let us now write a consumer with which we can read the message from the stream again.

$ 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

Let's take a look at the configuration again in detail:

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 of the consumer

  2. Delivery target = pull: Consumer pulls the messages from the sender

  3. Start policy = all: All messages in the stream should be read

  4. Replay policy = instant: Consumer will receive all messages as soon as possible

  5. Filter Stream by subject = com.bnova.language: Read only from this subject

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

  7. Maximum Acknowledgments Pending = 0: ???

Now we can read the message from the stream with our newly created consumer. To do this, we have to enter the following command:

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 now you know what NATS is and how you can set up a NATS environment in your Kubernetes cluster. Stay tuned!


This text was automatically translated with our golang markdown translator.

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.