SpringCloudAlibaba + k8s实现优雅停机

背景

在k8s+SpringCloudAlibaba技术体系下,实现后台服务的优雅停机。注册中心和配置中心都是nacos,该方案中要使用Nacos的API所以需要Nacos的版本 >= 2.2.0

目的

服务部署上线停机时,不能出现异常。为实现这样的目的,需要同时满足以下两个条件:

  1. 新的请求不能打到要停机的服务上。为实现这样的效果需要先切流量。
  2. 已经持有的请求可以在预期时间内正常返回。这需要在停机前要等待一段时间。

出发点

尽量少的改动

原理

当新Pod就绪之后,k8s会向旧Pod发送停机信号,Pod中的线程要在指定的时间内退出,否则就会被强制杀掉,所以要实现优雅停机具体流程如下:

  1. 接收到停机信号后将服务的权重降为0,为的是不再接收新的请求。通过entrypoint.sh脚本,调用NacosAPI实现,API中的参数通过Dockerfile设置。
  2. 等待客户端的缓存过期。等待的逻辑也在entrypoint.sh脚本。这里的缓存是指负载均衡的缓存,SpringCloudAlibaba体系下默认的是LoadBalancer,通过参数spring.cloud.loadbalancer.cache.ttl设置,默认35s。
  3. 等待已经持有的线程结束。
  4. 执行停机逻辑,从注册中心摘除。

具体实现

出于最小改动的目的,需要了解的参数如下:

  1. k8s参数terminationGracePeriodSeconds,默认为30s,如果向Pod发出停机信号后,在30s内没有退出,会被强制杀掉。
  2. LoadBalancer参数spring.cloud.loadbalancer.cache.ttl,负载缓存更新时间。
  3. 业务允许的最大请求时延,根据业务调整,如果超过30s,就需要调整 参数1
  4. 停机等待时间T。要满足(参数2+参数3) < T < 参数1

假设业务允许的最大请求时延是10s,LoadBalancer的过期时间也可以设置为10s,这样停机等待时间可以设置为23s,这样我们只需要通过Dockerfile设置ENV SERVER_SLEEP_TIME="23"并且添加启动参数--spring.cloud.loadbalancer.cache.ttl=10即可实现服务的优雅停机。所需脚本如下:

Dockerfile 复制代码
FROM registry.cn-hangzhou.aliyuncs.com/supx/openjdk:17-ea-slim
ENV SERVER_NAME="smooth-server"
ENV SERVER_PORT="1002"

ENV NACOS_SERVER_ADDR="main.supx.tech:8848"
ENV NACOS_NAMESPPACE_ID="f5ae6f36-5ed6-46ae-b07a-4c9f86f1f286"

# 权重降为0后等待停机时间
ENV SERVER_SLEEP_TIME="23"

#nacos 基础配置
ENV NACOS_BASE_CONF="--spring.cloud.nacos.server-addr=${NACOS_SERVER_ADDR} --spring.cloud.nacos.config.namespace=${NACOS_NAMESPPACE_ID} --spring.cloud.nacos.discovery.namespace=${NACOS_NAMESPPACE_ID}"
# nacos其他配置
ENV NACOS_CONF=""

ENV JAVA_OPTS="-Xms256m -Xmx256m"
ENV SPRING_OPTS="--spring.cloud.loadbalancer.cache.ttl=10 --spring.application.name=${SERVER_NAME} --server.port=${SERVER_PORT}"

WORKDIR /${SERVER_NAME}
COPY ./target/${SERVER_NAME}-1.0.0-SNAPSHOT.jar ${SERVER_NAME}-1.0.0-SNAPSHOT.jar
COPY entrypoint.sh entrypoint.sh

EXPOSE ${SERVER_PORT}
ENTRYPOINT ["./entrypoint.sh"]
#CMD ["/bin/bash","-c","java -jar ${SERVER_NAME}-1.0.0-SNAPSHOT.jar"]

entrypoint脚本内容如下:

shell 复制代码
#!/bin/bash

# 定义应用启动命令
start_command="java ${JAVA_OPTS} -jar ${SERVER_NAME}-1.0.0-SNAPSHOT.jar ${SPRING_OPTS} ${NACOS_BASE_CONF} ${NACOS_CONF}" "$@"
echo "启动命令:$start_command"
# 启动Java应用并获取其PID
$start_command &
pid=$!

# 定义一个函数来处理信号,并将其发送给Java进程
term_handler() {
    echo "Caught SIGTERM/SIGINT, stopping the application..."
    #服务降级处理
    ifconfig_output=$(ifconfig)
    ip_address=$(echo "$ifconfig_output" | grep -oE 'inet (addr:)?([0-9]{1,3}.){3}[0-9]{1,3}' | grep -oE '([0-9]{1,3}.){3}[0-9]{1,3}' | grep -v '127.0.0.1')
    curl -d "serviceName=${SERVER_NAME}" -d "ip=${ip_address}" -d "port=${SERVER_PORT}" -d "weight=0" -d "namespaceId=${NACOS_NAMESPPACE_ID}" -X PUT "${NACOS_SERVER_ADDR}/nacos/v2/ns/instance"
    echo "等待${SERVER_SLEEP_TIME}s..."
    sleep ${SERVER_SLEEP_TIME}
    #关闭程序
    kill -TERM "$pid"
    wait "$pid"
    exit 0
}

# 捕获SIGTERM和SIGINT信号
trap term_handler SIGTERM SIGINT

# 等待子进程结束或接收到信号
wait "$pid"

echo "Application has stopped."

优缺点

优点 很明显,只需要修改Dockerfile就能实现优雅停机,不需要改动应用。
缺点主要表现在以下几点

  1. NacosAPI,不能保证可用。测试时偶尔会出现API不可用的情况,需要删除/data/protocol数据。如果调用失败了,脚本不能阻止Pod被杀掉,也就不能实现优雅停机。这是这个方案最大的问题,依赖NacosAPI!
  2. 基础镜像需要安装curl net-tools,用来获取IP、调用NacoAPI。
  3. 默认情况下,本地配置无法覆盖配置中心的配置,需要在配置中心添加以下配置
yaml 复制代码
spring:
  cloud:
    config:
      overrideSystemProperties: false

思考

  1. 因为NacosAPI存在偶发问题,可以通过添加自定义接口的方式,用代码来控制停机逻辑。也可以重写spring-boot-starter-actuatorshutdownEndpoint。因为nacos的停机逻辑是在静态代码块中注册的退出钩子,因此只能通过外部调用触发停机逻辑,否则无法实现自定义停机。
  2. 有人建议直接使用反注册的接口,不要先降级,因为这样会有两次交互。但是实际测试发现,NacosAPI的注销实例接口达不到预期效果,请求还是会被路由到要下线的节点上,不知道用代码控制会不会好点。

参考: Nacos 实现服务平滑上下线(Ribbon 和 LB)_java控制nacos服务下线-CSDN博客

相关推荐
XINGTECODE37 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
程序猿进阶43 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺1 小时前
Spring Boot框架Starter组件整理
java·spring boot·后端
凡人的AI工具箱1 小时前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang
先天牛马圣体1 小时前
如何提升大型AI模型的智能水平
后端
java亮小白19971 小时前
Spring循环依赖如何解决的?
java·后端·spring
2301_811274312 小时前
大数据基于Spring Boot的化妆品推荐系统的设计与实现
大数据·spring boot·后端
草莓base2 小时前
【手写一个spring】spring源码的简单实现--容器启动
java·后端·spring
Ljw...2 小时前
表的增删改查(MySQL)
数据库·后端·mysql·表的增删查改
编程重生之路3 小时前
Springboot启动异常 错误: 找不到或无法加载主类 xxx.Application异常
java·spring boot·后端