Graceful Starting and Shutting down of Nodejs server

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.

  1. Maintaining Uptime for Seamless User Experience: As I have stated above, Graceful starting ensures that your server can handle incoming requests even during the initialization process. This prevents downtime and ensures that users can access your application without interruptions. Similarly, graceful shutdown ensures that ongoing requests are completed before the server is stopped, minimizing downtime and ensuring a seamless user experience without encountering errors.
  2. Preventing Data Loss: During the starting or shutting down process of a server, abrupt termination can result in data loss or corruption. Graceful starting and shutting down allow the server to handle ongoing transactions and connections properly, ensuring data integrity and preventing loss of critical information.
  3. Handling High Traffic: In scenarios where there is a sudden surge in traffic or when deploying updates, graceful starting and shutting down become even more critical. By allowing the server to handle incoming requests gracefully, even during high-load or maintenance activities, you can ensure that the application remains responsive and accessible to users.
  4. Resource Management: Graceful shutting down allows the server to release resources properly, such as closing open connections, freeing up memory, and terminating processes. This helps optimize resource utilization and ensures that resources are available for other applications or processes running on the server.
  5. Avoiding Service Disruptions: Without graceful starting and shutting down mechanisms, servers may experience service disruptions, such as dropped connections, incomplete transactions, or unexpected errors. Graceful handling of server initialization and shutdown helps mitigate these risks, ensuring consistent and reliable service delivery.

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.

  1. Request Queuing: During server initialization, incoming requests are queued rather than rejected. Queuing ensures that no requests are lost and can be processed once the server is fully operational. This prevents the loss of incoming requests during startup and ensures a smooth user experience.
  2. Health Checks: A health check is a continuous process to assess the readiness of the server before accepting incoming requests. It also verifies that necessary resources are available, and dependencies are initialized.
  3. Signal Handling: Signal handling involves intercepting signals such as SIGTERM and SIGINT, which are commonly used for graceful shutdowns. When a shutdown signal is received, the server initiates the shutdown process, allowing existing connections to complete before exiting.
  4. Connection Draining: Before shutting down the server, existing connections are allowed to complete their operations gracefully. Connection draining ensures that ongoing transactions are not abruptly terminated, maintaining data integrity and preventing disruptions for users. It involves notifying clients that the server is shutting down and allowing them time to complete their requests before closing the connections.
  5. Cleanup Procedures: Cleanup procedures involve releasing resources, closing database connections, and performing any necessary cleanup tasks before shutting down the server completely. These procedures ensure that resources are properly released, preventing resource leaks and optimizing resource utilization for better stability and reliability of the server environment.

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.

Download from internet

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:

  1. Set up a reverse proxy: Choose a reverse proxy server such as Nginx or HAProxy and configure it to forward incoming requests to your node's backend server.
  2. Enable buffering: Configure the reverse proxy to enable request buffering. This allows the reverse proxy to accept incoming requests even when the backend node is not yet ready to handle them.
  3. Set buffer size and timeout: Specify the buffer size and timeout values based on your application's requirements. The buffer size determines how many requests can be held in the queue, while the timeout determines how long the reverse proxy will wait for the backend node to become available before returning an error to the client.
  4. Drain the buffer: Once the backend node is ready to handle requests, signal the reverse proxy to forward the buffered requests. This can be achieved by customizing the configuration or using specific commands or API calls provided by the reverse proxy server.

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!


要查看或添加评论,请登录

社区洞察

其他会员也浏览了