天天看點

如何優雅停止 Springboot 運作

準備工作

@PreDestroy 會在系統關閉前執行

複制

package cn.netkiller;

import javax.annotation.PreDestroy;

import org.springframework.context.annotation.Configuration;

@Configuration
public class ShutdownConfiguration {

  public ShutdownConfiguration() {
    // TODO Auto-generated constructor stub
  }

  @PreDestroy
  public void preDestroy() {
    System.out.println("==============================");
    System.out.println("Destroying Spring");
    System.out.println("==============================");
  }

}           

複制

kill 指令示範

kill 指令本質是給程序發送終止信号,程序接收到終止信号後退出運作。

可以看到 Springboot 啟動後,程序 PID 44559,現在使用 kill 指令殺死這個程序

當執行 kill 44559 你會看到下面的輸出

複制

neo@MacBook-Pro-Neo ~/workspace/microservice/test % java -jar target/test-0.0.1-SNAPSHOT.jar
Starting...

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.3)

2021-07-29 11:05:09.862  INFO 44559 --- [           main] cn.netkiller.Application                 : Starting Application v0.0.1-SNAPSHOT using Java 16.0.1 on MacBook-Pro-Neo.local with PID 44559 (/Users/neo/workspace/microservice/test/target/test-0.0.1-SNAPSHOT.jar started by neo in /Users/neo/workspace/microservice/test)
2021-07-29 11:05:09.865  INFO 44559 --- [           main] cn.netkiller.Application                 : No active profile set, falling back to default profiles: default
2021-07-29 11:05:11.363  WARN 44559 --- [           main] io.undertow.websockets.jsr               : UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used
2021-07-29 11:05:11.399  INFO 44559 --- [           main] io.undertow.servlet                      : Initializing Spring embedded WebApplicationContext
2021-07-29 11:05:11.400  INFO 44559 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1451 ms
2021-07-29 11:05:12.041  INFO 44559 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 1 endpoint(s) beneath base path '/actuator'
2021-07-29 11:05:12.073  INFO 44559 --- [           main] io.undertow                              : starting server: Undertow - 2.2.9.Final
2021-07-29 11:05:12.085  INFO 44559 --- [           main] org.xnio                                 : XNIO version 3.8.4.Final
2021-07-29 11:05:12.099  INFO 44559 --- [           main] org.xnio.nio                             : XNIO NIO Implementation Version 3.8.4.Final
2021-07-29 11:05:12.197  INFO 44559 --- [           main] org.jboss.threads                        : JBoss Threads version 3.1.0.Final
2021-07-29 11:05:12.263  INFO 44559 --- [           main] o.s.b.w.e.undertow.UndertowWebServer     : Undertow started on port(s) 8080 (http)
2021-07-29 11:05:12.278  INFO 44559 --- [           main] cn.netkiller.Application                 : Started Application in 2.989 seconds (JVM running for 3.582)
2021-07-29 11:05:20.577  INFO 44559 --- [ionShutdownHook] io.undertow                              : stopping server: Undertow - 2.2.9.Final
==============================
Destroying Spring
==============================             

複制

而是用 kill -9 PID 就不會出現下面提示。

複制

neo@MacBook-Pro-Neo ~/workspace/microservice/test % java -jar target/test-0.0.1-SNAPSHOT.jar
Starting...

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.3)

2021-07-29 11:08:10.857  INFO 44613 --- [           main] cn.netkiller.Application                 : Starting Application v0.0.1-SNAPSHOT using Java 16.0.1 on MacBook-Pro-Neo.local with PID 44613 (/Users/neo/workspace/microservice/test/target/test-0.0.1-SNAPSHOT.jar started by neo in /Users/neo/workspace/microservice/test)
2021-07-29 11:08:10.860  INFO 44613 --- [           main] cn.netkiller.Application                 : No active profile set, falling back to default profiles: default
2021-07-29 11:08:12.377  WARN 44613 --- [           main] io.undertow.websockets.jsr               : UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used
2021-07-29 11:08:12.411  INFO 44613 --- [           main] io.undertow.servlet                      : Initializing Spring embedded WebApplicationContext
2021-07-29 11:08:12.411  INFO 44613 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1466 ms
2021-07-29 11:08:13.046  INFO 44613 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 1 endpoint(s) beneath base path '/actuator'
2021-07-29 11:08:13.081  INFO 44613 --- [           main] io.undertow                              : starting server: Undertow - 2.2.9.Final
2021-07-29 11:08:13.100  INFO 44613 --- [           main] org.xnio                                 : XNIO version 3.8.4.Final
2021-07-29 11:08:13.114  INFO 44613 --- [           main] org.xnio.nio                             : XNIO NIO Implementation Version 3.8.4.Final
2021-07-29 11:08:13.206  INFO 44613 --- [           main] org.jboss.threads                        : JBoss Threads version 3.1.0.Final
2021-07-29 11:08:13.275  INFO 44613 --- [           main] o.s.b.w.e.undertow.UndertowWebServer     : Undertow started on port(s) 8080 (http)
2021-07-29 11:08:13.290  INFO 44613 --- [           main] cn.netkiller.Application                 : Started Application in 3.195 seconds (JVM running for 3.808)
[1]    44613 killed     java -jar target/test-0.0.1-SNAPSHOT.jar
           

複制

這是因為 kill 指令會給程序發送終止信号,程序會正常退出.

什麼是正常退出呢?例如:

  • 完成為運作的邏輯
  • 将為寫入磁盤的檔案後寫入後退出
  • 執行完SQL并關閉資料庫
  • 寫入緩存,并關閉 redis
  • 完成使用者請求,并關閉連結

這就是為什麼當我們正常關閉程式需要等待很長時間,如果我們此時沒有運作狀态顯示,也沒有通過日志反應執行狀态,就會認為程式死了。其實此時程式可能盡職盡責的在工作,将未完成的工作完成,然後一步步正常退出。

尤其是多線程的程式,退出時需要等待每個線程完成請求,需要很長時間,我們常常因為更新時間緊迫而使用 kill -9 強行殺死程序,這會帶來很多問題。

kill -9 的弊端:

  1. 程式執行一半被強行退出,使用者端會出現 Timeout 逾時
  2. 檔案寫入一半被終止,如果是文本檔案隻有一半内容;如果是二進制檔案會造成損壞
  3. 資料庫操作一組SQL,隻執行了一半,會産生髒資料;如果使用事務處理會引起復原;

Ctrl + C 與 kill 沒有差別,也是給程序發送終止信号,現在我們來示範一下。

複制

neo@MacBook-Pro-Neo ~/workspace/microservice/test % java -jar target/test-0.0.1-SNAPSHOT.jar
Starting...
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.3)

2021-07-29 11:04:42.657  INFO 44546 --- [           main] cn.netkiller.Application                 : Starting Application v0.0.1-SNAPSHOT using Java 16.0.1 on MacBook-Pro-Neo.local with PID 44546 (/Users/neo/workspace/microservice/test/target/test-0.0.1-SNAPSHOT.jar started by neo in /Users/neo/workspace/microservice/test)
2021-07-29 11:04:42.660  INFO 44546 --- [           main] cn.netkiller.Application                 : No active profile set, falling back to default profiles: default
2021-07-29 11:04:44.212  WARN 44546 --- [           main] io.undertow.websockets.jsr               : UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used
2021-07-29 11:04:44.246  INFO 44546 --- [           main] io.undertow.servlet                      : Initializing Spring embedded WebApplicationContext
2021-07-29 11:04:44.246  INFO 44546 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1502 ms
2021-07-29 11:04:44.857  INFO 44546 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 1 endpoint(s) beneath base path '/actuator'
2021-07-29 11:04:44.889  INFO 44546 --- [           main] io.undertow                              : starting server: Undertow - 2.2.9.Final
2021-07-29 11:04:44.902  INFO 44546 --- [           main] org.xnio                                 : XNIO version 3.8.4.Final
2021-07-29 11:04:44.916  INFO 44546 --- [           main] org.xnio.nio                             : XNIO NIO Implementation Version 3.8.4.Final
2021-07-29 11:04:45.002  INFO 44546 --- [           main] org.jboss.threads                        : JBoss Threads version 3.1.0.Final
2021-07-29 11:04:45.068  INFO 44546 --- [           main] o.s.b.w.e.undertow.UndertowWebServer     : Undertow started on port(s) 8080 (http)
2021-07-29 11:04:45.084  INFO 44546 --- [           main] cn.netkiller.Application                 : Started Application in 3.149 seconds (JVM running for 3.748)
^C2021-07-29 11:04:47.082  INFO 44546 --- [ionShutdownHook] io.undertow                              : stopping server: Undertow - 2.2.9.Final
==============================
Destroying Spring
==============================    
           

複制

容器中如何優雅關閉 Springboot

容器與程序模式并沒有什麼差別,我們給容器發送終止信号,容器會轉發給 Springboot。

理論歸理論,我們還是需要親自實踐,這樣才能了解更深刻。

準備實驗環境和素材,下面是 docker-compose.yaml 編排檔案

複制

version: '3.9'
  
services:
  spring:
    image: openjdk:latest
    container_name: spring
    restart: always
    hostname: www.netkiller.cn
    environment:
      TZ: Asia/Shanghai
      JAVA_OPTS: -Xms256m -Xmx512m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m
    ports:
      - 8099:8080
    volumes:
      - ./test-0.0.1-SNAPSHOT.jar:/app/test-0.0.1-SNAPSHOT.jar
    entrypoint: java -jar /app/test-0.0.1-SNAPSHOT.jar
    command:
      --spring.profiles.active=dev
      --server.port=8080
               

複制

實驗步驟

  • 運作容器:docker-compose up
  • 觀察容器:docker-compose logs -f
  • 停止容器:

運作容器

複制

[root@localhost netkiller.cn]# docker-compose up -d
Starting spring ... done           

複制

觀察容器日志

複制

[root@localhost netkiller.cn]# docker-compose logs -f
spring    | Starting...
spring    | 
spring    |   .   ____          _            __ _ _
spring    |  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
spring    | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
spring    |  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
spring    |   '  |____| .__|_| |_|_| |_\__, | / / / /
spring    |  =========|_|==============|___/=/_/_/_/
spring    |  :: Spring Boot ::                (v2.5.3)
spring    | 
spring    | 2021-07-29 11:29:34.556  INFO 1 --- [           main] cn.netkiller.Application                 : Starting Application v0.0.1-SNAPSHOT using Java 16.0.2 on www.netkiller.cn with PID 1 (/app/test-0.0.1-SNAPSHOT.jar started by root in /)
spring    | 2021-07-29 11:29:34.559  INFO 1 --- [           main] cn.netkiller.Application                 : The following profiles are active: dev
spring    | 2021-07-29 11:29:35.903  WARN 1 --- [           main] io.undertow.websockets.jsr               : UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used
spring    | 2021-07-29 11:29:35.921  INFO 1 --- [           main] io.undertow.servlet                      : Initializing Spring embedded WebApplicationContext
spring    | 2021-07-29 11:29:35.921  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1274 ms
spring    | 2021-07-29 11:29:36.411  INFO 1 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 1 endpoint(s) beneath base path '/actuator'
spring    | 2021-07-29 11:29:36.437  INFO 1 --- [           main] io.undertow                              : starting server: Undertow - 2.2.9.Final
spring    | 2021-07-29 11:29:36.444  INFO 1 --- [           main] org.xnio                                 : XNIO version 3.8.4.Final
spring    | 2021-07-29 11:29:36.451  INFO 1 --- [           main] org.xnio.nio                             : XNIO NIO Implementation Version 3.8.4.Final
spring    | 2021-07-29 11:29:36.511  INFO 1 --- [           main] org.jboss.threads                        : JBoss Threads version 3.1.0.Final
spring    | 2021-07-29 11:29:36.547  INFO 1 --- [           main] o.s.b.w.e.undertow.UndertowWebServer     : Undertow started on port(s) 8080 (http)
spring    | 2021-07-29 11:29:36.560  INFO 1 --- [           main] cn.netkiller.Application                 : Started Application in 2.48 seconds (JVM running for 2.923)           

複制

停止容器

複制

[root@localhost netkiller.cn]# docker ps | grep spring
8901384d1973   openjdk:latest                "java -jar /app/test…"   3 minutes ago   Up About a minute   0.0.0.0:8099->8080/tcp, :::8099->8080/tcp                                              spring
[root@localhost netkiller.cn]# docker stop spring
spring
[root@localhost netkiller.cn]# docker ps | grep spring             

複制

在觀察日志

複制

spring    | 2021-07-29 11:31:31.807  INFO 1 --- [ionShutdownHook] io.undertow                              : stopping server: Undertow - 2.2.9.Final
spring    | ==============================
spring    | Destroying Spring
spring    | ==============================
spring exited with code 143    
               

複制

現在可以看到 Springboot 是正常退出的

下面我們再做一個實驗 docker kill

複制

[root@localhost netkiller.cn]# docker-compose start
Starting spring ... done

[root@localhost netkiller.cn]# docker-compose logs -f
    
[root@localhost netkiller.cn]# docker kill spring
spring               

複制

此時再觀察日志,隻輸出了一行。

複制

spring exited with code 137           

複制

結論,docker kill = kill -9

現在你應該明白什麼時候該使用什麼指令終止程式了吧,同時我們在寫程式的時候,也應該将程式的運作狀态反應出來,在我們停止程式運作的時候,可以去觀察程序的狀态,而不是半天沒有反應,隻能懷疑程序死了,必須執行B計劃(kill -9)這會造成很多資料丢失的問題。

寫入PID檔案

我們明白了 kill 的原理後,常常需要與 pid 打交道,使用 ps 指令是可以檢視 pid 的,但是當我們運作多個執行個體的時候會常常搞混,是以最好的方式是讓 springboot 把PID寫入到檔案中。

複制

package cn.netkiller;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.ApplicationPidFileWriter;

@SpringBootApplication

public class Application {

  public static void main(String[] args) {

    System.out.println("Starting...");
    // SpringApplication.run(Application.class, args);

    SpringApplication springApplication = new SpringApplication(Application.class);
    springApplication.addListeners(new ApplicationPidFileWriter());
    springApplication.run(args);
  }
}             

複制

程式運作後會在目前目錄下産生一個 PID 檔案

複制

neo@MacBook-Pro-Neo ~/workspace/microservice/test % cat application.pid 
44027           

複制

修改 pid 檔案位置可以配置 application.properties

複制

server.port=8080
spring.pid.file=/tmp/spring.pid           

複制

最後說說容器,容器的程序ID永遠是 1 是以配置與否自己斟酌。

複制

[root@localhost netkiller.cn]# docker exec -it spring cat /tmp/spring.pid
1           

複制