Dubbo在Docker中的优雅停机
优雅停机
优雅停机是指在停止应用时,执行的一系列保证应用正常关闭的操作。这些操作往往包括等待已有请求执行完成、关闭线程、关闭连接和释放资源等,优雅停机可以避免非正常关闭程序可能造成数据异常或丢失,应用异常等问题。优雅停机本质上是JVM即将关闭前执行的一些额外的处理代码。这个功能官方是支持的,只要正常kill SIGTERM或SIGIN 就可以。
Dubbo服务关闭流程
Provider在接收到停机指令后
- 从注册中心上注销所有服务;
- 从配置中心取消监听动态配置;
- 向所有连接的客户端发送只读事件,停止接收新请求;
- 等待一段时间以处理已到达的请求,然后关闭请求处理线程池;
- 断开所有客户端连接。
Consumer在接收到停机指令后
- 拒绝新到请求,直接返回调用异常;
- 等待当前已发送请求执行完毕,如果响应超时则强制关闭连接。
源码分析
参考Dubbo版本2.7.6
- Provider 启动时注册钩子
private DubboBootstrap() {
DubboShutdownHook.getDubboShutdownHook().register();
ShutdownHookCallbacks.INSTANCE.addCallback(new ShutdownHookCallback() {
@Override
public void callback() throws Throwable {
DubboBootstrap.this.destroy();
}
});
}
public void register() {
if (registered.compareAndSet(false, true)) {
DubboShutdownHook dubboShutdownHook = getDubboShutdownHook();
Runtime.getRuntime().addShutdownHook(dubboShutdownHook);
dispatch(new DubboShutdownHookRegisteredEvent(dubboShutdownHook));
}
}
- 收到退出信号
//执行销毁
DubboBootstrap.java
public void destroy() {
if (destroyLock.tryLock()) {
try {
DubboShutdownHook.destroyAll();
.
.
.
} finally {
destroyLock.unlock();
}
}
}
//销毁
DubboShutdownHook.java
public static void destroyAll() {
if (destroyed.compareAndSet(false, true)) {
AbstractRegistryFactory.destroyAll();
destroyProtocols();
}
}
//从注册中心删除,并取消监听
AbstractRegistry.java
public void destroy() {
//删除节点
Set destroyRegistered = new HashSet<>(getRegistered());
if (!destroyRegistered.isEmpty()) {
for (URL url : new HashSet<>(getRegistered())) {
if (url.getParameter(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> destroySubscribed = new HashMap<>(getSubscribed());
if (!destroySubscribed.isEmpty()) {
for (Map.Entry> entry : destroySubscribed.entrySet()) {
URL url = entry.getKey();
for (NotifyListener listener : entry.getValue()) {
try {
unsubscribe(url, listener);
} catch (Throwable t) {
logger.warn("Failed to unsubscribe url " + url + " to registry " + getUrl() + " on destroy, cause: " + t.getMessage(), t);
}
}
}
}
AbstractRegistryFactory.removeDestroyedRegistry(this);
}
//注销协议
DubboShutdownHook.java
public static void destroyProtocols() {
ExtensionLoader 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);
}
}
}
docker容器中Dubbo不优雅停机
按理说,我们不用做任何干预就可以实现优雅停机了,但是实际上却是Dubbo Provider服务重启时,总是能收到如下告警,且服务关闭慢。
Connection refused: /192.168.1.112:20880
at com.alibaba.dubbo.remoting.transport.netty.NettyClient.doConnect(NettyClient.java:124)
at com.alibaba.dubbo.remoting.transport.AbstractClient.connect(AbstractClient.java:280)
at com.alibaba.dubbo.remoting.transport.AbstractClient$1.run(AbstractClient.java:145)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Caused by: java.net.ConnectException: Connection refused: /192.168.1.112:20880
at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)
at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:717)
at org.jboss.netty.channel.socket.nio.NioClientBoss.connect(NioClientBoss.java:152)
at org.jboss.netty.channel.socket.nio.NioClientBoss.processSelectedKeys(NioClientBoss.java:105)
at org.jboss.netty.channel.socket.nio.NioClientBoss.process(NioClientBoss.java:79)
at org.jboss.netty.channel.socket.nio.AbstractNioSelector.run(AbstractNioSelector.java:337)
at org.jboss.netty.channel.socket.nio.NioClientBoss.run(NioClientBoss.java:42)
at org.jboss.netty.util.ThreadRenamingRunnable.run(ThreadRenamingRunnable.java:108)
at org.jboss.netty.util.internal.DeadLockProofWorker$1.run(DeadLockProofWorker.java:42)
... 3 common frames omitted
通过分析得知是Dockerfile中启动java进程命令导致的,原始:
CMD java ${CMD_JAVA_ARGS} -jar /opt/app/application.jar 2>&1
改为
CMD ["java",$CMD_JAVA_ARGS,"-jar","/opt/app/${JobFile}","2>&1"]
两种CMD启动方式
CMD ["executable","param1","param2"] (直接启动程序,这是推荐Docker官方推荐用法)
CMD command param1 param2 (通过shell启动)
按照方式一 ,进程结构为:
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Apr17 ? 00:11:21 java -jar application.jar
按照方式二,进程结构为
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Mar24 ? 00:00:00 /bin/sh -c java -jar application.jar
root 5 1 0 Mar24 ? 01:37:21 java -jar application.jar
此时,我们的sh进程是我们的java程序的父进程。
Docker停止容器
docker stop,当我们用docker stop命令来停掉容器的时候,docker默认会允许容器中的应用程序有10秒的时间用以终止运行。在docker stop命令执行的时候,会先向容器中PID为1的进程发送系统信号SIGTERM,然后等待容器中的应用程序终止执行,如果等待时间达到设定的超时时间,或者默认的10秒,会继续发送SIGKILL的系统信号强行kill掉进程。在容器中的应用程序,可以选择忽略和不处理SIGTERM信号,不过一旦达到超时时间,程序就会被系统强行kill掉,因为SIGKILL信号是直接发往系统内核的,应用程序没有机会去处理它。
那么问题来了,对于启动方式二,我们的bash进程是1号进程,但它并不会将SIGTERM信号传递给我们的java进程,我们就没有机会进行优雅停机了。
总结
Dubbo官方是支持优雅停机的,Docker官方也是支持优雅停机的,但是使用需掌握正确的姿势。
参考:
https://dubbo.apache.org/zh-cn/blog/dubbo-gracefully-shutdown.html
https://www.infoq.cn/article/2016/01/dumb-init-Docker
https://yeasy.gitbooks.io/docker_practice/image/dockerfile/cmd.html
https://xiaozhou.net/stop-docker-container-gracefully-2016-09-08.html