1 使用FatJar构建Docker镜像的弊端

在项目工程中,我们经常使用Dockerfile把项目的fatjar打入Docker 镜像的方式来构建。举个简单的例子,你可能在你的项目工程根目录里加一个Dockerfile,内容类似于这样:

FROM java:8-alpine
ADD ./target/xxx.jar /app/
CMD java -jar /app/xxx.jar

虽然Docker镜像是通过分层存储的,但是Spring Boot FatJar的大小动不动就过100MB,这样每次都把FatJar添加入镜像,由于基础镜像都会重用,所以你项目的每个版本的镜像的大小几乎就是FatJar的大小。

所以使用FatJar构建镜像弊端:

  • 如果你保留了较多的历史构建版本,则会占用更大的存储
  • 每次镜像 push 和 pull 的时候,也会占用更大的网络带宽
  • 基于以上两点,构建、push、pull的效率更定也相对较低

2 使用jib-maven-plugin避免直接把FarJar丢入镜像

jib-maven-plugin是Google开源的maven插件,作为开发者,你可以在本地不安装Docker的情况下构建并push镜像。按照官方介绍

  • Fast - Deploy your changes fast. Jib separates your application into multiple layers, splitting dependencies from classes. Now you don’t have to wait for Docker to rebuild your entire Java application - just deploy the layers that changed.
  • Reproducible - Rebuilding your container image with the same contents always generates the same image. Never trigger an unnecessary update again.
  • Daemonless - Reduce your CLI dependencies. Build your Docker image from within Maven or Gradle and push to any registry of your choice. No more writing Dockerfiles and calling docker build/push.
2.1 quick-start

在项目的pom.xml中加入以下配置:

            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>com.google.cloud.tools</groupId>
                <artifactId>jib-maven-plugin</artifactId>
                <version>1.6.1</version>
                <configuration>
                    <!--基础镜像,来自dockerhub,如果是私服,需要加上鉴权信息,和to下的auth节点相同-->
                    <from>
                        <image>thinkerwu/xxx:v1</image>
                    </from>
                    <!--构建后的镜像名称以及私服地址、鉴权信息-->
                    <to>
                        <image>registry.xxx.com/xxx/${project.name}:latest</image>
                        <auth>
                            <username>xxx</username>
                            <password>xxx</password>
                        </auth>
                    </to>
                    <!--允许非https-->
                    <allowInsecureRegistries>true</allowInsecureRegistries>
                </configuration>
                <executions>
                    <execution>
                        <id>build-and-push-docker-image</id>
                        <phase>package</phase>
                        <goals>
                            <goal>build</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

先后执行mvn compile命令和mvn jib:build命令,插件就会自己的构建镜像并且push到私服上去。

2.2 对jib-maven-plugin构建的镜像说明

举个例子,我下面的isc-struct-data-linux项目构建了很多个版本,但是在开发阶段,我每次使用:latest版本号push和pull的时候都只是对我最新更改的进行的操作。
mvn jib:build操作

E:\WorkCode\isc-struct-data\isc-struct-data-linux>mvn jib:build
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------< com.xxx.isc:isc-struct-data-linux >---------------
[INFO] Building isc-struct-data-linux 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- jib-maven-plugin:1.6.1:build (default-cli) @ isc-struct-data-linux ---
[INFO]
[INFO] Containerizing application to registry.xxx.com/isc/isc-struct-data-linux...
[INFO] The base image requires auth. Trying again for thinkerwu/hcnetsdk-jre8:v1...
[INFO]
[INFO] Container entrypoint set to [java, -cp, /app/resources:/app/classes:/app/libs/*, com.xxx.isc.struct.data.linux.StructDataLinuxApplication]
[INFO]
[INFO] Built and pushed image as registry.xxx.com/isc/isc-struct-data-linux
[INFO] Executing tasks:
[INFO] [===========================   ] 90.0% complete
[INFO] > launching layer pushers
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 18.303 s
[INFO] Finished at: 2019-11-03T13:28:24+08:00
[INFO] ------------------------------------------------------------------------

命令docker pull registry.xxx.com/isc/isc-struct-data-linux结果如下:

[root@localhost ~]# docker pull registry.xxx.com/isc/isc-struct-data-linux
Using default tag: latest
latest: Pulling from isc/isc-struct-data-linux
e110a4a17941: Already exists 
26b4e58aedd1: Already exists 
82bc5b7de68a: Already exists 
db13ed7f6533: Already exists 
ce54ff63d84d: Already exists 
12686f067d48: Already exists 
cb10ab70f6f7: Already exists 
5ba6e9db8d44: Already exists 
14d466d88edc: Pull complete 
Digest: sha256:32e8c596c06f64bd7ea55528fedffcb8dce4512b718e9ceabb8283b86413a56a
Status: Downloaded newer image for registry.xxx.com/isc/isc-struct-data-linux:latest
registry.xxx.com/isc/isc-struct-data-linux:latest

接下来,把这个镜像运行起来,通过docker exec -it进入容器看一下与直接把FatJar丢进镜像的区别是什么:

[root@localhost ~]# docker exec -it a9576 /bin/bash
bash-4.3# ls
app      bin      dev      etc      extlib   home     lib      lib64    linuxrc  media    mnt      opt      proc     root     run      sbin     srv      sys      tmp      usr      var
bash-4.3# cd app
bash-4.3# ls
classes    libs       resources
bash-4.3# apk add tree
(1/1) Installing tree (1.7.0-r0)
Executing busybox-1.24.2-r14.trigger
OK: 22 MiB in 23 packages
bash-4.3# tree .
.
├── classes
│   └── com
│       └── xxx
│           └── isc
│               └── struct
│                   └── data
│                       └── linux
│                           ├── ServerRunner.class
│                           ├── StructDataLinuxApplication.class
│                           ├── config
│                           │   ├── ExceptionHandleConfig.class
│                           │   ├── InitConfiguration.class
│                           │   ├── OkHttp3Config$1.class
│                           │   ├── OkHttp3Config.class
│                           │   └── RedisConfig.class
│                           ├── constant
│                           │   ├── ConfigConst$IsApiCommand.class
│                           │   ├── ConfigConst$RedisKey.class
│                           │   ├── ConfigConst$Storage.class
│                           │   ├── ConfigConst$Topic.class
│                           │   ├── ConfigConst$XmlTemplate.class
│                           │   ├── ConfigConst.class
├── libs
│   ├── HdrHistogram-2.1.9.jar
│   ├── LatencyUtils-2.0.3.jar
│   ├── animal-sniffer-annotations-1.17.jar
│   ├── annotations-3.0.1.jar
│   ├── common-api-1.0.0.jar
│   ├── commons-codec-1.11.jar
│   ├── commons-collections-3.2.2.jar
│   ├── commons-collections4-4.3.jar
│   ├── commons-compress-1.18.jar
└── resources
    ├── bootstrap.yml
    ├── com
    │   └── xxx
    │       └── isc
    └── logback-logstash.xml

以上对 tree命令的输出做了删减,能够说明问题即可。通过以上可知,此插件构建的镜像,把应用放在了/app下面,/app下有三个路径:

  • libs 存放项目的第三方jar的依赖
  • resources 项目的资源文件,也就是maven 项目src/main/resources下的文件
  • classes项目的java文件编译的class文件

通过以上我们可以看出,镜像里保存的不在是一个FatJar。因此,对于该Java应用过的运行,也不在是
java -jar xxx.jar了。对于此,通过docker inspect命令查看一下镜像,只关注启动命令如下:

            "Entrypoint": [
                "java",
                "-cp",
                "/app/resources:/app/classes:/app/libs/*",
                "com.xxx.isc.struct.data.linux.StructDataLinuxApplication"
             ]
            

通过以上我们可以看出,是直接通过java xxx的方式直接执行的Spring Boot引导类中的main方法,如果java -jar xxx.jar需要传递参数,自然可以通过java -server -Darg1=1 -Darg2=2 xxx方式传递。也就是说,镜像构建的时候,把参数配置到 Entrypoint 即可。怎么做呢?往下接着读。

3 开发灵活配置建议

回到quick-start中,你会发现,在pom.xml中配置镜像构建还配置了docker 私服的鉴权信息。作为开发人员,其实是可以没有必要知道和维护镜像私服鉴权信息,地址以及镜像的版本信息。也就是说,上面的配置中,除了一些命令是必须的,from节点中配置的基础镜像是开发人员需要了解的,其他都是要删除的。所以在开发过程的,我们应该删除上面的to节点 建议配置如下:

            <plugin>
                <groupId>com.google.cloud.tools</groupId>
                <artifactId>jib-maven-plugin</artifactId>
                <version>1.6.1</version>
                <configuration>
                    <from>
                        <image>thinkerwu/hcnetsdk-jre8:v1</image>
                    </from>
                </configuration>
                <executions>
                    <execution>
                        <id>build-and-push-docker-image</id>
                        <phase>package</phase>
                        <goals>
                            <goal>build</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

至于 to 节点中的信息,应该通过mvn命令传递。不但如此,还有上面提到的Entrypoint中Java应用启动的参数,也是要通过mvn命令传递的。例如:这个项目使用了nacos,应用运行的时候需要指定nacos服务器地址和命名空间。参考如下:

mvn compile jib:build \
    -Djib.allowInsecureRegistries=true \
    -Djib.to.image=registry.xxx.com/isc/isc-struct-data-linux:latest \
    -Djib.to.auth.username=xxx\
    -Djib.to.auth.password=xxx\
    -Djib.container.jvmFlags=-server,-Dspring.cloud.nacos.config.namespace=xxx,-Dspring.cloud.nacos.discovery.namespace=xxx,-Dspring.cloud.nacos.config.server-addr=10.0.0.1:8848,-Dspring.cloud.nacos.discovery.server-addr=10.0.0.1:8848

再通过 docker inspect查看一下Entrypoint 信息如下:

            "Entrypoint": [
                "java",
                "-server",
                "-Dspring.cloud.nacos.config.namespace=xxx",
                "-Dspring.cloud.nacos.discovery.namespace=xxx",
                "-Dspring.cloud.nacos.config.server-addr=10.0.0.1:8848",
                "-Dspring.cloud.nacos.discovery.server-addr=10.0.0.1:8848",
                "-cp",
                "/app/resources:/app/classes:/app/libs/*",
                "com.xxx.isc.struct.data.linux.StructDataLinuxApplication"
            ]

更多配置与命令参数信息,请看github官方仓库

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐