docker-compose下的java應用啟動順序兩部曲之二:實戰
上篇回顧
- 本文是《docker-compose下的java應用啟動順序兩部曲》的終篇,在上一篇《docker-compose下的java應用啟動順序兩部曲之一:問題分析》中,我們以SpringCloud環境下的註冊中心和業務服務為例,展示了docker-compose.yml中depends_on引數的不足:即只能控制容器建立順序,但我們想要的是eureka服務就緒之後再啟動業務服務,並且docker官方也認為depends_on引數是達不到這個要求的,如下圖所示:
針對上述問題,docker給出的解決辦法是使用wait-for-it.sh指令碼來解決問題,地址:https://docs.docker.com/compose/startup-order/ ,如下圖:
什麼是wait-for-it.sh
- wait-for-it.sh指令碼用來訪問指定的地址和埠,如果收不到響應就等待一段時間再去重試,直到收到響應後,再去做前面指定好的命令,如上圖紅框所示./wait-for-it.sh db:5432 -- python app.py的意思是:等到db:5432這個遠端訪問能夠響應的時候,就去執行python app.py命令
wait-for-it.sh檔案的連結:
https://raw.githubusercontent.com/zq2599/blog_demos/master/wait-for-it-demo/docker/wait-for-it.sh環境資訊
本次實戰的環境如下:
- 作業系統:CentOS Linux release 7.7.1908
- docker:1.13.1
- docker-compose:1.24.1
- spring cloud:Finchley.RELEASE
- maven:3.6.0
- jib:1.7.0
實戰簡介
上一篇的例子中,我們用到了eureka和service兩個容器,eureka是註冊中心,service是普通業務應用,service容器向eureka容器註冊時,eureka還沒有初始化完成,因此service註冊失敗,在稍後的自動重試時由於eureka進入ready狀態,因而service註冊成功。
今天我們來改造上一篇的例子,讓service用上docker官方推薦的wait-for-it.sh指令碼,等待eureka服務就緒再啟動java程序,確保service可以一次性註冊eureka成功;
為了達到上述目標,總共需要做以下幾步:
- 簡單介紹eureka和service容器的映象是怎麼製作的;
- 製作基礎映象,包含wait-for-it.sh指令碼;
- 使用新的基礎映象構建service映象;
- 改造docker-compose.yml;
- 啟動容器,驗證順序控制是否成功;
- wait-for-it.sh方案的缺陷;
接下來進入實戰環節;
原始碼下載
如果您不想編碼,也可以在GitHub上獲取文中所有原始碼和指令碼,地址和連結資訊如下表所示:
| 名稱 | 連結 | 備註|
| :-------- | :----| :----|
| 專案主頁| https://github.com/zq2599/blog_demos | 該專案在GitHub上的主頁 |
| git倉庫地址(https)| https://github.com/zq2599/blog_demos.git | 該專案原始碼的倉庫地址,https協議 |
| git倉庫地址(ssh)| [email protected]:zq2599/blog_demos.git | 該專案原始碼的倉庫地址,ssh協議 |
這個git專案中有多個資料夾,本章的應用在wait-for-it-demo資料夾下,如下圖紅框所示:
原始碼的結構如下圖所示:
接下來開始編碼了;
簡單介紹eureka和service容器
上一篇和本篇,我們都在用eureka和service這兩個容器做實驗,現在就來看看他們是怎麼做出來的:
- eureka是個maven工程,和SpringCloud環境中的eureka服務一樣,唯一不同的是它的pom.xml中使用了jib外掛,用來將工程構建成docker映象:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.bolingcavalry</groupId>
<artifactId>eureka</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>eureka</name>
<description>eureka</description>
<parent>
<groupId>com.bolingcavalry</groupId>
<artifactId>wait-for-it-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!--使用jib外掛-->
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>1.7.0</version>
<configuration>
<!--from節點用來設定映象的基礎映象,相當於Docerkfile中的FROM關鍵字-->
<from>
<!--使用openjdk官方映象,tag是8-jdk-stretch,表示映象的作業系統是debian9,裝好了jdk8-->
<image>openjdk:8-jdk-stretch</image>
</from>
<to>
<!--映象名稱和tag,使用了mvn內建變數${project.version},表示當前工程的version-->
<image>bolingcavalry/${project.artifactId}:${project.version}</image>
</to>
<!--容器相關的屬性-->
<container>
<!--jvm記憶體引數-->
<jvmFlags>
<jvmFlag>-Xms1g</jvmFlag>
<jvmFlag>-Xmx1g</jvmFlag>
</jvmFlags>
<!--要暴露的埠-->
<ports>
<port>8080</port>
</ports>
<useCurrentTimestamp>true</useCurrentTimestamp>
</container>
</configuration>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>dockerBuild</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
上述pom.xml中多了個jib外掛,這樣在執行mvn compile的時候,外掛就會用構建結果製作好docker映象並放入本地倉庫;
- service是個普通的SpringCloud應用,除了在pom.xml中也用到了jib外掛來構建映象,它的配置檔案中,訪問eureka的地址要寫成eureka容器的名稱:
spring:
application:
name: service
eureka:
client:
serviceUrl:
defaultZone: http://eureka:8080/eureka/
- 關於如何將java應用製作成docker映象,如果您想了解更多請參考以下兩篇文章:
《Docker與Jib(maven外掛版)實戰》
《Jib使用小結(Maven外掛版)》
製作基礎映象
從上面的pom.xml可見,我們將Java應用製作成docker映象時,使用的基礎映象是openjdk:8-jdk-stretch,這樣做出的應用映象是不含wait-for-it.sh指令碼的,自然就無法實現啟動順序控制了,因此我們要做一個帶有wait-for-it.sh的基礎映象給業務映象用:
- 把wait-for-it.sh檔案準備好,下載地址:https://raw.githubusercontent.com/zq2599/blog_demos/master/wait-for-it-demo/docker/wait-for-it.sh
- 在wait-for-it.sh檔案所在目錄新建Dockerfile檔案,內容如下:
FROM openjdk:8-jdk-stretch
ADD wait-for-it.sh /wait-for-it.sh
RUN sh -c 'chmod 777 /wait-for-it.sh'
注意:我這裡用的是openjdk:8-jdk-stretch,您可以根據自己的實際需要選擇不同的openjdk版本,可以參考:《openjdk映象的tag說明》
- 執行命令docker build -t bolingcavalry/jkd8-wait-for-it:0.0.2 .就能構建出名為bolingcavalry/jkd8-wait-for-it:0.0.2的映象了,請您根據自己的情況設定映象名稱和tag,注意命令的末尾有個小數點,不要漏了;
- 如果您有hub.docker.com賬號,建請使用docker push命令將新建的映象推送到映象倉庫上去,或者推送到私有倉庫,因為後面使用jib外掛構建映象是,jib外掛要去倉庫獲取基礎映象的元資料資訊,取不到會導致構建失敗;
使用新的基礎映象構建service映象
我們的目標是讓service服務等待eureka服務就緒,所以應該改造service服務,讓它用docker官方推薦的wait-for-it.sh方案來實現等待:
- 修改service工程的pom.xml,有關jib外掛的配置改為以下內容:
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>1.7.0</version>
<configuration>
<!--from節點用來設定映象的基礎映象,相當於Docerkfile中的FROM關鍵字-->
<from>
<!--使用自制的基礎映象,裡面有wait-for-it.sh指令碼-->
<image>bolingcavalry/jkd8-wait-for-it:0.0.2</image>
</from>
<to>
<!--映象名稱和tag,使用了mvn內建變數${project.version},表示當前工程的version-->
<image>bolingcavalry/${project.artifactId}:${project.version}</image>
</to>
<!--容器相關的屬性-->
<container>
<!--entrypoint的值等於INHERIT表示jib外掛不構建啟動命令了,此時要使用者自己控制,可以在啟動時輸入,或者寫在基礎映象中-->
<entrypoint>INHERIT</entrypoint>
<!--要暴露的埠-->
<ports>
<port>8080</port>
</ports>
<useCurrentTimestamp>true</useCurrentTimestamp>
</container>
</configuration>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>dockerBuild</goal>
</goals>
</execution>
</executions>
</plugin>
上述配置有幾點需要注意:
a. 基礎映象改為剛剛構建好的bolingcavalry/jkd8-wait-for-it:0.0.2
b. 增加entrypoint節點,內容是INHERIT,按照官方的說法,entrypoint的值等於INHERIT表示jib外掛不構建啟動命令了,此時要使用者自己控制,可以在啟動時輸入,或者寫在基礎映象中,這樣我們在docker-compose.yml中用command引數來設定service容器的啟動命令,就可以把wait-for-it.sh指令碼用上了
c. 去掉jvmFlags節點,按照官方文件的說法,entrypoint節點的值等於INHERIT時,jvmFlags和mainClass引數會被忽略,如下圖,地址是:https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin
至此,service工程改造完畢,接下來修改docker-compose.yml,讓service容器能用上wait-for-it.sh
### 改造docker-compose.yml
- 完整的docker-compose.yml內容如下所示:
version: '3'
services:
eureka:
image: bolingcavalry/eureka:0.0.1-SNAPSHOT
container_name: eureka
restart: unless-stopped
service:
image: bolingcavalry/service:0.0.1-SNAPSHOT
container_name: service
restart: unless-stopped
command: sh -c './wait-for-it.sh eureka:8080 -t 0 -- java -Xms1g -Xmx1g -cp /app/resources:/app/classes:/app/libs/* com.bolingcavalry.waitforitdemo.ServiceApplication'
depends_on:
- eureka
- 注意command引數的內容,如下,service容器建立後,會一直等待eureka:8080的響應,直到該地址有響應後,才會執行命令java -Xms1g -Xmx1g -cp /app/resources:/app/classes:/app/libs/* com.bolingcavalry.waitforitdemo.ServiceApplication:
sh -c './wait-for-it.sh eureka:8080 -t 0 -- java -Xms1g -Xmx1g -cp /app/resources:/app/classes:/app/libs/* com.bolingcavalry.waitforitdemo.ServiceApplication'
- 對於命令java -Xms1g -Xmx1g -cp /app/resources:/app/classes:/app/libs/* com.bolingcavalry.waitforitdemo.ServiceApplication,您可能覺得太長了不好寫,這裡有個小竅門,就是在不使用entrypoint節點的時候,用jib外掛製作的映象本身是帶有啟動命令的,容器執行的時候,您可以通過docker ps --no-trunc命令看到該容器的完整啟動命令,複製過來直接用就行了;
所有的改造工作都完成了,可以開始驗證了;
啟動容器,驗證順序控制是否成功
- 在docker-compose.yml檔案所在目錄執行命令docker-compose up,會建立兩個容器,並且日誌資訊會直接列印在控制檯,我們來分析這些日誌資訊,驗證順序控制是否成功;
- 如下圖,可見service容器中並沒有啟動java程序,而是在等待eureka:8080的響應:
- 繼續看日誌,可見eureka服務就緒的時候,service容器的wait-for-it.sh指令碼收到了響應,於是立即啟動service應用的程序:
繼續看日誌,如下圖,service在eureka上註冊成功:
綜上所述,使用docker官方推薦的wait-for-it.sh來控制java應用的啟動順序是可行的,可以按照業務自身的需求來量身定做合適的啟動順序;wait-for-it.sh方案的缺陷
使用docker官方推薦的wait-for-it.sh來控制容器啟動順序,雖然已滿足了我們的需求,但依舊留不是完美方案,留下的缺陷還是請您先知曉吧,也許這個缺陷會對您的系統產生嚴重的負面影響:
- 再開啟一個SSH連線,登入到實戰的linux電腦上,執行命令docker exec eureka ps -ef,將eureka容器內的程序打印出來,如下所示,java程序的PID等於1:
[root@maven ~]# docker exec eureka ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 2 07:04 ? 00:00:48 java -Xms1g -Xmx1g -cp /app/resources:/app/classes:/app/libs/* com.bolingcavalry.waitforitdemo.EurekaApplication
root 56 0 0 07:25 ? 00:00:00 /bin/bash
root 63 0 0 07:31 ? 00:00:00 ps -ef
- 再來看看service的程序情況,執行命令docker exec service ps -ef,將service容器內的程序打印出來,如下所示,PID等於1的程序不是java,而是啟動時的shell命令:
[root@maven ~]# docker exec service ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 07:04 ? 00:00:00 sh -c ./wait-for-it.sh eureka:8080 -t 0 -- java -Xms1g -Xmx1g -cp /app/resources:/app/classes:/app/libs/* com.bolingcavalry.waitforitdemo.ServiceApplication
root 7 1 1 07:04 ? 00:00:32 java -Xms1g -Xmx1g -cp /app/resources:/app/classes:/app/libs/* com.bolingcavalry.waitforitdemo.ServiceApplication
root 107 0 0 07:33 ? 00:00:00 ps -ef
- 通常情況下,在執行命令docker stop xxx停止容器時,只有PID=1的程序才會收到"SIGTERM"訊號量,所以在使用docker stop停止容器時,eureka容器中的java程序收到了"SIGTERM"可以立即停止,但是service容器中的java程序收不到"SIGTERM",因此只能等到預設的10秒超時時間到達的時候,被"SIGKILL"訊號量殺死,不但等待時間長,而且優雅停機的功能也用不上了;
- 您可以分別輸入docker stop eureka和docker stop service來感受一下,前者立即完成,後者要等待10秒。
- 我的shell技能過於平庸,目前還找不到好的解決辦法讓service容器中的java程序取得1號程序ID,個人覺得自定義entrypoint.sh指令碼來呼叫wait-for-it.sh並且處理"SIGTERM"說不定可行,如果您有好的辦法請留言告知,在此感激不盡;
- 目前看來,控制容器啟動順序最好的解決方案並非wait-for-it.sh,而是業務自己實現容錯,例如service註冊eureka失敗後會自動重試,但是這對業務的要求就略高了,尤其是在複雜的分散式環境中更加難以實現;
docker官方推薦使用wait-for-it.sh指令碼的文章地址是:https://docs.docker.com/compose/startup-order/ ,文章末尾顯示了頂和踩的數量,如下圖,頂的數量是145,踩的數量達到了563,一份官方文件居然這麼不受待見,也算是開了眼界,不知道和我前面提到的1號PID問題有沒有關係:
至此,java應用的容器順序控制實戰就完成了,希望您在對自己的應用做容器化的時候,此文能給您提供一些參考。歡迎關注公眾號:程式設計師欣宸