如何优雅关闭 Spring Boot 应用

前言

随着线上应用逐步采用 SpringBoot 构建,SpringBoot应用实例越来多,当线上某个应用需要升级部署时,常常简单粗暴地使用 kill 命令,这种停止应用的方式会让应用将所有处理中的请求丢弃,响应失败。这样的响应失败尤其是在处理重要业务逻辑时需要极力避免的,那么有什么更好的方式来平滑地关闭 SpringBoot 应用呢?那就通过本文一起来探究吧。(本文主要针对基于Spring Boot 内嵌 Tomcat 容器作为 Web 服务的应用)

本文示例代码可以通过下面仓库地址获取:

springboot-shutdown:github.com/wrcj12138aa…

环境支持:

JDK 8 SpringBoot 2.1.4 Maven 3.6.0

定制 Tomcat Connector 行为

要平滑关闭 Spring Boot 应用的前提就是首先要关闭其内置的 Web 容器,不再处理外部新进入的请求。为了能让应用接受关闭事件通知的时候,保证当前 Tomcat 处理所有已经进入的请求,我们需要实现 TomcatConnectorCustomizer 接口,这个接口的源码十分简单,从注释可以看出这是实现自定义 Tomcat Connector 行为的回调接口:

[图片上传失败...(image-3dfa1-1564387539823)]

<figcaption></figcaption>

这里如果小伙伴对 Connector 不太熟悉,我就简单描述下: Connector 属于 Tomcat 抽象组件,功能就是用来接受外部请求,以及内部传递,并返回响应内容,是Tomcat 中请求处理和响应的重要组件,具体实现有 HTTP Connector 和 AJP Connector。

通过定制 Connector 的行为,我们就可以允许在请求处理完毕后进行 Tomcat 线程池的关闭,具体实现代码如下:

[图片上传失败...(image-22801c-1564387539822)]

<figcaption></figcaption>

上述代码定义的 TIMEOUT 变量为 Tomcat 线程池延时关闭的最大等待时间,一旦超过这个时间就会强制关闭线程池,也就无法处理所有请求了,我们通过控制 Tomcat 线程池的关闭时机,来实现优雅关闭 Web 应用的功能。另外需要注意的是我们的类 CustomShutdown 实现了 ApplicationListener 接口,意味着监听着 Spring 容器关闭的事件,即当前的 ApplicationContext 执行 close 方法。

内嵌 Tomcat 添加 Connector 回调

有了定制的 Connector 回调,我们需要在启动过程中添加到内嵌的 Tomcat 容器中,然后等待执行。那这一步又是如何实现的呢,可以参考下面代码:

[图片上传失败...(image-62ef62-1564387539822)]

<figcaption></figcaption>

这里的 TomcatServletWebServerFactory 是 Spring Boot 实现内嵌 Tomcat 的工厂类,类似的其他 Web 容器,也有对应的工厂类如 JettyServletWebServerFactory,UndertowServletWebServerFactory。他们共同的特点就是继承同个抽象类 AbstractServletWebServerFactory,提供了 Web 容器默认的公共实现,如应用上下文设置,会话管理等。

如果我们需要定义Spring Boot 内嵌的 Tomcat 容器时,就可以使用 TomcatServletWebServerFactory 来进行个性化定义,例如下方为官方文档提供自定示例:

[图片上传失败...(image-b1cc68-1564387539822)]

<figcaption></figcaption>

好了说回正题,我们这里使用 addConnectorCustomizers 方法将自定义的 Connector 行为添加到内嵌的Tomcat 之上,为了查看加载效果,我们可以在 Spring Boot 程序启动后从容器中获取下webServerFactory 对象,然后观察,在它的 tomcatConnectorCustomizers 属性中可以看到已经有了 CustomeShutdown 对象。

[图片上传失败...(image-1dc76a-1564387539822)]

<figcaption></figcaption>

开启 Shutdown Endpoint

到目前让内嵌 Tomcat 容器平稳关闭的操作已经完成,接下来要做的就是如何关闭主动关闭 Spring 容器了,除了常规Linux 命令 Kill,我们可以利用 Spring Boot Actuator 来实现Spring 容器的远程关闭,怎么实现继续看

Spring Boot Actuator 是 Spring Boot 的一大特性,它提供了丰富的功能来帮助我们监控和管理生产环境中运行的 Spring Boot 应用。我们可以通过 HTTP 或者 JMX 方式来对我们应用进行管理,除此之外,它为我们的应用提供了审计,健康状态和度量信息收集的功能,能帮助我们更全面地了解运行中的应用。

Actuator, ['æktʃʊˌeɪtə] 中文翻译过来就是制动器,这是一个制造业的术语,指的是用于控制某物的机械装置。

在 Spring Boot Actuator 中也提供控制应用关闭的功能,所以我们要为应用引入 Spring Boot Actuator,具体方式就是要将对应的 starter 依赖添加到当前项目中,以 Maven 项目为例:

[图片上传失败...(image-617164-1564387539821)]

<figcaption></figcaption>

Spring Boot Actuator 采用向外部暴露 Endpoint (端点)的方式来让我们与应用进行监控和管理,引入 spring-boot-starter-actuator 之后,我们就需要启用我们需要的 Shutdown Endpoint,在配置文件 application.properties 中,设置如下

[图片上传失败...(image-bd8e30-1564387539821)]

<figcaption></figcaption>

第一行表示启用 Shutdown Endpoint ,第二行表示向外部以 HTTP 方式暴露所有 Endpoint,默认情况下除了 Shutdown Endpoint 之外,其他 Endpoint 都是启用的。

除了 Shutdown Endpoint,Actuator Endpoint 还有十余种,有的是特定操作,比如 heapdump 转储内存日志;有的是信息展示,比如 health 显示应用健康状态。具体所有 Endpoint 信息可以参见官方文档-53. Endpoints 一节。

到这里我们的前期配置工作就算完成了。当启动应用后,就可以通过POST 方式请求对应路径的 http://host:port/actuator/shutdown 来实现Spring Boot 应用远程关闭,是不是很简单呢。

模拟测试

这里为了模拟测试,我们首先模拟实现长达10s 时间处理业务的请求控制器 BusinessController,具体实现如下:

[图片上传失败...(image-2b4a4e-1564387539819)]

<figcaption></figcaption>

用 Thread.sleep 来阻塞当前请求线程,模拟业务处理,在此同时用 HTTP 方式访问 Shutdown Endpoint 试图关闭应用,可以通过观察控制台日志看是否应用是否会完成请求的处理后才真正进行关闭。

首先用 curl 命令模拟发送业务请求:

[图片上传失败...(image-69f4a3-1564387539819)]

<figcaption></figcaption>

然后在业务处理中,直接发送请求 actuator/shutdown,尝试关闭应用,同样采用 curl 方式:

[图片上传失败...(image-d37404-1564387539819)]

<figcaption></figcaption>

actuator/shutdown 请求发送后会立即返回响应结果,但应用并不会停止:

[图片上传失败...(image-174675-1564387539818)]

<figcaption></figcaption>

最后看下控制台的日志输出顺序:

[图片上传失败...(image-a6a260-1564387539818)]

<figcaption></figcaption>

可以看出在发送业务请求之后立刻发送关闭应用的请求,并不会立即将应用停止,而是在请求处理完毕之后,就是阻塞的 10s 后应用开始退出,这样可以保证已经接收到的请求能返回正常响应, 而关闭请求之后再进入的请求都不会被处理,到这里我们优雅关闭 Spring Boot 程序的操作就此实现了。

实现自动化

由于 Spring Boot 提供内嵌 Web 容器的便利性,我们经常将程序打包成 jar 然后发布。通常应用的启动和关闭操作流程是固定且重复的,本着 Don't Repeat Yourself 原则,我们有必要将这个操作过程自动化,将关闭和启用的 SpringBoot应用的操作写成 shell 脚本,以避免出现人为的差错,并且方便使用,提高操作效率。下面是我针对示例程序所写的程序启动脚本:(具体脚本可在示例项目查看)。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342