一文聊透 Dubbo 优雅停机

1 前言一年之前,我(老徐)曾经写过一篇《研究优雅停机时的一点思考》,主要介绍了 kill -9,kill -15 两个 linux 指令的含义,并且针对性的聊到了 Spring Boot 应用如何正确的优雅停机,算是本文的前置文章,如果你对上述概念不甚了解,建议先去浏览一遍,再回头来看这篇文章 。这篇文章将会以 Dubbo 为例,既聊架构设计,也聊源码,聊聊服务治理框架要真正实现优雅停机,需要注意哪些细节 。
本文的写作思路是从 Dubbo 2.5.x 开始,围绕优雅停机这个优化点,一直追溯到最新的 2.7.x 。先对 Dubbo 版本做一个简单的科普:2.7.x 和 2.6.x 是目前官方推荐使用的版本,其中 2.7.x 是捐献给 Apache 的版本,具备了很多新的特性,目前最新的 release 版本是 2.7.4,处于生产基本可用的状态;2.6.x 处于维护态,主要以 bugfix 为主,但经过了很多公司线上环境的验证,所以求稳的话,可以使用 2.6.x 分支最新的版本 。至于 2.5.x,社区已经放弃了维护,并且 2.5.x 存在一定数量的 bug,本文介绍的 Dubbo 优雅停机特性便体现了这一点 。
优雅停机一直是一个非常严谨的话题,但由于其仅仅存在于重启、下线这样的部署阶段,导致很多人忽视了它的重要性,但没有它,你永远不能得到一个完整的应用生命周期,永远会对系统的健壮性持怀疑态度 。
同时,优雅停机又是一个庞大的话题

  • 操作系统层面,提供了 kill -9 (SIGKILL)和 kill -15(SIGTERM) 两种停机策略
  • 语言层面,JAVA 应用有 JVM shutdown hook 这样的概念
  • 框架层面,Spring Boot 提供了 actuator 的下线 endpoint,提供了 ContextClosedEvent 事件
  • 容器层面,Docker :当执行 docker stop 命令时,容器内的进程会收到 SIGTERM 信号,那么 Docker Daemon 会在 10s 后,发出 SIGKILL 信号;K8S 在管理容器生命周期阶段中提供了 prestop 钩子方法
  • 应用架构层面,不同架构存在不同的部署方案 。单体式应用中,一般依靠 Nginx 这样的负载均衡组件进行手动切流,逐步部署集群;微服务架构中,各个节点之间有复杂的调用关系,上述这种方案就显得不可靠了,需要有自动化的机制 。
为避免该话题过度发散,本文的重点将会集中在框架和应用架构层面,探讨以 Dubbo 为代表的微服务架构在优雅停机上的最佳实践 。Dubbo 的优雅下线主要依赖于注册中心组件,由其通知消费者摘除下线的节点,如下图所示:
一文聊透 Dubbo 优雅停机

文章插图
 
上述的操作旨在让服务消费者避开已经下线的机器,但这样就算实现了优雅停机了吗?似乎还漏掉了一步,在应用停机时,可能还存在执行到了一半的任务,试想这样一个场景:一个 Dubbo 请求刚到达提供者,服务端正在处理请求,收到停机指令后,提供者直接停机,留给消费者的只会是一个没有处理完毕的超时请求 。
 
结合上述的案例,我们总结出 Dubbo 优雅停机需要满足两点基本诉求:
  1. 服务消费者不应该请求到已经下线的服务提供者
  2. 在途请求需要处理完毕,不能被停机指令中断
优雅停机的意义:应用的重启、停机等操作,不影响业务的连续性 。
3 优雅停机初始方案 — 2.5.x为了让读者对 Dubbo 的优雅停机有一个最基础的理解,我们首先研究下 Dubbo 2.5.x 的版本,这个版本实现优雅停机的方案相对简单,容易理解 。
3.1 入口类:AbstractConfig
public abstract class AbstractConfig implements Serializable { static { Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { public void run() { ProtocolConfig.destroyAll(); } }, "DubboShutdownHook")); }}在 AbstractConfig 的静态块中,Dubbo 注册了一个 shutdown hook,用于执行 Dubbo 预设的一些停机逻辑,继续跟进 ProtocolConfig.destroyAll()。
3.2 ProtocolConfig
public static void destroyAll() { if (!destroyed.compareAndSet(false, true)) { return; } AbstractRegistryFactory.destroyAll(); // ①注册中心注销 // Wait for registry notification try { Thread.sleep(ConfigUtils.getServerShutdownTimeout()); // ② sleep 等待 } catch (InterruptedException e) { logger.warn("Interrupted unexpectedly when waiting for registry notification during shutdown process!"); } 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); } }}Dubbo 中的 Protocol 这个词不太能望文生义,它一般被翻译为”协议”,但我更习惯将它理解为“流程”,从 Protocol 接口的三个方法反而更加容易理解 。


推荐阅读