你的Kubernetes Java应用优雅停机了吗?

简介: 你的Kubernetes Java应用优雅停机了吗?假如我们从 kafka 拉取数据然后生成任务处理数据,在服务退出时,如何保证内存中的数据能被正常处理完不丢失呢?假如服务是部署在 Kubernetes 中又该如何处理?Java 应用优雅停机我们首先考虑下,一般在什么场景下数据会丢失呢?升级服务时pod重启时服务器断电时

你的Kubernetes Java应用优雅停机了吗?

javayouhuiquan.jpg

假如我们从 kafka 拉取数据然后生成任务处理数据,在服务退出时,如何保证内存中的数据能被正常处理完不丢失呢?假如服务是部署在 Kubernetes 中又该如何处理?

Java 应用优雅停机

我们首先考虑下,一般在什么场景下数据会丢失呢?

  • 升级服务时
  • pod重启时
  • 服务器断电时

因为服务器断电属于极端情况,我们暂且不考虑。那就只有 Java 退出时我们要保证数据的完整性了。在 Java 中,有一个方法可以实现应用退出时候的优雅停机:shutdown hookSpring boot把这个东西封装了一下,可以通过 @PreDestroy 注解实现。当 JVM 收到退出的信号时,会调用 shutdown hook 中的方法,完成清理操作。示例代码如下:

Runtime.getRuntime().addShutdownHook(new Thread() {
  @Override
  public void run() {
    System.out.println("Start to run shutdown hook.");
  }
})

Shutdown hook 可以保证在我们代码主动调用 System.exit()OOM, 在终端执行 Ctrl+C,以及应用主动关闭等情况下时被调用。在实际的场景中,我们可以在上述的线程中执行清理操作。比如,停止 kafka 的数据消费,以及任务的及时处理等。

当我们使用 java -jar *.jar 运行 Java程序后,通过执行 kill $pid,可以发现程序确实可以优雅退出。但是当我把服务部署到 Kubernetes 时,发现这个逻辑并没有被执行,到底哪里出了问题?

在 Kubernetes 中优雅停机

当我们发送 delete 命令给 pod 时,Kubernetes 会使用优雅停机(默认30s时间),在优雅停机过程中,此 podAPI server 中会被更新为dead状态。当我们用kubectl 命令查看此pod时,它被展示为Terminating 的状态。当 Kubelet 看到 pod被标记为了 Terminating 状态时,它就会开始执行 podshutdown 程序。如果我们 pod 的容器定义了 preStop hook,那么这个 hook 会在容器中执行;与此同时,Kubelet 会向容器内发送一个TERM信号。Service也会将此 pod 从 endpoint 列表移除。当优雅停机时间过后,在 pod 里仍然存活的进程则会被SIGKILL命令杀掉。Kubelet会在 API server 里通过设置 grace period=0(立即删除)来完成 Pod 的删除操作。删除后此 Pod 会在API中消失,并且在客户端也不可见了。

以上,可以看出,我们的容器是会收到 TERM 信号的,按照常理,如果我们的 Java 进程收到了 TERM 信号是可以正常执行我们写的 shutdown hook 优雅退出的,但是这里却没有执行,很有可能是我们的 Java 进程根本就没有收到信号。

查看我们的 Dockerfile,发现我们定义的启动命令是执行一个 run.sh 的脚本,在 run.sh 脚本中,进一步执行了启动 Java 进程的命令。

# run.sh
...
sh start.sh start
...
while [1]
do 
  sleep 30
done

可以看到,我们在 run.sh 中进一步执行了 start.sh,Java 进程的启动逻辑在start.sh脚本中。我们可以执行 ps -ef 查看下当前容器中的进程

UID   PID   PPID    C   STIME     TTY   TIME    CMD
root    1   0   0 11:01   ? 00:00:00  bash ~/run.sh 
root    4084    1   8 11:01   ? 00:15:00  java -Dname=test
root    14913   1   0 13:49   ? 00:00:00  sleep 30
root    14914   0   0 13:50   pts/0 00:00:00  bash
root    14955   14914   0 13:50   pts/0 00:00:00  ps -ef

可以看到,我们运行的 run.sh 的 PID 是 1,Java 进程的 PID 是 4084,Java 进程是 run.sh 进程的一个子进程。问题就出在这里,在 pod 被删除时,TERM 信号只会发送给 1号进程,而 run.sh 接收到此信号后并不会将其转发给 Java 进程,因此 Java 便无法触发 shutdown hook,无法实现优雅退出。最终,Java 是被 SIGKILL 信号杀掉的(强制退出)。所以,我们只需要让 Java 进程作为 1号进程就行了。改写下脚本,我们把启动 Java 进程的命令放到 run.sh

# run.sh
...
exec java $JAVA_OPTS -jar ./*.jar --server.port=8080
...
while [1]
do 
  sleep 30
done

exec 的作用是被执行的命令行替换掉当前的 shell 进程。测试发现 OK,此时我们实现了优雅停机。但是,这足够优雅吗?

更优雅地停机

在上一步,我们实现了优雅停机,但是其实这并不是最优方案。我在看 start.sh 脚本中,发现此脚本定义了 start, restart, stop, status 4个方法,而且这个脚本中定义了很多额外的变量,如果我们要把之前的功能都实现的话,就需要把逻辑都搬到 run.sh 中。这无疑会增大工作量,这是不优雅的原因之一。

其次,一般是不推荐把 Java 进程作为1号进程的。因为在 Linux中,1号进程有特殊作用:1号进程会作为孤儿进程的父进程,它需要对自己的子进程进行清理回收,避免系统产生僵尸进程。bash可以很好地处理这种清理工作,我们一般自己写的 Java 程序是不会考虑这种东西的。

那么,就需要我们在 shell 中接收到 TERM 信号后把信号传递给 Java 进程了。这需要怎么做呢?我们需要使用trap命令。trap 命令的作用是捕捉信号和其他事件并执行命令。

# run.sh
...
sh start.sh start
grace_exit() {
  echo 'grace exit started'
  sh start.sh stop &
  wait $!
  echo 'grace exit finished'
}
trap 'grace_exit' TERM INT
...
while [1]
do 
  sleep 30
done

在脚本中,我们使用 trap 捕捉 TERMKubelet 发送的信号) 和 INT(快速关闭,当用户输入 Control-C时由终端程序发送) 信号,捕捉到了以后,我们执行了 grace_exit 方法,在此方法中,调用了 start.sh 脚本的 stop 方法,其实这个 stop 方法就是找到了 Java 进程,然后给其发送了 kill 命令,我们直接在 grace_exit 中执行相同逻辑也是可以的,这里是为了复用逻辑。我们还使用了 & 保证 stop 方法在后台运行,这样方便我们获取其进程号($!会返回shell最后运行的后台进程的 PID),等待其执行结束。 这样,当我们 delete``pod 时,Kubelet 发送 TERM 信号后,我们就能传达给 Java 进程,进而让 Java 进程进行优雅停机了。


标题你的Kubernetes Java应用优雅停机了吗?

作者末日没有进行曲

转载地址https://www.cnblogs.com/dengkaiting/p/15808785.html

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
23天前
|
人工智能 安全 Java
Java和Python在企业中的应用情况
Java和Python在企业中的应用情况
46 7
|
7天前
|
人工智能 Kubernetes 安全
赋能加速AI应用交付,F5 BIG-IP Next for Kubernetes方案解读
赋能加速AI应用交付,F5 BIG-IP Next for Kubernetes方案解读
43 13
|
6天前
|
存储 Kubernetes 关系型数据库
阿里云ACK备份中心,K8s集群业务应用数据的一站式灾备方案
本文源自2024云栖大会苏雅诗的演讲,探讨了K8s集群业务为何需要灾备及其重要性。文中强调了集群与业务高可用配置对稳定性的重要性,并指出人为误操作等风险,建议实施周期性和特定情况下的灾备措施。针对容器化业务,提出了灾备的新特性与需求,包括工作负载为核心、云资源信息的备份,以及有状态应用的数据保护。介绍了ACK推出的备份中心解决方案,支持命名空间、标签、资源类型等维度的备份,并具备存储卷数据保护功能,能够满足GitOps流程企业的特定需求。此外,还详细描述了备份中心的使用流程、控制台展示、灾备难点及解决方案等内容,展示了备份中心如何有效应对K8s集群资源和存储卷数据的灾备挑战。
|
17天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
56 6
|
15天前
|
监控 Java 数据库连接
Java线程管理:守护线程与用户线程的区分与应用
在Java多线程编程中,线程可以分为守护线程(Daemon Thread)和用户线程(User Thread)。这两种线程在行为和用途上有着明显的区别,了解它们的差异对于编写高效、稳定的并发程序至关重要。
26 2
|
25天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
48 6
|
22天前
|
关系型数据库 MySQL Java
MySQL索引优化与Java应用实践
【11月更文挑战第25天】在大数据量和高并发的业务场景下,MySQL数据库的索引优化是提升查询性能的关键。本文将深入探讨MySQL索引的多种类型、优化策略及其在Java应用中的实践,通过历史背景、业务场景、底层原理的介绍,并结合Java示例代码,帮助Java架构师更好地理解并应用这些技术。
23 2
|
25天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
25天前
|
Java 测试技术 API
Java 反射机制:深入解析与应用实践
《Java反射机制:深入解析与应用实践》全面解析Java反射API,探讨其内部运作原理、应用场景及最佳实践,帮助开发者掌握利用反射增强程序灵活性与可扩展性的技巧。
67 4
|
28天前
|
Java BI API
Java Excel报表生成:JXLS库的高效应用
在Java应用开发中,经常需要将数据导出到Excel文件中,以便于数据的分析和共享。JXLS库是一个强大的工具,它基于Apache POI,提供了一种简单而高效的方式来生成Excel报表。本文将详细介绍JXLS库的使用方法和技巧,帮助你快速掌握Java中的Excel导出功能。
61 6