Docker Compose + NATS: Microservices Development Made Easy
Wally Quevedo — June 20, 2016
Buzzwords are everywhere in our industry. “Microservices”, “Serverless computing”, “Nanoservices”, “Containerized” - you could fill a whole blog post just with these overused phrases. If you cut through the terminology flavor of the month, there are some very important common thread for application developers. One is simplicity. Regardless of what architectural approach you’re using - and what you may or may not refer to it as - you want it just work. You don’t want to spend days trying getting various pieces of your infrastructure up and running. Another important need is performance at scale. This requires both low latency and high throughput. Fast solutions are great - but they are not much good if they cannot scale up to meet demands of users. Low latency solutions are nice, but what good is low resource utilization without speed?
Simplicity and performance are even more important to keep in mind when your infrastructure runs Docker containers. The whole point of using containers in the first place is to have a decoupled, scalable, dependency free set of services that just work. Why use a messaging layer that isn’t designed for cloud or containers? Enter NATS.
The NATS Docker Image is incredibly simple, and has a very lightweight footprint. Imagelayers.io is a great tool by Centurylink to scan various Docker Containers and report back on the various ‘layers’ (components within the Dockerfile, and size). See below for a comparison of NATS to a few other messaging systems:
At just a few MB, and handful of layers, you can keep your Docker environment lean and scalable; you won’t even notice NATS is running on your container. As NATS has no external dependencies, and is simple plain-text messaging protocol - regardless of what your current or future infrastructure may look like NATS just works.
The simplicity of containers is perfectly aligned with the simplicity of NATS, but what about scale? If you’re looking to have a bunch of decoupled services functioning in real-time NATS is a great option. Various 3rd party benchmarks have shown a single NATS Server capable of sending 11-12 million messages.
Now, onto the fun stuff!
Docker Compose + NATS
Full example: https://gist.github.com/wallyqs/7f72efdc3fd6371364f8b28cbe32c5ee
In this basic example we will have a simple NATS based microservice setup, consisting of:
- An HTTP API external users of the service can make requests against
- A worker which processes the tasks being dispatched by the API server
The frontend HTTP API exposes a ‘/createTask’ to which we can send a requests and receive a response. Internally, the server will send a NATS request to the “tasks” subject and wait for a response from a worker which is subscribed to that subject to reply back once it has finished processing the request (or timeout, in case the response does not come back after 5 seconds).
For this example the server looks like:
package main
import (
"fmt"
"log"
"net/http"
"os"
"time"
"github.com/nats-io/go-nats"
)
type server struct {
nc *nats.Conn
}
func (s server) baseRoot(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Basic NATS based microservice example v0.0.1")
}
func (s server) createTask(w http.ResponseWriter, r *http.Request) {
requestAt := time.Now()
response, err := s.nc.Request("tasks", []byte("help please"), 5*time.Second)
if err != nil {
log.Println("Error making NATS request:", err)
}
duration := time.Since(requestAt)
fmt.Fprintf(w, "Task scheduled in %+v\nResponse: %v\n", duration, string(response.Data))
}
func (s server) healthz(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "OK")
}
func main() {
var s server
var err error
uri := os.Getenv("NATS_URI")
for i := 0; i < 5; i++ {
nc, err := nats.Connect(uri)
if err == nil {
s.nc = nc
break
}
fmt.Println("Waiting before connecting to NATS at:", uri)
time.Sleep(1 * time.Second)
}
if err != nil {
log.Fatal("Error establishing connection to NATS:", err)
}
fmt.Println("Connected to NATS at:", s.nc.ConnectedUrl())
http.HandleFunc("/", s.baseRoot)
http.HandleFunc("/createTask", s.createTask)
http.HandleFunc("/healthz", s.healthz)
fmt.Println("Server listening on port 8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
And the worker processing subscribed to NATS which will be processing the requests looks like:
package main
import (
"fmt"
"log"
"net/http"
"os"
"time"
"github.com/nats-io/go-nats"
)
func healthz(w http.ResponseWriter, r *http.Request) {
fmt.Println(w, "OK")
}
func main() {
uri := os.Getenv("NATS_URI")
var err error
var nc *nats.Conn
for i := 0; i < 5; i++ {
nc, err = nats.Connect(uri)
if err == nil {
break
}
fmt.Println("Waiting before connecting to NATS at:", uri)
time.Sleep(1 * time.Second)
}
if err != nil {
log.Fatal("Error establishing connection to NATS:", err)
}
fmt.Println("Connected to NATS at:", nc.ConnectedUrl())
nc.Subscribe("tasks", func(m *nats.Msg) {
fmt.Println("Got task request on:", m.Subject)
nc.Publish(m.Reply, []byte("Done!"))
})
fmt.Println("Worker subscribed to 'tasks' for processing requests...")
fmt.Println("Server listening on port 8181...")
http.HandleFunc("/healthz", healthz)
if err := http.ListenAndServe(":8181", nil); err != nil {
log.Fatal(err)
}
}
Let’s say that both of these workloads have their own set of dependencies, so our directory structure may look like something like the below. The only dependency then is the NATS client itself.
Code examples for api-server.go and worker.go are here: https://gist.github.com/wallyqs/7f72efdc3fd6371364f8b28cbe32c5ee
First, in our Docker Compose file (build.yml), we will declare how to build our dev setup and have it run as containers managed by the Docker engine:
version: "2"
services:
nats:
image: 'nats:0.8.0'
entrypoint: "/gnatsd -DV"
expose:
- "4222"
ports:
- "8222:8222"
hostname: nats-server
api:
build:
context: "./api"
entrypoint: /go/api-server
links:
- nats
environment:
- "NATS_URI=nats://nats:4222"
depends_on:
- nats
ports:
- "8080:8080"
worker:
build:
context: "./worker"
entrypoint: /go/worker
links:
- nats
environment:
- "NATS_URI=nats://nats:4222"
depends_on:
- nats
ports:
- "8181:8181"
Next, we use docker-compose -f build.ym build
to first create our containers:
Then, we start our services with docker-compose up:
In this example, we have the API server expose a /createTask
route, and doing a quick smoke test by sending a request with curl to confirm that requests are flowing through NATS:
And since we are using -DV in order to activate trace and debugging in the NATS server we can also confirm the traffic.
NOTE: This is ok for a dev/test environment (and for the purposes of this example) but not recommended for production, as it impacts performance.
So! There you have it. A quick example showing how simple NATS is. NATS - much like containers themselves - is all about simplicity and scalability.
NATS and Docker - a perfect match.
Want to get involved in the NATS Community and learn more? We would be happy to hear from you, and answer any questions you may have!
Follow us on Twitter: @nats_io
Back to Blog