项目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线程跑完。
好,分享就到这里。