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应用的容器顺序控制实战就完成了,但愿您在对本身的应用作容器化的时候,此文能给您提供一些参考。

欢迎关注公众号:程序员欣宸

相关文章
相关标签/搜索