docker-compose下的java應用啓動順序兩部曲之二:實戰

上篇回顧

  • 本文是《docker-compose下的java應用啓動順序兩部曲》的終篇,在上一篇《docker-compose下的java應用啓動順序兩部曲之一:問題分析》中,咱們以SpringCloud環境下的註冊中心和業務服務爲例,展現了docker-compose.yml中<font color="blue">depends_on</font>參數的不足:即只能控制容器建立順序,但咱們想要的是eureka服務就緒以後再啓動業務服務,而且docker官方也認爲<font color="blue">depends_on</font>參數是達不到這個要求的,以下圖所示: 在這裏插入圖片描述
  • 針對上述問題,docker給出的解決辦法是使用<font color="blue">wait-for-it.sh</font>腳原本解決問題,地址:https://docs.docker.com/compose/startup-order/ ,以下圖: 在這裏插入圖片描述

什麼是wait-for-it.sh

  • <font color="blue">wait-for-it.sh</font>腳本用來訪問指定的地址和端口,若是收不到響應就等待一段時間再去重試,直到收到響應後,再去作前面指定好的命令,如上圖紅框所示<font color="blue">./wait-for-it.sh db:5432 -- python app.py</font>的意思是:等到<font color="blue">db:5432</font>這個遠程訪問可以響應的時候,就去執行<font color="blue">python app.py</font>命令
  • wait-for-it.sh文件的連接:<br>https://raw.githubusercontent.com/zq2599/blog_demos/master/wait-for-it-demo/docker/wait-for-it.sh

環境信息

本次實戰的環境以下:java

  1. 操做系統:CentOS Linux release 7.7.1908
  2. docker:1.13.1
  3. docker-compose:1.24.1
  4. spring cloud:Finchley.RELEASE
  5. maven:3.6.0
  6. jib:1.7.0

實戰簡介

上一篇的例子中,咱們用到了eureka和service兩個容器,eureka是註冊中心,service是普通業務應用,service容器向eureka容器註冊時,eureka尚未初始化完成,所以service註冊失敗,在稍後的自動重試時因爲eureka進入ready狀態,於是service註冊成功。 今天咱們來改造上一篇的例子,讓service用上docker官方推薦的<font color="blue">wait-for-it.sh</font>腳本,等待eureka服務就緒再啓動java進程,確保service能夠一次性註冊eureka成功; 爲了達到上述目標,總共須要作如下幾步:python

  1. 簡單介紹eureka和service容器的鏡像是怎麼製做的;
  2. 製做基礎鏡像,包含<font color="blue">wait-for-it.sh</font>腳本;
  3. 使用新的基礎鏡像構建service鏡像;
  4. 改造docker-compose.yml;
  5. 啓動容器,驗證順序控制是否成功;
  6. wait-for-it.sh方案的缺陷;

接下來進入實戰環節;linux

源碼下載

若是您不想編碼,也能夠在GitHub上獲取文中全部源碼和腳本,地址和連接信息以下表所示: | 名稱 | 連接 | 備註| | :-------- | :----| :----| | 項目主頁| https://github.com/zq2599/blog_demos | 該項目在GitHub上的主頁 | | git倉庫地址(https)| https://github.com/zq2599/blog_demos.git | 該項目源碼的倉庫地址,https協議 | | git倉庫地址(ssh)| git@github.com:zq2599/blog_demos.git | 該項目源碼的倉庫地址,ssh協議 | </br>git

這個git項目中有多個文件夾,本章的應用在<font color="blue">wait-for-it-demo</font>文件夾下,以下圖紅框所示: 在這裏插入圖片描述 源碼的結構以下圖所示: 在這裏插入圖片描述 接下來開始編碼了;程序員

簡單介紹eureka和service容器

上一篇和本篇,咱們都在用eureka和service這兩個容器作實驗,如今就來看看他們是怎麼作出來的:github

  1. 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插件,這樣在執行<font color="blue">mvn compile</font>的時候,插件就會用構建結果製做好docker鏡像並放入本地倉庫; 2. service是個普通的SpringCloud應用,除了在pom.xml中也用到了jib插件來構建鏡像,它的配置文件中,訪問eureka的地址要寫成eureka容器的名稱:spring

spring:
  application:
    name: service

eureka:
  client:
    serviceUrl:
      defaultZone: http://eureka:8080/eureka/
  1. 關於如何將java應用製做成docker鏡像,若是您想了解更多請參考如下兩篇文章: 《Docker與Jib(maven插件版)實戰》 《Jib使用小結(Maven插件版)》

製做基礎鏡像

從上面的pom.xml可見,咱們將Java應用製做成docker鏡像時,使用的基礎鏡像是<font color="blue">openjdk:8-jdk-stretch</font>,這樣作出的應用鏡像是不含wait-for-it.sh腳本的,天然就沒法實現啓動順序控制了,所以咱們要作一個帶有wait-for-it.sh的基礎鏡像給業務鏡像用:docker

  1. 把wait-for-it.sh文件準備好,下載地址:https://raw.githubusercontent.com/zq2599/blog_demos/master/wait-for-it-demo/docker/wait-for-it.sh
  2. 在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'

<font color="red">注意:</font>我這裏用的是openjdk:8-jdk-stretch,您能夠根據本身的實際須要選擇不一樣的openjdk版本,能夠參考:《openjdk鏡像的tag說明》 3. 執行命令<font color="blue">docker build -t bolingcavalry/jkd8-wait-for-it:0.0.2 .</font>就能構建出名爲<font color="red">bolingcavalry/jkd8-wait-for-it:0.0.2</font>的鏡像了,請您根據本身的狀況設置鏡像名稱和tag,注意命令的末尾有個小數點,不要漏了; 4. 若是您有hub.docker.com帳號,建請使用<font color="blue">docker push</font>命令將新建的鏡像推送到鏡像倉庫上去,或者推送到私有倉庫,由於後面使用jib插件構建鏡像是,jib插件要去倉庫獲取基礎鏡像的元數據信息,取不到會致使構建失敗;shell

使用新的基礎鏡像構建service鏡像

咱們的目標是讓service服務等待eureka服務就緒,因此應該改造service服務,讓它用docker官方推薦的<font color="blue">wait-for-it.sh</font>方案來實現等待:apache

  • 修改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. 基礎鏡像改成剛剛構建好的<font color="blue">bolingcavalry/jkd8-wait-for-it:0.0.2</font> b. 增長<font color="blue">entrypoint</font>節點,內容是<font color="red">INHERIT</font>,按照官方的說法,entrypoint的值等於INHERIT表示jib插件不構建啓動命令了,此時要使用者本身控制,能夠在啓動時輸入,或者寫在基礎鏡像中,這樣咱們在docker-compose.yml中用command參數來設置service容器的啓動命令,就能夠把<font color="blue">wait-for-it.sh</font>腳本用上了 c. 去掉<font color="blue">jvmFlags</font>節點,按照官方文檔的說法,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

  1. 完整的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
  1. 注意command參數的內容,以下,service容器建立後,會一直等待eureka:8080的響應,直到該地址有響應後,纔會執行命令<font color="blue">java -Xms1g -Xmx1g -cp /app/resources:/app/classes:/app/libs/* com.bolingcavalry.waitforitdemo.ServiceApplication</font>:
sh -c './wait-for-it.sh eureka:8080 -t 0  -- java -Xms1g -Xmx1g -cp /app/resources:/app/classes:/app/libs/* com.bolingcavalry.waitforitdemo.ServiceApplication'
  1. 對於命令<font color="blue">java -Xms1g -Xmx1g -cp /app/resources:/app/classes:/app/libs/* com.bolingcavalry.waitforitdemo.ServiceApplication</font>,您可能以爲太長了很差寫,這裏有個小竅門,就是在不使用<font color="blue">entrypoint</font>節點的時候,用jib插件製做的鏡像自己是帶有啓動命令的,容器運行的時候,您能夠經過<font color="blue">docker ps --no-trunc</font>命令看到該容器的完整啓動命令,複製過來直接用就好了;

全部的改造工做都完成了,能夠開始驗證了;

啓動容器,驗證順序控制是否成功

  1. 在docker-compose.yml文件所在目錄執行命令<font color="blue">docker-compose up</font>,會建立兩個容器,而且日誌信息會直接打印在控制檯,咱們來分析這些日誌信息,驗證順序控制是否成功;
  2. 以下圖,可見service容器中並無啓動java進程,而是在等待eureka:8080的響應: 在這裏插入圖片描述
  3. 繼續看日誌,可見eureka服務就緒的時候,service容器的wait-for-it.sh腳本收到了響應,因而當即啓動service應用的進程: 在這裏插入圖片描述
  4. 繼續看日誌,以下圖,service在eureka上註冊成功: 在這裏插入圖片描述 綜上所述,使用docker官方推薦的wait-for-it.sh來控制java應用的啓動順序是可行的,能夠按照業務自身的需求來量身定作合適的啓動順序;

wait-for-it.sh方案的缺陷

使用docker官方推薦的<font color="blue">wait-for-it.sh</font>來控制容器啓動順序,雖然已知足了咱們的需求,但依舊留不是完美方案,留下的缺陷仍是請您先知曉吧,也許這個缺陷會對您的系統產生嚴重的負面影響:

  1. 再開啓一個SSH鏈接,登陸到實戰的linux電腦上,執行命令<font color="blue">docker exec eureka ps -ef</font>,將eureka容器內的進程打印出來,以下所示,<font color="red">java進程的PID等於1</font>:
[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
  1. 再來看看service的進程狀況,執行命令<font color="blue">docker exec service ps -ef</font>,將service容器內的進程打印出來,以下所示,<font color="red">PID等於1的進程不是java,而是啓動時的shell命令</font>:
[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
  1. 一般狀況下,在執行命令<font color="blue">docker stop xxx</font>中止容器時,只有PID=1的進程纔會收到"SIGTERM"信號量,因此在使用<font color="blue">docker stop</font>中止容器時,eureka容器中的java進程收到了"SIGTERM"能夠當即中止,可是service容器中的java進程收不到"SIGTERM",所以只能等到默認的10秒超時時間到達的時候,被"SIGKILL"信號量殺死,<font color="red">不但等待時間長,並且優雅停機的功能也用不上了</font>;
  2. 您能夠分別輸入<font color="blue">docker stop eureka</font>和<font color="blue">docker stop service</font>來感覺一下,前者當即完成,後者要等待10秒。
  3. 個人shell技能過於平庸,目前還找不到好的解決辦法讓service容器中的java進程取得1號進程ID,我的以爲自定義entrypoint.sh腳原本調用wait-for-it.sh而且處理"SIGTERM"說不定可行,若是您有好的辦法請留言告知,在此感激涕零;
  4. 目前看來,控制容器啓動順序最好的解決方案並不是wait-for-it.sh,而是業務本身實現容錯,例如service註冊eureka失敗後會自動重試,可是這對業務的要求就略高了,尤爲是在複雜的分佈式環境中更加難以實現;
  5. docker官方推薦使用wait-for-it.sh腳本的文章地址是:https://docs.docker.com/compose/startup-order/ ,文章末尾顯示了頂和踩的數量,以下圖,頂的數量是145,踩的數量達到了563,一份官方文檔竟然這麼不受待見,也算是開了眼界,不知道和我前面提到的1號PID問題有沒有關係: 在這裏插入圖片描述 至此,java應用的容器順序控制實戰就完成了,但願您在對本身的應用作容器化的時候,此文能給您提供一些參考。

歡迎關注公衆號:程序員欣宸

相關文章
相關標籤/搜索