写点什么

docker-compose 下的 java 应用启动顺序两部曲之二:实战

作者:程序员欣宸
  • 2022-11-08
    广东
  • 本文字数:7000 字

    阅读完需:约 23 分钟

docker-compose下的java应用启动顺序两部曲之二:实战

欢迎访问我的 GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

上篇回顾

  • 本文是《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

环境信息

  • 本次实战的环境如下:


  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 官方推荐的 wait-for-it.sh 脚本,等待 eureka 服务就绪再启动 java 进程,确保 service 可以一次性注册 eureka 成功;

  • 为了达到上述目标,总共需要做以下几步:


  1. 简单介绍 eureka 和 service 容器的镜像是怎么制作的;

  2. 制作基础镜像,包含 wait-for-it.sh 脚本;

  3. 使用新的基础镜像构建 service 镜像;

  4. 改造 docker-compose.yml;

  5. 启动容器,验证顺序控制是否成功;

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


  • 接下来进入实战环节;

源码下载

  • 如果您不想编码,也可以在 GitHub 上获取文中所有源码和脚本,地址和链接信息如下表所示:


  • 这个 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/
复制代码


制作基础镜像

  • 从上面的 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.shRUN 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.2b. 增加 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 -efUID         PID   PPID  C STIME TTY          TIME CMDroot          1      0  2 07:04 ?        00:00:48 java -Xms1g -Xmx1g -cp /app/resources:/app/classes:/app/libs/* com.bolingcavalry.waitforitdemo.EurekaApplicationroot         56      0  0 07:25 ?        00:00:00 /bin/bashroot         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 -efUID         PID   PPID  C STIME TTY          TIME CMDroot          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.ServiceApplicationroot          7      1  1 07:04 ?        00:00:32 java -Xms1g -Xmx1g -cp /app/resources:/app/classes:/app/libs/* com.bolingcavalry.waitforitdemo.ServiceApplicationroot        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 eurekadocker 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 应用的容器顺序控制实战就完成了,希望您在对自己的应用做容器化的时候,此文能给您提供一些参考。

欢迎关注 InfoQ:程序员欣宸

学习路上,你不孤单,欣宸原创一路相伴...


发布于: 2022-11-08阅读数: 19
用户头像

搜索"程序员欣宸",一起畅游Java宇宙 2018-04-19 加入

前腾讯、前阿里员工,从事Java后台工作,对Docker和Kubernetes充满热爱,所有文章均为作者原创,个人Github:https://github.com/zq2599/blog_demos

评论

发布
暂无评论
docker-compose下的java应用启动顺序两部曲之二:实战_Docker_程序员欣宸_InfoQ写作社区