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博客

相关推荐
Victor3566 小时前
https://editor.csdn.net/md/?articleId=139321571&spm=1011.2415.3001.9698
后端
Victor3566 小时前
Hibernate(89)如何在压力测试中使用Hibernate?
后端
灰子学技术8 小时前
go response.Body.close()导致连接异常处理
开发语言·后端·golang
Gogo8169 小时前
BigInt 与 Number 的爱恨情仇,为何大佬都劝你“能用 Number 就别用 BigInt”?
后端
fuquxiaoguang9 小时前
深入浅出:使用MDC构建SpringBoot全链路请求追踪系统
java·spring boot·后端·调用链分析
毕设源码_廖学姐9 小时前
计算机毕业设计springboot招聘系统网站 基于SpringBoot的在线人才对接平台 SpringBoot驱动的智能求职与招聘服务网
spring boot·后端·课程设计
野犬寒鸦11 小时前
从零起步学习并发编程 || 第六章:ReentrantLock与synchronized 的辨析及运用
java·服务器·数据库·后端·学习·算法
逍遥德11 小时前
如何学编程之01.理论篇.如何通过阅读代码来提高自己的编程能力?
前端·后端·程序人生·重构·软件构建·代码规范
MX_935912 小时前
Spring的bean工厂后处理器和Bean后处理器
java·后端·spring
程序员泠零澪回家种桔子13 小时前
Spring AI框架全方位详解
java·人工智能·后端·spring·ai·架构