Mastering Zero‑Downtime Deployments with Spring Boot Microservices
This article explains how to achieve lossless releases for Spring Boot microservices by deregistering old instances, handling shutdown hooks, configuring graceful shutdown in Spring Boot 2.3+, and integrating custom interceptors for various containers and registration centers.
Why Zero‑Downtime Releases Matter
In today’s fast‑moving internet era, microservice architecture is the default for enterprise applications, and Spring Boot is the most popular Java framework because of its simplicity, flexibility, and maintainability. However, releasing new versions can be risky; a lossless (zero‑downtime) release ensures users never notice the update, improving experience and avoiding business loss.
Principles and Steps of Lossless Release
All service nodes register with a discovery center, and callers obtain node information from it. To achieve lossless release, follow these steps:
Deploy the new version and verify its health (e.g., health checks, gray testing).
Deregister the old version’s nodes from the registry.
Wait for the registry to propagate the change and for in‑flight requests to finish.
Deregistering Nodes
Three implementation approaches exist:
Manually call the registry’s API to change node status, then stop the node after a delay.
Write a script that first changes the node status via the registry API.
Let the application handle the shutdown signal (SIGTERM), invoke the registry API, and wait before stopping.
The third method is preferred because it is self‑contained within the application. When a Spring Boot app receives SIGTERM, the JVM triggers a Shutdown Hook via Runtime.getRuntime().addShutdownHook(Thread). However, the hook runs asynchronously, and Spring beans may already be destroyed, making it unsafe to deregister nodes directly.
Spring’s Built‑in Shutdown Hook
// org.springframework.boot.SpringApplication#refreshContext(ConfigurableApplicationContext context)</code><code>// org.springframework.context.support.AbstractApplicationContext#registerShutdownHook()</code><code>@Override</code><code>public void registerShutdownHook() {</code><code> if (this.shutdownHook == null) {</code><code> this.shutdownHook = new Thread() {</code><code> @Override</code><code> public void run() {</code><code> synchronized (startupShutdownMonitor) {</code><code> doClose();</code><code> }</code><code> }</code><code> };</code><code> Runtime.getRuntime().addShutdownHook(this.shutdownHook);</code><code> }</code><code>}</code><code>protected void doClose() {</code><code> if (this.active.get() && this.closed.compareAndSet(false, true)) {</code><code> // publish ContextClosedEvent, stop lifecycle beans, destroy singletons, etc.</code><code> // ...</code><code> this.active.set(false);</code><code> }</code><code>}To reliably deregister nodes, we can listen for ContextClosedEvent and perform the deregistration before the beans are destroyed.
Custom Listener Example
import org.springframework.context.ApplicationListener;</code><code>import org.springframework.context.event.ContextClosedEvent;</code><code>public class GracefulShutdown implements ApplicationListener<ContextClosedEvent> {</code><code> @Override</code><code> public void onApplicationEvent(ContextClosedEvent event) {</code><code> // TODO: deregister node, wait for client updates</code><code> }</code><code>}Graceful Shutdown Configuration in Spring Boot 2.3+
From Spring Boot 2.3.0.RELEASE (inclusive), graceful shutdown is built‑in. Add the following properties:
# Enable graceful shutdown</code><code>server.shutdown=graceful</code><code># Maximum time to wait for in‑flight requests (default 30s)</code><code>spring.lifecycle.timeout-per-shutdown-phase=30sFor versions prior to 2.3.0, custom implementations are required for each embedded container.
Tomcat Example
import org.apache.catalina.connector.Connector;</code><code>import org.apache.tomcat.util.threads.ThreadPoolExecutor;</code><code>import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer;</code><code>import org.springframework.context.ApplicationListener;</code><code>import org.springframework.context.event.ContextClosedEvent;</code><code>public class GracefulShutdown implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {</code><code> private volatile Connector connector;</code><code> @Override</code><code> public void customize(Connector connector) { this.connector = connector; }</code><code> @Override</code><code> public void onApplicationEvent(ContextClosedEvent event) {</code><code> this.connector.pause();</code><code> Executor executor = this.connector.getProtocolHandler().getExecutor();</code><code> if (executor instanceof ThreadPoolExecutor) {</code><code> ((ThreadPoolExecutor) executor).shutdown();</code><code> }</code><code> }</code><code>}Undertow provides GracefulShutdownHandler but Spring Boot does not integrate it yet; similar logic can be applied.
Handling MQ Consumers and Bean Destruction
For message‑driven beans, stop the consumer and wait for pending messages before shutdown. Trigger points include listening to ContextClosedEvent, using @PreDestroy, or implementing DisposableBean.
Practical Implementation in Our Company
We packaged a common JAR that provides:
A WebMvcProperties class with configurable timeouts (Eureka update wait, post‑update wait, container termination wait).
An IShutdownInterceptor interface for custom pre‑ and post‑shutdown logic.
An EurekaClientShutdownInterceptor that sets the instance status to OUT_OF_SERVICE, polls until the status propagates, and then waits for the configured post‑update delay.
@Data</code><code>@ConfigurationProperties(prefix = "spring.webmvc")</code><code>public class WebMvcProperties {</code><code> private long awaitEurekaUpdate = 60000;</code><code> private long awaitAfterEurekaUpdated = 30000;</code><code> private int awaitTermination = 10000;</code><code>} public interface IShutdownInterceptor extends Ordered {</code><code> default void beforeShutdown() {}</code><code> default void afterShutdown(ThreadPoolExecutor threadPoolExecutor) {}</code><code>} @Slf4j</code><code>public class EurekaClientShutdownInterceptor implements IShutdownInterceptor {</code><code> private final WebMvcProperties props;</code><code> private final EurekaClient client;</code><code> private final EurekaHttpClient httpClient;</code><code> @Override</code><code> public void beforeShutdown() {</code><code> InstanceInfo info = client.getApplicationInfoManager().getInfo();</code><code> httpClient.statusUpdate(info.getAppName(), info.getId(), InstanceInfo.InstanceStatus.OUT_OF_SERVICE, info);</code><code> // wait for status propagation ...</code><code> }</code><code> @Override</code><code> public void afterShutdown(ThreadPoolExecutor executor) {</code><code> executor.awaitTermination(props.getAwaitTermination(), TimeUnit.SECONDS);</code><code> }</code><code> @Override public int getOrder() { return 100; }</code><code>}Auto‑configuration registers the graceful shutdown bean and, when Eureka is on the classpath, registers the Eureka interceptor.
@Configuration</code><code>@EnableConfigurationProperties(WebMvcProperties.class)</code><code>public class WebmvcComponentAutoConfiguration {</code><code> @ConditionalOnClass({TomcatConnectorCustomizer.class, TomcatServletWebServerFactory.class})</code><code> static class ShutdownGracefulConfiguration {</code><code> @Bean</code><code> public GracefulShutdown gracefulShutdown(@Autowired(required = false) List<IShutdownInterceptor> interceptors) {</code><code> return new GracefulShutdown(interceptors);</code><code> }</code><code> }</code><code> @ConditionalOnClass({EurekaClient.class, EurekaHttpClient.class})</code><code> @AutoConfigureBefore(ShutdownGracefulConfiguration.class)</code><code> static class EurekaShutdownConfiguration {</code><code> @Bean @ConditionalOnMissingBean @ConditionalOnBean({EurekaClient.class, EurekaHttpClient.class})</code><code> public EurekaClientShutdownInterceptor eurekaClientShutdownInterceptor(WebMvcProperties props, EurekaClient client, EurekaHttpClient http) {</code><code> return new EurekaClientShutdownInterceptor(props, client, http);</code><code> }</code><code> }</code><code>}Operational Platform Support
The platform provides health‑check configuration, manual node up/down, custom pre‑shutdown scripts (supporting Eureka and Nacos), and readiness/liveness probes for Kubernetes deployments. It also guides proper SIGTERM handling (using exec or trap) to ensure the Java process receives the termination signal.
By selecting the appropriate mechanism and configuring the shared components once, teams can achieve smooth, zero‑downtime upgrades across their Spring Boot microservices.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
G7 EasyFlow Tech Circle
Official G7 EasyFlow tech channel! All the hardcore tech, cutting‑edge innovations, and practical sharing you want are right here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
