Серия Spring Boot (3): подробное объяснение последней версии элегантного завершения работы.

Spring Boot

Любите жизнь, любите кодирование, эта статья включенаКолонка архитектурных технологийПодпишитесь на лайки, чтобы поделиться.

Проект с открытым исходным кодом:

**Весенняя коллекция ботинок**

Spring Boot Series (1): Начало работы с SpringApplication

Серия Spring Boot (2) Анализ особенностей конфигурации

Серия Spring Boot (3): подробное объяснение последней версии элегантного завершения работы.

Spring Boot Series (4): подробная динамическая конфигурация журнала

изящное завершение работы

В настоящее время Spring Boot разработан до 2.3.4.RELEASE, С появлением версии 2.3 механизм корректного завершения работы также стал более совершенным.

Текущая версия корректного завершения работы Spring Boot поддерживает Jetty, Reactor Netty, Tomcat и Undertow, а также реактивные веб-приложения и веб-приложения на основе сервлетов.

Цель изящного выключения:

Если корректного завершения работы нет, а сервер в это время выключается напрямую (kill -9), то бизнес, работающий в данный момент в контейнере, потерпит неудачу напрямую, и в некоторых особых сценариях будут сгенерированы грязные данные.

После добавления конфигурации корректного завершения работы:

При выключении сервера (kill -2) будет зарезервировано немного времени для выполнения внутренних бизнес-потоков контейнера, и контейнер не будет пропускать в это время новые запросы. Метод обработки нового запроса связан с веб-сервером. Reactor Netty и Tomcat прекратят доступ к запросу, а метод обработки Undertow должен вернуть 503.

новая конфигурация

YAML-конфигурация

Новая конфигурация версии очень проста, Server.shutdown = изящная доставка (обратите внимание, что элегантное завершение работы необходимо сотрудничать с Tomcat 9.0.33 (включительно) или выше)

server:
  port: 6080
  shutdown: graceful #开启优雅停机
spring:
  lifecycle:
    timeout-per-shutdown-phase: 20s #设置缓冲时间 默认30s

После установки параметра буфера timeout-per-shutdown-phase, если поток не может быть выполнен в течение указанного времени, он будет принудительно остановлен.

Давайте посмотрим на разницу между добавлением изящных журналов остановки и их недобавлением при выключении:


//未加优雅停机配置
Disconnected from the target VM, address: '127.0.0.1:49754', transport: 'socket'
Process finished with exit code 130 (interrupted by signal 2: SIGINT)

После добавления конфигурации изящного выключения журналы, которые можно найти, четкоWaiting for active requests to cpmplete, контейнер остановится после выполнения ShutdownHook.

Закрыть метод

1. Не используйте kill -9, используйте kill -2, чтобы закрыть контейнер. Это вызовет внутреннюю операцию java ShutdownHook, kill -9 не вызовет ShutdownHook.

2. Вы можете использовать конечные точки для мониторинга запросов POST./actuator/shutdownвыполнить изящное завершение работы.

Добавить ShutdownHook

Через приведенный выше лог мы обнаружили, что Druid выполнил свой собственный ShutdownHook, поэтому давайте также добавим ShutdownHook, есть несколько простых способов:

1. Реализуйте интерфейс DisposableBean и реализуйте метод уничтожения.

@Slf4j
@Service
public class DefaultDataStore implements DisposableBean {


    private final ExecutorService executorService = new ThreadPoolExecutor(OSUtil.getAvailableProcessors(), OSUtil.getAvailableProcessors() + 1, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(200), new DefaultThreadFactory("UploadVideo"));


    @Override
    public void destroy() throws Exception {
        log.info("准备优雅停止应用使用 DisposableBean");
        executorService.shutdown();
    }
}

2. Используйте аннотацию @PreDestroy

@Slf4j
@Service
public class DefaultDataStore {


    private final ExecutorService executorService = new ThreadPoolExecutor(OSUtil.getAvailableProcessors(), OSUtil.getAvailableProcessors() + 1, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(200), new DefaultThreadFactory("UploadVideo"));


    @PreDestroy
    public void shutdown() {
        log.info("准备优雅停止应用 @PreDestroy");
        executorService.shutdown();
    }

}

Обратите внимание, что @PreDestroy выполняется до DisposableBean.

принцип закрытия

1. Для закрытия используйте kill pid, исходный код очень простой, можете посмотреть на GracefulShutdown

	private void doShutdown(GracefulShutdownCallback callback) {
		List<Connector> connectors = getConnectors();
		connectors.forEach(this::close);
		try {
			for (Container host : this.tomcat.getEngine().findChildren()) {
				for (Container context : host.findChildren()) {
					while (isActive(context)) {
						if (this.aborted) {
							logger.info("Graceful shutdown aborted with one or more requests still active");
							callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
							return;
						}
						Thread.sleep(50);
					}
				}
			}

		}
		catch (InterruptedException ex) {
			Thread.currentThread().interrupt();
		}
		logger.info("Graceful shutdown complete");
		callback.shutdownComplete(GracefulShutdownResult.IDLE);
	}

2. Используйте конечные точки для мониторинга запросов POST/actuator/shutdownзакрытие

Поскольку все приводы используют расширение SPI, мы смотрим на AutoConfiguration и видим, что ключевым моментом является ShutdownEndpoint.

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnAvailableEndpoint(
    endpoint = ShutdownEndpoint.class
)
public class ShutdownEndpointAutoConfiguration {
    public ShutdownEndpointAutoConfiguration() {
    }

    @Bean(
        destroyMethod = ""
    )
    @ConditionalOnMissingBean
    public ShutdownEndpoint shutdownEndpoint() {
        return new ShutdownEndpoint();
    }
}

ShutdownEndpoint, в целях экономии места, только немного важно

@Endpoint(
    id = "shutdown",
    enableByDefault = false
)
public class ShutdownEndpoint implements ApplicationContextAware {
     
    @WriteOperation
    public Map<String, String> shutdown() {
        if (this.context == null) {
            return NO_CONTEXT_MESSAGE;
        } else {
            boolean var6 = false;

            Map var1;
            try {
                var6 = true;
                var1 = SHUTDOWN_MESSAGE;
                var6 = false;
            } finally {
                if (var6) {
                    Thread thread = new Thread(this::performShutdown);
                    thread.setContextClassLoader(this.getClass().getClassLoader());
                    thread.start();
                }
            }

            Thread thread = new Thread(this::performShutdown);
            thread.setContextClassLoader(this.getClass().getClassLoader());
            thread.start();
            return var1;
        }
    }
  
      private void performShutdown() {
        try {
            Thread.sleep(500L);
        } catch (InterruptedException var2) {
            Thread.currentThread().interrupt();
        }

        this.context.close();  //这里才是核心
    }
}

Когда вызывается this.context.close(), это на самом деле метод close() класса AbstractApplicationContext (фокус находится на doClose())

/**
	 * Close this application context, destroying all beans in its bean factory.
	 * <p>Delegates to {@code doClose()} for the actual closing procedure.
	 * Also removes a JVM shutdown hook, if registered, as it's not needed anymore.
	 * @see #doClose()
	 * @see #registerShutdownHook()
	 */
	@Override
	public void close() {
		synchronized (this.startupShutdownMonitor) {
			doClose(); //重点:销毁bean 并执行jvm shutdown hook
			// If we registered a JVM shutdown hook, we don't need it anymore now:
			// We've already explicitly closed the context.
			if (this.shutdownHook != null) {
				try {
					Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
				}
				catch (IllegalStateException ex) {
					// ignore - VM is already shutting down
				}
			}
		}
	}

постскриптум

здесь, о单机Изящное завершение работы версии Spring Boot выполнено. зачем говорить单机? Потому что каждый также может обнаружить, что при закрытии это только для того, чтобы обеспечить выполнение внутреннего потока сервера, а состояние вызывающего объекта не касается.

Будь то Dubbo или облачная структура распределенных услуг, необходимо обратить внимание на то, как отменить регистрацию поставщика в центре регистрации до того, как служба будет остановлена, а затем остановить поставщика услуг, чтобы гарантировать, что бизнес-система не будет генерировать различные 503, тайм-аут и другие явления.

К счастью, в этом нам помог текущий Spring Boot в сочетании с Kubernetes, то есть новые функции версии Spring Boot 2.3 Liveness (состояние выживания) и Readiness (состояние готовности)

Просто упомяните эти два состояния:

  • Liveness: Статус Liveness для просмотра внутренней ситуации можно понимать как Проверка работоспособности. Если Liveness дает сбой, это означает, что приложение находится в состоянии сбоя и в настоящее время не может восстановиться, это перезапускается. В настоящее время Kubernetes убьет Container, если обнаружение выживания не удастся.

  • Готовность (readiness): Используется, чтобы сообщить, готово ли приложение принимать запросы клиентов.Если Готовность не готова, k8s не может маршрутизировать трафик.