解决dubbo不能优雅停机

摘要: 从源码角度解读为啥2.5.3版本不支持优雅停机,通过修改源码达到优雅停机
  • 现状

dubbo-2.5.4以下的版本不支持优雅停机,是因为服务端关闭时,客户端主动关闭长连接,导致服务端响应消息不能返回。

在服务端和客户端配置参数:-Ddubbo.service.shutdown.wait=30000,该参数为停机等待时间,但是结果也不生效,还是马上停机了。

  • 期望结果
  1. 正在读或者写的消息,需要继续处理;
  2. 线程池需等所有任务执行完成后,才关闭;
  • 服务端停机时客户端操作

当服务端停机时,客户端检测断开事件,马上关闭了与该服务端之间的连接,并无限次重连服务提供折。如果服务提供者只有一个时,则不在进行服务的远程调用。

客户端断开连接时序图:

通过zk的节点变化事件触发notify,客户端刷新服务提供者,删除该服务提供者【destroyUnusedInvokers方法】;如果只有一个服务提供者时,则销毁所有Invoker【destroyAllInvokers方法】。

  • 客户端代码修改

在dubboInvoker销毁时,直接调用了client.close()方法,源码如下:

public void destroy() {
        if (super.isDestroyed()){
            return ;
        } else {
            destroyLock.lock();
            try{
                if (super.isDestroyed()){
                    return ;
                }
                super.destroy();
                if (invokers != null){
                    invokers.remove(this);
                }
                for (ExchangeClient client : clients) {
                    try {
                        //直接关闭
                        client.close();
                    } catch (Throwable t) {
                        logger.warn(t.getMessage(), t);
                    }
                }
                
            }finally {
                destroyLock.unlock();
            }
        }
    }

 修改client.close()为client.close(timeout),代码修改如下:

com.alibaba.dubbo.rpc.protocol.dubbo.DubboInvoker

public void destroy() {
        if (super.isDestroyed()){
            return ;
        } else {
            destroyLock.lock();
            try{
                if (super.isDestroyed()){
                    return ;
                }
                super.destroy();
                if (invokers != null){
                    invokers.remove(this);
                }
                for (ExchangeClient client : clients) {
                    try {
                        //修改关闭
                        this.close(client);
                    } catch (Throwable t) {
                        logger.warn(t.getMessage(), t);
                    }
                }
                
            }finally {
                destroyLock.unlock();
            }
        }
    }
    
    /**
     * 如果没有设置dubbo.service.shutdown.wait
     *    或者dubbo.service.shutdown.wait.seconds参数,则直接关闭
     * @param client
     * @author 夏志强
     */
    @SuppressWarnings("deprecation")
	private void close(ExchangeClient client) {
    	String timeout = ConfigUtils.getProperty(Constants.SHUTDOWN_WAIT_KEY);
    	if(timeout != null && timeout.length() > 0) {
    		try{
    			client.close(Integer.parseInt(timeout));
    		} catch(Exception e) {
    		}
    	}
    }

修改完上述代码,运行后发现客户端还是马上关闭连接,调试代码发现HeaderExchangeChannel的close方法里,判断HeaderExchangeChannel.this是否在DefaultFuture中和优雅退出是否超过超时时间,如果过了超时时间,则立即关闭,源码如下:

public void close(int timeout) {
        if (closed) {
            return;
        }
        closed = true;
        if (timeout > 0) {
            long start = System.currentTimeMillis();
            //DefaultFuture中CHANNELS不包含HeaderExchangeChannel类型
            while (DefaultFuture.hasFuture(HeaderExchangeChannel.this) 
                    && System.currentTimeMillis() - start < timeout) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    logger.warn(e.getMessage(), e);
                }
            }
        }
        close();
    }

查看HeaderExchangeChannel的request方法,

DefaultFuture future = new DefaultFuture(channel, req, timeout);

实际上channel是NettyClient,所以channel不会是HeaderExchangeChannel类型。

修改代码如下:

com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeChannel

// graceful close
    public void close(int timeout) {
        if (closed) {
            return;
        }
        closed = true;
        if (timeout > 0) {
            long start = System.currentTimeMillis();
            while (DefaultFuture.hasFuture(channel) 
                    && System.currentTimeMillis() - start < timeout) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    logger.warn(e.getMessage(), e);
                }
            }
        }
        close();
    }

再次运行代码,发现结果还是不对,客户端一直在等待关闭,而服务端已经关闭了。

  • 服务端停机时序图

目前我分析的是ProtocolConfig的destoryAll()方法,主要分两步,第一步删除zk服务提供者节点,关闭zk监听;第二步dubbo协议销毁,链接关闭。

dubbo协议销毁时序图:

通过时序图看到服务端的优雅关闭是在AbstractServer的close(timeout)方法,源码如下:

public void close(int timeout) {
        ExecutorUtil.gracefulShutdown(executor ,timeout);
        close();
    }

//ExecutorUtil类
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的线程派发模型,只有executor不为空时,才会等待线程池任务执行完后关闭。这里有一个坑,那就是调用es.awaitTermination时,一定要先调用es.shutdown(),否则就算线程池中的任务执行完或者超时后,都不会关闭,一直阻塞【详情请查看jdk】。调试时发现executor为空,executor值是通过AbstractServer的构造函数来初始化的,源码如下:

ExecutorService executor;

    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);
        }
        //设置executor
        if (handler instanceof WrappedChannelHandler ){
            executor = ((WrappedChannelHandler)handler).getExecutor();
        }
    }

我们默认dubbo的底层通信框架为netty,所以查看NettyServer代码,

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

//以下为ChannelHandlers代码
public static ChannelHandler wrap(ChannelHandler handler, URL url){
        return ChannelHandlers.getInstance().wrapInternal(handler, url);
    }

    protected ChannelHandler wrapInternal(ChannelHandler handler, URL url) {
        return new MultiMessageHandler(new HeartbeatHandler(ExtensionLoader.getExtensionLoader(Dispatcher.class)
                                        .getAdaptiveExtension().dispatch(handler, url)));
    }

构造函数中将handler进行了包装,此时handler类型已经变成了MultiMessageHandler,而不是WrappedChannelHandler。看一下handler的继承关系:

因为dubbo的默认线程模型为AllChannelHandler(参照dubbo官方文档),AllChannelHandler父类为WrappedChannelHandler类型,所以需要通过反射来设置executor值。

修改AbstractServer的构造函数,给executor赋值:

com.alibaba.dubbo.remoting.transport.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);
        }
        //修改s
        this.setExecutor(handler);
    }
    
    /**
     * 设置executor
     * @param handler
     * @author 夏志强
     */
    private void setExecutor(ChannelHandler handler) {
    	if(handler != null) {
    		if (handler instanceof WrappedChannelHandler ){
                executor = ((WrappedChannelHandler)handler).getExecutor();
            } else if (handler instanceof AbstractChannelHandlerDelegate ){
                try {
					Field field = AbstractChannelHandlerDelegate.class.getDeclaredField("handler");
					field.setAccessible(true);
					setExecutor((ChannelHandler)field.get(handler));
				} catch (Exception e) {
				} 
            }
    	}
    }

再次运行测试代码,这时返回结果正常。

在Springboot + Dubbo框架中,为了避免任务执行未完成而导致数据丢失,可以通过优雅停机的方式来解决优雅停机的实现方式一般有两种: 1. 优雅停机钩子 在Springboot中,可以通过实现ApplicationRunner或者CommandLineRunner接口来实现优雅停机钩子。在程序关闭之前,Springboot会调用这些接口实现的方法,我们可以在这些方法中做一些清理工作或者等待任务执行完成后再关闭。例如: ``` @Component public class ShutdownHook implements ApplicationRunner { private static final Logger logger = LoggerFactory.getLogger(ShutdownHook.class); @Value("${spring.application.name}") private String applicationName; @Override public void run(ApplicationArguments args) throws Exception { logger.info("ShutdownHook is starting..."); Runtime.getRuntime().addShutdownHook(new Thread(() -> { logger.info("ShutdownHook is executing..."); // 在这里进行一些清理工作 logger.info("ShutdownHook has executed successfully."); })); } } ``` 2. Dubbo优雅停机 Dubbo框架提供了优雅停机功能,可以通过配置dubbo.shutdown.wait.time参数来实现。该参数为Dubbo服务停机等待时间,单位为毫秒,Dubbo服务停机时会等待该时间后再停机。例如: ``` <dubbo:provider shutdown="graceful" shutdown.wait="10000" /> <dubbo:consumer shutdown="graceful" shutdown.wait="10000" /> ``` 通过上述两种方式,可以在Springboot + Dubbo框架中实现优雅停机,避免任务未完成而导致数据丢失的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值