最小化 Java 镜像的常用技巧
背景
随着容器技术的普及,越来越多的应用被容器化。人们使用容器的频率越来越高,但常常忽略一个基本但又非常重要的问题 - 容器镜像的体积。本文将介绍精简容器镜像的必要性并以基于 spring boot 的 java 应用为例描述最小化容器镜像的常用技巧。
精简容器镜像的必要性
精简容器镜像是非常必要的,下面分别从安全性和敏捷性两个角度进行阐释。
安全性
基于安全方面的考虑,将不必要的组件从镜像中移除可以减少攻击面、降低安全风险。虽然 docker 支持用户通过 Seccomp 限制容器内可以执行操作或者使用 AppArmor 为容器配置安全策略,但它们的使用门槛较高,要求用户具备安全领域的专业素养。
敏捷性
精简的容器镜像能提高容器的部署速度。假设某一时刻访问流量激增,您需要通过增加容器副本数以应对突发压力。如果某些宿主机不包含目标镜像,需要先拉取镜像,然后启动容器,这时使用体积较小的镜像能加速这一过程、缩短扩容时间。另外,镜像体积越小,其构建速度也越快,同时还能减少存储和传输的成本。
常用技巧
将一个 java 应用容器化所需的步骤可归纳如下:
- 编译 java 源码并生成 jar 包。
- 将应用 jar 包和依赖的第三方 jar 包移动到合适的位置。
本章所用的样例是一个基于 spring boot 的 java 应用 spring-boot-docker,所用的未经优化的 dockerfile 如下:
FROM maven:3.5-jdk-8
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package
ENTRYPOINT ["java","-jar","/usr/src/app/target/spring-boot-docker-1.0.0.jar"]
由于应用使用 maven 构建,dockerfile 中指定maven:3.5-jdk-8
作为基础镜像,该镜像的大小为 635MB。通过这种方式最终构建出的镜像非常大,达到了 719MB,这是因为一方面基础镜像本身就很大,另一方面 maven 在构建过程中会下载许多用于执行构建任务的 jar 包。
多阶段构建
Java 程序的运行只依赖 JRE,并不需要 maven 或者 JDK 中众多用于编译、调试、运行的工具,因此一个明显的优化方法是将用于编译构建 java 源码的镜像和用于运行 java 应用的镜像分开。为了达到这一目的,在 docker 17.05 版本之前需要用户维护 2 个 dockerfile 文件,这无疑增加了构建的复杂性。好在自 17.05 开始,docker 引入了多阶段构建的概念,它允许用户在一个 dockerfile 中使用多个 From 语句。每个 From 语句可以指定不同的基础镜像并将开启一个全新的构建流程。您可以选择性地将前一阶段的构建产物复制到另一个阶段,从而只将必要的内容保留在最终的镜像里。优化后的 dockerfile 如下:
FROM maven:3.5-jdk-8 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package
FROM openjdk:8-jre
ARG DEPENDENCY=/usr/src/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]
该 dockerfile 选用maven:3.5-jdk-8
作为第一阶段的构建镜像,选用openjdk:8-jre
作为运行 java 应用的基础镜像并且只拷贝了第一阶段编译好的.claass
文件和依赖的第三方 jar 包到最终的镜像里。通过这种方式优化后的镜像大小为 459MB。
使用 distroless 作为基础镜像
虽然通过多阶段构建能减小最终生成的镜像的大小,但 459MB 的体积仍相对过大。经调查发现,这是因为使用的基础镜像openjdk:8-jre
体积过大,到达了 443MB,因此下一步的优化方向是减小基础镜像的体积。
Google 开源的项目 distroless 正是为了解决基础镜像体积过大这一问题。Distroless 镜像只包含应用程序及其运行时依赖项,不包含包管理器、shell 以及在标准 Linux 发行版中可以找到的任何其他程序。目前,distroless 为依赖 java、python、nodejs、dotnet 等环境的应用提供了基础镜像。
使用 distroless 的 dockerfile 如下:
FROM maven:3.5-jdk-8 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package
FROM gcr.io/distroless/java
ARG DEPENDENCY=/usr/src/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]
该 dockerfile 和上一版的唯一区别在于将运行阶段依赖的基础镜像由openjdk:8-jre
(443 MB)替换成了gcr.io/distroless/java
(119 MB)。经过这一优化,最终镜像的大小为 135MB。
使用 distroless 的唯一不便是您无法 attach 到一个正在运行的容器上排查问题,因为镜像中不包含 shell。虽然 distroless 的 debug 镜像提供 busybox shell,但需要用户重新打包镜像、部署容器,对于那些已经基于非 debug 镜像部署的容器无济于事。 但从安全角度来看,无法 attach 容器并不完全是坏事,因为攻击者无法通过 shell 进行攻击。
使用 alpine 作为基础镜像
如果您确实有 attach 容器的需求,又希望最小化镜像的大小,可以选用 alpine 作为基础镜像。Alpine 镜像的特点是体积非常下,基础款镜像的体积仅 4 MB 左右。
使用 alpine 后的 dockerfile 如下:
FROM maven:3.5-jdk-8 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package
FROM openjdk:8-jre-alpine
ARG DEPENDENCY=/usr/src/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]
这里并未直接继承基础款 alpine,而是选用从 alpine 构建出的包含 java 运行时的openjdk:8-jre-alpine
(83MB)作为基础镜像。使用该 dockerfile 构建出的镜像体积为 99.2MB,比基于 distroless 的还要小。
执行命令docker exec -ti <container_id> sh
可以成功 attach 到运行的容器中。
distroless vs alpine
既然 distroless 和 alpine 都能提供非常小的基础镜像,那么在生产环境中到底应该选择哪一种呢?如果安全性是您的首要考虑因素,建议选用 distroless,因为它唯一可运行的二进制文件就是您打包的应用;如果您更关注镜像的体积,可以选用 alpine。
其他技巧
除了可以通过上述技巧精简镜像外,还有以下方式:
- 将 dockerfile 中的多条指令合并成一条,通过减少镜像层数的方式达到精简镜像体积的目的。
- 将稳定且体积较大的内容置于镜像下层,将变动频繁且体积较小的内容置于镜像上层。虽然该方式无法直接精简镜像体积,但充分利用了镜像的缓存机制,同样可以达到加快镜像构建和容器部署的目的。
想了解更多优化 dockerfile 的小窍门可参考教程 Best practices for writing Dockerfiles。
总结
- 本文通过一系列的优化,将 java 应用的镜像体积由最初的 719MB 缩小到 100MB 左右。如果您的应用依赖其他环境,也可以用类似的原则进行优化。
- 针对 java 镜像,google 提供的另一款工具 jib 能为您屏蔽镜像构建过程中的复杂细节,自动构建出精简的 java 镜像。使用它您无须编写 dockerfile,甚至不需要安装 docker。
- 对于类似 distroless 这样无法 attach 或者不方便 attach 的容器,建议您将它们的日志中心化存储,以便问题的追踪和排查。具体方法可参考文章面向容器日志的技术实践。
本文作者:吴波bruce_wu
本文为云栖社区原创内容,未经允许不得转载。
最小化 Java 镜像的常用技巧的更多相关文章
- ArchLinux最小化安装 必备库 常用命令
铸成强大的工作站环境——ArchLinux最小化安装 所有问题归结起来,只是一个问题:ArchLinux最小化安装,需要安装哪些包? 1.bash//最基本的Bash Shell(必须)2.bzip2 ...
- 九度OJ 1502 最大值最小化(JAVA)
题目1502:最大值最小化(二分答案) 九度OJ Java import java.util.Scanner; public class Main { public static int max(in ...
- 最小化安装Linux的常用配置整理
基于安全性考虑,将服务器进行最小化安装,毕竟软件包越少,漏洞越少,相对来说就约安全,但是最小化安装会给运维带来一些问题和不便,下面是我总结的,常见的一些配置和工具的安装,仅供各位大神参考,如有新的id ...
- 支持HTTP2的cURL——基于Alpine的最小化Docker镜像
cURL是我喜欢的开源软件之一.虽然cURL的强大常常被认为是理所当然的,但我真心地认为它值得感谢和尊重.如果我们的工具箱失去了curl,那些需要和网络重度交互的人(我们大多数人都是这样的)将会陷入到 ...
- 最小化docker镜像
kubernetes离线安装包,仅需三步 如何让镜像尽可能小 很容器想到from scratch, 就是没任何基础镜像 FROM scratch COPY p / ENTRYPOINT [" ...
- Java写入的常用技巧
一.批量写入 Java写入大量数据到磁盘/数据库等其它第三方介质时,由于IO是比较耗费资源的操作,通常采用攒一批然后批量写入的模式 //通常构造一个缓存池,一个限制指标,可以是内存大小也可以是时间 B ...
- java 字符串(String)常用技巧及自建方法模块汇总
1.String类常用方法汇总 (1)删除字符串的头尾空白符 public String trim() (2)从指定位置截取字符串 public String substring(int beginI ...
- Java写入的常用技巧(二)
在一般从流接收数据写入介质的场景中,大部分存在每批次数据较小,导致小文件较多的问题. 一般考虑设置一个缓冲池,将多个批次的数据先缓冲进去,达到一定大小,再一次性批量写入 //公共缓冲池和缓冲池大小,如 ...
- Effective Java 第三版——17. 最小化可变性
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
随机推荐
- 设置checkbox只读
1.checkbox没有readonly属性,所以在checkbox添加readonly属性是没有作用的. <input type="checkbox" readonly=& ...
- Vue.js基础语法(一)
vue学习的一系列,全部来自于表哥---表严肃,是我遇到过的讲课最通透,英文发音最好听的老师,想一起听课就去这里吧 https://biaoyansu.com/i/hzhj1206 前言: 前端解析数 ...
- 连接数据库报错:1130-Host 'xxx' is not allowed to connect to this MySQL server解决
出现这个问题的同学都很奇怪,为啥用localhost就可以连接上,但是使用本地ip就不行.出现这个问题的原因就是mysql未开启mysql远程访问权限导致. 这时候我们就用cmd去访问下你的mysql ...
- carousel 插件隐藏列表中几项导致左右切换出错
1. 一般的应用场景: 用于左右快速切换显示的列表内容,比如对员工的切换. 对于这种情况必不可少需要按照部门进行搜索,目前我的做法是首次加载所有该用户可以查看的员工列表,选择部门后又选择的隐藏掉其他不 ...
- 从零开始的全栈工程师——js篇2.18(js的运动)
一.元素的 client offset scroll 三个系列 clientWidth / clientHeight / clientTop / clientLeftoffsetWidth / off ...
- JS的函数参数传递为值传递
function setAge(i) { alert(i);//24 i = 18; alert(i);//18,i的改变不会影响外面的age }; var age = 24; setAge(age) ...
- AndroidStudio多AppId多渠道快速打包
一直感觉AndroidStudio没有eclipse快,但是最近由于遇到一个问题不得不将工程迁移到AndroidStudio上,迁移后之前在eclipse上所做的所有批量打包又得重新在AndroidS ...
- c#和c++的运算符优先级
闲来无聊乱写代码.发现基础的东西有的时候也非常的抽象.不信?那来看看下面这条语句: ; ; j = i = -i++; 如果上述代码是vc++代码,那么输出结果是: i=- j=- 请按任意键继续. ...
- 【Microsoft Azure学习之旅】Azure Java SDK - Service Bus的认证问题
[2014年12月12日增加备注:12月10日,Microsoft Azure Java SDK team发布了v0.7.0版本,增加对Service Bus SAS的支持,已解决这个问题:-)] 最 ...
- java环境变量配置备忘录
用鼠标右击“我的电脑”->属性->高级->环境变量系统变量->新建->变量名:JAVA_HOME 变量值:D:\Program Files\Java\jdk1.6.0_1 ...