Java项目容器化构建最佳实践

使用Dockerfile将源代码构建成容器镜像,进行镜像分发、部署。相比于Golang或Python项目,Java项目因企业一般会选择自建依赖仓库(如Maven)导致容器化构建难度高,因不熟悉Dockerfile缓存机制导致构建速度较慢。本文从典型用户场景(云上自建GitLab代码仓库、自建Maven仓库)出发,介绍如何利用Dockerfile构建Java项目,如何提速构建过程以及如何利用ACR-EE构建服务来进行自动化镜像构建。

前提条件

  • 已创建GitLab代码仓库。

  • 已创建Maven仓库。本文选择云效Maven仓库,更多内容,请参见云效Maven仓库

  • 已创建ACR-EE企业版,更多内容,请参见创建企业版实例

项目介绍

本文使用具有依赖的两个Java项目进行演示。分别有以下两个项目:

  • Provider:提供服务,供调用。

    • Core模块:提供公共接口。

    • Service模块:服务实现模块。

  • Consumer:调用Provider服务。

    • Service模块:需要依赖Provide项目里的Core模块。参考代码如下:

      .
      ├── consumer
      │ ├── Dockerfile
      │ ├── consumer.iml
      │ ├── pom.xml
      │ └── service
      │     ├── pom.xml
      │     ├── src
      │     └── target
      └── provider
          ├── Dockerfile
          ├── core
          │ ├── pom.xml
          │ ├── src
          │ └── target
          ├── pom.xml
          ├── provider.iml
          └── service
              ├── pom.xml
              ├── src
              └── target

构建产物:

  • 基于Provider工程构建出生产者应用镜像。

  • 基于Consumer工程构建出消费者应用镜像。

步骤一:确定公共依赖包已上传

项目引用的公共依赖包需要提前上传到自建Maven仓库。以这里的Provider为例,您可以在Provider目录下执行以下上传命令:

mvn clean install org.apache.maven.plugins:maven-deploy-plugin:2.8:deploy -DskipTests

步骤二:制作专属Maven基础镜像

为了在容器化构建环境内能访问到自建Maven仓库,需要把Maven仓库配置放到基础镜像内。这里建议基于官方Maven镜像来打造您的企业专属公共Maven基础镜像,应用项目直接引用该基础镜像即可访问Maven仓库。

  1. 将以下文件保存成Dockerfile并和Maven仓库的《settings.xml》文件放到同一目录。

    FROM maven:3.8-openjdk-8 #指定与项目匹配的Maven镜像,该示例环境为3.8版本的Maven。
    
    ADD settings.xml /root/.m2/ #将自建Maven仓库配置放到对应位置。
  2. 执行以下命令构建并推送到远程镜像仓库。

    ls
    Dockerfile   settings.xml
    
    docker build -t demo-registry-vpc.cn-beijing.cr.aliyuncs.com/demo/maven-base:3.8-openjdk-8 -f Dockerfile .
    Sending build context to Docker daemon   7.68kB
    Step 1/2 : FROM maven:3.8-openjdk-8
     ---> a3f42bfde036
    Step 2/2 : ADD settings.xml /root/.m2/
     ---> db0d5a5192e3
    Successfully built db0d5a5192e3
    Successfully tagged demo-registry-vpc.cn-beijing.cr.aliyuncs.com/demo/maven-base:3.8-openjdk-8
    
    docker push demo-registry-vpc.cn-beijing.cr.aliyuncs.com/demo/maven-base:3.8-openjdk-8

步骤三:构建Consumer应用镜像(Provider项目略过)

执行以下命令。(建议将构建过程中需要的基础镜像全部推送到阿里云镜像仓库。)

FROM demo-registry-vpc.cn-beijing.cr.aliyuncs.com/demo/maven-base:3.8-openjdk-8 AS builder

# add pom.xml and source code
ADD ./pom.xml pom.xml
ADD ./service service/

# package jar
RUN mvn clean package

# Second stage: minimal runtime environment
From demo-registry-vpc.cn-beijing.cr.aliyuncs.com/demo/openjdk:8-jre-alpine

# copy jar from the first stage
COPY --from=builder service/target/service-1.0-SNAPSHOT.jar service-1.0-SNAPSHOT.jar

EXPOSE 8080

CMD ["java", "-jar", "service-1.0-SNAPSHOT.jar"]

步骤四:优化构建速度

步骤三:构建Consumer应用镜像(Provider项目略过)中,您已经能构建出镜像,但是当您修改代码、重复构建时会发现每次都会重复拉取JAR包,而无法利用到JAR包缓存,构建速度比较缓慢。这是因为Dockerfile有自己的缓存生效机制,当修改源代码后,ADD指令内的文件内容hash后出现了值变化导致RUN指令需要重新构建,没办法利用到先前的构建结果缓存。更多内容,请参见关于缓存和Dockerfile最佳实践

我们优化构建速度的思路是将Maven依赖能缓存并重复利用:

  1. 首先将项目的《pom.xml》文件拷贝进容器,下载依赖。只要项目不改动《pom.xml》文件,后续构建都能利用到缓存。

  2. 拷贝项目的源码并编译。

一个改进的Dockerfile如下所示,项目首次构建耗时43s,后续如果仅仅是改动源代码构建时间可以缩短到7s。

FROM demo-registry-vpc.cn-beijing.cr.aliyuncs.com/demo/maven-base:3.8-openjdk-8 AS builder

# To resolve dependencies in a safe way (no re-download when the source code changes)
ADD ./pom.xml pom.xml
ADD ./service/pom.xml service/pom.xml
RUN  mvn install

ADD ./service service/

# package jar
RUN mvn clean package

# Second stage: minimal runtime environment
From demo-registry-vpc.cn-beijing.cr.aliyuncs.com/demo/openjdk:8-jre-alpine

# copy jar from the first stage
COPY --from=builder service/target/service-1.0-SNAPSHOT.jar service-1.0-SNAPSHOT.jar

EXPOSE 8080

CMD ["java", "-jar", "service-1.0-SNAPSHOT.jar"]

步骤五:基于ACR-EE的自动化构建流程

ACR-EE提供了企业级构建服务能力,推荐企业客户使用,更多内容,请参见使用企业版实例构建镜像

以下将为您介绍几个在使用过程中会用到的最佳实践。

  • 使用VPC内网构建模式

    针对云上自建GitLab,建议使用VPC内网安全构建模式来构建镜像, 避免向公网暴露服务。更多内容,请参见使用VPC安全构建模式构建容器镜像

  • 使用镜像版本不可变功能

    推荐仓库打开版本不可变功能,以防止线上版本被覆盖。创建镜像仓库

  • 创建基于Commit ID的构建规则

    每次构建生成两个镜像版本,一个版本使用代码Commit ID来表示,方便镜像版本和代码版本对应起来;另一个版本使用latest表示最新版本。修改构建规则

    提交代码并触发构建。下图是提交两次代码后自动触发的两次构建过程,每次都生成了两个镜像,并且因为缓存命中加速原因,第二次构建更快。1