1. 程式人生 > 實用技巧 >kill -9 和kill -15,區別,python可以自定義at_exit鉤子

kill -9 和kill -15,區別,python可以自定義at_exit鉤子

最近瞥了一眼專案的重啟指令碼,發現運維一直在使用 kill-9<pid> 的方式重啟 springboot embedded tomcat,其實大家幾乎一致認為:kill-9<pid> 的方式比較暴力,但究竟會帶來什麼問題卻很少有人能分析出個頭緒。這篇文章主要記錄下自己的思考過程。

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

在以前,我們釋出 WEB 應用通常的步驟是將程式碼打成 war 包,然後丟到一個配置好了應用容器(如 Tomcat,Weblogic)的 Linux 機器上,這時候我們想要啟動/關閉應用,方式很簡單,執行其中的啟動/關閉指令碼即可。而 springboot 提供了另一種方式,將整個應用連同內建的 tomcat 伺服器一起打包,這無疑給釋出應用帶來了很大的便捷性,與之而來也產生了一個問題:如何關閉 springboot 應用呢?一個顯而易見的做法便是,根據應用名找到程序 id,殺死程序 id 即可達到關閉應用的效果。

上述的場景描述引出了我的疑問:怎麼優雅地殺死一個 springboot 應用程序呢?這裡僅僅以最常用的 Linux 作業系統為例,在 Linux 中 kill 指令負責殺死程序,其後可以緊跟一個數字,代表訊號編號(Signal),執行 kill-l 指令,可以一覽所有的訊號編號。

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 應用為例展開討論,新增如下程式碼。

1 增加一個實現了 DisposableBean 介面的類

@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 ...");

        }

      }));
    }
}

測試步驟

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

  2. 測試kill-9pidkill-15pidctrl+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