美文网首页
Dubbo在Docker中的优雅停机

Dubbo在Docker中的优雅停机

作者: 望望hope | 来源:发表于2020-04-20 23:41 被阅读0次

    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<URL> 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<URL, Set<NotifyListener>> destroySubscribed = new HashMap<>(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);
                    } 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<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);
            }
        }
    }
    

    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中的优雅停机

          本文链接:https://www.haomeiwen.com/subject/duheihtx.html