该文章内容发布已经超过一年,请注意检查文章中内容是否过时。
对于任何一个线上应用,如何在服务更新部署过程中保证客户端无感知是开发者必须要解决的问题,即从应用停止到重启恢复服务这个阶段不能影响正常的业务请求。理想条件下,在没有请求的时候再进行更新是最安全可靠的,然而互联网应用必须要保证可用性,因此在技术层面上优化应用更新流程来保证服务在更新时无损是必要的。
传统的解决方式是通过将应用更新流程划分为手工摘流量、停应用、更新重启三个步骤,由人工操作实现客户端无对更新感知。这种方式简单而有效,但是限制较多:不仅需要使用借助网关的支持来摘流量,还需要在停应用前人工判断来保证在途请求已经处理完毕。这种需要人工介入的方式运维复杂度较高,只能适用规模较小的应用,无法在大规模系统上使用。
因此,如果在容器/框架级别提供某种自动化机制,来自动进行摘流量并确保处理完以到达的请求,不仅能保证业务不受更新影响,还可以极大地提升更新应用时的运维效率。
这个机制也就是优雅停机,目前Tomcat/Undertow/Dubbo等容器/框架都有提供相关实现。下面给出正式一些的定义:优雅停机是指在停止应用时,执行的一系列保证应用正常关闭的操作。这些操作往往包括等待已有请求执行完成、关闭线程、关闭连接和释放资源等,优雅停机可以避免非正常关闭程序可能造成数据异常或丢失,应用异常等问题。优雅停机本质上是JVM即将关闭前执行的一些额外的处理代码。
System.exit(int)
;OOM
);SIGTERM
或SIGINT
信号。在Dubbo中,优雅停机是默认开启的,停机等待时间为10000毫秒。可以通过配置dubbo.service.shutdown.wait
来修改等待时间。
例如将等待时间设置为20秒可通过增加以下配置实现:
dubbo.service.shutdown.wait=20000
当使用org.apache.dubbo.container.Main
这种容器方式来使用 Dubbo 时,也可以通过配置dubbo.shutdown.hook
为true
来开启优雅停机。
基于ShutdownHook
方式的优雅停机无法确保所有关闭流程一定执行完,所以 Dubbo 推出了多段关闭的方式来保证服务完全无损。
多段关闭即将停止应用分为多个步骤,通过运维自动化脚本或手工操作的方式来保证脚本每一阶段都能执行完毕。
在关闭应用前,首先通过 QOS 的offline
指令下线所有服务,然后等待一定时间确保已经到达请求全部处理完毕,由于服务已经在注册中心下线,当前应用不会有新的请求。这时再执行真正的关闭(SIGTERM
或 SIGINT
)流程,就能保证服务无损。
QOS可通过 telnet 或 HTTP 方式使用,具体方式请见Dubbo-QOS命令使用说明。
Provider在接收到停机指令后
Consumer在接收到停机指令后
当使用容器方式运行 Dubbo 时,在容器准备退出前,可进行一系列的资源释放和清理工。
例如使用 SpringContainer时,Dubbo 的ShutdownHook线程会执行ApplicationContext
的stop
和close
方法,保证 Bean的生命周期完整。
在加载类org.apache.dubbo.config.AbstractConfig
时,通过org.apache.dubbo.config.DubboShutdownHook
向JVM注册 ShutdownHook。
/**
* Register the ShutdownHook
*/
public void register() {
if (!registered.get() && registered.compareAndSet(false, true)) {
Runtime.getRuntime().addShutdownHook(getDubboShutdownHook());
}
}
每个ShutdownHook都是一个单独的线程,由JVM在退出时触发执行org.apache.dubbo.config.DubboShutdownHook
。
/**
* Destroy all the resources, including registries and protocols.
*/
public void doDestroy() {
if (!destroyed.compareAndSet(false, true)) {
return;
}
// destroy all the registries
AbstractRegistryFactory.destroyAll();
// destroy all the protocols
destroyProtocols();
}
首先关闭所有注册中心,这一步包括:
执行所有Protocol
的destroy()
,主要包括:
Invoker
和Exporter
;执行完毕,关闭JVM。
SIGKILL
关闭应用不会执行优雅停机;timeout
不是所有步骤等待时间的总和,而是每一个destroy
执行的最大时间。例如配置等待时间为5秒,则关闭Server、关闭Client等步骤会分别等待5秒。