目前部署在k8s上的springboot应用通过RollingUpdate的方式更新应用时发现以下情况:
一个新pod起来并经过health check检查可以接收流量之后会删除一个老pod。但是,删除老pod时,虽然没有新的流量进入老pod,该老pod中的java进程还有一些业务没有处理完毕就直接被kill掉。
因此,我们需要做到当新pod起来之后,老pod不接收新流量,但是已经在处理的流量请求处理完之后才能删除老pod。
其实,k8s本身对于pod是有一个terminationGracePeriodSeconds的配置项(默认是30秒)。当采用RollingUpdate方式更新应用时,一个新pod起来并经过health check后(如果配置了health check),则会通知k8s终止一个pod。k8s终止pod的生命周期如下(共5步):
- Pod被设置为Terminating状态,并从k8s的service/endpoint列表中删除。此时,Pod停止获得新流量。在Pod中运行的容器不会受到影响。
- preStop Hook被执行(如果deployment yaml中配置该项,默认没有配置)。preStop Hook是一个发送到Pod中的容器特殊命令或Http请求。
- K8S向Pod中的容器发送SIGTERM信号。这个信号让容器知道它们很快就会被关闭。
SIGTERM比较友好,进程能捕捉这个信号, 根据您的需要来关闭程序。在关闭程序之前,您可以结束打开的记录文件和完成正在做的任务。 在某些情况下, 假如进程正在进行作业而且不能中断,那么进程可以忽略这个 SIGTERM信号。相对于kill ${pid}命令。
对于SIGKILL信号,进程是不能忽略的。 这是一个 '“我不管您在做什么,立刻停止”'的信号。 假如您发送SIGKILL信号给进程,
FreeBSD就将进程停止在那里。相对于kill -9 ${pid}命令
SpringBoot应用的java process捕获到该SIGTERM信号时就会开始正常关闭自己。 - K8S等待优雅的终止(等待时间terminationGracePeriodSeconds)
注意:
- 等待优雅终止,在时间上是与preStop Hook和SiGTERM信号并行的。k8s不会等待preStop Hook完成。
- 如果应用程序完成关闭并在terminationGracePeriod(假设30S)完成之前退出,k8s是不会等30S而是立即进入下一步。
- 如果POD中的应用需要30秒才能关闭,则需要相应增加优雅终止期限。 K8S向Pod发送
- SIGKILL信号,并删除Pod
如果容器在优雅终止宽限期后仍在运行,则会发送SIGKILL信号并强制删除。 此时,相应的Kubernetes对象也会被清除。
从上面的步骤我们可以看出,k8s是终止优雅停机的;但是,还需要应用配合。如上面第4步第2条中所说的,应用在terminationGracePeriod之前就完成关闭,则pod会直接被删除。所以,我需要在SpringBoot应用在接收到SIGTERM信号退出应用时,做以下几点:
停止接收流量/请求。
注意:虽然上面第1步中说是会Pod从service/endpoint列表中删除,不再接收流量。那只是对通过k8s service访问pod方式的流量请求,例如zuul。对于springcloud中的其他的springboot应用,它们之间的调用是通过pod ip直接访问的。因此这时候需要应用本事停止接收流量/请求。温柔的终止线程池。
等待线程池终止。
相关代码实现如下:
import org.apache.catalina.connector.Connector;
import org.apache.tomcat.util.threads.ThreadPoolExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework