解决Dubbo不能优雅停机,升级版本

本文深入探讨了Dubbo版本2.5.3存在的优雅停机bug,分析了其内部实现机制,包括JVM优雅停机支持、源码分析及线程池管理。并给出了升级至2.6.5版本后的解决方案,以及系统参数、日志框架和线程池超时时间的配置建议。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

项目dubbo版本是2.5.3。每次发布新版本,使用kill -9 PID 来停机,导致业务中断,需要后期人工修复。

这个是背景,激发了我去探讨dubbo优雅停机,提升项目的可维护性,健壮性。

 

首先,dubbo是支持优雅停机的。但是2.5.3版本有bug,所以该版本并不支持。接下来分析

一、JVM支持优雅停机addShutdownHook

dubbo实现优雅停机,是依赖JVM的支持的。JVM提供了Runtime.getRuntime().addShutdownHook(new Thread()),这个系统方法,参数是一个线程。当发起kill PID或者System.exit(0),就会触发执行里面的线程。这也就是一切优雅停机的入口。

二、2.5.3源码分析

dubbo的优雅停机入口在AbstractConfig类上

static {
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            public void run() {
                if (logger.isInfoEnabled()) {
                    logger.info("Run shutdown hook now.");
                }
                ProtocolConfig.destroyAll();
            }
        }, "DubboShutdownHook"));
}

ProtocolConfig是实际执行类

    public static void destroyAll() {
        AbstractRegistryFactory.destroyAll();
        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);
            }
        }
    }

注意第一行

AbstractRegistryFactory.destroyAll();

它是取消注册中心的注册和订阅,我们项目的注册中心,采用的是Zookeeper。所以执行的是zookeeper的取消注册和订阅。下面,删了一些项目信息,但是不影响读者查看。读者做优雅停机测试,自行查看日志。

[DubboShutdownHook] INFO  com.alibaba.dubbo.registry.support.AbstractRegistryFactory(64) -  [DUBBO] Close all registries

[DubboShutdownHook] INFO  com.alibaba.dubbo.registry.zookeeper.ZookeeperRegistry(451) -  [DUBBO] Destroy registry:zookeeper:

[DubboShutdownHook] INFO  com.alibaba.dubbo.registry.zookeeper.ZookeeperRegistry(284) -  [DUBBO] Unregister: consumer:

注意

protocol.destroy();

Protocol 是基类,项目默认采用了DubboProtocol协议,看DubboProtocol的destroy方法

    public void destroy() {
        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());
                    }
                    server.close(getServerShutdownTimeout());
                } catch (Throwable t) {
                    logger.warn(t.getMessage(), t);
                }
            }
        }
        
        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());
                    }
                    client.close();
                } catch (Throwable t) {
                    logger.warn(t.getMessage(), t);
                }
            }
        }
        
        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());
                    }
                    client.close();
                } catch (Throwable t) {
                    logger.warn(t.getMessage(), t);
                }
            }
        }
        stubServiceMethodsMap.clear();
        super.destroy();
    }

 

这里理一下dubbo优雅关机的流程。第一步,它是取消zookeeper注册,使得不再有consumer向它发起请求。截断流量,这是必须的第一步。然后,已经发起的任务,需要等待结束。dubbo管理一个线程池,dubbo任务的线程被该线程池管理。所以第二步,发起线程池关闭。

server.close(getServerShutdownTimeout());目的是关键dubbo的线程池就是做这个事情。

getServerShutdownTimeout()是线程池关闭的最大等待时间。默认是10秒,系统参数dubbo.service.shutdown.wait可以配置

项目里,server的实现类,是NettyServer

public class NettyServer extends AbstractServer implements Server {
    
    private static final Logger logger = LoggerFactory.getLogger(NettyServer.class);

    private Map<String, Channel>  channels; // <ip:port, channel>

    private ServerBootstrap                 bootstrap;

    private org.jboss.netty.channel.Channel channel;

    public NettyServer(URL url, ChannelHandler handler) throws RemotingException{
        super(url, ChannelHandlers.wrap(handler, ExecutorUtil.setThreadName(url, SERVER_THREAD_POOL_NAME)));
    }

close()方法是继承AbstractServer的

    public AbstractServer(URL url, ChannelHandler handler) throws RemotingException {
        super(url, handler);
        localAddress = getUrl().toInetSocketAddress();
        String host = url.getParameter(Constants.ANYHOST_KEY, false) 
                        || NetUtils.isInvalidLocalHost(getUrl().getHost()) 
                        ? NetUtils.ANYHOST : getUrl().getHost();
        bindAddress = new InetSocketAddress(host, getUrl().getPort());
        this.accepts = url.getParameter(Constants.ACCEPTS_KEY, Constants.DEFAULT_ACCEPTS);
        this.idleTimeout = url.getParameter(Constants.IDLE_TIMEOUT_KEY, Constants.DEFAULT_IDLE_TIMEOUT);
        try {
            doOpen();
            if (logger.isInfoEnabled()) {
                logger.info("Start " + getClass().getSimpleName() + " bind " + getBindAddress() + ", export " + getLocalAddress());
            }
        } catch (Throwable t) {
            throw new RemotingException(url.toInetSocketAddress(), null, "Failed to bind " + getClass().getSimpleName() 
                                        + " on " + getLocalAddress() + ", cause: " + t.getMessage(), t);
        }
        if (handler instanceof WrappedChannelHandler ){
            executor = ((WrappedChannelHandler)handler).getExecutor();
        }
    }
    public void close(int timeout) {
        ExecutorUtil.gracefulShutdown(executor ,timeout);
        close();
    }

 

再看 ExecutorUtil.gracefulShutdown(executor ,timeout);

    public static void gracefulShutdown(Executor executor, int timeout) {
        if (!(executor instanceof ExecutorService) || isShutdown(executor)) {
            return;
        }
        final ExecutorService es = (ExecutorService) executor;
        try {
            es.shutdown(); // Disable new tasks from being submitted
        } catch (SecurityException ex2) {
            return ;
        } catch (NullPointerException ex2) {
            return ;
        }
        try {
            if(! es.awaitTermination(timeout, TimeUnit.MILLISECONDS)) {
                es.shutdownNow();
            }
        } catch (InterruptedException ex) {
            es.shutdownNow();
            Thread.currentThread().interrupt();
        }
        if (!isShutdown(es)){
            newThreadToCloseExecutor(es);
        }
    }

executor 就是dubbo管理的线程池对象,发起关闭请求,最大等待时间。

这里解析,为什么2.5.3不能优雅停机。看AbstractServer的构造函数和NettyServer的构造函数

if (handler instanceof WrappedChannelHandler ){
            executor = ((WrappedChannelHandler)handler).getExecutor();
        }
    public NettyServer(URL url, ChannelHandler handler) throws RemotingException{
        super(url, ChannelHandlers.wrap(handler, ExecutorUtil.setThreadName(url, SERVER_THREAD_POOL_NAME)));
    }

NettyServer的handler,被包装成com.alibaba.dubbo.remoting.transport.MultiMessageHandler类,它不是com.alibaba.dubbo.remoting.transport.dispatcher.WrappedChannelHandler的子类,

导致executor未被赋值,是null,优雅停机就实现不了。

这个bug,假如还想使用2.5.3版本,需要修改源码。网上有相应的解决方案。但是我更推荐采用dubbo新版本,新版本已经修复了这个bug。下面是2.6.5的AbstractServer的构造函数:

    public AbstractServer(URL url, ChannelHandler handler) throws RemotingException {
        super(url, handler);
        localAddress = getUrl().toInetSocketAddress();

        String bindIp = getUrl().getParameter(Constants.BIND_IP_KEY, getUrl().getHost());
        int bindPort = getUrl().getParameter(Constants.BIND_PORT_KEY, getUrl().getPort());
        if (url.getParameter(Constants.ANYHOST_KEY, false) || NetUtils.isInvalidLocalHost(bindIp)) {
            bindIp = NetUtils.ANYHOST;
        }
        bindAddress = new InetSocketAddress(bindIp, bindPort);
        this.accepts = url.getParameter(Constants.ACCEPTS_KEY, Constants.DEFAULT_ACCEPTS);
        this.idleTimeout = url.getParameter(Constants.IDLE_TIMEOUT_KEY, Constants.DEFAULT_IDLE_TIMEOUT);
        try {
            doOpen();
            if (logger.isInfoEnabled()) {
                logger.info("Start " + getClass().getSimpleName() + " bind " + getBindAddress() + ", export " + getLocalAddress());
            }
        } catch (Throwable t) {
            throw new RemotingException(url.toInetSocketAddress(), null, "Failed to bind " + getClass().getSimpleName()
                    + " on " + getLocalAddress() + ", cause: " + t.getMessage(), t);
        }
        //fixme replace this with better method
        DataStore dataStore = ExtensionLoader.getExtensionLoader(DataStore.class).getDefaultExtension();
        executor = (ExecutorService) dataStore.get(Constants.EXECUTOR_SERVICE_COMPONENT_KEY, Integer.toString(url.getPort()));
    }

解决了这个问题。

 

三、系统参数设置

-Ddubbo.application.logger=slf4j

这个参数,指定dubbo的日志框架。根绝读者项目的特点,可以选择其他。这个参数必须指定,否则,dubbo不会打印日志

第三方日志框架优先级
Log4j最高(默认就用这个)
SLF4J次高(上面没有采用这个)
Common Logging(jcl就是common logging)次低(Log4j和SLF4J在项目中均没有就用这个)
JDK log最低(最后的选择)

 

 

-Ddubbo.service.shutdown.wait=40000

dubbo线程池最大超时时间。毫秒为单位。默认是10秒,我测试用的是40秒。读者根据项目,灵活配置。

 

<configuration debug="off" monitorInterval="1800" shutdownHook="disable">

我们项目用的是log4j2.xml的配置,shutdownHook="disable"是必须配置的。因为log4j本身也有优雅关机特性,如果不配置,会产生log4j的异常,导致优雅停机失败这个参数很关键!!!

 第四部、总结优雅停机的实现和注意单

第三部的系统参数配置完成后,实现:

1.dubbo版本升级到2.6.5

2.关闭进程,采用kill PID

3.观察和等待优雅停机是否执行完毕,此时进程还存在,不能马上重启。

 

注意点:

dubbo优雅停机,只会关心dubbo管理的线程池,也就是说只会等待dubbo的线程,线程名是DubboServerHandler,不会等待进程中的其他线程。假如有非dubbo线程在执行业务,并且希望它能执行完。那么就要采用其他方法。

可以添加自定义关闭钩子

Runtime.getRuntime().addShutdownHook(new Thread())

最简单的方式,就是Thread线程,sleep一段时间,让非dubbo线程跑完。

 

 

好,分享就到这里。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值