1、优雅启动
1.1、说明
优雅启动的目的是为了保证在应用完全启动成功之后再接收到外部请求,否则在应用未完全准备好之前接收请求,则无法正常处理请求,会报错。
1.2、使用场景
在分布式应用中,经常会根据实际需要对应用服务集群进行扩容和缩容,而进行扩容时,即新启动加入集群的应用,应该在应用彻底启动完成后才能接收请求。
1.3、实现方案
消费方服务是从注册中心获取到提供方服务的信息,从而向提供方发起的请求。所以只需要保证在提供方应用未彻底启动之前,消费方不会从注册中心获取到提供方的信息,就不会向未启动成功的提供方发出请求了。
在应用启动完成之前,不要向Nacos-Server注册自身的信息,在应用启动完成之后,再向Nacos-Server注册。
Nacos-Client实现了SpringCloud提供的注册中心的接口,从而实现了在Springboot启动时自动注册。具体注册逻辑,可以参考此文:https://www.yuque.com/davi/pwc2vg/hdwoti
Nacos-Client在Springboot启动时自动向Server端注册时,是通过这个类:com.alibaba.cloud.nacos.registry.NacosServiceRegistry中的register方法来注册的。所以我们可以通过自定义实现一个CustomNacosServiceRegistry继承NacosServiceRegistry并且替换它,重写register方法,即可实现不自动注册。
通过自定义的CustomNacosServiceRegistry来组织它自动注册之后,还需要在应用启动完成之后再手动将应用注册到Nacos-Server。我们可以通过启动一个轮询线程NacosRegisterTask,这个轮询线程将会在应用启动期间不停的轮询应用的状态,如果应用的状态处于启动完成、可注册,那么该线程就会把应用注册到Nacos-Server。
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;import com.alibaba.cloud.nacos.registry.NacosServiceRegistry;import com.alibaba.nacos.api.naming.NamingService;import com.alibaba.nacos.api.naming.pojo.Instance;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.cloud.client.serviceregistry.Registration;public class CustomNacosServiceRegistry extends NacosServiceRegistry {private static final Logger log = LoggerFactory.getLogger(CustomNacosServiceRegistry.class);private final NacosDiscoveryProperties nacosDiscoveryProperties;private final NamingService namingService;public CustomNacosServiceRegistry(NacosDiscoveryProperties nacosDiscoveryProperties) {super(nacosDiscoveryProperties);this.nacosDiscoveryProperties = nacosDiscoveryProperties;this.namingService = nacosDiscoveryProperties.namingServiceInstance();}@Overridepublic void register(Registration registration) {if (StringUtils.isEmpty(registration.getServiceId())) {log.warn("No service to register for nacos client...");return;}String serviceId = registration.getServiceId();Instance instance = getNacosInstanceFromRegistration(registration);//将实例的健康状态设置为false,注册到nacos的实例就不会接受请求instance.setEnabled(false);try {namingService.registerInstance(serviceId, instance);log.warn("The system is not ready now, relegate to register thread to do register on nacos. ");//创建轮训任务new NacosRegisterTask(instance, namingService, serviceId).start();} catch (Exception e) {log.error("nacos registry, {} register failed...{},", serviceId,registration.toString(), e);}}private Instance getNacosInstanceFromRegistration(Registration registration) {Instance instance = new Instance();instance.setIp(registration.getHost());instance.setPort(registration.getPort());instance.setWeight(nacosDiscoveryProperties.getWeight());instance.setClusterName(nacosDiscoveryProperties.getClusterName());instance.setMetadata(registration.getMetadata());return instance;}}
如何用自定义的CustomNacosServiceRegistry替换原本的NacosServiceRegistry呢?原本的NacosServiceRegistry是会注册到Spring容器中的,那么我们可以将自定义的CustomNacosServiceRegistry也注册到容器中,并且设置优先级高于原本的NacosServiceRegistry,这样Springboot启动时获取注册中心实现的Bean时就会优先获取到我们自定义的CustomNacosServiceRegistry。
import cn.sunline.adp.boot.cedar.handler.CustomNacosServiceRegistry;import com.alibaba.cloud.nacos.NacosDiscoveryProperties;import com.alibaba.cloud.nacos.registry.NacosServiceRegistry;import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;@ConditionalOnProperty(name="spring.cloud.nacos.discovery.enabled",havingValue ="true",matchIfMissing=false)@Configurationpublic class NacosRegistryConfig {//设置一个标识位,标识应用当前的状态是否可以注册private boolean isRegistryNow=false;public boolean isRegistryNow() {return isRegistryNow;}//在应用启动完成之后,通过该set方法将标识位置为truepublic void setRegistryNow(boolean isRegistryNow) {this.isRegistryNow = isRegistryNow;}@Bean("NacosServiceRegistry") //指定BeanName,防止与原本的NacosServiceRegistry冲突@Primary //通过该注解可以设置Bean的优先级public NacosServiceRegistry nacosServiceRegistry(NacosDiscoveryProperties nacosDiscoveryProperties) {return new CustomNacosServiceRegistry(nacosDiscoveryProperties);}}
package cn.sunline.adp.boot.cedar.handler;import cn.sunline.adp.boot.configuration.NacosRegistryConfig;import cn.sunline.adp.cedar.base.logging.SysLog;import cn.sunline.adp.cedar.base.logging.SysLogUtil;import cn.sunline.adp.core.GlobalContext;import cn.sunline.adp.core.util.SpringUtils;import com.alibaba.nacos.api.exception.NacosException;import com.alibaba.nacos.api.naming.NamingService;import com.alibaba.nacos.api.naming.pojo.Instance;import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class NacosRegisterTask {private static final Logger log = LoggerFactory.getLogger(NacosRegisterTask.class);private Instance instance;private final NamingService namingService;private String serviceId;public NacosOnline(Instance instance, NamingService namingService, String serviceId) {this.instance=instance;this.namingService=namingService;this.serviceId=serviceId;}public void start() {//启动轮训线程new NacosRegistryThread().start();}class NacosRegistryThread extends Thread{//从Spring容器中获取设置的应用状态标识位NacosRegistryConfig nacosConfig= SpringUtils.getBean(NacosRegistryConfig.class);@Overridepublic void run() {try {//如果应用当前的状态是false,则自旋1秒while(!nacosConfig.isRegistryNow()){TimeUnit.SECOND.sleep(1);}//将实例的健康状态设置为trueinstance.setEnabled(true);namingService.registerInstance(serviceId, instance);log.info("The system is ready now, register to nacos success.");} catch (NacosException e) {e.printStackTrace();} catch (InterruptedException e) {e.printStackTrace();}String status=nacosConfig.isRegistryNow()==true?" online":" offline";log.info("nacos registry, {} {}:{} register "+status+" finished", serviceId,instance.getIp(), instance.getPort());}}
注意:一定要在系统启动完成之后,将系统状态标识位置为true,否则轮训线程会一直轮训,且不会注册到Nacos-server
2、优雅停机
2.1、说明
优雅停机的目的是为了在应用停机下线时,也应该保证该应用中所有的服务请求都正确执行完毕。
2.2、使用场景
在分布式应用中,经常会根据实际需要对应用服务集群进行扩容和缩容,而进行缩容时,即准备要停机下线的应用,也应该保证该应用中所有的服务请求都正确执行完毕。
2.3、实现方案
首先要明确一个前提,在Linux系统中,关闭一个JVM进程,通常是通过kill命令,而kill命令常用的有kill -9和kill -15。kill -9和kill -15这两种命令的区别,这里就不在赘述,如果使用kill -9命令来停机,那就不用考虑什么优雅停机了,所以我们在生产环境中是禁止使用kill -9命令停机的。而使用kill -15命令时,JVM就不会马上关闭,而是先做一些准备工作,并且提供了一个钩子函数:Runtime.getRuntime().addShutdownHook(this.shutdownHook)来给用户做一些关闭前的准备工作。
Spring就是通过这个钩子函数来接收关闭容器的信号的。Spring在启动过程中,在刷新容器refreshContext时,向JVM注册了这个钩子。
当应用准备停机下线时,该应用中很有可能还有正在处理中的服务,必须要保证这些正在处理中的服务能够正常处理完成之后,应用才停机。并且,当应用准备下线停机时,就不要再从外部接收新的请求了,所以需要应用主动的从Nacos中下线,这样就不会有新的请求发进来了。
总的来说,优雅停机就是两个步骤。第一步:主动从注册中心下线。第二步:将应用中正在处理的服务执行完毕再停机即可。
Spring容器在准备关闭之前,会发布一个事件:ContextClosedEvent。我们可以通过监听这个事件来知道什么时候要关闭应用了。
第一步:主动从注册中心下线。Nacos实现了SpringCloud提供的统一服务注册的接口:org.springframework.cloud.client.serviceregistry.ServiceRegistry。我们可以通过从Spring容器中获取该ServiceRegistry的Bean,且调用其deregister方法即可实现主动下线。而Nacos在消费方还有一个本地缓存的机制,消费方从Nacos中获取到服务信息之后,会在本地保存一个缓存,并定期刷新该缓存,当我们的提供方服务主动从nacos下线之后,还需要等待一段时间,直到所有消费方都刷新本地缓存。
第二步:等待应用中正在处理的服务执行完毕。这个步骤需要考虑到具体的web容器了,SpringBoot内置了几种常用的,比如Tomcat、Jetty、Undertown。这几种不通的web容器,具体停机的实现方式不同。这里仅给出这三种常用容器的停机策略,看代码。
import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.event.ContextClosedEvent;import org.springframework.context.event.SmartApplicationListener;import org.springframework.core.Ordered;import org.apache.catalina.connector.Connector;import org.apache.catalina.startup.Tomcat;import org.springframework.beans.factory.NoSuchBeanDefinitionException;import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext;import org.springframework.boot.web.context.WebServerInitializedEvent;import org.springframework.boot.web.embedded.jetty.JettyWebServer;import org.springframework.boot.web.embedded.tomcat.TomcatWebServer;import org.springframework.boot.web.embedded.undertow.UndertowServletWebServer;import org.springframework.boot.web.server.WebServer;import org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration;import org.springframework.cloud.client.serviceregistry.Registration;import org.springframework.cloud.client.serviceregistry.ServiceRegistry;import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaAutoServiceRegistration;import org.springframework.context.ApplicationContext;import org.springframework.context.ApplicationEvent;import org.springframework.util.ReflectionUtils;import io.micrometer.core.instrument.MeterRegistry;import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;import io.micrometer.core.instrument.simple.SimpleMeterRegistry;import io.undertow.Undertow;import io.undertow.server.ConnectorStatistics;import java.lang.reflect.Field;import java.util.List;import java.util.concurrent.Executor;import java.util.concurrent.ExecutorService;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicInteger;import org.slf4j.Logger;import org.slf4j.LoggerFactory;/*** 自定义监听器控制服务注册***/public class GracefulRegistryInstanceListener implements SmartApplicationListener {private static final Logger log = LoggerFactory.getLogger(GracefulRegistryInstanceListener.class);@Overridepublic boolean supportsSourceType(Class<?> sourceType) {return true;}@Overridepublic boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {return WebServerInitializedEvent.class.isAssignableFrom(eventType)|| ContextClosedEvent.class.isAssignableFrom(eventType);}private WebServerInitializedEvent event;private final int waitTime = 30;private ApplicationContext context;private WebServer webServer;@Overridepublic void onApplicationEvent(ApplicationEvent event) {//在Springboot启动时获取到当前使用的web容器if (event instanceof WebServerInitializedEvent) {onWebServerInitializedEvent((WebServerInitializedEvent) event);} else if(event instanceof ContextClosedEvent){//监听容器关闭时发布的ContextClosedEvent事件onContextClosedEvent((ContextClosedEvent) event);}}@Overridepublic int getOrder() {return Ordered.HIGHEST_PRECEDENCE;}private void onWebServerInitializedEvent(WebServerInitializedEvent event) {ApplicationContext context = event.getApplicationContext();if (context instanceof ConfigurableWebServerApplicationContext) {this.context = context;this.event = (WebServerInitializedEvent) event;this.webServer = ((WebServerInitializedEvent) event).getWebServer();}}private void onContextClosedEvent(ContextClosedEvent event) {if(webServer == null) {return;}//第一步:服务主动下线ServiceRegistry serviceRegistry=context.getBean(ServiceRegistry.class);serviceRegistry.deregister(context.getBean(Registration.class));//休眠1min,等待消费方刷新本地缓存try{TimeUnit.SECOND.sleep(60);}cache(Exception e){}//第二步:等待容器中正在处理的服务执行完毕,再关闭容器if (webServer instanceof TomcatWebServer) {TomcatWebServer tomcatWebServer = (TomcatWebServer) webServer;Tomcat tomcat = tomcatWebServer.getTomcat();Connector connector = tomcat.getConnector();connector.pause();Executor executor = connector.getProtocolHandler().getExecutor();if (executor instanceof ThreadPoolExecutor) {try {ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;threadPoolExecutor.shutdown();if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {LOGGER.info("Tomcat thread pool did not shut down gracefully within {} seconds. Proceeding with forceful shutdown.",waitTime);}} catch (InterruptedException ex) {Thread.currentThread().interrupt();}}} else if (webServer instanceof JettyWebServer) {webServer.stop();} else if (webServer instanceof UndertowServletWebServer) {UndertowShutdownWrapper gracefulShutdownWrapper = SpringUtils.getBean(UndertowShutdownWrapper.class);gracefulShutdownWrapper.getGracefulShutdownHandler().shutdown();UndertowServletWebServer undertowWebserver = (UndertowServletWebServer) webServer;try {Field field = undertowWebserver.getClass().getDeclaredField("undertow");field.setAccessible(true);Undertow undertow = (Undertow) field.get(undertowWebserver);List<Undertow.ListenerInfo> listenerInfo = undertow.getListenerInfo();Undertow.ListenerInfo listener = listenerInfo.get(0);ConnectorStatistics connectorStatistics = listener.getConnectorStatistics();long beginTime = System.currentTimeMillis();while (connectorStatistics.getActiveConnections() > 0) {long nowTime = System.currentTimeMillis();if ((nowTime - beginTime) > 30 * 1000) {break;}Thread.sleep(50);}} catch (Exception e) {// TODO: handle exception}}}}
