還在用 kill -9 停機?這纔是最優雅的姿式(轉)

最近瞥了一眼項目的重啓腳本,發現運維一直在使用 kill-9<pid> 的方式重啓 springboot embedded tomcat,其實你們幾乎一致認爲:kill-9<pid> 的方式比較暴力,但究竟會帶來什麼問題卻不多有人能分析出個頭緒。這篇文章主要記錄下本身的思考過程。java

kill -9 和 kill -15 有什麼區別?

在之前,咱們發佈 WEB 應用一般的步驟是將代碼打成 war 包,而後丟到一個配置好了應用容器(如 Tomcat,Weblogic)的 Linux 機器上,這時候咱們想要啓動/關閉應用,方式很簡單,運行其中的啓動/關閉腳本便可。而 springboot 提供了另外一種方式,將整個應用連同內置的 tomcat 服務器一塊兒打包,這無疑給發佈應用帶來了很大的便捷性,與之而來也產生了一個問題:如何關閉 springboot 應用呢?一個顯而易見的作法即是,根據應用名找到進程 id,殺死進程 id 便可達到關閉應用的效果。web

上述的場景描述引出了個人疑問:怎麼優雅地殺死一個 springboot 應用進程呢?這裏僅僅以最經常使用的 Linux 操做系統爲例,在 Linux 中 kill 指令負責殺死進程,其後能夠緊跟一個數字,表明信號編號(Signal),執行 kill-l 指令,能夠一覽全部的信號編號。spring

xu@ntzyz-qcloud ~ % kill -l                                                                    

HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM STKFLT CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH POLL PWR SYS

 

本文主要介紹下第 9 個信號編碼 KILL,以及第 15 個信號編號 TERM 。數據庫

先簡單理解下這二者的區別:kill-9pid 能夠理解爲操做系統從內核級別強行殺死某個進程, kill-15pid 則能夠理解爲發送一個通知,告知應用主動關閉。這麼對比仍是有點抽象,那咱們就從應用的表現來看看,這兩個命令殺死應用到底有啥區別。緩存

代碼準備

因爲筆者 springboot 接觸較多,因此以一個簡易的 springboot 應用爲例展開討論,添加以下代碼。tomcat

1 增長一個實現了 DisposableBean 接口的類springboot

@Component
public class TestDisposableBean implements DisposableBean{
   @Override
    public void destroy() throws Exception {
      System.out.println("測試 Bean 已銷燬 ...");
    }

}

2 增長 JVM 關閉時的鉤子服務器

@SpringBootApplication
@RestController
public class TestShutdownApplication implements DisposableBean {
  public static void main( String [] args) {
    SpringApplication.run( TestShutdownApplication.class, args);
      Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
        @Override
        public void run() {
          System.out.println("執行 ShutdownHook ...");

        }

      }));
    }
}

測試步驟restful

  1. 執行 java-jar test-shutdown-1.0.jar 將應用運行起來框架

  2. 測試 kill-9pid, kill-15pid, ctrl+c 後輸出日誌內容

測試結果

kill-15 pid & ctrl+c,效果同樣,輸出結果以下

2018-01-14 16:55:32.424  INFO 8762 --- [       Thread-3] ationConfigEmbeddedWebApplicationContext : Closing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2cdf8d8a: startup date [Sun Jan 14 16:55:24 UTC 2018]; root of context hierarchy

2018-01-14 16:55:32.432  INFO 8762 --- [       Thread-3] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown

執行 ShutdownHook ...

測試 Bean 已銷燬 ...

java -jar test-shutdown-1.0.jar  7.46s user 0.30s system 80% cpu 9.674 total

 

 

kill-9 pid,沒有輸出任何應用日誌

[1]    8802 killed     java -jar test-shutdown-1.0.jar

java -jar test-shutdown-1.0.jar  7.74s user 0.25s system 41% cpu 19.272 total

能夠發現,kill -9 pid 是給應用殺了個措手不及,沒有留給應用任何反應的機會。而反觀 kill -15 pid,則比較優雅,先是由 AnnotationConfigEmbeddedWebApplicationContext (一個 ApplicationContext 的實現類)收到了通知,緊接着執行了測試代碼中的 Shutdown Hook,最後執行了 DisposableBean#destory() 方法。孰優孰劣,立判高下。

通常咱們會在應用關閉時處理一下「善後」的邏輯,好比

  1. 關閉 socket 連接

  2. 清理臨時文件

  3. 發送消息通知給訂閱方,告知本身下線

  4. 將本身將要被銷燬的消息通知給子進程

  5. 各類資源的釋放

  6. ... ...

而 kill -9 pid 則是直接模擬了一次系統宕機,系統斷電,這對於應用來講太不友好了,不要用收割機來修剪花盆裏的花。取而代之,即是使用 kill -15 pid 來代替。若是在某次實際操做中發現:kill -15 pid 沒法關閉應用,則能夠考慮使用內核級別的 kill -9 pid ,但請過後務必排查出是什麼緣由致使 kill -15 pid 沒法關閉。

springboot 如何處理 -15 TERM Signal?

上面解釋過了,使用 kill -15 pid 的方式能夠比較優雅的關閉 springboot 應用,咱們可能有如下的疑惑:springboot/spring 是如何響應這一關閉行爲的呢?是先關閉了 tomcat,緊接着退出 JVM,仍是相反的次序?它們又是如何互相關聯的?

嘗試從日誌開始着手分析, AnnotationConfigEmbeddedWebApplicationContext 打印出了 Closing 的行爲,直接去源碼中一探究竟,最終在其父類 AbstractApplicationContext 中找到了關鍵的代碼:

@Override

public void registerShutdownHook() {

 if (this.shutdownHook == null) {

   this.shutdownHook = new Thread() {

     @Override

     public void run() {

       synchronized (startupShutdownMonitor) {

         doClose();

       }

     }

   };

   Runtime.getRuntime().addShutdownHook(this.shutdownHook);

 }

}

 

@Override

public void close() {

  synchronized (this.startupShutdownMonitor) {

     doClose();

     if (this.shutdownHook != null) {

        Runtime.getRuntime().removeShutdownHook(this.shutdownHook);

     }

  }

}

 

protected void doClose() {

  if (this.active.get() && this.closed.compareAndSet(false, true)) {

     LiveBeansView.unregisterApplicationContext(this);

     // 發佈應用內的關閉事件

     publishEvent(new ContextClosedEvent(this));

     // Stop all Lifecycle beans, to avoid delays during individual destruction.

     if (this.lifecycleProcessor != null) {

        this.lifecycleProcessor.onClose();

     }

     // spring 的 BeanFactory 可能會緩存單例的 Bean

     destroyBeans();

     // 關閉應用上下文&BeanFactory

     closeBeanFactory();

     // 執行子類的關閉邏輯

     onClose();

     this.active.set(false);

  }

}

爲了方便排版以及便於理解,我去除了源碼中的部分異常處理代碼,並添加了相關的註釋。在容器初始化時,ApplicationContext 便已經註冊了一個 Shutdown Hook,這個鉤子調用了 Close() 方法,因而當咱們執行 kill -15 pid 時,JVM 接收到關閉指令,觸發了這個 Shutdown Hook,進而由 Close() 方法去處理一些善後手段。具體的善後手段有哪些,則徹底依賴於 ApplicationContext 的 doClose() 邏輯,包括了註釋中說起的銷燬緩存單例對象,發佈 close 事件,關閉應用上下文等等,特別的,當 ApplicationContext 的實現類是 AnnotationConfigEmbeddedWebApplicationContext 時,還會處理一些 tomcat/jetty 一類內置應用服務器關閉的邏輯。

窺見了 springboot 內部的這些細節,更加應該瞭解到優雅關閉應用的必要性。JAVA 和 C 都提供了對 Signal 的封裝,咱們也能夠手動捕獲操做系統的這些 Signal,在此不作過多介紹,有興趣的朋友能夠本身嘗試捕獲下。

還有其餘優雅關閉應用的方式嗎?

spring-boot-starter-actuator 模塊提供了一個 restful 接口,用於優雅停機。

添加依賴

<dependency>

  <groupId>org.springframework.boot</groupId>

  <artifactId>spring-boot-starter-actuator</artifactId>

</dependency>

添加配置

#啓用shutdown

endpoints.shutdown.enabled=true

#禁用密碼驗證

endpoints.shutdown.sensitive=false

 

生產中請注意該端口須要設置權限,如配合 spring-security 使用。

執行 curl-X POST host:port/shutdown 指令,關閉成功即可以得到以下的返回:

{
"message"
:
"Shutting down, bye..."
}

雖然 springboot 提供了這樣的方式,但按我目前的瞭解,沒見到有人用這種方式停機,kill -15 pid 的方式達到的效果與此相同,將其列於此處只是爲了方案的完整性。

如何銷燬做爲成員變量的線程池?

儘管 JVM 關閉時會幫咱們回收必定的資源,但一些服務若是大量使用異步回調,定時任務,處理不當頗有可能會致使業務出現問題,在這其中,線程池如何關閉是一個比較典型的問題。

@Service

public class SomeService {

   ExecutorService executorService = Executors.newFixedThreadPool(10);

   public void concurrentExecute() {

       executorService.execute(new Runnable() {

           @Override

           public void run() {

               System.out.println("executed...");

           }

       });

   }

}

咱們須要想辦法在應用關閉時(JVM 關閉,容器中止運行),關閉線程池。

初始方案:什麼都不作。在通常狀況下,這不會有什麼大問題,由於 JVM 關閉,會釋放之,但顯然沒有作到本文一直在強調的兩個字,沒錯----優雅。

方法一的弊端在於線程池中提交的任務以及阻塞隊列中未執行的任務變得極其不可控,接收到停機指令後是馬上退出?仍是等待任務執行完成?抑或是等待必定時間任務還沒執行完成則關閉?

方案改進:

發現初始方案的劣勢後,我馬上想到了使用 DisposableBean 接口,像這樣:

@Service

public class SomeService implements DisposableBean{

 

   ExecutorService executorService = Executors.newFixedThreadPool(10);

 

   public void concurrentExecute() {

       executorService.execute(new Runnable() {

           @Override

           public void run() {

               System.out.println("executed...");

           }

       });

   }

 

   @Override

   public void destroy() throws Exception {

       executorService.shutdownNow();

       //executorService.shutdown();

   }

}

 

緊接着問題又來了,是 shutdown 仍是 shutdownNow 呢?這兩個方法仍是常常被誤用的,簡單對比這兩個方法。

ThreadPoolExecutor 在 shutdown 以後會變成 SHUTDOWN 狀態,沒法接受新的任務,隨後等待正在執行的任務執行完成。意味着,shutdown 只是發出一個命令,至於有沒有關閉仍是得看線程本身。

ThreadPoolExecutor 對於 shutdownNow 的處理則不太同樣,方法執行以後變成 STOP 狀態,並對執行中的線程調用 Thread.interrupt() 方法(但若是線程未處理中斷,則不會有任何事發生),因此並不表明「馬上關閉」。

查看 shutdown 和 shutdownNow 的 java doc,會發現以下的提示:

shutdown() :Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted.Invocation has no additional effect if already shut down.This method does not wait for previously submitted tasks to complete execution.Use {@link #awaitTermination awaitTermination} to do that.

shutdownNow():Attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the tasks that were awaiting execution. These tasks are drained (removed) from the task queue upon return from this method.This method does not wait for actively executing tasks to terminate. Use {@link #awaitTermination awaitTermination} to do that.There are no guarantees beyond best-effort attempts to stop processing actively executing tasks. This implementation cancels tasks via {@link Thread#interrupt}, so any task that fails to respond to interrupts may never terminate.

 

二者都提示咱們須要額外執行 awaitTermination 方法,僅僅執行 shutdown/shutdownNow 是不夠的。

最終方案:參考 spring 中線程池的回收策略,咱們獲得了最終的解決方案。

public abstract class ExecutorConfigurationSupport extends CustomizableThreadFactory

     implements DisposableBean{

   @Override

   public void destroy() {

       shutdown();

   }

 

   public void shutdown() {

       if (this.waitForTasksToCompleteOnShutdown) {

           this.executor.shutdown();

       }

       else {

           this.executor.shutdownNow();

       }

       awaitTerminationIfNecessary();

   }

   

   private void awaitTerminationIfNecessary() {

       if (this.awaitTerminationSeconds > 0) {

           try {

               this.executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS));

           }

           catch (InterruptedException ex) {

               Thread.currentThread().interrupt();

           }

       }

   }

}

 

保留了註釋,去除了一些日誌代碼,一個優雅關閉線程池的方案呈如今咱們的眼前。

1 經過 waitForTasksToCompleteOnShutdown 標誌來控制是想馬上終止全部任務,仍是等待任務執行完成後退出。

2 executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS)); 控制等待的時間,防止任務無限期的運行(前面已經強調過了,即便是 shutdownNow 也不能保證線程必定中止運行)。

更多須要思考的優雅停機策略

在咱們分析 RPC 原理的系列文章裏面曾經提到,服務治理框架通常會考慮到優雅停機的問題。一般的作法是事先隔斷流量,接着關閉應用。常見的作法是將服務節點從註冊中心摘除,訂閱者接收通知,移除節點,從而優雅停機;涉及到數據庫操做,則可使用事務的 ACID 特性來保證即便 crash 停機也能保證不出現異常數據,正常下線則更不用說了;又好比消息隊列能夠依靠 ACK 機制+消息持久化,或者是事務消息保障;定時任務較多的服務,處理下線則特別須要注意優雅停機的問題,由於這是一個長時間運行的服務,比其餘狀況更容易受停機問題的影響,可使用冪等和標誌位的方式來設計定時任務...

事務和 ACK 這類特性的支持,即便是宕機,停電,kill -9 pid 等狀況,也可使服務儘可能可靠;而一樣須要咱們思考的還有 kill -15 pid,正常下線等狀況下的停機策略。最後再補充下整理這個問題時,本身對 jvm shutdown hook 的一些理解。

When the virtual machine begins its shutdown sequence it will start all registered shutdown hooks in some unspecified order and let them run concurrently. When all the hooks have finished it will then run all uninvoked finalizers if finalization-on-exit has been enabled. Finally, the virtual machine will halt.

 

 

shutdown hook 會保證 JVM 一直運行,直到 hook 終止 (terminated)。這也啓示咱們,若是接收到 kill -15 pid 命令時,執行阻塞操做,能夠作到等待任務執行完成以後再關閉 JVM。同時,也解釋了一些應用執行 kill -15 pid 沒法退出的問題,沒錯,中斷被阻塞了。

 

 

轉自:https://mp.weixin.qq.com/s/RQaVlxA9uiP0G3GHACzPwQ

相關文章
相關標籤/搜索