Docker容器:如何讓應(yīng)用程序優(yōu)雅退出
伴隨著業(yè)務(wù)的不斷更新迭代,容器啟動的和停止經(jīng)常發(fā)生,當容器停止時,如果容器內(nèi)的程序未執(zhí)行完,那么將會造成數(shù)據(jù)不完整,特別是一些分布式事務(wù),可能會導(dǎo)致數(shù)據(jù)不一致,為此,容器引入優(yōu)雅關(guān)閉功能。
在上一篇 能在容器里面kill -9殺死容器的文章中,已經(jīng)介紹了如何捕獲信號,當我們執(zhí)行docker stop命令后,docker會向容器中進程ID為1的進程發(fā)送SIGTERM(kill -15)信號,當?shù)却欢螘r間后程序仍然沒有退出后,將發(fā)送SIGKILL(kill -9)信號強制殺死進程。等待時間可以通過參數(shù)設(shè)置
- # docker stop ----time=30 foo
但如果使用docker kill命令的話,則不會有等待時間,直接發(fā)送SIGKILL信號。Kubernetes在容器關(guān)閉時候也是通過docker stop命令優(yōu)雅關(guān)閉容器,當容器內(nèi)應(yīng)用接收到SIGTERM信號后將拒絕新的訪問請求并且執(zhí)行完未處理的任務(wù),回收占用的資源。下面通過一段Go的代碼舉例如何獲取SIGTERM信號并優(yōu)雅退出。
- term := make(chan os.Signal)
- signal.Notify(term, os.Interrupt, syscall.SIGTERM)
- cancel := make(chan struct{})
- select {
- case <-term:
- level.Warn(logger).Log(“msg“, “Received SIGTERM, exiting gracefully...“)
- #執(zhí)行具體回收動作
- }
如果其他編程語言也相似,但這里有個坑需要需要注意,由于Docker關(guān)閉時候只給進程號是1的進程發(fā)送信號,也就是說如果應(yīng)用程序的進程ID不是1,那么將不會收到信號Docker發(fā)出的信號。下面舉例說明,先看一個正常的JAVA程序:Kill.java 。
- class Kill {
- private static Thread main;
- public static void main(String[] a) throws Exception {
- Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
- public void run() {
- System.out.println("TERM");
- main.interrupt();
- for (int i = 0; i < 4; i++) {
- System.out.println("busy");
- try {
- Thread.sleep(1000);
- } catch (Exception e) {}
- }
- System.out.println("exit");
- }
- }));
- main = Thread.currentThread();
- while (true) {
- Thread.sleep(1000);
- System.out.println("run");
- }
- }
- }
執(zhí)行”Javac Kill.java“編譯代碼并打包到Docker鏡像中,Dockerfile如下:
- FROM openjdk:8-jre-alpine
- ADD Kill*.class /
- ENTRYPOINT ["java","Kill"]
啟動容器,進入容器可以看到”java Kill“進程號為1,當執(zhí)行docker stop命令后程序?qū)⒔邮盏絋ERM信號,并優(yōu)雅退出。然后我們再修改一下Dockerfile,添加一個啟動腳本start.sh,腳本非常簡單就兩行,如下:
- #! /bin/sh
- java Kill
重新構(gòu)建鏡像并啟動,新的Dockerfile如下:
- FROM openjdk:8-jre-alpine
- ADD Kill*.class /
- ADD start.sh /
- ENTRYPOINT ["sh","-c","/start.sh"]
啟動容器后,進入容器會發(fā)現(xiàn),JAVA進程的ID變成7,成為shell(進程ID為1)的子進程。
- # ps -ef
- ID USER TIME COMMAND
- 1 root 0:00 {start.sh} /bin/sh /start.sh
- root 0:00 java Kill
此時再次執(zhí)行docker stop命令,容器將不會收的TERM信號,并在默認的10秒優(yōu)雅關(guān)閉時間后,直接退出(其實是被SIGKILL直接干掉了)。所以當需要優(yōu)雅退出時必須保證應(yīng)用程序的進程ID為1。
那有沒有別的方法,能夠在保證PID不為1的時候也能優(yōu)雅退出呢?當然可以,有兩種常見的辦法:
(1)我們可以在容器關(guān)閉前執(zhí)行prestop腳本,腳本里面首先動態(tài)獲取JAVA進程的ID,然后通過kill直接對這個進程發(fā)送TERM信號,從而優(yōu)雅關(guān)閉程序。如下所示:
- PID=`pidof java` && kill -SIGTERM $PID。
- (2)可以通過tini容器啟動應(yīng)用,tini的作為ID為1的進程,當他收到信號后就會轉(zhuǎn)發(fā)信號給子進程,那么子進程就可以執(zhí)行優(yōu)雅退出操作了。而且tini本身還具有回收僵尸進程的能力。
(2)可以通過tini容器啟動應(yīng)用,tini的作為ID為1的進程,當他收到信號后就會轉(zhuǎn)發(fā)信號給子進程,那么子進程就可以執(zhí)行優(yōu)雅退出操作了。而且tini本身還具有回收僵尸進程的能力。






















