Spring Boot 1.X и 2.X изящно перезапускают реальный бой

Spring Boot задняя часть Spring Безопасность

В процессе повторной публикации проекта, если некоторые запросы занимают много времени и еще не были выполнены, перезапуск в это время приведет к прерыванию запроса и повлияет на бизнес-функции.Мягкий перезапуск может гарантировать, что никакие внешние новые запросы не будут полученные, когда он остановлен.Запросы, ожидающие завершения невыполненных запросов, что обеспечивает целостность данных.

Spring Boot 1.X


import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.catalina.connector.Connector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextClosedEvent;

/**
 * Spring Boot1.X Tomcat容器优雅停机
 * @author yinjihuan
 *
 */
@Configuration
public class ShutdownConfig {
	
	/**
     * 用于接受shutdown事件
     * @return
     */
    @Bean
    public GracefulShutdown gracefulShutdown() {
        return new GracefulShutdown();
    }

    /**
     * 用于注入 connector
     * @return
     */
    @Bean
    public EmbeddedServletContainerCustomizer tomcatCustomizer() {
        return new EmbeddedServletContainerCustomizer() {
            @Override
            public void customize(ConfigurableEmbeddedServletContainer container) {
                if (container instanceof TomcatEmbeddedServletContainerFactory) {
                    ((TomcatEmbeddedServletContainerFactory) container).addConnectorCustomizers(gracefulShutdown());
                }
            }
        };
    }
    
    private static class GracefulShutdown implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {

        private static final Logger log = LoggerFactory.getLogger(GracefulShutdown.class);
        private volatile Connector connector;
        private final int waitTime = 120;

        @Override
        public void customize(Connector connector) {
            this.connector = connector;
        }

        @Override
        public void onApplicationEvent(ContextClosedEvent event) {
            this.connector.pause();
            Executor executor = this.connector.getProtocolHandler().getExecutor();
            if (executor instanceof ThreadPoolExecutor) {
                try {
                    ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
                    log.info("shutdown start");
                    threadPoolExecutor.shutdown();
                    log.info("shutdown end");
                    if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
                    	log.info("Tomcat 进程在" + waitTime + "秒内无法结束,尝试强制结束");
                    }
                    log.info("shutdown success");
                } catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
}

Spring Boot 2.X

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.catalina.connector.Connector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextClosedEvent;

/**
 * Spring Boot2.X Tomcat容器优雅停机
 * @author yinjihuan
 *
 */
@Configuration
public class ShutdownConfig {
	
	/**
     * 用于接受shutdown事件
     * @return
     */
    @Bean
    public GracefulShutdown gracefulShutdown() {
        return new GracefulShutdown();
    }

    @Bean
    public ServletWebServerFactory servletContainer() {
      TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
      tomcat.addConnectorCustomizers(gracefulShutdown());
      return tomcat;
    }
    
    private static class GracefulShutdown implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {

        private static final Logger log = LoggerFactory.getLogger(GracefulShutdown.class);
        private volatile Connector connector;
        private final int waitTime = 120;

        @Override
        public void customize(Connector connector) {
            this.connector = connector;
        }

        @Override
        public void onApplicationEvent(ContextClosedEvent event) {
            this.connector.pause();
            Executor executor = this.connector.getProtocolHandler().getExecutor();
            if (executor instanceof ThreadPoolExecutor) {
                try {
                    ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
                    log.info("shutdown start");
                    threadPoolExecutor.shutdown();
                    log.info("shutdown end");
                    if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
                    	log.info("Tomcat 进程在" + waitTime + "秒内无法结束,尝试强制结束");
                    }
                    log.info("shutdown success");
                } catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
}

Сценарий перезапуска службы:

LANG="zh_CN.UTF-8"
pid=`ps ax | grep fangjia-youfang-web | grep java | head -1 | awk '{print $1}'`
echo $pid
#kill $pid
curl -X POST http://127.0.0.1:8086/shutdown?token=认证信息
while [[ $pid != "" ]]; do
    echo '服务停止中...'
    sleep 1
    pid=`ps ax | grep fangjia-youfang-web | grep java | head -1 | awk '{print $1}'`
done
echo '服务停止成功,开始重启服务...'

java -jar xxx.jar

Сначала отправьте команду перезапуска на конечную точку перед перезапуском или используйте метод уничтожения идентификатора процесса, никогда не используйте команду kill -9.

Затем выполните цикл, чтобы проверить, существует ли процесс. Если служба останавливается нормально, процесс не существует. Если процесс все еще существует, он доказывает, что есть незавершенные запросы, останавливается на 1 секунду и продолжает обнаружение.

Что касается перезапуска службы, рекомендуется использовать метод kill, чтобы вам не приходилось полагаться на spring-boot-starter-actuator.Если вы используете метод конечной точки, вам необходимо контролировать разрешения, иначе это может быть перезапущен в любое время, вы можете использовать безопасность для управления разрешениями, I Это контролируется самим фильтром.

Если вы используете привод для перезапуска, вам необходимо настроить функцию включения перезапуска: 1.x конфигурация следующая:

endpoints.shutdown.enabled=true

В версии 2.x больше конфигураций, по умолчанию выставлено только несколько часто используемых, да и адреса доступа тоже изменились, например, health, который раньше напрямую обращался к /health, теперь требует для доступа /actuator/health. Мы можем настроить его для совместимости с предыдущим адресом доступа.

Завершение работы не отображается по умолчанию. Его можно открыть и запустить через конфигурацию. Конфигурация выглядит следующим образом:

#访问路径,配置后就和1.x版本路径一样
management.endpoints.web.base-path=/
# 暴露所有,也可以暴露单个或多个
management.endpoints.web.exposure.include=*
# 开启shutdown
management.endpoint.shutdown.enabled=true

Документацию см. по адресу: https://docs.spring.io/spring-boot/docs/2.0.2.RELEASE/reference/htmlsingle/#production-ready.

как проверить

Для тестирования мы можем написать простой интерфейс, подождать в интерфейсе, а затем выполнить скрипт для остановки проекта.Если он нормальный, он выдаст, что служба остановлена.После завершения выполнения вашего интерфейса процесс исчезнет, ​​но если оно превысит настроенное вами значение, время ожидания принудительно завершит работу.

@GetMapping("/hello")
public String hello() {
	System.out.println("req.........");
	try {
		Thread.sleep(1000 * 60 * 3);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
	return "hello";
}

Проблемы, о которых следует знать

Если вы используете в своем проекте другие пулы потоков, такие как Spring ThreadPoolTaskExecutor, незнакомые студенты могут обратиться к этой моей статье.«Асинхронное выполнение Spring Boot Async»

После отправки команды остановки, если у ThreadPoolTaskExecutor есть необработанные потоки, процесс не будет автоматически закрыт в это время. В это время нам нужно закрыть пул потоков, и добавленный код выглядит следующим образом:

AsyncTaskExecutePool asyncTaskExecutePool = event.getApplicationContext().getBean(AsyncTaskExecutePool.class);
Executor executors = asyncTaskExecutePool.getAsyncExecutor();
try {
      if (executors instanceof ThreadPoolTaskExecutor) {
           ThreadPoolTaskExecutor threadPoolExecutor = (ThreadPoolTaskExecutor) executors;
           log.info("Async shutdown start");
           threadPoolExecutor.setWaitForTasksToCompleteOnShutdown(true);
	       threadPoolExecutor.setAwaitTerminationSeconds(waitTime);
           threadPoolExecutor.shutdown();
      }
} catch (Exception ex) {
     Thread.currentThread().interrupt();
}

ThreadPoolTaskExecutor имеет только метод завершения работы и не имеет метода awaitTermination.Просмотрев исходный код, установив setWaitForTasksToCompleteOnShutdown и setAwaitTerminationSeconds перед завершением работы, вы также можете достичь awaitTermination.

Исходный код выглядит следующим образом:

public void shutdown() {
		if (logger.isInfoEnabled()) {
			logger.info("Shutting down ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : ""));
		}
		if (this.executor != null) {
			if (this.waitForTasksToCompleteOnShutdown) {
				this.executor.shutdown();
			}
			else {
				for (Runnable remainingTask : this.executor.shutdownNow()) {
					cancelRemainingTask(remainingTask);
				}
			}
			awaitTerminationIfNecessary(this.executor);
		}
	}

Когда значение waitForTasksToCompleteOnShutdown равно true, непосредственно вызывается executor.shutdown(); и, наконец, выполняется метод awaitTerminationIfNecessary.

private void awaitTerminationIfNecessary(ExecutorService executor) {
		if (this.awaitTerminationSeconds > 0) {
			try {
				if (!executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS)) {
					if (logger.isWarnEnabled()) {
						logger.warn("Timed out while waiting for executor" +
								(this.beanName != null ? " '" + this.beanName + "'" : "") + " to terminate");
					}
				}
			}
			catch (InterruptedException ex) {
				if (logger.isWarnEnabled()) {
					logger.warn("Interrupted while waiting for executor" +
							(this.beanName != null ? " '" + this.beanName + "'" : "") + " to terminate");
				}
				Thread.currentThread().interrupt();
			}
		}
	}

В awaitTerminationIfNecessary будет оценено, что атрибут awaitTerminationSeconds равен значению, и будет выполнена логика обнаружения ожидания выключения, которая аналогична коду, который мы имеем дело с выключением tomcat.

Я обнаружил, что после этого это не дало никакого эффекта, поэтому я изменил способ написания и выполнил логику выключения напрямую, получив ThreadPoolExecutor в ThreadPoolTaskExecutor:

AsyncTaskExecutePool asyncTaskExecutePool = event.getApplicationContext().getBean(AsyncTaskExecutePool.class);
Executor executors = asyncTaskExecutePool.getAsyncExecutor();
 try {
      if (executors instanceof ThreadPoolTaskExecutor) {
            ThreadPoolTaskExecutor threadPoolExecutor = (ThreadPoolTaskExecutor) executors;
            log.info("Async shutdown start");
	        threadPoolExecutor.getThreadPoolExecutor().shutdown();

	        log.info("Async shutdown end"+threadPoolExecutor.getThreadPoolExecutor().isTerminated());
	        if (!threadPoolExecutor.getThreadPoolExecutor().awaitTermination(waitTime, TimeUnit.SECONDS)) {
                log.info("Tomcat 进程在" + waitTime + "秒内无法结束,尝试强制结束");
            }
	        log.info("Async shutdown success");
       }
} catch (Exception ex) {
      Thread.currentThread().interrupt(); 
}

Этот метод бесполезен для достижения желаемого эффекта. Когда я запускаю команду kill, я выхожу напрямую. На самом деле у меня есть фоновый поток, работающий в ThreadPoolTaskExecutor. Из журнала вывода я вижу, что до тех пор, пока выключение и isTerminated методы называются The return is true, а это значит, что он был закрыт.Причина этого не найдена.Если вы исследовали его, пожалуйста, поделитесь им.

Для большего обмена технологиями, пожалуйста, обратите внимание на общедоступную учетную запись WeChat: Yuantiandi

image.png