Graceful Starting and Shutting down of Nodejs server
Node.js has become a powerhouse in modern web development, offering a non-blocking, event-driven architecture that is well-suited for real-time applications. However, as with any server-side technology, ensuring smooth and uninterrupted service is crucial. One fundamental concept that every Node.js developer should understand is graceful starting and shutting down. It refers to kickstarting a Node.js server in a way that allows it to handle incoming requests while it is still starting up. The graceful shutting down is the opposite of completing ongoing requests and releasing resources gracefully. The ultimate purpose is to ensure incoming requests are not lost or dropped while the server gets initialized or shut down without disrupting the user experience. In this article, we'll delve into what graceful starting means, why it's important, and how to implement it effectively in your Node.js applications.
If you find it insightful and appreciate my writing, consider following me for updates on future content. I'm committed to sharing my knowledge and contributing to the coding community. Join me in spreading the word and helping others to learn.
Graceful Starting Vs Graceful Shutdown
Graceful starting refers to the process of initializing a Node.js server in a manner that allows it to handle incoming requests while still undergoing initialization procedures. Traditionally, servers might reject or drop incoming requests during startup, leading to potential data loss or disruptions in user experience. Graceful starting aims to mitigate these issues by ensuring that incoming requests are queued or handled appropriately until the server is fully operational.
On the contrary, Graceful shutdown refers to the process of shutting down a Node.js server in a controlled manner, allowing it to complete ongoing requests and release resources gracefully. This helps prevent data loss, maintain data integrity, and ensure a seamless user experience during server shutdowns or restarts.
Importance
Be it starting or shutting down the Nodejs server, the primary objective is to ensure uninterrupted and consistent user experience with 24x7 availability. Let's analyze other factors one by one.
How It Works
Several key components contribute to achieving a graceful starting and shutting down process in the Node.js application. Let me articulate those steps in layman's terms.
Nodejs Implementation of Graceful Starting
In the following example, I utilize the Cluster module to create multiple worker processes, each capable of handling incoming requests independently. This ensures that the server can handle incoming requests while still undergoing initialization procedures, thus achieving graceful starting.
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork();
});
} else {
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello World\n');
});
server.listen(3000, () => {
console.log(`Worker ${process.pid} is running on port 3000`);
});
}
Nodejs Implementation of Graceful Shutdown
Let me enhance the else-block of the previous code snippet I have written above. The following snippet handles incoming connections and stores them in an array for tracking. Upon receiving a SIGTERM or SIGINT signal, indicating a shutdown request, the server closes all existing connections gracefully. It notifies clients that the server is restarting, destroys the connections, and finally exits the process.
领英推荐
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World\n');
});
server.listen(3000, () => {
console.log('Server started');
});
let connections = [];
server.on('connection', (connection) => {
connections.push(connection);
connection.on('close', () => {
connections = connections.filter((curr) => {
return curr !== connection;
});
});
});
function closeConnections() {
console.log('Closing connections');
connections.forEach((connection) => {
connection.end('Server is restarting\n');
connection.destroy();
});
}
process.on('SIGTERM', () => {
console.log('Received SIGTERM');
server.close(() => {
console.log('Server closed');
closeConnections();
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('Received SIGINT');
server.close(() => {
console.log('Server closed');
closeConnections();
process.exit(0);
});
});
process.on('uncaughtException', err => {
console.log(`Uncaught Exception: ${err.message}`)
process.exit(1)
});
Handling Shutdown in a Dockerized Node.js Application
When running a Node.js application in a Docker container, there are additional considerations to take into account. When Docker is asked to stop a running container, it sends a usual SIGTERM signal to the main process running in the container. It does the same thing by finishing what it's currently doing, cleaning up as needed, and then terminating.
However, if the process does not terminate within a certain period (~10 seconds by default), Docker will then send a SIGKILL signal to forcibly terminate the process. This is akin to pulling the plug on the application - it won't have a chance to finish what it's doing or clean up. Sometimes, our application may need more than 10 seconds to shut down gracefully. For example, it might need to finish processing a long-running request, or it might need to wait for a database transaction to commit. In such cases, we can tell Docker to wait longer before it sends the SIGKILL signal, by using the --stop-timeout option when we run our Docker container. Example:
docker run --stop-timeout 30 my-nodejs-app
Continuous Health Checking Operation
As I stated above, the health check is a continuous and essential process for handling the Nodejs server gracefully. Usually, a load balancer uses health checks to determine if an application instance is healthy and can accept requests. Kubernetes also does the same thing to check the liveness and readiness of the server. If you want to learn more about the K8s health check process, you may explore their official documentation. Otherwise, there are some NPM packages available to make the custom implementation as follows.
const express = require('express')
const actuator = require('express-actuator')
const app = express()
app.use(actuator())
app.listen(3000)
I have used express-actuator middleware in this example. There are other NPM packages as well like Lightship, Terminus to perform the same operation.
Request Buffering by Reverse Proxy
Let's deep dive into another approach of request buffering by Reverse proxy. It buffers incoming requests while the node gets started up. This buffering mechanism ensures that client requests are held in a queue and processed once the node becomes available, minimizing downtime and improving the user experience. Typically, there are a few high-level steps to be followed:
This is how zero downtime is maintained at the production level. If you want to learn more about how AWS or Azure cloud platform achieves it, you might be interested in their official documentation.
If you like this article, please like, share, and let me know your feedback in the comment section. Stay tuned for the upcoming articles!