Overview:
In this tutorial, I would like to introduce NATS Messaging to you & talk about its advantages in Microservices Architecture and some code samples in Java.
NATS Messaging Server:
We had discussed a lot about inter microservices communication using Kafka, gRPC, RSocket, even Redis in this blog. They are all great and they have their own pros and cons. For ex: Kafka has very good throughput – but the infrastructure set up is quite complex! Redis setup is easy compared to Kafka but it is an in-memory DB, does not have the partitions/replications as Kafka does.
NATS is an open source messaging system. It is a very simple, scalable messaging server for cloud native applications with its own pros and cons.
Pros:
- An extremely high throughput messaging system for Microservices!
- It is really lightweight that it is less than 7 MB in size.
- Highly scalable and performant!.
- It has built-in load-balancing, service-discovery etc.
- Supports pub / sub / streaming (separate NATS-Streaming server).
- It is part of Cloud Native Computing Foundation – CNCF (a foundation which supports & builds the ecosystems for cloud native open source software ex: Kubernetes).
Cons:
- It has very minimal (but important) functionalities.
NATS Messaging Server:
- Lets start with setting up NATS Messaging server! Use the below docker command and you are good! (It gets downloaded and starts in no time! It is truly lightweight!!!)
docker run -p 4222:4222 nats:alpine
- Now the NATS server is up and running.
- Create a simple java maven project and include the below dependency.
<dependency>
<groupId>io.nats</groupId>
<artifactId>jnats</artifactId>
<version>2.6.8</version>
</dependency>
- Connect to the NATS Messaging server
// nats connection
// it connects to nats://localhost:4222 by default
Connection nats = Nats.connect();
// remote server
Connection nats = Nats.connect("nats://10.11.12.13:4222");
Features:
NATS has 3 basic features.
- Pub / Sub
- Request / Reply
- Queue Group
Pub/Sub:
Pub/Sub is a messaging pattern in which senders/publishers send the messages to a channel/topic instead of sending to a specific receiver. In fact, the publisher does not have any idea who the receiver is! The interested parties (aka receivers/subscribers) will subscribe to the channels, receive the message and process them. It is an one-to-many communication also known as fan-out communication. (Image courtesy – nats.io site)
Subscriber:
// connect to nats server
Connection nats = Nats.connect();
// message dispatcher
Dispatcher dispatcher = nats.createDispatcher(msg -> {});
// subscribes to nats.demo.service channel
dispatcher.subscribe("nats.demo.service", msg -> {
System.out.println("Received : " + new String(msg.getData()));
});
Here we subscribe to nats.demo.service which is the name for our topic. The msg.getData() returns the passed object in byte[] format!
Publisher:
// connect to nats server
Connection nats = Nats.connect();
// publish a message to the channel
nats.publish("nats.demo.service", "Hello NATS".getBytes());
I could see this output in the subscriber console.
Received : Hello NATS
Wildcards Support:
A single subscriber can listen to multiple channels.
dispatcher.subscribe("nats.*.service", msg -> {
System.out.println("Received : " + new String(msg.getData()));
});
For example, in the above example, the subscriber can receive messages from below channels if there are any etc.
- nats.demo.service
- nats.order.service
- nats.payment.service
The * matches single token in the address. Not the entire string/address.
For ex: nats.* will match any channels starting with nats.<some-token>. but not nats.<some-token>.<some-other-token>
If we need to match any channel/topic starting with nats, we need to use nats.> which will match anything starts with nats.
Subscriber Channel Format | Channel Names | Match? |
---|---|---|
foo.* | foo.bar | Yes |
foo.* | foo.bar.service | No |
foo.*.service | foo.bar.service | Yes |
foo.*.service | foo.bar | No |
foo.> | foo.bar | Yes |
foo.> | foo.bar.service | Yes |
foo.*.> | foo.bar.service | Yes |
Request/Reply:
The above pub/sub is more of a fire-and-forget mode of communication. Sometimes we expect a response back for the request we send.
// subscribes to nats.demo.service channel
dispatcher.subscribe("nats.demo.service", msg -> {
System.out.println("Received : " + new String(msg.getData()));
nats.publish(msg.getReplyTo(), "Hello Publisher!".getBytes());
});
Check the above subscriber. It can respond back to the sender by using the sender’s address. Imagine this as 2 persons communicating via email. When the subscriber/receiver receives the message, it simply replies to the message.
reply-to address would be null for the fire-and-forget mode communication.
The publisher side code would be like this. Note that the publisher requests for response by using the request method. (We can also pass the Duration for timeout as additional parameter.)
nats.request("nats.demo.service", "Hello NATS".getBytes())
.thenApply(Message::getData) // gets executed when we get response from receiver
.thenApply(String::new)
.thenAccept(s -> System.out.println("Response from Receiver: " + s));
Subscriber output:
Received : Hello NATS
Publisher output:
Response from Receiver: Hello Publisher!
Queue Group:
In a typical pub/sub messaging pattern, every single subscriber will be notified and receiving the messages. Sometimes, we might want just 1 single subscriber to process the message. Not all. We do not want redundant processing.
For ex: Lets consider an audit-service which is responsible for logging the audit events.
// subscriber 1
dispatcher.subscribe("audit.event.service", "audit-service", msg -> {
System.out.println("Received 1 : " + new String(msg.getData()));
});
// subscriber 2
dispatcher.subscribe("audit.event.service", "audit-service", msg -> {
System.out.println("Received 2 : " + new String(msg.getData()));
});
- audit.event.service
- channel name
- audit-service
- group name
- the lambda function
- message handler
When we use group name and NATS sees multiple subscribers to this channel, it will automatically distribute the incoming messages. There is no change in the publisher side. Publisher does not have to know anything about the subscribers. It does not matter if it is 1 subscriber or group of subscribers. It will simply publish as usual in the fire-and-forget mode.
nats.publish("audit.event.service", "an order placed".getBytes());
Unsubscribe:
The subscribers can unsubscribe anytime or after processing specific number of messages.
Subscription subscription = dispatcher.subscribe("nats.demo.service", msg -> {
System.out.println("Received : " + new String(msg.getData()));
});
dispatcher.unsubscribe(subscription, 10);
In this example, the subscriber is interested in processing only 10 messages. It does not get any more messages after that.
NATS Messaging In Microservices:
The above NATS Messaging server functionalities can be used for Microservices architecture.
- Communication:
- A Microservice can talk to another Microservice by sending a message via a channel
- Response can be received by using the reply-to address.
- Service Discovery:
- When there are multiple services running, A channel name can be used to uniquely identify a service.
- Load Balancing:
- When there are multiple instances running for a specific service, one of the instances can process the incoming request by running as Queue Group
Summary:
With these examples above, we can clearly see NATS Messaging server is very simple to use. As it is 7 MB in size and starts in no time, It is very easy to scale for the modern cloud native applications. It can even run in IoT devices. We can also run multiple instances of NATS Messaging servers in the clustering mode for high availability.
Learn more about NATS.
Happy learning 🙂