Mastering Graceful Shutdown in Distributed Systems and Microservices

Jainal Gosaliya
8 min readMar 17, 2024

--

Introduction

In the intricate landscape of distributed systems and microservices, ensuring seamless transitions during shutdown procedures is paramount. Graceful shutdown, a concept pivotal to this endeavor, not only minimizes disruptions but also safeguards data integrity. This blog elucidates the intricacies of graceful shutdown and offers expert strategies for its seamless implementation.

Understanding Graceful Shutdown

In the intricate dance of distributed systems and microservices, graceful shutdown stands as a crucial choreography. It orchestrates the cessation of services with finesse, minimizing disruptions and upholding data integrity. From updating deployments to scaling down resources, its necessity in handling failures is indisputable. Yet, its execution is riddled with challenges, from managing in-flight requests to preserving data consistency amidst dependencies.

Strategies for Graceful Shutdown

1. Signal Handling: The initiation of graceful shutdown often hinges on signals like SIGTERM. Crafting robust signal handling mechanisms is essential, tailored to the idiosyncrasies of different programming languages and frameworks.

2. Connection Draining: As requests flow ceaselessly, connection draining techniques ease the burden of ongoing interactions. Load balancer configurations play a pivotal role, gradually diverting traffic away from services with finesse.

3. Dependency Management: In the intricate web of microservices, dependencies intertwine. Managing them during shutdown requires delicacy, preventing cascading failures by gracefully handling upstream and downstream services.

4. Data Integrity: As curtains draw close, preserving data integrity becomes paramount. Techniques ranging from flushing buffers to persisting state ensure the sanctity of information, even amidst long-running processes or background tasks.

Context of Graceful Shutdown

In the context of graceful shutdown, this script showcases several key aspects:

  • Signal Handling: Termination signals (SIGINT, SIGTERM) trigger the graceful shutdown process, allowing the application to clean up resources.
  • Producer-Consumer Pattern: The script demonstrates how to gracefully stop a producer and wait for consumers to finish processing remaining tasks before exiting.
  • Asynchronous Programming: Asynchronous tasks are used to manage concurrent operations efficiently, ensuring responsiveness during shutdown procedures.
  • Containerization: The Dockerfile enables seamless deployment of the application in containerized environments, facilitating scalability and resource isolation.

Kubernetes Integration

Kubernetes gives us a fine grain control for handling graceful shutdowns.

The Kubernetes deployment configuration ensures that the application gracefully handles pod termination, enhancing reliability and resilience in a distributed environment.

Well, in production the lifecycle of kubernetes pod termination is as follows:

Kubernetes Pod Termination
  1. User Initiates Pod Deletion: The process starts when a user submits a request to delete a Pod through the Kubernetes API.
  2. Kubelet Receives Pod Deletion Event: The Kubernetes control plane (e.g., API server) sends a Pod deletion event to the Kubelet running on the node where the Pod is scheduled.
  3. Kubelet Calls PreStop Hook (if defined): If a preStop hook is defined in the Pod specification, the Kubelet calls this hook before terminating the containers. The preStop hook allows containers to perform cleanup operations.
  4. PreStop Hook Completes: After the preStop hook finishes executing, the Kubelet proceeds with the termination process.
  5. Kubelet Sends SIGTERM Signal: The Kubelet sends a SIGTERM signal to the main process inside each container in the Pod, signaling it to gracefully terminate.
  6. Graceful Shutdown Period: Kubernetes provides a configurable grace period (typically 30 seconds by default) for the containers to handle the SIGTERM signal, perform cleanup tasks, and shut down gracefully.
  7. Application Shuts Down: During the graceful shutdown period, the application running inside the containers should handle the SIGTERM signal, flush any data to disk, and cleanly stop any running processes.
  8. Kubelet Sends SIGKILL (if necessary): If the grace period expires and the containers have not terminated, the Kubelet sends a SIGKILL signal to forcibly terminate the remaining processes within the containers.
  9. Kubelet Notifies API Server of Pod Termination: After all containers have been terminated, the Kubelet notifies the Kubernetes API server that the Pod has been terminated.
  10. API Server Confirms Pod Deletion: The API server confirms the Pod deletion to the user who initiated the request.

In terms of resource cleanup such as network, Storage & pod itself following is the lifecycle for it:

Kubernetes Pod Termination (cleanup)
  1. User Initiates Pod Deletion: The process begins when a user executes the kubectl delete pod command to delete a Pod.
  2. Container Runtime Interface (CRI): The kubelet interacts with the Container Runtime Interface (CRI) to stop the Pod. The CRI sends a SIGTERM signal to the Pod to initiate a graceful shutdown.
  3. Graceful Shutdown Period: The Pod has a configurable grace period (typically 30 seconds by default) to handle the SIGTERM signal, perform cleanup tasks, and shut down gracefully.
  4. Container Network Interface (CNI): After the Pod has terminated, the Container Network Interface (CNI) is responsible for removing the network resources associated with the Pod, such as network interfaces and IP addresses.
  5. Container Storage Interface (CSI): The Container Storage Interface (CSI) removes any persistent volumes or storage resources that were attached to the Pod.
  6. Pod Removal: Once the network and storage resources have been cleaned up, the Pod object itself is removed from the Kubernetes cluster.
  7. Cluster State Update: The Kubernetes control plane updates the cluster’s desired state to reflect the removal of the Pod.

Important, thing to note here is that resources such as Network and Volumes are removed after the pod is removed. But, when we are gracefully terminating a pod we want all the pending jobs to complete and send the results to the existing connected clients. But, we also, dont want to accept new client connections and new jobs and those new connections should be redirected to the new pod that was started as soon as we send the pod termination to current pod. So, how do we restrict new client to connect to the pod during the pod-termination while also, actively maintain existing clients. By, allowing the pod to intercept, SIGTERM signals and set up readinessProbe

readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5

Through the readinessProbe kubernetes will call the endpoint defined in the path and will check the status code as 200. The period duration of this is done through periodSeconds. Once, the application sends a non-200 signal. The duration Kubernetes waits before considering the probe as failed depends on the failureThreshold setting in the probe configuration. The failureThreshold specifies the number of consecutive failures that must occur before the probe is considered to have failed. By default, the failureThreshold is set to 3, meaning that if the probe fails 3 times in a row, Kubernetes will consider the probe to have failed.

Now for Graceful shutdowns and efficiently handle client connections, your application should intercepts a SIGTERM signal and start sending a non-200 status code. During a readiness probe, Kubernetes marks the pod as not ready. This means the pod will not receive new traffic from the service, but it does not immediately terminate existing connections. The existing connections are maintained until they are closed or a timeout is reached. This behavior is crucial for graceful shutdown, allowing the pod to finish processing current requests before being terminated.

However, it’s important to note that while the pod is marked as not ready and stops receiving new traffic, it can still make outbound requests if needed. This means that if your application has dependencies on external services or needs to complete processing for existing connections, it can do so without interruption. The pod’s ability to make outbound requests is not affected by its readiness status.

In summary, if your application intercepts a SIGTERM signal and starts sending a non-200 status code during a readiness probe, Kubernetes will mark the pod as not ready and stop sending new traffic to it. However, existing client connections are not immediately terminated and can continue to be processed until they are closed or a timeout occurs. This ensures that your application can gracefully shut down, completing any necessary processing before the pod is terminated.

By incorporating these elements, we can exemplifies best practices for implementing graceful shutdown in distributed systems and microservices, ensuring stability and data integrity even during transitions.

Implementing Graceful Shutdown: A Code Walkthrough

To understand how graceful shutdown is implemented in practice, let’s dissect the provided Python script along with its Dockerfile and Kubernetes deployment configuration.

The Python Script

This Python script simulates a distributed system where there’s a producer adding events to a shared queue and multiple consumers processing these events concurrently. Here’s a breakdown of what’s happening:

  • The producer function simulates adding events to the queue at regular intervals.
  • The heavy_task function simulates CPU-bound processing.
  • The consumer function dequeues events from the queue, processes them, and marks them as done.
  • The signal_handler function catches termination signals (SIGINT and SIGTERM) and sets the stop_event.
  • In the main function, the producer and consumers are started as asynchronous tasks. Upon receiving a termination signal, the script waits for the producer to stop and then waits for all consumers to finish processing remaining tasks before exiting.

Dockerfile

This Dockerfile sets up the Python environment, copies the Python script (async.py) into the container, and sets it as the entrypoint.

Running the code in the docker container looks like this:-

Graceful Shut Down as a Docker Container

As you can see the when the sigterm signal is sent from the docker cli, the producer script stops producing events to the queue and the consumer processes events from the queue, when all the remaining tasks are completed then only the container stops as compared to abruptly killing the container on docker container stop.

Kubernetes Deployment Configuration

Having the same image running in the container yields the same result as seen in the below gif.

This Kubernetes Deployment configuration deploys the containerized application (`jainal09/graceful-shutdown`) with one replica. It specifies a termination grace period of 300 seconds, allowing time for graceful shutdown when a termination signal is received.

Graceful shut down in kubernetes

Testing and Validation

In the crucible of reliability, testing shutdown procedures is non-negotiable. Through chaos engineering or integration testing, the efficacy of graceful shutdown mechanisms is validated, fortifying systems against potential disruptions.

Monitoring and Observability

As the symphony of shutdown unfolds, monitoring and observability serve as vigilant guardians. Detecting and diagnosing issues in real-time, they ensure the harmony of services amidst transitions. Key metrics and monitoring tools provide a compass for navigating these turbulent waters.

Conclusion

Graceful shutdown isn’t merely a technical maneuver; it’s the backbone of system stability and reliability. By mastering its intricacies, experts in distributed systems and microservices fortify their infrastructure against disruptions, ensuring seamless transitions even in the face of uncertainty.

#GracefulShutdown #DistributedSystems #Microservices #DevOps #Kubernetes #SystemReliability #TechBlog

--

--