本文讨论如何让 jvm 优雅停机
什么是优雅停机
向应用进程发出停止指令之后,能保证正在执行的业务操作不受影响,直到操作运行完毕之后再停止进程
要解决的问题
- 停止接收新的请求,停止启动新的定时任务,停止接收消息
- 正在处理的请求,等待请求处理完;正在执行的任务、消息,执行完毕
异常关闭应用程序的危害
如果暴力的关闭应用程序,比如通过kill -9 <pid>
命令强制直接关闭应用程序进程,可能造成如下危害:
- 正在执行的任务数据丢失或者产生中间数据
- 任务所持有的全局资源等不到释放,比如当前任务持有 redis 的锁,并没有设置过期时间
- 注册中心场景,没有主动更新服务列表,导致一段时间内请求到不存在的实例
JVM 的停机场景
导致 JVM 正常关闭的场景有:
- 程序运行结束
- JVM 发生 OOM 而退出
- Java 程序执行 System.exit() 指令
- 操作系统关闭
- kill pid(kill -15 pid)
- 终端通过 Ctrl-C 终止进程
导致 JVM 异常关闭的场景有:
- kill -9
- 主机关机
- 主机内存不够触发操作系统 OOM-KILLER
JVM 优雅停机的实现
实现思路
- 利用 JDK 提供的 Runtime.getRuntime().addShutdownHook() 方法注册 Shutdown Hook,用来接收操作系统发送的停止信号;
- 当接收到停止信号时,执行停机的处理逻辑:
- 拒绝接收新的请求、消息、任务
- 处理已接收的请求、消息、任务
- 注意超时和异常:在处理已有请求和执行停机逻辑时,可能有时间过长的时间,需要设置一个超时时间,避免进程一直无法退出;捕获异常防止停机逻辑中断
处理已有请求
注册JDK的ShutdownHook
1 | package org.example; |
在 web 服务中,需要等待已接收的请求处理完,请求都有超时时间,只要等待大于超时时间即可
1 | Runtime.getRuntime().addShutdownHook(new Thread(() -> { |
对于线程池
1 | private static void hook(ExecutorService executor) { |
拒绝新的请求
如何拒绝新的请求,不同的服务架构做法不同,下面例举几个常见的场景
Nginx 挂载多服务实例
先了解下 nginx 如何维护健康实例列表
Nginx 支持两种健康检查模式:
被动检查
nginx 在代理请求过程中,会检测 upstream_server 对请求的响应,如果得到某个 upstream_server 在单位周期(fail_timeout)内失败次数超过阈值(max_fails,默认值为1),标记该 upstream_server 为异常;等下一个 fail_timeout,会再去请求一次,如果成功,将 upstream_server 标记为可用
upstream_server 不需要专门提供健康检查接口
如果发现本次请求失败,nginx 会转发给其他节点处理
主动检查
Nginx 服务定时向 upstream_server 发出健康检查请求,验证各个 upstream_server 的状态,同上
upstream_server 需要专门给 nginx 提供一个健康检查的低消耗接口
使用 nginx 做负载均衡,执行shutdown Hook如下:
- 停机实例拒绝 nginx 的健康检查,nginx 将停机实例从均衡器列表中移除
- 停机实例关闭服务的Server端监听,保证不接受请求
服务注册发现
当服务端实例停止后,由于服务端不会主动去更改注册中心的注册信息,注册中心需要40秒(目前应用到Zookeeper的会话超时时间配置)才能剔除这个异常配置,在这40秒内会不断尝试访问这个不存在的实例,导致产生大量业务报错,所以在停机时需要主动从注册中心下线
从注册中心下线,断开与 zookeeper 的连接
Provider 取消所有注册
Consumer 取消所有订阅
协议关闭(关闭心跳检测、关闭通道)
关闭 provider 端的监听(server.close)
关闭 consumer 对其他服务的引用(client.close)
消息发布
消费者:
- 停止 poll 消息
- 关闭 consumer 与 broker 的连接(consumer.close)
Reference
[Dubbo源代码分析九:优雅停机](