Java程序在K8S下实现优雅关闭

故事背景

多个项目一起发布,经常出现部分请求失败或者超时,影响用户体验。造成这个情况的原因是容器收到退出信号直接关闭了,此时Java程序还有部分请求还没处理完,然后网关就会报请求超时。为了避免这个情况,提高发布期间服务的可用性,我们需要实现优雅关闭(graceful shutdown)。

优化方案

在讨论方案之前先要了解一下在发布期间容器的生命周期。

图中可以发现K8S其实是支持自定义钩子,在容器停止前有30秒的时间处理未完成的请求。

上图是SpringBoot官方文档对K8S容器如何实现优雅关闭的描述,总结一下我们需要做以下2点:

  • 定义preStop操作
  • 处理SIGTERM信号

preStop

结合云平台的配置,我们需要在镜像里增加一个脚本,如/opt/preStop.sh。

参考脚本

#!/bin/bash
# 退出容器前的预处理
# 获取待kill的进程ID,根据自己的业务需要填写这个匹配规则
pids=`pgrep -f main.jar`
if [ "$pids" != "" ]; then
  # 注意kill指令是非阻塞的
  echo "$pids" | xargs kill
fi
# 由于kill指令是非阻塞的,所以需要sleep一段时间来等待程序处理完成,sleep时长需要结合业务需要调整
sleep 15

因为这个脚本一退出容器就会重启,所以一定要阻塞等待,sleep是一种方式,也可以使用循环来检查进程是否都退出完毕 除了增加preStop脚本,还有一个更简单的方法,Java进程改为1号进程,在启动脚本的java命令前加上exec,使关闭信号能被jvm接收到。

SIGTERM

SpringBoot2.3以上版本开始支持优雅关闭,需要配置如下:


server:
  shutdown: graceful  #启用优雅停止
spring:
  lifecycle:
    timeout-per-shutdown-phase: 20s #强制终止的宽限时间,要小于30秒

Tomcat、Undertow等WEB容器都支持优雅关闭。

Nacos需要使用alibaba的starter才能支持,关闭动作包括注销实例和停止心跳。

目前的SpringBoot是2.1版本,升级成本太高,官方Issue提供一个简单的实现:https://github.com/spring-projects/spring-boot/issues/4657#issuecomment-161354811

大家可能好奇Java是如何处理关闭信号的,这里简单介绍一下实现原理,JVM提供了关闭钩子,在关闭时可以利用一个线程来做一些收尾工作,所以信号实际是JVM处理的。

// JVM关闭前会先调用之前注册的所有钩子
Runtime.getRuntime().addShutdownHook();

容器重启效果图

SmartLifeCycle

除了Nacos,还需要实现优雅关闭的场景如下:

  • Kafka
  • 其他定时任务

可以通过实现SpringBoot的SmartLifeCycle接口实现。

服务秒级上下线通知

使用的网关是SpringCloudGateway,负载均衡的组件是Ribbon,它的拉取服务列表并刷新缓存的默认实现是定时30秒,当服务下线后,请求进来有可能转发到已经下线的服务。

解决方案:

Eureka的实现是订阅服务的变更,及时刷新缓存,我们可以参考它的实现,结合已有的定时刷新,推拉结合,实现实时的上下线通知。

官方的issue,nacos的spring-cloud-starter不支持实时上下线通知的问题存在已久,https://github.com/alibaba/spring-cloud-alibaba/issues/1554

发表评论 / Comment

用心评论~