Dubbo在Docker中的优雅停机

Dubbo在Docker中的优雅停机

优雅停机

优雅停机是指在停止应用时,执行的一系列保证应用正常关闭的操作。这些操作往往包括等待已有请求执行完成、关闭线程、关闭连接和释放资源等,优雅停机可以避免非正常关闭程序可能造成数据异常或丢失,应用异常等问题。优雅停机本质上是JVM即将关闭前执行的一些额外的处理代码。这个功能官方是支持的,只要正常kill SIGTERM或SIGIN 就可以。

Dubbo服务关闭流程

Provider在接收到停机指令后

  • 从注册中心上注销所有服务;
  • 从配置中心取消监听动态配置;
  • 向所有连接的客户端发送只读事件,停止接收新请求;
  • 等待一段时间以处理已到达的请求,然后关闭请求处理线程池;
  • 断开所有客户端连接。

Consumer在接收到停机指令后

  • 拒绝新到请求,直接返回调用异常;
  • 等待当前已发送请求执行完毕,如果响应超时则强制关闭连接。

源码分析

参考Dubbo版本2.7.6

  1. 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));
    }
}
  1. 收到退出信号
//执行销毁
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启动方式

  1. CMD ["executable","param1","param2"] (直接启动程序,这是推荐Docker官方推荐用法)

  2. 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

你可能感兴趣的:(Dubbo在Docker中的优雅停机)