为何考虑采用Docker?
Docker是提供用户构建镜像的一种容器化技术,所构建的镜像包含了主要的应用程序和运行应用所需的所有依赖项。该镜像可在任何虚拟机或物理机器上的Docker容器上运行。它的强大之处在于允许用户在开发、测试、预生产和生产中运行同样的镜像,而不必担心在每个环境中依赖项的安装或配置。
采用Docker构建和运行应用
以Java程序员的视角看,Docker的典型应用场景是在容器内运行应用。这固然不错,但如果Docker能提供应用的构建是不是更好?本文中,我将演示如何在容器内用Docker来编排、构建和运行Spring Boot应用。请先按如下步骤创建一个Docker镜像:
从源主机复制应用程序源代码到镜像的临时构建目录
采用Maven完成应用的编译和打包,生成可执行的JAR文件
采用JRE运行JAR文件
镜像大小的提示
关注所构建镜像文件的大小非常重要。较小的镜像文件具有更快的构建速度、下载速度和更低的存储成本优势。所以要尽可能地让镜像只包括所需的几项组件即可。
采用较小的基本镜像
同样的道理,选用只包含必须功能的基础镜像文件也是最佳的选择。本文后续采用Alpine镜像也是基于同样考虑,Alpine是只有5MB的超细Linux发行版。非常适合构建精细的镜像。同时Alpine提供一个包管理器,让用户可以安装任何需要的包。但由于Alpine的初始包非常小,所以安装大量包的过程会有些麻烦。如果有看DockerHub的话,就会发现很多流行的镜像都提供了Alpine版,可以直接使用。后续我们也将用到Alpine版本的Maven和Open JDK JRE镜像。
抛弃不需要的内容
在稍后过程中所定义编译、打包并运行的Spring Boot应用的镜像。就是可部署运行的最终Docker镜像,因此它只需要包含应用本身和运行时依赖项,能够满足在单个容器中构建和运行就可以了。也就是说它可以纯粹就是可执行的JAR包和运行所需的Java JRE文件,而无需包含Maven(包括本地Maven库)或目标目录的全部内容。
那么,用户所要做的就是构建应用,然后从最终镜像中剔除不需要的内容。这个正是多阶段构建的作用所在。它允许用户将Docker构建分解为不同的步骤,并在步骤之间复制特定的目标项,抛弃非必须的内容,从而实现抛弃构建工具本身和其他对应用没有关联的内容。
测试案例执行步骤
项目构建非常简单,举个例子,我用一个类创建一个标准的Spring Boot应用,并在项目的根目录中添加了一个Dockerfile。(用户可在GitHub[1]上获取这个实验的完整源代码,同步实验。)
主类的代码显示如下,且没有添加任何其他内容。接下来我将采用默认的执行器健康状况端点来测试这个应用。
定义Docker镜像
如下内容是Dockerfile中定义的镜像文件,尽管内容不多,但包含了很多步的工作。我将在下面详细解释每一行。
代码备注:
FROM maven:3.5.2-jdk-8-alpine AS MAVEN_BUILD告知Docker采用Maven编译器。maven:3.5.2-jdk-8-alpine构建第一步采用的基础镜像,Docker将首先在本地查找镜像,本地不存在后,将从DockerHub拉取。Maven会在最后阶段被剔除掉(后续COPY命令介绍)考虑下载快速和镜像大小控制的原因,选择Alpine版的Maven镜像。
MAINTAINERBrianHannaway非必选项,但是为映像作者提供一个接触点可提高可维护性。(本实验应用验证的点)
COPY pom.xml/build/在镜像中创建一个build目录, 并拷入pom.xml文件。
COPY src/build/src/拷入src目录到镜像中build目录。
WORKDIR/build/设置build为工作目录。后续任何命令都在此目录中运行。
RUN mvnpackage执行mvn包来运行编译和打包应用,生成成可执行的JAR文件。在第一次构建镜像时,Maven将从公共Maven库拉取所有需要的依赖项,并将它们缓存在镜像的本地。后续的构建将使用这个缓存版的镜像层,这意味着依赖项将在本地引用,而不必再次从外部拉取。至此,已经完成了镜像定义,只需等其构建成一个可执行的JAR文件。这是多阶段构建的第一部分。下一阶段将获取JAR并运行它。
FROM openjdk:8-jre-alpine告知Docker多阶段构建的下一步采用openjdk:8-jre-alpine的基础镜像。再次使用Java 8 JRE的Alpine版本,这一步的选择其实比前面的Maven版本选择更为重要,因为存在于最终版的镜像只是openjdk:8-jre-alpine,因此如果要尽可能控制最终镜像大小的话,选择轻量级JRE镜像就非常重要。
WORKDIR/app告知Docker在镜像内创建另一个/app工作目录,后续任何命令都在此目录中运行。
COPY--from=MAVEN_BUILD/build/target/docker-boot-intro-0.1.0.jar/app/告知Docker从MAVEN_BUILD阶段的/build/target目录复制ocker-boot-intro-0.1.0.jar到/app目录。
如前文所述,多阶段构建的优势就是允许用户将特定的内容从一个构建阶段复制到另一个构建阶段,并丢弃其他所有的内容。如果需要保留从MAVENBUILD阶段开始的所有内容,那最终镜像会包含Maven(包括Maven本地库)工具,以及目标目录中生成的所有类文件。通过从MAVENBUILD阶段选择必须要的内容,那最终得到的镜像会小很多。
ENTRYPOINT["java","-jar","app.jar"]告知Docker在容器运行本镜像时,运行哪些命令。本部分用冒号进行多命令的隔离。本案例中,需要把执行JAR文件复制到/app目录运行。
构建镜像
完成Docker镜像定义后,就可以着手构建。打开包含Dockerfile(根目录)的目录。运行以下命令构建镜像:
-t参数为指定名称和可选标签。如果不指定标签,Docker会自动标记为最latest。
运行构建时,Docker将逐条执行Docker文件中的每个命令。为每个步骤创建一个带有唯一ID的层。例如,步骤1创建的层的ID为293423a981a7。
第一次构建图像时,Docker将从DockerHub获取它需要的任何外部图像,然后在此之上开始构建新的层。这会使得第一次构建速度非常慢。
在构建过程中,Docker在尝试构建层之前会检查缓存,看看是否已经有所构建层的缓存版本。如果该层的缓存版本可用,Docker将直接使用它而不是从头开始构建。这意味着一旦构建了一个镜像层,后续的构建就是重用,速度会快很多。你可以在上面的构建输出中通过Docker缓存输出的hash值看到使用了缓存层。以上面第6步所发生的为例:
作为RUN mvn包命令的一部分,Docker将从公共Maven库获取所有POM依赖项,构建成一个可执行JAR,并将所有这些内容存储在ID为c48659e0197e的层中。下一次构建这个镜像时,Maven依赖项和应用程序JAR将从缓存层中取出,而不必再次下载和构建。
镜像大小
运行docker image ls命令将罗列出所有的本地镜像。可发现docker-boot-intro镜像大小为105 MB。
我在前文中提到过尽可能保持镜像大小的最佳实践,接下来让我们细探一下docker-boot-intro镜像的105MB由什么组成的。运行如下命令:
将看到镜像中各个层的内容情况。
如上所显示5.53 MB的Alpine基础镜像处于第一层。在之上的几层配置了一系列的环境变量,然后是大小为79.4 MB的JRE文件。最后的3层是我们在Dockerfile中定义的层,并包含了20.1 MB的应用JAR。可以发现这个镜像只包括了运行应用所必须的组件,是一个非常不错的轻量级镜像。
运行容器
镜像构建好后,可以使用以下命令运行一个容器:
run命令包括一个可选的-p参数,作用是允许用户将容器应用的端口映射到主机的端口。熟悉Spring Boot的人都知道,应用程序的默认启动端口就是8080。运行一个容器时,Docker将运行可执行JAR文件来启动应用,使用容器的8080端口。但如果要访问容器中的应用,需要通过主机的端口访问,通过端口映射去到容器端口。-p 8080:8080参数就是将容器端口8080映射到主机端口8080。如果没有异常的话,应该可以看到应用程序在端口8080成功启动的信息。
应用测试
如果看到类似于上面显示的信息输出,那表示容器已经顺利启动。接下来就可以测试应用。如果你在Windows或Mac上运行Docker,需要使用的工具是一个Linux虚拟机Docker Toolbox。需要通过运行docker-machine ip命令可以获得Linux VM的IP地址。本案例中的Linux VM IP是192.168.99.100。
获得IP后,可以使用cURL命令cURL 192.168.99.100:8080/actuator/health来调用应用的健康检查点来测试应用情况。如果应用程序启动并运行正常,即可获得HTTP 200的响应,响应内容为{“status”:“up”}。
本方法的局限性
我在前文提到过,可以重用Docker缓存层以减少构建时间。虽然这是事实,但是在构建Java应用时需要考虑存在的例外。每当对Java源代码或POM文件进行更改后,Docker将会发现变更差异,从而忽略缓存的副本层,重新构建所需的层。这是正常的,但问题是这个变化会导致缓存中的Maven依赖项丢失。因此,当使用mvn包命令重新构建这个层时,所有Maven依赖项将再次从远程库中拉取一次,导致显著减慢了构建的速度,成为开发过程中真正的痛点。而且这个问题在构建没有Docker的Java应用程序时完全不存在,仅仅发生在使用Docker构建应用层时发生。
解决方案是什么?
目前解决这个问题的方法是使用主机上的本地Maven存储库作为Maven依赖项的源。通过卷告诉Docker去访问主机本地的Maven库,而非从公共库中拉取依赖项。这种方法可以解决这个问题。但也是有利有弊。
从好的方面看,你使用的是主机缓存的Maven依赖项,可以在更改源代码后,快速重新构建,节省了构建时间。
但不利的方面是Docker镜像的管理因此而失去了一些自主性。使用Docker的主要初衷之一就是不必担心在其运行的环境中的软件配置。理想情况下,Docker镜像应该是自我构建且拥有构建和运行所需的一切元素,而不必存在主机依赖。而这个方法恰好违背了这个初衷,让Docker构建失去了部分自主性。在下一篇文章中,我们将介绍Docker卷,并展示如何使用它们访问主机上的Maven库。
结束语
在本文中,我们定义了一个Docker镜像来构建和运行一个Spring Boot应用程序。我们讨论了让镜像保持尽可能小的重要性,可以通过使用超级小的Alpine基础镜像和在多阶段构建过程中进行内容剔除的方式来实现。我们还讨论了使用Docker构建Java应用程序的局限性和可能的解决方案。用户可以从GitHub[1]获取文章中的测试完整源代码。
相关链接:
https://github.com/briansjavablog/build-and-run-spring-boot-with-docker
原文链接: https://dzone.com/articles/build-package-and-run-spring-boot-apps-with-docker
基于Kubernetes的DevOps实战培训
基于Kubernetes的DevOps实战培训将于2019年12月27日在上海开课,3天时间带你系统掌握Kubernetes,学习效果不好可以继续学习 。本次培训包括:容器特性、镜像、网络;Kubernetes架构、核心组件、基本功能;Kubernetes设计理念、架构设计、基本功能、常用对象、设计原则;Kubernetes的数据库、运行时、网络、插件已经落地经验;微服务架构、组件、监控方案等,点击下方图片或者阅读原文链接查看详情。