Java应用的优雅停机

引:对于一个鲁棒的Java应用来说,优雅停机 必不可少的。下面我将先介绍Java优雅停机的实现方式,然后介绍在Dubbo中的服务是如何实现优雅停机的。

信号

在Linux中,信号是进程间通讯的一种方式,它采用的是异步机制。当信号发送到某个进程中时,操作系统会中断该进程的正常流程,并进入相应的信号处理函数执行操作,完成后再回到中断的地方继续执行。

信号的响应动作

每个信号都有自己的响应动作,当接收到信号时,进程会根据信号的响应动作执行相应的操作,信号的响应动作有以下几种:

  • 中止进程(Term)
  • 忽略信号(Ign)
  • 中止进程并保存内存信息(Core)
  • 停止进程(Stop)
  • 继续运行进程(Cont)

停机信号

在linux上,我们停机主要是使用 kill 的方式。关于停机对应的信号主要有下面两个,也是我们平时经常使用的两种:

信号 动作 说明
SIGTERM 15 Term 结束程序(可以被捕获、阻塞或忽略) kill的默认信号
SIGKILL 9 Term 无条件结束程序(不能被捕获、阻塞或忽略)

Java停机方式

优雅停机:指的是在应用关闭时能够处理一下“善后”的逻辑,比如

  1. 关闭 socket 链接
  2. 清理临时文件
  3. 发送消息通知给订阅方,告知自己下线
  4. 将自己将要被销毁的消息通知给子进程
  5. 各种资源的释放

我们知道在执行kill -9 pid时是无条件结束程序的,所以在这种情况下我们无法优雅停机,只有在执行kill -15 pid 或者kill pid时才能实现。但是如果发现:kill -15 pid 无法关闭应用,则可以考虑使用kill -9 pid,但请事后务必排查出是什么原因导致kill -15 pid 无法关闭。所以在编写停机脚本时也要先kill -15 pid ,如果关闭失败,再执行kill -9 pid

Shutdown Hook

先看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {

public static void main(String[] args) throws InterruptedException {
Runtime.getRuntime().addShutdownHook(new DbShutdownWork());
System.out.println("JVM 已启动");

while(true){
Thread.sleep(10L);
}
}

static class DbShutdownWork extends Thread{
@Override
public void run(){
System.out.println("关闭数据库连接");
}
}
}

当我们执行kill pid输出:

1
2
JVM 已启动
关闭数据库连接

Shutdown Hook 会保证 JVM 一直运行,直到 hook 终止。

SignalHandler

先看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Main1 {

public static void main(String[] args) throws InterruptedException {
// 信号处理实例
DbSignalHandler mySignalHandler = new DbSignalHandler();

// 注册对指定信号的处理
Signal.handle(new Signal("TERM") ,mySignalHandler);

System.out.println("JVM 已启动");

while(true){
Thread.sleep(10L);
}
}
}

class DbSignalHandler implements SignalHandler {

@Override
public void handle(Signal signal) {

// 信号量名称
String name = signal.getName();
// 信号量数值
int number = signal.getNumber();

if(name.equals("TERM") && number == 15){
System.out.println("关闭数据库连接");
System.exit(0);
}
}
}

SignalHandler是在sun.misc下的,就是JDK提供的各种后面啦。

看了上面两种方法,其实实质都是一样的,都是利用信用,在捕获终止信号的时候做一些操作来实现优雅停机。

Dubbo优雅停机

先引入官方文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 原理
## 服务提供方
* 停止时,先标记为不接收新请求,新请求过来时直接报错,让客户端重试其它机器。
* 然后,检测线程池中的线程是否正在运行,如果有,等待所有线程执行完成,除非超时,则强制关闭。

## 服务消费方
* 停止时,不再发起新的调用请求,所有新的调用在客户端即报错。
* 然后,检测有没有请求的响应还没有返回,等待响应返回,除非超时,则强制关闭。

## 设置方式
设置优雅停机超时时间,缺省超时时间是 10 秒,如果超时则强制关闭。
dubbo.properties
dubbo.service.shutdown.wait=15000
如果 ShutdownHook 不能生效,可以自行调用,使用tomcat等容器部署的場景,建议通过扩展ContextListener等自行调用以下代码实现优雅停机:
ProtocolConfig.destroyAll();

我们看到了熟悉的ShutdownHook,所以下面我们需要去找ShutdownHook是在哪添加的?

ShutdownHook

Dubbo 的优雅停机 ShutdownHookAbstractConfig 的静态代码块初始化:

1
2
3
static {
Runtime.getRuntime().addShutdownHook(DubboShutdownHook.getDubboShutdownHook());
}

我们看到DubboShutdownHook继承于Thread,所以我们需要去看的run方法,就能知道在停机时需要干什么:

1
2
3
4
5
6
7
8
9
10
11
12
public void run() {
destroyAll();
}
public void destroyAll() {
if (!destroyed.compareAndSet(false, true)) {
return;
}
// 关闭注册中心
AbstractRegistryFactory.destroyAll();
// 标记为不接收新请求,同时不再发起新的调用请求
destroyProtocols();
}

RegistryDestroy

上面我们看到停机之后主要做了两件事情,我们先来看第一件事情,关闭注册中心连接,,取消服务中的服务提供者和消费者的订阅注册。 我们以Zookeeper为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// org.apache.dubbo.registry.zookeeper.ZookeeperRegistry
public void destroy() {
// 调用父类FailbackRegistry的destroy方法
super.destroy();
try {
// 关闭zkClient的连接
zkClient.close();
} catch (Exception e) {
logger.warn("Failed to close zookeeper client " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
// org.apache.dubbo.registry.support.FailbackRegistry
public void destroy() {
// 调用父类AbstractRegistry的destroy方法
super.destroy();
try {
// 关闭心跳重试任务
retryFuture.cancel(true);
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
// 待会解析
ExecutorUtil.gracefulShutdown(retryExecutor, retryPeriod);
}

// org.apache.dubbo.registry.support.AbstractRegistry
public void destroy() {
// 取消注册
Set<URL> destroyRegistered = new HashSet<URL>(getRegistered());
if (!destroyRegistered.isEmpty()) {
for (URL url : new HashSet<URL>(getRegistered())) {
if (url.getParameter(Constants.DYNAMIC_KEY, true)) {
try {
// 取消注册
unregister(url);
if (logger.isInfoEnabled()) {
logger.info("Destroy unregister url " + url);
}
} catch (Throwable t) {
logger.warn("Failed to unregister url " + url + " to registry " + getUrl() + " on destroy, cause: " + t.getMessage(), t);
}
}
}
}
// 取消订阅
Map<URL, Set<NotifyListener>> destroySubscribed = new HashMap<URL, Set<NotifyListener>>(getSubscribed());
if (!destroySubscribed.isEmpty()) {
for (Map.Entry<URL, Set<NotifyListener>> entry : destroySubscribed.entrySet()) {
URL url = entry.getKey();
for (NotifyListener listener : entry.getValue()) {
try {
// 取消订阅
unsubscribe(url, listener);
if (logger.isInfoEnabled()) {
logger.info("Destroy unsubscribe url " + url);
}
} catch (Throwable t) {
logger.warn("Failed to unsubscribe url " + url + " to registry " + getUrl() + " on destroy, cause: " + t.getMessage(), t);
}
}
}
}
}

ProtocolDestroy

现在我们来看第二件事:标记为不接收新请求,同时不再发起新的调用请求,即销毁所有通信 ExchangeClient 和 ExchangeServer,其实最终就是关闭NettyServer和Client,这里以Dubbo协议为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
  private void destroyProtocols() {
ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
for (String protocolName : loader.getLoadedExtensions()) {
try {
Protocol protocol = loader.getLoadedExtension(protocolName);
if (protocol != null) {
// 关闭服务
protocol.destroy();
}
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}

// org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
public void destroy() {
// 销毁所有 ExchangeServer
for (String key : new ArrayList<String>(serverMap.keySet())) {
ExchangeServer server = serverMap.remove(key);
if (server != null) {
try {
if (logger.isInfoEnabled()) {
logger.info("Close dubbo server: " + server.getLocalAddress());
}
// 调用 HeaderExchangeServer#close
server.close(ConfigUtils.getServerShutdownTimeout());
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}
// 销毁所有 ExchangeClient
for (String key : new ArrayList<String>(referenceClientMap.keySet())) {
ExchangeClient client = referenceClientMap.remove(key);
if (client != null) {
try {
if (logger.isInfoEnabled()) {
logger.info("Close dubbo connect: " + client.getLocalAddress() + "-->" + client.getRemoteAddress());
}
// 调用 ReferenceCountExchangeClient#close
client.close(ConfigUtils.getServerShutdownTimeout());
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}
// 销毁所有幽灵 ExchangeClient
// 幽灵Client,是指在获取链接的时候,链接为空或者已经被关闭了
for (String key : new ArrayList<String>(ghostClientMap.keySet())) {
ExchangeClient client = ghostClientMap.remove(key);
if (client != null) {
try {
if (logger.isInfoEnabled()) {
logger.info("Close dubbo connect: " + client.getLocalAddress() + "-->" + client.getRemoteAddress());
}
// 调用 LazyConnectExchangeClient#close
client.close(ConfigUtils.getServerShutdownTimeout());
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}
stubServiceMethodsMap.clear();
// 调用父类AbstractProtocol的destroy方法
super.destroy();
}
// org.apache.dubbo.rpc.protocol.AbstractProtocol
public void destroy() {
// 不再发起新的调用请求
for (Invoker<?> invoker : invokers) {
if (invoker != null) {
invokers.remove(invoker);
try {
if (logger.isInfoEnabled()) {
logger.info("Destroy reference: " + invoker.getUrl());
}
invoker.destroy();
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}
// 标记为不接收新请求
for (String key : new ArrayList<String>(exporterMap.keySet())) {
Exporter<?> exporter = exporterMap.remove(key);
if (exporter != null) {
try {
if (logger.isInfoEnabled()) {
logger.info("Unexport service: " + exporter.getInvoker().getUrl());
}
exporter.unexport();
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}
}

ExecutorUtil

其实我们在分析中会看到ExecutorUtil#gracefulShutdown()这样一个方法,它其实对应的是检测线程池中的线程是否正在运行,如果有,等待所有线程执行完成,除非超时,则强制关闭。文档说它用的是Java自带的线程池关闭策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static void gracefulShutdown(Executor executor, int timeout) {
// 如果不是 ExecutorService ,或者已经关闭,忽略
if (!(executor instanceof ExecutorService) || isTerminated(executor)) {
return;
}
final ExecutorService es = (ExecutorService) executor;
try {
// 禁止新的任务提交,将原有任务执行完,这些都是自带线程池的机制
es.shutdown();
} catch (SecurityException ex2) {
return;
} catch (NullPointerException ex2) {
return;
}
try {
// 等待原有任务执行完。若等待超时,强制结束所有任务,默认为10秒
if (!es.awaitTermination(timeout, TimeUnit.MILLISECONDS)) {
// 抛弃队列中的任务,并中断所有工作线程
es.shutdownNow();
}
} catch (InterruptedException ex) {
// 发生 InterruptedException 异常,也强制结束所有任务
es.shutdownNow();
Thread.currentThread().interrupt();
}
// 若未关闭成功,新开线程去关闭
if (!isTerminated(es)) {
newThreadToCloseExecutor(es);
}
}

参考

  1. linux信号调用机制

  2. 研究优雅停机时的一点思考

  3. 精尽 Dubbo 源码解析 —— 优雅停机