We love Node.js, and we’re very pleased to offer it to our clients as our solution of choice. However, due to Node being relatively new and sometimes being viewed as a shiny toy, I notice regularly that considerations regarding the performance, scale and availability of applications built using it, sometimes fall by the wayside.
Here’s some rules we stick by to ensure that our applications consistently deliver.
Please note: This article assumes an understanding of Node.js and developing web applications using it.
Don’t directly expose Node to web traffic
It’s very easy to fall into this trap – You see a package like Express and think “Awesome! Let’s get started” – you code away and you’ve got an application that does what you want. This is excellent and, to be honest, you’ve won a lot of the battle. However, you will lose the war if you upload your app to a server and have it listen on your HTTP port, because you’ve forgotten a very crucial thing: Node is not a web server.
As soon as any volume of traffic starts to hit your application, you’ll notice that things start to go wrong: connections are dropped, assets stop being served or, at the very worst, your server crashes. What you’re doing is attempting to have Node deal with all of the complicated things that a proven web server does really well. Why reinvent the wheel?
Use a real web server! I’ve used both Apache and nginx for real-world Node applications, with my preference being the latter. What they do really well is proxy connections to your application (or a group of clustered applications – more on this later), freeing up your Node application to do what it does best – speak to databases, handle your business logic and generate responses.
If you’d like to learn more about setting up a webserver to proxy to a Node instance, I’ve included the following tutorial links below:
Which leads me on to my next point:
Don’t use Node to serve static assets
You set up a new Node app, then you set up a route or mount-point to serve your static assets like client JS, CSS, fonts or images.
This is not good, because again Node doesn’t do this efficiently. Say we ask for a reasonably large image file and we’re serving it from a Express mount-point, this works when we’re developing it locally, because it’s just us using it. No load really.
But let’s think of this from a flow perspective:
- Request the file /catinabox.jpg
- Express checks the file system to see if it exists
- Express reads the file into memory using fs.readFileStream() or a similar variant
- Express then has to figure out what type of file it is to set the appropriate response headers
- It also needs to figure out what’s going on in terms of caching and tagging the response for the client
- Express then writes the file back to the response stream
This is just for one request, for one image and bearing in mind this is memory that your application could be using for important stuff like reading a database or handling complicated logic; why would you cripple your application for the sake of convenience?
There are actually a couple of ways to combat this:
- Configure your web server to serve static assets itself. There are lots of guides for using pattern matching for these assets, alongside a Node upstream proxy
- Don’t serve any assets locally, unless you’re in production. Use a content delivery network (CDN) to disperse these assets across the world and have the network decide where to serve them from. This leads to a snappy, responsive application and next to no load on your application server.
Don’t use ANY synchronous methods while serving requests
If you come from a synchronous programming background its really easy to fall into the trap of using the *sync methods like fs.readFileSync() for convenience. Node.js should always be an asynchronous environment. Here’s why:
As soon as you start serving more than one request at a time (e.g. any website) your code has to wait for any synchronous requests to finish, before it’ll even listen to another connection request. This is a big problem. If, for example, you’re reading a file from the filesystem, no other request will be fulfilled until the file is read for the request that asked for it. Your beautiful, responsive application becomes a lumbering beast.
I’ve seen people fall into this trap and it’s so easy to fix!
Your application will be responsive even if it has to deal with a massive file! Excellent.
Don’t expect Node to recover after an error
If like me, you came from a largely Apache/PHP based background, this one will seem familiar. You write a PHP app and upload it to the server first off, any changes are generally instantly visible. No configuration or starting and stopping services. Also, if there’s an error in a part of the application, just that part of the application will be broken.
Not so in Node.js – if something breaks – the whole service goes down!
Node follows the same model, but in a server environment no one is there to refresh the page or respond to the issue. It just sits there, broken.
This is a huge problem for highly-available services like the ones we deploy. We can’t have that. Luckily there are some solutions. There are always solutions.
- Use a process supervisor like forever or supervisor. What these do is watch your Node application and, if they detect that an instance of the app is in an error state, they’ll restart it instantly, resulting in minimal downtime
- Use Node’s built in cluster module to ‘spawn’ multiple versions of the same app that listen on the same socket. The ‘cluster master’ handles creating new instances (generally the rule of thumb is one instance for each physical CPU core on the server machine) and also takes care of restarting failed instances.
Read the Cluster documentation in the Node API for more information about how to get the latter solution to work: https://nodejs.org/api/cluster.html
Introduce instrumentation and logging straight away
So we’ve installed a solution to keep our application up and serving requests – but how do we find out about errors? Why did it crash? How do we see what’s slowing things down?
These questions can generally be answered by instrumentation and logging. In our applications we have an instrumentation class that times things to see where the bottlenecks are as well as providing in depth logging that can be accessed through our logs interface.
I cannot stress enough how important it is to bake this into your application right from the get go. With this in place, you can rest easy knowing what’s causing issues. When building a component, ask yourself: “Will I need to know about this later? Can I decipher the response as successful or not? Is this going to be a bottleneck?”. Implement instrumentation from the get go and you will know!
Alongside the reporting, we built a basic alerting system in our module that sends an email when certain constraints are met, or if there were requests that we deemed as a failure. It could easily be extended using Twilio to send text messages or even call us! I hope to open source this for your own applications in the near future.
We utilise our own solution alongside a service such as New Relic, which allows us to get instant alerts when things go wrong. It also allows for us to monitor our server performance which informs us as to whether portions of our application are taking up too much memory. I’d recommend signing up for a trial; there’s a Node plugin that’s very easy to install.
Thanks for reading, hopefully you learnt something new today – Next time I’ll be talking about integrating these practices alongside a continuous deployment methodology. Follow me @tomhallam for more.