springcloud-优雅停机

一、前言

 

先了解下eureka发现/注册流程,跳转链接

client端:1、服务启动后registry到注册中心(可配置不注册上去)

2、每30秒,往注册中心丢心跳包,进行续约

3、每60秒,从注册中心拉取服务信息, 拉取到的信息,缓存到本地

二、背景

噱头:在编程开发的节奏上,敏捷开发已经成为了一种主流;

问题1:那么问题来了,服务需要发布上线,线上的流量又还在请求进来?

目前springcloud服务发现及下线都是有缓存的,每个服务都会保存一份其他服务请求地址在本地缓存中。

那么线上的流量又在请求服务且服务又在发布,所以客户会操作不了功能。而且重启服务或者发布服务会带来并发症状。

问题2:比如:其他服务流量暴增、服务可用性降低。

springcloud主打的就是cap中的ap;

三、服务下线过程

直接杀进程 kill -9

0s,kill掉服务,未通知eureka-service通知下线。

29s,检查服务在是否续约,是否超过90%续约失败,未超过,则不会剔除。注意:超过这里也不会被剔除 (剔除的前提,是60s内,超过90%未续约,才会被剔除节点)。发出ping指令。

60s,继续检查是否续约,不会剔除

90s,检查是否已经续约,超过了90s且超过90%未续约,剔除该节点。

其他服务拉去eureka-service的注册信息是60s更新一次。

而剔除下线需要90s;所以其他感知到下线需要 60s+90 s?在这个期间系统功能可能就是不可用了。当然服务也可能是做了集群,所以有时好,有时就可能不行了。

四、针对问题,解决问题

使用kill -15 进程id

JVM进程在接收到kill -15信号通知的时候,会做一些清理动作的。同时,也提供了hook机制,来让开发者自定义清理动作,对应的方法为:Java.Runtime.getRunTime().addShutdownHook(Thread hook)。

OK..

来看下springboot启动,是怎么提供了一个入口?

上代码。

// 第一步方法
public static void main(String[] args) {
	SpringApplication.run(XXApplication.class, args);
}
// 第二部方法
/**
	 * Static helper that can be used to run a {@link SpringApplication} from the
	 * specified sources using default settings and user supplied arguments.
	 * @param primarySources the primary sources to load
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return the running {@link ApplicationContext}
	 */
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
	return new SpringApplication(primarySources).run(args);
}
// 第三步方法
/**
	 * Run the Spring application, creating and refreshing a new
	 * {@link ApplicationContext}.
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return a running {@link ApplicationContext}
	 */
public ConfigurableApplicationContext run(String... args) {
	....
	try {
		...		
		prepareContext(context, environment, listeners, applicationArguments, printedBanner);
		// 在这里
		refreshContext(context);
		afterRefresh(context, applicationArguments);
		stopWatch.stop();
		if (this.logStartupInfo) {
			new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
		}
		listeners.started(context);
		callRunners(context, applicationArguments);
	}
	catch (Throwable ex) {
		handleRunFailure(context, ex, exceptionReporters, listeners);
		throw new IllegalStateException(ex);
	}
		....
	return context;
}

// 第四个方法
// 这里是扩展自定义xx
private void refreshContext(ConfigurableApplicationContext context) {
refresh(context);
// 默认为true
if (this.registerShutdownHook) {
	try {
		// 在这里注册了一个SpringApplicationShutdownHook,进去看看
		context.registerShutdownHook();
	}
	catch (AccessControlException ex) {
		// Not allowed in some environments.
	}
}
}

// 第五步
/**
	 * Register a shutdown hook {@linkplain Thread#getName() named}
	 * {@code SpringContextShutdownHook} with the JVM runtime, closing this
	 * context on JVM shutdown unless it has already been closed at that time.
	 * <p>Delegates to {@code doClose()} for the actual closing procedure.
	 * @see Runtime#addShutdownHook
	 * @see ConfigurableApplicationContext#SHUTDOWN_HOOK_THREAD_NAME
	 * @see #close()
	 * @see #doClose()
	 */
// 看,注册了一个关闭线程的钩子。好,我们看看doClose都干了什么。
@Override
public void registerShutdownHook() {
	if (this.shutdownHook == null) {
		// No shutdown hook registered yet.
		this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) {
			@Override
			public void run() {
				synchronized (startupShutdownMonitor) {
					doClose();
				}
			}
		};
		Runtime.getRuntime().addShutdownHook(this.shutdownHook);
	}
}


/**
	 * Actually performs context closing: publishes a ContextClosedEvent and
	 * destroys the singletons in the bean factory of this application context.
	 * <p>Called by both {@code close()} and a JVM shutdown hook, if any.
	 * @see org.springframework.context.event.ContextClosedEvent
	 * @see #destroyBeans()
	 * @see #close()
	 * @see #registerShutdownHook()
	 */
protected void doClose() {
	// Check whether an actual close attempt is necessary...
	if (this.active.get() && this.closed.compareAndSet(false, true)) {
		...
		try {
			// 看这里,有个close的事件,这个事件可以自行定义的
			publishEvent(new ContextClosedEvent(this));
		}
		catch (Throwable ex) {
			logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
		}
		// 以下只是清空spring容器里面的东西。
		// Stop all Lifecycle beans, to avoid delays during individual destruction.
		if (this.lifecycleProcessor != null) {
			try {
				this.lifecycleProcessor.onClose();
			}
			catch (Throwable ex) {
				logger.warn("Exception thrown from LifecycleProcessor on context close", ex);
			}
		}

		// Destroy all cached singletons in the context's BeanFactory.
		destroyBeans();
		// Close the state of this context itself.
		closeBeanFactory();
		// Let subclasses do some final clean-up if they wish...
		onClose();
		// Reset local application listeners to pre-refresh state.
		if (this.earlyApplicationListeners != null) {
			this.applicationListeners.clear();
			this.applicationListeners.addAll(this.earlyApplicationListeners);
		}
		// Switch to inactive.
		this.active.set(false);
	}
}

如上是spring的入口;

那么来自定义我们自行实现这个事件监听

五、自定义ContextClosedEvent监听器

/**
 * @Description : kill -15; SpringApplicationShutdownHook钩子的监听器
 * @author: 
 * @date : 2023/6/8
 */

@Slf4j
@Configuration
public class GracefulShutDownHookListener implements ApplicationListener<ContextClosedEvent>, TomcatConnectorCustomizer {

    /**
     * 连接器
     */
    private volatile Connector connector;

    /**
     * 线程休眠时间
     */
    private final static Integer THREAD_SLEEP_TIME = 150000;


    /**
     * 注意,这里是可以拿到上下文ApplicationContext的
     * @param contextClosedEvent 上下文关闭事件监听
     * @return: void
     */
    @Override
    public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
        // 官方虽然不建议使用,但是内部的ribbon,也还在使用,内部管理eureka-client还是在使用这个
        DiscoveryManager discoveryManager = DiscoveryManager.getInstance();
        // 获取eureka-client
        EurekaClient eurekaClient = discoveryManager.getEurekaClient();
        if(null == eurekaClient) {
            threadShutdown();
            return;
        }
        try {
            long start = System.currentTimeMillis();
            log.info("服务下线开始:{}", start);
            eurekaClient.shutdown();
            log.info("服务下线结束:{}", System.currentTimeMillis() - start);
        } catch (Exception e) {
            log.error("服务下线失败:{},异常:{}", contextClosedEvent, e);
        }

        try {
            Thread.sleep(THREAD_SLEEP_TIME);
        } catch (Exception e) {
            log.error("取消eureka-client下下线休眠失败:{}", e);
        }
        threadShutdown();
    }

    /**
     * 因为我们服务是通过kill -15 ,所以我们内部可能还是有线程在处理
     * 如果线程处理时间过慢了,我们这里需要手动进行阻断线程的.
     */
    private void threadShutdown() {
        // 阻断线程进来了
        this.connector.pause();
        // 获取连接器里面的线程
        Executor executor = this.connector.getProtocolHandler().getExecutor();
        if(executor instanceof ThreadPoolExecutor) {
            ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
            threadPoolExecutor.shutdown();
            try {
                // 线程是否30秒终止了
                if(!threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
                    // 目前还没有说明需要做变更更
                }
            } catch (Exception e) {
                // 线程阻塞下
                Thread.currentThread().interrupt();
                log.error("");
            }
        }
    }


    @Bean
    public ConfigurableServletWebServerFactory servletWebServerFactory(final GracefulShutDownHookListener gracefulShutDownHookListener) {
        TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory();
        tomcatServletWebServerFactory.addConnectorCustomizers(gracefulShutDownHookListener);
        return tomcatServletWebServerFactory;
    }

    @Override
    public void customize(Connector connector) {
        this.connector = connector;
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值