背景:

服务更新上线或重启,网站会出现请求接口错误,单纯的靠k8s自带的健康检测是无法实现平滑上线,以下为平滑上线的解决方案

一、优雅停机的好处

  • 保障业务连续性 :确保正在进行的请求能顺利完成,避免业务中断。
  • 优化资源利用 :优雅停机时可及时释放数据库连接池、消息队列连接等资源,提高资源利用率。
  • 提升系统稳定性 :平稳过渡到新状态,减少因强制停止引发的异常,降低服务雪崩等故障风险。
  • 减少错误与提高容错能力 :通过特定终止逻辑处理未完成任务,减少错误发生,增强系统容错性。
  • 便于运维管理 :为运维人员提供控制和灵活性,利于部署新版本、维护等操作,提高运维效率。
  • 提高用户体验 :避免服务突然中断,保障用户操作顺利进行,提升整体体验。
  • 便于故障排查 :停机前记录日志和状态信息,为后续故障排查提供重要依据。

二、实现原理

  1. 服务默认不上线:通过-Dspring.cloud.nacos.discovery.instanceEnabled=false参数设置服务注册到NACOS默认不上线,启动不接收服务发现流量。
  2. startupProbe:配置容器启动后过一段时间进行请求健康检测接口,健康检测通过后把该服务Nacos的状态设置为上线,接收服务发现流量。
  3. livenessreadiness 探针配合 :liveness 探针判断应用是否存活,及时发现停机过程中的异常情况并重启容器。readiness 探针判断应用是否准备好接收ingress流量。
  4. preStop :配置preStop 钩子在容器停止前发送 HTTP 请求把该服务Nacos的状态设置为下线,不接收服务发现流量。

三、实现步骤

设置服务默认不上线

配置JAVA_OPTS的-Dspring.cloud.nacos.discovery.instanceEnabled参数为false,即可实现服务启动注册后是下线状态

spec:
containers:
- name: gac-iop-base
env:
- name: JAVA_OPTS
value: '-Xms2g -Xmx4g -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m -Dspring.profiles.active=uat -Dspring.cloud.consul.enabled=false -Dtsf.discovery.ribbon.enabled=false -Dspring.cloud.nacos.discovery.instanceEnabled=false'

配置startupProbe探针

startupProbe:
exec:
command:
- /bin/bash
- '-c'
- 'HEALTH_HTTP_CODE=$(curl --connect-timeout 5 -I -s http://localhost:8090/health/Check | grep HTTP | awk ''{print $2}'') && if [[ $HEALTH_HTTP_CODE -eq 200 ]]; then curl -X POST "http://localhost:8090/actuator/serviceregistry" --header ''Content-Type: application/json'' -d ''{"status": "UP"}'' && sleep 2s && NACOS_SERVICE_STATUS=$(curl http://localhost:8090/actuator/service-registry-status) && if [[ $NACOS_SERVICE_STATUS == *''"status":"UP"''* ]]; then echo ''Nacos服务状态为上线状态''; exit 0; else exit 1; fi; else echo ''健康检查失败''; exit 1; fi'
initialDelaySeconds: 30
timeoutSeconds: 5
periodSeconds: 10
successThreshold: 1
failureThreshold: 5

脚本逻辑:

  • 请求服务健康检测接口,判断接口返回的状态码是否为200,如果为200请求服务接口把Nacos状态改为上线,因为上线接口为异步接口,睡眠2s后,请求接口查看服务状态是否为上线,如果是上线状态返回为成功,如果不是上线状态退出重启

配置 liveness 和 readiness 探针

配置 liveness 探针,检查应用健康状态的端点,若探测失败则重启容器

livenessProbe:
httpGet:
path: /health/Check
port: 8090
scheme: HTTP
initialDelaySeconds: 30
timeoutSeconds: 1
periodSeconds: 10
successThreshold: 1
failureThreshold: 3

配置 readiness 探针,判断应用是否准备好接收流量

readinessProbe:
httpGet:
path: /health/Check
port: 8090
scheme: HTTP
initialDelaySeconds: 30
timeoutSeconds: 1
periodSeconds: 10
successThreshold: 1
failureThreshold: 3

配置preStop钩子

设置 preStop 钩子,在容器关闭前执行服务下线

    lifecycle:
preStop:
exec:
command:
- /bin/bash
- '-c'
- 'curl -X POST "http://localhost:8090/actuator/serviceregistry" --header ''Content-Type: application/json'' -d ''{"status": "DOWN"}'';sleep 40;'
restartPolicy: Always
terminationGracePeriodSeconds: 45

可在 Pod 定义中添加 terminationGracePeriodSeconds: 30(可根据实际情况调整时间),确保给 Java 应用足够的时间完成优雅停机逻辑。当30s还未处理完commoand,容器会直接kill掉
⚠️自建Nacos,服务上下线会做主动推送,云服务因安全问题,Nacos SDK 1.X版本不会做主动推送。服务会轮询去Nacos拉取服务状态(时间间隔默认是30s),可观察服务代码和日志确认时间间隔,设置preStop钩子的时间大于该间隔,terminationGracePeriodSeconds时间大于preStop处理时间

通过以上步骤,即可在 Kubernetes 中实现 Java 应用的优雅停机,充分发挥其带来的种种优势,保障应用的稳定运行和良好的用户体验。

四、扩展

Nacos服务上下线的两种方式

1. 请求Nacos的API接口
使用 curl 请求 Nacos 接口无需额外配置,其默认支持,但需确保以下参数正确无误:

  • 必要参数 :nacos 的 IP 和端口,服务注册的命名空间、服务名、集群名、分组名,以及服务的实例 IP 和端口。

服务下线

curl -X PUT "http://nacosip:port/nacos/v1/ns/instance?serviceName=myservice&groupName=travel-pro&ip=service_ip&port=service_port&namespaceId=namespace-pro&clusterName=cluster-pro&enabled=false"

服务上线

curl -X PUT "http://nacosip:port/nacos/v1/ns/instance?serviceName=myservice&groupName=travel-pro&ip=service_ip&port=service_port&namespaceId=namespace-pro&clusterName=cluster-pro&enabled=true"

2. 基于Spring Cloud的service-registry端点

在配置文件中开启service-registry端点:

management:
endpoints:
web:
exposure:
include: service-registry
base-path: /actuator
endpoint:
serviceregistry:
enabled: true

通过curl命令来进行服务状态的修改:上线是UP,下线是DOWN

curl -X POST "http://localhost:$port/actuator/serviceregistry" --header 'Content-Type: application/json' -d '{"status": "DOWN"}'

⚠️AI查询serviceregistry端口,PUT请求可更改上下线状态,GET请求可获取服务状态,实测GET请求无任何返回,后续由开发新写接口service-registry-status来查询服务状态是否上下线。
springcloud引入actuator的版本的版本不同,端点也会有差异,需与开发确认端点是serviceregistry还是service-registry

优雅停机清理资源

结合service-registry端点与Shutdown Hook实现资源的完美清理

  • service-registry端点是Spring Cloud提供的一个强大工具,用于管理和监控服务注册状态。通过该端点,我们可以方便地查看和修改服务实例在服务注册中心的状态。例如,当服务需要下线进行维护或更新时,我们可以通过发送HTTP请求将服务状态设置为DOWN或OUT_OF_SERVICE,从而通知其他服务该实例暂时不可用。
  • Shutdown Hook是Java提供的一种机制,允许我们在应用停止时执行清理工作。通过Runtime.getRuntime().addShutdownHook()添加Shutdown Hook线程我们可以在其中实现诸如关闭数据库连接池、关闭线程池以及等待正在处理的请求完成等操作。这些操作确保了应用在停止时不会出现资源泄漏或任务中断的问题。

优雅停机的协同流程

服务下线流程

  1. 设置服务状态:通过调用service-registry端点,将服务状态设置为DOWN,使服务不再接收新的请求。
  2. 执行清理工作:在应用的Shutdown Hook中,执行内部资源的清理逻辑,如关闭数据库连接池、关闭线程池等。
  3. 停止服务实例:完成上述步骤后,服务实例可以安全地停止。

服务重启或更新流程

  1. 设置服务状态:在服务重启或更新前,通过service-registry端点将服务状态设置为DOWN。
  2. 执行清理工作:触发Shutdown Hook中的清理逻辑,确保资源被正确释放。
  3. 重启服务:重新启动服务实例。
  4. 恢复服务状态:通过service-registry端点将服务状态重新设置为UP,使服务恢复对外提供服务。

以下是一个简单的示例,展示如何在Spring Boot应用中添加Shutdown Hook以实现优雅停机:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class GracefulShutdownApplication {

public static void main(String[] args) {
SpringApplication.run(GracefulShutdownApplication.class, args);

// 添加Shutdown Hook
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
// 关闭数据库连接池
// dataSource.close();
// 关闭线程池
// executorService.shutdown();
// 等待正在处理的请求完成
System.out.println("正在执行Shutdown Hook,进行资源清理...");
}));
}
}

参考文档: