K8s 部署SpringCloud项目,停止时PreDestroy无法正常触发
项目的雪花算法需要使用到 Redis 获取唯一的 WorkerId,项目停止时主动释放占用的 WorkerId,如果未正常释放会导致 WorkerId 被占用10分钟。
问题1
问题:项目部署到 K8s Pod 后无法正常调用PreDestroy函数。
原因:项目打包时 Docker 启用方式是:CMD ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
此时 Pod 中 PID 1 的进程是 shell,shell 接收到 SIGTERM 后不会把信号转发给服务。
调整为以下方式,让服务本身成为 PID 1
# 5.1 生成运行脚本命令
RUN printf '#!/bin/sh\nexec java $JAVA_OPTS -jar app.jar\n' > entrypoint.sh
RUN chmod +x entrypoint.sh
# 9. 启动应用
ENTRYPOINT ["./entrypoint.sh"]
问题2
问题:服务停止时有触发stopping server: Undertow - 2.2.25.Final,但是执行PreDestroy函数机率非常低
原因:项目代码中分配给线程一个死循环任务,用于主动刷新 Redis 中的 WorkerId 的过期时间,线程无法正常停止导致资源释放流程停滞。
处理:
使用的是Hutool的全局线程池线程,服务在停止前会将所有线程标识为 isInterrupted=true,任务中根据此标识判断停止任务。
代码片段:
private void refreshRedisCache() {
// Refresh the expiration time of the worker thread id continuously
ThreadUtil.execAsync(() -> {
while (refreshCache && !Thread.currentThread().isInterrupted()) {
if (workerId == null) {
log.error("snowflake worker id is null");
}
String r = (String) cache.get(cacheWorkerKey(workerId));
if (random.equals(r)) {
cache.expire(cacheWorkerKey(workerId), cacheTime);
log.debug("snowflake worker id {} refresh redis cache success", workerId);
} else {
log.error("snowflake worker id {} not match redis cache, random {} cache random {}", workerId, random, r);
}
ThreadUtil.sleep(sleepTime);
}
});
}
总结
问题1就不说了,问题2在本地也是无法复现的,说法是 idea 停止服务时有自己的处理机制,会 kill 无法正常停止的线程,保证服务正常停止,所以为了线上服务正常,无限执行的任务最好判断 isInterrupted 状态来主动停止任务。