Here on the product team at Joyent, we enjoy making sophisticated applications easy to deploy and manage. For us, that means containerizing components, automating them with ContainerPilot and the Autopilot Pattern. This typically means that we are containerizing the components that comprise our solutions with Docker and are using a service discovery catalog like Consul. Up until this year, we didn’t have a working solution to solve the message queuing problem. That is, until we decided to leverage NATS to address our message queuing needs.
In order to demonstrate the usefulness of the Autopilot Pattern we’ve relied on a couple of Node.js example applications. Both of these application use a free and open-source Go product named ContainerPilot. ContainerPilot is intended to run inside of a Docker container and provides a number of benefits to your application, most notably is simplified Consul registration, service change notifications, telemetry reporting for Prometheus, and lifecycle events that are relevant to running in a container. Neither of the Node.js examples demonstrated how to integrate a message queueing server into the Autopilot Pattern.
Prior to integrating NATS into the Node.js Example, the example application was very basic. There is a detailed blog post explaining how each service worked together and could be scaled up and down easily with help from ContainerPilot. The application relies on a SmartThings hub pushing sensor data to a collector service. From here, the data is split into the type sensor it relates to and is stored in a database accordingly. This works well for a single hub, but once you start pushing loads and loads of sensor data to a single service then the sensor workers that format and serialize the data can have difficulty keeping up. Below is an architectural diagram that illustrates the pre-NATS example application.
We chose to bring NATS into the architecture for several reasons. First, it’s written in Go, which is a language we already enjoy developing in, e.g. ContainerPilot is written in Go. Secondly, it has a Docker image that works well with minimal configuration. Lastly, NATS has a simple, easy to understand, API with a well supported Node.js client. This meant that it would be somewhat trivial to integrate it into an existing Node.js application.
However, to use NATS in an Autopilot Pattern application meant that we needed to make NATS work well with a service discovery catalog and be able to run with ContainerPilot starting it. This was actually straightforward to accomplish because of the configurable nature of NATS. We are discovering other NATS instances to cluster by using consul-template and Consul. ContainerPilot takes care of registering NATS with Consul and then notifies each NATS instance when a new NATS service is registered with Consul. As a result, the gnatsd.conf configuration file is regenerated with the current list of healthy NATS servers and the gnatsd process is sent a hangup signal to reload the configuration. All of the code that makes NATS work in this way can be found on the NATS repo under the autopilotpattern github organization. As a result of the service discovery catalog and local notifications for changes to the catalog, when NATS is running with the Autopilot Pattern it’s able to auto-cluster itself as new instances are introduced or old ones are destroyed.
After we had a working NATS Docker image that implemented the Autopilot Pattern, we were ready to integrate it with the existing Node.js Example. We decided to insert NATS between the sensor data receiver service and the sensor data workers that formatted the data. Inside of NATS we have a message queue for each of the types of data we care about, and one or more workers that can pull messages off the queue, format them, then send them to a service to save in a database. With the updated architecture the workers are able to scale easily as demand changes without having to increase the number of services receiving data in front of NATS. This also meant that the service receiving the sensor data from the external SmartThings hub only needs to concern itself with sending data to the correct queue, and doesn’t need to worry about pushing data to workers. Below is the current architectural diagram for the Node.js example with NATS and the workers introduced.
Going forward we plan to continue to build solutions on top of NATS and the Autopilot Pattern. NATS really is a wonderful queueing solution that is a pleasure to work with, especially inside of Node.js applications. We also value open-source at Joyent, therefore, keep an eye out on our github orgs at autopilotpattern and joyent for future solutions.