问题:在分布式服务中,业务模块间的通信场景很多,我们各模块间的通信使用的是motan,在老的框架中,每当某个服务模块上下线或出现故障时,总会导致调用该模块的其他服务报503,出现阻塞或报错现象。
分析:这其实就是上下线的模块在关闭服务时没有及时通知到注册中心下线节点导致服务下线仍有大量请求打过来,也就涉及到motan的优雅关闭问题。
Motan支持在Consul、ZooKeeper集群环境下优雅的关闭节点,当需要关闭或重启节点时,可以先将待上线节点从集群中摘除,避免直接关闭影响正常请求。待关闭节点需要调用以下代码,建议通过servlet或业务的管理模块进行该调用。
MotanSwitcherUtil.setSwitcherValue(MotanConstants.REGISTRY_HEARTBEAT_SWITCHER, false)
motan的启动
web应用 public class ContextListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent servletContextEvent) { // 启用 Motan MotanSwitcherUtil.setSwitcherValue(MotanConstants.REGISTRY_HEARTBEAT_SWITCHER, true); } @Override public void contextDestroyed(ServletContextEvent servletContextEvent) { } }
非web的spring-boot应用 @SpringBootApplication public class AccountApplication { public static void main(String[] args){ SpringApplication.run(AccountApplication.class, args); MotanSwitcherUtil.setSwitcherValue(MotanConstants.REGISTRY_HEARTBEAT_SWITCHER, true); } }
存在问题:在web项目中没有在contextDestroyed的监听中调用motan的销毁反注册钩子,导致motan服务已经下线,但是zk中依然保留服务的注册信息,客户端不能及时感知到服务的下线,导致下线后依然打过来大量的请求,造成客户端阻塞或者报错。
解决方案 针对web程序 在ServletContextListener中contextDestroyed方法中显示调用motan的反注册 @Override public void contextDestroyed(ServletContextEvent servletContextEvent) { MotanSwitcherUtil.setSwitcherValue(MotanConstants.REGISTRY_HEARTBEAT_SWITCHER, false); TimeUnit.SECONDS.sleep(1); }
针对springboot/普通 java应用 @SpringBootApplication public class AccountApplication { public static void main(String[] args){ SpringApplication.run(AccountApplication.class, args); Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { try { MotanSwitcherUtil.setSwitcherValue(MotanConstants.REGISTRY_HEARTBEAT_SWITCHER, false); TimeUnit.SECONDS.sleep(1); logger.info("motan service has shutdown gracefully!"); } catch (Exception e) { logger.error("error occured during montan service shutdown! pls check! ", e); } } }); } }
源码解析
官方推荐我们优雅关闭是推荐这句代码:
MotanSwitcherUtil.setSwitcherValue(MotanConstants.REGISTRY_HEARTBEAT_SWITCHER, false);
我们来看下Motan向MotanConstants.REGISTRY_HEARTBEAT_SWITCHER这个开关项注册了什么监听
public AbstractRegistry(URL url) { this.registryUrl = url.createCopy(); // register a heartbeat switcher to perceive service state change and change available state MotanSwitcherUtil.initSwitcher(MotanConstants.REGISTRY_HEARTBEAT_SWITCHER, false); MotanSwitcherUtil.registerSwitcherListener(MotanConstants.REGISTRY_HEARTBEAT_SWITCHER, new SwitcherListener() { @Override public void onValueChanged(String key, Boolean value) { if (key != null && value != null) { if (value) { available(null); } else { unavailable(null); } } } }); }
可见在Registry的抽象类中注册了这个开关的监听,当开关值变化后处理相应的动作,如果关闭,则触发unavailable方法,这个方法会注销所有的服务(在registry中删除所有的节点)。这里我们以zookeeper的实现为例:
@Override protected void doUnavailable(URL url) { try{ serverLock.lock(); if (url == null) { availableServices.removeAll(getRegisteredServiceUrls()); for (URL u : getRegisteredServiceUrls()) { removeNode(u, ZkNodeType.AVAILABLE_SERVER); removeNode(u, ZkNodeType.UNAVAILABLE_SERVER); createNode(u, ZkNodeType.UNAVAILABLE_SERVER); } } else { availableServices.remove(url); removeNode(url, ZkNodeType.AVAILABLE_SERVER); removeNode(url, ZkNodeType.UNAVAILABLE_SERVER); createNode(url, ZkNodeType.UNAVAILABLE_SERVER); } } finally { serverLock.unlock(); } }
监听触发后会注销所有的服务。
问题
这个监听到底是在哪个阶段执行呢,是否会在停止的过程中依然进来部分请求?
这个就需要看下ServletContextListener的生命周期跟JVM的ShutdownHook的执行时机了。
1、 ServletContextListener的生命周期
ServletContextListener监听 ServletContext 对象的生命周期,实际上就是监听 Web 应用的生命周期。
当Servlet 容器启动或终止Web 应用时,会触发ServletContextEvent 事件,该事件由ServletContextListener 来处理。在 ServletContextListener 接口中定义了处理ServletContextEvent 事件的两个方法。
/** * 当Servlet 容器启动Web 应用时调用该方法。在调用完该方法之后,容器再对Filter 初始化, * 并且对那些在Web 应用启动时就需要被初始化的Servlet 进行初始化。 */ contextInitialized(ServletContextEvent sce) /** * 当Servlet 容器终止Web 应用时调用该方法。在调用该方法之前,容器会先销毁所有的Servlet 和Filter 过滤器。 */ contextDestroyed(ServletContextEvent sce)
可见,不能在contextDestroyed中做motan的关闭,因为这会导致关闭前一些请求进来,但是相关引用对象已被销毁而无法执行。
2、ShutdownHook
当 JVM 接受到系统的关闭通知之后,调用 ShutdownHook 内的方法,它会在容器销毁前执行,很多开源框架基于这个机制实现优雅停机,如dubbo、spring等。
我们的实现:
@ConditionalOnClass({ MotanSwitcherUtil.class, ZookeeperRegistryFactory.class, ProtocolConfigBean.class, NettyChannelFactory.class }) public class MotanConfigListenerAutoConfiguration { private static final Logger logger = Loggers.getFrameworkLogger(); @Bean public MotanConfigPrintSpringListener motanConfigPrintSpringListener() { return new MotanConfigPrintSpringListener(); } @Bean public ApplicationListener<ApplicationEvent> motanSwitcherListener() { return event -> { if (event instanceof ApplicationReadyEvent) { MotanSwitcherUtil.setSwitcherValue(MotanConstants.REGISTRY_HEARTBEAT_SWITCHER, true); logger.info("motan service has started!"); Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { MotanSwitcherUtil.setSwitcherValue(MotanConstants.REGISTRY_HEARTBEAT_SWITCHER, false); TimeUnit.SECONDS.sleep(1); logger.info("motan service has shutdown gracefully!"); } catch (Exception e) { logger.error("error occurred during motan service shutdown! pls check! ", e); } })); logger.info("motan service shutdown hook added!"); } else if (event instanceof ContextClosedEvent) { logger.info("ContextClosedEvent triggered, will start shutdown motan service..."); } }; }