跪拜 Guibai
← All articles
Backend

Why Clicking 'Offline' in Nacos Still Routes Traffic to a Dead Instance

By 神奇小汤圆 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Every rolling deployment that skips these steps produces user-facing `Connection Refused` or `Read Timeout` errors during the release window. The three-part pattern — PreStop deregistration, client retry on connect failure, and graceful shutdown — eliminates those errors without slowing down deployments, and the retry safety argument (no business logic executed) makes it auditable for compliance-sensitive systems.

Summary

The gap between a Nacos instance going offline and every gateway learning about it is a cache-propagation delay, not a registry bug. Gateways and microservices using Ribbon or Spring Cloud LoadBalancer poll for updates on a timer; UDP push notifications can be lost in production networks. During that window, requests still hit the old IP, and if the process is already killed, the OS returns a RST packet and the caller gets a `Connection Refused`.

Small teams often insert a `sleep 40` into their CI/CD pipeline after calling the Nacos deregistration API, letting the machine keep serving while caches converge. That approach works but makes rolling a 500-node service take hours and offers zero protection against sudden machine failures.

A production-grade lossless shutdown ties three mechanisms together: a K8s PreStop hook that deregisters the Pod from Nacos before termination, client-side retry in the load balancer that catches `ConnectException` at the TCP handshake stage and silently retries another IP, and Spring Boot's graceful shutdown that drains in-flight requests before the process exits. The retry is safe because a connection refused at the TCP layer means no application logic ever ran, making the operation effectively idempotent.

Takeaways
Clicking 'offline' in Nacos or sending `kill -15` does not immediately remove an instance from every caller's local ServerList cache; the refresh interval and unreliable UDP push create a window of tens of seconds where stale IPs are still targeted.
Inserting `sleep 40` after deregistration lets caches converge while the instance keeps serving, but it makes large-cluster rollouts impractically slow and fails entirely when a machine crashes without warning.
A K8s PreStop hook should call the Nacos API to deregister the Pod before the container receives SIGTERM, with a short 3–5 second pause to allow UDP push propagation.
Enabling Spring Cloud LoadBalancer client retry catches `Connection Refused` at the TCP handshake phase, where no application data has been sent, making the retry safe and idempotent; the load balancer silently swaps in a healthy IP.
Spring Boot's `server.shutdown: graceful` with a `timeout-per-shutdown-phase` drains in-flight requests after new traffic has been diverted, preventing mid-request failures during shutdown.
Conclusions

The `sleep 40` pattern is a surprisingly common interview answer, which suggests many engineers treat the registry as real-time and never trace the cache-update path end to end.

The safety of the client retry hinges on a low-level TCP detail: a `ConnectException` means the three-way handshake never completed, so the HTTP request body was never transmitted and no business side effect occurred. That turns a network error into a free retry.

Binding the deregistration call to the K8s PreStop hook rather than an external CI script makes the shutdown self-contained; the Pod itself is responsible for leaving the pool cleanly, which also handles unplanned scale-down events that a Jenkins pipeline would never see.

Concepts & terms
Nacos ServerList cache
A local in-memory list of service instance IPs maintained by client-side load balancers like Ribbon or Spring Cloud LoadBalancer. It is updated on a polling interval and optionally via UDP push, which means it can be stale for tens of seconds after a Nacos server-side change.
K8s PreStop Hook
A lifecycle hook in Kubernetes that executes a command inside a container immediately before the container receives SIGTERM. It is used here to deregister the Pod from Nacos so that no new traffic is routed to it before shutdown begins.
Client-side retry on ConnectException
A load-balancer retry mechanism that catches TCP connection-refused errors before any HTTP data is sent. Because the three-way handshake failed, no application logic executed, making the retry idempotent and safe to perform transparently.
Graceful shutdown (Spring Boot)
A server configuration (`server.shutdown: graceful`) that causes the application to stop accepting new requests and wait for in-flight requests to complete up to a configured timeout before destroying the application context and releasing resources.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗