Come chiudere un'applicazione Spring Boot in modo corretto?


120

Nel documento Spring Boot, hanno affermato che "Ogni SpringApplication registrerà un hook di arresto con la JVM per garantire che ApplicationContext venga chiuso correttamente all'uscita".

Quando faccio clic ctrl+csul comando della shell, l'applicazione può essere chiusa con garbo. Se eseguo l'applicazione su una macchina di produzione, devo usare il comando java -jar ProApplicaton.jar. Ma non posso chiudere il terminale della shell, altrimenti chiuderà il processo.

Se eseguo un comando simile nohup java -jar ProApplicaton.jar &, non posso usarlo ctrl+cper spegnerlo con grazia.

Qual è il modo corretto per avviare e arrestare un'applicazione Spring Boot nell'ambiente di produzione?


A seconda della configurazione, il PID dell'applicazione viene scritto su file. Puoi inviare un segnale kill a quel PID. Vedi anche i commenti in questo numero .
M. Deinum

Quale segnale dovrei usare, non credo che kill -9 sia una buona idea, giusto?
Chris

5
Ecco perché ti ho indicato quel filo ... Ma qualcosa del genere kill -SIGTERM <PID>dovrebbe funzionare.
M. Deinum

kill with pid, no -9
Dapeng

1
kill $ (lsof -ti tcp: <port>) - nel caso in cui non vuoi usare l'attuatore e hai bisogno di un'uccisione rapida
Alex Nolasco

Risposte:


62

Se si utilizza il modulo attuatore, è possibile chiudere l'applicazione tramite JMXo HTTPse l'endpoint è abilitato.

aggiungi a application.properties:

endpoints.shutdown.enabled = true

Sarà disponibile il seguente URL:

/actuator/shutdown - Consente all'applicazione di essere chiuso normalmente (non abilitato per impostazione predefinita).

A seconda di come viene esposto un endpoint, il parametro sensitive può essere utilizzato come suggerimento per la sicurezza.

Ad esempio, gli endpoint sensibili richiederanno un nome utente / password quando vi si accede HTTP(o semplicemente disabilitati se la sicurezza web non è abilitata).

Dalla documentazione di Spring Boot


1
Non voglio includere il modulo attuatore. Ma ho scoperto che, nel registro della mia console, Spring Boot stampa il PID nella prima riga. C'è un modo per consentire a Spring Boot di stampare il PID in un altro file senza aggiungere il modulo attuatore (ApplicationPidListener)?
Chris,

2
Nota che questo deve essere un post http per questo endpoint. Puoi abilitarlo con endpoints.shutdown.enabled = true
sparkyspider

54

Ecco un'altra opzione che non richiede di modificare il codice o di esporre un endpoint di arresto. Crea i seguenti script e usali per avviare e arrestare la tua app.

start.sh

#!/bin/bash
java -jar myapp.jar & echo $! > ./pid.file &

Avvia la tua app e salva l'ID del processo in un file

stop.sh

#!/bin/bash
kill $(cat ./pid.file)

Arresta l'app utilizzando l'ID processo salvato

start_silent.sh

#!/bin/bash
nohup ./start.sh > foo.out 2> foo.err < /dev/null &

Se è necessario avviare l'app utilizzando ssh da un computer remoto o da una pipeline CI, utilizzare invece questo script per avviare l'app. L'uso diretto di start.sh può lasciare la shell in sospeso.

Dopo ad es. re / distribuendo la tua app puoi riavviarla usando:

sshpass -p password ssh -oStrictHostKeyChecking=no userName@www.domain.com 'cd /home/user/pathToApp; ./stop.sh; ./start_silent.sh'

Questa dovrebbe essere la risposta. Ho appena confermato che un segnale 15 di spegnimento dice a Spring di spegnersi con grazia.
Ciad

1
Perché non chiamate java - jar execution con nohup all'interno di start.sh, invece di chiamare java - jar execution all'interno di start.sh che viene chiamato con nohup all'interno di un altro script di shell esterno ??
Anand Varkey Philips

2
@AnandVarkeyPhilips L'unico motivo è che a volte chiamo start.sh dalla riga di comando a scopo di test, ma se ne hai sempre bisogno nohuppuoi semplicemente unire i comandi
Jens

@ Jens, grazie per le informazioni. Puoi dirmi che fai questo: foo.out 2> foo.err </ dev / null &
Anand Varkey Philips

1
@jens, grazie l'ho fatto con successo e ho pubblicato il mio script di avvio e arresto qui sotto. ( stackoverflow.com/questions/26547532/… )
Anand Varkey Philips

52

Per quanto riguarda la risposta di @ Jean-Philippe Bond,

ecco un rapido esempio di maven per l'utente esperto per configurare l'endpoint HTTP per chiudere un'app Web di avvio primaverile utilizzando spring-boot-starter-Actuator in modo da poter copiare e incollare:

1.Maven pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2.application.properties:

#No auth  protected 
endpoints.shutdown.sensitive=false

#Enable shutdown endpoint
endpoints.shutdown.enabled=true

Tutti gli endpoint sono elencati qui :

3.Invia un metodo di post per chiudere l'app:

curl -X POST localhost:port/shutdown

Nota sulla sicurezza:

se hai bisogno del metodo di spegnimento protetto da autenticazione, potresti aver bisogno anche di

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

configurare i dettagli :


Dopo il secondo passaggio, durante il tentativo di distribuzione, si è verificato questo messaggio di errore: Descrizione: il parametro 0 del metodo setAuthenticationConfiguration in org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter richiedeva un bean di tipo 'org.springframework.security.config .annotation.authentication.configuration.AuthenticationConfiguration "che non è stato trovato. Azione: prendere in considerazione la definizione di un bean di tipo "org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration" nella configurazione.
Roberto

2
Nota: se ho incluso qualcosa di simile server.contextPath=/appNamenel mio application.properties Quindi ora il comando per lo spegnimento diventerebbe: curl -X POST localhost:8080/appName/shutdown Spero che possa aiutare qualcuno. Ho dovuto lottare molto a causa di questo errore.
Naveen Kumar

Con Spring Boot 1.5.8 si suggerisce, se senza sicurezza, di avere in application.properties endpoints.shutdown.enabled=true management.security.enabled=false.
Stefano Scarpanti

Se non vuoi esporre il punto finale e utilizzare il file PID per interrompere e avviare tramite script di shell, prova questo: stackoverflow.com/questions/26547532/…
Anand Varkey Philips

27

Puoi fare in modo che l'applicazione springboot scriva il PID nel file e puoi usare il file pid per arrestare o riavviare o ottenere lo stato usando uno script bash. Per scrivere il PID in un file, registra un listener in SpringApplication utilizzando ApplicationPidFileWriter come mostrato di seguito:

SpringApplication application = new SpringApplication(Application.class);
application.addListeners(new ApplicationPidFileWriter("./bin/app.pid"));
application.run();

Quindi scrivi uno script bash per eseguire l'applicazione di avvio primaverile. Riferimento .

Ora puoi utilizzare lo script per avviare, arrestare o riavviare.


Lo script di avvio e arresto completo di Generic e Jenkins compatibile può essere trovato qui ( stackoverflow.com/questions/26547532/… )
Anand Varkey Philips,


14

Sembra che in tutte le risposte manchi il fatto che potrebbe essere necessario completare una parte del lavoro in modo coordinato durante l'arresto regolare (ad esempio, in un'applicazione aziendale).

@PreDestroyconsente di eseguire il codice di arresto nei singoli bean. Qualcosa di più sofisticato sarebbe simile a questo:

@Component
public class ApplicationShutdown implements ApplicationListener<ContextClosedEvent> {
     @Autowired ... //various components and services

     @Override
     public void onApplicationEvent(ContextClosedEvent event) {
         service1.changeHeartBeatMessage(); // allows loadbalancers & clusters to prepare for the impending shutdown
         service2.deregisterQueueListeners();
         service3.finishProcessingTasksAtHand();
         service2.reportFailedTasks();
         service4.gracefullyShutdownNativeSystemProcessesThatMayHaveBeenLaunched(); 
         service1.eventLogGracefulShutdownComplete();
     }
}

Questo è proprio quello di cui avevo bisogno. Dopo aver eseguito l'applicazione, premere ctrl-c Grazie a @Michal
Claudio Moscoso

8

Non espongo alcun endpoint e inizio ( con nohup in background e senza file out creati tramite nohup ) e mi fermo con lo script della shell (con KILL PID con grazia e forza l'uccisione se l'app è ancora in esecuzione dopo 3 minuti ). Creo solo un jar eseguibile e uso il writer di file PID per scrivere il file PID e memorizzare Jar e Pid nella cartella con lo stesso nome del nome dell'applicazione e gli script della shell hanno anche lo stesso nome con inizio e fine alla fine. Li chiamo anche stop script e start script tramite jenkins pipeline. Nessun problema finora. Funziona perfettamente per 8 applicazioni (script molto generici e facili da applicare per qualsiasi app).

Classe principale

@SpringBootApplication
public class MyApplication {

    public static final void main(String[] args) {
        SpringApplicationBuilder app = new SpringApplicationBuilder(MyApplication.class);
        app.build().addListeners(new ApplicationPidFileWriter());
        app.run();
    }
}

FILE YML

spring.pid.fail-on-write-error: true
spring.pid.file: /server-path-with-folder-as-app-name-for-ID/appName/appName.pid

Ecco lo script di avvio (start-appname.sh):

#Active Profile(YAML)
ACTIVE_PROFILE="preprod"
# JVM Parameters and Spring boot initialization parameters
JVM_PARAM="-Xms512m -Xmx1024m -Dspring.profiles.active=${ACTIVE_PROFILE} -Dcom.webmethods.jms.clientIDSharing=true"
# Base Folder Path like "/folder/packages"
CURRENT_DIR=$(readlink -f "$0")
BASE_PACKAGE="${CURRENT_DIR%/bin/*}"
# Shell Script file name after removing path like "start-yaml-validator.sh"
SHELL_SCRIPT_FILE_NAME=$(basename -- "$0")
# Shell Script file name after removing extension like "start-yaml-validator"
SHELL_SCRIPT_FILE_NAME_WITHOUT_EXT="${SHELL_SCRIPT_FILE_NAME%.sh}"
# App name after removing start/stop strings like "yaml-validator"
APP_NAME=${SHELL_SCRIPT_FILE_NAME_WITHOUT_EXT#start-}

PIDS=`ps aux |grep [j]ava.*-Dspring.profiles.active=$ACTIVE_PROFILE.*$APP_NAME.*jar | awk {'print $2'}`
if [ -z "$PIDS" ]; then
  echo "No instances of $APP_NAME with profile:$ACTIVE_PROFILE is running..." 1>&2
else
  for PROCESS_ID in $PIDS; do
        echo "Please stop the process($PROCESS_ID) using the shell script: stop-$APP_NAME.sh"
  done
  exit 1
fi

# Preparing the java home path for execution
JAVA_EXEC='/usr/bin/java'
# Java Executable - Jar Path Obtained from latest file in directory
JAVA_APP=$(ls -t $BASE_PACKAGE/apps/$APP_NAME/$APP_NAME*.jar | head -n1)
# To execute the application.
FINAL_EXEC="$JAVA_EXEC $JVM_PARAM -jar $JAVA_APP"
# Making executable command using tilde symbol and running completely detached from terminal
`nohup $FINAL_EXEC  </dev/null >/dev/null 2>&1 &`
echo "$APP_NAME start script is  completed."

Ecco lo script di arresto (stop-appname.sh):

#Active Profile(YAML)
ACTIVE_PROFILE="preprod"
#Base Folder Path like "/folder/packages"
CURRENT_DIR=$(readlink -f "$0")
BASE_PACKAGE="${CURRENT_DIR%/bin/*}"
# Shell Script file name after removing path like "start-yaml-validator.sh"
SHELL_SCRIPT_FILE_NAME=$(basename -- "$0")
# Shell Script file name after removing extension like "start-yaml-validator"
SHELL_SCRIPT_FILE_NAME_WITHOUT_EXT="${SHELL_SCRIPT_FILE_NAME%.*}"
# App name after removing start/stop strings like "yaml-validator"
APP_NAME=${SHELL_SCRIPT_FILE_NAME_WITHOUT_EXT:5}

# Script to stop the application
PID_PATH="$BASE_PACKAGE/config/$APP_NAME/$APP_NAME.pid"

if [ ! -f "$PID_PATH" ]; then
   echo "Process Id FilePath($PID_PATH) Not found"
else
    PROCESS_ID=`cat $PID_PATH`
    if [ ! -e /proc/$PROCESS_ID -a /proc/$PROCESS_ID/exe ]; then
        echo "$APP_NAME was not running with PROCESS_ID:$PROCESS_ID.";
    else
        kill $PROCESS_ID;
        echo "Gracefully stopping $APP_NAME with PROCESS_ID:$PROCESS_ID..."
        sleep 5s
    fi
fi
PIDS=`/bin/ps aux |/bin/grep [j]ava.*-Dspring.profiles.active=$ACTIVE_PROFILE.*$APP_NAME.*jar | /bin/awk {'print $2'}`
if [ -z "$PIDS" ]; then
  echo "All instances of $APP_NAME with profile:$ACTIVE_PROFILE has has been successfully stopped now..." 1>&2
else
  for PROCESS_ID in $PIDS; do
    counter=1
    until [ $counter -gt 150 ]
        do
            if ps -p $PROCESS_ID > /dev/null; then
                echo "Waiting for the process($PROCESS_ID) to finish on it's own for $(( 300 - $(( $counter*5)) ))seconds..."
                sleep 2s
                ((counter++))
            else
                echo "$APP_NAME with PROCESS_ID:$PROCESS_ID is stopped now.."
                exit 0;
            fi
    done
    echo "Forcefully Killing $APP_NAME with PROCESS_ID:$PROCESS_ID."
    kill -9 $PROCESS_ID
  done
fi

7

Spring Boot ha fornito diversi listener di applicazioni mentre cerca di creare il contesto dell'applicazione, uno di questi è ApplicationFailedEvent. Possiamo usare per sapere se il contesto dell'applicazione è stato inizializzato o meno.

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.boot.context.event.ApplicationFailedEvent; 
    import org.springframework.context.ApplicationListener;

    public class ApplicationErrorListener implements 
                    ApplicationListener<ApplicationFailedEvent> {

        private static final Logger LOGGER = 
        LoggerFactory.getLogger(ApplicationErrorListener.class);

        @Override
        public void onApplicationEvent(ApplicationFailedEvent event) {
           if (event.getException() != null) {
                LOGGER.info("!!!!!!Looks like something not working as 
                                expected so stoping application.!!!!!!");
                         event.getApplicationContext().close();
                  System.exit(-1);
           } 
        }
    }

Aggiungi alla classe listener sopra a SpringApplication.

    new SpringApplicationBuilder(Application.class)
            .listeners(new ApplicationErrorListener())
            .run(args);  

La migliore risposta che ho trovato! Grazie!
Vagif

[@ user3137438], In che modo è diverso dal loggarsi all'interno dell'annotazione pre-distruzione?
Anand Varkey Philips

6

A partire da Spring Boot 2.3 e versioni successive, è disponibile un grazioso meccanismo di spegnimento incorporato .

Prima di Spring Boot 2.3 , non esiste un grazioso meccanismo di spegnimento immediato. Alcuni avviatori con avvio a molla forniscono questa funzionalità:

  1. https://github.com/jihor/hiatus-spring-boot
  2. https://github.com/gesellix/graceful-shutdown-spring-boot
  3. https://github.com/corentin59/spring-boot-graceful-shutdown

Sono l'autore di nr. 1. Lo starter si chiama "Hiatus for Spring Boot". Funziona a livello di bilanciamento del carico, ovvero contrassegna semplicemente il servizio come OUT_OF_SERVICE, non interferendo in alcun modo con il contesto dell'applicazione. Ciò consente di effettuare un grazioso spegnimento e significa che, se necessario, il servizio può essere messo fuori servizio per qualche tempo e poi riportato in vita. Lo svantaggio è che non ferma la JVM, dovrai farlo con il killcomando. Dato che gestisco tutto in container, questo non è stato un grosso problema per me, perché dovrò comunque fermare e rimuovere il container.

I numeri 2 e 3 sono più o meno basati su questo post di Andy Wilkinson. Funzionano a senso unico: una volta attivati, alla fine chiudono il contesto.


5

SpringApplication registra implicitamente un hook di arresto con la JVM per garantire che ApplicationContext venga chiuso correttamente all'uscita. Questo chiamerà anche tutti i metodi bean annotati con @PreDestroy. Ciò significa che non dobbiamo usare esplicitamente il registerShutdownHook()metodo di a ConfigurableApplicationContextin un'applicazione di avvio, come dobbiamo fare nell'applicazione principale di primavera.

@SpringBootConfiguration
public class ExampleMain {
    @Bean
    MyBean myBean() {
        return new MyBean();
    }

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(ExampleMain.class, args);
        MyBean myBean = context.getBean(MyBean.class);
        myBean.doSomething();

        //no need to call context.registerShutdownHook();
    }

    private static class MyBean {

        @PostConstruct
        public void init() {
            System.out.println("init");
        }

        public void doSomething() {
            System.out.println("in doSomething()");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("destroy");
        }
    }
}

In alternativa a @PostConstructe @PreDestroyho usato gli attributi initMethode destroyMethodall'interno @Beandell'annotazione. Così, per questo esempio: @Bean(initMethod="init", destroyMethod="destroy").
acker9

Un problema che @PreDestroypochi sviluppatori potrebbero non sapere è che tali metodi vengono chiamati solo per i bean con ambito Singleton. Gli sviluppatori devono gestire la parte di pulizia del ciclo di vita del bean per altri ambiti
asgs

2

Esistono molti modi per arrestare un'applicazione di primavera. Uno è chiamare close () su ApplicationContext:

ApplicationContext ctx =
    SpringApplication.run(HelloWorldApplication.class, args);
// ...
ctx.close()

La tua domanda suggerisce di voler chiudere la tua applicazione facendo Ctrl+C, che è spesso usato per terminare un comando. In questo caso...

L'uso endpoints.shutdown.enabled=truenon è la ricetta migliore. Significa che esponi un endpoint per terminare l'applicazione. Quindi, a seconda del tuo caso d'uso e del tuo ambiente, dovrai proteggerlo ...

Ctrl+Cdovrebbe funzionare molto bene nel tuo caso. Presumo che il tuo problema sia causato dalla e commerciale (&) Altre spiegazioni:

Un contesto dell'applicazione Spring potrebbe aver registrato un hook di chiusura con il runtime JVM. Vedere la documentazione di ApplicationContext .

Non so se Spring Boot configuri questo hook automaticamente come hai detto. Suppongo che lo sia.

Su Ctrl+C, la tua shell invia un INTsegnale all'applicazione in primo piano. Significa "interrompi la tua esecuzione". L'applicazione può intercettare questo segnale ed eseguire la pulizia prima della sua terminazione (l'hook registrato da Spring), o semplicemente ignorarlo (cattivo).

nohupè il comando che esegue il seguente programma con una trap per ignorare il segnale HUP. HUP viene utilizzato per terminare il programma quando si riaggancia (ad esempio, chiudere la connessione SSH). Inoltre reindirizza gli output per evitare che il programma si blocchi su una TTY scomparsa. nohupNON ignora il segnale INT. Quindi NON impedisce Ctrl+Cdi funzionare.

Presumo che il tuo problema sia causato dalla e commerciale (&), non da nohup. Ctrl+Cinvia un segnale ai processi in primo piano. La e commerciale fa sì che l'applicazione venga eseguita in background. Una soluzione: fai

kill -INT pid

Utilizzare kill -9o kill -KILLè cattivo perché l'applicazione (qui la JVM) non può intercettarlo per terminare correttamente.

Un'altra soluzione è riportare la tua applicazione in primo piano. Allora Ctrl+Cfunzionerà. Dai un'occhiata al controllo di Bash Job, più precisamente su fg.


2

Utilizza il exit()metodo statico nella classe SpringApplication per chiudere con garbo la tua applicazione di avvio primaverile.

public class SomeClass {
    @Autowire
    private ApplicationContext context

    public void close() {
        SpringApplication.exit(context);
    }
}

Questo funziona per me. Grazie mille.
Khachornchit Songsaen

1

Spring Boot ora supporta l'arresto regolare (attualmente nelle versioni pre-rilascio, 2.3.0.BUILD-SNAPSHOT)

Se abilitato, l'arresto dell'applicazione includerà un periodo di grazia di durata configurabile. Durante questo periodo di grazia, le richieste esistenti potranno essere completate ma non saranno consentite nuove richieste

Puoi abilitarlo con:

server.shutdown.grace-period=30s

https://docs.spring.io/spring-boot/docs/2.3.0.BUILD-SNAPSHOT/reference/html/spring-boot-features.html#boot-features-graceful-shutdown



0

Se ti trovi in ​​un ambiente Linux tutto ciò che devi fare è creare un collegamento simbolico al tuo file .jar dall'interno di /etc/init.d/

sudo ln -s /path/to/your/myboot-app.jar /etc/init.d/myboot-app

Quindi puoi avviare l'applicazione come qualsiasi altro servizio

sudo /etc/init.d/myboot-app start

Per chiudere l'applicazione

sudo /etc/init.d/myboot-app stop

In questo modo, l'applicazione non terminerà quando esci dal terminale. E l'applicazione verrà chiusa con grazia con il comando di arresto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.